summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--.gitlab/ci/global.gitlab-ci.yml12
-rw-r--r--.gitlab/ci/qa.gitlab-ci.yml10
-rw-r--r--.gitlab/ci/reports.gitlab-ci.yml4
-rw-r--r--.gitlab/ci/review.gitlab-ci.yml28
-rw-r--r--.mdlrc.style2
-rw-r--r--CHANGELOG.md240
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--VERSION2
-rw-r--r--app/assets/javascripts/api.js15
-rw-r--r--app/assets/javascripts/diffs/components/diff_expansion_cell.vue6
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue2
-rw-r--r--app/assets/javascripts/event_tracking/issue_sidebar.js2
-rw-r--r--app/assets/javascripts/issue_show/index.js4
-rw-r--r--app/assets/javascripts/main.js17
-rw-r--r--app/assets/javascripts/monitoring/components/charts/area.vue3
-rw-r--r--app/assets/javascripts/monitoring/components/charts/time_series.vue334
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue24
-rw-r--r--app/assets/javascripts/monitoring/constants.js9
-rw-r--r--app/assets/javascripts/notes/stores/getters.js39
-rw-r--r--app/assets/javascripts/pages/projects/wikis/wikis.js42
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_url.vue2
-rw-r--r--app/assets/javascripts/registry/components/app.vue101
-rw-r--r--app/assets/javascripts/registry/components/svg_message.vue26
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_avatar.vue48
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue83
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_title.vue13
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees.vue212
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/collapsed_assignee.vue27
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue121
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue96
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue8
-rw-r--r--app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue7
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue3
-rw-r--r--app/assets/javascripts/users_select.js66
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js3
-rw-r--r--app/assets/javascripts/vue_shared/components/recaptcha_modal.vue7
-rw-r--r--app/assets/stylesheets/errors.scss2
-rw-r--r--app/assets/stylesheets/framework/header.scss2
-rw-r--r--app/assets/stylesheets/highlight/common.scss13
-rw-r--r--app/assets/stylesheets/highlight/themes/dark.scss4
-rw-r--r--app/assets/stylesheets/highlight/themes/monokai.scss4
-rw-r--r--app/assets/stylesheets/highlight/themes/none.scss5
-rw-r--r--app/assets/stylesheets/highlight/themes/solarized-dark.scss4
-rw-r--r--app/assets/stylesheets/highlight/themes/solarized-light.scss4
-rw-r--r--app/assets/stylesheets/highlight/white_base.scss18
-rw-r--r--app/assets/stylesheets/pages/container_registry.scss4
-rw-r--r--app/assets/stylesheets/pages/issuable.scss28
-rw-r--r--app/controllers/autocomplete_controller.rb2
-rw-r--r--app/controllers/concerns/invisible_captcha.rb4
-rw-r--r--app/controllers/concerns/issuable_collections.rb56
-rw-r--r--app/controllers/concerns/issuable_collections_action.rb2
-rw-r--r--app/controllers/concerns/sorting_preference.rb85
-rw-r--r--app/controllers/concerns/toggle_award_emoji.rb19
-rw-r--r--app/controllers/dashboard/projects_controller.rb22
-rw-r--r--app/controllers/explore/projects_controller.rb19
-rw-r--r--app/controllers/projects/issues_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests_controller.rb2
-rw-r--r--app/controllers/projects/pipelines_controller.rb33
-rw-r--r--app/controllers/projects/wikis_controller.rb31
-rw-r--r--app/finders/award_emojis_finder.rb55
-rw-r--r--app/finders/awarded_emoji_finder.rb21
-rw-r--r--app/graphql/mutations/award_emojis/add.rb9
-rw-r--r--app/graphql/mutations/award_emojis/remove.rb15
-rw-r--r--app/graphql/mutations/award_emojis/toggle.rb14
-rw-r--r--app/helpers/issuables_helper.rb6
-rw-r--r--app/mailers/notify.rb15
-rw-r--r--app/models/award_emoji.rb6
-rw-r--r--app/models/ci/build.rb2
-rw-r--r--app/models/concerns/awardable.rb26
-rw-r--r--app/models/project.rb2
-rw-r--r--app/models/project_wiki.rb4
-rw-r--r--app/serializers/deployment_entity.rb2
-rw-r--r--app/serializers/deployment_serializer.rb2
-rw-r--r--app/serializers/issuable_sidebar_basic_entity.rb1
-rw-r--r--app/serializers/merge_request_serializer.rb2
-rw-r--r--app/serializers/merge_request_sidebar_basic_entity.rb9
-rw-r--r--app/serializers/merge_request_widget_entity.rb4
-rw-r--r--app/services/award_emojis/add_service.rb42
-rw-r--r--app/services/award_emojis/base_service.rb32
-rw-r--r--app/services/award_emojis/collect_user_emoji_service.rb23
-rw-r--r--app/services/award_emojis/destroy_service.rb21
-rw-r--r--app/services/award_emojis/toggle_service.rb13
-rw-r--r--app/services/issuable_base_service.rb5
-rw-r--r--app/services/merge_requests/base_service.rb8
-rw-r--r--app/services/system_note_service.rb4
-rw-r--r--app/views/admin/applications/_delete_form.html.haml2
-rw-r--r--app/views/doorkeeper/authorized_applications/_delete_form.html.haml2
-rw-r--r--app/views/errors/access_denied.html.haml4
-rw-r--r--app/views/layouts/errors.html.haml6
-rw-r--r--app/views/layouts/header/_default.html.haml7
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml4
-rw-r--r--app/views/projects/pipelines/_info.html.haml2
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml2
-rw-r--r--app/views/projects/wikis/_form.html.haml18
-rw-r--r--app/views/projects/wikis/_main_links.html.haml6
-rw-r--r--app/views/projects/wikis/_new.html.haml18
-rw-r--r--app/views/projects/wikis/_sidebar.html.haml2
-rw-r--r--app/views/projects/wikis/edit.html.haml11
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml8
-rw-r--r--app/views/shared/issuable/_sidebar_assignees.html.haml2
-rw-r--r--app/workers/ci/archive_traces_cron_worker.rb2
-rw-r--r--changelogs/unreleased/10-adjust-copy-for-adding-additional-members.yml5
-rw-r--r--changelogs/unreleased/10972-be-allow-restricting-group-members-by-a-domain-whitelist-ce.yml5
-rw-r--r--changelogs/unreleased/11090-export-design-management-lfs-data.yml5
-rw-r--r--changelogs/unreleased/12502-add-view-stats-to-cycle-analytics.yml5
-rw-r--r--changelogs/unreleased/17276-breakage-in-displaying-svg-in-the-same-repository.yml5
-rw-r--r--changelogs/unreleased/19186-redirect-wiki-git-route-to-wiki.yml5
-rw-r--r--changelogs/unreleased/20137-starrers.yml5
-rw-r--r--changelogs/unreleased/21671-multiple-pipeline-status-api.yml5
-rw-r--r--changelogs/unreleased/24705-multi-selection-for-delete-on-registry-page.yml5
-rw-r--r--changelogs/unreleased/26866-api-endpoint-to-list-the-docker-images-tags-of-a-group.yml6
-rw-r--r--changelogs/unreleased/30974-issue-search-by-number.yml5
-rw-r--r--changelogs/unreleased/31434-make-issue-boards-importable.yml5
-rw-r--r--changelogs/unreleased/32032-html-code-shown-in-merge-request.yml5
-rw-r--r--changelogs/unreleased/32495-improve-slack-notification-on-pipeline-status.yml5
-rw-r--r--changelogs/unreleased/34414-update-personal-access-token-scope-descriptions-to-reflect-registry-permissions.yml6
-rw-r--r--changelogs/unreleased/39217-remove-kubernetes-service-integration.yml5
-rw-r--r--changelogs/unreleased/43080-speed-up-deploy-keys.yml5
-rw-r--r--changelogs/unreleased/44036-fix-someone-edited-the-issue-at-the-same-time-false-warning.yml5
-rw-r--r--changelogs/unreleased/46299-wiki-page-creation.yml5
-rw-r--r--changelogs/unreleased/47814-search-view-labels.yml5
-rw-r--r--changelogs/unreleased/48717-rate-limit-raw-controller-show.yml5
-rw-r--r--changelogs/unreleased/50020-allow-email-notifications-to-be-disabled-for-all-users-of-a-group.yml5
-rw-r--r--changelogs/unreleased/50020-fe-allow-email-notifications-to-be-disabled-for-all-users-of-a-group.yml5
-rw-r--r--changelogs/unreleased/50070-legacy-attachments.yml5
-rw-r--r--changelogs/unreleased/50130-cluster-cluster-details-update-automatically-after-cluster-is-created.yml5
-rw-r--r--changelogs/unreleased/52494-separate-namespace-per-project-environment-slug.yml5
-rw-r--r--changelogs/unreleased/55564-remove-if-in-before-after-action.yml5
-rw-r--r--changelogs/unreleased/56100-make-quick-action-commands-applied-banner-more-useful.yml5
-rw-r--r--changelogs/unreleased/56130-deployed_at.yml5
-rw-r--r--changelogs/unreleased/56130-deployment-date.yml5
-rw-r--r--changelogs/unreleased/57953-fix-unfolded-diff-suggestions.yml5
-rw-r--r--changelogs/unreleased/58035-expand-mr-diff.yml5
-rw-r--r--changelogs/unreleased/58256-incorrect-empty-state-message-displayed-on-explore-projects-tab.yml5
-rw-r--r--changelogs/unreleased/59325-units-are-not-shown-on-the-performance-dashboard-2.yml5
-rw-r--r--changelogs/unreleased/59521-job-sidebar-has-a-blank-block.yml5
-rw-r--r--changelogs/unreleased/59590-keyboard-shortcut-for-jump-to-next-unresolved-discussion.yml5
-rw-r--r--changelogs/unreleased/59712-resolve-the-search-problem-issue.yml5
-rw-r--r--changelogs/unreleased/59786-show-renamed-file-in-mr.yml5
-rw-r--r--changelogs/unreleased/59829-fix-style-lint-wiki.yml5
-rw-r--r--changelogs/unreleased/60449-reduce-gitaly-calls-when-rendering-commits-in-md.yml5
-rw-r--r--changelogs/unreleased/60516-uninstall-tiller.yml5
-rw-r--r--changelogs/unreleased/60664-kubernetes-applications-uninstall-cert-manager.yml5
-rw-r--r--changelogs/unreleased/60668-kubernetes-applications-uninstall-knative.yml5
-rw-r--r--changelogs/unreleased/60948-display-groupid-on-group-admin-page.yml5
-rw-r--r--changelogs/unreleased/60949-display-projectid-on-project-admin-page.yml5
-rw-r--r--changelogs/unreleased/61207-adjusted-hoverable-area-in-sidebar.yml5
-rw-r--r--changelogs/unreleased/61332-web-ide-mr-branch-dropdown-closes-unexpectedly.yml5
-rw-r--r--changelogs/unreleased/61335-fix-file-icon-status.yml5
-rw-r--r--changelogs/unreleased/61445-prevent-persisting-auto-switch-discussion-filter.yml6
-rw-r--r--changelogs/unreleased/61776-fixing-the-U2F-warning-message-text-colour.yml5
-rw-r--r--changelogs/unreleased/61787-broadcast-messages-colour-selector-provide-default-options-with-descriptive-labels.yml5
-rw-r--r--changelogs/unreleased/62137-add-tooltip-to-improve-clarity-of-detached-label-state-in-the-merge-request-pipeline.yml5
-rw-r--r--changelogs/unreleased/62286-Consistent-selection-elements-in-user-settings-preferences.yml5
-rw-r--r--changelogs/unreleased/62609-test-summary-loading-spinner-is-too-large.yml5
-rw-r--r--changelogs/unreleased/62971-embed-specific-metrics-chart-in-issue.yml5
-rw-r--r--changelogs/unreleased/62973-specify-time-frame-in-shareable-link-for-embedding-metrics.yml5
-rw-r--r--changelogs/unreleased/63181-collapsible-line.yml5
-rw-r--r--changelogs/unreleased/63438-oauth2-support-with-gitlab-personal-access-token.yml5
-rw-r--r--changelogs/unreleased/63485-fix-pipeline-emails-to-use-group-setting.yml5
-rw-r--r--changelogs/unreleased/63547-add-system-notes-for-when-a-zoom-call-was-added-removed-from-an-issue.yml5
-rw-r--r--changelogs/unreleased/63568-access-email-notifications-custom-email.yml5
-rw-r--r--changelogs/unreleased/63571-fix-gc-profiler-data-being-wiped.yml5
-rw-r--r--changelogs/unreleased/63671-remove-extra-padding-from-the-disabled-comment-area.yml5
-rw-r--r--changelogs/unreleased/63730-fix-500-status-labels-pd.yml5
-rw-r--r--changelogs/unreleased/63833-fix-jira-issues-url.yml5
-rw-r--r--changelogs/unreleased/63888-snippets-usage-ping-for-create-smau.yml5
-rw-r--r--changelogs/unreleased/63942-remove-config-action_dispatch-use_authenticated_cookie_encryption-configuration.yml5
-rw-r--r--changelogs/unreleased/64081-override-helm-release-name.yml5
-rw-r--r--changelogs/unreleased/64092-removes-update-statistics-namespace-feature-flag.yml5
-rw-r--r--changelogs/unreleased/64160-fix-duplicate-buttons.yml5
-rw-r--r--changelogs/unreleased/64180-membersfinder-contains-slow-database-query-with-or-conditions.yml5
-rw-r--r--changelogs/unreleased/64190-add-mr-form.yml5
-rw-r--r--changelogs/unreleased/64257-active_session_lookup_key_cleanup.yml5
-rw-r--r--changelogs/unreleased/64257-warden_set_user_fix.yml5
-rw-r--r--changelogs/unreleased/64265-center-loading-icon.yml5
-rw-r--r--changelogs/unreleased/64295-predictable-environment-slugs.yml5
-rw-r--r--changelogs/unreleased/64341-user-callout-deferred-link-support.yml5
-rw-r--r--changelogs/unreleased/64383-pattern-matching-with-variables-causes-gitlabs-ci-lint-to-throw-500.yml5
-rw-r--r--changelogs/unreleased/64608-double-tooltips.yml5
-rw-r--r--changelogs/unreleased/64675-Dashboard-URL-legend-border.yml5
-rw-r--r--changelogs/unreleased/64697-markdown-issues-checkbox-inside-blockquote-status-won-t-be-saved.yml5
-rw-r--r--changelogs/unreleased/64700-fix-the-color-of-the-visibility-icon-on-project-lists.yml5
-rw-r--r--changelogs/unreleased/64730-metrics-dashboard-menu-is-cramped-with-new-features-enabled.yml5
-rw-r--r--changelogs/unreleased/64746-Commit-authors-avatar-sretched-in-commit-view-if-no-image-is-loaded.yml5
-rw-r--r--changelogs/unreleased/64763-fix-tags-page-layout.yml5
-rw-r--r--changelogs/unreleased/64831-add-padding-to-merged-by-widget.yml5
-rw-r--r--changelogs/unreleased/64950-move-download-csv-button-functionality-in-metrics-dashboard-cards-i.yml5
-rw-r--r--changelogs/unreleased/64972-fix-unicorn-workers-metric.yml5
-rw-r--r--changelogs/unreleased/64974-remove-livesum-from-ruby-sampler-metrics.yml5
-rw-r--r--changelogs/unreleased/65088-incorrect-message-interpolation-on-project-listing.yml5
-rw-r--r--changelogs/unreleased/65263-manual-action.yml5
-rw-r--r--changelogs/unreleased/65278-fix-puma-master-counter-wipe.yml5
-rw-r--r--changelogs/unreleased/65412-add-support-for-line-charts.yml5
-rw-r--r--changelogs/unreleased/65427-improve-system-notes-for-zoom-links.yml5
-rw-r--r--changelogs/unreleased/65483-add-a-resend-confirmation-link.yml5
-rw-r--r--changelogs/unreleased/65530-add-externalization-and-fix-regression-in-shortcuts-helper-modal.yml6
-rw-r--r--changelogs/unreleased/65660-update-karma-to-4-2-0.yml5
-rw-r--r--changelogs/unreleased/65671-update-mini_magick-to-4-9-5.yml5
-rw-r--r--changelogs/unreleased/65700-document-max-replication-slots-pg-ha.yml5
-rw-r--r--changelogs/unreleased/65705-two-buttons.yml5
-rw-r--r--changelogs/unreleased/65790-highlight.yml5
-rw-r--r--changelogs/unreleased/65803-invalidate-branches-cache-on-refresh.yml5
-rw-r--r--changelogs/unreleased/66008-fix-project-image-in-slack-notifications.yml5
-rw-r--r--changelogs/unreleased/66023-starrers-count-do-not-match-after-searching.yml5
-rw-r--r--changelogs/unreleased/66066-dark-theme-style-for-expansion-on-mr-diffs.yml5
-rw-r--r--changelogs/unreleased/FixLocaleEN.yml5
-rw-r--r--changelogs/unreleased/GL-12412.yml5
-rw-r--r--changelogs/unreleased/GL-12757.yml5
-rw-r--r--changelogs/unreleased/ab-add-index-on-environments.yml5
-rw-r--r--changelogs/unreleased/ab-count-strategies.yml5
-rw-r--r--changelogs/unreleased/add-caching-to-archive-endpoint.yml5
-rw-r--r--changelogs/unreleased/add-git-blame-api.yml5
-rw-r--r--changelogs/unreleased/add-outbound-requests-whitelist-for-local-networks.yml5
-rw-r--r--changelogs/unreleased/add-release-to-github-importer.yml5
-rw-r--r--changelogs/unreleased/add-support-for-start-sha-to-commits-api.yml5
-rw-r--r--changelogs/unreleased/add_links_to_latest_pipelines.yml5
-rw-r--r--changelogs/unreleased/adjust-group-level-analytics-to-accept-multiple-ids.yml5
-rw-r--r--changelogs/unreleased/alipniagov-fix-wiki_can_not_be_created_total-counter.yml5
-rw-r--r--changelogs/unreleased/allow-all-users-to-see-history.yml4
-rw-r--r--changelogs/unreleased/an-sidekiq-chaos.yml5
-rw-r--r--changelogs/unreleased/an-sidekiq-scheduling_latency.yml5
-rw-r--r--changelogs/unreleased/avoid-race-condition-of-archive-trace-cron-worker.yml5
-rw-r--r--changelogs/unreleased/bjk-64064_cache_metrics.yml5
-rw-r--r--changelogs/unreleased/bjk-usage_ping.yml5
-rw-r--r--changelogs/unreleased/bring-scoped-environment-variables-to-core.yml5
-rw-r--r--changelogs/unreleased/bump_helm_kubectl_gitlab.yml5
-rw-r--r--changelogs/unreleased/bvl-mark-remote-mirrors-as-failed-sooner.yml5
-rw-r--r--changelogs/unreleased/bvl-remote-mirror-exception-handling.yml6
-rw-r--r--changelogs/unreleased/bw-add-index-for-relative-position.yml5
-rw-r--r--changelogs/unreleased/ce-22058-improve-ux-multi-assignees-in-mr.yml5
-rw-r--r--changelogs/unreleased/ce-xanf-add-links-to-admin-area.yml5
-rw-r--r--changelogs/unreleased/ci-config-on-policy.yml5
-rw-r--r--changelogs/unreleased/dblessing-fix-admin-user-radio-labels.yml5
-rw-r--r--changelogs/unreleased/dblessing-fix-public-project-ssh-only-ci-failure.yml5
-rw-r--r--changelogs/unreleased/delete-designs-v2.yml4
-rw-r--r--changelogs/unreleased/dm-process-commit-worker-n-1.yml5
-rw-r--r--changelogs/unreleased/double-slash-64592.yml5
-rw-r--r--changelogs/unreleased/ee-2502-refactor-ee-app-assets-javascripts-approvals-components-approvers_select-vue-to-remove-approverusers.yml5
-rw-r--r--changelogs/unreleased/enable-specific-embeds.yml5
-rw-r--r--changelogs/unreleased/extract_auto_deploy_into_base_image.yml5
-rw-r--r--changelogs/unreleased/fe-delete-old-boardservice.yml6
-rw-r--r--changelogs/unreleased/feat-add-support-page-link-in-help-menu.yml5
-rw-r--r--changelogs/unreleased/feat-smime-signed-notification-emails.yml5
-rw-r--r--changelogs/unreleased/feature-gb-serverless-app-deployment-template.yml5
-rw-r--r--changelogs/unreleased/filter-title-description-and-body-from-logs.yml5
-rw-r--r--changelogs/unreleased/fix-alignment-on-security-reports.yml5
-rw-r--r--changelogs/unreleased/fix-bin-web-puma-script-to-consider-rails-env.yml5
-rw-r--r--changelogs/unreleased/fix-commits-api-empty-refname.yml5
-rw-r--r--changelogs/unreleased/fix-job-log-formatting.yml5
-rw-r--r--changelogs/unreleased/fix-name-vs-path-problem-for-cycle-analytics.yml5
-rw-r--r--changelogs/unreleased/fj-avoid-incresaing-usage-ping-when-not-enabled.yml5
-rw-r--r--changelogs/unreleased/fj-count-web-ide-merge-requests.yml5
-rw-r--r--changelogs/unreleased/fj-navbar-searches-usage-ping-counter.yml5
-rw-r--r--changelogs/unreleased/georgekoltsov-18720-persistent-dashboard-sort.yml5
-rw-r--r--changelogs/unreleased/georgekoltsov-48854-fix-empty-flash-message.yml6
-rw-r--r--changelogs/unreleased/georgekoltsov-51260-add-filtering-to-bitbucket-server-import.yml5
-rw-r--r--changelogs/unreleased/georgekoltsov-55474-outbound-setting-system-hooks.yml5
-rw-r--r--changelogs/unreleased/georgekoltsov-63408-user-mapping.yml5
-rw-r--r--changelogs/unreleased/georgekoltsov-64311-set-visibility-private-if-internal-restricted.yml5
-rw-r--r--changelogs/unreleased/georgekoltsov-64377-add-better-log-msg-to-members-mapper.yml6
-rw-r--r--changelogs/unreleased/gitaly-version-v1.57.0.yml5
-rw-r--r--changelogs/unreleased/gitaly-version-v1.59.0.yml5
-rw-r--r--changelogs/unreleased/group-milestones-dashboard-blunceford.yml5
-rw-r--r--changelogs/unreleased/id-mr-widget-etag-caching.yml5
-rw-r--r--changelogs/unreleased/id-source-code-smau.yml5
-rw-r--r--changelogs/unreleased/implement-dag.yml5
-rw-r--r--changelogs/unreleased/improve-quick-action-messages.yml5
-rw-r--r--changelogs/unreleased/issue-61873-no-error-message-for-general-settings.yml5
-rw-r--r--changelogs/unreleased/issue_58494.yml5
-rw-r--r--changelogs/unreleased/je-separate-namespace-fe.yml5
-rw-r--r--changelogs/unreleased/jivanvl-add-chart-empty-state.yml5
-rw-r--r--changelogs/unreleased/jprovazn-fix-positioning.yml5
-rw-r--r--changelogs/unreleased/jprovazn-project-search.yml5
-rw-r--r--changelogs/unreleased/jramsay-fix-mirroring-help-text-typo.yml5
-rw-r--r--changelogs/unreleased/jupyter-fixes-v1.yml5
-rw-r--r--changelogs/unreleased/khair1-master-patch-79459.yml5
-rw-r--r--changelogs/unreleased/label-descr-push-opts.yml5
-rw-r--r--changelogs/unreleased/latest-pipeline.yml5
-rw-r--r--changelogs/unreleased/lm-download-csv-of-charts-from-metrics-dashboard.yml5
-rw-r--r--changelogs/unreleased/load-search-counts-async.yml5
-rw-r--r--changelogs/unreleased/maintainers-can-create-subgroup.yml5
-rw-r--r--changelogs/unreleased/mc-feature-add-at-colon-variable-masking.yml5
-rw-r--r--changelogs/unreleased/mc-feature-manual-job-variables.yml5
-rw-r--r--changelogs/unreleased/new-cycle-analytics-backend-migrations.yml5
-rw-r--r--changelogs/unreleased/optimize-note-indexes.yml5
-rw-r--r--changelogs/unreleased/post-migrate-private-profile.yml5
-rw-r--r--changelogs/unreleased/rails-template-update.yml5
-rw-r--r--changelogs/unreleased/remove-line-profile-from-performance-bar.yml5
-rw-r--r--changelogs/unreleased/remove-margin-from-user-header.yml5
-rw-r--r--changelogs/unreleased/remove-peek-gc.yml5
-rw-r--r--changelogs/unreleased/remove_deployment_metrics_deployment_platform_fallback.yml6
-rw-r--r--changelogs/unreleased/report-missing-job-dependency.yml6
-rw-r--r--changelogs/unreleased/rf-remove-group-overview-security-dashboard-feature-flag.yml5
-rw-r--r--changelogs/unreleased/rm-src-branch.yml5
-rw-r--r--changelogs/unreleased/safe-archiving-for-traces.yml5
-rw-r--r--changelogs/unreleased/security-2873-blocked-user-slash-command-bypass-master.yml5
-rw-r--r--changelogs/unreleased/security-60551-fix-upload-scope.yml5
-rw-r--r--changelogs/unreleased/sh-add-cmaps-for-pdfjs.yml5
-rw-r--r--changelogs/unreleased/sh-add-gitaly-and-rugged-data-sidekiq.yml5
-rw-r--r--changelogs/unreleased/sh-add-index-extern-uid.yml5
-rw-r--r--changelogs/unreleased/sh-add-missing-csp-report-uri.yml5
-rw-r--r--changelogs/unreleased/sh-add-rugged-logs.yml5
-rw-r--r--changelogs/unreleased/sh-add-rugged-to-peek.yml5
-rw-r--r--changelogs/unreleased/sh-break-out-invited-group-members.yml5
-rw-r--r--changelogs/unreleased/sh-disable-redis-peek.yml5
-rw-r--r--changelogs/unreleased/sh-disable-registry-delete.yml5
-rw-r--r--changelogs/unreleased/sh-enable-bootsnap.yml5
-rw-r--r--changelogs/unreleased/sh-fix-discussions-api-perf.yml5
-rw-r--r--changelogs/unreleased/sh-fix-import-export-suggestions.yml5
-rw-r--r--changelogs/unreleased/sh-fix-pipelines-not-being-created.yml5
-rw-r--r--changelogs/unreleased/sh-fix-special-role-error-500.yml5
-rw-r--r--changelogs/unreleased/sh-ignore-git-errors-delete-project.yml5
-rw-r--r--changelogs/unreleased/sh-make-githost-json.yml5
-rw-r--r--changelogs/unreleased/sh-only-flush-tags-once-per-push.yml5
-rw-r--r--changelogs/unreleased/sh-optimize-commit-deltas-post-receive.yml5
-rw-r--r--changelogs/unreleased/sh-post-receive-cache-clear-once.yml5
-rw-r--r--changelogs/unreleased/sh-remove-pdfjs-deprecations.yml5
-rw-r--r--changelogs/unreleased/sh-rename-githost-to-gitjson.yml5
-rw-r--r--changelogs/unreleased/sh-support-csp-nonce.yml5
-rw-r--r--changelogs/unreleased/sh-update-mermaid.yml5
-rw-r--r--changelogs/unreleased/sh-update-rouge-3-7-0.yml5
-rw-r--r--changelogs/unreleased/sh-update-rugged-0-28-3.yml5
-rw-r--r--changelogs/unreleased/sh-use-redis-caching-store.yml5
-rw-r--r--changelogs/unreleased/sh-use-shared-state-cluster-pubsub.yml5
-rw-r--r--changelogs/unreleased/snowplow-ee-to-ce.yml5
-rw-r--r--changelogs/unreleased/speed-up-labels-api.yml5
-rw-r--r--changelogs/unreleased/tr-embed-metric-links.yml5
-rw-r--r--changelogs/unreleased/tr-remove-embed-metrics-flag.yml5
-rw-r--r--changelogs/unreleased/uncomment_commit_signatures_feature_flag.yml5
-rw-r--r--changelogs/unreleased/update-babel-to-7-5-5.yml5
-rw-r--r--changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-7-0.yml5
-rw-r--r--changelogs/unreleased/update-graphicsmagick-to-1-3-33.yml5
-rw-r--r--changelogs/unreleased/update-pipelines-minutes-expiry-banner-to-an-alert-component-type.yml5
-rw-r--r--changelogs/unreleased/visual-review-tools-constant-storage-keys.yml5
-rw-r--r--changelogs/unreleased/wiki-usage-pings.yml5
-rw-r--r--config/gitlab.yml.example9
-rw-r--r--config/initializers/1_settings.rb2
-rw-r--r--config/initializers/action_mailer_hooks.rb5
-rw-r--r--config/initializers/rack_attack_logging.rb4
-rw-r--r--config/initializers/sidekiq.rb4
-rw-r--r--config/initializers/zz_metrics.rb6
-rw-r--r--config/prometheus/common_metrics.yml8
-rw-r--r--config/routes/project.rb3
-rw-r--r--config/routes/wiki.rb1
-rw-r--r--config/smime_signature_settings.rb11
-rw-r--r--danger/only_documentation/Dangerfile2
-rw-r--r--db/fixtures/development/15_award_emoji.rb35
-rw-r--r--db/migrate/20190808152507_add_projects_sorting_field_to_user_preferences.rb13
-rw-r--r--db/migrate/20190814205640_import_common_metrics_line_charts.rb13
-rw-r--r--db/schema.rb1
-rw-r--r--doc/README.md2
-rw-r--r--doc/administration/container_registry.md15
-rw-r--r--doc/administration/geo/disaster_recovery/index.md2
-rw-r--r--doc/administration/geo/replication/index.md4
-rw-r--r--doc/administration/gitaly/index.md82
-rw-r--r--doc/administration/high_availability/load_balancer.md57
-rw-r--r--doc/administration/index.md1
-rw-r--r--doc/administration/instance_review.md1
-rw-r--r--doc/administration/job_artifacts.md2
-rw-r--r--doc/administration/logs.md2
-rw-r--r--doc/administration/merge_request_diffs.md2
-rw-r--r--doc/administration/monitoring/gitlab_instance_administration_project/index.md2
-rw-r--r--doc/administration/monitoring/performance/request_profiling.md2
-rw-r--r--doc/administration/plugins.md2
-rw-r--r--doc/administration/reply_by_email_postfix_setup.md2
-rw-r--r--doc/administration/smime_signing_email.md49
-rw-r--r--doc/administration/troubleshooting/kubernetes_cheat_sheet.md28
-rw-r--r--doc/administration/troubleshooting/sidekiq.md118
-rw-r--r--doc/administration/uploads.md4
-rw-r--r--doc/api/README.md4
-rw-r--r--doc/api/api_resources.md2
-rw-r--r--doc/api/boards.md2
-rw-r--r--doc/api/dependencies.md8
-rw-r--r--doc/api/deploy_keys.md1
-rw-r--r--doc/api/discussions.md2
-rw-r--r--doc/api/group_boards.md2
-rw-r--r--doc/api/issues.md2
-rw-r--r--doc/api/merge_requests.md2
-rw-r--r--doc/api/projects.md1
-rw-r--r--doc/api/protected_branches.md32
-rw-r--r--doc/api/settings.md2
-rw-r--r--doc/ci/README.md2
-rw-r--r--doc/ci/directed_acyclic_graph/index.md2
-rw-r--r--doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md5
-rw-r--r--doc/ci/multi_project_pipelines.md2
-rw-r--r--doc/ci/pipelines.md4
-rw-r--r--doc/ci/ssh_keys/README.md2
-rw-r--r--doc/ci/variables/README.md7
-rw-r--r--doc/ci/yaml/README.md24
-rw-r--r--doc/development/README.md3
-rw-r--r--doc/development/api_styleguide.md4
-rw-r--r--doc/development/automatic_ce_ee_merge.md8
-rw-r--r--doc/development/distributed_tracing.md1
-rw-r--r--doc/development/ee_features.md4
-rw-r--r--doc/development/elasticsearch.md43
-rw-r--r--doc/development/emails.md4
-rw-r--r--doc/development/img/elasticsearch_architecture.svg1
-rw-r--r--doc/development/lfs.md2
-rw-r--r--doc/development/namespaces_storage_statistics.md178
-rw-r--r--doc/development/rake_tasks.md1
-rw-r--r--doc/development/repository_mirroring.md2
-rw-r--r--doc/development/testing_guide/end_to_end/index.md8
-rw-r--r--doc/development/testing_guide/end_to_end/page_objects.md2
-rw-r--r--doc/development/testing_guide/frontend_testing.md168
-rw-r--r--doc/gitlab-basics/start-using-git.md21
-rw-r--r--doc/install/azure/index.md2
-rw-r--r--doc/install/installation.md2
-rw-r--r--doc/integration/jenkins.md2
-rw-r--r--doc/integration/oauth2_generic.md2
-rw-r--r--doc/migrate_ci_to_ce/README.md2
-rw-r--r--doc/public_access/public_access.md2
-rw-r--r--doc/raketasks/backup_restore.md1
-rw-r--r--doc/raketasks/cleanup.md6
-rw-r--r--doc/raketasks/import.md6
-rw-r--r--doc/raketasks/web_hooks.md12
-rw-r--r--doc/security/rate_limits.md1
-rw-r--r--doc/topics/autodevops/index.md43
-rw-r--r--doc/topics/autodevops/quick_start_guide.md2
-rw-r--r--doc/university/README.md2
-rw-r--r--doc/update/patch_versions.md2
-rw-r--r--doc/update/upgrading_from_ce_to_ee.md2
-rw-r--r--doc/update/upgrading_from_source.md2
-rw-r--r--doc/user/application_security/index.md2
-rw-r--r--doc/user/application_security/license_management/index.md48
-rw-r--r--doc/user/gitlab_com/index.md16
-rw-r--r--doc/user/group/saml_sso/index.md19
-rw-r--r--doc/user/markdown.md21
-rw-r--r--doc/user/permissions.md2
-rw-r--r--doc/user/profile/account/two_factor_authentication.md1
-rw-r--r--doc/user/project/import/gitlab_com.md14
-rw-r--r--doc/user/project/import/img/gitlab_new_project_page.pngbin21251 -> 0 bytes
-rw-r--r--doc/user/project/import/img/gitlab_new_project_page_v12_2.pngbin0 -> 214213 bytes
-rw-r--r--doc/user/project/index.md2
-rw-r--r--doc/user/project/integrations/img/download_as_csv.pngbin0 -> 33801 bytes
-rw-r--r--doc/user/project/integrations/img/generate_link_to_chart.pngbin0 -> 35573 bytes
-rw-r--r--doc/user/project/integrations/prometheus.md12
-rw-r--r--doc/user/project/members/img/access_requests_management.pngbin11005 -> 10436 bytes
-rw-r--r--doc/user/project/members/img/request_access_button.pngbin25271 -> 27958 bytes
-rw-r--r--doc/user/project/members/img/withdraw_access_request_button.pngbin26123 -> 28154 bytes
-rw-r--r--doc/user/project/merge_requests/fast_forward_merge.md10
-rw-r--r--doc/user/project/merge_requests/img/ff_merge_mr.pngbin0 -> 21380 bytes
-rw-r--r--doc/user/project/merge_requests/index.md4
-rw-r--r--doc/user/project/repository/img/repository_languages.pngbin26516 -> 0 bytes
-rw-r--r--doc/user/project/repository/img/repository_languages_v12_2.gifbin0 -> 159195 bytes
-rw-r--r--doc/user/project/repository/index.md2
-rw-r--r--doc/user/project/wiki/index.md9
-rw-r--r--doc/workflow/git_annex.md2
-rw-r--r--doc/workflow/notifications.md4
-rw-r--r--lib/api/api.rb2
-rw-r--r--lib/api/award_emoji.rb8
-rw-r--r--lib/api/projects.rb2
-rw-r--r--lib/feature/gitaly.rb6
-rw-r--r--lib/gitlab/action_rate_limiter.rb4
-rw-r--r--lib/gitlab/ci/build/policy/variables.rb2
-rw-r--r--lib/gitlab/ci/build/rules.rb37
-rw-r--r--lib/gitlab/ci/build/rules/rule.rb32
-rw-r--r--lib/gitlab/ci/build/rules/rule/clause.rb31
-rw-r--r--lib/gitlab/ci/build/rules/rule/clause/changes.rb23
-rw-r--r--lib/gitlab/ci/build/rules/rule/clause/if.rb19
-rw-r--r--lib/gitlab/ci/config/entry/job.rb31
-rw-r--r--lib/gitlab/ci/config/entry/rules.rb33
-rw-r--r--lib/gitlab/ci/config/entry/rules/rule.rb42
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/matches.rb3
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb3
-rw-r--r--lib/gitlab/ci/pipeline/seed/build.rb67
-rw-r--r--lib/gitlab/config/entry/validators.rb24
-rw-r--r--lib/gitlab/daemon.rb5
-rw-r--r--lib/gitlab/email/hook/smime_signature_interceptor.rb50
-rw-r--r--lib/gitlab/email/smime/certificate.rb36
-rw-r--r--lib/gitlab/email/smime/signer.rb29
-rw-r--r--lib/gitlab/grape_logging/loggers/client_env_logger.rb16
-rw-r--r--lib/gitlab/sidekiq_logging/structured_logger.rb46
-rw-r--r--lib/gitlab/sidekiq_middleware/monitor.rb20
-rw-r--r--lib/gitlab/sidekiq_monitor.rb182
-rw-r--r--locale/gitlab.pot53
-rw-r--r--package.json10
-rw-r--r--qa/README.md2
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb2
-rw-r--r--spec/config/smime_signature_settings_spec.rb56
-rw-r--r--spec/controllers/concerns/issuable_collections_spec.rb72
-rw-r--r--spec/controllers/concerns/sorting_preference_spec.rb93
-rw-r--r--spec/controllers/dashboard/projects_controller_spec.rb8
-rw-r--r--spec/controllers/explore/projects_controller_spec.rb95
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb33
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb19
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb65
-rw-r--r--spec/controllers/projects/raw_controller_spec.rb4
-rw-r--r--spec/controllers/projects/wikis_controller_spec.rb77
-rw-r--r--spec/controllers/registrations_controller_spec.rb4
-rw-r--r--spec/controllers/snippets/notes_controller_spec.rb6
-rw-r--r--spec/features/projects/wiki/markdown_preview_spec.rb93
-rw-r--r--spec/features/projects/wiki/user_creates_wiki_page_spec.rb68
-rw-r--r--spec/features/projects/wiki/user_updates_wiki_page_spec.rb16
-rw-r--r--spec/features/projects/wiki/user_views_wiki_page_spec.rb5
-rw-r--r--spec/finders/award_emojis_finder_spec.rb49
-rw-r--r--spec/fixtures/api/schemas/deployment.json4
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_sidebar.json1
-rw-r--r--spec/frontend/api_spec.js22
-rw-r--r--spec/frontend/autosave_spec.js11
-rw-r--r--spec/frontend/mocks/mocks_helper_spec.js4
-rw-r--r--spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js85
-rw-r--r--spec/frontend/sidebar/components/assignees/assignee_avatar_spec.js78
-rw-r--r--spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js189
-rw-r--r--spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js49
-rw-r--r--spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js103
-rw-r--r--spec/frontend/sidebar/user_data_mock.js9
-rw-r--r--spec/frontend/test_setup.js6
-rw-r--r--spec/frontend/wikis_spec.js74
-rw-r--r--spec/initializers/action_mailer_hooks_spec.rb46
-rw-r--r--spec/javascripts/diffs/components/diff_file_header_spec.js2
-rw-r--r--spec/javascripts/issue_show/components/description_spec.js2
-rw-r--r--spec/javascripts/monitoring/charts/time_series_spec.js335
-rw-r--r--spec/javascripts/monitoring/components/dashboard_spec.js (renamed from spec/javascripts/monitoring/dashboard_spec.js)2
-rw-r--r--spec/javascripts/monitoring/mock_data.js4
-rw-r--r--spec/javascripts/notes/stores/getters_spec.js130
-rw-r--r--spec/javascripts/registry/components/app_spec.js11
-rw-r--r--spec/javascripts/sidebar/assignee_title_spec.js14
-rw-r--r--spec/javascripts/sidebar/assignees_spec.js206
-rw-r--r--spec/javascripts/sidebar/confidential_issue_sidebar_spec.js8
-rw-r--r--spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js8
-rw-r--r--spec/javascripts/sidebar/subscriptions_spec.js9
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js18
-rw-r--r--spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js42
-rw-r--r--spec/lib/gitlab/action_rate_limiter_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/build/policy/variables_spec.rb14
-rw-r--r--spec/lib/gitlab/ci/build/rules/rule_spec.rb50
-rw-r--r--spec/lib/gitlab/ci/build/rules_spec.rb168
-rw-r--r--spec/lib/gitlab/ci/config/entry/job_spec.rb111
-rw-r--r--spec/lib/gitlab/ci/config/entry/policy_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb208
-rw-r--r--spec/lib/gitlab/ci/config/entry/rules_spec.rb135
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb28
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb28
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/build_spec.rb287
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb114
-rw-r--r--spec/lib/gitlab/daemon_spec.rb30
-rw-r--r--spec/lib/gitlab/email/hook/disable_email_interceptor_spec.rb3
-rw-r--r--spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb52
-rw-r--r--spec/lib/gitlab/email/smime/certificate_spec.rb77
-rw-r--r--spec/lib/gitlab/email/smime/signer_spec.rb26
-rw-r--r--spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb37
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/monitor_spec.rb49
-rw-r--r--spec/lib/gitlab/sidekiq_monitor_spec.rb261
-rw-r--r--spec/mailers/notify_spec.rb67
-rw-r--r--spec/models/award_emoji_spec.rb23
-rw-r--r--spec/models/ci/build_spec.rb50
-rw-r--r--spec/models/concerns/awardable_spec.rb10
-rw-r--r--spec/requests/api/award_emoji_spec.rb16
-rw-r--r--spec/requests/api/graphql/mutations/award_emojis/add_spec.rb17
-rw-r--r--spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb17
-rw-r--r--spec/requests/api/projects_spec.rb11
-rw-r--r--spec/requests/rack_attack_global_spec.rb12
-rw-r--r--spec/serializers/deployment_entity_spec.rb4
-rw-r--r--spec/serializers/merge_request_sidebar_basic_entity_spec.rb22
-rw-r--r--spec/services/award_emojis/add_service_spec.rb103
-rw-r--r--spec/services/award_emojis/collect_user_emoji_service_spec.rb (renamed from spec/finders/awarded_emoji_finder_spec.rb)2
-rw-r--r--spec/services/award_emojis/destroy_service_spec.rb89
-rw-r--r--spec/services/award_emojis/toggle_service_spec.rb72
-rw-r--r--spec/services/issues/update_service_spec.rb4
-rw-r--r--spec/services/projects/create_service_spec.rb1
-rw-r--r--spec/services/system_note_service_spec.rb4
-rw-r--r--spec/support/helpers/smime_helper.rb55
-rw-r--r--spec/support/helpers/stub_gitlab_calls.rb4
-rw-r--r--spec/support/shared_examples/award_emoji_todo_shared_examples.rb59
-rw-r--r--spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb6
-rw-r--r--spec/workers/ci/archive_traces_cron_worker_spec.rb16
-rw-r--r--yarn.lock44
572 files changed, 7623 insertions, 2759 deletions
diff --git a/.gitignore b/.gitignore
index cb718a6939f..104c6930050 100644
--- a/.gitignore
+++ b/.gitignore
@@ -75,6 +75,8 @@ eslint-report.html
/.rspec
/plugins/*
/.gitlab_pages_secret
+/.gitlab_smime_key
+/.gitlab_smime_cert
package-lock.json
/junit_*.xml
/coverage-frontend/
diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml
index 78ef346d417..56fe9739d5f 100644
--- a/.gitlab/ci/global.gitlab-ci.yml
+++ b/.gitlab/ci/global.gitlab-ci.yml
@@ -76,3 +76,15 @@
- apk add --update openssl
- wget $CI_PROJECT_URL/raw/$CI_COMMIT_SHA/scripts/$SCRIPT_NAME
- chmod 755 $(basename $SCRIPT_NAME)
+
+.review-only: &review-only
+ only:
+ refs:
+ - branches@gitlab-org/gitlab-ce
+ - branches@gitlab-org/gitlab-ee
+ kubernetes: active
+ except:
+ refs:
+ - master
+ - /^\d+-\d+-auto-deploy-\d+$/
+ - /(^docs[\/-].+|.+-docs$)/
diff --git a/.gitlab/ci/qa.gitlab-ci.yml b/.gitlab/ci/qa.gitlab-ci.yml
index dcc681294d2..144e5392e55 100644
--- a/.gitlab/ci/qa.gitlab-ci.yml
+++ b/.gitlab/ci/qa.gitlab-ci.yml
@@ -15,13 +15,13 @@
- branches@gitlab-org/gitlab-ce
- branches@gitlab-org/gitlab-ee
-package-and-qa:
- extends: .package-and-qa-base
+package-and-qa-manual:
+ extends:
+ - .package-and-qa-base
+ - .no-docs-and-no-qa
when: manual
- except:
- - /(^qa[\/-].*|.*-qa$)/
-package-and-qa-always:
+package-and-qa:
extends: .package-and-qa-base
allow_failure: true
only:
diff --git a/.gitlab/ci/reports.gitlab-ci.yml b/.gitlab/ci/reports.gitlab-ci.yml
index fc0e93ee9f1..ccd7d6ec84d 100644
--- a/.gitlab/ci/reports.gitlab-ci.yml
+++ b/.gitlab/ci/reports.gitlab-ci.yml
@@ -27,7 +27,9 @@ dependency_scanning:
cache: {}
dast:
- extends: .dedicated-no-docs
+ extends:
+ - .dedicated-runner
+ - .review-only
stage: qa
dependencies:
- review-deploy
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml
index 3cbfa32d9a5..beb049c0b3b 100644
--- a/.gitlab/ci/review.gitlab-ci.yml
+++ b/.gitlab/ci/review.gitlab-ci.yml
@@ -1,15 +1,3 @@
-.review-only: &review-only
- only:
- refs:
- - branches@gitlab-org/gitlab-ce
- - branches@gitlab-org/gitlab-ee
- kubernetes: active
- except:
- refs:
- - master
- - /^\d+-\d+-auto-deploy-\d+$/
- - /(^docs[\/-].+|.+-docs$)/
-
.review-schedules-only: &review-schedules-only
only:
refs:
@@ -24,8 +12,9 @@
- /(^docs[\/-].+|.+-docs$)/
.review-base: &review-base
- extends: .dedicated-runner
- <<: *review-only
+ extends:
+ - .dedicated-runner
+ - .review-only
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
cache: {}
dependencies: []
@@ -68,7 +57,7 @@ build-qa-image:
- BUILD_TRIGGER_TOKEN=$REVIEW_APPS_BUILD_TRIGGER_TOKEN ./scripts/trigger-build cng
review-build-cng:
- <<: *review-only
+ extends: .review-only
<<: *review-build-cng-base
schedule:review-build-cng:
@@ -117,8 +106,9 @@ schedule:review-deploy:
<<: *review-schedules-only
review-stop:
- <<: *review-only
- extends: .single-script-job-dedicated-runner
+ extends:
+ - .single-script-job-dedicated-runner
+ - .review-only
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
stage: review
when: manual
@@ -181,7 +171,9 @@ review-qa-all:
- gitlab-qa Test::Instance::Any "${QA_IMAGE}" "${CI_ENVIRONMENT_URL}" -- --format RspecJunitFormatter --out tmp/rspec-${CI_JOB_ID}.xml --format html --out tmp/rspec.htm --color --format documentation
parallel-spec-reports:
- extends: .dedicated-runner
+ extends:
+ - .dedicated-runner
+ - .no-docs
dependencies:
- review-qa-all
image: ruby:2.6-alpine
diff --git a/.mdlrc.style b/.mdlrc.style
index b315c99e3fc..85bc3aaa99b 100644
--- a/.mdlrc.style
+++ b/.mdlrc.style
@@ -18,7 +18,7 @@ rule "MD025"
rule "MD028"
rule "MD029", :style => :one
rule "MD030"
-rule "MD032"
+# rule "MD032"
rule "MD034"
rule "MD037"
rule "MD038"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 267a1caafec..a6c8eccf0b2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,246 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 12.2.0
+
+### Security (4 changes, 1 of them is from the community)
+
+- Update mini_magick to 4.9.5. !31505 (Takuya Noguchi)
+- Upgrade Rugged to 0.28.3. !31794
+- Queries for Upload should be scoped by model.
+- Restrict slash commands to users who can log in.
+
+### Removed (3 changes)
+
+- Remove Kubernetes service integration page. !31365
+- Remove line profiler from performance bar.
+- Remove GC metrics from performance bar.
+
+### Fixed (74 changes, 4 of them are from the community)
+
+- Resolve Incorrect empty state message on Explore projects. !25578
+- Search issuables by iids. !28302 (Riccardo Padovani)
+- Make it easier to find invited group members. !28436
+- fix: updates to include units for the y axis label. !30330
+- Align access permissions for wiki history to those of wiki pages. !30470
+- Add index for issues on relative position, project, and state for manual sorting. !30542
+- Fix suggestion on lines that are not part of an MR. !30606
+- Add empty chart component. !30682
+- Remove blank block from job sidebar. !30754
+- Remove duplicate buttons in diff discussion. !30757
+- Order projects in 'Move issue' dropdown by name. !30778
+- Fix bug in dashboard display of closed milestones. !30820
+- Fixes alignment issues with reports. !30839
+- Ensure visibility icons in group/project listings are grey. !30858
+- Fix admin labels page when there are invalid records. !30885
+- Extra logging for new live trace architecture. !30892
+- Fix pipeline emails not respecting group notification email setting. !30907
+- Handle trailing slashes when generating Jira issue URLs. !30911
+- Optimize relative re-positioning when moving issues. !30938
+- Better support clickable tasklists inside blockquotes. !30952
+- Add space to "merged by" widget. !30972
+- Remove duplicated mapping key in config/locales/en.yml. !30980 (Peter Dave Hello)
+- Update Mermaid to v8.2.3. !30985
+- Use persistent Redis cluster for Workhorse pub/sub notifications. !30990
+- Remove :livesum from RubySampler metrics. !31047
+- Fix pid discovery for Unicorn processes in `PidProvider`. !31056
+- Respect group notification email when sending group access notifications. !31089
+- Default dependency job stage index to Infinity, and correctly report it as undefined in prior stages. !31116
+- Fix incorrect use of message interpolation. !31121
+- Moved labels out of fields on Search page. !31137
+- Ensure Warden triggers after_authentication callback. !31138
+- Fix admin area user access level radio button labels. !31154
+- Ignore Gitaly errors if cache flushing fails on project destruction. !31164
+- Prevent double slash in review apps path. !31212
+- Make pdf.js render CJK characters. !31220
+- Prevent discussion filter from persisting to `Show all activity` when opening links to notes. !31229
+- Improve layout of dropdowns in the metrics dashboard page. !31239
+- Remove pdf.js deprecation warnings. !31253
+- Fix GC::Profiler metrics fetching. !31331
+- Jupyter fixes. !31332 (Amit Rathi)
+- Fix first-time contributor notes not rendering. !31340
+- Fix inline rendering of relative paths to SVGs from the current repository. !31352
+- Make `bin/web_puma` consider RAILS_ENV. !31378
+- Removed extrenal dashboard legend border. !31407
+- Fix visual review app storage keys. !31427
+- Fix flashing conflict warning when editing issues. !31469
+- Fix broken issue links and possible 500 error on cycle analytics page when project name and path are different. !31471
+- Prevent turning plain links into embedded when moving issues. !31489
+- Add a field for released_at to GH importer. !31496
+- Adjust size and align MR-widget loading icon. !31503
+- Fix an issue where clicking outside the MR/branch search box in WebIDE closed the dropdown. !31523
+- Don't attempt to contact registry if it is disabled. !31553
+- Fix IDE new files icon in tree. !31560
+- Fix missing author line (`Created by: <user>`) in MRs/issues/comments of imported Bitbucket Cloud project. !31579
+- Add missing report-uri to CSP config. !31593
+- Fixed display of some sections and externalized all text in the shortcuts modal overlay. !31594
+- Remove extra padding from disabled comment box. !31603
+- Allow CI to clone public projects when HTTP protocol is disabled. !31632
+- error message for general settings. !31636 (Mesut Güneş)
+- Invalidate branches cache on PostReceive. !31653
+- Fix active metric files being wiped after the app starts. !31668
+- Fix :wiki_can_not_be_created_total counter. !31673
+- Fix job logs where style changes were broken down into separate lines. !31674
+- Properly save suggestions in project exports. !31690
+- Fix project avatar image in Slack pipeline notifications. !31788
+- Fix empty error flash message on profile:account page when updating username with username that has already been taken. !31809
+- Fix starrers counts after searching. !31823
+- Fix pipelines not always being created after a push. !31927
+- Fix 500 errors in commits api caused by empty ref_name parameter.
+- Center loading icon in CI action component.
+- Prevents showing 2 tooltips in pipelines table.
+- Fix tag page layout.
+- Prevent duplicated trigger action button.
+- Hides loading spinner in pipelines actions after request has been fullfiled.
+
+### Changed (31 changes, 5 of them are from the community)
+
+- Update cluster page automatically when cluster is created. !27189
+- Add branch/tags/commits dropdown filter on the search page for searching codes. !28282 (minghuan lei)
+- Add support for start_sha to commits API. !29598
+- Maintainers can create subgroups. !29718 (Fabio Papa)
+- Extract Auto DevOps deploy functions into a base image. !30404
+- Add MR form to Visual Review (EE) runtime configuration. !30481
+- Adjust redis cache metrics. !30572
+- Add DS_PIP_DEPENDENCY_PATH option to configure Dependency Scanning for projects using pip. !30762
+- Bring scoped environment variables to core. !30779
+- Add Web IDE Usage Ping for Create SMAU. !30800
+- Update the container scanning CI template to use v12 of the clair scanner. !30809
+- Multiple pipeline support for Commit status. !30828 (Gaetan Semet)
+- Add support for exporting repository type data for LFS objects. !30830
+- Avoid increasing redis counters when usage_ping is disabled. !30949
+- Added navbar searches usage ping counter. !30953
+- Convert githost.log to JSON format. !30967
+- Adjusted the clickable area of collapsed sidebar elements. !30974 (Michel Engelen)
+- Mark push mirrors as failed after 1 hour. !30999
+- Allows masking @ and : characters. !31065
+- Remove incorrect fallback when determining which cluster to use when retrieving MR performance metrics. !31126
+- Retry push mirrors faster when running concurrently, improve error handling when push mirrors fail. !31247
+- Make issue boards importable. !31434 (Jason Colyer)
+- Allow users to resend a confirmation link when the grace period has expired. !31476
+- Remove counts from default labels API responses. !31543
+- Upgrade to Gitaly v1.57.0. !31568
+- Rename githost.log -> git_json.log. !31634
+- Load search result counts asynchronously. !31663
+- feat: adds a download to csv functionality to the dropdown in prometheus metrics. !31679
+- Adjust copy for adding additional members. !31726
+- Upgrade to Gitaly v1.59.0. !31743
+- Filter title, description, and body parameters from logs.
+
+### Performance (17 changes, 1 of them is from the community)
+
+- Add partial index on identities table to speed up LDAP lookups. !26710
+- Improve MembersFinder query performance using UNION. !30451 (Jacopo Beschi @jacopo-beschi)
+- Rake task to cleanup expired ActiveSession lookup keys. !30668
+- Update usage ping cron behavior. !30842
+- Make Bootsnap available via ENABLE_BOOTSNAP=1. !30963
+- Batch processing of commit refs in markdown processing. !31037
+- Use tablesample approximate counting by default. !31048
+- Create index on environments by state. !31231
+- Split MR widget into etag-cached and non-cached serializers. !31354
+- Speed up loading and filtering deploy keys and their projects. !31384
+- Only track Redis calls if Peek is enabled. !31438
+- Only expire tag cache once per push. !31641
+- Reduce Gitaly calls in PostReceive. !31741
+- Eliminate many Gitaly calls in discussions API. !31834
+- Optimize DB indexes for ES indexing of notes. !31846
+- Expire project caches once per push instead of once per ref. !31876
+- Look up upstream commits once before queuing ProcessCommitWorkers.
+
+### Added (51 changes, 11 of them are from the community)
+
+- Make starred projects and starrers of a project publicly visible. !24690
+- Make quick action commands applied banner more useful. !26672 (Jacopo Beschi @jacopo-beschi)
+- Allow Helm to be uninstalled from the UI. !27359
+- Improve pipeline status Slack notifications. !27683
+- Add links to relevant configuration areas in admin area overview. !29306
+- Display project id on project admin page. !29734 (Zsolt Kovari)
+- Display group id on group admin page. !29735 (Zsolt Kovari)
+- Resolve Keyboard shortcut for jump to NEXT unresolved discussion. !30144
+- Personal access tokens are accepted using OAuth2 header format. !30277
+- Add Outbound requests whitelist for local networks. !30350 (Istvan Szalai)
+- Allow multiple Auto DevOps projects to deploy to a single namespace within a k8s cluster. !30360 (James Keogh)
+- Allow Knative to be uninstalled from the UI. !30458
+- Add admin-configurable "Support page URL" link to top Help dropdown menu. !30459 (Diego Louzán)
+- Allow specifying variables when running manual jobs. !30485
+- Use predictable environment slugs. !30551
+- Return an ETag header for the archive endpoint. !30581
+- Add Rate Request Limiter to RawController#show endpoint. !30635
+- Add git blame to GitLab API. !30675 (Oleg Zubchenko)
+- Use separate Kubernetes namespaces per environment. !30711
+- Support remove source branch on merge w/ push options. !30728
+- Deploy serverless apps with gitlabktl. !30740
+- Adjust group level analytics to accept multiple ids. !30744
+- Adds event enum column to DesignsVersions join table. !30745
+- Allow email notifications to be disabled for all members of a group or project. !30755 (Dustin Spicuzza)
+- Export and download CSV from metrics charts. !30760
+- Add API endpoints to return container repositories and tags from the group level. !30817
+- Add support for deferred links in persistent user callouts. !30818
+- Add system notes for when a Zoom call was added/removed from an issue. !30857 (Jacopo Beschi @jacopo-beschi)
+- Count wiki creation, update and delete events. !30864
+- Add new expansion options for merge request diffs. !30927
+- Count snippet creation, update and comment events. !30930
+- Update namespace label for GitLab-managed clusters. !30935
+- UI for disabling group/project email notifications. !30961 (Dustin Spicuzza)
+- Support setting of merge request title and description using git push options. !31068
+- Add new table to store email domain per group. !31071
+- Redirect from a project wiki git route to the project wiki home. !31085
+- Link and embed metrics in GitLab Flavored Markdown. !31106
+- Moves snowplow tracking from ee to ce. !31160 (jejacks0n)
+- Allow Cert-Manager to be uninstalled. !31166
+- Add new outbound network requests application setting for system hooks. !31177
+- Allow links to metrics dashboard at a specific time. !31283
+- Enable embedding of specific metrics charts in GFM. !31304
+- Support creating DAGs in CI config through the `needs` key. !31328
+- Generate shareable link for specific metric charts. !31339
+- Add support for Content-Security-Policy. !31402
+- Add BitBucketServer project import filtering. !31420
+- Embed specific metrics chart in issue. !31644
+- Track page views for cycle analytics show page. !31717
+- Add usage pings for source code pushes. !31734
+- Makes collapsible title clickable in job log.
+- Adds highlight to the collapsible section.
+
+### Other (36 changes, 9 of them are from the community)
+
+- Rewrite `if:` argument in before_action and alike when `only:` is also used. !24412 (George Thomas @thegeorgeous)
+- Create rake tasks for migrating legacy uploads out of deprecated paths. !29409
+- Remove the warning style from the U2F device message in user settings > account. !30119 (matejlatin)
+- Set visibility level 'Private' for restricted 'Internal' imported projects when 'Internal' visibility setting is restricted in admin settings. !30522
+- Change BoardService in favor of boardsStore on board blank state of the component board. !30546 (eduarmreyes)
+- Adds Sidekiq scheduling latency structured logging field. !30784
+- Adds chaos endpoints to Sidekiq. !30814
+- Added multi-select deletion of container registry images. !30837
+- When GitLab import fails during importer user mapping step, add an explicit error message mentioning importer. !30838
+- Add Rugged calls and duration to API and Rails logs. !30871
+- Fixed distorted avatars when resource not reachable. !30904 (Marc Schwede)
+- Update GitLab Runner Helm Chart to 0.7.0. !30950
+- Use Rails 5.2 Redis caching store. !30966
+- Add Rugged calls to performance bar. !30983
+- add color selector to broadcast messages form. !30988
+- Harmonize selections in user settings. !31110 (Marc Schwede)
+- Update rouge to v3.7.0. !31254
+- Update 'Ruby on Rails' project template. !31310
+- Fix mirroring help text. !31348 (jramsay)
+- Enhance style of the shared runners limit. !31386
+- Enables storage statistics for root namespaces on database. !31392
+- Improve quick action error messages. !31451
+- Enable authenticated cookie encryption. !31463
+- Update karma to 4.2.0. !31495 (Takuya Noguchi)
+- Add max_replication_slots to PG HA documentation. !31534
+- Create database tables for the new cycle analytics backend. !31621
+- Updated the detached pipeline badge tooltip text to offer a better explanation. !31626
+- Add Gitaly and Rugged call timing in Sidekiq logs. !31651
+- Fix the style-lint errors and warnings for `app/assets/stylesheets/pages/wiki.scss`. !31656
+- Update GraphicsMagick from 1.3.29 to 1.3.33 for CI tests. !31692 (Takuya Noguchi)
+- Migrate remaining users with null private_profile. !31708
+- Bump Helm to 2.14.3 and kubectl to 1.11.10 for Kubernetes integration. !31716
+- Updated the personal access token api scope description to reflect the permissions it grants. !31759
+- Add finished_at to the internal API Deployment entity. !31808
+- Remove Security Dashboard feature flag. !31820
+- Update Packer.gitlab-ci.yml to use latest image. (Kelly Hair)
+
+
## 12.1.5
### Security (2 changes)
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index bb120e876c6..4d5fde5bd16 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-1.59.0
+1.60.0
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 8148c559489..ccfb75e5120 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-9.4.0
+9.4.1
diff --git a/VERSION b/VERSION
index 4dd919b3b06..80212c6e1f0 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-12.2.0-pre
+12.3.0-pre
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index a649c521405..136ffdf8b9d 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -14,6 +14,7 @@ const Api = {
projectPath: '/api/:version/projects/:id',
forkedProjectsPath: '/api/:version/projects/:id/forks',
projectLabelsPath: '/:namespace_path/:project_path/-/labels',
+ projectUsersPath: '/api/:version/projects/:id/users',
projectMergeRequestsPath: '/api/:version/projects/:id/merge_requests',
projectMergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid',
projectMergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes',
@@ -108,6 +109,20 @@ const Api = {
});
},
+ projectUsers(projectPath, query = '', options = {}) {
+ const url = Api.buildUrl(this.projectUsersPath).replace(':id', encodeURIComponent(projectPath));
+
+ return axios
+ .get(url, {
+ params: {
+ search: query,
+ per_page: 20,
+ ...options,
+ },
+ })
+ .then(({ data }) => data);
+ },
+
// Return single project
project(projectPath) {
const url = Api.buildUrl(Api.projectPath).replace(':id', encodeURIComponent(projectPath));
diff --git a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
index 43edc116ed1..839ab542377 100644
--- a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
+++ b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
@@ -212,12 +212,12 @@ export default {
</script>
<template>
- <td :colspan="colspan">
+ <td :colspan="colspan" class="text-center">
<div class="content js-line-expansion-content">
<a
v-if="canExpandUp"
v-tooltip
- class="cursor-pointer js-unfold unfold-icon"
+ class="cursor-pointer js-unfold unfold-icon d-inline-block pt-2 pb-2"
data-placement="top"
data-container="body"
:title="__('Expand up')"
@@ -231,7 +231,7 @@ export default {
<a
v-if="canExpandDown"
v-tooltip
- class="cursor-pointer js-unfold-down has-tooltip unfold-icon"
+ class="cursor-pointer js-unfold-down has-tooltip unfold-icon d-inline-block pt-2 pb-2"
data-placement="top"
data-container="body"
:title="__('Expand down')"
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index 32fbeaaa905..69ec6ab8600 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -130,7 +130,7 @@ export default {
return `\`${this.diffFile.file_path}\``;
},
isFileRenamed() {
- return this.diffFile.viewer.name === diffViewerModes.renamed;
+ return this.diffFile.renamed_file;
},
isModeChanged() {
return this.diffFile.viewer.name === diffViewerModes.mode_changed;
diff --git a/app/assets/javascripts/event_tracking/issue_sidebar.js b/app/assets/javascripts/event_tracking/issue_sidebar.js
new file mode 100644
index 00000000000..6909f82c66f
--- /dev/null
+++ b/app/assets/javascripts/event_tracking/issue_sidebar.js
@@ -0,0 +1,2 @@
+export const initSidebarTracking = () => {};
+export const trackEvent = () => {};
diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js
index 529b6386221..5a9dd91817e 100644
--- a/app/assets/javascripts/issue_show/index.js
+++ b/app/assets/javascripts/issue_show/index.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import { initSidebarTracking } from 'ee_else_ce/event_tracking/issue_sidebar';
import issuableApp from './components/app.vue';
import { parseIssuableData } from './utils/parse_data';
import '../vue_shared/vue_resource_interceptor';
@@ -9,6 +10,9 @@ export default function initIssueableApp() {
components: {
issuableApp,
},
+ mounted() {
+ initSidebarTracking();
+ },
render(createElement) {
return createElement('issuable-app', {
props: parseIssuableData(),
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index ba33d72b1f3..39f2097c174 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -9,7 +9,11 @@ import './commons';
import './behaviors';
// lib/utils
-import { handleLocationHash, addSelectOnFocusBehaviour } from './lib/utils/common_utils';
+import {
+ handleLocationHash,
+ addSelectOnFocusBehaviour,
+ getCspNonceValue,
+} from './lib/utils/common_utils';
import { localTimeAgo } from './lib/utils/datetime_utility';
import { getLocationHash, visitUrl } from './lib/utils/url_utility';
@@ -39,6 +43,17 @@ import 'ee_else_ce/main_ee';
window.jQuery = jQuery;
window.$ = jQuery;
+// Add nonce to jQuery script handler
+jQuery.ajaxSetup({
+ converters: {
+ // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings, func-names
+ 'text script': function(text) {
+ jQuery.globalEval(text, { nonce: getCspNonceValue() });
+ return text;
+ },
+ },
+});
+
// inject test utilities if necessary
if (process.env.NODE_ENV !== 'production' && gon && gon.test_env) {
$.fx.off = true;
diff --git a/app/assets/javascripts/monitoring/components/charts/area.vue b/app/assets/javascripts/monitoring/components/charts/area.vue
index 90c764587a3..78d97a3c122 100644
--- a/app/assets/javascripts/monitoring/components/charts/area.vue
+++ b/app/assets/javascripts/monitoring/components/charts/area.vue
@@ -12,6 +12,9 @@ import { graphDataValidatorForValues } from '../../utils';
let debouncedResize;
+// TODO: Remove this component in favor of the more general time_series.vue
+// Please port all changes here to time_series.vue as well.
+
export default {
components: {
GlAreaChart,
diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue
new file mode 100644
index 00000000000..2fdc75f63ca
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue
@@ -0,0 +1,334 @@
+<script>
+import { __ } from '~/locale';
+import { mapState } from 'vuex';
+import { GlLink, GlButton } from '@gitlab/ui';
+import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
+import dateFormat from 'dateformat';
+import { debounceByAnimationFrame, roundOffFloat } from '~/lib/utils/common_utils';
+import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
+import Icon from '~/vue_shared/components/icon.vue';
+import { chartHeight, graphTypes, lineTypes, symbolSizes, dateFormats } from '../../constants';
+import { makeDataSeries } from '~/helpers/monitor_helper';
+import { graphDataValidatorForValues } from '../../utils';
+
+let debouncedResize;
+
+export default {
+ components: {
+ GlAreaChart,
+ GlLineChart,
+ GlButton,
+ GlChartSeriesLabel,
+ GlLink,
+ Icon,
+ },
+ inheritAttrs: false,
+ props: {
+ graphData: {
+ type: Object,
+ required: true,
+ validator: graphDataValidatorForValues.bind(null, false),
+ },
+ containerWidth: {
+ type: Number,
+ required: true,
+ },
+ deploymentData: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ projectPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ showBorder: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ thresholds: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ data() {
+ return {
+ tooltip: {
+ title: '',
+ content: [],
+ commitUrl: '',
+ isDeployment: false,
+ sha: '',
+ },
+ width: 0,
+ height: chartHeight,
+ svgs: {},
+ primaryColor: null,
+ };
+ },
+ computed: {
+ ...mapState('monitoringDashboard', ['exportMetricsToCsvEnabled']),
+ chartData() {
+ // Transforms & supplements query data to render appropriate labels & styles
+ // Input: [{ queryAttributes1 }, { queryAttributes2 }]
+ // Output: [{ seriesAttributes1 }, { seriesAttributes2 }]
+ return this.graphData.queries.reduce((acc, query) => {
+ const { appearance } = query;
+ const lineType =
+ appearance && appearance.line && appearance.line.type
+ ? appearance.line.type
+ : lineTypes.default;
+ const lineWidth =
+ appearance && appearance.line && appearance.line.width
+ ? appearance.line.width
+ : undefined;
+ const areaStyle = {
+ opacity:
+ appearance && appearance.area && typeof appearance.area.opacity === 'number'
+ ? appearance.area.opacity
+ : undefined,
+ };
+
+ const series = makeDataSeries(query.result, {
+ name: this.formatLegendLabel(query),
+ lineStyle: {
+ type: lineType,
+ width: lineWidth,
+ },
+ showSymbol: false,
+ areaStyle: this.graphData.type === 'area-chart' ? areaStyle : undefined,
+ });
+
+ return acc.concat(series);
+ }, []);
+ },
+ chartOptions() {
+ return {
+ xAxis: {
+ name: __('Time'),
+ type: 'time',
+ axisLabel: {
+ formatter: date => dateFormat(date, dateFormats.timeOfDay),
+ },
+ axisPointer: {
+ snap: true,
+ },
+ },
+ yAxis: {
+ name: this.yAxisLabel,
+ axisLabel: {
+ formatter: num => roundOffFloat(num, 3).toString(),
+ },
+ },
+ series: this.scatterSeries,
+ dataZoom: this.dataZoomConfig,
+ };
+ },
+ dataZoomConfig() {
+ const handleIcon = this.svgs['scroll-handle'];
+
+ return handleIcon ? { handleIcon } : {};
+ },
+ earliestDatapoint() {
+ return this.chartData.reduce((acc, series) => {
+ const { data } = series;
+ const { length } = data;
+ if (!length) {
+ return acc;
+ }
+
+ const [first] = data[0];
+ const [last] = data[length - 1];
+ const seriesEarliest = first < last ? first : last;
+
+ return seriesEarliest < acc || acc === null ? seriesEarliest : acc;
+ }, null);
+ },
+ glChartComponent() {
+ const chartTypes = {
+ 'area-chart': GlAreaChart,
+ 'line-chart': GlLineChart,
+ };
+ return chartTypes[this.graphData.type] || GlAreaChart;
+ },
+ isMultiSeries() {
+ return this.tooltip.content.length > 1;
+ },
+ recentDeployments() {
+ return this.deploymentData.reduce((acc, deployment) => {
+ if (deployment.created_at >= this.earliestDatapoint) {
+ const { id, created_at, sha, ref, tag } = deployment;
+ acc.push({
+ id,
+ createdAt: created_at,
+ sha,
+ commitUrl: `${this.projectPath}/commit/${sha}`,
+ tag,
+ tagUrl: tag ? `${this.tagsPath}/${ref.name}` : null,
+ ref: ref.name,
+ showDeploymentFlag: false,
+ });
+ }
+
+ return acc;
+ }, []);
+ },
+ scatterSeries() {
+ return {
+ type: graphTypes.deploymentData,
+ data: this.recentDeployments.map(deployment => [deployment.createdAt, 0]),
+ symbol: this.svgs.rocket,
+ symbolSize: symbolSizes.default,
+ itemStyle: {
+ color: this.primaryColor,
+ },
+ };
+ },
+ yAxisLabel() {
+ return `${this.graphData.y_label}`;
+ },
+ csvText() {
+ const chartData = this.chartData[0].data;
+ const header = `timestamp,${this.graphData.y_label}\r\n`; // eslint-disable-line @gitlab/i18n/no-non-i18n-strings
+ return chartData.reduce((csv, data) => {
+ const row = data.join(',');
+ return `${csv}${row}\r\n`;
+ }, header);
+ },
+ downloadLink() {
+ const data = new Blob([this.csvText], { type: 'text/plain' });
+ return window.URL.createObjectURL(data);
+ },
+ },
+ watch: {
+ containerWidth: 'onResize',
+ },
+ beforeDestroy() {
+ window.removeEventListener('resize', debouncedResize);
+ },
+ created() {
+ debouncedResize = debounceByAnimationFrame(this.onResize);
+ window.addEventListener('resize', debouncedResize);
+ this.setSvg('rocket');
+ this.setSvg('scroll-handle');
+ },
+ methods: {
+ formatLegendLabel(query) {
+ return `${query.label}`;
+ },
+ formatTooltipText(params) {
+ this.tooltip.title = dateFormat(params.value, dateFormats.default);
+ this.tooltip.content = [];
+ params.seriesData.forEach(dataPoint => {
+ const [xVal, yVal] = dataPoint.value;
+ this.tooltip.isDeployment = dataPoint.componentSubType === graphTypes.deploymentData;
+ if (this.tooltip.isDeployment) {
+ const [deploy] = this.recentDeployments.filter(
+ deployment => deployment.createdAt === xVal,
+ );
+ this.tooltip.sha = deploy.sha.substring(0, 8);
+ this.tooltip.commitUrl = deploy.commitUrl;
+ } else {
+ const { seriesName, color } = dataPoint;
+ const value = yVal.toFixed(3);
+ this.tooltip.content.push({
+ name: seriesName,
+ value,
+ color,
+ });
+ }
+ });
+ },
+ setSvg(name) {
+ getSvgIconPathContent(name)
+ .then(path => {
+ if (path) {
+ this.$set(this.svgs, name, `path://${path}`);
+ }
+ })
+ .catch(e => {
+ // eslint-disable-next-line no-console, @gitlab/i18n/no-non-i18n-strings
+ console.error('SVG could not be rendered correctly: ', e);
+ });
+ },
+ onChartUpdated(chart) {
+ [this.primaryColor] = chart.getOption().color;
+ },
+ onResize() {
+ if (!this.$refs.chart) return;
+ const { width } = this.$refs.chart.$el.getBoundingClientRect();
+ this.width = width;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="prometheus-graph col-12 col-lg-6" :class="[showBorder ? 'p-2' : 'p-0']">
+ <div :class="{ 'prometheus-graph-embed w-100 p-3': showBorder }">
+ <div class="prometheus-graph-header">
+ <h5 class="prometheus-graph-title js-graph-title">{{ graphData.title }}</h5>
+ <gl-button
+ v-if="exportMetricsToCsvEnabled"
+ :href="downloadLink"
+ :title="__('Download CSV')"
+ :aria-label="__('Download CSV')"
+ style="margin-left: 200px;"
+ download="chart_metrics.csv"
+ >
+ {{ __('Download CSV') }}
+ </gl-button>
+ <div class="prometheus-graph-widgets js-graph-widgets">
+ <slot></slot>
+ </div>
+ </div>
+
+ <component
+ :is="glChartComponent"
+ ref="chart"
+ v-bind="$attrs"
+ :data="chartData"
+ :option="chartOptions"
+ :format-tooltip-text="formatTooltipText"
+ :thresholds="thresholds"
+ :width="width"
+ :height="height"
+ @updated="onChartUpdated"
+ >
+ <template v-if="tooltip.isDeployment">
+ <template slot="tooltipTitle">
+ {{ __('Deployed') }}
+ </template>
+ <div slot="tooltipContent" class="d-flex align-items-center">
+ <icon name="commit" class="mr-2" />
+ <gl-link :href="tooltip.commitUrl">{{ tooltip.sha }}</gl-link>
+ </div>
+ </template>
+ <template v-else>
+ <template slot="tooltipTitle">
+ <div class="text-nowrap">
+ {{ tooltip.title }}
+ </div>
+ </template>
+ <template slot="tooltipContent">
+ <div
+ v-for="(content, key) in tooltip.content"
+ :key="key"
+ class="d-flex justify-content-between"
+ >
+ <gl-chart-series-label :color="isMultiSeries ? content.color : ''">
+ {{ content.name }}
+ </gl-chart-series-label>
+ <div class="prepend-left-32">
+ {{ content.value }}
+ </div>
+ </div>
+ </template>
+ </template>
+ </component>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index c414c26ca61..d330ceb836c 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -14,9 +14,9 @@ import { __, s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility';
import invalidUrl from '~/lib/utils/invalid_url';
-import MonitorAreaChart from './charts/area.vue';
+import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
+import MonitorTimeSeriesChart from './charts/time_series.vue';
import MonitorSingleStatChart from './charts/single_stat.vue';
-import PanelType from './panel_type.vue';
import GraphGroup from './graph_group.vue';
import EmptyState from './empty_state.vue';
import { sidebarAnimationDuration, timeWindows } from '../constants';
@@ -26,7 +26,7 @@ let sidebarMutationObserver;
export default {
components: {
- MonitorAreaChart,
+ MonitorTimeSeriesChart,
MonitorSingleStatChart,
PanelType,
GraphGroup,
@@ -141,6 +141,16 @@ export default {
required: false,
default: false,
},
+ alertsEndpoint: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ prometheusAlertsAvailable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -449,11 +459,13 @@ export default {
:clipboard-text="generateLink(groupData.group, graphData.title, graphData.y_label)"
:graph-data="graphData"
:dashboard-width="elWidth"
+ :alerts-endpoint="alertsEndpoint"
+ :prometheus-alerts-available="prometheusAlertsAvailable"
:index="`${index}-${graphIndex}`"
/>
</template>
<template v-else>
- <monitor-area-chart
+ <monitor-time-series-chart
v-for="(graphData, graphIndex) in chartsWithData(groupData.metrics)"
:key="graphIndex"
:graph-data="graphData"
@@ -461,7 +473,7 @@ export default {
:thresholds="getGraphAlertValues(graphData.queries)"
:container-width="elWidth"
:project-path="projectPath"
- group-id="monitor-area-chart"
+ group-id="monitor-time-series-chart"
>
<div class="d-flex align-items-center">
<alert-widget
@@ -503,7 +515,7 @@ export default {
</gl-dropdown-item>
</gl-dropdown>
</div>
- </monitor-area-chart>
+ </monitor-time-series-chart>
</template>
</graph-group>
</div>
diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js
index d7d89522732..13aba3d9f44 100644
--- a/app/assets/javascripts/monitoring/constants.js
+++ b/app/assets/javascripts/monitoring/constants.js
@@ -8,6 +8,10 @@ export const graphTypes = {
deploymentData: 'scatter',
};
+export const symbolSizes = {
+ default: 14,
+};
+
export const lineTypes = {
default: 'solid',
};
@@ -21,6 +25,11 @@ export const timeWindows = {
oneWeek: __('1 week'),
};
+export const dateFormats = {
+ timeOfDay: 'h:MM TT',
+ default: 'dd mmm yyyy, h:MMTT',
+};
+
export const secondsIn = {
thirtyMinutes: 60 * 30,
threeHours: 60 * 60 * 3,
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index 52410f18d4a..3d0ec8cd3a7 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -171,26 +171,33 @@ export const isLastUnresolvedDiscussion = (state, getters) => (discussionId, dif
return lastDiscussionId === discussionId;
};
-// Gets the ID of the discussion following the one provided, respecting order (diff or date)
-// @param {Boolean} discussionId - id of the current discussion
-// @param {Boolean} diffOrder - is ordered by diff?
-export const nextUnresolvedDiscussionId = (state, getters) => (discussionId, diffOrder) => {
- const idsOrdered = getters.unresolvedDiscussionsIdsOrdered(diffOrder);
- const currentIndex = idsOrdered.indexOf(discussionId);
- const slicedIds = idsOrdered.slice(currentIndex + 1, currentIndex + 2);
+export const findUnresolvedDiscussionIdNeighbor = (state, getters) => ({
+ discussionId,
+ diffOrder,
+ step,
+}) => {
+ const ids = getters.unresolvedDiscussionsIdsOrdered(diffOrder);
+ const index = ids.indexOf(discussionId) + step;
+
+ if (index < 0 && step < 0) {
+ return ids[ids.length - 1];
+ }
+
+ if (index === ids.length && step > 0) {
+ return ids[0];
+ }
- // Get the first ID if there is none after the currentIndex
- return slicedIds.length ? idsOrdered.slice(currentIndex + 1, currentIndex + 2)[0] : idsOrdered[0];
+ return ids[index];
};
-export const previousUnresolvedDiscussionId = (state, getters) => (discussionId, diffOrder) => {
- const idsOrdered = getters.unresolvedDiscussionsIdsOrdered(diffOrder);
- const currentIndex = idsOrdered.indexOf(discussionId);
- const slicedIds = idsOrdered.slice(currentIndex - 1, currentIndex);
+// Gets the ID of the discussion following the one provided, respecting order (diff or date)
+// @param {Boolean} discussionId - id of the current discussion
+// @param {Boolean} diffOrder - is ordered by diff?
+export const nextUnresolvedDiscussionId = (state, getters) => (discussionId, diffOrder) =>
+ getters.findUnresolvedDiscussionIdNeighbor({ discussionId, diffOrder, step: 1 });
- // Get the last ID if there is none after the currentIndex
- return slicedIds.length ? slicedIds[0] : idsOrdered[idsOrdered.length - 1];
-};
+export const previousUnresolvedDiscussionId = (state, getters) => (discussionId, diffOrder) =>
+ getters.findUnresolvedDiscussionIdNeighbor({ discussionId, diffOrder, step: -1 });
// @param {Boolean} diffOrder - is ordered by diff?
export const firstUnresolvedDiscussionId = (state, getters) => diffOrder => {
diff --git a/app/assets/javascripts/pages/projects/wikis/wikis.js b/app/assets/javascripts/pages/projects/wikis/wikis.js
index 9b58d42b47d..d41199f6374 100644
--- a/app/assets/javascripts/pages/projects/wikis/wikis.js
+++ b/app/assets/javascripts/pages/projects/wikis/wikis.js
@@ -1,6 +1,5 @@
import bp from '../../../breakpoints';
-import { parseQueryStringIntoObject } from '../../../lib/utils/common_utils';
-import { mergeUrlParams, redirectTo } from '../../../lib/utils/url_utility';
+import { s__, sprintf } from '~/locale';
export default class Wikis {
constructor() {
@@ -12,32 +11,37 @@ export default class Wikis {
sidebarToggles[i].addEventListener('click', e => this.handleToggleSidebar(e));
}
- this.newWikiForm = document.querySelector('form.new-wiki-page');
- if (this.newWikiForm) {
- this.newWikiForm.addEventListener('submit', e => this.handleNewWikiSubmit(e));
+ this.isNewWikiPage = Boolean(document.querySelector('.js-new-wiki-page'));
+ this.editTitleInput = document.querySelector('form.wiki-form #wiki_title');
+ this.commitMessageInput = document.querySelector('form.wiki-form #wiki_message');
+ this.commitMessageI18n = this.isNewWikiPage
+ ? s__('WikiPageCreate|Create %{pageTitle}')
+ : s__('WikiPageEdit|Update %{pageTitle}');
+
+ if (this.editTitleInput) {
+ // Initialize the commit message on load
+ if (this.editTitleInput.value) this.setWikiCommitMessage(this.editTitleInput.value);
+
+ // Set the commit message as the page title is changed
+ this.editTitleInput.addEventListener('keyup', e => this.handleWikiTitleChange(e));
}
window.addEventListener('resize', () => this.renderSidebar());
this.renderSidebar();
}
- handleNewWikiSubmit(e) {
- if (!this.newWikiForm) return;
-
- const slugInput = this.newWikiForm.querySelector('#new_wiki_path');
-
- const slug = slugInput.value;
+ handleWikiTitleChange(e) {
+ this.setWikiCommitMessage(e.target.value);
+ }
- if (slug.length > 0) {
- const wikisPath = slugInput.getAttribute('data-wikis-path');
+ setWikiCommitMessage(rawTitle) {
+ let title = rawTitle;
- // If the wiki is empty, we need to merge the current URL params to keep the "create" view.
- const params = parseQueryStringIntoObject(window.location.search.substr(1));
- const url = mergeUrlParams(params, `${wikisPath}/${slug}`);
- redirectTo(url);
+ // Replace hyphens with spaces
+ if (title) title = title.replace(/-+/g, ' ');
- e.preventDefault();
- }
+ const newCommitMessage = sprintf(this.commitMessageI18n, { pageTitle: title });
+ this.commitMessageInput.value = newCommitMessage;
}
handleToggleSidebar(e) {
diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue
index 4efc1d2408a..a08f732dda7 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_url.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue
@@ -67,7 +67,7 @@ export default {
<span
v-if="pipeline.flags.latest"
v-gl-tooltip
- :title="__('Latest pipeline for this branch')"
+ :title="__('Latest pipeline for the most recent commit on this branch')"
class="js-pipeline-url-latest badge badge-success"
>
{{ __('latest') }}
diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue
index efbf0a4e3cf..346dc470a59 100644
--- a/app/assets/javascripts/registry/components/app.vue
+++ b/app/assets/javascripts/registry/components/app.vue
@@ -1,10 +1,9 @@
<script>
import { mapGetters, mapActions } from 'vuex';
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
import store from '../stores';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
import CollapsibleContainer from './collapsible_container.vue';
-import SvgMessage from './svg_message.vue';
import { s__, sprintf } from '../../locale';
export default {
@@ -12,8 +11,8 @@ export default {
components: {
clipboardButton,
CollapsibleContainer,
+ GlEmptyState,
GlLoadingIcon,
- SvgMessage,
},
props: {
endpoint: {
@@ -93,7 +92,9 @@ export default {
this.setMainEndpoint(this.endpoint);
},
mounted() {
- this.fetchRepos();
+ if (!this.characterError) {
+ this.fetchRepos();
+ }
},
methods: {
...mapActions(['setMainEndpoint', 'fetchRepos']),
@@ -102,61 +103,63 @@ export default {
</script>
<template>
<div>
- <svg-message v-if="characterError" id="invalid-characters" :svg-path="containersErrorImage">
- <h4>
- {{ s__('ContainerRegistry|Docker connection error') }}
- </h4>
- <p v-html="dockerConnectionErrorText"></p>
- </svg-message>
+ <gl-empty-state
+ v-if="characterError"
+ :title="s__('ContainerRegistry|Docker connection error')"
+ :svg-path="containersErrorImage"
+ >
+ <template #description>
+ <p v-html="dockerConnectionErrorText"></p>
+ </template>
+ </gl-empty-state>
- <gl-loading-icon v-else-if="isLoading && !characterError" size="md" class="prepend-top-16" />
+ <gl-loading-icon v-else-if="isLoading" size="md" class="prepend-top-16" />
- <div v-else-if="!isLoading && !characterError && repos.length">
+ <div v-else-if="!isLoading && repos.length">
<h4>{{ s__('ContainerRegistry|Container Registry') }}</h4>
<p v-html="introText"></p>
<collapsible-container v-for="item in repos" :key="item.id" :repo="item" />
</div>
- <svg-message
- v-else-if="!isLoading && !characterError && !repos.length"
- id="no-container-images"
+ <gl-empty-state
+ v-else
+ :title="s__('ContainerRegistry|There are no container images stored for this project')"
:svg-path="noContainersImage"
+ class="container-message"
>
- <h4>
- {{ s__('ContainerRegistry|There are no container images stored for this project') }}
- </h4>
- <p v-html="noContainerImagesText"></p>
-
- <h5>{{ s__('ContainerRegistry|Quick Start') }}</h5>
- <p>
- {{
- s__(
- 'ContainerRegistry|You can add an image to this registry with the following commands:',
- )
- }}
- </p>
+ <template #description>
+ <p class="js-no-container-images-text" v-html="noContainerImagesText"></p>
+ <h5>{{ s__('ContainerRegistry|Quick Start') }}</h5>
+ <p>
+ {{
+ s__(
+ 'ContainerRegistry|You can add an image to this registry with the following commands:',
+ )
+ }}
+ </p>
- <div class="input-group append-bottom-10">
- <input :value="dockerBuildCommand" type="text" class="form-control monospace" readonly />
- <span class="input-group-append">
- <clipboard-button
- :text="dockerBuildCommand"
- :title="s__('ContainerRegistry|Copy build command to clipboard')"
- class="input-group-text"
- />
- </span>
- </div>
+ <div class="input-group append-bottom-10">
+ <input :value="dockerBuildCommand" type="text" class="form-control monospace" readonly />
+ <span class="input-group-append">
+ <clipboard-button
+ :text="dockerBuildCommand"
+ :title="s__('ContainerRegistry|Copy build command to clipboard')"
+ class="input-group-text"
+ />
+ </span>
+ </div>
- <div class="input-group">
- <input :value="dockerPushCommand" type="text" class="form-control monospace" readonly />
- <span class="input-group-append">
- <clipboard-button
- :text="dockerPushCommand"
- :title="s__('ContainerRegistry|Copy push command to clipboard')"
- class="input-group-text"
- />
- </span>
- </div>
- </svg-message>
+ <div class="input-group">
+ <input :value="dockerPushCommand" type="text" class="form-control monospace" readonly />
+ <span class="input-group-append">
+ <clipboard-button
+ :text="dockerPushCommand"
+ :title="s__('ContainerRegistry|Copy push command to clipboard')"
+ class="input-group-text"
+ />
+ </span>
+ </div>
+ </template>
+ </gl-empty-state>
</div>
</template>
diff --git a/app/assets/javascripts/registry/components/svg_message.vue b/app/assets/javascripts/registry/components/svg_message.vue
deleted file mode 100644
index 617093e054e..00000000000
--- a/app/assets/javascripts/registry/components/svg_message.vue
+++ /dev/null
@@ -1,26 +0,0 @@
-<script>
-export default {
- name: 'RegistrySvgMessage',
- props: {
- id: {
- type: String,
- required: true,
- },
- svgPath: {
- type: String,
- required: true,
- },
- },
-};
-</script>
-
-<template>
- <div :id="id" class="empty-state container-message">
- <div class="svg-content">
- <img :src="svgPath" class="flex-align-self-center" />
- </div>
- <div class="text-content">
- <slot></slot>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar.vue
new file mode 100644
index 00000000000..71a1fc31315
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar.vue
@@ -0,0 +1,48 @@
+<script>
+import { __, sprintf } from '~/locale';
+
+export default {
+ props: {
+ user: {
+ type: Object,
+ required: true,
+ },
+ imgSize: {
+ type: Number,
+ required: true,
+ },
+ issuableType: {
+ type: String,
+ required: false,
+ default: 'issue',
+ },
+ },
+ computed: {
+ assigneeAlt() {
+ return sprintf(__("%{userName}'s avatar"), { userName: this.user.name });
+ },
+ avatarUrl() {
+ return this.user.avatar || this.user.avatar_url || gon.default_avatar_url;
+ },
+ isMergeRequest() {
+ return this.issuableType === 'merge_request';
+ },
+ hasMergeIcon() {
+ return this.isMergeRequest && !this.user.can_merge;
+ },
+ },
+};
+</script>
+
+<template>
+ <span class="position-relative">
+ <img
+ :alt="assigneeAlt"
+ :src="avatarUrl"
+ :width="imgSize"
+ :class="`s${imgSize}`"
+ class="avatar avatar-inline m-0"
+ />
+ <i v-if="hasMergeIcon" aria-hidden="true" class="fa fa-exclamation-triangle merge-icon"></i>
+ </span>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue
new file mode 100644
index 00000000000..6633a63d046
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue
@@ -0,0 +1,83 @@
+<script>
+import { __, sprintf } from '~/locale';
+import { GlTooltipDirective, GlLink } from '@gitlab/ui';
+import { joinPaths } from '~/lib/utils/url_utility';
+import AssigneeAvatar from './assignee_avatar.vue';
+
+export default {
+ components: {
+ AssigneeAvatar,
+ GlLink,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ user: {
+ type: Object,
+ required: true,
+ },
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ tooltipPlacement: {
+ type: String,
+ default: 'bottom',
+ required: false,
+ },
+ tooltipHasName: {
+ type: Boolean,
+ default: true,
+ required: false,
+ },
+ issuableType: {
+ type: String,
+ default: 'issue',
+ required: false,
+ },
+ },
+ computed: {
+ cannotMerge() {
+ return this.issuableType === 'merge_request' && !this.user.can_merge;
+ },
+ tooltipTitle() {
+ if (this.cannotMerge && this.tooltipHasName) {
+ return sprintf(__('%{userName} (cannot merge)'), { userName: this.user.name });
+ } else if (this.cannotMerge) {
+ return __('Cannot merge');
+ } else if (this.tooltipHasName) {
+ return this.user.name;
+ }
+
+ return '';
+ },
+ tooltipOption() {
+ return {
+ container: 'body',
+ placement: this.tooltipPlacement,
+ boundary: 'viewport',
+ };
+ },
+ assigneeUrl() {
+ return joinPaths(`${this.rootPath}`, `${this.user.username}`);
+ },
+ },
+};
+</script>
+
+<template>
+ <!-- must be `d-inline-block` or parent flex-basis causes width issues -->
+ <gl-link
+ v-gl-tooltip="tooltipOption"
+ :href="assigneeUrl"
+ :title="tooltipTitle"
+ class="d-inline-block"
+ >
+ <!-- use d-flex so that slot can be appropriately styled -->
+ <span class="d-flex">
+ <assignee-avatar :user="user" :img-size="32" :issuable-type="issuableType" />
+ <slot :user="user"></slot>
+ </span>
+ </gl-link>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
index fa6b6bfaef1..63b93a80ead 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
@@ -1,5 +1,6 @@
<script>
import { n__ } from '~/locale';
+import { trackEvent } from 'ee_else_ce/event_tracking/issue_sidebar';
export default {
name: 'AssigneeTitle',
@@ -29,13 +30,23 @@ export default {
return n__('Assignee', `%d Assignees`, assignees);
},
},
+ methods: {
+ trackEdit() {
+ trackEvent('click_edit_button', 'assignee');
+ },
+ },
};
</script>
<template>
<div class="title hide-collapsed">
{{ assigneeTitle }}
<i v-if="loading" aria-hidden="true" class="fa fa-spinner fa-spin block-loading"></i>
- <a v-if="editable" class="js-sidebar-dropdown-toggle edit-link float-right" href="#">
+ <a
+ v-if="editable"
+ class="js-sidebar-dropdown-toggle edit-link float-right"
+ href="#"
+ @click.prevent="trackEdit"
+ >
{{ __('Edit') }}
</a>
<a
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
index 631e2e28d4d..d9739e8d197 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
@@ -1,13 +1,14 @@
<script>
-import { __, sprintf } from '~/locale';
-import tooltip from '~/vue_shared/directives/tooltip';
+import CollapsedAssigneeList from '../assignees/collapsed_assignee_list.vue';
+import UncollapsedAssigneeList from '../assignees/uncollapsed_assignee_list.vue';
export default {
// name: 'Assignees' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26#possible-false-positives
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
name: 'Assignees',
- directives: {
- tooltip,
+ components: {
+ CollapsedAssigneeList,
+ UncollapsedAssigneeList,
},
props: {
rootPath: {
@@ -24,171 +25,34 @@ export default {
},
issuableType: {
type: String,
- require: true,
+ required: false,
default: 'issue',
},
},
- data() {
- return {
- defaultRenderCount: 5,
- defaultMaxCounter: 99,
- showLess: true,
- };
- },
computed: {
- firstUser() {
- return this.users[0];
- },
- hasMoreThanTwoAssignees() {
- return this.users.length > 2;
- },
- hasMoreThanOneAssignee() {
- return this.users.length > 1;
- },
- hasAssignees() {
- return this.users.length > 0;
- },
hasNoUsers() {
return !this.users.length;
},
- hasOneUser() {
- return this.users.length === 1;
- },
- renderShowMoreSection() {
- return this.users.length > this.defaultRenderCount;
- },
- numberOfHiddenAssignees() {
- return this.users.length - this.defaultRenderCount;
- },
- isHiddenAssignees() {
- return this.numberOfHiddenAssignees > 0;
- },
- hiddenAssigneesLabel() {
- const { numberOfHiddenAssignees } = this;
- return sprintf(__('+ %{numberOfHiddenAssignees} more'), { numberOfHiddenAssignees });
- },
- collapsedTooltipTitle() {
- const maxRender = Math.min(this.defaultRenderCount, this.users.length);
- const renderUsers = this.users.slice(0, maxRender);
- const names = renderUsers.map(u => u.name);
-
- if (this.users.length > maxRender) {
- names.push(`+ ${this.users.length - maxRender} more`);
- }
-
- if (!this.users.length) {
- const emptyTooltipLabel = __('Assignee(s)');
- names.push(emptyTooltipLabel);
- }
-
- return names.join(', ');
- },
- sidebarAvatarCounter() {
- let counter = `+${this.users.length - 1}`;
-
- if (this.users.length > this.defaultMaxCounter) {
- counter = `${this.defaultMaxCounter}+`;
- }
+ sortedAssigness() {
+ const canMergeUsers = this.users.filter(user => user.can_merge);
+ const canNotMergeUsers = this.users.filter(user => !user.can_merge);
- return counter;
- },
- mergeNotAllowedTooltipMessage() {
- const assigneesCount = this.users.length;
-
- if (this.issuableType !== 'merge_request' || assigneesCount === 0) {
- return null;
- }
-
- const cannotMergeCount = this.users.filter(u => u.can_merge === false).length;
- const canMergeCount = assigneesCount - cannotMergeCount;
-
- if (canMergeCount === assigneesCount) {
- // Everyone can merge
- return null;
- } else if (cannotMergeCount === assigneesCount && assigneesCount > 1) {
- return __('No one can merge');
- } else if (assigneesCount === 1) {
- return __('Cannot merge');
- }
-
- return sprintf(__('%{canMergeCount}/%{assigneesCount} can merge'), {
- canMergeCount,
- assigneesCount,
- });
+ return [...canMergeUsers, ...canNotMergeUsers];
},
},
methods: {
assignSelf() {
this.$emit('assign-self');
},
- toggleShowLess() {
- this.showLess = !this.showLess;
- },
- renderAssignee(index) {
- return !this.showLess || (index < this.defaultRenderCount && this.showLess);
- },
- avatarUrl(user) {
- return user.avatar || user.avatar_url || gon.default_avatar_url;
- },
- assigneeUrl(user) {
- return `${this.rootPath}${user.username}`;
- },
- assigneeAlt(user) {
- return sprintf(__("%{userName}'s avatar"), { userName: user.name });
- },
- assigneeUsername(user) {
- return `@${user.username}`;
- },
- shouldRenderCollapsedAssignee(index) {
- const firstTwo = this.users.length <= 2 && index <= 2;
-
- return index === 0 || firstTwo;
- },
},
};
</script>
<template>
<div>
- <div
- v-tooltip
- :class="{ 'multiple-users': hasMoreThanOneAssignee }"
- :title="collapsedTooltipTitle"
- class="sidebar-collapsed-icon sidebar-collapsed-user"
- data-container="body"
- data-placement="left"
- data-boundary="viewport"
- >
- <i v-if="hasNoUsers" :aria-label="__('None')" class="fa fa-user"> </i>
- <button
- v-for="(user, index) in users"
- v-if="shouldRenderCollapsedAssignee(index)"
- :key="user.id"
- type="button"
- class="btn-link"
- >
- <img
- :alt="assigneeAlt(user)"
- :src="avatarUrl(user)"
- width="24"
- class="avatar avatar-inline s24"
- />
- <span class="author"> {{ user.name }} </span>
- </button>
- <button v-if="hasMoreThanTwoAssignees" class="btn-link" type="button">
- <span class="avatar-counter sidebar-avatar-counter"> {{ sidebarAvatarCounter }} </span>
- </button>
- </div>
+ <collapsed-assignee-list :users="sortedAssigness" :issuable-type="issuableType" />
+
<div class="value hide-collapsed">
- <span
- v-if="mergeNotAllowedTooltipMessage"
- v-tooltip
- :title="mergeNotAllowedTooltipMessage"
- data-placement="left"
- class="float-right cannot-be-merged"
- >
- <i aria-hidden="true" data-hidden="true" class="fa fa-exclamation-triangle"></i>
- </span>
<template v-if="hasNoUsers">
<span class="assign-yourself no-value qa-assign-yourself">
{{ __('None') }}
@@ -200,51 +64,13 @@ export default {
</template>
</span>
</template>
- <template v-else-if="hasOneUser">
- <a :href="assigneeUrl(firstUser)" class="author-link bold">
- <img
- :alt="assigneeAlt(firstUser)"
- :src="avatarUrl(firstUser)"
- width="32"
- class="avatar avatar-inline s32"
- />
- <span class="author"> {{ firstUser.name }} </span>
- <span class="username"> {{ assigneeUsername(firstUser) }} </span>
- </a>
- </template>
- <template v-else>
- <div class="user-list">
- <div
- v-for="(user, index) in users"
- v-if="renderAssignee(index)"
- :key="user.id"
- class="user-item"
- >
- <a
- :href="assigneeUrl(user)"
- :data-title="user.name"
- class="user-link has-tooltip"
- data-container="body"
- data-placement="bottom"
- >
- <img
- :alt="assigneeAlt(user)"
- :src="avatarUrl(user)"
- width="32"
- class="avatar avatar-inline s32"
- />
- </a>
- </div>
- </div>
- <div v-if="renderShowMoreSection" class="user-list-more">
- <button type="button" class="btn-link" @click="toggleShowLess">
- <template v-if="showLess">
- {{ hiddenAssigneesLabel }}
- </template>
- <template v-else>{{ __('- show less') }}</template>
- </button>
- </div>
- </template>
+
+ <uncollapsed-assignee-list
+ v-else
+ :users="sortedAssigness"
+ :root-path="rootPath"
+ :issuable-type="issuableType"
+ />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee.vue b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee.vue
new file mode 100644
index 00000000000..2f654409561
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee.vue
@@ -0,0 +1,27 @@
+<script>
+import AssigneeAvatar from './assignee_avatar.vue';
+
+export default {
+ components: {
+ AssigneeAvatar,
+ },
+ props: {
+ user: {
+ type: Object,
+ required: true,
+ },
+ issuableType: {
+ type: String,
+ required: false,
+ default: 'issue',
+ },
+ },
+};
+</script>
+
+<template>
+ <button type="button" class="btn-link">
+ <assignee-avatar :user="user" :img-size="24" :issuable-type="issuableType" />
+ <span class="author"> {{ user.name }} </span>
+ </button>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
new file mode 100644
index 00000000000..5b4a43399ca
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
@@ -0,0 +1,121 @@
+<script>
+import { __, sprintf } from '~/locale';
+import { GlTooltipDirective } from '@gitlab/ui';
+import CollapsedAssignee from './collapsed_assignee.vue';
+
+const DEFAULT_MAX_COUNTER = 99;
+const DEFAULT_RENDER_COUNT = 5;
+
+export default {
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ components: {
+ CollapsedAssignee,
+ },
+ props: {
+ users: {
+ type: Array,
+ required: true,
+ },
+ issuableType: {
+ type: String,
+ required: false,
+ default: 'issue',
+ },
+ },
+ computed: {
+ isMergeRequest() {
+ return this.issuableType === 'merge_request';
+ },
+ hasNoUsers() {
+ return !this.users.length;
+ },
+ hasMoreThanOneAssignee() {
+ return this.users.length > 1;
+ },
+ hasMoreThanTwoAssignees() {
+ return this.users.length > 2;
+ },
+ allAssigneesCanMerge() {
+ return this.users.every(user => user.can_merge);
+ },
+ sidebarAvatarCounter() {
+ if (this.users.length > DEFAULT_MAX_COUNTER) {
+ return `${DEFAULT_MAX_COUNTER}+`;
+ }
+
+ return `+${this.users.length - 1}`;
+ },
+ collapsedUsers() {
+ const collapsedLength = this.hasMoreThanTwoAssignees ? 1 : this.users.length;
+
+ return this.users.slice(0, collapsedLength);
+ },
+ tooltipTitleMergeStatus() {
+ if (!this.isMergeRequest) {
+ return '';
+ }
+
+ const mergeLength = this.users.filter(u => u.can_merge).length;
+
+ if (mergeLength === this.users.length) {
+ return '';
+ } else if (mergeLength > 0) {
+ return sprintf(__('%{mergeLength}/%{usersLength} can merge'), {
+ mergeLength,
+ usersLength: this.users.length,
+ });
+ }
+
+ return this.users.length === 1 ? __('cannot merge') : __('no one can merge');
+ },
+ tooltipTitle() {
+ const maxRender = Math.min(DEFAULT_RENDER_COUNT, this.users.length);
+ const renderUsers = this.users.slice(0, maxRender);
+ const names = renderUsers.map(u => u.name);
+
+ if (!this.users.length) {
+ return __('Assignee(s)');
+ }
+
+ if (this.users.length > names.length) {
+ names.push(sprintf(__('+ %{amount} more'), { amount: this.users.length - names.length }));
+ }
+
+ const text = names.join(', ');
+
+ return this.tooltipTitleMergeStatus ? `${text} (${this.tooltipTitleMergeStatus})` : text;
+ },
+
+ tooltipOptions() {
+ return { container: 'body', placement: 'left', boundary: 'viewport' };
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ v-gl-tooltip="tooltipOptions"
+ :class="{ 'multiple-users': hasMoreThanOneAssignee }"
+ :title="tooltipTitle"
+ class="sidebar-collapsed-icon sidebar-collapsed-user"
+ >
+ <i v-if="hasNoUsers" :aria-label="__('None')" class="fa fa-user"> </i>
+ <collapsed-assignee
+ v-for="user in collapsedUsers"
+ :key="user.id"
+ :user="user"
+ :issuable-type="issuableType"
+ />
+ <button v-if="hasMoreThanTwoAssignees" class="btn-link" type="button">
+ <span class="avatar-counter sidebar-avatar-counter"> {{ sidebarAvatarCounter }} </span>
+ <i
+ v-if="isMergeRequest && !allAssigneesCanMerge"
+ aria-hidden="true"
+ class="fa fa-exclamation-triangle merge-icon"
+ ></i>
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
index be1e4811856..c6cc04a139f 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
@@ -29,7 +29,7 @@ export default {
},
issuableType: {
type: String,
- require: true,
+ required: false,
default: 'issue',
},
},
diff --git a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
new file mode 100644
index 00000000000..3a4623121f4
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
@@ -0,0 +1,96 @@
+<script>
+import { __, sprintf } from '~/locale';
+import AssigneeAvatarLink from './assignee_avatar_link.vue';
+
+const DEFAULT_RENDER_COUNT = 5;
+
+export default {
+ components: {
+ AssigneeAvatarLink,
+ },
+ props: {
+ users: {
+ type: Array,
+ required: true,
+ },
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ issuableType: {
+ type: String,
+ required: false,
+ default: 'issue',
+ },
+ },
+ data() {
+ return {
+ showLess: true,
+ };
+ },
+ computed: {
+ firstUser() {
+ return this.users[0];
+ },
+ hasOneUser() {
+ return this.users.length === 1;
+ },
+ hiddenAssigneesLabel() {
+ const { numberOfHiddenAssignees } = this;
+ return sprintf(__('+ %{numberOfHiddenAssignees} more'), { numberOfHiddenAssignees });
+ },
+ renderShowMoreSection() {
+ return this.users.length > DEFAULT_RENDER_COUNT;
+ },
+ numberOfHiddenAssignees() {
+ return this.users.length - DEFAULT_RENDER_COUNT;
+ },
+ uncollapsedUsers() {
+ const uncollapsedLength = this.showLess
+ ? Math.min(this.users.length, DEFAULT_RENDER_COUNT)
+ : this.users.length;
+ return this.showLess ? this.users.slice(0, uncollapsedLength) : this.users;
+ },
+ username() {
+ return `@${this.firstUser.username}`;
+ },
+ },
+ methods: {
+ toggleShowLess() {
+ this.showLess = !this.showLess;
+ },
+ },
+};
+</script>
+
+<template>
+ <assignee-avatar-link
+ v-if="hasOneUser"
+ v-slot="{ user }"
+ tooltip-placement="left"
+ :tooltip-has-name="false"
+ :user="firstUser"
+ :root-path="rootPath"
+ :issuable-type="issuableType"
+ >
+ <div class="ml-2">
+ <span class="author"> {{ user.name }} </span>
+ <span class="username"> {{ username }} </span>
+ </div>
+ </assignee-avatar-link>
+ <div v-else>
+ <div class="user-list">
+ <div v-for="user in uncollapsedUsers" :key="user.id" class="user-item">
+ <assignee-avatar-link :user="user" :root-path="rootPath" :issuable-type="issuableType" />
+ </div>
+ </div>
+ <div v-if="renderShowMoreSection" class="user-list-more">
+ <button type="button" class="btn-link" @click="toggleShowLess">
+ <template v-if="showLess">
+ {{ hiddenAssigneesLabel }}
+ </template>
+ <template v-else>{{ __('- show less') }}</template>
+ </button>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
index 597b723a9d9..1c75b6148e8 100644
--- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
@@ -5,6 +5,7 @@ import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue';
+import { trackEvent } from 'ee_else_ce/event_tracking/issue_sidebar';
export default {
components: {
@@ -51,6 +52,11 @@ export default {
toggleForm() {
this.edit = !this.edit;
},
+ onEditClick() {
+ this.toggleForm();
+
+ trackEvent('click_edit_button', 'confidentiality');
+ },
updateConfidentialAttribute(confidential) {
this.service
.update('issue', { confidential })
@@ -82,7 +88,7 @@ export default {
v-if="isEditable"
class="float-right confidential-edit"
href="#"
- @click.prevent="toggleForm"
+ @click.prevent="onEditClick"
>
{{ __('Edit') }}
</a>
diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
index c5cfa92f3c8..ec2a7b93a98 100644
--- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
@@ -6,6 +6,7 @@ import issuableMixin from '~/vue_shared/mixins/issuable';
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue';
+import { trackEvent } from 'ee_else_ce/event_tracking/issue_sidebar';
export default {
components: {
@@ -65,7 +66,11 @@ export default {
toggleForm() {
this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen;
},
+ onEditClick() {
+ this.toggleForm();
+ trackEvent('click_edit_button', 'lock_issue');
+ },
updateLockedAttribute(locked) {
this.mediator.service
.update(this.issuableType, {
@@ -109,7 +114,7 @@ export default {
v-if="isEditable"
class="float-right lock-edit"
type="button"
- @click.prevent="toggleForm"
+ @click.prevent="onEditClick"
>
{{ __('Edit') }}
</button>
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
index 0d1faceef11..1f5f19d1931 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
@@ -4,6 +4,7 @@ import icon from '~/vue_shared/components/icon.vue';
import toggleButton from '~/vue_shared/components/toggle_button.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import eventHub from '../../event_hub';
+import { trackEvent } from 'ee_else_ce/event_tracking/issue_sidebar';
const ICON_ON = 'notifications';
const ICON_OFF = 'notifications-off';
@@ -63,6 +64,8 @@ export default {
// Component event emission.
this.$emit('toggleSubscription', this.id);
+
+ trackEvent('toggle_button', 'notifications', this.subscribed ? 0 : 1);
},
onClickCollapsedIcon() {
this.$emit('toggleSidebar');
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 33cedf78331..12c939aa70f 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -62,6 +62,8 @@ function UsersSelect(currentUser, els, options = {}) {
options.showCurrentUser = $dropdown.data('currentUser');
options.todoFilter = $dropdown.data('todoFilter');
options.todoStateFilter = $dropdown.data('todoStateFilter');
+ options.iid = $dropdown.data('iid');
+ options.issuableType = $dropdown.data('issuableType');
showNullUser = $dropdown.data('nullUser');
defaultNullUser = $dropdown.data('nullUserDefault');
showMenuAbove = $dropdown.data('showMenuAbove');
@@ -239,7 +241,7 @@ function UsersSelect(currentUser, els, options = {}) {
'<% if( avatar ) { %> <a class="author-link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>',
);
assigneeTemplate = _.template(
- `<% if (username) { %> <a class="author-link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself">
+ `<% if (username) { %> <a class="author-link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself">
${sprintf(s__('UsersSelect|No assignee - %{openingTag} assign yourself %{closingTag}'), {
openingTag: '<a href="#" class="js-assign-yourself">',
closingTag: '</a>',
@@ -423,6 +425,8 @@ function UsersSelect(currentUser, els, options = {}) {
const { $el, e, isMarking } = options;
const user = options.selectedObj;
+ $el.tooltip('dispose');
+
if ($dropdown.hasClass('js-multiselect')) {
const isActive = $el.hasClass('is-active');
const previouslySelected = $dropdown
@@ -570,20 +574,11 @@ function UsersSelect(currentUser, els, options = {}) {
user.name,
)}</a></li>`;
} else {
- img = "<img src='" + avatar + "' class='avatar avatar-inline' width='32' />";
+ // 0 margin, because it's now handled by a wrapper
+ img = "<img src='" + avatar + "' class='avatar avatar-inline m-0' width='32' />";
}
- return `
- <li data-user-id=${user.id}>
- <a href='#' class='dropdown-menu-user-link ${selected === true ? 'is-active' : ''}'>
- ${img}
- <strong class='dropdown-menu-user-full-name'>
- ${_.escape(user.name)}
- </strong>
- ${username ? `<span class='dropdown-menu-user-username'>${username}</span>` : ''}
- </a>
- </li>
- `;
+ return _this.renderRow(options.issuableType, user, selected, username, img);
},
});
};
@@ -764,6 +759,11 @@ UsersSelect.prototype.users = function(query, options, callback) {
author_id: options.authorId || null,
skip_users: options.skipUsers || null,
};
+
+ if (options.issuableType === 'merge_request') {
+ params.merge_request_iid = options.iid || null;
+ }
+
return axios.get(url, { params }).then(({ data }) => {
callback(data);
});
@@ -776,4 +776,44 @@ UsersSelect.prototype.buildUrl = function(url) {
return url;
};
+UsersSelect.prototype.renderRow = function(issuableType, user, selected, username, img) {
+ const tooltip = issuableType === 'merge_request' && !user.can_merge ? __('Cannot merge') : '';
+ const tooltipClass = tooltip ? `has-tooltip` : '';
+ const selectedClass = selected === true ? 'is-active' : '';
+ const linkClasses = `${selectedClass} ${tooltipClass}`;
+ const tooltipAttributes = tooltip
+ ? `data-container="body" data-placement="left" data-title="${tooltip}"`
+ : '';
+
+ return `
+ <li data-user-id=${user.id}>
+ <a href="#" class="dropdown-menu-user-link d-flex align-items-center ${linkClasses}" ${tooltipAttributes}>
+ ${this.renderRowAvatar(issuableType, user, img)}
+ <span class="d-flex flex-column overflow-hidden">
+ <strong class="dropdown-menu-user-full-name">
+ ${_.escape(user.name)}
+ </strong>
+ ${username ? `<span class="dropdown-menu-user-username">${username}</span>` : ''}
+ </span>
+ </a>
+ </li>
+ `;
+};
+
+UsersSelect.prototype.renderRowAvatar = function(issuableType, user, img) {
+ if (user.beforeDivider) {
+ return img;
+ }
+
+ const mergeIcon =
+ issuableType === 'merge_request' && !user.can_merge
+ ? '<i class="fa fa-exclamation-triangle merge-icon"></i>'
+ : '';
+
+ return `<span class="position-relative mr-2">
+ ${img}
+ ${mergeIcon}
+ </span>`;
+};
+
export default UsersSelect;
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
index c7b064b8506..339e154affc 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
@@ -50,6 +50,7 @@ export default {
startTag: '<span class="label-branch">',
endTag: '</span>',
},
+ false,
);
},
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index 0f55bebd3fc..7843409f4a7 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -87,9 +87,6 @@ export default class MergeRequestStore {
this.allowCollaboration = data.allow_collaboration;
this.sourceProjectId = data.source_project_id;
this.targetProjectId = data.target_project_id;
- this.mergePipelinesEnabled = Boolean(data.merge_pipelines_enabled);
- this.mergeTrainsCount = data.merge_trains_count || 0;
- this.mergeTrainIndex = data.merge_train_index;
// CI related
this.hasCI = data.has_ci;
diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
index df19906309c..f0aae20477b 100644
--- a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
@@ -30,9 +30,16 @@ export default {
},
mounted() {
+ if (window.recaptchaDialogCallback) {
+ throw new Error('recaptchaDialogCallback is already defined!');
+ }
window.recaptchaDialogCallback = this.submit.bind(this);
},
+ beforeDestroy() {
+ window.recaptchaDialogCallback = null;
+ },
+
methods: {
appendRecaptchaScript() {
this.removeRecaptchaScript();
diff --git a/app/assets/stylesheets/errors.scss b/app/assets/stylesheets/errors.scss
index d287215096e..89029a58d1e 100644
--- a/app/assets/stylesheets/errors.scss
+++ b/app/assets/stylesheets/errors.scss
@@ -96,7 +96,7 @@ a {
}
.error-nav {
- padding: 0;
+ padding: $gl-padding 0 0;
text-align: center;
li {
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 1bc597bd4ae..ca737c53318 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -131,7 +131,6 @@
> li:not(.d-none) a {
@include media-breakpoint-down(xs) {
margin-left: 0;
- min-width: 100%;
}
}
}
@@ -233,7 +232,6 @@
.impersonation-btn,
.impersonation-btn:hover {
background-color: $white-light;
- margin-left: 0;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
diff --git a/app/assets/stylesheets/highlight/common.scss b/app/assets/stylesheets/highlight/common.scss
index ac3214a07d9..bdeac7e97c0 100644
--- a/app/assets/stylesheets/highlight/common.scss
+++ b/app/assets/stylesheets/highlight/common.scss
@@ -16,3 +16,16 @@
color: $dark-diff-match-bg;
background: $dark-diff-match-color;
}
+
+@mixin diff-expansion($background, $border, $link) {
+ background-color: $background;
+
+ td {
+ border-top: 1px solid $border;
+ border-bottom: 1px solid $border;
+ }
+
+ a {
+ color: $link;
+ }
+}
diff --git a/app/assets/stylesheets/highlight/themes/dark.scss b/app/assets/stylesheets/highlight/themes/dark.scss
index 16893dd047e..cbce0ba3f1e 100644
--- a/app/assets/stylesheets/highlight/themes/dark.scss
+++ b/app/assets/stylesheets/highlight/themes/dark.scss
@@ -111,6 +111,10 @@ $dark-il: #de935f;
color: $dark-line-color;
}
+ .line_expansion {
+ @include diff-expansion($dark-main-bg, $dark-border, $dark-na);
+ }
+
// Diff line
.line_holder {
&.match .line_content,
diff --git a/app/assets/stylesheets/highlight/themes/monokai.scss b/app/assets/stylesheets/highlight/themes/monokai.scss
index 37fe61b925c..1b61ffa37e3 100644
--- a/app/assets/stylesheets/highlight/themes/monokai.scss
+++ b/app/assets/stylesheets/highlight/themes/monokai.scss
@@ -111,6 +111,10 @@ $monokai-gi: #a6e22e;
color: $monokai-text-color;
}
+ .line_expansion {
+ @include diff-expansion($monokai-bg, $monokai-border, $monokai-k);
+ }
+
// Diff line
.line_holder {
&.match .line_content,
diff --git a/app/assets/stylesheets/highlight/themes/none.scss b/app/assets/stylesheets/highlight/themes/none.scss
index b4217aac37a..a7ede266fb5 100644
--- a/app/assets/stylesheets/highlight/themes/none.scss
+++ b/app/assets/stylesheets/highlight/themes/none.scss
@@ -34,8 +34,11 @@
color: $gl-text-color;
}
-// Diff line
+ .line_expansion {
+ @include diff-expansion($gray-light, $white-normal, $gl-text-color);
+ }
+ // Diff line
$none-over-bg: #ded7fc;
$none-expanded-border: #e0e0e0;
$none-expanded-bg: #e0e0e0;
diff --git a/app/assets/stylesheets/highlight/themes/solarized-dark.scss b/app/assets/stylesheets/highlight/themes/solarized-dark.scss
index a4e9eda22c9..6569f3abc8b 100644
--- a/app/assets/stylesheets/highlight/themes/solarized-dark.scss
+++ b/app/assets/stylesheets/highlight/themes/solarized-dark.scss
@@ -115,6 +115,10 @@ $solarized-dark-il: #2aa198;
color: $solarized-dark-pre-color;
}
+ .line_expansion {
+ @include diff-expansion($solarized-dark-line-bg, $solarized-dark-border, $solarized-dark-kd);
+ }
+
// Diff line
.line_holder {
&.match .line_content,
diff --git a/app/assets/stylesheets/highlight/themes/solarized-light.scss b/app/assets/stylesheets/highlight/themes/solarized-light.scss
index b604d1ccb6c..4e74a9ea50a 100644
--- a/app/assets/stylesheets/highlight/themes/solarized-light.scss
+++ b/app/assets/stylesheets/highlight/themes/solarized-light.scss
@@ -122,6 +122,10 @@ $solarized-light-il: #2aa198;
color: $solarized-light-pre-color;
}
+ .line_expansion {
+ @include diff-expansion($solarized-light-line-bg, $solarized-light-border, $solarized-light-kd);
+ }
+
// Diff line
.line_holder {
&.match .line_content,
diff --git a/app/assets/stylesheets/highlight/white_base.scss b/app/assets/stylesheets/highlight/white_base.scss
index b3974df8639..973f94c63aa 100644
--- a/app/assets/stylesheets/highlight/white_base.scss
+++ b/app/assets/stylesheets/highlight/white_base.scss
@@ -101,24 +101,8 @@ pre.code,
color: $white-code-color;
}
-// Expansion line
.line_expansion {
- background-color: $gray-light;
-
- td {
- border-top: 1px solid $border-color;
- border-bottom: 1px solid $border-color;
- text-align: center;
- }
-
- a {
- color: $blue-600;
- }
-
- .unfold-icon {
- display: inline-block;
- padding: 8px 0;
- }
+ @include diff-expansion($gray-light, $border-color, $blue-600);
}
// Diff line
diff --git a/app/assets/stylesheets/pages/container_registry.scss b/app/assets/stylesheets/pages/container_registry.scss
index 0f4bdb219a3..b88bd78cf3d 100644
--- a/app/assets/stylesheets/pages/container_registry.scss
+++ b/app/assets/stylesheets/pages/container_registry.scss
@@ -3,10 +3,6 @@
*/
.container-message {
- pre {
- white-space: pre-line;
- }
-
span .btn {
margin: 0;
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index fa52ce6402d..0e844b0e4a5 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -126,6 +126,16 @@
}
}
+.assignee {
+ .merge-icon {
+ color: $orange-500;
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ text-shadow: -1px -1px 0 $white-light, 1px -1px 0 $white-light, -1px 1px 0 $white-light, 1px 1px 0 $white-light;
+ }
+}
+
.right-sidebar {
position: fixed;
top: $header-height;
@@ -202,7 +212,6 @@
&.assignee {
.author-link {
display: block;
- padding-left: 42px;
position: relative;
&:hover {
@@ -210,12 +219,6 @@
text-decoration: underline;
}
}
-
- .avatar {
- left: 0;
- position: absolute;
- top: 0;
- }
}
}
}
@@ -354,13 +357,6 @@
margin-top: 0;
}
- .assignee .avatar {
- float: left;
- margin-right: 10px;
- margin-bottom: 0;
- margin-left: 0;
- }
-
.assignee .user-list .avatar {
margin: 0;
}
@@ -521,6 +517,10 @@
display: none;
}
+ .merge-icon {
+ font-size: 10px;
+ }
+
.multiple-users {
position: relative;
height: 24px;
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index f111c7ca8cc..30a567c3bef 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -36,7 +36,7 @@ class AutocompleteController < ApplicationController
end
def award_emojis
- render json: AwardedEmojiFinder.new(current_user).execute
+ render json: AwardEmojis::CollectUserEmojiService.new(current_user).execute
end
def merge_request_target_branches
diff --git a/app/controllers/concerns/invisible_captcha.rb b/app/controllers/concerns/invisible_captcha.rb
index c9f66e5c194..45c0a5c58ef 100644
--- a/app/controllers/concerns/invisible_captcha.rb
+++ b/app/controllers/concerns/invisible_captcha.rb
@@ -41,9 +41,9 @@ module InvisibleCaptcha
request_information = {
message: message,
env: :invisible_captcha_signup_bot_detected,
- ip: request.ip,
+ remote_ip: request.ip,
request_method: request.request_method,
- fullpath: request.fullpath
+ path: request.fullpath
}
Gitlab::AuthLogger.error(request_information)
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 3489ea78b77..8ea77b994de 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -2,8 +2,8 @@
module IssuableCollections
extend ActiveSupport::Concern
- include CookiesHelper
include SortingHelper
+ include SortingPreference
include Gitlab::IssuableMetadata
include Gitlab::Utils::StrongMemoize
@@ -127,47 +127,8 @@ module IssuableCollections
'opened'
end
- def set_sort_order
- set_sort_order_from_user_preference || set_sort_order_from_cookie || default_sort_order
- end
-
- def set_sort_order_from_user_preference
- return unless current_user
- return unless issuable_sorting_field
-
- user_preference = current_user.user_preference
-
- sort_param = params[:sort]
- sort_param ||= user_preference[issuable_sorting_field]
-
- return sort_param if Gitlab::Database.read_only?
-
- if user_preference[issuable_sorting_field] != sort_param
- user_preference.update(issuable_sorting_field => sort_param)
- end
-
- sort_param
- end
-
- # Implement issuable_sorting_field method on controllers
- # to choose which column to store the sorting parameter.
- def issuable_sorting_field
- nil
- end
-
- def set_sort_order_from_cookie
- sort_param = params[:sort] if params[:sort].present?
- # fallback to legacy cookie value for backward compatibility
- sort_param ||= cookies['issuable_sort']
- sort_param ||= cookies[remember_sorting_key]
-
- sort_value = update_cookie_value(sort_param)
- set_secure_cookie(remember_sorting_key, sort_value)
- sort_value
- end
-
- def remember_sorting_key
- @remember_sorting_key ||= "#{collection_type.downcase}_sort"
+ def legacy_sort_cookie_name
+ 'issuable_sort'
end
def default_sort_order
@@ -178,17 +139,6 @@ module IssuableCollections
end
end
- # Update old values to the actual ones.
- def update_cookie_value(value)
- case value
- when 'id_asc' then sort_value_oldest_created
- when 'id_desc' then sort_value_recently_created
- when 'downvotes_asc' then sort_value_popularity
- when 'downvotes_desc' then sort_value_popularity
- else value
- end
- end
-
def finder
@finder ||= issuable_finder_for(finder_type)
end
diff --git a/app/controllers/concerns/issuable_collections_action.rb b/app/controllers/concerns/issuable_collections_action.rb
index 4ad287c4a13..0a6f684a9fc 100644
--- a/app/controllers/concerns/issuable_collections_action.rb
+++ b/app/controllers/concerns/issuable_collections_action.rb
@@ -32,7 +32,7 @@ module IssuableCollectionsAction
private
- def issuable_sorting_field
+ def sorting_field
case action_name
when 'issues'
Issue::SORTING_PREFERENCE_FIELD
diff --git a/app/controllers/concerns/sorting_preference.rb b/app/controllers/concerns/sorting_preference.rb
new file mode 100644
index 00000000000..a51b68147d5
--- /dev/null
+++ b/app/controllers/concerns/sorting_preference.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+module SortingPreference
+ include SortingHelper
+ include CookiesHelper
+
+ def set_sort_order
+ set_sort_order_from_user_preference || set_sort_order_from_cookie || params[:sort] || default_sort_order
+ end
+
+ # Implement sorting_field method on controllers
+ # to choose which column to store the sorting parameter.
+ def sorting_field
+ nil
+ end
+
+ # Implement default_sort_order method on controllers
+ # to choose which default sort should be applied if
+ # sort param is not provided.
+ def default_sort_order
+ nil
+ end
+
+ # Implement legacy_sort_cookie_name method on controllers
+ # to set sort from cookie for backwards compatibility.
+ def legacy_sort_cookie_name
+ nil
+ end
+
+ private
+
+ def set_sort_order_from_user_preference
+ return unless current_user
+ return unless sorting_field
+
+ user_preference = current_user.user_preference
+
+ sort_param = params[:sort]
+ sort_param ||= user_preference[sorting_field]
+
+ return sort_param if Gitlab::Database.read_only?
+
+ if user_preference[sorting_field] != sort_param
+ user_preference.update(sorting_field => sort_param)
+ end
+
+ sort_param
+ end
+
+ def set_sort_order_from_cookie
+ return unless legacy_sort_cookie_name
+
+ sort_param = params[:sort] if params[:sort].present?
+ # fallback to legacy cookie value for backward compatibility
+ sort_param ||= cookies[legacy_sort_cookie_name]
+ sort_param ||= cookies[remember_sorting_key]
+
+ sort_value = update_cookie_value(sort_param)
+ set_secure_cookie(remember_sorting_key, sort_value)
+ sort_value
+ end
+
+ # Convert sorting_field to legacy cookie name for backwards compatibility
+ # :merge_requests_sort => 'mergerequest_sort'
+ # :issues_sort => 'issue_sort'
+ def remember_sorting_key
+ @remember_sorting_key ||= sorting_field
+ .to_s
+ .split('_')[0..-2]
+ .map(&:singularize)
+ .join('')
+ .concat('_sort')
+ end
+
+ # Update old values to the actual ones.
+ def update_cookie_value(value)
+ case value
+ when 'id_asc' then sort_value_oldest_created
+ when 'id_desc' then sort_value_recently_created
+ when 'downvotes_asc' then sort_value_popularity
+ when 'downvotes_desc' then sort_value_popularity
+ else value
+ end
+ end
+end
diff --git a/app/controllers/concerns/toggle_award_emoji.rb b/app/controllers/concerns/toggle_award_emoji.rb
index 97b343f8b1a..24d178781d6 100644
--- a/app/controllers/concerns/toggle_award_emoji.rb
+++ b/app/controllers/concerns/toggle_award_emoji.rb
@@ -7,12 +7,9 @@ module ToggleAwardEmoji
authenticate_user!
name = params.require(:name)
- if awardable.user_can_award?(current_user)
- awardable.toggle_award_emoji(name, current_user)
-
- todoable = to_todoable(awardable)
- TodoService.new.new_award_emoji(todoable, current_user) if todoable
+ service = AwardEmojis::ToggleService.new(awardable, name, current_user).execute
+ if service[:status] == :success
render json: { ok: true }
else
render json: { ok: false }
@@ -21,18 +18,6 @@ module ToggleAwardEmoji
private
- def to_todoable(awardable)
- case awardable
- when Note
- # we don't create todos for personal snippet comments for now
- awardable.for_personal_snippet? ? nil : awardable.noteable
- when MergeRequest, Issue
- awardable
- when Snippet
- nil
- end
- end
-
def awardable
raise NotImplementedError
end
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index daeb8fda417..71f18694613 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -4,10 +4,12 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
include ParamsBackwardCompatibility
include RendersMemberAccess
include OnboardingExperimentHelper
+ include SortingHelper
+ include SortingPreference
prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) }
before_action :set_non_archived_param
- before_action :default_sorting
+ before_action :set_sorting
before_action :projects, only: [:index]
skip_cross_project_access_check :index, :starred
@@ -59,11 +61,6 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
end
end
- def default_sorting
- params[:sort] ||= 'latest_activity_desc'
- @sort = params[:sort]
- end
-
# rubocop: disable CodeReuse/ActiveRecord
def load_projects(finder_params)
@total_user_projects_count = ProjectsFinder.new(params: { non_public: true }, current_user: current_user).execute
@@ -88,4 +85,17 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?)
end
+
+ def set_sorting
+ params[:sort] = set_sort_order
+ @sort = params[:sort]
+ end
+
+ def default_sort_order
+ sort_value_latest_activity
+ end
+
+ def sorting_field
+ Project::SORTING_PREFERENCE_FIELD
+ end
end
diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb
index ef86d5f981a..271f2b4b57d 100644
--- a/app/controllers/explore/projects_controller.rb
+++ b/app/controllers/explore/projects_controller.rb
@@ -3,12 +3,13 @@
class Explore::ProjectsController < Explore::ApplicationController
include ParamsBackwardCompatibility
include RendersMemberAccess
+ include SortingHelper
+ include SortingPreference
before_action :set_non_archived_param
+ before_action :set_sorting
def index
- params[:sort] ||= 'latest_activity_desc'
- @sort = params[:sort]
@projects = load_projects
respond_to do |format|
@@ -23,7 +24,6 @@ class Explore::ProjectsController < Explore::ApplicationController
def trending
params[:trending] = true
- @sort = params[:sort]
@projects = load_projects
respond_to do |format|
@@ -67,4 +67,17 @@ class Explore::ProjectsController < Explore::ApplicationController
prepare_projects_for_rendering(projects)
end
# rubocop: enable CodeReuse/ActiveRecord
+
+ def set_sorting
+ params[:sort] = set_sort_order
+ @sort = params[:sort]
+ end
+
+ def default_sort_order
+ sort_value_latest_activity
+ end
+
+ def sorting_field
+ Project::SORTING_PREFERENCE_FIELD
+ end
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index bc9166b9df3..b7fd286bfe0 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -190,7 +190,7 @@ class Projects::IssuesController < Projects::ApplicationController
protected
- def issuable_sorting_field
+ def sorting_field
Issue::SORTING_PREFERENCE_FIELD
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index f4d381244d9..f4cc0a5851b 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -219,7 +219,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
alias_method :issuable, :merge_request
alias_method :awardable, :merge_request
- def issuable_sorting_field
+ def sorting_field
MergeRequest::SORTING_PREFERENCE_FIELD
end
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index db3b7c8b177..e6e3a440925 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -3,6 +3,7 @@
class Projects::PipelinesController < Projects::ApplicationController
before_action :whitelist_query_limiting, only: [:create, :retry]
before_action :pipeline, except: [:index, :new, :create, :charts]
+ before_action :set_pipeline_path, only: [:show]
before_action :authorize_read_pipeline!
before_action :authorize_read_build!, only: [:index]
before_action :authorize_create_pipeline!, only: [:new, :create]
@@ -174,14 +175,36 @@ class Projects::PipelinesController < Projects::ApplicationController
# rubocop: disable CodeReuse/ActiveRecord
def pipeline
- @pipeline ||= project
- .all_pipelines
- .includes(user: :status)
- .find_by!(id: params[:id])
- .present(current_user: current_user)
+ @pipeline ||= if params[:id].blank? && params[:latest]
+ latest_pipeline
+ else
+ project
+ .all_pipelines
+ .includes(user: :status)
+ .find_by!(id: params[:id])
+ .present(current_user: current_user)
+ end
end
# rubocop: enable CodeReuse/ActiveRecord
+ def set_pipeline_path
+ @pipeline_path ||= if params[:id].blank? && params[:latest]
+ latest_project_pipelines_path(@project, params['ref'])
+ else
+ project_pipeline_path(@project, @pipeline)
+ end
+ end
+
+ def latest_pipeline
+ ref = params['ref'].presence || @project.default_branch
+ sha = @project.commit(ref)&.sha
+
+ @project.ci_pipelines
+ .newest_first(ref: ref, sha: sha)
+ .first
+ &.present(current_user: current_user)
+ end
+
def whitelist_query_limiting
# Also see https://gitlab.com/gitlab-org/gitlab-ce/issues/42343
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42339')
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index d1914c35bd3..b187fdb2723 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -16,6 +16,10 @@ class Projects::WikisController < Projects::ApplicationController
redirect_to(project_wiki_path(@project, @page))
end
+ def new
+ redirect_to project_wiki_path(@project, SecureRandom.uuid, random_title: true)
+ end
+
def pages
@wiki_pages = Kaminari.paginate_array(
@project_wiki.list_pages(sort: params[:sort], direction: params[:direction])
@@ -24,17 +28,25 @@ class Projects::WikisController < Projects::ApplicationController
@wiki_entries = WikiPage.group_by_directory(@wiki_pages)
end
+ # `#show` handles a number of scenarios:
+ #
+ # - If `id` matches a WikiPage, then show the wiki page.
+ # - If `id` is a file in the wiki repository, then send the file.
+ # - If we know the user wants to create a new page with the given `id`,
+ # then display a create form.
+ # - Otherwise show the empty wiki page and invite the user to create a page.
def show
- view_param = @project_wiki.empty? ? params[:view] : 'create'
-
if @page
set_encoding_error unless valid_encoding?
render 'show'
elsif file_blob
send_blob(@project_wiki.repository, file_blob)
- elsif can?(current_user, :create_wiki, @project) && view_param == 'create'
- @page = build_page(title: params[:id])
+ elsif show_create_form?
+ # Assign a title to the WikiPage unless `id` is a randomly generated slug from #new
+ title = params[:id] unless params[:random_title].present?
+
+ @page = build_page(title: title)
render 'edit'
else
@@ -110,6 +122,15 @@ class Projects::WikisController < Projects::ApplicationController
private
+ def show_create_form?
+ can?(current_user, :create_wiki, @project) &&
+ @page.nil? &&
+ # Always show the create form when the wiki has had at least one page created.
+ # Otherwise, we only show the form when the user has navigated from
+ # the 'empty wiki' page
+ (@project_wiki.exists? || params[:view] == 'create')
+ end
+
def load_project_wiki
@project_wiki = load_wiki
@@ -135,7 +156,7 @@ class Projects::WikisController < Projects::ApplicationController
params.require(:wiki).permit(:title, :content, :format, :message, :last_commit_sha)
end
- def build_page(args)
+ def build_page(args = {})
WikiPage.new(@project_wiki).tap do |page|
page.update_attributes(args) # rubocop:disable Rails/ActiveRecordAliases
end
diff --git a/app/finders/award_emojis_finder.rb b/app/finders/award_emojis_finder.rb
new file mode 100644
index 00000000000..7320e035409
--- /dev/null
+++ b/app/finders/award_emojis_finder.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+class AwardEmojisFinder
+ attr_reader :awardable, :params
+
+ def initialize(awardable, params = {})
+ @awardable = awardable
+ @params = params
+
+ validate_params
+ end
+
+ def execute
+ awards = awardable.award_emoji
+ awards = by_name(awards)
+ awards = by_awarded_by(awards)
+ awards
+ end
+
+ private
+
+ def by_name(awards)
+ return awards unless params[:name]
+
+ awards.named(params[:name])
+ end
+
+ def by_awarded_by(awards)
+ return awards unless params[:awarded_by]
+
+ awards.awarded_by(params[:awarded_by])
+ end
+
+ def validate_params
+ return unless params.present?
+
+ validate_name_param
+ validate_awarded_by_param
+ end
+
+ def validate_name_param
+ return unless params[:name]
+
+ raise ArgumentError, 'Invalid name param' unless params[:name].in?(Gitlab::Emoji.emojis_names)
+ end
+
+ def validate_awarded_by_param
+ return unless params[:awarded_by]
+
+ # awarded_by can be a `User`, or an ID
+ unless params[:awarded_by].is_a?(User) || params[:awarded_by].to_s.match(/\A\d+\Z/)
+ raise ArgumentError, 'Invalid awarded_by param'
+ end
+ end
+end
diff --git a/app/finders/awarded_emoji_finder.rb b/app/finders/awarded_emoji_finder.rb
deleted file mode 100644
index f0cc17f3b26..00000000000
--- a/app/finders/awarded_emoji_finder.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-# Class for retrieving information about emoji awarded _by_ a particular user.
-class AwardedEmojiFinder
- attr_reader :current_user
-
- # current_user - The User to generate the data for.
- def initialize(current_user = nil)
- @current_user = current_user
- end
-
- def execute
- return [] unless current_user
-
- # We want the resulting data set to be an Array containing the emoji names
- # in descending order, based on how often they were awarded.
- AwardEmoji
- .award_counts_for_user(current_user)
- .map { |name, _| { name: name } }
- end
-end
diff --git a/app/graphql/mutations/award_emojis/add.rb b/app/graphql/mutations/award_emojis/add.rb
index 8e050dd6d29..85f3eb065bb 100644
--- a/app/graphql/mutations/award_emojis/add.rb
+++ b/app/graphql/mutations/award_emojis/add.rb
@@ -10,14 +10,11 @@ module Mutations
check_object_is_awardable!(awardable)
- # TODO this will be handled by AwardEmoji::AddService
- # See https://gitlab.com/gitlab-org/gitlab-ce/issues/63372 and
- # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/29782
- award = awardable.create_award_emoji(args[:name], current_user)
+ service = ::AwardEmojis::AddService.new(awardable, args[:name], current_user).execute
{
- award_emoji: (award if award.persisted?),
- errors: errors_on_object(award)
+ award_emoji: (service[:award] if service[:status] == :success),
+ errors: service[:errors] || []
}
end
end
diff --git a/app/graphql/mutations/award_emojis/remove.rb b/app/graphql/mutations/award_emojis/remove.rb
index 3ba85e445b8..f8a3d0ce390 100644
--- a/app/graphql/mutations/award_emojis/remove.rb
+++ b/app/graphql/mutations/award_emojis/remove.rb
@@ -10,22 +10,11 @@ module Mutations
check_object_is_awardable!(awardable)
- # TODO this check can be removed once AwardEmoji services are available.
- # See https://gitlab.com/gitlab-org/gitlab-ce/issues/63372 and
- # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/29782
- unless awardable.awarded_emoji?(args[:name], current_user)
- raise Gitlab::Graphql::Errors::ResourceNotAvailable,
- 'You have not awarded emoji of type name to the awardable'
- end
-
- # TODO this will be handled by AwardEmoji::DestroyService
- # See https://gitlab.com/gitlab-org/gitlab-ce/issues/63372 and
- # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/29782
- awardable.remove_award_emoji(args[:name], current_user)
+ service = ::AwardEmojis::DestroyService.new(awardable, args[:name], current_user).execute
{
# Mutation response is always a `nil` award_emoji
- errors: []
+ errors: service[:errors] || []
}
end
end
diff --git a/app/graphql/mutations/award_emojis/toggle.rb b/app/graphql/mutations/award_emojis/toggle.rb
index c03902e8035..d822048f3a6 100644
--- a/app/graphql/mutations/award_emojis/toggle.rb
+++ b/app/graphql/mutations/award_emojis/toggle.rb
@@ -15,23 +15,15 @@ module Mutations
check_object_is_awardable!(awardable)
- # TODO this will be handled by AwardEmoji::ToggleService
- # See https://gitlab.com/gitlab-org/gitlab-ce/issues/63372 and
- # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/29782
- award = awardable.toggle_award_emoji(args[:name], current_user)
-
- # Destroy returns a collection :(
- award = award.first if award.is_a?(Array)
-
- errors = errors_on_object(award)
+ service = ::AwardEmojis::ToggleService.new(awardable, args[:name], current_user).execute
toggled_on = awardable.awarded_emoji?(args[:name], current_user)
{
# For consistency with the AwardEmojis::Remove mutation, only return
# the AwardEmoji if it was created and not destroyed
- award_emoji: (award if toggled_on),
- errors: errors,
+ award_emoji: (service[:award] if toggled_on),
+ errors: service[:errors] || [],
toggled_on: toggled_on
}
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index e2e007eee50..b88b25eb845 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -405,7 +405,11 @@ module IssuablesHelper
placement: is_collapsed ? 'left' : nil,
container: is_collapsed ? 'body' : nil,
boundary: 'viewport',
- is_collapsed: is_collapsed
+ is_collapsed: is_collapsed,
+ track_label: "right_sidebar",
+ track_property: "update_todo",
+ track_event: "click_button",
+ track_value: ""
}
end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 5d292094a05..3683f2ea9a9 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -125,9 +125,8 @@ class Notify < BaseMailer
def mail_thread(model, headers = {})
add_project_headers
add_unsubscription_headers_and_links
+ add_model_headers(model)
- headers["X-GitLab-#{model.class.name}-ID"] = model.id
- headers["X-GitLab-#{model.class.name}-IID"] = model.iid if model.respond_to?(:iid)
headers['X-GitLab-Reply-Key'] = reply_key
@reason = headers['X-GitLab-NotificationReason']
@@ -196,6 +195,18 @@ class Notify < BaseMailer
@reply_key ||= SentNotification.reply_key
end
+ # This method applies threading headers to the email to identify
+ # the instance we are discussing.
+ #
+ # All model instances must have `#id`, and may implement `#iid`.
+ def add_model_headers(object)
+ # Use replacement so we don't strip the module.
+ prefix = "X-GitLab-#{object.class.name.gsub(/::/, '-')}"
+
+ headers["#{prefix}-ID"] = object.id
+ headers["#{prefix}-IID"] = object.iid if object.respond_to?(:iid)
+ end
+
def add_project_headers
return unless @project
diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb
index e26162f6151..0ab302a0f3e 100644
--- a/app/models/award_emoji.rb
+++ b/app/models/award_emoji.rb
@@ -16,8 +16,10 @@ class AwardEmoji < ApplicationRecord
participant :user
- scope :downvotes, -> { where(name: DOWNVOTE_NAME) }
- scope :upvotes, -> { where(name: UPVOTE_NAME) }
+ scope :downvotes, -> { named(DOWNVOTE_NAME) }
+ scope :upvotes, -> { named(UPVOTE_NAME) }
+ scope :named, -> (names) { where(name: names) }
+ scope :awarded_by, -> (users) { where(user: users) }
after_save :expire_etag_cache
after_destroy :expire_etag_cache
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 3c0efca31db..7930bef5cf2 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -121,6 +121,8 @@ module Ci
scope :scheduled_actions, ->() { where(when: :delayed, status: COMPLETED_STATUSES + %i[scheduled]) }
scope :ref_protected, -> { where(protected: true) }
scope :with_live_trace, -> { where('EXISTS (?)', Ci::BuildTraceChunk.where('ci_builds.id = ci_build_trace_chunks.build_id').select(1)) }
+ scope :with_stale_live_trace, -> { with_live_trace.finished_before(12.hours.ago) }
+ scope :finished_before, -> (date) { finished.where('finished_at < ?', date) }
scope :matches_tag_ids, -> (tag_ids) do
matcher = ::ActsAsTaggableOn::Tagging
diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb
index 14bc56f0eee..f229b42ade6 100644
--- a/app/models/concerns/awardable.rb
+++ b/app/models/concerns/awardable.rb
@@ -106,30 +106,6 @@ module Awardable
end
def awarded_emoji?(emoji_name, current_user)
- award_emoji.where(name: emoji_name, user: current_user).exists?
- end
-
- def create_award_emoji(name, current_user)
- return unless emoji_awardable?
-
- award_emoji.create(name: normalize_name(name), user: current_user)
- end
-
- def remove_award_emoji(name, current_user)
- award_emoji.where(name: name, user: current_user).destroy_all # rubocop: disable DestroyAll
- end
-
- def toggle_award_emoji(emoji_name, current_user)
- if awarded_emoji?(emoji_name, current_user)
- remove_award_emoji(emoji_name, current_user)
- else
- create_award_emoji(emoji_name, current_user)
- end
- end
-
- private
-
- def normalize_name(name)
- Gitlab::Emoji.normalize_emoji_name(name)
+ award_emoji.named(emoji_name).awarded_by(current_user).exists?
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 8efe4b06f87..10679fb1f85 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -55,6 +55,8 @@ class Project < ApplicationRecord
VALID_MIRROR_PORTS = [22, 80, 443].freeze
VALID_MIRROR_PROTOCOLS = %w(http https ssh git).freeze
+ SORTING_PREFERENCE_FIELD = :projects_sort
+
cache_markdown_field :description, pipeline: :description
delegate :feature_available?, :builds_enabled?, :wiki_enabled?,
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index c91add6439f..4a19e05bf76 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -85,6 +85,10 @@ class ProjectWiki
list_pages(limit: 1).empty?
end
+ def exists?
+ !empty?
+ end
+
# Lists wiki pages of the repository.
#
# limit - max number of pages returned by the method.
diff --git a/app/serializers/deployment_entity.rb b/app/serializers/deployment_entity.rb
index 6e91317eb20..d6466ad1cdd 100644
--- a/app/serializers/deployment_entity.rb
+++ b/app/serializers/deployment_entity.rb
@@ -18,7 +18,7 @@ class DeploymentEntity < Grape::Entity
end
expose :created_at
- expose :finished_at
+ expose :deployed_at
expose :tag
expose :last?
expose :user, using: UserEntity
diff --git a/app/serializers/deployment_serializer.rb b/app/serializers/deployment_serializer.rb
index 3fd3e1b9cc8..b48037dd53f 100644
--- a/app/serializers/deployment_serializer.rb
+++ b/app/serializers/deployment_serializer.rb
@@ -4,7 +4,7 @@ class DeploymentSerializer < BaseSerializer
entity DeploymentEntity
def represent_concise(resource, opts = {})
- opts[:only] = [:iid, :id, :sha, :created_at, :finished_at, :tag, :last?, :id, ref: [:name]]
+ opts[:only] = [:iid, :id, :sha, :created_at, :deployed_at, :tag, :last?, :id, ref: [:name]]
represent(resource, opts)
end
end
diff --git a/app/serializers/issuable_sidebar_basic_entity.rb b/app/serializers/issuable_sidebar_basic_entity.rb
index c02fd024345..058c707ef9d 100644
--- a/app/serializers/issuable_sidebar_basic_entity.rb
+++ b/app/serializers/issuable_sidebar_basic_entity.rb
@@ -4,6 +4,7 @@ class IssuableSidebarBasicEntity < Grape::Entity
include RequestAwareEntity
expose :id
+ expose :iid
expose :type do |issuable|
issuable.to_ability_name
end
diff --git a/app/serializers/merge_request_serializer.rb b/app/serializers/merge_request_serializer.rb
index 8ad1df5dfe0..bd2e682a122 100644
--- a/app/serializers/merge_request_serializer.rb
+++ b/app/serializers/merge_request_serializer.rb
@@ -8,7 +8,7 @@ class MergeRequestSerializer < BaseSerializer
entity ||=
case opts[:serializer]
when 'sidebar'
- IssuableSidebarBasicEntity
+ MergeRequestSidebarBasicEntity
when 'sidebar_extras'
MergeRequestSidebarExtrasEntity
when 'basic'
diff --git a/app/serializers/merge_request_sidebar_basic_entity.rb b/app/serializers/merge_request_sidebar_basic_entity.rb
new file mode 100644
index 00000000000..3c911bbe4c8
--- /dev/null
+++ b/app/serializers/merge_request_sidebar_basic_entity.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class MergeRequestSidebarBasicEntity < IssuableSidebarBasicEntity
+ expose :current_user, if: lambda { |_issuable| current_user } do
+ expose :can_merge do |merge_request|
+ merge_request.can_be_merged_by?(current_user)
+ end
+ end
+end
diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb
index 554b307d4f8..c8088608cb0 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -84,8 +84,10 @@ class MergeRequestWidgetEntity < Grape::Entity
private
+ delegate :current_user, to: :request
+
def presenter(merge_request)
@presenters ||= {}
- @presenters[merge_request] ||= MergeRequestPresenter.new(merge_request, current_user: request.current_user) # rubocop: disable CodeReuse/Presenter
+ @presenters[merge_request] ||= MergeRequestPresenter.new(merge_request, current_user: current_user) # rubocop: disable CodeReuse/Presenter
end
end
diff --git a/app/services/award_emojis/add_service.rb b/app/services/award_emojis/add_service.rb
new file mode 100644
index 00000000000..eac15dabbf0
--- /dev/null
+++ b/app/services/award_emojis/add_service.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module AwardEmojis
+ class AddService < AwardEmojis::BaseService
+ include Gitlab::Utils::StrongMemoize
+
+ def execute
+ unless awardable.user_can_award?(current_user)
+ return error('User cannot award emoji to awardable', status: :forbidden)
+ end
+
+ unless awardable.emoji_awardable?
+ return error('Awardable cannot be awarded emoji', status: :unprocessable_entity)
+ end
+
+ award = awardable.award_emoji.create(name: name, user: current_user)
+
+ if award.persisted?
+ TodoService.new.new_award_emoji(todoable, current_user) if todoable
+ success(award: award)
+ else
+ error(award.errors.full_messages, award: award)
+ end
+ end
+
+ private
+
+ def todoable
+ strong_memoize(:todoable) do
+ case awardable
+ when Note
+ # We don't create todos for personal snippet comments for now
+ awardable.noteable unless awardable.for_personal_snippet?
+ when MergeRequest, Issue
+ awardable
+ when Snippet
+ nil
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/award_emojis/base_service.rb b/app/services/award_emojis/base_service.rb
new file mode 100644
index 00000000000..a677d03a221
--- /dev/null
+++ b/app/services/award_emojis/base_service.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module AwardEmojis
+ class BaseService < ::BaseService
+ attr_accessor :awardable, :name
+
+ def initialize(awardable, name, current_user)
+ @awardable = awardable
+ @name = normalize_name(name)
+
+ super(awardable.project, current_user)
+ end
+
+ private
+
+ def normalize_name(name)
+ Gitlab::Emoji.normalize_emoji_name(name)
+ end
+
+ # Provide more error state data than what BaseService allows.
+ # - An array of errors
+ # - The `AwardEmoji` if present
+ def error(errors, award: nil, status: nil)
+ errors = Array.wrap(errors)
+
+ super(errors.to_sentence.presence, status).merge({
+ award: award,
+ errors: errors
+ })
+ end
+ end
+end
diff --git a/app/services/award_emojis/collect_user_emoji_service.rb b/app/services/award_emojis/collect_user_emoji_service.rb
new file mode 100644
index 00000000000..6cab23f3edf
--- /dev/null
+++ b/app/services/award_emojis/collect_user_emoji_service.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+# Class for retrieving information about emoji awarded _by_ a particular user.
+module AwardEmojis
+ class CollectUserEmojiService
+ attr_reader :current_user
+
+ # current_user - The User to generate the data for.
+ def initialize(current_user = nil)
+ @current_user = current_user
+ end
+
+ def execute
+ return [] unless current_user
+
+ # We want the resulting data set to be an Array containing the emoji names
+ # in descending order, based on how often they were awarded.
+ AwardEmoji
+ .award_counts_for_user(current_user)
+ .map { |name, _| { name: name } }
+ end
+ end
+end
diff --git a/app/services/award_emojis/destroy_service.rb b/app/services/award_emojis/destroy_service.rb
new file mode 100644
index 00000000000..3789a8403bc
--- /dev/null
+++ b/app/services/award_emojis/destroy_service.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module AwardEmojis
+ class DestroyService < AwardEmojis::BaseService
+ def execute
+ unless awardable.user_can_award?(current_user)
+ return error('User cannot destroy emoji on the awardable', status: :forbidden)
+ end
+
+ awards = AwardEmojisFinder.new(awardable, name: name, awarded_by: current_user).execute
+
+ if awards.empty?
+ return error("User has not awarded emoji of type #{name} on the awardable", status: :forbidden)
+ end
+
+ award = awards.destroy_all.first # rubocop: disable DestroyAll
+
+ success(award: award)
+ end
+ end
+end
diff --git a/app/services/award_emojis/toggle_service.rb b/app/services/award_emojis/toggle_service.rb
new file mode 100644
index 00000000000..812dd1c2889
--- /dev/null
+++ b/app/services/award_emojis/toggle_service.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module AwardEmojis
+ class ToggleService < AwardEmojis::BaseService
+ def execute
+ if awardable.awarded_emoji?(name, current_user)
+ DestroyService.new(awardable, name, current_user).execute
+ else
+ AddService.new(awardable, name, current_user).execute
+ end
+ end
+ end
+end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 77c2224ee3b..2ab6e88599f 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -344,10 +344,7 @@ class IssuableBaseService < BaseService
def toggle_award(issuable)
award = params.delete(:emoji_award)
- if award
- todo_service.new_award_emoji(issuable, current_user)
- issuable.toggle_award_emoji(award, current_user)
- end
+ AwardEmojis::ToggleService.new(issuable, award, current_user).execute if award
end
def associations_before_update(issuable)
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index 067510a8a0a..c6aae4c28f2 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -17,11 +17,9 @@ module MergeRequests
end
def execute_hooks(merge_request, action = 'open', old_rev: nil, old_associations: {})
- if merge_request.project
- merge_data = hook_data(merge_request, action, old_rev: old_rev, old_associations: old_associations)
- merge_request.project.execute_hooks(merge_data, :merge_request_hooks)
- merge_request.project.execute_services(merge_data, :merge_request_hooks)
- end
+ merge_data = hook_data(merge_request, action, old_rev: old_rev, old_associations: old_associations)
+ merge_request.project.execute_hooks(merge_data, :merge_request_hooks)
+ merge_request.project.execute_services(merge_data, :merge_request_hooks)
end
def cleanup_environments(merge_request)
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index e30debbbe75..ee7223d6349 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -598,11 +598,11 @@ module SystemNoteService
end
def zoom_link_added(issue, project, author)
- create_note(NoteSummary.new(issue, project, author, _('a Zoom call was added to this issue'), action: 'pinned_embed'))
+ create_note(NoteSummary.new(issue, project, author, _('added a Zoom call to this issue'), action: 'pinned_embed'))
end
def zoom_link_removed(issue, project, author)
- create_note(NoteSummary.new(issue, project, author, _('a Zoom call was removed from this issue'), action: 'pinned_embed'))
+ create_note(NoteSummary.new(issue, project, author, _('removed a Zoom call from this issue'), action: 'pinned_embed'))
end
private
diff --git a/app/views/admin/applications/_delete_form.html.haml b/app/views/admin/applications/_delete_form.html.haml
index 82781f6716d..86f09bf1cb0 100644
--- a/app/views/admin/applications/_delete_form.html.haml
+++ b/app/views/admin/applications/_delete_form.html.haml
@@ -1,4 +1,4 @@
- submit_btn_css ||= 'btn btn-link btn-remove btn-sm'
= form_tag admin_application_path(application) do
%input{ :name => "_method", :type => "hidden", :value => "delete" }/
- = submit_tag 'Destroy', onclick: "return confirm('Are you sure?')", class: submit_btn_css
+ = submit_tag 'Destroy', class: submit_btn_css, data: { confirm: _('Are you sure?') }
diff --git a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml
index 69cc510e9c1..9bc5e2ee42f 100644
--- a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml
+++ b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml
@@ -5,4 +5,4 @@
= form_tag path do
%input{ :name => "_method", :type => "hidden", :value => "delete" }/
- = submit_tag _('Revoke'), onclick: "return confirm('#{_('Are you sure?')}')", class: 'btn btn-remove btn-sm'
+ = submit_tag _('Revoke'), class: 'btn btn-remove btn-sm', data: { confirm: _('Are you sure?') }
diff --git a/app/views/errors/access_denied.html.haml b/app/views/errors/access_denied.html.haml
index 46931b5932d..1ed7b56db1d 100644
--- a/app/views/errors/access_denied.html.haml
+++ b/app/views/errors/access_denied.html.haml
@@ -10,7 +10,7 @@
= message
%p
= s_('403|Please contact your GitLab administrator to get permission.')
- .action-container.js-go-back{ style: 'display: none' }
- %a{ href: 'javascript:history.back()', class: 'btn btn-success' }
+ .action-container.js-go-back{ hidden: true }
+ %button{ type: 'button', class: 'btn btn-success' }
= s_('Go Back')
= render "errors/footer"
diff --git a/app/views/layouts/errors.html.haml b/app/views/layouts/errors.html.haml
index 74484005b48..dc924a0e25d 100644
--- a/app/views/layouts/errors.html.haml
+++ b/app/views/layouts/errors.html.haml
@@ -14,6 +14,10 @@
var goBackElement = document.querySelector('.js-go-back');
if (goBackElement && history.length > 1) {
- goBackElement.style.display = 'block';
+ goBackElement.removeAttribute('hidden');
+
+ goBackElement.querySelector('button').addEventListener('click', function() {
+ history.back();
+ });
}
}());
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 89f99472270..14c7b2428b2 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -4,6 +4,7 @@
- search_path_url = search_path(group_id: group.id)
- else
- search_path_url = search_path
+- has_impersonation_link = header_link?(:admin_impersonation)
%header.navbar.navbar-gitlab.navbar-expand-sm.js-navbar{ data: { qa_selector: 'navbar' } }
%a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content
@@ -64,14 +65,14 @@
.dropdown-menu.dropdown-menu-right
= render 'layouts/header/help_dropdown'
- if header_link?(:user_dropdown)
- %li.nav-item.header-user.dropdown{ data: { track_label: "profile_dropdown", track_event: "click_dropdown", track_value: "", qa_selector: 'user_menu' } }
+ %li.nav-item.header-user.dropdown{ data: { track_label: "profile_dropdown", track_event: "click_dropdown", track_value: "", qa_selector: 'user_menu' }, class: ('mr-0' if has_impersonation_link) }
= link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
= image_tag avatar_icon_for_user(current_user, 23), width: 23, height: 23, class: "header-user-avatar qa-user-avatar"
= sprite_icon('angle-down', css_class: 'caret-down')
.dropdown-menu.dropdown-menu-right
= render 'layouts/header/current_user_dropdown'
- - if header_link?(:admin_impersonation)
- %li.nav-item.impersonation
+ - if has_impersonation_link
+ %li.nav-item.impersonation.ml-0
= link_to admin_impersonation_path, class: 'nav-link impersonation-btn', method: :delete, title: _('Stop impersonation'), aria: { label: _('Stop impersonation') }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
= icon('user-secret')
- if header_link?(:sign_in)
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index 65ef9690062..08a39fc4f58 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -31,7 +31,7 @@
= s_('AccessTokens|It cannot be used to access any other data.')
.col-lg-8.feed-token-reset
= label_tag :feed_token, s_('AccessTokens|Feed token'), class: "label-bold"
- = text_field_tag :feed_token, current_user.feed_token, class: 'form-control', readonly: true, onclick: 'this.select()'
+ = text_field_tag :feed_token, current_user.feed_token, class: 'form-control js-select-on-focus', readonly: true
%p.form-text.text-muted
- reset_link = link_to s_('AccessTokens|reset it'), [:reset, :feed_token, :profile], method: :put, data: { confirm: s_('AccessTokens|Are you sure? Any RSS or calendar URLs currently in use will stop working.') }
- reset_message = s_('AccessTokens|Keep this token secret. Anyone who gets ahold of it can read activity and issue RSS feeds or your calendar feed as if they were you. You should %{link_reset_it} if that ever happens.') % { link_reset_it: reset_link }
@@ -49,7 +49,7 @@
= s_('AccessTokens|It cannot be used to access any other data.')
.col-lg-8.incoming-email-token-reset
= label_tag :incoming_email_token, s_('AccessTokens|Incoming email token'), class: "label-bold"
- = text_field_tag :incoming_email_token, current_user.incoming_email_token, class: 'form-control', readonly: true, onclick: 'this.select()'
+ = text_field_tag :incoming_email_token, current_user.incoming_email_token, class: 'form-control js-select-on-focus', readonly: true
%p.form-text.text-muted
- reset_link = link_to s_('AccessTokens|reset it'), [:reset, :incoming_email_token, :profile], method: :put, data: { confirm: s_('AccessTokens|Are you sure? Any issue email addresses currently in use will stop working.') }
- reset_message = s_('AccessTokens|Keep this token secret. Anyone who gets ahold of it can create issues as if they were you. You should %{link_reset_it} if that ever happens.') % { link_reset_it: reset_link }
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index 9614f33fe2f..53bb3c7487d 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -21,7 +21,7 @@
.icon-container
= sprite_icon('flag')
- if @pipeline.latest?
- %span.js-pipeline-url-latest.badge.badge-success.has-tooltip{ title: _("Latest pipeline for this branch") }
+ %span.js-pipeline-url-latest.badge.badge-success.has-tooltip{ title: _("Latest pipeline for the most recent commit on this branch") }
latest
- if @pipeline.has_yaml_errors?
%span.js-pipeline-url-yaml.badge.badge-danger.has-tooltip{ title: @pipeline.yaml_errors }
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index c04f076a3ab..56995ffbcee 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -1,7 +1,7 @@
.tabs-holder
%ul.pipelines-tabs.nav-links.no-top.no-bottom.mobile-separator.nav.nav-tabs
%li.js-pipeline-tab-link
- = link_to project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-pipeline', action: 'pipelines', toggle: 'tab' }, class: 'pipeline-tab' do
+ = link_to @pipeline_path, data: { target: '#js-tab-pipeline', action: 'pipelines', toggle: 'tab' }, class: 'pipeline-tab' do
= _('Pipeline')
%li.js-builds-tab-link
= link_to builds_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index 858731b2dda..a153f527ee0 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -1,8 +1,8 @@
-- commit_message = @page.persisted? ? s_("WikiPageEdit|Update %{page_title}") : s_("WikiPageCreate|Create %{page_title}")
-- commit_message = commit_message % { page_title: @page.title }
+- form_classes = 'wiki-form common-note-form prepend-top-default js-quick-submit'
+- form_classes += ' js-new-wiki-page' unless @page.persisted?
= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post,
- html: { class: 'wiki-form common-note-form prepend-top-default js-quick-submit' },
+ html: { class: form_classes },
data: { uploads_path: uploads_path } do |f|
= form_errors(@page)
@@ -12,12 +12,14 @@
.form-group.row
.col-sm-12= f.label :title, class: 'control-label-full-width'
.col-sm-12
- = f.text_field :title, class: 'form-control qa-wiki-title-textbox', value: @page.title
- - if @page.persisted?
- %span.d-inline-block.mw-100.prepend-top-5
- = icon('lightbulb-o')
+ = f.text_field :title, class: 'form-control qa-wiki-title-textbox', value: @page.title, required: true, autofocus: !@page.persisted?, placeholder: _('Wiki|Page title')
+ %span.d-inline-block.mw-100.prepend-top-5
+ = icon('lightbulb-o')
+ - if @page.persisted?
= s_("WikiEditPageTip|Tip: You can move this page by adding the path to the beginning of the title.")
= link_to icon('question-circle'), help_page_path('user/project/wiki/index', anchor: 'moving-a-wiki-page'), target: '_blank'
+ - else
+ = s_("WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories.")
.form-group.row
.col-sm-12= f.label :format, class: 'control-label-full-width'
.col-sm-12
@@ -43,7 +45,7 @@
.form-group.row
.col-sm-12= f.label :commit_message, class: 'control-label-full-width'
- .col-sm-12= f.text_field :message, class: 'form-control qa-wiki-message-textbox', rows: 18, value: commit_message
+ .col-sm-12= f.text_field :message, class: 'form-control qa-wiki-message-textbox', rows: 18, value: nil
.form-actions
- if @page && @page.persisted?
diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml
index 643b51e01d1..2e1e176c42a 100644
--- a/app/views/projects/wikis/_main_links.html.haml
+++ b/app/views/projects/wikis/_main_links.html.haml
@@ -1,9 +1,9 @@
- if (@page && @page.persisted?)
- if can?(current_user, :create_wiki, @project)
- = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-success", "data-toggle" => "modal" do
+ = link_to project_wikis_new_path(@project), class: "add-new-wiki btn btn-success", role: "button" do
= s_("Wiki|New page")
- = link_to project_wiki_history_path(@project, @page), class: "btn" do
+ = link_to project_wiki_history_path(@project, @page), class: "btn", role: "button" do
= s_("Wiki|Page history")
- if can?(current_user, :create_wiki, @project) && @page.latest? && @valid_encoding
- = link_to project_wiki_edit_path(@project, @page), class: "btn js-wiki-edit" do
+ = link_to project_wiki_edit_path(@project, @page), class: "btn js-wiki-edit", role: "button" do
= _("Edit")
diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml
deleted file mode 100644
index 2c675c0de9c..00000000000
--- a/app/views/projects/wikis/_new.html.haml
+++ /dev/null
@@ -1,18 +0,0 @@
-#modal-new-wiki.modal
- .modal-dialog
- .modal-content
- .modal-header
- %h3.page-title= s_("WikiNewPageTitle|New Wiki Page")
- %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
- %span{ "aria-hidden": true } &times;
- .modal-body
- %form.new-wiki-page
- .form-group
- = label_tag :new_wiki_path do
- %span= s_("WikiPage|Page slug")
- = text_field_tag :new_wiki_path, nil, placeholder: s_("WikiNewPagePlaceholder|how-to-setup"), class: 'form-control', required: true, :'data-wikis-path' => project_wikis_path(@project), autofocus: true
- %span.d-inline-block.mw-100.prepend-top-5
- = icon('lightbulb-o')
- = s_("WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories.")
- .form-actions
- = button_tag s_("Wiki|Create page"), class: "build-new-wiki btn btn-success"
diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml
index 28353927135..a9d21470944 100644
--- a/app/views/projects/wikis/_sidebar.html.haml
+++ b/app/views/projects/wikis/_sidebar.html.haml
@@ -19,5 +19,3 @@
.block
= link_to project_wikis_pages_path(@project), class: 'btn btn-block' do
= s_("Wiki|More Pages")
-
-= render 'projects/wikis/new'
diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml
index e8b59a3b8c4..815b4a51261 100644
--- a/app/views/projects/wikis/edit.html.haml
+++ b/app/views/projects/wikis/edit.html.haml
@@ -13,14 +13,11 @@
%h2.wiki-page-title
- if @page.persisted?
= link_to @page.human_title, project_wiki_path(@project, @page)
- - else
- = @page.human_title
- %span.light
- &middot;
- - if @page.persisted?
+ %span.light
+ &middot;
= s_("Wiki|Edit Page")
- - else
- = s_("Wiki|Create Page")
+ - else
+ = s_("Wiki|Create New Page")
.nav-controls
- if @page.persisted?
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 825088a58e7..aea09bf8d4d 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -38,7 +38,7 @@
= _('Milestone')
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
- = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right'
+ = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { track_label: "right_sidebar", track_property: "milestone", track_event: "click_edit_button", track_value: "" }
.value.hide-collapsed
- if milestone.present?
= link_to milestone[:title], milestone[:web_url], class: "bold has-tooltip", title: sidebar_milestone_remaining_days(milestone), data: { container: "body", html: 'true', boundary: 'viewport' }
@@ -66,7 +66,7 @@
= _('Due date')
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
- = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right'
+ = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { track_label: "right_sidebar", track_property: "due_date", track_event: "click_edit_button", track_value: "" }
.value.hide-collapsed
%span.value-content
- if issuable_sidebar[:due_date]
@@ -102,7 +102,7 @@
= _('Labels')
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
- = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link qa-edit-link-labels float-right'
+ = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link qa-edit-link-labels float-right', data: { track_label: "right_sidebar", track_property: "labels", track_event: "click_edit_button", track_value: "" }
.value.issuable-show-labels.dont-hide.hide-collapsed.qa-labels-block{ class: ("has-labels" if selected_labels.any?) }
- if selected_labels.any?
- selected_labels.each do |label_hash|
@@ -160,7 +160,7 @@
= custom_icon('icon_arrow_right')
.dropdown.sidebar-move-issue-dropdown.hide-collapsed
%button.btn.btn-default.btn-block.js-sidebar-dropdown-toggle.js-move-issue{ type: 'button',
- data: { toggle: 'dropdown', display: 'static' } }
+ data: { toggle: 'dropdown', display: 'static', track_label: "right_sidebar", track_property: "move_issue", track_event: "click_button", track_value: "" } }
= _('Move issue')
.dropdown-menu.dropdown-menu-selectable.dropdown-extended-height
= dropdown_title(_('Move issue'))
diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml
index ab01094ed6e..1dc538826dc 100644
--- a/app/views/shared/issuable/_sidebar_assignees.html.haml
+++ b/app/views/shared/issuable/_sidebar_assignees.html.haml
@@ -20,6 +20,8 @@
placeholder: _('Search users'),
data: { first_user: issuable_sidebar.dig(:current_user, :username),
current_user: true,
+ iid: issuable_sidebar[:iid],
+ issuable_type: issuable_type,
project_id: issuable_sidebar[:project_id],
author_id: issuable_sidebar[:author_id],
field_name: "#{issuable_type}[assignee_ids][]",
diff --git a/app/workers/ci/archive_traces_cron_worker.rb b/app/workers/ci/archive_traces_cron_worker.rb
index 75e68d0233a..ef2da729705 100644
--- a/app/workers/ci/archive_traces_cron_worker.rb
+++ b/app/workers/ci/archive_traces_cron_worker.rb
@@ -10,7 +10,7 @@ module Ci
# Archive stale live traces which still resides in redis or database
# This could happen when ArchiveTraceWorker sidekiq jobs were lost by receiving SIGKILL
# More details in https://gitlab.com/gitlab-org/gitlab-ce/issues/36791
- Ci::Build.finished.with_live_trace.find_each(batch_size: 100) do |build|
+ Ci::Build.with_stale_live_trace.find_each(batch_size: 100) do |build|
Ci::ArchiveTraceService.new.execute(build, worker_name: self.class.name)
end
end
diff --git a/changelogs/unreleased/10-adjust-copy-for-adding-additional-members.yml b/changelogs/unreleased/10-adjust-copy-for-adding-additional-members.yml
deleted file mode 100644
index f249eff572c..00000000000
--- a/changelogs/unreleased/10-adjust-copy-for-adding-additional-members.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adjust copy for adding additional members
-merge_request: 31726
-author:
-type: changed
diff --git a/changelogs/unreleased/10972-be-allow-restricting-group-members-by-a-domain-whitelist-ce.yml b/changelogs/unreleased/10972-be-allow-restricting-group-members-by-a-domain-whitelist-ce.yml
deleted file mode 100644
index d93e7634ae5..00000000000
--- a/changelogs/unreleased/10972-be-allow-restricting-group-members-by-a-domain-whitelist-ce.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add new table to store email domain per group
-merge_request: 31071
-author:
-type: added
diff --git a/changelogs/unreleased/11090-export-design-management-lfs-data.yml b/changelogs/unreleased/11090-export-design-management-lfs-data.yml
deleted file mode 100644
index 36b773124d7..00000000000
--- a/changelogs/unreleased/11090-export-design-management-lfs-data.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add support for exporting repository type data for LFS objects
-merge_request: 30830
-author:
-type: changed
diff --git a/changelogs/unreleased/12502-add-view-stats-to-cycle-analytics.yml b/changelogs/unreleased/12502-add-view-stats-to-cycle-analytics.yml
deleted file mode 100644
index ccfd929b6ba..00000000000
--- a/changelogs/unreleased/12502-add-view-stats-to-cycle-analytics.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Track page views for cycle analytics show page
-merge_request: 31717
-author:
-type: added
diff --git a/changelogs/unreleased/17276-breakage-in-displaying-svg-in-the-same-repository.yml b/changelogs/unreleased/17276-breakage-in-displaying-svg-in-the-same-repository.yml
deleted file mode 100644
index 93936d441e7..00000000000
--- a/changelogs/unreleased/17276-breakage-in-displaying-svg-in-the-same-repository.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix inline rendering of relative paths to SVGs from the current repository
-merge_request: 31352
-author:
-type: fixed
diff --git a/changelogs/unreleased/19186-redirect-wiki-git-route-to-wiki.yml b/changelogs/unreleased/19186-redirect-wiki-git-route-to-wiki.yml
deleted file mode 100644
index 705621d06f7..00000000000
--- a/changelogs/unreleased/19186-redirect-wiki-git-route-to-wiki.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Redirect from a project wiki git route to the project wiki home
-merge_request: 31085
-author:
-type: added
diff --git a/changelogs/unreleased/20137-starrers.yml b/changelogs/unreleased/20137-starrers.yml
deleted file mode 100644
index d597b06f224..00000000000
--- a/changelogs/unreleased/20137-starrers.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make starred projects and starrers of a project publicly visible
-merge_request: 24690
-author:
-type: added
diff --git a/changelogs/unreleased/21671-multiple-pipeline-status-api.yml b/changelogs/unreleased/21671-multiple-pipeline-status-api.yml
deleted file mode 100644
index b7b0f5fa0c7..00000000000
--- a/changelogs/unreleased/21671-multiple-pipeline-status-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Multiple pipeline support for Commit status
-merge_request: 30828
-author: Gaetan Semet
-type: changed
diff --git a/changelogs/unreleased/24705-multi-selection-for-delete-on-registry-page.yml b/changelogs/unreleased/24705-multi-selection-for-delete-on-registry-page.yml
deleted file mode 100644
index 5254bd36b9c..00000000000
--- a/changelogs/unreleased/24705-multi-selection-for-delete-on-registry-page.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Added multi-select deletion of container registry images
-merge_request: 30837
-author:
-type: other
diff --git a/changelogs/unreleased/26866-api-endpoint-to-list-the-docker-images-tags-of-a-group.yml b/changelogs/unreleased/26866-api-endpoint-to-list-the-docker-images-tags-of-a-group.yml
deleted file mode 100644
index adbd7971a14..00000000000
--- a/changelogs/unreleased/26866-api-endpoint-to-list-the-docker-images-tags-of-a-group.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Add API endpoints to return container repositories and tags from the group
- level
-merge_request: 30817
-author:
-type: added
diff --git a/changelogs/unreleased/30974-issue-search-by-number.yml b/changelogs/unreleased/30974-issue-search-by-number.yml
deleted file mode 100644
index da50ee32c83..00000000000
--- a/changelogs/unreleased/30974-issue-search-by-number.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: "Search issuables by iids"
-merge_request: 28302
-author: Riccardo Padovani
-type: fixed
diff --git a/changelogs/unreleased/31434-make-issue-boards-importable.yml b/changelogs/unreleased/31434-make-issue-boards-importable.yml
deleted file mode 100644
index fd270a236dc..00000000000
--- a/changelogs/unreleased/31434-make-issue-boards-importable.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make issue boards importable
-merge_request: 31434
-author: Jason Colyer
-type: changed
diff --git a/changelogs/unreleased/32032-html-code-shown-in-merge-request.yml b/changelogs/unreleased/32032-html-code-shown-in-merge-request.yml
new file mode 100644
index 00000000000..ffd58067784
--- /dev/null
+++ b/changelogs/unreleased/32032-html-code-shown-in-merge-request.yml
@@ -0,0 +1,5 @@
+---
+title: Fix HTML rendering for fast-forward rebases in merge request widget
+merge_request: 32032
+author:
+type: fixed
diff --git a/changelogs/unreleased/32495-improve-slack-notification-on-pipeline-status.yml b/changelogs/unreleased/32495-improve-slack-notification-on-pipeline-status.yml
deleted file mode 100644
index b7b39303c2e..00000000000
--- a/changelogs/unreleased/32495-improve-slack-notification-on-pipeline-status.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improve pipeline status Slack notifications
-merge_request: 27683
-author:
-type: added
diff --git a/changelogs/unreleased/34414-update-personal-access-token-scope-descriptions-to-reflect-registry-permissions.yml b/changelogs/unreleased/34414-update-personal-access-token-scope-descriptions-to-reflect-registry-permissions.yml
deleted file mode 100644
index f0cc7fe9b6d..00000000000
--- a/changelogs/unreleased/34414-update-personal-access-token-scope-descriptions-to-reflect-registry-permissions.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Updated the personal access token api scope description to reflect the permissions
- it grants
-merge_request: 31759
-author:
-type: other
diff --git a/changelogs/unreleased/39217-remove-kubernetes-service-integration.yml b/changelogs/unreleased/39217-remove-kubernetes-service-integration.yml
deleted file mode 100644
index e13e3e86a37..00000000000
--- a/changelogs/unreleased/39217-remove-kubernetes-service-integration.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove Kubernetes service integration page
-merge_request: 31365
-author:
-type: removed
diff --git a/changelogs/unreleased/43080-speed-up-deploy-keys.yml b/changelogs/unreleased/43080-speed-up-deploy-keys.yml
deleted file mode 100644
index 73c9a9e5f82..00000000000
--- a/changelogs/unreleased/43080-speed-up-deploy-keys.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Speed up loading and filtering deploy keys and their projects
-merge_request: 31384
-author:
-type: performance
diff --git a/changelogs/unreleased/44036-fix-someone-edited-the-issue-at-the-same-time-false-warning.yml b/changelogs/unreleased/44036-fix-someone-edited-the-issue-at-the-same-time-false-warning.yml
deleted file mode 100644
index 674d53286e6..00000000000
--- a/changelogs/unreleased/44036-fix-someone-edited-the-issue-at-the-same-time-false-warning.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix flashing conflict warning when editing issues
-merge_request: 31469
-author:
-type: fixed
diff --git a/changelogs/unreleased/46299-wiki-page-creation.yml b/changelogs/unreleased/46299-wiki-page-creation.yml
new file mode 100644
index 00000000000..2e8f2accf45
--- /dev/null
+++ b/changelogs/unreleased/46299-wiki-page-creation.yml
@@ -0,0 +1,5 @@
+---
+title: Remove wiki page slug dialog step when creating wiki page
+merge_request: 31362
+author:
+type: changed
diff --git a/changelogs/unreleased/47814-search-view-labels.yml b/changelogs/unreleased/47814-search-view-labels.yml
deleted file mode 100644
index b4f10150d13..00000000000
--- a/changelogs/unreleased/47814-search-view-labels.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Moved labels out of fields on Search page
-merge_request: 31137
-author:
-type: fixed
diff --git a/changelogs/unreleased/48717-rate-limit-raw-controller-show.yml b/changelogs/unreleased/48717-rate-limit-raw-controller-show.yml
deleted file mode 100644
index 38ee95a7553..00000000000
--- a/changelogs/unreleased/48717-rate-limit-raw-controller-show.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add Rate Request Limiter to RawController#show endpoint
-merge_request: 30635
-author:
-type: added
diff --git a/changelogs/unreleased/50020-allow-email-notifications-to-be-disabled-for-all-users-of-a-group.yml b/changelogs/unreleased/50020-allow-email-notifications-to-be-disabled-for-all-users-of-a-group.yml
deleted file mode 100644
index 9137e9339aa..00000000000
--- a/changelogs/unreleased/50020-allow-email-notifications-to-be-disabled-for-all-users-of-a-group.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow email notifications to be disabled for all members of a group or project
-merge_request: 30755
-author: Dustin Spicuzza
-type: added
diff --git a/changelogs/unreleased/50020-fe-allow-email-notifications-to-be-disabled-for-all-users-of-a-group.yml b/changelogs/unreleased/50020-fe-allow-email-notifications-to-be-disabled-for-all-users-of-a-group.yml
deleted file mode 100644
index a5fe7b1d18e..00000000000
--- a/changelogs/unreleased/50020-fe-allow-email-notifications-to-be-disabled-for-all-users-of-a-group.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: UI for disabling group/project email notifications
-merge_request: 30961
-author: Dustin Spicuzza
-type: added
diff --git a/changelogs/unreleased/50070-legacy-attachments.yml b/changelogs/unreleased/50070-legacy-attachments.yml
deleted file mode 100644
index 03f1cec0f67..00000000000
--- a/changelogs/unreleased/50070-legacy-attachments.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Create rake tasks for migrating legacy uploads out of deprecated paths
-merge_request: 29409
-author:
-type: other
diff --git a/changelogs/unreleased/50130-cluster-cluster-details-update-automatically-after-cluster-is-created.yml b/changelogs/unreleased/50130-cluster-cluster-details-update-automatically-after-cluster-is-created.yml
deleted file mode 100644
index dc718572cfb..00000000000
--- a/changelogs/unreleased/50130-cluster-cluster-details-update-automatically-after-cluster-is-created.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update cluster page automatically when cluster is created
-merge_request: 27189
-author:
-type: changed
diff --git a/changelogs/unreleased/52494-separate-namespace-per-project-environment-slug.yml b/changelogs/unreleased/52494-separate-namespace-per-project-environment-slug.yml
deleted file mode 100644
index 645c92127a3..00000000000
--- a/changelogs/unreleased/52494-separate-namespace-per-project-environment-slug.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use separate Kubernetes namespaces per environment
-merge_request: 30711
-author:
-type: added
diff --git a/changelogs/unreleased/55564-remove-if-in-before-after-action.yml b/changelogs/unreleased/55564-remove-if-in-before-after-action.yml
deleted file mode 100644
index a787faa8a9c..00000000000
--- a/changelogs/unreleased/55564-remove-if-in-before-after-action.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Rewrite `if:` argument in before_action and alike when `only:` is also used
-merge_request: 24412
-author: George Thomas @thegeorgeous
-type: other
diff --git a/changelogs/unreleased/56100-make-quick-action-commands-applied-banner-more-useful.yml b/changelogs/unreleased/56100-make-quick-action-commands-applied-banner-more-useful.yml
deleted file mode 100644
index a2fa07c6ed2..00000000000
--- a/changelogs/unreleased/56100-make-quick-action-commands-applied-banner-more-useful.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make quick action commands applied banner more useful
-merge_request: 26672
-author: Jacopo Beschi @jacopo-beschi
-type: added
diff --git a/changelogs/unreleased/56130-deployed_at.yml b/changelogs/unreleased/56130-deployed_at.yml
new file mode 100644
index 00000000000..c53658de752
--- /dev/null
+++ b/changelogs/unreleased/56130-deployed_at.yml
@@ -0,0 +1,5 @@
+---
+title: Replace finished_at with deployed_at for the internal API Deployment entity
+merge_request: 32000
+author:
+type: other
diff --git a/changelogs/unreleased/56130-deployment-date.yml b/changelogs/unreleased/56130-deployment-date.yml
deleted file mode 100644
index 7d1e84bbaa4..00000000000
--- a/changelogs/unreleased/56130-deployment-date.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add finished_at to the internal API Deployment entity
-merge_request: 31808
-author:
-type: other
diff --git a/changelogs/unreleased/57953-fix-unfolded-diff-suggestions.yml b/changelogs/unreleased/57953-fix-unfolded-diff-suggestions.yml
deleted file mode 100644
index f634c0cd98a..00000000000
--- a/changelogs/unreleased/57953-fix-unfolded-diff-suggestions.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix suggestion on lines that are not part of an MR
-merge_request: 30606
-author:
-type: fixed
diff --git a/changelogs/unreleased/58035-expand-mr-diff.yml b/changelogs/unreleased/58035-expand-mr-diff.yml
deleted file mode 100644
index 7163cce29f2..00000000000
--- a/changelogs/unreleased/58035-expand-mr-diff.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add new expansion options for merge request diffs
-merge_request: 30927
-author:
-type: added
diff --git a/changelogs/unreleased/58256-incorrect-empty-state-message-displayed-on-explore-projects-tab.yml b/changelogs/unreleased/58256-incorrect-empty-state-message-displayed-on-explore-projects-tab.yml
deleted file mode 100644
index f719338b9cb..00000000000
--- a/changelogs/unreleased/58256-incorrect-empty-state-message-displayed-on-explore-projects-tab.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Resolve Incorrect empty state message on Explore projects
-merge_request: 25578
-author:
-type: fixed
diff --git a/changelogs/unreleased/59325-units-are-not-shown-on-the-performance-dashboard-2.yml b/changelogs/unreleased/59325-units-are-not-shown-on-the-performance-dashboard-2.yml
deleted file mode 100644
index 38cfa0f273e..00000000000
--- a/changelogs/unreleased/59325-units-are-not-shown-on-the-performance-dashboard-2.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'fix: updates to include units for the y axis label'
-merge_request: 30330
-author:
-type: fixed
diff --git a/changelogs/unreleased/59521-job-sidebar-has-a-blank-block.yml b/changelogs/unreleased/59521-job-sidebar-has-a-blank-block.yml
deleted file mode 100644
index 4c93a108f2b..00000000000
--- a/changelogs/unreleased/59521-job-sidebar-has-a-blank-block.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove blank block from job sidebar
-merge_request: 30754
-author:
-type: fixed
diff --git a/changelogs/unreleased/59590-keyboard-shortcut-for-jump-to-next-unresolved-discussion.yml b/changelogs/unreleased/59590-keyboard-shortcut-for-jump-to-next-unresolved-discussion.yml
deleted file mode 100644
index 02e81c7fc87..00000000000
--- a/changelogs/unreleased/59590-keyboard-shortcut-for-jump-to-next-unresolved-discussion.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Resolve Keyboard shortcut for jump to NEXT unresolved discussion
-merge_request: 30144
-author:
-type: added
diff --git a/changelogs/unreleased/59712-resolve-the-search-problem-issue.yml b/changelogs/unreleased/59712-resolve-the-search-problem-issue.yml
deleted file mode 100644
index 964962cb817..00000000000
--- a/changelogs/unreleased/59712-resolve-the-search-problem-issue.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add branch/tags/commits dropdown filter on the search page for searching codes
-merge_request: 28282
-author: minghuan lei
-type: changed
diff --git a/changelogs/unreleased/59786-show-renamed-file-in-mr.yml b/changelogs/unreleased/59786-show-renamed-file-in-mr.yml
new file mode 100644
index 00000000000..e8c52b592d2
--- /dev/null
+++ b/changelogs/unreleased/59786-show-renamed-file-in-mr.yml
@@ -0,0 +1,5 @@
+---
+title: Fix to show renamed file in mr
+merge_request: 31888
+author:
+type: changed
diff --git a/changelogs/unreleased/59829-fix-style-lint-wiki.yml b/changelogs/unreleased/59829-fix-style-lint-wiki.yml
deleted file mode 100644
index 48242a77c6b..00000000000
--- a/changelogs/unreleased/59829-fix-style-lint-wiki.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix the style-lint errors and warnings for `app/assets/stylesheets/pages/wiki.scss`
-merge_request: 31656
-author:
-type: other
diff --git a/changelogs/unreleased/60449-reduce-gitaly-calls-when-rendering-commits-in-md.yml b/changelogs/unreleased/60449-reduce-gitaly-calls-when-rendering-commits-in-md.yml
deleted file mode 100644
index ef11e8743f6..00000000000
--- a/changelogs/unreleased/60449-reduce-gitaly-calls-when-rendering-commits-in-md.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Batch processing of commit refs in markdown processing
-merge_request: 31037
-author:
-type: performance
diff --git a/changelogs/unreleased/60516-uninstall-tiller.yml b/changelogs/unreleased/60516-uninstall-tiller.yml
deleted file mode 100644
index db25e7b3338..00000000000
--- a/changelogs/unreleased/60516-uninstall-tiller.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow Helm to be uninstalled from the UI
-merge_request: 27359
-author:
-type: added
diff --git a/changelogs/unreleased/60664-kubernetes-applications-uninstall-cert-manager.yml b/changelogs/unreleased/60664-kubernetes-applications-uninstall-cert-manager.yml
deleted file mode 100644
index efc3ec241e2..00000000000
--- a/changelogs/unreleased/60664-kubernetes-applications-uninstall-cert-manager.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow Cert-Manager to be uninstalled
-merge_request: 31166
-author:
-type: added
diff --git a/changelogs/unreleased/60668-kubernetes-applications-uninstall-knative.yml b/changelogs/unreleased/60668-kubernetes-applications-uninstall-knative.yml
deleted file mode 100644
index c33dc0f50cd..00000000000
--- a/changelogs/unreleased/60668-kubernetes-applications-uninstall-knative.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow Knative to be uninstalled from the UI
-merge_request: 30458
-author:
-type: added
diff --git a/changelogs/unreleased/60948-display-groupid-on-group-admin-page.yml b/changelogs/unreleased/60948-display-groupid-on-group-admin-page.yml
deleted file mode 100644
index 17763b4c69e..00000000000
--- a/changelogs/unreleased/60948-display-groupid-on-group-admin-page.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Display group id on group admin page
-merge_request: 29735
-author: Zsolt Kovari
-type: added
diff --git a/changelogs/unreleased/60949-display-projectid-on-project-admin-page.yml b/changelogs/unreleased/60949-display-projectid-on-project-admin-page.yml
deleted file mode 100644
index 3ff83ede2fa..00000000000
--- a/changelogs/unreleased/60949-display-projectid-on-project-admin-page.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Display project id on project admin page
-merge_request: 29734
-author: Zsolt Kovari
-type: added
diff --git a/changelogs/unreleased/61207-adjusted-hoverable-area-in-sidebar.yml b/changelogs/unreleased/61207-adjusted-hoverable-area-in-sidebar.yml
deleted file mode 100644
index 99fc817d703..00000000000
--- a/changelogs/unreleased/61207-adjusted-hoverable-area-in-sidebar.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: "Adjusted the clickable area of collapsed sidebar elements"
-merge_request: 30974
-author: Michel Engelen
-type: changed
diff --git a/changelogs/unreleased/61332-web-ide-mr-branch-dropdown-closes-unexpectedly.yml b/changelogs/unreleased/61332-web-ide-mr-branch-dropdown-closes-unexpectedly.yml
deleted file mode 100644
index 1f5e507d48d..00000000000
--- a/changelogs/unreleased/61332-web-ide-mr-branch-dropdown-closes-unexpectedly.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix an issue where clicking outside the MR/branch search box in WebIDE closed the dropdown.
-merge_request: 31523
-author:
-type: fixed
diff --git a/changelogs/unreleased/61335-fix-file-icon-status.yml b/changelogs/unreleased/61335-fix-file-icon-status.yml
deleted file mode 100644
index d524d91b246..00000000000
--- a/changelogs/unreleased/61335-fix-file-icon-status.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix IDE new files icon in tree
-merge_request: 31560
-author:
-type: fixed
diff --git a/changelogs/unreleased/61445-prevent-persisting-auto-switch-discussion-filter.yml b/changelogs/unreleased/61445-prevent-persisting-auto-switch-discussion-filter.yml
deleted file mode 100644
index 58e29212462..00000000000
--- a/changelogs/unreleased/61445-prevent-persisting-auto-switch-discussion-filter.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Prevent discussion filter from persisting to `Show all activity` when opening
- links to notes
-merge_request: 31229
-author:
-type: fixed
diff --git a/changelogs/unreleased/61776-fixing-the-U2F-warning-message-text-colour.yml b/changelogs/unreleased/61776-fixing-the-U2F-warning-message-text-colour.yml
deleted file mode 100644
index 988eb77db12..00000000000
--- a/changelogs/unreleased/61776-fixing-the-U2F-warning-message-text-colour.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove the warning style from the U2F device message in user settings > account
-merge_request: 30119
-author: matejlatin
-type: other
diff --git a/changelogs/unreleased/61787-broadcast-messages-colour-selector-provide-default-options-with-descriptive-labels.yml b/changelogs/unreleased/61787-broadcast-messages-colour-selector-provide-default-options-with-descriptive-labels.yml
deleted file mode 100644
index ec6e9c5aff8..00000000000
--- a/changelogs/unreleased/61787-broadcast-messages-colour-selector-provide-default-options-with-descriptive-labels.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: add color selector to broadcast messages form
-merge_request: 30988
-author:
-type: other
diff --git a/changelogs/unreleased/62137-add-tooltip-to-improve-clarity-of-detached-label-state-in-the-merge-request-pipeline.yml b/changelogs/unreleased/62137-add-tooltip-to-improve-clarity-of-detached-label-state-in-the-merge-request-pipeline.yml
deleted file mode 100644
index ccc3195e6ae..00000000000
--- a/changelogs/unreleased/62137-add-tooltip-to-improve-clarity-of-detached-label-state-in-the-merge-request-pipeline.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Updated the detached pipeline badge tooltip text to offer a better explanation
-merge_request: 31626
-author:
-type: other
diff --git a/changelogs/unreleased/62286-Consistent-selection-elements-in-user-settings-preferences.yml b/changelogs/unreleased/62286-Consistent-selection-elements-in-user-settings-preferences.yml
deleted file mode 100644
index 10f2b7eaed5..00000000000
--- a/changelogs/unreleased/62286-Consistent-selection-elements-in-user-settings-preferences.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Harmonize selections in user settings
-merge_request: 31110
-author: Marc Schwede
-type: other
diff --git a/changelogs/unreleased/62609-test-summary-loading-spinner-is-too-large.yml b/changelogs/unreleased/62609-test-summary-loading-spinner-is-too-large.yml
deleted file mode 100644
index f8e4a26dad8..00000000000
--- a/changelogs/unreleased/62609-test-summary-loading-spinner-is-too-large.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adjust size and align MR-widget loading icon
-merge_request: 31503
-author:
-type: fixed
diff --git a/changelogs/unreleased/62971-embed-specific-metrics-chart-in-issue.yml b/changelogs/unreleased/62971-embed-specific-metrics-chart-in-issue.yml
deleted file mode 100644
index b6bc03f4003..00000000000
--- a/changelogs/unreleased/62971-embed-specific-metrics-chart-in-issue.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Embed specific metrics chart in issue
-merge_request: 31644
-author:
-type: added
diff --git a/changelogs/unreleased/62973-specify-time-frame-in-shareable-link-for-embedding-metrics.yml b/changelogs/unreleased/62973-specify-time-frame-in-shareable-link-for-embedding-metrics.yml
deleted file mode 100644
index aaf0ddfa48d..00000000000
--- a/changelogs/unreleased/62973-specify-time-frame-in-shareable-link-for-embedding-metrics.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow links to metrics dashboard at a specific time
-merge_request: 31283
-author:
-type: added
diff --git a/changelogs/unreleased/63181-collapsible-line.yml b/changelogs/unreleased/63181-collapsible-line.yml
deleted file mode 100644
index c13d4eeab6c..00000000000
--- a/changelogs/unreleased/63181-collapsible-line.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Makes collapsible title clickable in job log
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/63438-oauth2-support-with-gitlab-personal-access-token.yml b/changelogs/unreleased/63438-oauth2-support-with-gitlab-personal-access-token.yml
deleted file mode 100644
index 815010e15ae..00000000000
--- a/changelogs/unreleased/63438-oauth2-support-with-gitlab-personal-access-token.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Personal access tokens are accepted using OAuth2 header format
-merge_request: 30277
-author:
-type: added
diff --git a/changelogs/unreleased/63485-fix-pipeline-emails-to-use-group-setting.yml b/changelogs/unreleased/63485-fix-pipeline-emails-to-use-group-setting.yml
deleted file mode 100644
index c3ee3108216..00000000000
--- a/changelogs/unreleased/63485-fix-pipeline-emails-to-use-group-setting.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix pipeline emails not respecting group notification email setting
-merge_request: 30907
-author:
-type: fixed
diff --git a/changelogs/unreleased/63547-add-system-notes-for-when-a-zoom-call-was-added-removed-from-an-issue.yml b/changelogs/unreleased/63547-add-system-notes-for-when-a-zoom-call-was-added-removed-from-an-issue.yml
deleted file mode 100644
index 387c01dc135..00000000000
--- a/changelogs/unreleased/63547-add-system-notes-for-when-a-zoom-call-was-added-removed-from-an-issue.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add system notes for when a Zoom call was added/removed from an issue
-merge_request: 30857
-author: Jacopo Beschi @jacopo-beschi
-type: added
diff --git a/changelogs/unreleased/63568-access-email-notifications-custom-email.yml b/changelogs/unreleased/63568-access-email-notifications-custom-email.yml
deleted file mode 100644
index ece6442d7cf..00000000000
--- a/changelogs/unreleased/63568-access-email-notifications-custom-email.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Respect group notification email when sending group access notifications
-merge_request: 31089
-author:
-type: fixed
diff --git a/changelogs/unreleased/63571-fix-gc-profiler-data-being-wiped.yml b/changelogs/unreleased/63571-fix-gc-profiler-data-being-wiped.yml
deleted file mode 100644
index 7943d9573f3..00000000000
--- a/changelogs/unreleased/63571-fix-gc-profiler-data-being-wiped.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix GC::Profiler metrics fetching
-merge_request: 31331
-author:
-type: fixed
diff --git a/changelogs/unreleased/63671-remove-extra-padding-from-the-disabled-comment-area.yml b/changelogs/unreleased/63671-remove-extra-padding-from-the-disabled-comment-area.yml
deleted file mode 100644
index ab116674ced..00000000000
--- a/changelogs/unreleased/63671-remove-extra-padding-from-the-disabled-comment-area.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove extra padding from disabled comment box
-merge_request: 31603
-author:
-type: fixed
diff --git a/changelogs/unreleased/63730-fix-500-status-labels-pd.yml b/changelogs/unreleased/63730-fix-500-status-labels-pd.yml
deleted file mode 100644
index a1e2ae0e5df..00000000000
--- a/changelogs/unreleased/63730-fix-500-status-labels-pd.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix admin labels page when there are invalid records
-merge_request: 30885
-author:
-type: fixed
diff --git a/changelogs/unreleased/63833-fix-jira-issues-url.yml b/changelogs/unreleased/63833-fix-jira-issues-url.yml
deleted file mode 100644
index 24d6bca3842..00000000000
--- a/changelogs/unreleased/63833-fix-jira-issues-url.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Handle trailing slashes when generating Jira issue URLs
-merge_request: 30911
-author:
-type: fixed
diff --git a/changelogs/unreleased/63888-snippets-usage-ping-for-create-smau.yml b/changelogs/unreleased/63888-snippets-usage-ping-for-create-smau.yml
deleted file mode 100644
index 1a5a552b120..00000000000
--- a/changelogs/unreleased/63888-snippets-usage-ping-for-create-smau.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Count snippet creation, update and comment events
-merge_request: 30930
-author:
-type: added
diff --git a/changelogs/unreleased/63942-remove-config-action_dispatch-use_authenticated_cookie_encryption-configuration.yml b/changelogs/unreleased/63942-remove-config-action_dispatch-use_authenticated_cookie_encryption-configuration.yml
deleted file mode 100644
index 741763403a5..00000000000
--- a/changelogs/unreleased/63942-remove-config-action_dispatch-use_authenticated_cookie_encryption-configuration.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Enable authenticated cookie encryption
-merge_request: 31463
-author:
-type: other
diff --git a/changelogs/unreleased/64081-override-helm-release-name.yml b/changelogs/unreleased/64081-override-helm-release-name.yml
deleted file mode 100644
index 2bf39b17c03..00000000000
--- a/changelogs/unreleased/64081-override-helm-release-name.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow multiple Auto DevOps projects to deploy to a single namespace within a k8s cluster
-merge_request: 30360
-author: James Keogh
-type: added
diff --git a/changelogs/unreleased/64092-removes-update-statistics-namespace-feature-flag.yml b/changelogs/unreleased/64092-removes-update-statistics-namespace-feature-flag.yml
deleted file mode 100644
index 272c830a914..00000000000
--- a/changelogs/unreleased/64092-removes-update-statistics-namespace-feature-flag.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Enables storage statistics for root namespaces on database
-merge_request: 31392
-author:
-type: other
diff --git a/changelogs/unreleased/64160-fix-duplicate-buttons.yml b/changelogs/unreleased/64160-fix-duplicate-buttons.yml
deleted file mode 100644
index 12416a611ed..00000000000
--- a/changelogs/unreleased/64160-fix-duplicate-buttons.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove duplicate buttons in diff discussion
-merge_request: 30757
-author:
-type: fixed
diff --git a/changelogs/unreleased/64180-membersfinder-contains-slow-database-query-with-or-conditions.yml b/changelogs/unreleased/64180-membersfinder-contains-slow-database-query-with-or-conditions.yml
deleted file mode 100644
index f86c63a15b6..00000000000
--- a/changelogs/unreleased/64180-membersfinder-contains-slow-database-query-with-or-conditions.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improve MembersFinder query performance using UNION
-merge_request: 30451
-author: Jacopo Beschi @jacopo-beschi
-type: performance
diff --git a/changelogs/unreleased/64190-add-mr-form.yml b/changelogs/unreleased/64190-add-mr-form.yml
deleted file mode 100644
index 08340d01fd8..00000000000
--- a/changelogs/unreleased/64190-add-mr-form.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add MR form to Visual Review (EE) runtime configuration
-merge_request: 30481
-author:
-type: changed
diff --git a/changelogs/unreleased/64257-active_session_lookup_key_cleanup.yml b/changelogs/unreleased/64257-active_session_lookup_key_cleanup.yml
deleted file mode 100644
index df3cd98830e..00000000000
--- a/changelogs/unreleased/64257-active_session_lookup_key_cleanup.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Rake task to cleanup expired ActiveSession lookup keys
-merge_request: 30668
-author:
-type: performance
diff --git a/changelogs/unreleased/64257-warden_set_user_fix.yml b/changelogs/unreleased/64257-warden_set_user_fix.yml
deleted file mode 100644
index 7b6818876fb..00000000000
--- a/changelogs/unreleased/64257-warden_set_user_fix.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Ensure Warden triggers after_authentication callback
-merge_request: 31138
-author:
-type: fixed
diff --git a/changelogs/unreleased/64265-center-loading-icon.yml b/changelogs/unreleased/64265-center-loading-icon.yml
deleted file mode 100644
index cd4253b63c6..00000000000
--- a/changelogs/unreleased/64265-center-loading-icon.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Center loading icon in CI action component
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/64295-predictable-environment-slugs.yml b/changelogs/unreleased/64295-predictable-environment-slugs.yml
deleted file mode 100644
index de581b2b8e1..00000000000
--- a/changelogs/unreleased/64295-predictable-environment-slugs.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use predictable environment slugs
-merge_request: 30551
-author:
-type: added
diff --git a/changelogs/unreleased/64341-user-callout-deferred-link-support.yml b/changelogs/unreleased/64341-user-callout-deferred-link-support.yml
deleted file mode 100644
index 05230ddc124..00000000000
--- a/changelogs/unreleased/64341-user-callout-deferred-link-support.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add support for deferred links in persistent user callouts.
-merge_request: 30818
-author:
-type: added
diff --git a/changelogs/unreleased/64383-pattern-matching-with-variables-causes-gitlabs-ci-lint-to-throw-500.yml b/changelogs/unreleased/64383-pattern-matching-with-variables-causes-gitlabs-ci-lint-to-throw-500.yml
new file mode 100644
index 00000000000..ba4bd614170
--- /dev/null
+++ b/changelogs/unreleased/64383-pattern-matching-with-variables-causes-gitlabs-ci-lint-to-throw-500.yml
@@ -0,0 +1,5 @@
+---
+title: Fix 500 errors caused by pattern matching with variables in CI Lint
+merge_request: 31719
+author:
+type: fixed
diff --git a/changelogs/unreleased/64608-double-tooltips.yml b/changelogs/unreleased/64608-double-tooltips.yml
deleted file mode 100644
index f6cb1944d26..00000000000
--- a/changelogs/unreleased/64608-double-tooltips.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Prevents showing 2 tooltips in pipelines table
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/64675-Dashboard-URL-legend-border.yml b/changelogs/unreleased/64675-Dashboard-URL-legend-border.yml
deleted file mode 100644
index f35261fcd6c..00000000000
--- a/changelogs/unreleased/64675-Dashboard-URL-legend-border.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Removed extrenal dashboard legend border
-merge_request: 31407
-author:
-type: fixed
diff --git a/changelogs/unreleased/64697-markdown-issues-checkbox-inside-blockquote-status-won-t-be-saved.yml b/changelogs/unreleased/64697-markdown-issues-checkbox-inside-blockquote-status-won-t-be-saved.yml
deleted file mode 100644
index 00664d64050..00000000000
--- a/changelogs/unreleased/64697-markdown-issues-checkbox-inside-blockquote-status-won-t-be-saved.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Better support clickable tasklists inside blockquotes
-merge_request: 30952
-author:
-type: fixed
diff --git a/changelogs/unreleased/64700-fix-the-color-of-the-visibility-icon-on-project-lists.yml b/changelogs/unreleased/64700-fix-the-color-of-the-visibility-icon-on-project-lists.yml
deleted file mode 100644
index 0d2fbaf01ed..00000000000
--- a/changelogs/unreleased/64700-fix-the-color-of-the-visibility-icon-on-project-lists.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Ensure visibility icons in group/project listings are grey
-merge_request: 30858
-author:
-type: fixed
diff --git a/changelogs/unreleased/64730-metrics-dashboard-menu-is-cramped-with-new-features-enabled.yml b/changelogs/unreleased/64730-metrics-dashboard-menu-is-cramped-with-new-features-enabled.yml
deleted file mode 100644
index c564b98bb41..00000000000
--- a/changelogs/unreleased/64730-metrics-dashboard-menu-is-cramped-with-new-features-enabled.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improve layout of dropdowns in the metrics dashboard page
-merge_request: 31239
-author:
-type: fixed
diff --git a/changelogs/unreleased/64746-Commit-authors-avatar-sretched-in-commit-view-if-no-image-is-loaded.yml b/changelogs/unreleased/64746-Commit-authors-avatar-sretched-in-commit-view-if-no-image-is-loaded.yml
deleted file mode 100644
index fb0f4cedc62..00000000000
--- a/changelogs/unreleased/64746-Commit-authors-avatar-sretched-in-commit-view-if-no-image-is-loaded.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed distorted avatars when resource not reachable
-merge_request: 30904
-author: Marc Schwede
-type: other
diff --git a/changelogs/unreleased/64763-fix-tags-page-layout.yml b/changelogs/unreleased/64763-fix-tags-page-layout.yml
deleted file mode 100644
index db6b1f31506..00000000000
--- a/changelogs/unreleased/64763-fix-tags-page-layout.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix tag page layout
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/64831-add-padding-to-merged-by-widget.yml b/changelogs/unreleased/64831-add-padding-to-merged-by-widget.yml
deleted file mode 100644
index 2a45eec78ef..00000000000
--- a/changelogs/unreleased/64831-add-padding-to-merged-by-widget.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add space to "merged by" widget
-merge_request: 30972
-author:
-type: fixed
diff --git a/changelogs/unreleased/64950-move-download-csv-button-functionality-in-metrics-dashboard-cards-i.yml b/changelogs/unreleased/64950-move-download-csv-button-functionality-in-metrics-dashboard-cards-i.yml
deleted file mode 100644
index 21771c76873..00000000000
--- a/changelogs/unreleased/64950-move-download-csv-button-functionality-in-metrics-dashboard-cards-i.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'feat: adds a download to csv functionality to the dropdown in prometheus metrics'
-merge_request: 31679
-author:
-type: changed
diff --git a/changelogs/unreleased/64972-fix-unicorn-workers-metric.yml b/changelogs/unreleased/64972-fix-unicorn-workers-metric.yml
deleted file mode 100644
index f80111f0bd9..00000000000
--- a/changelogs/unreleased/64972-fix-unicorn-workers-metric.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix pid discovery for Unicorn processes in `PidProvider`
-merge_request: 31056
-author:
-type: fixed
diff --git a/changelogs/unreleased/64974-remove-livesum-from-ruby-sampler-metrics.yml b/changelogs/unreleased/64974-remove-livesum-from-ruby-sampler-metrics.yml
deleted file mode 100644
index 4fa3b7783c5..00000000000
--- a/changelogs/unreleased/64974-remove-livesum-from-ruby-sampler-metrics.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove :livesum from RubySampler metrics
-merge_request: 31047
-author:
-type: fixed
diff --git a/changelogs/unreleased/65088-incorrect-message-interpolation-on-project-listing.yml b/changelogs/unreleased/65088-incorrect-message-interpolation-on-project-listing.yml
deleted file mode 100644
index dd74b8443bc..00000000000
--- a/changelogs/unreleased/65088-incorrect-message-interpolation-on-project-listing.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix incorrect use of message interpolation
-merge_request: 31121
-author:
-type: fixed
diff --git a/changelogs/unreleased/65263-manual-action.yml b/changelogs/unreleased/65263-manual-action.yml
deleted file mode 100644
index 47b2a2ed329..00000000000
--- a/changelogs/unreleased/65263-manual-action.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Hides loading spinner in pipelines actions after request has been fullfiled
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/65278-fix-puma-master-counter-wipe.yml b/changelogs/unreleased/65278-fix-puma-master-counter-wipe.yml
deleted file mode 100644
index fb9d6fa251d..00000000000
--- a/changelogs/unreleased/65278-fix-puma-master-counter-wipe.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix active metric files being wiped after the app starts
-merge_request: 31668
-author:
-type: fixed
diff --git a/changelogs/unreleased/65412-add-support-for-line-charts.yml b/changelogs/unreleased/65412-add-support-for-line-charts.yml
new file mode 100644
index 00000000000..cb9043596b7
--- /dev/null
+++ b/changelogs/unreleased/65412-add-support-for-line-charts.yml
@@ -0,0 +1,5 @@
+---
+title: Create component to display area and line charts in monitor dashboards
+merge_request: 31639
+author:
+type: added
diff --git a/changelogs/unreleased/65427-improve-system-notes-for-zoom-links.yml b/changelogs/unreleased/65427-improve-system-notes-for-zoom-links.yml
new file mode 100644
index 00000000000..d081620c710
--- /dev/null
+++ b/changelogs/unreleased/65427-improve-system-notes-for-zoom-links.yml
@@ -0,0 +1,5 @@
+---
+title: Improve system notes for Zoom links
+merge_request: 31410
+author: Jacopo Beschi @jacopo-beschi
+type: changed
diff --git a/changelogs/unreleased/65483-add-a-resend-confirmation-link.yml b/changelogs/unreleased/65483-add-a-resend-confirmation-link.yml
deleted file mode 100644
index a5f62dbcd56..00000000000
--- a/changelogs/unreleased/65483-add-a-resend-confirmation-link.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow users to resend a confirmation link when the grace period has expired
-merge_request: 31476
-author:
-type: changed
diff --git a/changelogs/unreleased/65530-add-externalization-and-fix-regression-in-shortcuts-helper-modal.yml b/changelogs/unreleased/65530-add-externalization-and-fix-regression-in-shortcuts-helper-modal.yml
deleted file mode 100644
index fc29a514c42..00000000000
--- a/changelogs/unreleased/65530-add-externalization-and-fix-regression-in-shortcuts-helper-modal.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Fixed display of some sections and externalized all text in the shortcuts modal
- overlay
-merge_request: 31594
-author:
-type: fixed
diff --git a/changelogs/unreleased/65660-update-karma-to-4-2-0.yml b/changelogs/unreleased/65660-update-karma-to-4-2-0.yml
deleted file mode 100644
index c0cb40ce169..00000000000
--- a/changelogs/unreleased/65660-update-karma-to-4-2-0.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update karma to 4.2.0
-merge_request: 31495
-author: Takuya Noguchi
-type: other
diff --git a/changelogs/unreleased/65671-update-mini_magick-to-4-9-5.yml b/changelogs/unreleased/65671-update-mini_magick-to-4-9-5.yml
deleted file mode 100644
index a6f8576ae0b..00000000000
--- a/changelogs/unreleased/65671-update-mini_magick-to-4-9-5.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update mini_magick to 4.9.5
-merge_request: 31505
-author: Takuya Noguchi
-type: security
diff --git a/changelogs/unreleased/65700-document-max-replication-slots-pg-ha.yml b/changelogs/unreleased/65700-document-max-replication-slots-pg-ha.yml
deleted file mode 100644
index bacc3f5fc11..00000000000
--- a/changelogs/unreleased/65700-document-max-replication-slots-pg-ha.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add max_replication_slots to PG HA documentation
-merge_request: 31534
-author:
-type: other
diff --git a/changelogs/unreleased/65705-two-buttons.yml b/changelogs/unreleased/65705-two-buttons.yml
deleted file mode 100644
index b92e28f9d68..00000000000
--- a/changelogs/unreleased/65705-two-buttons.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Prevent duplicated trigger action button
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/65790-highlight.yml b/changelogs/unreleased/65790-highlight.yml
deleted file mode 100644
index 2531a3730ed..00000000000
--- a/changelogs/unreleased/65790-highlight.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adds highlight to the collapsible section
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/65803-invalidate-branches-cache-on-refresh.yml b/changelogs/unreleased/65803-invalidate-branches-cache-on-refresh.yml
deleted file mode 100644
index 217db8aa05a..00000000000
--- a/changelogs/unreleased/65803-invalidate-branches-cache-on-refresh.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Invalidate branches cache on PostReceive
-merge_request: 31653
-author:
-type: fixed
diff --git a/changelogs/unreleased/66008-fix-project-image-in-slack-notifications.yml b/changelogs/unreleased/66008-fix-project-image-in-slack-notifications.yml
deleted file mode 100644
index df0ac649ac1..00000000000
--- a/changelogs/unreleased/66008-fix-project-image-in-slack-notifications.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix project avatar image in Slack pipeline notifications
-merge_request: 31788
-author:
-type: fixed
diff --git a/changelogs/unreleased/66023-starrers-count-do-not-match-after-searching.yml b/changelogs/unreleased/66023-starrers-count-do-not-match-after-searching.yml
deleted file mode 100644
index 1caa5fa84ce..00000000000
--- a/changelogs/unreleased/66023-starrers-count-do-not-match-after-searching.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix starrers counts after searching
-merge_request: 31823
-author:
-type: fixed
diff --git a/changelogs/unreleased/66066-dark-theme-style-for-expansion-on-mr-diffs.yml b/changelogs/unreleased/66066-dark-theme-style-for-expansion-on-mr-diffs.yml
new file mode 100644
index 00000000000..13607ae938a
--- /dev/null
+++ b/changelogs/unreleased/66066-dark-theme-style-for-expansion-on-mr-diffs.yml
@@ -0,0 +1,5 @@
+---
+title: Add syntax highlighting for line expansion
+merge_request: 31821
+author:
+type: fixed
diff --git a/changelogs/unreleased/FixLocaleEN.yml b/changelogs/unreleased/FixLocaleEN.yml
deleted file mode 100644
index 49738a6d127..00000000000
--- a/changelogs/unreleased/FixLocaleEN.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove duplicated mapping key in config/locales/en.yml
-merge_request: 30980
-author: Peter Dave Hello
-type: fixed
diff --git a/changelogs/unreleased/GL-12412.yml b/changelogs/unreleased/GL-12412.yml
deleted file mode 100644
index 304bd63d150..00000000000
--- a/changelogs/unreleased/GL-12412.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add DS_PIP_DEPENDENCY_PATH option to configure Dependency Scanning for projects using pip.
-merge_request: 30762
-author:
-type: changed
diff --git a/changelogs/unreleased/GL-12757.yml b/changelogs/unreleased/GL-12757.yml
deleted file mode 100644
index e58ecf9259f..00000000000
--- a/changelogs/unreleased/GL-12757.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update the container scanning CI template to use v12 of the clair scanner.
-merge_request: 30809
-author:
-type: changed
diff --git a/changelogs/unreleased/ab-add-index-on-environments.yml b/changelogs/unreleased/ab-add-index-on-environments.yml
deleted file mode 100644
index 6c7641912f4..00000000000
--- a/changelogs/unreleased/ab-add-index-on-environments.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Create index on environments by state
-merge_request: 31231
-author:
-type: performance
diff --git a/changelogs/unreleased/ab-count-strategies.yml b/changelogs/unreleased/ab-count-strategies.yml
deleted file mode 100644
index bd95ff45d6f..00000000000
--- a/changelogs/unreleased/ab-count-strategies.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use tablesample approximate counting by default.
-merge_request: 31048
-author:
-type: performance
diff --git a/changelogs/unreleased/add-caching-to-archive-endpoint.yml b/changelogs/unreleased/add-caching-to-archive-endpoint.yml
deleted file mode 100644
index 770ec16e804..00000000000
--- a/changelogs/unreleased/add-caching-to-archive-endpoint.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Return an ETag header for the archive endpoint
-merge_request: 30581
-author:
-type: added
diff --git a/changelogs/unreleased/add-git-blame-api.yml b/changelogs/unreleased/add-git-blame-api.yml
deleted file mode 100644
index cdb77041433..00000000000
--- a/changelogs/unreleased/add-git-blame-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add git blame to GitLab API
-merge_request: 30675
-author: Oleg Zubchenko
-type: added
diff --git a/changelogs/unreleased/add-outbound-requests-whitelist-for-local-networks.yml b/changelogs/unreleased/add-outbound-requests-whitelist-for-local-networks.yml
deleted file mode 100644
index 9b50175f536..00000000000
--- a/changelogs/unreleased/add-outbound-requests-whitelist-for-local-networks.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add Outbound requests whitelist for local networks
-merge_request: 30350
-author: Istvan Szalai
-type: added
diff --git a/changelogs/unreleased/add-release-to-github-importer.yml b/changelogs/unreleased/add-release-to-github-importer.yml
deleted file mode 100644
index d11e7c725f7..00000000000
--- a/changelogs/unreleased/add-release-to-github-importer.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add a field for released_at to GH importer
-merge_request: 31496
-author:
-type: fixed
diff --git a/changelogs/unreleased/add-support-for-start-sha-to-commits-api.yml b/changelogs/unreleased/add-support-for-start-sha-to-commits-api.yml
deleted file mode 100644
index f810c2c5ada..00000000000
--- a/changelogs/unreleased/add-support-for-start-sha-to-commits-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add support for start_sha to commits API
-merge_request: 29598
-author:
-type: changed
diff --git a/changelogs/unreleased/add_links_to_latest_pipelines.yml b/changelogs/unreleased/add_links_to_latest_pipelines.yml
new file mode 100644
index 00000000000..333cd6c92b4
--- /dev/null
+++ b/changelogs/unreleased/add_links_to_latest_pipelines.yml
@@ -0,0 +1,5 @@
+---
+title: 'Add links for latest pipelines'
+merge_request: 20865
+author: Alex Ives
+type: added
diff --git a/changelogs/unreleased/adjust-group-level-analytics-to-accept-multiple-ids.yml b/changelogs/unreleased/adjust-group-level-analytics-to-accept-multiple-ids.yml
deleted file mode 100644
index 5e138e1059c..00000000000
--- a/changelogs/unreleased/adjust-group-level-analytics-to-accept-multiple-ids.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adjust group level analytics to accept multiple ids
-merge_request: 30744
-author:
-type: added
diff --git a/changelogs/unreleased/alipniagov-fix-wiki_can_not_be_created_total-counter.yml b/changelogs/unreleased/alipniagov-fix-wiki_can_not_be_created_total-counter.yml
deleted file mode 100644
index 58f969ed742..00000000000
--- a/changelogs/unreleased/alipniagov-fix-wiki_can_not_be_created_total-counter.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix :wiki_can_not_be_created_total counter
-merge_request: 31673
-author:
-type: fixed
diff --git a/changelogs/unreleased/allow-all-users-to-see-history.yml b/changelogs/unreleased/allow-all-users-to-see-history.yml
deleted file mode 100644
index 7423fa079cc..00000000000
--- a/changelogs/unreleased/allow-all-users-to-see-history.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Align access permissions for wiki history to those of wiki pages
-merge_request: 30470
-type: fixed
diff --git a/changelogs/unreleased/an-sidekiq-chaos.yml b/changelogs/unreleased/an-sidekiq-chaos.yml
deleted file mode 100644
index cede35c95cc..00000000000
--- a/changelogs/unreleased/an-sidekiq-chaos.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adds chaos endpoints to Sidekiq
-merge_request: 30814
-author:
-type: other
diff --git a/changelogs/unreleased/an-sidekiq-scheduling_latency.yml b/changelogs/unreleased/an-sidekiq-scheduling_latency.yml
deleted file mode 100644
index 2d6f462745e..00000000000
--- a/changelogs/unreleased/an-sidekiq-scheduling_latency.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adds Sidekiq scheduling latency structured logging field
-merge_request: 30784
-author:
-type: other
diff --git a/changelogs/unreleased/avoid-race-condition-of-archive-trace-cron-worker.yml b/changelogs/unreleased/avoid-race-condition-of-archive-trace-cron-worker.yml
new file mode 100644
index 00000000000..b3f84ec32d2
--- /dev/null
+++ b/changelogs/unreleased/avoid-race-condition-of-archive-trace-cron-worker.yml
@@ -0,0 +1,5 @@
+---
+title: Avoid conflicts between ArchiveTracesCronWorker and ArchiveTraceWorker
+merge_request: 31376
+author:
+type: fixed
diff --git a/changelogs/unreleased/bjk-64064_cache_metrics.yml b/changelogs/unreleased/bjk-64064_cache_metrics.yml
deleted file mode 100644
index c9baff7cb7b..00000000000
--- a/changelogs/unreleased/bjk-64064_cache_metrics.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adjust redis cache metrics
-merge_request: 30572
-author:
-type: changed
diff --git a/changelogs/unreleased/bjk-usage_ping.yml b/changelogs/unreleased/bjk-usage_ping.yml
deleted file mode 100644
index dee6c1ad291..00000000000
--- a/changelogs/unreleased/bjk-usage_ping.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update usage ping cron behavior
-merge_request: 30842
-author:
-type: performance
diff --git a/changelogs/unreleased/bring-scoped-environment-variables-to-core.yml b/changelogs/unreleased/bring-scoped-environment-variables-to-core.yml
deleted file mode 100644
index 48dfe662206..00000000000
--- a/changelogs/unreleased/bring-scoped-environment-variables-to-core.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Bring scoped environment variables to core
-merge_request: 30779
-author:
-type: changed
diff --git a/changelogs/unreleased/bump_helm_kubectl_gitlab.yml b/changelogs/unreleased/bump_helm_kubectl_gitlab.yml
deleted file mode 100644
index d768462e130..00000000000
--- a/changelogs/unreleased/bump_helm_kubectl_gitlab.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Bump Helm to 2.14.3 and kubectl to 1.11.10 for Kubernetes integration
-merge_request: 31716
-author:
-type: other
diff --git a/changelogs/unreleased/bvl-mark-remote-mirrors-as-failed-sooner.yml b/changelogs/unreleased/bvl-mark-remote-mirrors-as-failed-sooner.yml
deleted file mode 100644
index 1db0a4952b2..00000000000
--- a/changelogs/unreleased/bvl-mark-remote-mirrors-as-failed-sooner.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Mark push mirrors as failed after 1 hour
-merge_request: 30999
-author:
-type: changed
diff --git a/changelogs/unreleased/bvl-remote-mirror-exception-handling.yml b/changelogs/unreleased/bvl-remote-mirror-exception-handling.yml
deleted file mode 100644
index 962376086b0..00000000000
--- a/changelogs/unreleased/bvl-remote-mirror-exception-handling.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Retry push mirrors faster when running concurrently, improve error handling
- when push mirrors fail
-merge_request: 31247
-author:
-type: changed
diff --git a/changelogs/unreleased/bw-add-index-for-relative-position.yml b/changelogs/unreleased/bw-add-index-for-relative-position.yml
deleted file mode 100644
index 80ca20992e6..00000000000
--- a/changelogs/unreleased/bw-add-index-for-relative-position.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add index for issues on relative position, project, and state for manual sorting
-merge_request: 30542
-author:
-type: fixed
diff --git a/changelogs/unreleased/ce-22058-improve-ux-multi-assignees-in-mr.yml b/changelogs/unreleased/ce-22058-improve-ux-multi-assignees-in-mr.yml
new file mode 100644
index 00000000000..e7453a2b9bd
--- /dev/null
+++ b/changelogs/unreleased/ce-22058-improve-ux-multi-assignees-in-mr.yml
@@ -0,0 +1,5 @@
+---
+title: Update assignee (cannot merge) style
+merge_request: 31545
+author:
+type: changed
diff --git a/changelogs/unreleased/ce-xanf-add-links-to-admin-area.yml b/changelogs/unreleased/ce-xanf-add-links-to-admin-area.yml
deleted file mode 100644
index 9eb692c948b..00000000000
--- a/changelogs/unreleased/ce-xanf-add-links-to-admin-area.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add links to relevant configuration areas in admin area overview
-merge_request: 29306
-author:
-type: added
diff --git a/changelogs/unreleased/ci-config-on-policy.yml b/changelogs/unreleased/ci-config-on-policy.yml
new file mode 100644
index 00000000000..d9804f0323a
--- /dev/null
+++ b/changelogs/unreleased/ci-config-on-policy.yml
@@ -0,0 +1,5 @@
+---
+title: Introduced Build::Rules configuration for Ci::Build
+merge_request: 29011
+author:
+type: added
diff --git a/changelogs/unreleased/dblessing-fix-admin-user-radio-labels.yml b/changelogs/unreleased/dblessing-fix-admin-user-radio-labels.yml
deleted file mode 100644
index 4f119d46a1f..00000000000
--- a/changelogs/unreleased/dblessing-fix-admin-user-radio-labels.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix admin area user access level radio button labels
-merge_request: 31154
-author:
-type: fixed
diff --git a/changelogs/unreleased/dblessing-fix-public-project-ssh-only-ci-failure.yml b/changelogs/unreleased/dblessing-fix-public-project-ssh-only-ci-failure.yml
deleted file mode 100644
index 615a1571e95..00000000000
--- a/changelogs/unreleased/dblessing-fix-public-project-ssh-only-ci-failure.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow CI to clone public projects when HTTP protocol is disabled
-merge_request: 31632
-author:
-type: fixed
diff --git a/changelogs/unreleased/delete-designs-v2.yml b/changelogs/unreleased/delete-designs-v2.yml
deleted file mode 100644
index a678e4f93b9..00000000000
--- a/changelogs/unreleased/delete-designs-v2.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Adds event enum column to DesignsVersions join table
-merge_request: 30745
-type: added
diff --git a/changelogs/unreleased/dm-process-commit-worker-n-1.yml b/changelogs/unreleased/dm-process-commit-worker-n-1.yml
deleted file mode 100644
index 0bd7de6730a..00000000000
--- a/changelogs/unreleased/dm-process-commit-worker-n-1.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Look up upstream commits once before queuing ProcessCommitWorkers
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/double-slash-64592.yml b/changelogs/unreleased/double-slash-64592.yml
deleted file mode 100644
index e3b5b197ac5..00000000000
--- a/changelogs/unreleased/double-slash-64592.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Prevent double slash in review apps path
-merge_request: 31212
-author:
-type: fixed
diff --git a/changelogs/unreleased/ee-2502-refactor-ee-app-assets-javascripts-approvals-components-approvers_select-vue-to-remove-approverusers.yml b/changelogs/unreleased/ee-2502-refactor-ee-app-assets-javascripts-approvals-components-approvers_select-vue-to-remove-approverusers.yml
new file mode 100644
index 00000000000..dfa0f0cb593
--- /dev/null
+++ b/changelogs/unreleased/ee-2502-refactor-ee-app-assets-javascripts-approvals-components-approvers_select-vue-to-remove-approverusers.yml
@@ -0,0 +1,5 @@
+---
+title: 'Add new API method in Api.js: projectUsers'
+merge_request: 31801
+author:
+type: other
diff --git a/changelogs/unreleased/enable-specific-embeds.yml b/changelogs/unreleased/enable-specific-embeds.yml
deleted file mode 100644
index f2e591621a8..00000000000
--- a/changelogs/unreleased/enable-specific-embeds.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Enable embedding of specific metrics charts in GFM
-merge_request: 31304
-author:
-type: added
diff --git a/changelogs/unreleased/extract_auto_deploy_into_base_image.yml b/changelogs/unreleased/extract_auto_deploy_into_base_image.yml
deleted file mode 100644
index ff0d1f3bd71..00000000000
--- a/changelogs/unreleased/extract_auto_deploy_into_base_image.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Extract Auto DevOps deploy functions into a base image
-merge_request: 30404
-author:
-type: changed
diff --git a/changelogs/unreleased/fe-delete-old-boardservice.yml b/changelogs/unreleased/fe-delete-old-boardservice.yml
deleted file mode 100644
index bb06bfe80d5..00000000000
--- a/changelogs/unreleased/fe-delete-old-boardservice.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Change BoardService in favor of boardsStore on board blank state of the component
- board
-merge_request: 30546
-author: eduarmreyes
-type: other
diff --git a/changelogs/unreleased/feat-add-support-page-link-in-help-menu.yml b/changelogs/unreleased/feat-add-support-page-link-in-help-menu.yml
deleted file mode 100644
index 2cddd52212e..00000000000
--- a/changelogs/unreleased/feat-add-support-page-link-in-help-menu.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add admin-configurable "Support page URL" link to top Help dropdown menu
-merge_request: 30459
-author: Diego Louzán
-type: added
diff --git a/changelogs/unreleased/feat-smime-signed-notification-emails.yml b/changelogs/unreleased/feat-smime-signed-notification-emails.yml
new file mode 100644
index 00000000000..9672d0d964c
--- /dev/null
+++ b/changelogs/unreleased/feat-smime-signed-notification-emails.yml
@@ -0,0 +1,5 @@
+---
+title: Notification emails can be signed with SMIME
+merge_request: 30644
+author: Diego Louzán
+type: added
diff --git a/changelogs/unreleased/feature-gb-serverless-app-deployment-template.yml b/changelogs/unreleased/feature-gb-serverless-app-deployment-template.yml
deleted file mode 100644
index bd9001bd671..00000000000
--- a/changelogs/unreleased/feature-gb-serverless-app-deployment-template.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Deploy serverless apps with gitlabktl
-merge_request: 30740
-author:
-type: added
diff --git a/changelogs/unreleased/filter-title-description-and-body-from-logs.yml b/changelogs/unreleased/filter-title-description-and-body-from-logs.yml
deleted file mode 100644
index 8b592790629..00000000000
--- a/changelogs/unreleased/filter-title-description-and-body-from-logs.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Filter title, description, and body parameters from logs
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/fix-alignment-on-security-reports.yml b/changelogs/unreleased/fix-alignment-on-security-reports.yml
deleted file mode 100644
index 5339b6d764d..00000000000
--- a/changelogs/unreleased/fix-alignment-on-security-reports.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixes alignment issues with reports
-merge_request: 30839
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-bin-web-puma-script-to-consider-rails-env.yml b/changelogs/unreleased/fix-bin-web-puma-script-to-consider-rails-env.yml
deleted file mode 100644
index a0564369b02..00000000000
--- a/changelogs/unreleased/fix-bin-web-puma-script-to-consider-rails-env.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make `bin/web_puma` consider RAILS_ENV
-merge_request: 31378
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-commits-api-empty-refname.yml b/changelogs/unreleased/fix-commits-api-empty-refname.yml
deleted file mode 100644
index efdb950e45d..00000000000
--- a/changelogs/unreleased/fix-commits-api-empty-refname.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix 500 errors in commits api caused by empty ref_name parameter
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-job-log-formatting.yml b/changelogs/unreleased/fix-job-log-formatting.yml
deleted file mode 100644
index 0dd545aaecc..00000000000
--- a/changelogs/unreleased/fix-job-log-formatting.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix job logs where style changes were broken down into separate lines
-merge_request: 31674
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-name-vs-path-problem-for-cycle-analytics.yml b/changelogs/unreleased/fix-name-vs-path-problem-for-cycle-analytics.yml
deleted file mode 100644
index 7d171c2cf5b..00000000000
--- a/changelogs/unreleased/fix-name-vs-path-problem-for-cycle-analytics.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix broken issue links and possible 500 error on cycle analytics page when project name and path are different
-merge_request: 31471
-author:
-type: fixed
diff --git a/changelogs/unreleased/fj-avoid-incresaing-usage-ping-when-not-enabled.yml b/changelogs/unreleased/fj-avoid-incresaing-usage-ping-when-not-enabled.yml
deleted file mode 100644
index f1077f2d56d..00000000000
--- a/changelogs/unreleased/fj-avoid-incresaing-usage-ping-when-not-enabled.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Avoid increasing redis counters when usage_ping is disabled
-merge_request: 30949
-author:
-type: changed
diff --git a/changelogs/unreleased/fj-count-web-ide-merge-requests.yml b/changelogs/unreleased/fj-count-web-ide-merge-requests.yml
deleted file mode 100644
index adcd0af37e8..00000000000
--- a/changelogs/unreleased/fj-count-web-ide-merge-requests.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add Web IDE Usage Ping for Create SMAU
-merge_request: 30800
-author:
-type: changed
diff --git a/changelogs/unreleased/fj-navbar-searches-usage-ping-counter.yml b/changelogs/unreleased/fj-navbar-searches-usage-ping-counter.yml
deleted file mode 100644
index ab7c1697fd6..00000000000
--- a/changelogs/unreleased/fj-navbar-searches-usage-ping-counter.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Added navbar searches usage ping counter
-merge_request: 30953
-author:
-type: changed
diff --git a/changelogs/unreleased/georgekoltsov-18720-persistent-dashboard-sort.yml b/changelogs/unreleased/georgekoltsov-18720-persistent-dashboard-sort.yml
new file mode 100644
index 00000000000..7eed8550acd
--- /dev/null
+++ b/changelogs/unreleased/georgekoltsov-18720-persistent-dashboard-sort.yml
@@ -0,0 +1,5 @@
+---
+title: Add persistance to last choice of projects sorting on projects dashboard page
+merge_request: 31669
+author:
+type: added
diff --git a/changelogs/unreleased/georgekoltsov-48854-fix-empty-flash-message.yml b/changelogs/unreleased/georgekoltsov-48854-fix-empty-flash-message.yml
deleted file mode 100644
index e28dbd6f0c4..00000000000
--- a/changelogs/unreleased/georgekoltsov-48854-fix-empty-flash-message.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Fix empty error flash message on profile:account page when updating username
- with username that has already been taken
-merge_request: 31809
-author:
-type: fixed
diff --git a/changelogs/unreleased/georgekoltsov-51260-add-filtering-to-bitbucket-server-import.yml b/changelogs/unreleased/georgekoltsov-51260-add-filtering-to-bitbucket-server-import.yml
deleted file mode 100644
index c455b4cf642..00000000000
--- a/changelogs/unreleased/georgekoltsov-51260-add-filtering-to-bitbucket-server-import.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add BitBucketServer project import filtering
-merge_request: 31420
-author:
-type: added
diff --git a/changelogs/unreleased/georgekoltsov-55474-outbound-setting-system-hooks.yml b/changelogs/unreleased/georgekoltsov-55474-outbound-setting-system-hooks.yml
deleted file mode 100644
index fb1acb1e9f5..00000000000
--- a/changelogs/unreleased/georgekoltsov-55474-outbound-setting-system-hooks.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add new outbound network requests application setting for system hooks
-merge_request: 31177
-author:
-type: added
diff --git a/changelogs/unreleased/georgekoltsov-63408-user-mapping.yml b/changelogs/unreleased/georgekoltsov-63408-user-mapping.yml
deleted file mode 100644
index 451aac9c2e3..00000000000
--- a/changelogs/unreleased/georgekoltsov-63408-user-mapping.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Fix missing author line (`Created by: <user>`) in MRs/issues/comments of imported Bitbucket Cloud project'
-merge_request: 31579
-author:
-type: fixed
diff --git a/changelogs/unreleased/georgekoltsov-64311-set-visibility-private-if-internal-restricted.yml b/changelogs/unreleased/georgekoltsov-64311-set-visibility-private-if-internal-restricted.yml
deleted file mode 100644
index 18af16e5216..00000000000
--- a/changelogs/unreleased/georgekoltsov-64311-set-visibility-private-if-internal-restricted.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Set visibility level 'Private' for restricted 'Internal' imported projects when 'Internal' visibility setting is restricted in admin settings
-merge_request: 30522
-author:
-type: other
diff --git a/changelogs/unreleased/georgekoltsov-64377-add-better-log-msg-to-members-mapper.yml b/changelogs/unreleased/georgekoltsov-64377-add-better-log-msg-to-members-mapper.yml
deleted file mode 100644
index 9557e633f76..00000000000
--- a/changelogs/unreleased/georgekoltsov-64377-add-better-log-msg-to-members-mapper.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: When GitLab import fails during importer user mapping step, add an explicit
- error message mentioning importer
-merge_request: 30838
-author:
-type: other
diff --git a/changelogs/unreleased/gitaly-version-v1.57.0.yml b/changelogs/unreleased/gitaly-version-v1.57.0.yml
deleted file mode 100644
index 65596199e16..00000000000
--- a/changelogs/unreleased/gitaly-version-v1.57.0.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Upgrade to Gitaly v1.57.0
-merge_request: 31568
-author:
-type: changed
diff --git a/changelogs/unreleased/gitaly-version-v1.59.0.yml b/changelogs/unreleased/gitaly-version-v1.59.0.yml
deleted file mode 100644
index d103f6b248e..00000000000
--- a/changelogs/unreleased/gitaly-version-v1.59.0.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Upgrade to Gitaly v1.59.0
-merge_request: 31743
-author:
-type: changed
diff --git a/changelogs/unreleased/group-milestones-dashboard-blunceford.yml b/changelogs/unreleased/group-milestones-dashboard-blunceford.yml
deleted file mode 100644
index a6ded27cb8c..00000000000
--- a/changelogs/unreleased/group-milestones-dashboard-blunceford.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix bug in dashboard display of closed milestones
-merge_request: 30820
-author:
-type: fixed
diff --git a/changelogs/unreleased/id-mr-widget-etag-caching.yml b/changelogs/unreleased/id-mr-widget-etag-caching.yml
deleted file mode 100644
index 69bf89991d4..00000000000
--- a/changelogs/unreleased/id-mr-widget-etag-caching.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Split MR widget into etag-cached and non-cached serializers
-merge_request: 31354
-author:
-type: performance
diff --git a/changelogs/unreleased/id-source-code-smau.yml b/changelogs/unreleased/id-source-code-smau.yml
deleted file mode 100644
index 6ba5068544e..00000000000
--- a/changelogs/unreleased/id-source-code-smau.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add usage pings for source code pushes
-merge_request: 31734
-author:
-type: added
diff --git a/changelogs/unreleased/implement-dag.yml b/changelogs/unreleased/implement-dag.yml
deleted file mode 100644
index 72f3f9a510c..00000000000
--- a/changelogs/unreleased/implement-dag.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: "Support creating DAGs in CI config through the `needs` key"
-merge_request: 31328
-author:
-type: added
diff --git a/changelogs/unreleased/improve-quick-action-messages.yml b/changelogs/unreleased/improve-quick-action-messages.yml
deleted file mode 100644
index 86f8c251860..00000000000
--- a/changelogs/unreleased/improve-quick-action-messages.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improve quick action error messages
-merge_request: 31451
-author:
-type: other
diff --git a/changelogs/unreleased/issue-61873-no-error-message-for-general-settings.yml b/changelogs/unreleased/issue-61873-no-error-message-for-general-settings.yml
deleted file mode 100644
index 606c60721b8..00000000000
--- a/changelogs/unreleased/issue-61873-no-error-message-for-general-settings.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: error message for general settings
-merge_request: 31636
-author: Mesut Güneş
-type: fixed
diff --git a/changelogs/unreleased/issue_58494.yml b/changelogs/unreleased/issue_58494.yml
deleted file mode 100644
index c74768fc020..00000000000
--- a/changelogs/unreleased/issue_58494.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Prevent turning plain links into embedded when moving issues
-merge_request: 31489
-author:
-type: fixed
diff --git a/changelogs/unreleased/je-separate-namespace-fe.yml b/changelogs/unreleased/je-separate-namespace-fe.yml
deleted file mode 100644
index fb6be071eb6..00000000000
--- a/changelogs/unreleased/je-separate-namespace-fe.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update namespace label for GitLab-managed clusters
-merge_request: 30935
-author:
-type: added
diff --git a/changelogs/unreleased/jivanvl-add-chart-empty-state.yml b/changelogs/unreleased/jivanvl-add-chart-empty-state.yml
deleted file mode 100644
index 7b81ee82582..00000000000
--- a/changelogs/unreleased/jivanvl-add-chart-empty-state.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add empty chart component
-merge_request: 30682
-author:
-type: fixed
diff --git a/changelogs/unreleased/jprovazn-fix-positioning.yml b/changelogs/unreleased/jprovazn-fix-positioning.yml
deleted file mode 100644
index 5d703008bba..00000000000
--- a/changelogs/unreleased/jprovazn-fix-positioning.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Optimize relative re-positioning when moving issues.
-merge_request: 30938
-author:
-type: fixed
diff --git a/changelogs/unreleased/jprovazn-project-search.yml b/changelogs/unreleased/jprovazn-project-search.yml
deleted file mode 100644
index f76d4858924..00000000000
--- a/changelogs/unreleased/jprovazn-project-search.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Order projects in 'Move issue' dropdown by name.
-merge_request: 30778
-author:
-type: fixed
diff --git a/changelogs/unreleased/jramsay-fix-mirroring-help-text-typo.yml b/changelogs/unreleased/jramsay-fix-mirroring-help-text-typo.yml
deleted file mode 100644
index 4c049dffc58..00000000000
--- a/changelogs/unreleased/jramsay-fix-mirroring-help-text-typo.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix mirroring help text
-merge_request: 31348
-author: jramsay
-type: other
diff --git a/changelogs/unreleased/jupyter-fixes-v1.yml b/changelogs/unreleased/jupyter-fixes-v1.yml
deleted file mode 100644
index 7a34f273c90..00000000000
--- a/changelogs/unreleased/jupyter-fixes-v1.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Jupyter fixes
-merge_request: 31332
-author: Amit Rathi
-type: fixed
diff --git a/changelogs/unreleased/khair1-master-patch-79459.yml b/changelogs/unreleased/khair1-master-patch-79459.yml
deleted file mode 100644
index 22b0877336d..00000000000
--- a/changelogs/unreleased/khair1-master-patch-79459.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update Packer.gitlab-ci.yml to use latest image
-merge_request:
-author: Kelly Hair
-type: other
diff --git a/changelogs/unreleased/label-descr-push-opts.yml b/changelogs/unreleased/label-descr-push-opts.yml
deleted file mode 100644
index 9b01cfdaed2..00000000000
--- a/changelogs/unreleased/label-descr-push-opts.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Support setting of merge request title and description using git push options
-merge_request: 31068
-author:
-type: added
diff --git a/changelogs/unreleased/latest-pipeline.yml b/changelogs/unreleased/latest-pipeline.yml
new file mode 100644
index 00000000000..1177bb57121
--- /dev/null
+++ b/changelogs/unreleased/latest-pipeline.yml
@@ -0,0 +1,5 @@
+---
+title: Updated latest pipeline tag tooltip to be more descriptive
+merge_request: 31624
+author:
+type: changed
diff --git a/changelogs/unreleased/lm-download-csv-of-charts-from-metrics-dashboard.yml b/changelogs/unreleased/lm-download-csv-of-charts-from-metrics-dashboard.yml
deleted file mode 100644
index 59f12fca1f1..00000000000
--- a/changelogs/unreleased/lm-download-csv-of-charts-from-metrics-dashboard.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Export and download CSV from metrics charts
-merge_request: 30760
-author:
-type: added
diff --git a/changelogs/unreleased/load-search-counts-async.yml b/changelogs/unreleased/load-search-counts-async.yml
deleted file mode 100644
index 1f466450e76..00000000000
--- a/changelogs/unreleased/load-search-counts-async.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Load search result counts asynchronously
-merge_request: 31663
-author:
-type: changed
diff --git a/changelogs/unreleased/maintainers-can-create-subgroup.yml b/changelogs/unreleased/maintainers-can-create-subgroup.yml
deleted file mode 100644
index 180f0f7247f..00000000000
--- a/changelogs/unreleased/maintainers-can-create-subgroup.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Maintainers can create subgroups
-merge_request: 29718
-author: Fabio Papa
-type: changed
diff --git a/changelogs/unreleased/mc-feature-add-at-colon-variable-masking.yml b/changelogs/unreleased/mc-feature-add-at-colon-variable-masking.yml
deleted file mode 100644
index 9b8eff8e043..00000000000
--- a/changelogs/unreleased/mc-feature-add-at-colon-variable-masking.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Allows masking @ and : characters.'
-merge_request: 31065
-author:
-type: changed
diff --git a/changelogs/unreleased/mc-feature-manual-job-variables.yml b/changelogs/unreleased/mc-feature-manual-job-variables.yml
deleted file mode 100644
index a71cabfe303..00000000000
--- a/changelogs/unreleased/mc-feature-manual-job-variables.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow specifying variables when running manual jobs
-merge_request: 30485
-author:
-type: added
diff --git a/changelogs/unreleased/new-cycle-analytics-backend-migrations.yml b/changelogs/unreleased/new-cycle-analytics-backend-migrations.yml
deleted file mode 100644
index d56a07fe569..00000000000
--- a/changelogs/unreleased/new-cycle-analytics-backend-migrations.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Create database tables for the new cycle analytics backend
-merge_request: 31621
-author:
-type: other
diff --git a/changelogs/unreleased/optimize-note-indexes.yml b/changelogs/unreleased/optimize-note-indexes.yml
deleted file mode 100644
index bfb84779abf..00000000000
--- a/changelogs/unreleased/optimize-note-indexes.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Optimize DB indexes for ES indexing of notes
-merge_request: 31846
-author:
-type: performance
diff --git a/changelogs/unreleased/post-migrate-private-profile.yml b/changelogs/unreleased/post-migrate-private-profile.yml
deleted file mode 100644
index 53a55661aa0..00000000000
--- a/changelogs/unreleased/post-migrate-private-profile.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Migrate remaining users with null private_profile
-merge_request: 31708
-author:
-type: other
diff --git a/changelogs/unreleased/rails-template-update.yml b/changelogs/unreleased/rails-template-update.yml
deleted file mode 100644
index 53eb57c84ff..00000000000
--- a/changelogs/unreleased/rails-template-update.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update 'Ruby on Rails' project template
-merge_request: 31310
-author:
-type: other
diff --git a/changelogs/unreleased/remove-line-profile-from-performance-bar.yml b/changelogs/unreleased/remove-line-profile-from-performance-bar.yml
deleted file mode 100644
index c1c7450fbbd..00000000000
--- a/changelogs/unreleased/remove-line-profile-from-performance-bar.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove line profiler from performance bar
-merge_request:
-author:
-type: removed
diff --git a/changelogs/unreleased/remove-margin-from-user-header.yml b/changelogs/unreleased/remove-margin-from-user-header.yml
new file mode 100644
index 00000000000..31200d13c93
--- /dev/null
+++ b/changelogs/unreleased/remove-margin-from-user-header.yml
@@ -0,0 +1,5 @@
+---
+title: Remove margin from user header
+merge_request: 30878
+author: lucyfox
+type: fixed
diff --git a/changelogs/unreleased/remove-peek-gc.yml b/changelogs/unreleased/remove-peek-gc.yml
deleted file mode 100644
index 9412cd7c9a6..00000000000
--- a/changelogs/unreleased/remove-peek-gc.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove GC metrics from performance bar
-merge_request:
-author:
-type: removed
diff --git a/changelogs/unreleased/remove_deployment_metrics_deployment_platform_fallback.yml b/changelogs/unreleased/remove_deployment_metrics_deployment_platform_fallback.yml
deleted file mode 100644
index d32cbd1d8e0..00000000000
--- a/changelogs/unreleased/remove_deployment_metrics_deployment_platform_fallback.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Remove incorrect fallback when determining which cluster to use when retrieving
- MR performance metrics
-merge_request: 31126
-author:
-type: changed
diff --git a/changelogs/unreleased/report-missing-job-dependency.yml b/changelogs/unreleased/report-missing-job-dependency.yml
deleted file mode 100644
index 660cdfc856e..00000000000
--- a/changelogs/unreleased/report-missing-job-dependency.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Default dependency job stage index to Infinity, and correctly report it as
- undefined in prior stages
-merge_request: 31116
-author:
-type: fixed
diff --git a/changelogs/unreleased/rf-remove-group-overview-security-dashboard-feature-flag.yml b/changelogs/unreleased/rf-remove-group-overview-security-dashboard-feature-flag.yml
deleted file mode 100644
index f412ba11b91..00000000000
--- a/changelogs/unreleased/rf-remove-group-overview-security-dashboard-feature-flag.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove Security Dashboard feature flag
-merge_request: 31820
-author:
-type: other
diff --git a/changelogs/unreleased/rm-src-branch.yml b/changelogs/unreleased/rm-src-branch.yml
deleted file mode 100644
index 03b91d0c7db..00000000000
--- a/changelogs/unreleased/rm-src-branch.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Support remove source branch on merge w/ push options
-merge_request: 30728
-author:
-type: added
diff --git a/changelogs/unreleased/safe-archiving-for-traces.yml b/changelogs/unreleased/safe-archiving-for-traces.yml
deleted file mode 100644
index 2b9070bacfe..00000000000
--- a/changelogs/unreleased/safe-archiving-for-traces.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Extra logging for new live trace architecture
-merge_request: 30892
-author:
-type: fixed
diff --git a/changelogs/unreleased/security-2873-blocked-user-slash-command-bypass-master.yml b/changelogs/unreleased/security-2873-blocked-user-slash-command-bypass-master.yml
deleted file mode 100644
index cd31fe0f35c..00000000000
--- a/changelogs/unreleased/security-2873-blocked-user-slash-command-bypass-master.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Restrict slash commands to users who can log in
-merge_request:
-author:
-type: security
diff --git a/changelogs/unreleased/security-60551-fix-upload-scope.yml b/changelogs/unreleased/security-60551-fix-upload-scope.yml
deleted file mode 100644
index 7d7096833a7..00000000000
--- a/changelogs/unreleased/security-60551-fix-upload-scope.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Queries for Upload should be scoped by model
-merge_request:
-author:
-type: security
diff --git a/changelogs/unreleased/sh-add-cmaps-for-pdfjs.yml b/changelogs/unreleased/sh-add-cmaps-for-pdfjs.yml
deleted file mode 100644
index f4686484e33..00000000000
--- a/changelogs/unreleased/sh-add-cmaps-for-pdfjs.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make pdf.js render CJK characters
-merge_request: 31220
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-add-gitaly-and-rugged-data-sidekiq.yml b/changelogs/unreleased/sh-add-gitaly-and-rugged-data-sidekiq.yml
deleted file mode 100644
index d2143e83045..00000000000
--- a/changelogs/unreleased/sh-add-gitaly-and-rugged-data-sidekiq.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add Gitaly and Rugged call timing in Sidekiq logs
-merge_request: 31651
-author:
-type: other
diff --git a/changelogs/unreleased/sh-add-index-extern-uid.yml b/changelogs/unreleased/sh-add-index-extern-uid.yml
deleted file mode 100644
index 531770237a8..00000000000
--- a/changelogs/unreleased/sh-add-index-extern-uid.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add partial index on identities table to speed up LDAP lookups
-merge_request: 26710
-author:
-type: performance
diff --git a/changelogs/unreleased/sh-add-missing-csp-report-uri.yml b/changelogs/unreleased/sh-add-missing-csp-report-uri.yml
deleted file mode 100644
index 656eb8e9c37..00000000000
--- a/changelogs/unreleased/sh-add-missing-csp-report-uri.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add missing report-uri to CSP config
-merge_request: 31593
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-add-rugged-logs.yml b/changelogs/unreleased/sh-add-rugged-logs.yml
deleted file mode 100644
index 1f464dd92ff..00000000000
--- a/changelogs/unreleased/sh-add-rugged-logs.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add Rugged calls and duration to API and Rails logs
-merge_request: 30871
-author:
-type: other
diff --git a/changelogs/unreleased/sh-add-rugged-to-peek.yml b/changelogs/unreleased/sh-add-rugged-to-peek.yml
deleted file mode 100644
index 8a030f3daf2..00000000000
--- a/changelogs/unreleased/sh-add-rugged-to-peek.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add Rugged calls to performance bar
-merge_request: 30983
-author:
-type: other
diff --git a/changelogs/unreleased/sh-break-out-invited-group-members.yml b/changelogs/unreleased/sh-break-out-invited-group-members.yml
deleted file mode 100644
index 091f1d48843..00000000000
--- a/changelogs/unreleased/sh-break-out-invited-group-members.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make it easier to find invited group members
-merge_request: 28436
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-disable-redis-peek.yml b/changelogs/unreleased/sh-disable-redis-peek.yml
deleted file mode 100644
index de86c0031c7..00000000000
--- a/changelogs/unreleased/sh-disable-redis-peek.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Only track Redis calls if Peek is enabled
-merge_request: 31438
-author:
-type: performance
diff --git a/changelogs/unreleased/sh-disable-registry-delete.yml b/changelogs/unreleased/sh-disable-registry-delete.yml
deleted file mode 100644
index 180b983e07c..00000000000
--- a/changelogs/unreleased/sh-disable-registry-delete.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Don't attempt to contact registry if it is disabled
-merge_request: 31553
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-enable-bootsnap.yml b/changelogs/unreleased/sh-enable-bootsnap.yml
deleted file mode 100644
index 674a900ee01..00000000000
--- a/changelogs/unreleased/sh-enable-bootsnap.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make Bootsnap available via ENABLE_BOOTSNAP=1
-merge_request: 30963
-author:
-type: performance
diff --git a/changelogs/unreleased/sh-fix-discussions-api-perf.yml b/changelogs/unreleased/sh-fix-discussions-api-perf.yml
deleted file mode 100644
index 8cdbbf03dab..00000000000
--- a/changelogs/unreleased/sh-fix-discussions-api-perf.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Eliminate many Gitaly calls in discussions API
-merge_request: 31834
-author:
-type: performance
diff --git a/changelogs/unreleased/sh-fix-import-export-suggestions.yml b/changelogs/unreleased/sh-fix-import-export-suggestions.yml
deleted file mode 100644
index 4b15fc3858f..00000000000
--- a/changelogs/unreleased/sh-fix-import-export-suggestions.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Properly save suggestions in project exports
-merge_request: 31690
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-fix-pipelines-not-being-created.yml b/changelogs/unreleased/sh-fix-pipelines-not-being-created.yml
deleted file mode 100644
index a6937eae588..00000000000
--- a/changelogs/unreleased/sh-fix-pipelines-not-being-created.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix pipelines not always being created after a push
-merge_request: 31927
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-fix-special-role-error-500.yml b/changelogs/unreleased/sh-fix-special-role-error-500.yml
deleted file mode 100644
index 9aed0710da3..00000000000
--- a/changelogs/unreleased/sh-fix-special-role-error-500.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix first-time contributor notes not rendering
-merge_request: 31340
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-ignore-git-errors-delete-project.yml b/changelogs/unreleased/sh-ignore-git-errors-delete-project.yml
deleted file mode 100644
index f5e2147f00e..00000000000
--- a/changelogs/unreleased/sh-ignore-git-errors-delete-project.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Ignore Gitaly errors if cache flushing fails on project destruction
-merge_request: 31164
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-make-githost-json.yml b/changelogs/unreleased/sh-make-githost-json.yml
deleted file mode 100644
index f4113a693cc..00000000000
--- a/changelogs/unreleased/sh-make-githost-json.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Convert githost.log to JSON format
-merge_request: 30967
-author:
-type: changed
diff --git a/changelogs/unreleased/sh-only-flush-tags-once-per-push.yml b/changelogs/unreleased/sh-only-flush-tags-once-per-push.yml
deleted file mode 100644
index 502fc22ebbd..00000000000
--- a/changelogs/unreleased/sh-only-flush-tags-once-per-push.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Only expire tag cache once per push
-merge_request: 31641
-author:
-type: performance
diff --git a/changelogs/unreleased/sh-optimize-commit-deltas-post-receive.yml b/changelogs/unreleased/sh-optimize-commit-deltas-post-receive.yml
deleted file mode 100644
index cd63b9bf425..00000000000
--- a/changelogs/unreleased/sh-optimize-commit-deltas-post-receive.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Reduce Gitaly calls in PostReceive
-merge_request: 31741
-author:
-type: performance
diff --git a/changelogs/unreleased/sh-post-receive-cache-clear-once.yml b/changelogs/unreleased/sh-post-receive-cache-clear-once.yml
deleted file mode 100644
index b677adf78d9..00000000000
--- a/changelogs/unreleased/sh-post-receive-cache-clear-once.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Expire project caches once per push instead of once per ref
-merge_request: 31876
-author:
-type: performance
diff --git a/changelogs/unreleased/sh-remove-pdfjs-deprecations.yml b/changelogs/unreleased/sh-remove-pdfjs-deprecations.yml
deleted file mode 100644
index a00ceedf3f4..00000000000
--- a/changelogs/unreleased/sh-remove-pdfjs-deprecations.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove pdf.js deprecation warnings
-merge_request: 31253
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-rename-githost-to-gitjson.yml b/changelogs/unreleased/sh-rename-githost-to-gitjson.yml
deleted file mode 100644
index 24fcd1e9781..00000000000
--- a/changelogs/unreleased/sh-rename-githost-to-gitjson.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Rename githost.log -> git_json.log
-merge_request: 31634
-author:
-type: changed
diff --git a/changelogs/unreleased/sh-support-csp-nonce.yml b/changelogs/unreleased/sh-support-csp-nonce.yml
deleted file mode 100644
index 3e6ac1e4a32..00000000000
--- a/changelogs/unreleased/sh-support-csp-nonce.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add support for Content-Security-Policy
-merge_request: 31402
-author:
-type: added
diff --git a/changelogs/unreleased/sh-update-mermaid.yml b/changelogs/unreleased/sh-update-mermaid.yml
deleted file mode 100644
index 58d94ec6235..00000000000
--- a/changelogs/unreleased/sh-update-mermaid.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update Mermaid to v8.2.3
-merge_request: 30985
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-update-rouge-3-7-0.yml b/changelogs/unreleased/sh-update-rouge-3-7-0.yml
deleted file mode 100644
index 6828f48863c..00000000000
--- a/changelogs/unreleased/sh-update-rouge-3-7-0.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update rouge to v3.7.0
-merge_request: 31254
-author:
-type: other
diff --git a/changelogs/unreleased/sh-update-rugged-0-28-3.yml b/changelogs/unreleased/sh-update-rugged-0-28-3.yml
deleted file mode 100644
index 86446564e12..00000000000
--- a/changelogs/unreleased/sh-update-rugged-0-28-3.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Upgrade Rugged to 0.28.3
-merge_request: 31794
-author:
-type: security
diff --git a/changelogs/unreleased/sh-use-redis-caching-store.yml b/changelogs/unreleased/sh-use-redis-caching-store.yml
deleted file mode 100644
index e61bdb490bc..00000000000
--- a/changelogs/unreleased/sh-use-redis-caching-store.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use Rails 5.2 Redis caching store
-merge_request: 30966
-author:
-type: other
diff --git a/changelogs/unreleased/sh-use-shared-state-cluster-pubsub.yml b/changelogs/unreleased/sh-use-shared-state-cluster-pubsub.yml
deleted file mode 100644
index 5e72f23d7ad..00000000000
--- a/changelogs/unreleased/sh-use-shared-state-cluster-pubsub.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use persistent Redis cluster for Workhorse pub/sub notifications
-merge_request: 30990
-author:
-type: fixed
diff --git a/changelogs/unreleased/snowplow-ee-to-ce.yml b/changelogs/unreleased/snowplow-ee-to-ce.yml
deleted file mode 100644
index 85c959f0510..00000000000
--- a/changelogs/unreleased/snowplow-ee-to-ce.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Moves snowplow tracking from ee to ce
-merge_request: 31160
-author: jejacks0n
-type: added
diff --git a/changelogs/unreleased/speed-up-labels-api.yml b/changelogs/unreleased/speed-up-labels-api.yml
deleted file mode 100644
index d5ac4313414..00000000000
--- a/changelogs/unreleased/speed-up-labels-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove counts from default labels API responses
-merge_request: 31543
-author:
-type: changed
diff --git a/changelogs/unreleased/tr-embed-metric-links.yml b/changelogs/unreleased/tr-embed-metric-links.yml
deleted file mode 100644
index 6918114a4ae..00000000000
--- a/changelogs/unreleased/tr-embed-metric-links.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Generate shareable link for specific metric charts
-merge_request: 31339
-author:
-type: added
diff --git a/changelogs/unreleased/tr-remove-embed-metrics-flag.yml b/changelogs/unreleased/tr-remove-embed-metrics-flag.yml
deleted file mode 100644
index a327a6868d3..00000000000
--- a/changelogs/unreleased/tr-remove-embed-metrics-flag.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Link and embed metrics in GitLab Flavored Markdown
-merge_request: 31106
-author:
-type: added
diff --git a/changelogs/unreleased/uncomment_commit_signatures_feature_flag.yml b/changelogs/unreleased/uncomment_commit_signatures_feature_flag.yml
new file mode 100644
index 00000000000..8f6c89e7dd3
--- /dev/null
+++ b/changelogs/unreleased/uncomment_commit_signatures_feature_flag.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade to Gitaly 1.60.0
+merge_request: 31981
+author:
+type: changed
diff --git a/changelogs/unreleased/update-babel-to-7-5-5.yml b/changelogs/unreleased/update-babel-to-7-5-5.yml
new file mode 100644
index 00000000000..c498e2adfe8
--- /dev/null
+++ b/changelogs/unreleased/update-babel-to-7-5-5.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade babel to 7.5.5
+merge_request: 31819
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-7-0.yml b/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-7-0.yml
deleted file mode 100644
index ab1e7d77520..00000000000
--- a/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-7-0.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update GitLab Runner Helm Chart to 0.7.0
-merge_request: 30950
-author:
-type: other
diff --git a/changelogs/unreleased/update-graphicsmagick-to-1-3-33.yml b/changelogs/unreleased/update-graphicsmagick-to-1-3-33.yml
deleted file mode 100644
index 898fdef830a..00000000000
--- a/changelogs/unreleased/update-graphicsmagick-to-1-3-33.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update GraphicsMagick from 1.3.29 to 1.3.33 for CI tests
-merge_request: 31692
-author: Takuya Noguchi
-type: other
diff --git a/changelogs/unreleased/update-pipelines-minutes-expiry-banner-to-an-alert-component-type.yml b/changelogs/unreleased/update-pipelines-minutes-expiry-banner-to-an-alert-component-type.yml
deleted file mode 100644
index 8c1a033dd29..00000000000
--- a/changelogs/unreleased/update-pipelines-minutes-expiry-banner-to-an-alert-component-type.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Enhance style of the shared runners limit
-merge_request: 31386
-author:
-type: other
diff --git a/changelogs/unreleased/visual-review-tools-constant-storage-keys.yml b/changelogs/unreleased/visual-review-tools-constant-storage-keys.yml
deleted file mode 100644
index 4c9b048aaa3..00000000000
--- a/changelogs/unreleased/visual-review-tools-constant-storage-keys.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix visual review app storage keys
-merge_request: 31427
-author:
-type: fixed
diff --git a/changelogs/unreleased/wiki-usage-pings.yml b/changelogs/unreleased/wiki-usage-pings.yml
deleted file mode 100644
index c3d084228c3..00000000000
--- a/changelogs/unreleased/wiki-usage-pings.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Count wiki creation, update and delete events
-merge_request: 30864
-author:
-type: added
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 226f2ec3722..2f6658594cc 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -95,6 +95,15 @@ production: &base
email_display_name: GitLab
email_reply_to: noreply@example.com
email_subject_suffix: ''
+ email_smime:
+ # Uncomment and set to true if you need to enable email S/MIME signing (default: false)
+ # enabled: false
+ # S/MIME private key file in PEM format, unencrypted
+ # Default is '.gitlab_smime_key' relative to Rails.root (i.e. root of the GitLab app).
+ # key_file: /home/git/gitlab/.gitlab_smime_key
+ # S/MIME public certificate key in PEM format, will be attached to signed messages
+ # Default is '.gitlab_smime_cert' relative to Rails.root (i.e. root of the GitLab app).
+ # cert_file: /home/git/gitlab/.gitlab_smime_cert
# Email server smtp settings are in config/initializers/smtp_settings.rb.sample
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 828732126b6..fdc6b0a05ab 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -1,5 +1,6 @@
require_relative '../settings'
require_relative '../object_store_settings'
+require_relative '../smime_signature_settings'
# Default settings
Settings['ldap'] ||= Settingslogic.new({})
@@ -171,6 +172,7 @@ Settings.gitlab['email_from'] ||= ENV['GITLAB_EMAIL_FROM'] || "gitlab@#{Settings
Settings.gitlab['email_display_name'] ||= ENV['GITLAB_EMAIL_DISPLAY_NAME'] || 'GitLab'
Settings.gitlab['email_reply_to'] ||= ENV['GITLAB_EMAIL_REPLY_TO'] || "noreply@#{Settings.gitlab.host}"
Settings.gitlab['email_subject_suffix'] ||= ENV['GITLAB_EMAIL_SUBJECT_SUFFIX'] || ""
+Settings.gitlab['email_smime'] = SmimeSignatureSettings.parse(Settings.gitlab['email_smime'])
Settings.gitlab['base_url'] ||= Settings.__send__(:build_base_gitlab_url)
Settings.gitlab['url'] ||= Settings.__send__(:build_gitlab_url)
Settings.gitlab['user'] ||= 'git'
diff --git a/config/initializers/action_mailer_hooks.rb b/config/initializers/action_mailer_hooks.rb
index f1b3c1f8ae8..02ca6ef13bf 100644
--- a/config/initializers/action_mailer_hooks.rb
+++ b/config/initializers/action_mailer_hooks.rb
@@ -10,3 +10,8 @@ ActionMailer::Base.register_interceptors(
)
ActionMailer::Base.register_observer(::Gitlab::Email::Hook::DeliveryMetricsObserver)
+
+if Gitlab.config.gitlab.email_enabled && Gitlab.config.gitlab.email_smime.enabled
+ ActionMailer::Base.register_interceptor(::Gitlab::Email::Hook::SmimeSignatureInterceptor)
+ Gitlab::AppLogger.debug "S/MIME signing of outgoing emails enabled"
+end
diff --git a/config/initializers/rack_attack_logging.rb b/config/initializers/rack_attack_logging.rb
index 7eb34bd69e5..b43fff24bb0 100644
--- a/config/initializers/rack_attack_logging.rb
+++ b/config/initializers/rack_attack_logging.rb
@@ -7,9 +7,9 @@ ActiveSupport::Notifications.subscribe('rack.attack') do |name, start, finish, r
rack_attack_info = {
message: 'Rack_Attack',
env: req.env['rack.attack.match_type'],
- ip: req.ip,
+ remote_ip: req.ip,
request_method: req.request_method,
- fullpath: req.fullpath
+ path: req.fullpath
}
if %w(throttle_authenticated_api throttle_authenticated_web).include? req.env['rack.attack.matched']
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index 7217f098fd9..9f3e104bc2b 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -28,11 +28,13 @@ if Rails.env.development?
end
enable_json_logs = Gitlab.config.sidekiq.log_format == 'json'
+enable_sidekiq_monitor = ENV.fetch("SIDEKIQ_MONITOR_WORKER", 0).to_i.nonzero?
Sidekiq.configure_server do |config|
config.redis = queues_config_hash
config.server_middleware do |chain|
+ chain.add Gitlab::SidekiqMiddleware::Monitor if enable_sidekiq_monitor
chain.add Gitlab::SidekiqMiddleware::Metrics if Settings.monitoring.sidekiq_exporter
chain.add Gitlab::SidekiqMiddleware::ArgumentsLogger if ENV['SIDEKIQ_LOG_ARGUMENTS'] && !enable_json_logs
chain.add Gitlab::SidekiqMiddleware::MemoryKiller if ENV['SIDEKIQ_MEMORY_KILLER_MAX_RSS']
@@ -57,6 +59,8 @@ Sidekiq.configure_server do |config|
# Clear any connections that might have been obtained before starting
# Sidekiq (e.g. in an initializer).
ActiveRecord::Base.clear_all_connections!
+
+ Gitlab::SidekiqMonitor.instance.start if enable_sidekiq_monitor
end
if enable_reliable_fetch?
diff --git a/config/initializers/zz_metrics.rb b/config/initializers/zz_metrics.rb
index b005fdf159b..af4aec7b355 100644
--- a/config/initializers/zz_metrics.rb
+++ b/config/initializers/zz_metrics.rb
@@ -100,11 +100,7 @@ def instrument_classes(instrumentation)
instrumentation.instrument_instance_methods(Gitlab::Elastic::SnippetSearchResults)
instrumentation.instrument_methods(Gitlab::Elastic::Helper)
- instrumentation.instrument_instance_methods(Elastic::ApplicationSearch)
- instrumentation.instrument_instance_methods(Elastic::IssuesSearch)
- instrumentation.instrument_instance_methods(Elastic::MergeRequestsSearch)
- instrumentation.instrument_instance_methods(Elastic::MilestonesSearch)
- instrumentation.instrument_instance_methods(Elastic::NotesSearch)
+ instrumentation.instrument_instance_methods(Elastic::ApplicationVersionedSearch)
instrumentation.instrument_instance_methods(Elastic::ProjectsSearch)
instrumentation.instrument_instance_methods(Elastic::RepositoriesSearch)
instrumentation.instrument_instance_methods(Elastic::SnippetsSearch)
diff --git a/config/prometheus/common_metrics.yml b/config/prometheus/common_metrics.yml
index 32475ef8380..08504d6f7d5 100644
--- a/config/prometheus/common_metrics.yml
+++ b/config/prometheus/common_metrics.yml
@@ -166,7 +166,7 @@ panel_groups:
label: Total (cores)
unit: "cores"
- title: "Memory Usage (Pod average)"
- type: "area-chart"
+ type: "line-chart"
y_label: "Memory Used per Pod (MB)"
weight: 2
metrics:
@@ -175,7 +175,7 @@ panel_groups:
label: Pod average (MB)
unit: MB
- title: "Canary: Memory Usage (Pod Average)"
- type: "area-chart"
+ type: "line-chart"
y_label: "Memory Used per Pod (MB)"
weight: 2
metrics:
@@ -185,7 +185,7 @@ panel_groups:
unit: MB
track: canary
- title: "Core Usage (Pod Average)"
- type: "area-chart"
+ type: "line-chart"
y_label: "Cores per Pod"
weight: 1
metrics:
@@ -194,7 +194,7 @@ panel_groups:
label: Pod average (cores)
unit: "cores"
- title: "Canary: Core Usage (Pod Average)"
- type: "area-chart"
+ type: "line-chart"
y_label: "Cores per Pod"
weight: 1
metrics:
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 9a453d101a1..29e462f904d 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -359,6 +359,9 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
collection do
resource :pipelines_settings, path: 'settings', only: [:show, :update]
get :charts
+ scope '(*ref)', constraints: { ref: Gitlab::PathRegex.git_reference_regex } do
+ get :latest, action: :show, defaults: { latest: true }
+ end
end
member do
diff --git a/config/routes/wiki.rb b/config/routes/wiki.rb
index 2ca52e55fca..d439c99270e 100644
--- a/config/routes/wiki.rb
+++ b/config/routes/wiki.rb
@@ -2,6 +2,7 @@ scope(controller: :wikis) do
scope(path: 'wikis', as: :wikis) do
get :git_access
get :pages
+ get :new
get '/', to: redirect('%{namespace_id}/%{project_id}/wikis/home')
post '/', to: 'wikis#create'
end
diff --git a/config/smime_signature_settings.rb b/config/smime_signature_settings.rb
new file mode 100644
index 00000000000..3d19db84c19
--- /dev/null
+++ b/config/smime_signature_settings.rb
@@ -0,0 +1,11 @@
+# Set default values for email_smime settings
+class SmimeSignatureSettings
+ def self.parse(email_smime)
+ email_smime ||= Settingslogic.new({})
+ email_smime['enabled'] = false unless email_smime['enabled']
+ email_smime['key_file'] ||= Rails.root.join('.gitlab_smime_key')
+ email_smime['cert_file'] ||= Rails.root.join('.gitlab_smime_cert')
+
+ email_smime
+ end
+end
diff --git a/danger/only_documentation/Dangerfile b/danger/only_documentation/Dangerfile
index ff65f8713d2..dad12c0d29c 100644
--- a/danger/only_documentation/Dangerfile
+++ b/danger/only_documentation/Dangerfile
@@ -1,7 +1,7 @@
# rubocop:disable Style/SignalException
# frozen_string_literal: true
-has_only_docs_changes = helper.all_changed_files.all? { |file| file.start_with?('doc/', '.gitlab/ci/docs.gitlab-ci.yml', '.mdlrc') }
+has_only_docs_changes = helper.all_changed_files.all? { |file| file.start_with?('doc/', '.gitlab/ci/docs.gitlab-ci.yml', '.mdlrc') || file.end_with?('.md') }
is_docs_only_branch = gitlab.branch_for_head =~ /(^docs[\/-].*|.*-docs$)/
if is_docs_only_branch && !has_only_docs_changes
diff --git a/db/fixtures/development/15_award_emoji.rb b/db/fixtures/development/15_award_emoji.rb
index 137a036edaf..a9dcc048586 100644
--- a/db/fixtures/development/15_award_emoji.rb
+++ b/db/fixtures/development/15_award_emoji.rb
@@ -1,35 +1,22 @@
require './spec/support/sidekiq'
Gitlab::Seeder.quiet do
- emoji = Gitlab::Emoji.emojis.keys
+ EMOJI = Gitlab::Emoji.emojis.keys
- Issue.order(Gitlab::Database.random).limit(Issue.count / 2).each do |issue|
- project = issue.project
+ def seed_award_emoji(klass)
+ klass.order(Gitlab::Database.random).limit(klass.count / 2).each do |awardable|
+ awardable.project.authorized_users.where('project_authorizations.access_level > ?', Gitlab::Access::GUEST).sample(2).each do |user|
+ AwardEmojis::AddService.new(awardable, EMOJI.sample, user).execute
- project.team.users.sample(2).each do |user|
- issue.create_award_emoji(emoji.sample, user)
+ awardable.notes.user.sample(2).each do |note|
+ AwardEmojis::AddService.new(note, EMOJI.sample, user).execute
+ end
- issue.notes.sample(2).each do |note|
- next if note.system?
- note.create_award_emoji(emoji.sample, user)
+ print '.'
end
-
- print '.'
end
end
- MergeRequest.order(Gitlab::Database.random).limit(MergeRequest.count / 2).each do |mr|
- project = mr.project
-
- project.team.users.sample(2).each do |user|
- mr.create_award_emoji(emoji.sample, user)
-
- mr.notes.sample(2).each do |note|
- next if note.system?
- note.create_award_emoji(emoji.sample, user)
- end
-
- print '.'
- end
- end
+ seed_award_emoji(Issue)
+ seed_award_emoji(MergeRequest)
end
diff --git a/db/migrate/20190808152507_add_projects_sorting_field_to_user_preferences.rb b/db/migrate/20190808152507_add_projects_sorting_field_to_user_preferences.rb
new file mode 100644
index 00000000000..941fead655e
--- /dev/null
+++ b/db/migrate/20190808152507_add_projects_sorting_field_to_user_preferences.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class AddProjectsSortingFieldToUserPreferences < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def up
+ add_column :user_preferences, :projects_sort, :string, limit: 64
+ end
+
+ def down
+ remove_column :user_preferences, :projects_sort
+ end
+end
diff --git a/db/migrate/20190814205640_import_common_metrics_line_charts.rb b/db/migrate/20190814205640_import_common_metrics_line_charts.rb
new file mode 100644
index 00000000000..1c28d686a42
--- /dev/null
+++ b/db/migrate/20190814205640_import_common_metrics_line_charts.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class ImportCommonMetricsLineCharts < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def up
+ ::Gitlab::DatabaseImporters::CommonMetrics::Importer.new.execute
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index ce5fd38129a..3f7917654cf 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -3411,6 +3411,7 @@ ActiveRecord::Schema.define(version: 2019_08_15_093949) do
t.string "epics_sort"
t.integer "roadmap_epics_state"
t.string "roadmaps_sort"
+ t.string "projects_sort", limit: 64
t.index ["user_id"], name: "index_user_preferences_on_user_id", unique: true
end
diff --git a/doc/README.md b/doc/README.md
index 8ce5d2e240a..f12c06199c2 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -358,7 +358,7 @@ The following documentation relates to the DevOps **Secure** stage:
| [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. |
+| [License Compliance](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. |
diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md
index d0adeb89543..d43b3718bf9 100644
--- a/doc/administration/container_registry.md
+++ b/doc/administration/container_registry.md
@@ -137,6 +137,15 @@ otherwise you will run into conflicts.
1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+1. Validate using:
+
+ ```sh
+ openssl s_client -showcerts -servername gitlab.example.com -connect gitlab.example.com:443 > cacert.pem
+ ```
+
+NOTE: **Note:**
+If your certificate provider provides the CA Bundle certificates, append them to the TLS certificate file.
+
**Installations from source**
1. Open `/home/git/gitlab/config/gitlab.yml`, find the `registry` entry and
@@ -163,9 +172,13 @@ docker login gitlab.example.com:4567
If the Registry is configured to use its own domain, you will need a TLS
certificate for that specific domain (e.g., `registry.example.com`) or maybe
-a wildcard certificate if hosted under a subdomain of your existing GitLab
+a wildcard certificate if hosted under a subdomain of your existing GitLab
domain (e.g., `registry.gitlab.example.com`).
+NOTE: **Note:**
+As well as manually generated SSL certificates (explained here), certificates automatically
+generated by Let's Encrypt are also [supported in Omnibus installs](https://docs.gitlab.com/omnibus/settings/ssl.html#host-services).
+
Let's assume that you want the container Registry to be accessible at
`https://registry.gitlab.example.com`.
diff --git a/doc/administration/geo/disaster_recovery/index.md b/doc/administration/geo/disaster_recovery/index.md
index d44e141b66b..ba95843b0b0 100644
--- a/doc/administration/geo/disaster_recovery/index.md
+++ b/doc/administration/geo/disaster_recovery/index.md
@@ -1,4 +1,4 @@
-# Disaster Recovery **(PREMIUM ONLY)**
+# Disaster Recovery (Geo) **(PREMIUM ONLY)**
Geo replicates your database, your Git repositories, and few other assets.
We will support and replicate more data in the future, that will enable you to
diff --git a/doc/administration/geo/replication/index.md b/doc/administration/geo/replication/index.md
index 5aedb665935..dbd466b906d 100644
--- a/doc/administration/geo/replication/index.md
+++ b/doc/administration/geo/replication/index.md
@@ -1,4 +1,4 @@
-# Geo Replication **(PREMIUM ONLY)**
+# Replication (Geo) **(PREMIUM ONLY)**
> - Introduced in GitLab Enterprise Edition 8.9.
> - Using Geo in combination with
@@ -6,7 +6,7 @@
> is considered **Generally Available** (GA) in
> [GitLab Premium](https://about.gitlab.com/pricing/) 10.4.
-Geo is the solution for widely distributed development teams.
+Replication with Geo is the solution for widely distributed development teams.
## Overview
diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md
index 150494c47e5..878f0ef842d 100644
--- a/doc/administration/gitaly/index.md
+++ b/doc/administration/gitaly/index.md
@@ -64,6 +64,7 @@ The following list depicts what the network architecture of Gitaly is:
topology.
- A `(Gitaly address, Gitaly token)` corresponds to a Gitaly server.
- A Gitaly server hosts one or more storages.
+- A GitLab server can use one or more Gitaly servers.
- Gitaly addresses must be specified in such a way that they resolve
correctly for ALL Gitaly clients.
- Gitaly clients are: Unicorn, Sidekiq, gitlab-workhorse,
@@ -77,14 +78,16 @@ The following list depicts what the network architecture of Gitaly is:
- Authentication is done through a static token which is shared among the Gitaly
and GitLab Rails nodes.
-Below we describe how to configure a Gitaly server at address
-`gitaly.internal:8075` with secret token `abc123secret`. We assume
-your GitLab installation has two repository storages, `default` and
-`storage1`.
+Below we describe how to configure two Gitaly servers one at
+`gitaly1.internal` and the other at `gitaly2.internal`
+with secret token `abc123secret`. We assume
+your GitLab installation has three repository storages: `default`,
+`storage1` and `storage2`.
### 1. Installation
-First install Gitaly using either Omnibus GitLab or install it from source:
+First install Gitaly on each Gitaly server using either
+Omnibus GitLab or install it from source:
- For Omnibus GitLab: [Download/install](https://about.gitlab.com/install/) the Omnibus GitLab
package you want using **steps 1 and 2** from the GitLab downloads page but
@@ -119,7 +122,7 @@ Configure a token on the instance that runs the GitLab Rails application.
### 3. Gitaly server configuration
-Next, on the Gitaly server, you need to configure storage paths, enable
+Next, on the Gitaly servers, you need to configure storage paths, enable
the network listener and configure the token.
NOTE: **Note:** if you want to reduce the risk of downtime when you enable
@@ -175,15 +178,29 @@ Check the directory layout on your Gitaly server to be sure.
gitaly['listen_addr'] = "0.0.0.0:8075"
gitaly['auth_token'] = 'abc123secret'
+ # To use TLS for Gitaly you need to add
+ gitaly['tls_listen_addr'] = "0.0.0.0:9999"
+ gitaly['certificate_path'] = "path/to/cert.pem"
+ gitaly['key_path'] = "path/to/key.pem"
+ ```
+
+1. Append the following to `/etc/gitlab/gitlab.rb` for each respective server:
+
+ For `gitaly1.internal`:
+
+ ```
gitaly['storage'] = [
{ 'name' => 'default' },
{ 'name' => 'storage1' },
]
+ ```
+
+ For `gitaly2.internal`:
- # To use TLS for Gitaly you need to add
- gitaly['tls_listen_addr'] = "0.0.0.0:9999"
- gitaly['certificate_path'] = "path/to/cert.pem"
- gitaly['key_path'] = "path/to/key.pem"
+ ```
+ gitaly['storage'] = [
+ { 'name' => 'storage2' },
+ ]
```
NOTE: **Note:**
@@ -206,13 +223,26 @@ Check the directory layout on your Gitaly server to be sure.
[auth]
token = 'abc123secret'
+ ```
+
+1. Append the following to `/home/git/gitaly/config.toml` for each respective server:
+
+ For `gitaly1.internal`:
+ ```toml
[[storage]]
name = 'default'
[[storage]]
name = 'storage1'
```
+
+ For `gitaly2.internal`:
+
+ ```toml
+ [[storage]]
+ name = 'storage2'
+ ```
NOTE: **Note:**
In some cases, you'll have to set `path` for each `[[storage]]` in the
@@ -231,9 +261,13 @@ then all Gitaly requests will fail.
Additionally, you need to
[disable Rugged if previously manually enabled](../high_availability/nfs.md#improving-nfs-performance-with-gitlab).
-We assume that your Gitaly server can be reached at
-`gitaly.internal:8075` from your GitLab server, and that Gitaly can read and
-write to `/mnt/gitlab/default` and `/mnt/gitlab/storage1` respectively.
+We assume that your `gitaly1.internal` Gitaly server can be reached at
+`gitaly1.internal:8075` from your GitLab server, and that Gitaly server
+can read and write to `/mnt/gitlab/default` and `/mnt/gitlab/storage1`.
+
+We assume also that your `gitaly2.internal` Gitaly server can be reached at
+`gitaly2.internal:8075` from your GitLab server, and that Gitaly server
+can read and write to `/mnt/gitlab/storage2`.
**For Omnibus GitLab**
@@ -241,8 +275,9 @@ write to `/mnt/gitlab/default` and `/mnt/gitlab/storage1` respectively.
```ruby
git_data_dirs({
- 'default' => { 'gitaly_address' => 'tcp://gitaly.internal:8075' },
- 'storage1' => { 'gitaly_address' => 'tcp://gitaly.internal:8075' },
+ 'default' => { 'gitaly_address' => 'tcp://gitaly1.internal:8075' },
+ 'storage1' => { 'gitaly_address' => 'tcp://gitaly1.internal:8075' },
+ 'storage2' => { 'gitaly_address' => 'tcp://gitaly2.internal:8075' },
})
gitlab_rails['gitaly_token'] = 'abc123secret'
@@ -268,9 +303,11 @@ write to `/mnt/gitlab/default` and `/mnt/gitlab/storage1` respectively.
repositories:
storages:
default:
- gitaly_address: tcp://gitaly.internal:8075
+ gitaly_address: tcp://gitaly1.internal:8075
storage1:
- gitaly_address: tcp://gitaly.internal:8075
+ gitaly_address: tcp://gitaly1.internal:8075
+ storage2:
+ gitaly_address: tcp://gitaly2.internal:8075
gitaly:
token: 'abc123secret'
@@ -350,8 +387,9 @@ To configure Gitaly with TLS:
```ruby
git_data_dirs({
- 'default' => { 'gitaly_address' => 'tls://gitaly.internal:9999' },
- 'storage1' => { 'gitaly_address' => 'tls://gitaly.internal:9999' },
+ 'default' => { 'gitaly_address' => 'tls://gitaly1.internal:9999' },
+ 'storage1' => { 'gitaly_address' => 'tls://gitaly1.internal:9999' },
+ 'storage2' => { 'gitaly_address' => 'tls://gitaly2.internal:9999' },
})
gitlab_rails['gitaly_token'] = 'abc123secret'
@@ -377,9 +415,11 @@ To configure Gitaly with TLS:
repositories:
storages:
default:
- gitaly_address: tls://gitaly.internal:9999
+ gitaly_address: tls://gitaly1.internal:9999
storage1:
- gitaly_address: tls://gitaly.internal:9999
+ gitaly_address: tls://gitaly1.internal:9999
+ storage2:
+ gitaly_address: tls://gitaly2.internal:9999
gitaly:
token: 'abc123secret'
diff --git a/doc/administration/high_availability/load_balancer.md b/doc/administration/high_availability/load_balancer.md
index 9e9f604317a..f11d27487d1 100644
--- a/doc/administration/high_availability/load_balancer.md
+++ b/doc/administration/high_availability/load_balancer.md
@@ -60,11 +60,21 @@ for details on managing SSL certificates and configuring Nginx.
### Basic ports
-| LB Port | Backend Port | Protocol |
-| ------- | ------------ | --------------- |
-| 80 | 80 | HTTP [^1] |
-| 443 | 443 | TCP or HTTPS [^1] [^2] |
-| 22 | 22 | TCP |
+| LB Port | Backend Port | Protocol |
+| ------- | ------------ | ------------------------ |
+| 80 | 80 | HTTP (*1*) |
+| 443 | 443 | TCP or HTTPS (*1*) (*2*) |
+| 22 | 22 | TCP |
+
+- (*1*): [Web terminal](../../ci/environments.md#web-terminals) support requires
+ your load balancer to correctly handle WebSocket connections. When using
+ HTTP or HTTPS proxying, this means your load balancer must be configured
+ to pass through the `Connection` and `Upgrade` hop-by-hop headers. See the
+ [web terminal](../integration/terminal.md) integration guide for
+ more details.
+- (*2*): When using HTTPS protocol for port 443, you will need to add an SSL
+ certificate to the load balancers. If you wish to terminate SSL at the
+ GitLab application server instead, use TCP protocol.
### GitLab Pages Ports
@@ -72,12 +82,19 @@ If you're using GitLab Pages with custom domain support you will need some
additional port configurations.
GitLab Pages requires a separate virtual IP address. Configure DNS to point the
`pages_external_url` from `/etc/gitlab/gitlab.rb` at the new virtual IP address. See the
-[GitLab Pages documentation][gitlab-pages] for more information.
+[GitLab Pages documentation](../pages/index.md) for more information.
-| LB Port | Backend Port | Protocol |
-| ------- | ------------ | -------- |
-| 80 | Varies [^3] | HTTP |
-| 443 | Varies [^3] | TCP [^4] |
+| LB Port | Backend Port | Protocol |
+| ------- | ------------- | --------- |
+| 80 | Varies (*1*) | HTTP |
+| 443 | Varies (*1*) | TCP (*2*) |
+
+- (*1*): The backend port for GitLab Pages depends on the
+ `gitlab_pages['external_http']` and `gitlab_pages['external_https']`
+ setting. See [GitLab Pages documentation](../pages/index.md) for more details.
+- (*2*): Port 443 for GitLab Pages should always use the TCP protocol. Users can
+ configure custom domains with custom SSL, which would not be possible
+ if SSL was terminated at the load balancer.
### Alternate SSH Port
@@ -86,7 +103,7 @@ it may be helpful to configure an alternate SSH hostname that allows users
to use SSH on port 443. An alternate SSH hostname will require a new virtual IP address
compared to the other GitLab HTTP configuration above.
-Configure DNS for an alternate SSH hostname such as altssh.gitlab.example.com.
+Configure DNS for an alternate SSH hostname such as `altssh.gitlab.example.com`.
| LB Port | Backend Port | Protocol |
| ------- | ------------ | -------- |
@@ -101,24 +118,6 @@ Read more on high-availability configuration:
1. [Configure NFS](nfs.md)
1. [Configure the GitLab application servers](gitlab.md)
-[^1]: [Web terminal](../../ci/environments.md#web-terminals) support requires
- your load balancer to correctly handle WebSocket connections. When using
- HTTP or HTTPS proxying, this means your load balancer must be configured
- to pass through the `Connection` and `Upgrade` hop-by-hop headers. See the
- [web terminal](../integration/terminal.md) integration guide for
- more details.
-[^2]: When using HTTPS protocol for port 443, you will need to add an SSL
- certificate to the load balancers. If you wish to terminate SSL at the
- GitLab application server instead, use TCP protocol.
-[^3]: The backend port for GitLab Pages depends on the
- `gitlab_pages['external_http']` and `gitlab_pages['external_https']`
- setting. See [GitLab Pages documentation][gitlab-pages] for more details.
-[^4]: Port 443 for GitLab Pages should always use the TCP protocol. Users can
- configure custom domains with custom SSL, which would not be possible
- if SSL was terminated at the load balancer.
-
-[gitlab-pages]: ../pages/index.md
-
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
diff --git a/doc/administration/index.md b/doc/administration/index.md
index 2b25e8efd23..9dbd90ecea3 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -64,6 +64,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [External Classification Policy Authorization](../user/admin_area/settings/external_authorization.md) **(PREMIUM ONLY)**
- [Upload a license](../user/admin_area/license.md): Upload a license to unlock features that are in paid tiers of GitLab. **(STARTER ONLY)**
- [Admin Area](../user/admin_area/index.md): for self-managed instance-wide configuration and maintenance.
+- [S/MIME Signing](smime_signing_email.md): how to sign all outgoing notification emails with S/MIME
#### Customizing GitLab's appearance
diff --git a/doc/administration/instance_review.md b/doc/administration/instance_review.md
index ab6a4646a71..825435deff9 100644
--- a/doc/administration/instance_review.md
+++ b/doc/administration/instance_review.md
@@ -14,4 +14,3 @@ Additionally you will be contacted by our team for further review which should h
[6995]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6995
[ee]: https://about.gitlab.com/pricing/
-
diff --git a/doc/administration/job_artifacts.md b/doc/administration/job_artifacts.md
index 2b624f8de77..60921ca791f 100644
--- a/doc/administration/job_artifacts.md
+++ b/doc/administration/job_artifacts.md
@@ -115,7 +115,7 @@ The connection settings match those provided by [Fog](https://github.com/fog), a
| `aws_access_key_id` | AWS credentials, or compatible | |
| `aws_secret_access_key` | AWS credentials, or compatible | |
| `aws_signature_version` | AWS signature version to use. 2 or 4 are valid options. Digital Ocean Spaces and other providers may need 2. | 4 |
-| `enable_signature_v4_streaming` | Set to true to enable HTTP chunked transfers with AWS v4 signatures (https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html). Oracle Cloud S3 needs this to be false | true
+| `enable_signature_v4_streaming` | Set to true to enable HTTP chunked transfers with [AWS v4 signatures](https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html). Oracle Cloud S3 needs this to be false | true |
| `region` | AWS region | us-east-1 |
| `host` | S3 compatible host for when not using AWS, e.g. `localhost` or `storage.example.com` | s3.amazonaws.com |
| `endpoint` | Can be used when configuring an S3 compatible service such as [Minio](https://www.minio.io), by entering a URL such as `http://127.0.0.1:9000` | (optional) |
diff --git a/doc/administration/logs.md b/doc/administration/logs.md
index a57ef8ddc7d..9030c941cad 100644
--- a/doc/administration/logs.md
+++ b/doc/administration/logs.md
@@ -88,7 +88,7 @@ Introduced in GitLab 10.0, this file lives in
It helps you see requests made directly to the API. For example:
```json
-{"time":"2018-10-29T12:49:42.123Z","severity":"INFO","duration":709.08,"db":14.59,"view":694.49,"status":200,"method":"GET","path":"/api/v4/projects","params":[{"key":"action","value":"git-upload-pack"},{"key":"changes","value":"_any"},{"key":"key_id","value":"secret"},{"key":"secret_token","value":"[FILTERED]"}],"host":"localhost","ip":"::1","ua":"Ruby","route":"/api/:version/projects","user_id":1,"username":"root","queue_duration":100.31,"gitaly_calls":30,"gitaly_duration":5.36}
+{"time":"2018-10-29T12:49:42.123Z","severity":"INFO","duration":709.08,"db":14.59,"view":694.49,"status":200,"method":"GET","path":"/api/v4/projects","params":[{"key":"action","value":"git-upload-pack"},{"key":"changes","value":"_any"},{"key":"key_id","value":"secret"},{"key":"secret_token","value":"[FILTERED]"}],"host":"localhost","remote_ip":"::1","ua":"Ruby","route":"/api/:version/projects","user_id":1,"username":"root","queue_duration":100.31,"gitaly_calls":30,"gitaly_duration":5.36}
```
This entry above shows an access to an internal endpoint to check whether an
diff --git a/doc/administration/merge_request_diffs.md b/doc/administration/merge_request_diffs.md
index 0b065082ded..d52d865cec5 100644
--- a/doc/administration/merge_request_diffs.md
+++ b/doc/administration/merge_request_diffs.md
@@ -90,7 +90,7 @@ The connection settings match those provided by [Fog](https://github.com/fog), a
| `aws_access_key_id` | AWS credentials, or compatible | |
| `aws_secret_access_key` | AWS credentials, or compatible | |
| `aws_signature_version` | AWS signature version to use. 2 or 4 are valid options. Digital Ocean Spaces and other providers may need 2. | 4 |
-| `enable_signature_v4_streaming` | Set to true to enable HTTP chunked transfers with AWS v4 signatures (https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html). Oracle Cloud S3 needs this to be false | true
+| `enable_signature_v4_streaming` | Set to true to enable HTTP chunked transfers with [AWS v4 signatures](https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html). Oracle Cloud S3 needs this to be false | true |
| `region` | AWS region | us-east-1 |
| `host` | S3 compatible host for when not using AWS, e.g. `localhost` or `storage.example.com` | s3.amazonaws.com |
| `endpoint` | Can be used when configuring an S3 compatible service such as [Minio](https://www.minio.io), by entering a URL such as `http://127.0.0.1:9000` | (optional) |
diff --git a/doc/administration/monitoring/gitlab_instance_administration_project/index.md b/doc/administration/monitoring/gitlab_instance_administration_project/index.md
index 8e33cea6217..d445b68721d 100644
--- a/doc/administration/monitoring/gitlab_instance_administration_project/index.md
+++ b/doc/administration/monitoring/gitlab_instance_administration_project/index.md
@@ -27,7 +27,7 @@ If that's not the case or if you have an external Prometheus instance or an HA s
you should
[configure it manually](../../../user/project/integrations/prometheus.md#manual-configuration-of-prometheus).
-## Taking action on Prometheus alerts **[ULTIMATE]**
+## Taking action on Prometheus alerts **(ULTIMATE)**
You can [add a webhook](../../../user/project/integrations/prometheus.md#external-prometheus-instances)
to the Prometheus config in order for GitLab to receive notifications of any alerts.
diff --git a/doc/administration/monitoring/performance/request_profiling.md b/doc/administration/monitoring/performance/request_profiling.md
index 9f671e0db11..c32edb60f9d 100644
--- a/doc/administration/monitoring/performance/request_profiling.md
+++ b/doc/administration/monitoring/performance/request_profiling.md
@@ -5,7 +5,7 @@
1. Grab the profiling token from **Monitoring > Requests Profiles** admin page
(highlighted in a blue in the image below).
![Profile token](img/request_profiling_token.png)
-1. Pass the header `X-Profile-Token: <token>` and `X-Profile-Mode: <mode>`(where <mode> can be `execution` or `memory`) to the request you want to profile. You can use:
+1. Pass the header `X-Profile-Token: <token>` and `X-Profile-Mode: <mode>`(where `<mode>` can be `execution` or `memory`) to the request you want to profile. You can use:
- Browser extensions. For example, [ModHeader](https://chrome.google.com/webstore/detail/modheader/idgpnmonknjnojddfkpgkljpfnnfcklj) Chrome extension.
- `curl`. For example, `curl --header 'X-Profile-Token: <token>' --header 'X-Profile-Mode: <mode>' https://gitlab.example.com/group/project`.
1. Once request is finished (which will take a little longer than usual), you can
diff --git a/doc/administration/plugins.md b/doc/administration/plugins.md
index 4cf3c607dae..52f1e7cde28 100644
--- a/doc/administration/plugins.md
+++ b/doc/administration/plugins.md
@@ -112,4 +112,4 @@ Validating plugins from /plugins directory
[system hooks]: ../system_hooks/system_hooks.md
[webhooks]: ../user/project/integrations/webhooks.md
-[highly available]: ./high_availability/README.md \ No newline at end of file
+[highly available]: ./high_availability/README.md
diff --git a/doc/administration/reply_by_email_postfix_setup.md b/doc/administration/reply_by_email_postfix_setup.md
index 406f7e8a034..6d1ffbffefb 100644
--- a/doc/administration/reply_by_email_postfix_setup.md
+++ b/doc/administration/reply_by_email_postfix_setup.md
@@ -331,7 +331,7 @@ Courier, which we will install later to add IMAP authentication, requires mailbo
a logout
```
-## Done!
+## Done
If all the tests were successful, Postfix is all set up and ready to receive email! Continue with the [incoming email] guide to configure GitLab.
diff --git a/doc/administration/smime_signing_email.md b/doc/administration/smime_signing_email.md
new file mode 100644
index 00000000000..9f719088f25
--- /dev/null
+++ b/doc/administration/smime_signing_email.md
@@ -0,0 +1,49 @@
+# Signing outgoing email with S/MIME
+
+Notification emails sent by Gitlab can be signed with S/MIME for improved
+security.
+
+> **Note:**
+Please be aware that S/MIME certificates and TLS/SSL certificates are not the
+same and are used for different purposes: TLS creates a secure channel, whereas
+S/MIME signs and/or encrypts the message itself
+
+## Enable S/MIME signing
+
+This setting must be explicitly enabled and a single pair of key and certificate
+files must be provided in `gitlab.rb` or `gitlab.yml` if you are using Omnibus
+GitLab or installed GitLab from source respectively:
+
+```yaml
+email_smime:
+ enabled: true
+ key_file: /etc/pki/smime/private/gitlab.key
+ cert_file: /etc/pki/smime/certs/gitlab.crt
+```
+
+- Both files must be provided PEM-encoded.
+- The key file must be unencrypted so that Gitlab can read it without user
+ intervention.
+
+NOTE: **Note:** Be mindful of the access levels for your private keys and visibility to
+third parties.
+
+### How to convert S/MIME PKCS#12 / PFX format to PEM encoding
+
+Typically S/MIME certificates are handled in binary PKCS#12 format (`.pfx` or `.p12`
+extensions), which contain the following in a single encrypted file:
+
+- Server certificate
+- Intermediate certificates (if any)
+- Private key
+
+In order to export the required files in PEM encoding from the PKCS#12 file,
+the `openssl` command can be used:
+
+```bash
+#-- Extract private key in PEM encoding (no password, unencrypted)
+$ openssl pkcs12 -in gitlab.p12 -nocerts -nodes -out gitlab.key
+
+#-- Extract certificates in PEM encoding (full certs chain including CA)
+$ openssl pkcs12 -in gitlab.p12 -nokeys -out gitlab.crt
+```
diff --git a/doc/administration/troubleshooting/kubernetes_cheat_sheet.md b/doc/administration/troubleshooting/kubernetes_cheat_sheet.md
index 95cdb1508fa..238c522a0ee 100644
--- a/doc/administration/troubleshooting/kubernetes_cheat_sheet.md
+++ b/doc/administration/troubleshooting/kubernetes_cheat_sheet.md
@@ -29,7 +29,7 @@ and they will assist you with any issues you are having.
```bash
# for minikube:
minikube dashboard —url
- # for non-local installations if access via kubectl is configured:
+ # for non-local installations if access via Kubectl is configured:
kubectl proxy
```
@@ -49,7 +49,7 @@ and they will assist you with any issues you are having.
- What to do with pods in `CrashLoopBackoff` status:
- Check logs via Kubernetes dashboard.
- - Check logs via `kubectl`:
+ - Check logs via Kubectl:
```bash
kubectl logs <unicorn pod> -c dependencies
@@ -72,7 +72,7 @@ and they will assist you with any issues you are having.
This is the principle of Kubernetes, read [Twelve-factor app](https://12factor.net/)
for details.
-## Gitlab-specific kubernetes information
+## GitLab-specific kubernetes information
- Minimal config that can be used to test a Kubernetes helm chart can be found
[here](https://gitlab.com/charts/gitlab/issues/620).
@@ -83,7 +83,7 @@ and they will assist you with any issues you are having.
kubectl logs gitlab-unicorn-7656fdd6bf-jqzfs -c unicorn
```
-- It is not possible to get all the logs via `kubectl` at once, like with `gitlab-ctl tail`,
+- It is not possible to get all the logs via Kubectl at once, like with `gitlab-ctl tail`,
but a number of third-party tools can be used to do it:
- [Kubetail](https://github.com/johanhaleby/kubetail)
@@ -131,7 +131,7 @@ and they will assist you with any issues you are having.
- Check the output of `kubectl get events -w --all-namespaces`.
- Check the logs of pods within `gitlab-managed-apps` namespace.
- On the side of GitLab check sidekiq log and kubernetes log. When GitLab is installed
- via helm chart, kubernetes.log can be found inside the sidekiq pod.
+ via Helm Chart, `kubernetes.log` can be found inside the sidekiq pod.
- How to get your initial admin password <https://docs.gitlab.com/charts/installation/deployment.html#initial-login>:
@@ -142,19 +142,19 @@ and they will assist you with any issues you are having.
kubectl get secret <secret-name> -ojsonpath={.data.password} | base64 --decode ; echo
```
-- How to connect to a GitLab postgres database:
+- How to connect to a GitLab Postgres database:
```bash
kubectl exec -it <task-runner-pod-name> -- /srv/gitlab/bin/rails dbconsole -p
```
-- How to get info about helm installation status:
+- How to get info about Helm installation status:
```bash
helm status name-of-installation
```
-- How to update GitLab installed using helm chart:
+- How to update GitLab installed using Helm Chart:
```bash
helm repo upgrade
@@ -179,25 +179,25 @@ and they will assist you with any issues you are having.
helm upgrade <release name> <chart path> -f gitlab.yaml
```
-## Installation of minimal GitLab config via minukube on macOS
+## Installation of minimal GitLab config via Minukube on macOS
This section is based on [Developing for Kubernetes with Minikube](https://gitlab.com/charts/gitlab/blob/master/doc/minikube/index.md)
and [Helm](https://gitlab.com/charts/gitlab/blob/master/doc/helm/index.md). Refer
to those documents for details.
-- Install kubectl via Homebrew:
+- Install Kubectl via Homebrew:
```bash
brew install kubernetes-cli
```
-- Install minikube via Homebrew:
+- Install Minikube via Homebrew:
```bash
brew cask install minikube
```
-- Start minikube and configure it. If minikube cannot start, try running `minikube delete && minikube start`
+- Start Minikube and configure it. If Minikube cannot start, try running `minikube delete && minikube start`
and repeat the steps:
```bash
@@ -206,7 +206,7 @@ to those documents for details.
minikube addons enable kube-dns
```
-- Install helm via Homebrew and initialize it:
+- Install Helm via Homebrew and initialize it:
```bash
brew install kubernetes-helm
@@ -219,7 +219,7 @@ to those documents for details.
- Find the IP address in the output of `minikube ip` and update the yaml file with
this IP address.
-- Install the GitLab helm chart:
+- Install the GitLab Helm Chart:
```bash
helm repo add gitlab https://charts.gitlab.io
diff --git a/doc/administration/troubleshooting/sidekiq.md b/doc/administration/troubleshooting/sidekiq.md
index 7067958ecb4..9b016c64e29 100644
--- a/doc/administration/troubleshooting/sidekiq.md
+++ b/doc/administration/troubleshooting/sidekiq.md
@@ -169,3 +169,121 @@ The PostgreSQL wiki has details on the query you can run to see blocking
queries. The query is different based on PostgreSQL version. See
[Lock Monitoring](https://wiki.postgresql.org/wiki/Lock_Monitoring) for
the query details.
+
+## Managing Sidekiq queues
+
+It is possible to use [Sidekiq API](https://github.com/mperham/sidekiq/wiki/API)
+to perform a number of troubleshoting on Sidekiq.
+
+These are the administrative commands and it should only be used if currently
+admin interface is not suitable due to scale of installation.
+
+All this commands should be run using `gitlab-rails console`.
+
+### View the queue size
+
+```ruby
+Sidekiq::Queue.new("pipeline_processing:build_queue").size
+```
+
+### Enumerate all enqueued jobs
+
+```ruby
+queue = Sidekiq::Queue.new("chaos:chaos_sleep")
+queue.each do |job|
+ # job.klass # => 'MyWorker'
+ # job.args # => [1, 2, 3]
+ # job.jid # => jid
+ # job.queue # => chaos:chaos_sleep
+ # job["retry"] # => 3
+ # job.item # => {
+ # "class"=>"Chaos::SleepWorker",
+ # "args"=>[1000],
+ # "retry"=>3,
+ # "queue"=>"chaos:chaos_sleep",
+ # "backtrace"=>true,
+ # "queue_namespace"=>"chaos",
+ # "jid"=>"39bc482b823cceaf07213523",
+ # "created_at"=>1566317076.266069,
+ # "correlation_id"=>"c323b832-a857-4858-b695-672de6f0e1af",
+ # "enqueued_at"=>1566317076.26761},
+ # }
+
+ # job.delete if job.jid == 'abcdef1234567890'
+end
+```
+
+### Enumerate currently running jobs
+
+```ruby
+workers = Sidekiq::Workers.new
+workers.each do |process_id, thread_id, work|
+ # process_id is a unique identifier per Sidekiq process
+ # thread_id is a unique identifier per thread
+ # work is a Hash which looks like:
+ # {"queue"=>"chaos:chaos_sleep",
+ # "payload"=>
+ # { "class"=>"Chaos::SleepWorker",
+ # "args"=>[1000],
+ # "retry"=>3,
+ # "queue"=>"chaos:chaos_sleep",
+ # "backtrace"=>true,
+ # "queue_namespace"=>"chaos",
+ # "jid"=>"b2a31e3eac7b1a99ff235869",
+ # "created_at"=>1566316974.9215662,
+ # "correlation_id"=>"e484fb26-7576-45f9-bf21-b99389e1c53c",
+ # "enqueued_at"=>1566316974.9229589},
+ # "run_at"=>1566316974}],
+end
+```
+
+### Remove sidekiq jobs for given parameters (destructive)
+
+```ruby
+# for jobs like this:
+# RepositoryImportWorker.new.perform_async(100)
+id_list = [100]
+
+queue = Sidekiq::Queue.new('repository_import')
+queue.each do |job|
+ job.delete if id_list.include?(job.args[0])
+end
+```
+
+### Remove specific job ID (destructive)
+
+```ruby
+queue = Sidekiq::Queue.new('repository_import')
+queue.each do |job|
+ job.delete if job.jid == 'my-job-id'
+end
+```
+
+## Canceling running jobs (destructive)
+
+> Introduced in GitLab 12.3.
+
+This is highly risky operation and use it as last resort.
+Doing that might result in data corruption, as the job
+is interrupted mid-execution and it is not guaranteed
+that proper rollback of transactions is implemented.
+
+```ruby
+Gitlab::SidekiqMonitor.cancel_job('job-id')
+```
+
+> This requires the Sidekiq to be run with `SIDEKIQ_MONITOR_WORKER=1`
+> environment variable.
+
+To perform of the interrupt we use `Thread.raise` which
+has number of drawbacks, as mentioned in [Why Ruby’s Timeout is dangerous (and Thread.raise is terrifying)](https://jvns.ca/blog/2015/11/27/why-rubys-timeout-is-dangerous-and-thread-dot-raise-is-terrifying/):
+
+> This is where the implications get interesting, and terrifying. This means that an exception can get raised:
+>
+> * during a network request (ok, as long as the surrounding code is prepared to catch Timeout::Error)
+> * during the cleanup for the network request
+> * during a rescue block
+> * while creating an object to save to the database afterwards
+> * in any of your code, regardless of whether it could have possibly raised an exception before
+>
+> Nobody writes code to defend against an exception being raised on literally any line. That’s not even possible. So Thread.raise is basically like a sneak attack on your code that could result in almost anything. It would probably be okay if it were pure-functional code that did not modify any state. But this is Ruby, so that’s unlikely :)
diff --git a/doc/administration/uploads.md b/doc/administration/uploads.md
index 99f1c386183..9e9d12f23bb 100644
--- a/doc/administration/uploads.md
+++ b/doc/administration/uploads.md
@@ -78,7 +78,7 @@ The connection settings match those provided by [Fog](https://github.com/fog), a
| `aws_access_key_id` | AWS credentials, or compatible | |
| `aws_secret_access_key` | AWS credentials, or compatible | |
| `aws_signature_version` | AWS signature version to use. 2 or 4 are valid options. Digital Ocean Spaces and other providers may need 2. | 4 |
-| `enable_signature_v4_streaming` | Set to true to enable HTTP chunked transfers with AWS v4 signatures (https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html). Oracle Cloud S3 needs this to be false | true
+| `enable_signature_v4_streaming` | Set to true to enable HTTP chunked transfers with [AWS v4 signatures](https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html). Oracle Cloud S3 needs this to be false | true |
| `region` | AWS region | us-east-1 |
| `host` | S3 compatible host for when not using AWS, e.g. `localhost` or `storage.example.com` | s3.amazonaws.com |
| `endpoint` | Can be used when configuring an S3 compatible service such as [Minio](https://www.minio.io), by entering a URL such as `http://127.0.0.1:9000` | (optional) |
@@ -220,7 +220,7 @@ _The uploads are stored by default in
openstack_temp_url_key: OPENSTACK_TEMP_URL_KEY
openstack_auth_url: 'https://auth.cloud.ovh.net/v2.0/'
openstack_region: DE1
- openstack_tenant: 'TENANT_ID'
+ openstack_tenant: 'TENANT_ID'
```
1. Save the file and [reconfigure GitLab][] for the changes to take effect.
diff --git a/doc/api/README.md b/doc/api/README.md
index 9156d719e11..33394d46a2d 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -157,8 +157,8 @@ for example, without needing to explicitly pass an access token.
With a few API endpoints you can use a [GitLab CI job token](../user/project/new_ci_build_permissions_model.md#job-token)
to authenticate with the API:
-* [Get job artifacts](jobs.md#get-job-artifacts)
-* [Pipeline triggers](pipeline_triggers.md)
+- [Get job artifacts](jobs.md#get-job-artifacts)
+- [Pipeline triggers](pipeline_triggers.md)
### Impersonation tokens
diff --git a/doc/api/api_resources.md b/doc/api/api_resources.md
index b32f11464ef..9af5430f1c8 100644
--- a/doc/api/api_resources.md
+++ b/doc/api/api_resources.md
@@ -67,7 +67,7 @@ The following API resources are available in the project context:
| [Search](search.md) | `/projects/:id/search` (also available for groups and standalone) |
| [Services](services.md) | `/projects/:id/services` |
| [Tags](tags.md) | `/projects/:id/repository/tags` |
-| [Vulnerabilities](vulnerabilities.md) **(ULTIMATE)** | `/projects/:id/vulnerabilities` (also available for groups) |
+| [Vulnerabilities](vulnerabilities.md) **(ULTIMATE)** | `/projects/:id/vulnerabilities`
| [Wikis](wikis.md) | `/projects/:id/wikis` |
## Group resources
diff --git a/doc/api/boards.md b/doc/api/boards.md
index 08ec1d832df..b848d7788cd 100644
--- a/doc/api/boards.md
+++ b/doc/api/boards.md
@@ -466,7 +466,7 @@ Example response:
## Delete a board list
-Only for admins and project owners. Soft deletes the board list in question.
+Only for admins and project owners. Deletes the board list in question.
```
DELETE /projects/:id/boards/:board_id/lists/:list_id
diff --git a/doc/api/dependencies.md b/doc/api/dependencies.md
index 015ffbe60f6..5767d3572dd 100644
--- a/doc/api/dependencies.md
+++ b/doc/api/dependencies.md
@@ -5,15 +5,15 @@ This API is in an alpha stage and considered unstable.
The response payload may be subject to change or breakage
across GitLab releases.
-Every call to this endpoint requires authentication. To perform this call, user should be authorized to read
-[Project Security Dashboard](../user/application_security/security_dashboard/index.md#project-security-dashboard).
+Every call to this endpoint requires authentication. To perform this call, user should be authorized to read
+[Project Security Dashboard](../user/application_security/security_dashboard/index.md#project-security-dashboard).
## List project dependencies
-Get a list of project dependencies. This API partially mirroring
+Get a list of project dependencies. This API partially mirroring
[Dependency List](../user/application_security/dependency_list/index.md) feature.
This list can be generated only for [languages and package managers](../user/application_security/dependency_scanning/index.md#supported-languages-and-package-managers)
-supported by Gemnasium.
+supported by Gemnasium.
```
GET /projects/:id/dependencies
diff --git a/doc/api/deploy_keys.md b/doc/api/deploy_keys.md
index 94351e1a300..df8cf06fc4a 100644
--- a/doc/api/deploy_keys.md
+++ b/doc/api/deploy_keys.md
@@ -203,6 +203,7 @@ Example response:
"created_at" : "2015-08-29T12:44:31.550Z"
}
```
+
## Adding deploy keys to multiple projects
If you want to easily add the same deploy key to multiple projects in the same
diff --git a/doc/api/discussions.md b/doc/api/discussions.md
index b4a2d0b15f6..12dbba78291 100644
--- a/doc/api/discussions.md
+++ b/doc/api/discussions.md
@@ -160,7 +160,7 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab
Adds a new note to the thread. This can also [create a thread from a single comment](../user/discussions/#start-a-thread-by-replying-to-a-standard-comment).
**WARNING**
-Notes can be added to other items than comments (system notes, etc.) making them threads.
+Notes can be added to other items than comments (system notes, etc.) making them threads.
```
POST /projects/:id/issues/:issue_iid/discussions/:discussion_id/notes
diff --git a/doc/api/group_boards.md b/doc/api/group_boards.md
index 4d10f83720b..99b522a7ae9 100644
--- a/doc/api/group_boards.md
+++ b/doc/api/group_boards.md
@@ -536,7 +536,7 @@ Example response:
## Delete a group issue board list
-Only for admins and group owners. Soft deletes the board list in question.
+Only for admins and group owners. Deletes the board list in question.
```
DELETE /groups/:id/boards/:board_id/lists/:list_id
diff --git a/doc/api/issues.md b/doc/api/issues.md
index 11c18ef94ba..4f2b4a966c9 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -788,7 +788,7 @@ the `weight` parameter:
## Delete an issue
-Only for admins and project owners. Soft deletes the issue in question.
+Only for admins and project owners. Deletes the issue in question.
```
DELETE /projects/:id/issues/:issue_iid
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 1ade46efb1c..49ed4968b0d 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -1127,7 +1127,7 @@ the `approvals_before_merge` parameter:
## Delete a merge request
-Only for admins and project owners. Soft deletes the merge request in question.
+Only for admins and project owners. Deletes the merge request in question.
```
DELETE /projects/:id/merge_requests/:merge_request_iid
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 70df44ec0fd..373607f8f4b 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -855,6 +855,7 @@ GET /projects/:id/users
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `search` | string | no | Search for specific users |
+| `skip_users` | array[int] | no | Filter out users with the specified IDs |
```json
[
diff --git a/doc/api/protected_branches.md b/doc/api/protected_branches.md
index 9309306ba05..ffb4a70168b 100644
--- a/doc/api/protected_branches.md
+++ b/doc/api/protected_branches.md
@@ -185,6 +185,7 @@ Example response:
{
"access_level": 30,
"access_level_description": "Developers + Maintainers"
+ }
],
"unprotect_access_levels": [
{
@@ -217,6 +218,7 @@ Example response:
"user_id": null,
"group_id": null,
"access_level_description": "Developers + Maintainers"
+ }
],
"unprotect_access_levels": [
{
@@ -232,7 +234,7 @@ Example response:
### Example with user / group level access **(STARTER)**
Elements in the `allowed_to_push` / `allowed_to_merge` / `allowed_to_unprotect` array should take the
-form `{user_id: integer}`, `{group_id: integer}` or `{access_level: integer}`. Each user must have access to the project and each group must [have this project shared](../user/project/members/share_project_with_groups.md). These access levels allow [more granular control over protected branch access](../user/project/protected_branches.md#restricting-push-and-merge-access-to-certain-users-starter) and were [added to the API in ][ee-3516] in GitLab 10.3 EE.
+form `{user_id: integer}`, `{group_id: integer}` or `{access_level: integer}`. Each user must have access to the project and each group must [have this project shared](../user/project/members/share_project_with_groups.md). These access levels allow [more granular control over protected branch access](../user/project/protected_branches.md#restricting-push-and-merge-access-to-certain-users-starter) and were [added to the API in](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3516) in GitLab 10.3 EE.
```bash
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" 'https://gitlab.example.com/api/v4/projects/5/protected_branches?name=*-stable&allowed_to_push%5B%5D%5Buser_id%5D=1'
@@ -242,29 +244,29 @@ Example response:
```json
{
- "name":"*-stable",
+ "name": "*-stable",
"push_access_levels": [
{
- "access_level":null,
- "user_id":1,
- "group_id":null,
- "access_level_description":"Administrator"
+ "access_level": null,
+ "user_id": 1,
+ "group_id": null,
+ "access_level_description": "Administrator"
}
],
"merge_access_levels": [
{
- "access_level":40,
- "user_id":null,
- "group_id":null,
- "access_level_description":"Maintainers"
+ "access_level": 40,
+ "user_id": null,
+ "group_id": null,
+ "access_level_description": "Maintainers"
}
],
"unprotect_access_levels": [
{
- "access_level":40,
- "user_id":null,
- "group_id":null,
- "access_level_description":"Maintainers"
+ "access_level": 40,
+ "user_id": null,
+ "group_id": null,
+ "access_level_description": "Maintainers"
}
]
}
@@ -286,5 +288,3 @@ curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" 'https://git
| --------- | ---- | -------- | ----------- |
| `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 branch |
-
-[ee-3516]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3516 "ProtectedBranches API handles per user/group granularity"
diff --git a/doc/api/settings.md b/doc/api/settings.md
index 248d19461f6..4b5b2b924d7 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -322,7 +322,7 @@ are listed in the descriptions of the relevant settings.
| `version_check_enabled` | boolean | no | Let GitLab inform you when an update is available. |
| `local_markdown_version` | integer | no | Increase this value when any cached markdown should be invalidated. |
| `snowplow_enabled` | boolean | no | Enable snowplow tracking. |
-| `snowplow_collector_hostname` | string | required by: `snowplow_enabled` | The Snowplow collector hostname. (e.g. `snowplow.trx.gitlab.net`) |
+| `snowplow_collector_hostname` | string | required by: `snowplow_enabled` | The Snowplow collector hostname. (e.g. `snowplow.trx.gitlab.net`) |
| `snowplow_site_id` | string | no | The Snowplow site name / application id. (e.g. `gitlab`) |
| `snowplow_cookie_domain` | string | no | The Snowplow cookie domain. (e.g. `.gitlab.com`) |
| `geo_node_allowed_ips` | string | yes | **(PREMIUM)** Comma-separated list of IPs and CIDRs of allowed secondary nodes. For example, `1.1.1.1, 2.2.2.0/24`. |
diff --git a/doc/ci/README.md b/doc/ci/README.md
index ca9d0aa61bd..94da8354f0b 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -131,7 +131,7 @@ Its feature set is listed on the table below according to DevOps stages.
| **Secure** ||
| [Container Scanning](../user/application_security/container_scanning/index.md) **(ULTIMATE)** | Check your Docker containers for known vulnerabilities.|
| [Dependency Scanning](../user/application_security/dependency_scanning/index.md) **(ULTIMATE)** | Analyze your dependencies for known vulnerabilities. |
-| [License Management](../user/application_security/license_management/index.md) **(ULTIMATE)** | Search your project dependencies for their licenses. |
+| [License Compliance](../user/application_security/license_management/index.md) **(ULTIMATE)** | Search your project dependencies for their licenses. |
| [Security Test reports](../user/project/merge_requests/index.md#security-reports-ultimate) **(ULTIMATE)** | Check for app vulnerabilities. |
## Examples
diff --git a/doc/ci/directed_acyclic_graph/index.md b/doc/ci/directed_acyclic_graph/index.md
index e54be9f3bd9..1c38c08b7cb 100644
--- a/doc/ci/directed_acyclic_graph/index.md
+++ b/doc/ci/directed_acyclic_graph/index.md
@@ -65,7 +65,7 @@ as quickly as possible.
Relationships are defined between jobs using the [`needs:` keyword](../yaml/README.md#needs).
Note that `needs:` also works with the [parallel](../yaml/README.md#parallel) keyword,
-giving your powerful options for parallelization within your pipeline.
+giving you powerful options for parallelization within your pipeline.
## Limitations
diff --git a/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md
index ad07c662965..126e12e460f 100644
--- a/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md
+++ b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md
@@ -35,10 +35,9 @@ get out of WIP status or resolve merge conflicts as soon as possible.
## Requirements and limitations
-Pipelines for merged results require:
+Pipelines for merged results require a [GitLab Runner][runner] 11.9 or newer.
-- [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-runner) 11.9 or newer.
-- [Gitaly](https://gitlab.com/gitlab-org/gitaly) 1.21.0 or newer.
+[runner]: https://gitlab.com/gitlab-org/gitlab-runner
In addition, pipelines for merged results have the following limitations:
diff --git a/doc/ci/multi_project_pipelines.md b/doc/ci/multi_project_pipelines.md
index cb8d383f7d9..61f260cb70d 100644
--- a/doc/ci/multi_project_pipelines.md
+++ b/doc/ci/multi_project_pipelines.md
@@ -205,5 +205,5 @@ Some features are not implemented yet. For example, support for environments.
- `stage`
- `allow_failure`
- `only` and `except`
-- `when`
+- `when` (only with `on_success`, `on_failure`, and `always` values)
- `extends`
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
index ed8d0e3bc35..eaa6efc526d 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -377,6 +377,10 @@ This functionality is only available:
- For users with at least Developer access.
- If the the stage contains [manual actions](#manual-actions-from-pipeline-graphs).
+## Most Recent Pipeline
+
+There's a link to the latest pipeline for the last commit of a given branch at `/project/pipelines/[branch]/latest`. Also, `/project/pipelines/latest` will redirect you to the latest pipeline for the last commit on the project's default branch.
+
## Security on protected branches
A strict security model is enforced when pipelines are executed on
diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md
index d9f022a7125..b6aebd3bd78 100644
--- a/doc/ci/ssh_keys/README.md
+++ b/doc/ci/ssh_keys/README.md
@@ -76,7 +76,7 @@ to access it. This is where an SSH key pair comes in handy.
## without extra base64 encoding.
## https://gitlab.com/gitlab-examples/ssh-private-key/issues/1#note_48526556
##
- - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null
+ - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
##
## Create the SSH directory and give it the right permissions
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index d741482b662..5a15b907da0 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -94,7 +94,10 @@ This means that the value of the variable will be hidden in job logs,
though it must match certain requirements to do so:
- The value must be in a single line.
-- The value must only consist of characters from the Base64 alphabet ([RFC4648](https://tools.ietf.org/html/rfc4648)) with the addition of `@` and `:`.
+- The value must only consist of characters from the Base64 alphabet (RFC4648).
+
+ [In GitLab 12.2](https://gitlab.com/gitlab-org/gitlab-ce/issues/63043)
+ and newer, `@` and `:` are also valid values.
- The value must be at least 8 characters long.
- The value must not use variables.
@@ -509,7 +512,7 @@ Below you can find supported syntax reference:
1. Checking for an empty variable
Examples:
-
+
- `$VARIABLE == ""`
- `$VARIABLE != ""` (introduced in GitLab 11.11)
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 76b73729d0b..89a61b2a9e3 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -518,10 +518,24 @@ Four keys are available:
- `changes`
- `kubernetes`
-If you use multiple keys under `only` or `except`, they act as an AND. The logic is:
+If you use multiple keys under `only` or `except`, the keys will be evaluated as a
+single conjoined expression. That is:
+
+- `only:` means "include this job if all of the conditions match".
+- `except:` means "exclude this job if any of the conditions match".
+
+With `only`, individual keys are logically joined by an AND:
> (any of refs) AND (any of variables) AND (any of changes) AND (if kubernetes is active)
+`except` is implemented as a negation of this complete expression:
+
+> NOT((any of refs) AND (any of variables) AND (any of changes) AND (if kubernetes is active))
+
+This, more intuitively, means the keys join by an OR. A functionally equivalent expression:
+
+> (any of refs) OR (any of variables) OR (any of changes) OR (if kubernetes is active)
+
#### `only:refs`/`except:refs`
> `refs` policy introduced in GitLab 10.0.
@@ -1568,7 +1582,7 @@ dashboards.
The `license_management` report collects [Licenses](../../user/project/merge_requests/license_management.md)
as artifacts.
-The collected License Management report will be uploaded to GitLab as an artifact and will
+The collected License Compliance report will be uploaded to GitLab as an artifact and will
be automatically shown in merge requests, pipeline view and provide data for security
dashboards.
@@ -1721,7 +1735,7 @@ This example creates three paths of execution:
1. If `needs:` is set to point to a job that is not instantiated
because of `only/except` rules or otherwise does not exist, the
job will fail.
-1. Note that on day one of the launch, we are temporarily limiting the
+1. Note that on day one of the launch, we are temporarily limiting the
maximum number of jobs that a single job can need in the `needs:` array. Track
our [infrastructure issue](https://gitlab.com/gitlab-com/gl-infra/infrastructure/issues/7541)
for details on the current limit.
@@ -1734,8 +1748,8 @@ This example creates three paths of execution:
in the first stage (see [gitlab-ce#65504](https://gitlab.com/gitlab-org/gitlab-ce/issues/65504)).
1. If `needs:` refers to a job that is marked as `parallel:`.
the current job will depend on all parallel jobs created.
-1. `needs:` is similar to `dependencies:` in that needs to use jobs from
- prior stages, this means that it is impossible to create circular
+1. `needs:` is similar to `dependencies:` in that it needs to use jobs from
+ prior stages, meaning it is impossible to create circular
dependencies or depend on jobs in the current stage (see [gitlab-ce#65505](https://gitlab.com/gitlab-org/gitlab-ce/issues/65505)).
1. Related to the above, stages must be explicitly defined for all jobs
that have the keyword `needs:` or are referred to by one.
diff --git a/doc/development/README.md b/doc/development/README.md
index 6281bb809ff..5e35d4c7437 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -5,7 +5,7 @@ description: 'Learn how to contribute to GitLab.'
# Contributor and Development Docs
-## Get started!
+## Get started
- Set up GitLab's development environment with [GitLab Development Kit (GDK)](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/README.md)
- [GitLab contributing guide](contributing/index.md)
@@ -116,6 +116,7 @@ description: 'Learn how to contribute to GitLab.'
## Case studies
- [Database case study: Filtering by label](filtering_by_label.md)
+- [Database case study: Namespaces storage statistics](namespaces_storage_statistics.md)
## Integration guides
diff --git a/doc/development/api_styleguide.md b/doc/development/api_styleguide.md
index 0866d3baeeb..61576236c96 100644
--- a/doc/development/api_styleguide.md
+++ b/doc/development/api_styleguide.md
@@ -51,7 +51,7 @@ allowed.
– <https://github.com/ruby-grape/grape#declared>
-### Exclude params from parent namespaces!
+### Exclude params from parent namespaces
> By default `declared(params)`includes parameters that were defined in all
parent namespaces.
@@ -64,7 +64,7 @@ In most cases you will want to exclude params from the parent namespaces:
declared(params, include_parent_namespaces: false)
```
-### When to use `declared(params)`?
+### When to use `declared(params)`
You should always use `declared(params)` when you pass the params hash as
arguments to a method call.
diff --git a/doc/development/automatic_ce_ee_merge.md b/doc/development/automatic_ce_ee_merge.md
index 001a92790e1..158606aa6a2 100644
--- a/doc/development/automatic_ce_ee_merge.md
+++ b/doc/development/automatic_ce_ee_merge.md
@@ -173,13 +173,13 @@ Now, every time you create an MR for CE and EE:
## How we run the Automatic CE->EE merge at GitLab
-At GitLab, we use the [Merge Train](https://gitlab.com/gitlab-org/merge-train)
-project to keep our [gitlab-ee](https://gitlab.com/gitlab-org/gitlab-ee)
-repository updated with commits from
+At GitLab, we use the [Merge Train](https://gitlab.com/gitlab-org/merge-train)
+project to keep our [gitlab-ee](https://gitlab.com/gitlab-org/gitlab-ee)
+repository updated with commits from
[gitlab-ce](https://gitlab.com/gitlab-org/gitlab-ce).
We have a mirror of the [Merge Train](https://gitlab.com/gitlab-org/merge-train)
-project [configured](https://ops.gitlab.net/gitlab-org/merge-train) to run an
+project [configured](https://ops.gitlab.net/gitlab-org/merge-train) to run an
automatic CE->EE merge job every twenty minutes as a scheduled CI job. The
[configured](https://ops.gitlab.net/gitlab-org/merge-train) Merge Train project
is only accessible to authorized GitLab staff.
diff --git a/doc/development/distributed_tracing.md b/doc/development/distributed_tracing.md
index bfce7488a8d..4776c8348d4 100644
--- a/doc/development/distributed_tracing.md
+++ b/doc/development/distributed_tracing.md
@@ -179,4 +179,3 @@ By default, the Jaeger search UI is available at <http://localhost:16686/search>
TIP: **Tip:**
Don't forget that you will need to generate traces by using the application before
they appear in the Jaeger UI.
-
diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md
index 2217dedccd3..a732a94b7c4 100644
--- a/doc/development/ee_features.md
+++ b/doc/development/ee_features.md
@@ -945,7 +945,7 @@ export default {
- Since we [can't async load a mixin](https://github.com/vuejs/vue-loader/issues/418#issuecomment-254032223) we will use the [`ee_else_ce`](../development/ee_features.md#javascript-code-in-assetsjavascripts) alias we already have for webpack.
- This means all the EE specific props, computed properties, methods, etc that are EE only should be in a mixin in the `ee/` folder and we need to create a CE counterpart of the mixin
-##### Example:
+##### Example
```javascript
import mixin from 'ee_else_ce/path/mixin';
@@ -976,7 +976,7 @@ For regular JS files, the approach is similar.
1. An EE file should be created with the EE only code, and it should extend the CE counterpart.
1. For code inside functions that can't be extended, the code should be moved into a new file and we should use `ee_else_ce` helper:
-#### Example:
+#### Example
```javascript
import eeCode from 'ee_else_ce/ee_code';
diff --git a/doc/development/elasticsearch.md b/doc/development/elasticsearch.md
index 635895051bc..090e5235619 100644
--- a/doc/development/elasticsearch.md
+++ b/doc/development/elasticsearch.md
@@ -148,26 +148,49 @@ Uses an [Edge NGram token filter](https://www.elastic.co/guide/en/elasticsearch/
- Searches can have their own analyzers. Remember to check when editing analyzers
- `Character` filters (as opposed to token filters) always replace the original character, so they're not a good choice as they can hinder exact searches
-## Architecture
+## Zero downtime reindexing with multiple indices
-GitLab uses `elasticsearch-rails` for handling communication with Elasticsearch server. However, in order to achieve zero-downtime deployment during schema changes, an extra abstraction layer is built to allow:
+Currently GitLab can only handle a single version of setting. Any setting/schema changes would require reindexing everything from scratch. Since reindexing can take a long time, this can cause search functionality downtime.
-* Indexing (writes) to multiple indexes, with different mappings
-* Switching to different index for searches (reads) on the fly
+To avoid downtime, GitLab is working to support multiple indices that
+can function at the same time. Whenever the schema changes, the admin
+will be able to create a new index and reindex to it, while searches
+continue to go to the older, stable index. Any data updates will be
+forwarded to both indices. Once the new index is ready, an admin can
+mark it active, which will direct all searches to it, and remove the old
+index.
-Currently we are on the process of migrating models to this new design (e.g. `Snippet`), and it is hardwired to work with a single version for now.
+This is also helpful for migrating to new servers, e.g. moving to/from AWS.
-Traditionally, `elasticsearch-rails` provides class and instance level `__elasticsearch__` proxy methods. If you call `Issue.__elasticsearch__`, you will get an instance of `Elasticsearch::Model::Proxy::ClassMethodsProxy`, and if you call `Issue.first.__elasticsearch__`, you will get an instance of `Elasticsearch::Model::Proxy::InstanceMethodsProxy`. These proxy objects would talk to Elasticsearch server directly.
+Currently we are on the process of migrating to this new design. Everything is hardwired to work with one single version for now.
-In the new design, `__elasticsearch__` instead represents one extra layer of proxy. It would keep multiple versions of the actual proxy objects, and it would forward read and write calls to the proxy of the intended version.
+### Architecture
-The `elasticsearch-rails`'s way of specifying each model's mappings and other settings is to create a module for the model to include. However in the new design, each model would have its own corresponding subclassed proxy object, where the settings reside in. For example, snippet related setting in the past reside in `SnippetsSearch` module, but in the new design would reside in `SnippetClassProxy` (which is a subclass of `Elasticsearch::Model::Proxy::ClassMethodsProxy`). This reduces namespace pollution in model classes.
+The traditional setup, provided by `elasticsearch-rails`, is to communicate through its internal proxy classes. Developers would write model-specific logic in a module for the model to include in (e.g. `SnippetsSearch`). The `__elasticsearch__` methods would return a proxy object, e.g.:
+
+- `Issue.__elasticsearch__` returns an instance of `Elasticsearch::Model::Proxy::ClassMethodsProxy`
+- `Issue.first.__elasticsearch__` returns an instance of `Elasticsearch::Model::Proxy::InstanceMethodsProxy`.
+
+These proxy objects would talk to Elasticsearch server directly (see top half of the diagram).
+
+![Elasticsearch Architecture](img/elasticsearch_architecture.svg)
+
+In the planned new design, each model would have a pair of corresponding subclassed proxy objects, in which model-specific logic is located. For example, `Snippet` would have `SnippetClassProxy` and `SnippetInstanceProxy` (being subclass of `Elasticsearch::Model::Proxy::ClassMethodsProxy` and `Elasticsearch::Model::Proxy::InstanceMethodsProxy`, respectively).
+
+`__elasticsearch__` would represent another layer of proxy object, keeping track of multiple actual proxy objects. It would forward method calls to the appropriate index. For example:
+
+- `model.__elasticsearch__.search` would be forwarded to the one stable index, since it is a read operation.
+- `model.__elasticsearch__.update_document` would be forwarded to all indices, to keep all indices up-to-date.
The global configurations per version are now in the `Elastic::(Version)::Config` class. You can change mappings there.
### Creating new version of schema
-Currently GitLab would still work with a single version of setting. Once it is implemented, multiple versions of setting can exists in different folders (e.g. `ee/lib/elastic/v12p1` and `ee/lib/elastic/v12p3`). To keep a continuous git history, the latest version lives under the `/latest` folder, but is aliased as the latest version.
+NOTE: **Note:** this is not applicable yet as multiple indices functionality is not fully implemented.
+
+Folders like `ee/lib/elastic/v12p1` contain snapshots of search logic from different versions. To keep a continuous git history, the latest version lives under `ee/lib/elastic/latest`, but its classes are aliased under an actual version (e.g. `ee/lib/elastic/v12p3`). When referencing these classes, never use the `Latest` namespace directly, but use the actual version (e.g. `V12p3`).
+
+The version name basically follows GitLab's release version. If setting is changed in 12.3, we will create a new namespace called `V12p3` (p stands for "point"). Raise an issue if there is a need to name a version differently.
If the current version is `v12p1`, and we need to create a new version for `v12p3`, the steps are as follows:
@@ -176,7 +199,7 @@ If the current version is `v12p1`, and we need to create a new version for `v12p
1. Delete `v12p1` folder
1. Copy the entire folder of `latest` as `v12p1`
1. Change the namespace for files under `v12p1` folder from `Latest` to `V12p1`
-1. Make changes to `Latest` as needed
+1. Make changes to files under the `latest` folder as needed
## Troubleshooting
diff --git a/doc/development/emails.md b/doc/development/emails.md
index e6af075a282..edec0f86989 100644
--- a/doc/development/emails.md
+++ b/doc/development/emails.md
@@ -5,6 +5,10 @@
To view rendered emails "sent" in your development instance, visit
[`/rails/letter_opener`](http://localhost:3000/rails/letter_opener).
+Please note that [S/MIME signed](../administration/smime_signing_email.md) emails
+[cannot be currently previewed](https://github.com/fgrehm/letter_opener_web/issues/96) with
+`letter_opener`.
+
## Mailer previews
Rails provides a way to preview our mailer templates in HTML and plaintext using
diff --git a/doc/development/img/elasticsearch_architecture.svg b/doc/development/img/elasticsearch_architecture.svg
new file mode 100644
index 00000000000..2f38f9b04ee
--- /dev/null
+++ b/doc/development/img/elasticsearch_architecture.svg
@@ -0,0 +1 @@
+<svg version="1.2" width="210mm" height="297mm" viewBox="0 0 21000 29700" preserveAspectRatio="xMidYMid" fill-rule="evenodd" stroke-width="28.222" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><defs class="ClipPathGroup"><clipPath id="a" clipPathUnits="userSpaceOnUse"><path d="M0 0h21000v29700H0z"/></clipPath></defs><g class="SlideGroup"><g class="Slide" clip-path="url(#a)"><g class="Page"><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M1975 5575h3051v1651H1975z"/><path fill="#FFF" d="M3500 7200H2000V5600h3000v1600H3500z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M3500 7200H2000V5600h3000v1600H3500z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="2778" y="6311"><tspan>Snippet</tspan></tspan><tspan class="TextPosition" x="2099" y="6785"><tspan>(ActiveRecord)</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M1475 3975h4051v3551H1475z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M3500 7500H1500V4000h4000v3500H3500z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="1788" y="5048"><tspan>ApplicationSearch</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.ConnectorShape"><path class="BoundingBox" fill="none" d="M5975 4675h8051v701H5975z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M6000 5350h4000v-650h4000"/></g><g class="com.sun.star.drawing.ConnectorShape"><path class="BoundingBox" fill="none" d="M5975 5325h8051v1101H5975z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M6000 5350h4000v1050h4000"/></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M1075 2875h4951v4951H1075z"/><path fill="none" stroke="#F33" stroke-width="50" d="M3550 7800H1100V2900h4900v4900H3550z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="700"><tspan class="TextPosition" x="1946" y="3514"><tspan fill="#C9211E">SnippetsSearch</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M1975 12175h3051v1651H1975z"/><path fill="#FFF" d="M3500 13800H2000v-1600h3000v1600H3500z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M3500 13800H2000v-1600h3000v1600H3500z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="2778" y="12911"><tspan>Snippet</tspan></tspan><tspan class="TextPosition" x="2099" y="13385"><tspan>(ActiveRecord)</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M1075 10775h4951v3251H1075z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M3550 14000H1100v-3200h4900v3200H3550z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="2511" y="11461"><tspan>Application</tspan></tspan><tspan class="TextPosition" x="1933" y="11935"><tspan>VersionedSearch</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.ConnectorShape"><path class="BoundingBox" fill="none" d="M3525 13975h4501v7451H3525z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M3550 14000v7400h4450"/></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M14008 14075h4985v851h-4985z"/><path fill="none" stroke="#999" stroke-width="50" d="M16500 14900h-2467v-800h4934v800h-2467z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="14720" y="14648"><tspan fill="gray">ClassMethodProxy</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M13375 13075h6251v2151h-6251z"/><path fill="none" stroke="#F33" stroke-width="50" d="M16500 15200h-3100v-2100h6200v2100h-3100z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="700"><tspan class="TextPosition" x="13799" y="13731"><tspan fill="#C9211E">V12p1::SnippetClassProxy</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M7975 14575h3051v1851H7975z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M9500 16400H8000v-1800h3000v1800H9500z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="8277" y="15411"><tspan>MultiVersion-</tspan></tspan><tspan class="TextPosition" x="8429" y="15885"><tspan>ClassProxy</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M14008 16875h4985v851h-4985z"/><path fill="none" stroke="#999" stroke-width="50" d="M16500 17700h-2467v-800h4934v800h-2467z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="14720" y="17448"><tspan fill="gray">ClassMethodProxy</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M13375 15875h6251v2151h-6251z"/><path fill="none" stroke="#F33" stroke-width="50" d="M16500 18000h-3100v-2100h6200v2100h-3100z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="700"><tspan class="TextPosition" x="13799" y="16531"><tspan fill="#C9211E">V12p2::SnippetClassProxy</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.ConnectorShape"><path class="BoundingBox" fill="none" d="M10975 14125h2451v1401h-2451z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M11000 15500h1463v-1350h937"/></g><g class="com.sun.star.drawing.ConnectorShape"><path class="BoundingBox" fill="none" d="M10975 15475h2451v1501h-2451z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M11000 15500h1463v1450h937"/></g><g class="com.sun.star.drawing.ConnectorShape"><path class="BoundingBox" fill="none" d="M3525 13975h4501v1551H3525z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M3550 14000v1500h4450"/></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M14008 19975h4985v851h-4985z"/><path fill="none" stroke="#999" stroke-width="50" d="M16500 20800h-2467v-800h4934v800h-2467z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="14445" y="20548"><tspan fill="gray">InstanceMethodProxy</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M13375 18975h6251v2151h-6251z"/><path fill="none" stroke="#F33" stroke-width="50" d="M16500 21100h-3100v-2100h6200v2100h-3100z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="700"><tspan class="TextPosition" x="13505" y="19631"><tspan fill="#C9211E">V12p1::SnippetInstanceProxy</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M7975 20275h3051v2251H7975z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M9500 22500H8000v-2200h3000v2200H9500z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="8277" y="21311"><tspan>MultiVersion-</tspan></tspan><tspan class="TextPosition" x="8154" y="21785"><tspan>InstanceProxy</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M14008 22775h4985v851h-4985z"/><path fill="none" stroke="#999" stroke-width="50" d="M16500 23600h-2467v-800h4934v800h-2467z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="14445" y="23348"><tspan fill="gray">InstanceMethodProxy</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M13375 21775h6251v2151h-6251z"/><path fill="none" stroke="#F33" stroke-width="50" d="M16500 23900h-3100v-2100h6200v2100h-3100z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="700"><tspan class="TextPosition" x="13505" y="22431"><tspan fill="#C9211E">V12p2::SnippetInstanceProxy</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.ConnectorShape"><path class="BoundingBox" fill="none" d="M10975 20025h2451v1401h-2451z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M11000 21400h1463v-1350h937"/></g><g class="com.sun.star.drawing.ConnectorShape"><path class="BoundingBox" fill="none" d="M10975 21375h2451v1501h-2451z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M11000 21400h1463v1450h937"/></g><g class="com.sun.star.drawing.TextShape"><path class="BoundingBox" fill="none" d="M900 1600h10697v879H900z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="564" font-weight="400"><tspan class="TextPosition" x="1150" y="2233"><tspan>Standard elasticsearch-rails setup</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.TextShape"><path class="BoundingBox" fill="none" d="M900 9300h7683v879H900z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="564" font-weight="400"><tspan class="TextPosition" x="1150" y="9933"><tspan>GitLab multi-indices setup</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.TextShape"><path class="BoundingBox" fill="none" d="M3400 21300h4821v1197H3400z"/><text class="TextShape"><tspan class="TextParagraph" font-size="388" font-weight="400"><tspan class="TextPosition" x="4250" y="21840"><tspan fill="gray">(instance method)</tspan></tspan><tspan class="TextPosition" x="3651" y="22264"><tspan font-family="Courier" font-size="423">__elasticsearch__</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.TextShape"><path class="BoundingBox" fill="none" d="M3380 15400h4821v1197H3380z"/><text class="TextShape"><tspan class="TextParagraph" font-size="388" font-weight="400"><tspan class="TextPosition" x="4512" y="15940"><tspan fill="gray">(class method)</tspan></tspan><tspan class="TextPosition" x="3631" y="16364"><tspan font-family="Courier" font-size="423">__elasticsearch__</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.TextShape"><path class="BoundingBox" fill="none" d="M9000 3500h4821v1197H9000z"/><text class="TextShape"><tspan class="TextParagraph" font-size="388" font-weight="400"><tspan class="TextPosition" x="10132" y="4040"><tspan fill="gray">(class method)</tspan></tspan><tspan class="TextPosition" x="9251" y="4464"><tspan font-family="Courier" font-size="423">__elasticsearch__</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.TextShape"><path class="BoundingBox" fill="none" d="M9000 6400h4821v1197H9000z"/><text class="TextShape"><tspan class="TextParagraph" font-size="388" font-weight="400"><tspan class="TextPosition" x="9850" y="6940"><tspan fill="gray">(instance method)</tspan></tspan><tspan class="TextPosition" x="9251" y="7364"><tspan font-family="Courier" font-size="423">__elasticsearch__</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M1975 25175h2051v851H1975z"/><path fill="none" stroke="#999" stroke-width="50" d="M3000 26000H2000v-800h2000v800H3000z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="2634" y="25748"><tspan fill="gray">Foo</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.TextShape"><path class="BoundingBox" fill="none" d="M4400 25200h7101v726H4400z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="4650" y="25710"><tspan>elasticsearch-rails’ internal class</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.TextShape"><path class="BoundingBox" fill="none" d="M4400 26400h8601v1200H4400z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="4650" y="26910"><tspan>where model-specific logic is</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M1975 26275h2051v851H1975z"/><path fill="none" stroke="#F33" stroke-width="50" d="M3000 27100H2000v-800h2000v800H3000z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="700"><tspan class="TextPosition" x="2613" y="26848"><tspan fill="#C9211E">Foo</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.TextShape"><path class="BoundingBox" fill="none" d="M4900 17289h5901v2312H4900z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="370" font-weight="400"><tspan class="TextPosition" x="7236" y="17748"><tspan fill="gray">Write operations like </tspan></tspan><tspan class="TextPosition" x="5323" y="18159"><tspan fill="gray">indexing/updating are forwarded </tspan></tspan><tspan class="TextPosition" x="8024" y="18570"><tspan fill="gray">to all instances.</tspan></tspan></tspan><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="370" font-weight="400"><tspan class="TextPosition" x="5501" y="18981"><tspan fill="gray">Read operations are forwarded </tspan></tspan><tspan class="TextPosition" x="7126" y="19392"><tspan fill="gray">to specified instance.</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.ConnectorShape"><path class="BoundingBox" fill="none" d="M10785 15769h1422v2691h-1422z"/><path fill="none" stroke="#999" stroke-width="30" d="M10800 18444c1429 0 934-1618 1119-2337"/><path fill="#999" d="M12206 15769l-460 293 267 217 193-510z"/></g><g class="com.sun.star.drawing.ConnectorShape"><path class="BoundingBox" fill="none" d="M10785 18429h1528v2862h-1528z"/><path fill="none" stroke="#999" stroke-width="30" d="M10800 18444c1509 0 970 1782 1200 2526"/><path fill="#999" d="M12312 21290l-227-496-252 235 479 261z"/></g><g class="com.sun.star.drawing.TextShape"><path class="BoundingBox" fill="none" d="M1800 24000h7101v807H1800z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="494" font-weight="700"><tspan class="TextPosition" x="2050" y="24574"><tspan>Legend</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M13975 4275h5085v851h-5085z"/><path fill="none" stroke="#999" stroke-width="50" d="M16517 5100h-2517v-800h5034v800h-2517z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="14737" y="4848"><tspan fill="gray">ClassMethodProxy</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M13975 5975h5085v851h-5085z"/><path fill="none" stroke="#999" stroke-width="50" d="M16517 6800h-2517v-800h5034v800h-2517z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="14462" y="6548"><tspan fill="gray">InstanceMethodProxy</tspan></tspan></tspan></text></g></g></g></g></svg> \ No newline at end of file
diff --git a/doc/development/lfs.md b/doc/development/lfs.md
index 8c3408eb6e2..cb4c2d8967b 100644
--- a/doc/development/lfs.md
+++ b/doc/development/lfs.md
@@ -8,4 +8,4 @@ In April 2019, Francisco Javier López hosted a [Deep Dive] on GitLab's [Git LFS
[Git LFS]: ../workflow/lfs/manage_large_binaries_with_git_lfs.html
[recording on YouTube]: https://www.youtube.com/watch?v=Yyxwcksr0Qc
[Google Slides]: https://docs.google.com/presentation/d/1E-aw6-z0rYd0346YhIWE7E9A65zISL9iIMAOq2zaw9E/edit
-[PDF]: https://gitlab.com/gitlab-org/create-stage/uploads/07a89257a140db067bdfb484aecd35e1/Git_LFS_Deep_Dive__Create_.pdf \ No newline at end of file
+[PDF]: https://gitlab.com/gitlab-org/create-stage/uploads/07a89257a140db067bdfb484aecd35e1/Git_LFS_Deep_Dive__Create_.pdf
diff --git a/doc/development/namespaces_storage_statistics.md b/doc/development/namespaces_storage_statistics.md
new file mode 100644
index 00000000000..b3285de4705
--- /dev/null
+++ b/doc/development/namespaces_storage_statistics.md
@@ -0,0 +1,178 @@
+# Database case study: Namespaces storage statistics
+
+## Introduction
+
+On [Storage and limits management for groups](https://gitlab.com/groups/gitlab-org/-/epics/886),
+we want to facilitate a method for easily viewing the amount of
+storage consumed by a group, and allow easy management.
+
+## Proposal
+
+1. Create a new ActiveRecord model to hold the namespaces' statistics in an aggregated form (only for root namespaces).
+1. Refresh the statistics in this model every time a project belonging to this namespace is changed.
+
+## Problem
+
+In GitLab, we update the project storage statistics through a
+[callback](https://gitlab.com/gitlab-org/gitlab-ce/blob/v12.2.0.pre/app/models/project.rb#L90)
+every time the project is saved.
+
+The summary of those statistics per namespace is then retrieved
+by [`Namespaces#with_statistics`](https://gitlab.com/gitlab-org/gitlab-ce/blob/v12.2.0.pre/app/models/namespace.rb#L70) scope. Analyzing this query we noticed that:
+
+* It takes up to `1.2` seconds for namespaces with over `15k` projects.
+* It can't be analyzed with [ChatOps](chatops_on_gitlabcom.md), as it times out.
+
+Additionally, the pattern that is currently used to update the project statistics
+(the callback) doesn't scale adequately. It is currently one of the largest
+[database queries transactions on production](https://gitlab.com/gitlab-org/gitlab-ce/issues/62488)
+that takes the most time overall. We can't add one more query to it as
+it will increase the transaction's length.
+
+Because of all of the above, we can't apply the same pattern to store
+and update the namespaces statistics, as the `namespaces` table is one
+of the largest tables on GitLab.com. Therefore we needed to find a performant and
+alternative method.
+
+## Attempts
+
+### Attempt A: PostgreSQL materialized view
+
+Model can be updated through a refresh strategy based on a project routes SQL and a [materialized view](https://www.postgresql.org/docs/9.6/rules-materializedviews.html):
+
+```sql
+SELECT split_part("rs".path, '/', 1) as root_path,
+ COALESCE(SUM(ps.storage_size), 0) AS storage_size,
+ COALESCE(SUM(ps.repository_size), 0) AS repository_size,
+ COALESCE(SUM(ps.wiki_size), 0) AS wiki_size,
+ COALESCE(SUM(ps.lfs_objects_size), 0) AS lfs_objects_size,
+ COALESCE(SUM(ps.build_artifacts_size), 0) AS build_artifacts_size,
+ COALESCE(SUM(ps.packages_size), 0) AS packages_size
+FROM "projects"
+ INNER JOIN routes rs ON rs.source_id = projects.id AND rs.source_type = 'Project'
+ INNER JOIN project_statistics ps ON ps.project_id = projects.id
+GROUP BY root_path
+```
+
+We could then execute the query with:
+
+```sql
+REFRESH MATERIALIZED VIEW root_namespace_storage_statistics;
+```
+
+While this implied a single query update (and probably a fast one), it has some downsides:
+
+* Materialized views syntax varies from PostgreSQL and MySQL. While this feature was worked on, MySQL was still supported by GitLab.
+* Rails does not have native support for materialized views. We'd need to use a specialized gem to take care of the management of the database views, which implies additional work.
+
+### Attempt B: An update through a CTE
+
+Similar to Attempt A: Model update done through a refresh strategy with a [Common Table Expression](https://www.postgresql.org/docs/9.1/queries-with.html)
+
+```sql
+WITH refresh AS (
+ SELECT split_part("rs".path, '/', 1) as root_path,
+ COALESCE(SUM(ps.storage_size), 0) AS storage_size,
+ COALESCE(SUM(ps.repository_size), 0) AS repository_size,
+ COALESCE(SUM(ps.wiki_size), 0) AS wiki_size,
+ COALESCE(SUM(ps.lfs_objects_size), 0) AS lfs_objects_size,
+ COALESCE(SUM(ps.build_artifacts_size), 0) AS build_artifacts_size,
+ COALESCE(SUM(ps.packages_size), 0) AS packages_size
+ FROM "projects"
+ INNER JOIN routes rs ON rs.source_id = projects.id AND rs.source_type = 'Project'
+ INNER JOIN project_statistics ps ON ps.project_id = projects.id
+ GROUP BY root_path)
+UPDATE namespace_storage_statistics
+SET storage_size = refresh.storage_size,
+ repository_size = refresh.repository_size,
+ wiki_size = refresh.wiki_size,
+ lfs_objects_size = refresh.lfs_objects_size,
+ build_artifacts_size = refresh.build_artifacts_size,
+ packages_size = refresh.packages_size
+FROM refresh
+ INNER JOIN routes rs ON rs.path = refresh.root_path AND rs.source_type = 'Namespace'
+WHERE namespace_storage_statistics.namespace_id = rs.source_id
+```
+
+Same benefits and downsides as attempt A.
+
+### Attempt C: Get rid of the model and store the statistics on Redis
+
+We could get rid of the model that stores the statistics in aggregated form and instead use a Redis Set.
+This would be the [boring solution](https://about.gitlab.com/handbook/values/#boring-solutions) and the fastest one
+to implement, as GitLab already includes Redis as part of its [Architecture](architecture.md#redis).
+
+The downside of this approach is that Redis does not provide the same persistence/consistency guarantees as PostgreSQL,
+and this is information we can't afford to lose in a Redis failure.
+
+### Attempt D: Tag the root namespace and its child namespaces
+
+Directly relate the root namespace to its child namespaces, so
+whenever a namespace is created without a parent, this one is tagged
+with the root namespace ID:
+
+| id | root_id | parent_id
+|:---|:--------|:----------
+| 1 | 1 | NULL
+| 2 | 1 | 1
+| 3 | 1 | 2
+
+To aggregate the statistics inside a namespace we'd execute something like:
+
+```sql
+SELECT COUNT(...)
+FROM projects
+WHERE namespace_id IN (
+ SELECT id
+ FROM namespaces
+ WHERE root_id = X
+)
+```
+
+Even though this approach would make aggregating much easier, it has some major downsides:
+
+* We'd have to migrate **all namespaces** by adding and filling a new column. Because of the size of the table, dealing with time/cost will not be great. The background migration will take approximately `153h`, see <https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/29772>.
+* Background migration has to be shipped one release before, delaying the functionality by another milestone.
+
+### Attempt E (final): Update the namespace storage statistics in async way
+
+This approach consists of keep using the incremental statistics updates we currently already have,
+but we refresh them through Sidekiq jobs and in different transactions:
+
+1. Create a second table (`namespace_aggregation_schedules`) with two columns `id` and `namespace_id`.
+1. Whenever the statistics of a project changes, insert a row into `namespace_aggregation_schedules`
+ - We don't insert a new row if there's already one related to the root namespace.
+ - Keeping in mind the length of the transaction that involves updating `project_statistics`(<https://gitlab.com/gitlab-org/gitlab-ce/issues/62488>), the insertion should be done in a different transaction and through a Sidekiq Job.
+1. After inserting the row, we schedule another worker to be executed async at two different moments:
+ - One enqueued for immediate execution and another one scheduled in `1.5h` hours.
+ - We only schedule the jobs, if we can obtain a `1.5h` lease on Redis on a key based on the root namespace ID.
+ - If we can't obtain the lease, it indicates there's another aggregation already in progress, or scheduled in no more than `1.5h`.
+1. This worker will:
+ - Update the root namespace storage statistics by querying all the namespaces through a service.
+ - Delete the related `namespace_aggregation_schedules` after the update.
+1. Another Sidekiq job is also included to traverse any remaining rows on the `namespace_aggregation_schedules` table and schedule jobs for every pending row.
+ - This job is scheduled with cron to run every night (UTC).
+
+This implementation has the following benefits:
+
+* All the updates are done async, so we're not increasing the length of the transactions for `project_statistics`.
+* We're doing the update in a single SQL query.
+* It is compatible with PostgreSQL and MySQL.
+* No background migration required.
+
+The only downside of this approach is that namespaces' statistics are updated up to `1.5` hours after the change is done,
+which means there's a time window in which the statistics are inaccurate. Because we're still not
+[enforcing storage limits](https://gitlab.com/gitlab-org/gitlab-ce/issues/30421), this is not a major problem.
+
+## Conclusion
+
+Updating the storage statistics asynchronously, was the less problematic and
+performant approach of aggregating the root namespaces.
+
+All the details regarding this use case can be found on:
+
+* <https://gitlab.com/gitlab-org/gitlab-ce/issues/62214>
+* Merge Request with the implementation: <https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/28996>
+
+Performance of the namespace storage statistics were measured in staging and production (GitLab.com). All results were posted
+on <https://gitlab.com/gitlab-org/gitlab-ce/issues/64092>: No problem has been reported so far.
diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md
index fc96820c555..e9d6cfe00b2 100644
--- a/doc/development/rake_tasks.md
+++ b/doc/development/rake_tasks.md
@@ -216,4 +216,3 @@ bundle exec rake routes
Since these take some time to create, it's often helpful to save the output to
a file for quick reference.
-
diff --git a/doc/development/repository_mirroring.md b/doc/development/repository_mirroring.md
index f8c33ff2b85..dc51bf80e92 100644
--- a/doc/development/repository_mirroring.md
+++ b/doc/development/repository_mirroring.md
@@ -8,4 +8,4 @@ In December 2018, Tiago Botelho hosted a [Deep Dive] on GitLab's [Pull Repositor
[Pull Repository Mirroring functionality]: ../workflow/repository_mirroring.md#pulling-from-a-remote-repository-starter
[recording on YouTube]: https://www.youtube.com/watch?v=sSZq0fpdY-Y
[Google Slides]: https://docs.google.com/presentation/d/17BTT6M6RyNRckV4wTt-dr07nIfBvD325_xVBoLtSoPM/edit?usp=sharing
-[PDF]: https://gitlab.com/gitlab-org/create-stage/uploads/8693404888a941fd851f8a8ecdec9675/Gitlab_Create_-_Pull_Mirroring_Deep_Dive.pdf \ No newline at end of file
+[PDF]: https://gitlab.com/gitlab-org/create-stage/uploads/8693404888a941fd851f8a8ecdec9675/Gitlab_Create_-_Pull_Mirroring_Deep_Dive.pdf
diff --git a/doc/development/testing_guide/end_to_end/index.md b/doc/development/testing_guide/end_to_end/index.md
index d6b944a3e74..3ae3ce183d9 100644
--- a/doc/development/testing_guide/end_to_end/index.md
+++ b/doc/development/testing_guide/end_to_end/index.md
@@ -45,11 +45,11 @@ Results are reported in the `#qa-staging` Slack channel.
### Testing code in merge requests
-#### Using the `package-and-qa` job
+#### Using the `package-and-qa-manual` job
It is possible to run end-to-end tests for a merge request, eventually being run in
a pipeline in the [`gitlab-qa`](https://gitlab.com/gitlab-org/gitlab-qa/) project,
-by triggering the `package-and-qa` manual action in the `test` stage (not
+by triggering the `package-and-qa-manual` manual action in the `test` stage (not
available for forks).
**This runs end-to-end tests against a custom Omnibus package built from your
@@ -71,7 +71,7 @@ graph LR
B2[`Trigger-qa` stage<br>`Trigger:qa-test` job] -.->|2. Triggers a gitlab-qa pipeline and wait for it to be done| A3
subgraph "gitlab-ce/ee pipeline"
- A1[`test` stage<br>`package-and-qa` job]
+ A1[`test` stage<br>`package-and-qa-manual` job]
end
subgraph "omnibus-gitlab pipeline"
@@ -79,7 +79,7 @@ subgraph "omnibus-gitlab pipeline"
end
subgraph "gitlab-qa pipeline"
- A3>QA jobs run] -.->|3. Reports back the pipeline result to the `package-and-qa` job<br>and post the result on the original commit tested| A1
+ A3>QA jobs run] -.->|3. Reports back the pipeline result to the `package-and-qa-manual` job<br>and post the result on the original commit tested| A1
end
```
diff --git a/doc/development/testing_guide/end_to_end/page_objects.md b/doc/development/testing_guide/end_to_end/page_objects.md
index 47e58a425fd..850ea6b60ac 100644
--- a/doc/development/testing_guide/end_to_end/page_objects.md
+++ b/doc/development/testing_guide/end_to_end/page_objects.md
@@ -40,7 +40,7 @@ the time it would take to build packages and test everything.
That is why when someone changes `t.text_field :login` to
`t.text_field :username` in the _new session_ view we won't know about this
change until our GitLab QA nightly pipeline fails, or until someone triggers
-`package-and-qa` action in their merge request.
+`package-and-qa-manual` action in their merge request.
Obviously such a change would break all tests. We call this problem a _fragile
tests problem_.
diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md
index 52ac746716c..7dc89a3fcdb 100644
--- a/doc/development/testing_guide/frontend_testing.md
+++ b/doc/development/testing_guide/frontend_testing.md
@@ -232,7 +232,7 @@ module. GitLab has a custom `spyOnDependency` method which utilizes
[babel-plugin-rewire](https://github.com/speedskater/babel-plugin-rewire) to
achieve this. It can be used like so:
-```js
+```javascript
// my_module.js
import { visitUrl } from '~/lib/utils/url_utility';
@@ -241,7 +241,7 @@ export default function doSomething() {
}
```
-```js
+```javascript
// my_module_spec.js
import doSomething from '~/my_module';
@@ -619,6 +619,42 @@ See also [Notes on testing Vue components](../fe_guide/vue.html#testing-vue-comp
Unit tests are on the lowest abstraction level and typically test functionality that is not directly perceivable by a user.
+```mermaid
+graph RL
+ plain[Plain JavaScript];
+ Vue[Vue Components];
+ feature-flags[Feature Flags];
+ license-checks[License Checks];
+
+ plain---Vuex;
+ plain---GraphQL;
+ Vue---plain;
+ Vue---Vuex;
+ Vue---GraphQL;
+ browser---plain;
+ browser---Vue;
+ plain---backend;
+ Vuex---backend;
+ GraphQL---backend;
+ Vue---backend;
+ backend---database;
+ backend---feature-flags;
+ backend---license-checks;
+
+ class plain tested;
+ class Vuex tested;
+
+ classDef node color:#909090,fill:#f0f0f0,stroke-width:2px,stroke:#909090
+ classDef label stroke-width:0;
+ classDef tested color:#000000,fill:#a0c0ff,stroke:#6666ff,stroke-width:2px,stroke-dasharray: 5, 5;
+
+ subgraph " "
+ tested;
+ mocked;
+ class tested tested;
+ end
+```
+
#### When to use unit tests
<details>
@@ -711,6 +747,41 @@ Unit tests are on the lowest abstraction level and typically test functionality
Component tests cover the state of a single component that is perceivable by a user depending on external signals such as user input, events fired from other components, or application state.
+```mermaid
+graph RL
+ plain[Plain JavaScript];
+ Vue[Vue Components];
+ feature-flags[Feature Flags];
+ license-checks[License Checks];
+
+ plain---Vuex;
+ plain---GraphQL;
+ Vue---plain;
+ Vue---Vuex;
+ Vue---GraphQL;
+ browser---plain;
+ browser---Vue;
+ plain---backend;
+ Vuex---backend;
+ GraphQL---backend;
+ Vue---backend;
+ backend---database;
+ backend---feature-flags;
+ backend---license-checks;
+
+ class Vue tested;
+
+ classDef node color:#909090,fill:#f0f0f0,stroke-width:2px,stroke:#909090
+ classDef label stroke-width:0;
+ classDef tested color:#000000,fill:#a0c0ff,stroke:#6666ff,stroke-width:2px,stroke-dasharray: 5, 5;
+
+ subgraph " "
+ tested;
+ mocked;
+ class tested tested;
+ end
+```
+
#### When to use component tests
- Vue components
@@ -781,6 +852,46 @@ Component tests cover the state of a single component that is perceivable by a u
Integration tests cover the interaction between all components on a single page.
Their abstraction level is comparable to how a user would interact with the UI.
+```mermaid
+graph RL
+ plain[Plain JavaScript];
+ Vue[Vue Components];
+ feature-flags[Feature Flags];
+ license-checks[License Checks];
+
+ plain---Vuex;
+ plain---GraphQL;
+ Vue---plain;
+ Vue---Vuex;
+ Vue---GraphQL;
+ browser---plain;
+ browser---Vue;
+ plain---backend;
+ Vuex---backend;
+ GraphQL---backend;
+ Vue---backend;
+ backend---database;
+ backend---feature-flags;
+ backend---license-checks;
+
+ class plain tested;
+ class Vue tested;
+ class Vuex tested;
+ class GraphQL tested;
+ class browser tested;
+ linkStyle 0,1,2,3,4,5,6 stroke:#6666ff,stroke-width:2px,stroke-dasharray: 5, 5;
+
+ classDef node color:#909090,fill:#f0f0f0,stroke-width:2px,stroke:#909090
+ classDef label stroke-width:0;
+ classDef tested color:#000000,fill:#a0c0ff,stroke:#6666ff,stroke-width:2px,stroke-dasharray: 5, 5;
+
+ subgraph " "
+ tested;
+ mocked;
+ class tested tested;
+ end
+```
+
#### When to use integration tests
<details>
@@ -838,6 +949,47 @@ This also implies that database queries are executed which makes this category s
See also the [RSpec testing guidelines](../testing_guide/best_practices.md#rspec).
+```mermaid
+graph RL
+ plain[Plain JavaScript];
+ Vue[Vue Components];
+ feature-flags[Feature Flags];
+ license-checks[License Checks];
+
+ plain---Vuex;
+ plain---GraphQL;
+ Vue---plain;
+ Vue---Vuex;
+ Vue---GraphQL;
+ browser---plain;
+ browser---Vue;
+ plain---backend;
+ Vuex---backend;
+ GraphQL---backend;
+ Vue---backend;
+ backend---database;
+ backend---feature-flags;
+ backend---license-checks;
+
+ class backend tested;
+ class plain tested;
+ class Vue tested;
+ class Vuex tested;
+ class GraphQL tested;
+ class browser tested;
+ linkStyle 0,1,2,3,4,5,6,7,8,9,10 stroke:#6666ff,stroke-width:2px,stroke-dasharray: 5, 5;
+
+ classDef node color:#909090,fill:#f0f0f0,stroke-width:2px,stroke:#909090
+ classDef label stroke-width:0;
+ classDef tested color:#000000,fill:#a0c0ff,stroke:#6666ff,stroke-width:2px,stroke-dasharray: 5, 5;
+
+ subgraph " "
+ tested;
+ mocked;
+ class tested tested;
+ end
+```
+
#### When to use feature tests
- Use cases that require a backend and cannot be tested using fixtures.
@@ -847,7 +999,7 @@ See also the [RSpec testing guidelines](../testing_guide/best_practices.md#rspec
A `:js` flag is added to the test to make sure the full environment is loaded.
-```js
+```ruby
scenario 'successfully', :js do
sign_in(create(:admin))
end
@@ -857,7 +1009,7 @@ The steps of each test are written using capybara methods ([documentation](https
Bear in mind <abbr title="XMLHttpRequest">XHR</abbr> calls might require you to use `wait_for_requests` in between steps, like so:
-```rspec
+```ruby
find('.form-control').native.send_keys(:enter)
wait_for_requests
@@ -871,7 +1023,7 @@ expect(page).not_to have_selector('.card')
We have a helper available to make testing actions easier, as per [official documentation](https://vuex.vuejs.org/guide/testing.html):
-```js
+```javascript
testAction(
actions.actionName, // action
{ }, // params to be passed to action
@@ -899,7 +1051,7 @@ To make mounting a Vue component easier and more readable, we have a few helpers
Examples of usage:
-```js
+```javascript
beforeEach(() => {
vm = createComponentWithStore(Component, store);
@@ -909,7 +1061,7 @@ beforeEach(() => {
});
```
-```js
+```javascript
beforeEach(() => {
vm = mountComponentWithStore(Component, {
el: '#dummy-element',
@@ -921,7 +1073,7 @@ beforeEach(() => {
Don't forget to clean up:
-```js
+```javascript
afterEach(() => {
vm.$destroy();
});
diff --git a/doc/gitlab-basics/start-using-git.md b/doc/gitlab-basics/start-using-git.md
index 3e3f96fb31f..5de173abbff 100644
--- a/doc/gitlab-basics/start-using-git.md
+++ b/doc/gitlab-basics/start-using-git.md
@@ -63,8 +63,8 @@ git config --global user.email
You'll need to do this only once, since you are using the `--global` option. It tells
Git to always use this information for anything you do on that system. If you want
-to override this with a different username or email address for specific projects,
-you can run the command without the `--global` option when you’re in that project.
+to override this with a different username or email address for specific projects or repositories,
+you can run the command without the `--global` option when you’re in that project, and that will default to `--local`. You can read more on how Git manages configurations in the [Git Config](https://git-scm.com/book/en/v2/Customizing-Git-Git-Configuration) documentation.
## Check your information
@@ -102,8 +102,7 @@ files to your local computer, automatically preserving the Git connection with t
remote repository.
You can either clone it via HTTPS or [SSH](../ssh/README.md). If you chose to clone
-it via HTTPS, you'll have to enter your credentials every time you pull and push.
-With SSH, you enter your credentials only once.
+it via HTTPS, you'll have to enter your credentials every time you pull and push. You can read more about credential storage in the [Git Credentials documentation](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage). With SSH, you enter your credentials only once.
You can find both paths (HTTPS and SSH) by navigating to your project's landing page
and clicking **Clone**. GitLab will prompt you with both paths, from which you can copy
@@ -152,13 +151,15 @@ to get the main branch code, or the branch name of the branch you are currently
in.
```bash
-git pull REMOTE <name-of-branch>
+git pull <REMOTE> <name-of-branch>
```
-When you first clone a repository, REMOTE is typically `origin`. This is where the
+When you clone a repository, `REMOTE` is typically `origin`. This is where the
repository was cloned from, and it indicates the SSH or HTTPS URL of the repository
on the remote server. `<name-of-branch>` is usually `master`, but it may be any existing
-branch.
+branch. You can create additional named remotes and branches as necessary.
+
+You can learn more on how Git manages remote repositories in the [Git Remote documentation](https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes).
### View your remote repositories
@@ -168,6 +169,8 @@ To view your remote repositories, type:
git remote -v
```
+The `-v` flag stands for verbose.
+
### Add a remote repository
To add a link to a remote repository:
@@ -186,7 +189,7 @@ following (spaces won't be recognized in the branch name, so you will need to us
hyphen or underscore):
```bash
-git checkout -b <name-of-branch>>
+git checkout -b <name-of-branch>
```
### Work on an existing branch
@@ -238,7 +241,7 @@ git commit -m "COMMENT TO DESCRIBE THE INTENTION OF THE COMMIT"
```
NOTE: **Note:**
-The `.` character typically means _all_ in Git.
+The `.` character means _all file changes in the current directory and all subdirectories_.
### Send changes to GitLab.com
diff --git a/doc/install/azure/index.md b/doc/install/azure/index.md
index 543a222bd25..c5939dc6856 100644
--- a/doc/install/azure/index.md
+++ b/doc/install/azure/index.md
@@ -407,7 +407,7 @@ on any cloud service you choose.
## Where to next?
-Check out our other [Technical Articles](../../articles/index.md) or browse the [GitLab Documentation][GitLab-Docs](../../README.md) to learn more about GitLab.
+Check out our other [Technical Articles](../../articles/index.md) or browse the [GitLab Documentation](../../README.md) to learn more about GitLab.
### Useful links
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 295d9804497..6d64b8a4936 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -789,7 +789,7 @@ nginx: configuration file /etc/nginx/nginx.conf test failed`
sudo service nginx restart
```
-## Done!
+## Post-install
### Double-check Application Status
diff --git a/doc/integration/jenkins.md b/doc/integration/jenkins.md
index 6755640d39c..d865f977799 100644
--- a/doc/integration/jenkins.md
+++ b/doc/integration/jenkins.md
@@ -35,7 +35,7 @@ and [Migrating from Jenkins to GitLab](https://www.youtube.com/watch?v=RlEVGOpYF
For a real use case, read the blog post [Continuous integration: From Jenkins to GitLab using Docker](https://about.gitlab.com/2017/07/27/docker-my-precious/).
-NOTE: **Moving from a traditional CI plug-in to a single application for the entire software development lifecycle can decrease hours spent on maintaining toolchains by 10% or more.**
+NOTE: **Moving from a traditional CI plug-in to a single application for the entire software development lifecycle can decrease hours spent on maintaining toolchains by 10% or more.**
Visit the ['GitLab vs. Jenkins' comparison page](https://about.gitlab.com/devops-tools/jenkins-vs-gitlab.html) to learn how our built-in CI compares to Jenkins.
## Requirements
diff --git a/doc/integration/oauth2_generic.md b/doc/integration/oauth2_generic.md
index f4119b1d1ce..ad59592c45f 100644
--- a/doc/integration/oauth2_generic.md
+++ b/doc/integration/oauth2_generic.md
@@ -12,7 +12,7 @@ This strategy is designed to allow configuration of the simple OmniAuth SSO proc
1. Strategy parses user information from the response, using a **configurable** format
1. GitLab finds or creates the returned user and logs them in
-## Limitations of this Strategy:
+## Limitations of this Strategy
- It can only be used for Single Sign on, and will not provide any other access granted by any OAuth provider
(importing projects or users, etc)
diff --git a/doc/migrate_ci_to_ce/README.md b/doc/migrate_ci_to_ce/README.md
index 9947c19a667..2b8379141c3 100644
--- a/doc/migrate_ci_to_ce/README.md
+++ b/doc/migrate_ci_to_ce/README.md
@@ -227,7 +227,7 @@ sudo mv /path/to/12345_gitlab_ci_backup.tar /var/opt/gitlab/backups/
sudo mv /path/to/12345_gitlab_ci_backup.tar /home/git/gitlab/tmp/backups/
```
-### 5. Import the CI data into GitLab.
+### 5. Import the CI data into GitLab
This step will delete any existing CI data on your GitLab server. There should
be no CI data yet because you turned CI on the GitLab server off earlier.
diff --git a/doc/public_access/public_access.md b/doc/public_access/public_access.md
index 0142e5075cc..dc6ee9b2503 100644
--- a/doc/public_access/public_access.md
+++ b/doc/public_access/public_access.md
@@ -6,7 +6,7 @@ type: reference
GitLab allows [Owners](../user/permissions.md) to set a projects' visibility as **public**, **internal**
or **private**. These visibility levels affect who can see the project in the
-public access directory (`/public` under your GitLab instance), like at [https://gitlab.com/public]().
+public access directory (`/public` under your GitLab instance), like at <https://gitlab.com/public>
## Visibility of projects
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 51f04df27c7..b4b7d711d5a 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -978,4 +978,3 @@ If this happens, check the following:
1. Confirm there is sufficent diskspace for the gzip operation.
1. If NFS is being used, check if the mount option `timeo` is set. The default is `600`, and changing this to smaller values have resulted in this error.
-
diff --git a/doc/raketasks/cleanup.md b/doc/raketasks/cleanup.md
index 832078d23cb..f84d29cca9a 100644
--- a/doc/raketasks/cleanup.md
+++ b/doc/raketasks/cleanup.md
@@ -1,6 +1,10 @@
# Cleanup
-## Remove garbage from filesystem. Important! Data loss!
+## Remove garbage from filesystem
+
+DANGER: **Danger:**
+The commands below will remove data permanently from your GitLab instance. Only use
+these commands if you are 100% certain that it is safe to delete this data.
Remove namespaces(dirs) from all repository storage paths if they don't exist in GitLab database.
diff --git a/doc/raketasks/import.md b/doc/raketasks/import.md
index 8f65fab366e..c76180a242e 100644
--- a/doc/raketasks/import.md
+++ b/doc/raketasks/import.md
@@ -11,7 +11,7 @@
## How to use
-### Create a new folder to import your Git repositories from.
+### Create a new folder to import your Git repositories from
The new folder needs to have git user ownership and read/write/execute access for git user and its group:
@@ -19,7 +19,7 @@ The new folder needs to have git user ownership and read/write/execute access fo
sudo -u git mkdir -p /var/opt/gitlab/git-data/repository-import-<date>/new_group
```
-### Copy your bare repositories inside this newly created folder:
+### Copy your bare repositories inside this newly created folder
- Any .git repositories found on any of the subfolders will be imported as projects
- Groups will be created as needed, these could be nested folders. Example:
@@ -38,7 +38,7 @@ sudo chown -R git:git /var/opt/gitlab/git-data/repository-import-<date>
If you are using an installation from source, replace `/var/opt/gitlab/` with `/home/git`.
-### Run the command below depending on your type of installation:
+### Run the command below depending on your type of installation
#### Omnibus Installation
diff --git a/doc/raketasks/web_hooks.md b/doc/raketasks/web_hooks.md
index cc1166a04cc..d9a042ec8fe 100644
--- a/doc/raketasks/web_hooks.md
+++ b/doc/raketasks/web_hooks.md
@@ -1,6 +1,6 @@
# Webhooks administration **(CORE ONLY)**
-## Add a webhook for **ALL** projects:
+## Add a webhook for **ALL** projects
```sh
# omnibus-gitlab
@@ -9,7 +9,7 @@ sudo gitlab-rake gitlab:web_hook:add URL="http://example.com/hook"
bundle exec rake gitlab:web_hook:add URL="http://example.com/hook" RAILS_ENV=production
```
-## Add a webhook for projects in a given **NAMESPACE**:
+## Add a webhook for projects in a given **NAMESPACE**
```sh
# omnibus-gitlab
@@ -18,7 +18,7 @@ sudo gitlab-rake gitlab:web_hook:add URL="http://example.com/hook" NAMESPACE=acm
bundle exec rake gitlab:web_hook:add URL="http://example.com/hook" NAMESPACE=acme RAILS_ENV=production
```
-## Remove a webhook from **ALL** projects using:
+## Remove a webhook from **ALL** projects using
```sh
# omnibus-gitlab
@@ -27,7 +27,7 @@ sudo gitlab-rake gitlab:web_hook:rm URL="http://example.com/hook"
bundle exec rake gitlab:web_hook:rm URL="http://example.com/hook" RAILS_ENV=production
```
-## Remove a webhook from projects in a given **NAMESPACE**:
+## Remove a webhook from projects in a given **NAMESPACE**
```sh
# omnibus-gitlab
@@ -36,7 +36,7 @@ sudo gitlab-rake gitlab:web_hook:rm URL="http://example.com/hook" NAMESPACE=acme
bundle exec rake gitlab:web_hook:rm URL="http://example.com/hook" NAMESPACE=acme RAILS_ENV=production
```
-## List **ALL** webhooks:
+## List **ALL** webhooks
```sh
# omnibus-gitlab
@@ -45,7 +45,7 @@ sudo gitlab-rake gitlab:web_hook:list
bundle exec rake gitlab:web_hook:list RAILS_ENV=production
```
-## List the webhooks from projects in a given **NAMESPACE**:
+## List the webhooks from projects in a given **NAMESPACE**
```sh
# omnibus-gitlab
diff --git a/doc/security/rate_limits.md b/doc/security/rate_limits.md
index c80f2f264b2..80088da77a0 100644
--- a/doc/security/rate_limits.md
+++ b/doc/security/rate_limits.md
@@ -30,4 +30,3 @@ similarly mitigated by a rate limit.
This method of rate limiting is cumbersome, but has some advantages. It allows
throttling of specific paths, and is also integrated into Git and container
registry requests. See [Rack Attack initializer](rack_attack.md).
-
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 4bfcd4aad96..26d5221acc5 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -65,7 +65,7 @@ project in a simple and automatic way:
1. [Auto Code Quality](#auto-code-quality-starter) **(STARTER)**
1. [Auto SAST (Static Application Security Testing)](#auto-sast-ultimate) **(ULTIMATE)**
1. [Auto Dependency Scanning](#auto-dependency-scanning-ultimate) **(ULTIMATE)**
-1. [Auto License Management](#auto-license-management-ultimate) **(ULTIMATE)**
+1. [Auto License Compliance](#auto-license-compliance-ultimate) **(ULTIMATE)**
1. [Auto Container Scanning](#auto-container-scanning-ultimate) **(ULTIMATE)**
1. [Auto Review Apps](#auto-review-apps)
1. [Auto DAST (Dynamic Application Security Testing)](#auto-dast-ultimate) **(ULTIMATE)**
@@ -401,13 +401,13 @@ check out.
Any security warnings are also shown in the merge request widget. Read more about
[Dependency Scanning](../../user/application_security/dependency_scanning/index.md).
-### Auto License Management **(ULTIMATE)**
+### Auto License Compliance **(ULTIMATE)**
> Introduced in [GitLab Ultimate][ee] 11.0.
-License Management uses the
-[License Management Docker image](https://gitlab.com/gitlab-org/security-products/license-management)
-to search the project dependencies for their license. The Auto License Management stage
+License Compliance uses the
+[License Compliance Docker image](https://gitlab.com/gitlab-org/security-products/license-management)
+to search the project dependencies for their license. The Auto License Compliance stage
will be skipped on licenses other than Ultimate.
Once the
@@ -415,7 +415,7 @@ report is created, it's uploaded as an artifact which you can later download and
check out.
Any licenses are also shown in the merge request widget. Read more how
-[License Management works](../../user/application_security/license_management/index.md).
+[License Compliance works](../../user/application_security/license_management/index.md).
### Auto Container Scanning **(ULTIMATE)**
@@ -587,29 +587,32 @@ procfile exec` to replicate the environment where your application will run.
#### Workers
Some web applications need to run extra deployments for "worker processes". For
-example it is common in a Rails application to have a separate worker process
+example, it is common in a Rails application to have a separate worker process
to run background tasks like sending emails.
The [default Helm chart](https://gitlab.com/gitlab-org/charts/auto-deploy-app)
used in Auto Deploy [has support for running worker
processes](https://gitlab.com/gitlab-org/charts/auto-deploy-app/merge_requests/9).
-In order to run a worker you'll need to ensure that it is able to respond to
-the standard health checks which expect a successful HTTP response on port
-`5000`. For sidekiq you could make use of the
-[sidekiq_alive gem](https://rubygems.org/gems/sidekiq_alive) to do this.
+In order to run a worker, you'll need to ensure that it is able to respond to
+the standard health checks, which expect a successful HTTP response on port
+`5000`. For [Sidekiq](https://github.com/mperham/sidekiq), you could make use of
+the [`sidekiq_alive` gem](https://rubygems.org/gems/sidekiq_alive) to do this.
-In order to work with sidekiq you'll also need to ensure your deployments have
-access to a redis instance. Auto DevOps won't deploy this for you so you'll
-need to manage this separately and then set a CI variable
-`K8S_SECRET_REDIS_URL` which the URL of this instance to ensure it's passed
-into your deployments.
+In order to work with Sidekiq, you'll also need to ensure your deployments have
+access to a Redis instance. Auto DevOps won't deploy this for you so you'll
+need to:
-Once you have configured your worker to respond to health checks you you will
+- Maintain your own Redis instance.
+- Set a CI variable `K8S_SECRET_REDIS_URL`, which the URL of this instance to
+ ensure it's passed into your deployments.
+
+Once you have configured your worker to respond to health checks, you will
need to configure a CI variable `HELM_UPGRADE_EXTRA_ARGS` with the value
-`--values helm-values.yaml`. Then you can, for example, run a
-[sidekiq](https://github.com/mperham/sidekiq) worker for your rails application
-by adding a file named `helm-values.yaml` to your repo with the following
+`--values helm-values.yaml`.
+
+Then you can, for example, run a Sidekiq worker for your Rails application
+by adding a file named `helm-values.yaml` to your repository with the following
content:
```yml
diff --git a/doc/topics/autodevops/quick_start_guide.md b/doc/topics/autodevops/quick_start_guide.md
index 7ab59b80374..35a5aff6a60 100644
--- a/doc/topics/autodevops/quick_start_guide.md
+++ b/doc/topics/autodevops/quick_start_guide.md
@@ -167,7 +167,7 @@ In the **test** stage, GitLab runs various checks on the application:
- The `sast` job runs static analysis on the current code to check for potential
security issues and is allowed to fail([Auto SAST](index.md#auto-sast-ultimate)) **(ULTIMATE)**
- The `license_management` job searches the application's dependencies to determine each of their
- licenses and is allowed to fail ([Auto License Management](index.md#auto-license-management-ultimate)) **(ULTIMATE)**
+ licenses and is allowed to fail ([Auto License Compliance](index.md#auto-license-compliance-ultimate)) **(ULTIMATE)**
NOTE: **Note:**
As you might have noticed, all jobs except `test` are allowed to fail in the
diff --git a/doc/university/README.md b/doc/university/README.md
index b17f40b91a5..25f77906e68 100644
--- a/doc/university/README.md
+++ b/doc/university/README.md
@@ -57,7 +57,7 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
1. [Migrating from SVN](../user/project/import/svn.md)
1. [Migrating from Fogbugz](../user/project/import/fogbugz.md)
-### 1.6. GitLab Inc.
+### 1.6. The GitLab team
1. [About GitLab](https://about.gitlab.com/about/)
1. [GitLab Direction](https://about.gitlab.com/direction/)
diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md
index 0506d992d4b..6a41c6aec32 100644
--- a/doc/update/patch_versions.md
+++ b/doc/update/patch_versions.md
@@ -41,7 +41,7 @@ sudo -u git -H git checkout -- Gemfile.lock db/schema.rb locale
sudo -u git -H git checkout LATEST_TAG -b LATEST_TAG
```
-### 3. Install libs, migrations, etc.
+### 3. Install libs, migrations, etc
```bash
cd /home/git/gitlab
diff --git a/doc/update/upgrading_from_ce_to_ee.md b/doc/update/upgrading_from_ce_to_ee.md
index bea5bcd9dd7..49e2fb2568e 100644
--- a/doc/update/upgrading_from_ce_to_ee.md
+++ b/doc/update/upgrading_from_ce_to_ee.md
@@ -54,7 +54,7 @@ sudo -u git -H git remote add -f ee https://gitlab.com/gitlab-org/gitlab-ee.git
sudo -u git -H git checkout EE_BRANCH
```
-### 3. Install libs, migrations, etc.
+### 3. Install libs, migrations, etc
```sh
cd /home/git/gitlab
diff --git a/doc/update/upgrading_from_source.md b/doc/update/upgrading_from_source.md
index df35638cba2..f6b64dcf694 100644
--- a/doc/update/upgrading_from_source.md
+++ b/doc/update/upgrading_from_source.md
@@ -313,7 +313,7 @@ For Ubuntu 16.04.1 LTS:
sudo systemctl daemon-reload
```
-### 13. Install libs, migrations, etc.
+### 13. Install libs, migrations, etc
```bash
cd /home/git/gitlab
diff --git a/doc/user/application_security/index.md b/doc/user/application_security/index.md
index 83ea0ea3386..fcd683ca2db 100644
--- a/doc/user/application_security/index.md
+++ b/doc/user/application_security/index.md
@@ -28,7 +28,7 @@ GitLab can scan and report any vulnerabilities found in your project.
| [Dependency List](dependency_list/index.md) **(ULTIMATE)** | View your project's dependencies and their known vulnerabilities. |
| [Dependency Scanning](dependency_scanning/index.md) **(ULTIMATE)** | Analyze your dependencies for known vulnerabilities. |
| [Dynamic Application Security Testing (DAST)](dast/index.md) **(ULTIMATE)** | Analyze running web applications for known vulnerabilities. |
-| [License Management](license_management/index.md) **(ULTIMATE)** | Search your project's dependencies for their licenses. |
+| [License Compliance](license_management/index.md) **(ULTIMATE)** | Search your project's dependencies for their licenses. |
| [Security Dashboard](security_dashboard/index.md) **(ULTIMATE)** | View vulnerabilities in all your projects and groups. |
| [Static Application Security Testing (SAST)](sast/index.md) **(ULTIMATE)** | Analyze source code for known vulnerabilities. |
diff --git a/doc/user/application_security/license_management/index.md b/doc/user/application_security/license_management/index.md
index c324848c703..912f2f0e209 100644
--- a/doc/user/application_security/license_management/index.md
+++ b/doc/user/application_security/license_management/index.md
@@ -2,7 +2,7 @@
type: reference, howto
---
-# License Management **(ULTIMATE)**
+# License Compliance **(ULTIMATE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5483)
in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.0.
@@ -10,18 +10,18 @@ in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.0.
## Overview
If you are using [GitLab CI/CD](../../../ci/README.md), you can search your project dependencies for their licenses
-using License Management.
+using License Compliance.
-You can take advantage of License Management by either [including the job](#configuration)
+You can take advantage of License Compliance by either [including the job](#configuration)
in your existing `.gitlab-ci.yml` file or by implicitly using
-[Auto License Management](../../../topics/autodevops/index.md#auto-license-management-ultimate)
+[Auto License Compliance](../../../topics/autodevops/index.md#auto-license-compliance-ultimate)
that is provided by [Auto DevOps](../../../topics/autodevops/index.md).
-GitLab checks the License Management report, compares the licenses between the
+GitLab checks the License Compliance report, compares the licenses between the
source and target branches, and shows the information right on the merge request.
Blacklisted licenses will be clearly visible with an `x` red icon next to them
as well as new licenses which need a decision from you. In addition, you can
-[manually approve or blacklist](#project-policies-for-license-management)
+[manually approve or blacklist](#project-policies-for-license-compliance)
licenses in your project's settings.
NOTE: **Note:**
@@ -31,7 +31,7 @@ will be displayed in the merge request area. That is the case when you add the
Consecutive merge requests will have something to compare to and the license
management report will be shown properly.
-![License Management Widget](img/license_management.png)
+![License Compliance Widget](img/license_management.png)
If you are a project or group Maintainer, you can click on a license to be given
the choice to approve it or blacklist it.
@@ -66,12 +66,12 @@ The following languages and package managers are supported.
## Requirements
-To run a License Management scanning job, you need GitLab Runner with the
+To run a License Compliance scanning job, you need GitLab Runner with the
[`docker` executor](https://docs.gitlab.com/runner/executors/docker.html).
## Configuration
-For GitLab 11.9 and later, to enable License Management, you must
+For GitLab 11.9 and later, to enable License Compliance, you must
[include](../../../ci/yaml/README.md#includetemplate) the
[`License-Management.gitlab-ci.yml` template](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/lib/gitlab/ci/templates/Security/License-Management.gitlab-ci.yml)
that's provided as a part of your GitLab installation.
@@ -89,14 +89,14 @@ The included template will create a `license_management` job in your CI/CD pipel
and scan your dependencies to find their licenses.
The results will be saved as a
-[License Management report artifact](../../../ci/yaml/README.md#artifactsreportslicense_management-ultimate)
+[License Compliance report artifact](../../../ci/yaml/README.md#artifactsreportslicense_management-ultimate)
that you can later download and analyze. Due to implementation limitations, we
-always take the latest License Management artifact available. Behind the scenes, the
-[GitLab License Management Docker image](https://gitlab.com/gitlab-org/security-products/license-management)
+always take the latest License Compliance artifact available. Behind the scenes, the
+[GitLab License Compliance Docker image](https://gitlab.com/gitlab-org/security-products/license-management)
is used to detect the languages/frameworks and in turn analyzes the licenses.
-The License Management settings can be changed through environment variables by using the
-[`variables`](../../../ci/yaml/README.md#variables) parameter in `.gitlab-ci.yml`. These variables are documented in the [License Management documentation](https://gitlab.com/gitlab-org/security-products/license-management#settings).
+The License Compliance settings can be changed through environment variables by using the
+[`variables`](../../../ci/yaml/README.md#variables) parameter in `.gitlab-ci.yml`. These variables are documented in the [License Compliance documentation](https://gitlab.com/gitlab-org/security-products/license-management#settings).
### Installing custom dependencies
@@ -143,7 +143,7 @@ license_management:
### Configuring Maven projects
-The License Management tool provides a `MAVEN_CLI_OPTS` environment variable which can hold
+The License Compliance tool provides a `MAVEN_CLI_OPTS` environment variable which can hold
the command line arguments to pass to the `mvn install` command which is executed under the hood.
Feel free to use it for the customization of Maven execution. For example:
@@ -169,7 +169,7 @@ If you still need to run tests during `mvn install`, add `-DskipTests=false` to
> [Introduced](https://gitlab.com/gitlab-org/security-products/license-management/merge_requests/36) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.0.
-License Management uses Python 2.7 and pip 10.0 by default.
+License Compliance uses Python 2.7 and pip 10.0 by default.
If your project requires Python 3, you can switch to Python 3.5 and pip 19.1
by setting the `LM_PYTHON_VERSION` environment variable to `3`.
@@ -182,7 +182,7 @@ license_management:
LM_PYTHON_VERSION: 3
```
-## Project policies for License Management
+## Project policies for License Compliance
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5940)
in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.4.
@@ -196,10 +196,10 @@ To approve or blacklist a license:
1. Either use the **Manage licenses** button in the merge request widget, or
navigate to the project's **Settings > CI/CD** and expand the
- **License Management** section.
+ **License Compliance** section.
1. Click the **Add a license** button.
- ![License Management Add License](img/license_management_add_license.png)
+ ![License Compliance Add License](img/license_management_add_license.png)
1. In the **License name** dropdown, either:
- Select one of the available licenses. You can search for licenses in the field
@@ -211,17 +211,17 @@ To approve or blacklist a license:
To modify an existing license:
-1. In the **License Management** list, click the **Approved/Declined** dropdown to change it to the desired status.
+1. In the **License Compliance** list, click the **Approved/Declined** dropdown to change it to the desired status.
- ![License Management Settings](img/license_management_settings.png)
+ ![License Compliance Settings](img/license_management_settings.png)
Searching for Licenses:
1. Use the **Search** box to search for a specific license.
- ![License Management Search](img/license_management_search.png)
+ ![License Compliance Search](img/license_management_search.png)
-## License Management report under pipelines
+## License Compliance report under pipelines
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5491)
in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.2.
@@ -230,7 +230,7 @@ From your project's left sidebar, navigate to **CI/CD > Pipelines** and click on
pipeline ID that has a `license_management` job to see the Licenses tab with the listed
licenses (if any).
-![License Management Pipeline Tab](img/license_management_pipeline_tab.png)
+![License Compliance Pipeline Tab](img/license_management_pipeline_tab.png)
<!-- ## Troubleshooting
diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md
index d21a325d401..af37cc896ad 100644
--- a/doc/user/gitlab_com/index.md
+++ b/doc/user/gitlab_com/index.md
@@ -43,13 +43,15 @@ Host gitlab.com
Below are the settings for [GitLab Pages].
-| Setting | GitLab.com | Default |
-| ----------------------- | ---------------- | ------------- |
-| Domain name | `gitlab.io` | - |
-| IP address | `35.185.44.232` | - |
-| Custom domains support | yes | no |
-| TLS certificates support| yes | no |
+| Setting | GitLab.com | Default |
+| --------------------------- | ---------------- | ------------- |
+| Domain name | `gitlab.io` | - |
+| IP address | `35.185.44.232` | - |
+| Custom domains support | yes | no |
+| TLS certificates support | yes | no |
+| Maximum size (uncompressed) | 1G | 100M |
+NOTE: **Note:**
The maximum size of your Pages site is regulated by the artifacts maximum size
which is part of [GitLab CI/CD](#gitlab-cicd).
@@ -59,7 +61,7 @@ Below are the current settings regarding [GitLab CI/CD](../../ci/README.md).
| Setting | GitLab.com | Default |
| ----------- | ----------------- | ------------- |
-| Artifacts maximum size | 1G | 100M |
+| Artifacts maximum size (uncompressed) | 1G | 100M |
| Artifacts [expiry time](../../ci/yaml/README.md#artifactsexpire_in) | kept forever | deleted after 30 days unless otherwise specified |
## Repository size limit
diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md
index e3f657af564..bd50367681e 100644
--- a/doc/user/group/saml_sso/index.md
+++ b/doc/user/group/saml_sso/index.md
@@ -39,6 +39,25 @@ However, users will not be prompted to log via SSO on each visit. GitLab will ch
We intend to add a similar SSO requirement for [Git and API activity](https://gitlab.com/gitlab-org/gitlab-ee/issues/9152) in the future.
+#### Group-managed accounts
+
+[Introduced in GitLab 12.1](https://gitlab.com/groups/gitlab-org/-/epics/709).
+
+When SSO is being enforced, groups can enable an additional level of protection by enforcing the creation of dedicated user accounts to access the group.
+
+Without group-managed accounts, users can link their SAML identity with any existing user on the instance. With group-managed accounts enabled, users are required to create a new, dedicated user linked to the group. The notification email address associated with the user is locked to the email address received from the configured identity provider.
+
+When this option is enabled:
+
+- All existing and new users in the group will be required to log in via the SSO URL associated with the group.
+- On successfully authenticating, GitLab will prompt the user to create a new, dedicated account using the email address received from the configured identity provider.
+- After the group managed account has been created, group activity will require the use of this user account.
+
+Since use of the group managed account requires the use of SSO, users of group managed accounts will lose access to these accounts when they are no longer able to authenticate with the connected identity provider. In the case of an offboarded employee who has been removed from your identity provider:
+
+- The user will be unable to access the group (their credentials will no longer work on the identity provider when prompted to SSO).
+- Contributions in the group (e.g. issues, merge requests) will remain intact.
+
### NameID
GitLab.com uses the SAML NameID to identify users. The NameID element:
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index 6cfc8b6429b..17bbed2945d 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -57,12 +57,6 @@ render incorrectly:
- milk
```
-1. Chocolate
- - dark
- - milk
-
----
-
Simply add a space to each nested item to align the `-` with the first character of
the top list item (`C` in this case):
@@ -265,8 +259,7 @@ this font installed by default.
### Front matter
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/23331)
- in GitLab 11.6.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/23331) in GitLab 11.6.
Front matter is metadata included at the beginning of a markdown document, preceding
its content. This data can be used by static site generators such as [Jekyll](https://jekyllrb.com/docs/front-matter/),
@@ -866,18 +859,6 @@ or underscores
___
```
-Three or more hyphens,
-
----
-
-asterisks,
-
-***
-
-or underscores
-
-___
-
### Images
Examples:
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 9ecc8a80b3a..80d1bf992ec 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -45,7 +45,7 @@ The following table depicts the various user permission levels in a project.
| Leave comments | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
| View Insights charts **(ULTIMATE)** | ✓ | ✓ | ✓ | ✓ | ✓ |
| View approved/blacklisted licenses **(ULTIMATE)** | ✓ | ✓ | ✓ | ✓ | ✓ |
-| View license management reports **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
+| View License Compliance reports **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
| View Security reports **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
| View Dependency list **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
| View [Design Management](project/issues/design_management.md) pages **(PREMIUM)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md
index e2b1a20a605..f7ba921aa7d 100644
--- a/doc/user/profile/account/two_factor_authentication.md
+++ b/doc/user/profile/account/two_factor_authentication.md
@@ -48,6 +48,7 @@ To enable 2FA:
- [andOTP](https://github.com/andOTP/andOTP): feature rich open source app for Android which supports PGP encrypted backups.
- [FreeOTP](https://freeotp.github.io/): open source app for Android.
- [Google Authenticator](https://support.google.com/accounts/answer/1066447?hl=en): proprietary app for iOS and Android.
+ - [SailOTP](https://openrepos.net/content/seiichiro0185/sailotp): open source app for SailFish OS.
1. In the application, add a new entry in one of two ways:
- Scan the code presented in GitLab with your device's camera to add the
entry automatically.
diff --git a/doc/user/project/import/gitlab_com.md b/doc/user/project/import/gitlab_com.md
index f48a158e2d3..2f87f257754 100644
--- a/doc/user/project/import/gitlab_com.md
+++ b/doc/user/project/import/gitlab_com.md
@@ -1,21 +1,21 @@
# Project importing from GitLab.com to your private GitLab instance
-You can import your existing GitLab.com projects to your GitLab instance. But keep in mind that it is possible only if
-GitLab.com integration is enabled on your GitLab instance.
+You can import your existing GitLab.com projects to your GitLab instance, but keep in
+mind that it is possible only if GitLab.com integration is enabled on your GitLab instance.
[Read more about GitLab.com integration for self-managed GitLab instances](../../../integration/gitlab.md).
To get to the importer page you need to go to "New project" page.
>**Note:**
-If you are interested in importing Wiki and Merge Request data to your new
-instance, you'll need to follow the instructions for [project export](../settings/import_export.md)
+If you are interested in importing Wiki and Merge Request data to your new instance,
+you'll need to follow the instructions for [exporting a project](../settings/import_export.md#exporting-a-project-and-its-data)
-![New project page](img/gitlab_new_project_page.png)
+![New project page](img/gitlab_new_project_page_v12_2.png)
-Click on the "Import projects from GitLab.com" link and you will be redirected to GitLab.com
+Go to the **Import Projects** tab, then click on **GitLab.com**, and you will be redirected to GitLab.com
for permission to access your projects. After accepting, you'll be automatically redirected to the importer.
![Importer page](img/gitlab_importer.png)
-To import a project, you can simple click "Import". The importer will import your repository and issues.
+To import a project, click "Import". The importer will import your repository and issues.
Once the importer is done, a new GitLab project will be created with your imported data.
diff --git a/doc/user/project/import/img/gitlab_new_project_page.png b/doc/user/project/import/img/gitlab_new_project_page.png
deleted file mode 100644
index c673724f436..00000000000
--- a/doc/user/project/import/img/gitlab_new_project_page.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/import/img/gitlab_new_project_page_v12_2.png b/doc/user/project/import/img/gitlab_new_project_page_v12_2.png
new file mode 100644
index 00000000000..e79c27f32c0
--- /dev/null
+++ b/doc/user/project/import/img/gitlab_new_project_page_v12_2.png
Binary files differ
diff --git a/doc/user/project/index.md b/doc/user/project/index.md
index 30ff0e9ff07..932e7fd10b2 100644
--- a/doc/user/project/index.md
+++ b/doc/user/project/index.md
@@ -98,7 +98,7 @@ When you create a project in GitLab, you'll have access to a large number of
- [Maven packages](packages/maven_repository.md): your private Maven repository in GitLab. **(PREMIUM)**
- [NPM packages](packages/npm_registry.md): your private NPM package registry in GitLab. **(PREMIUM)**
- [Code owners](code_owners.md): specify code owners for certain files **(STARTER)**
-- [License Management](../application_security/license_management/index.md): approve and blacklist licenses for projects. **(ULTIMATE)**
+- [License Compliance](../application_security/license_management/index.md): approve and blacklist licenses for projects. **(ULTIMATE)**
- [Dependency List](../application_security/dependency_list/index.md): view project dependencies. **(ULTIMATE)**
### Project integrations
diff --git a/doc/user/project/integrations/img/download_as_csv.png b/doc/user/project/integrations/img/download_as_csv.png
new file mode 100644
index 00000000000..0ed5ab8db89
--- /dev/null
+++ b/doc/user/project/integrations/img/download_as_csv.png
Binary files differ
diff --git a/doc/user/project/integrations/img/generate_link_to_chart.png b/doc/user/project/integrations/img/generate_link_to_chart.png
new file mode 100644
index 00000000000..03e018969b1
--- /dev/null
+++ b/doc/user/project/integrations/img/generate_link_to_chart.png
Binary files differ
diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md
index aa7db97c413..9fc0ade809e 100644
--- a/doc/user/project/integrations/prometheus.md
+++ b/doc/user/project/integrations/prometheus.md
@@ -269,6 +269,12 @@ Note the following properties:
![single stat panel type](img/prometheus_dashboard_single_stat_panel_type.png)
+### Downloading data as CSV
+
+Data from Prometheus charts on the metrics dashboard can be downloaded as CSV.
+
+![Downloading as CSV](img/download_as_csv.png)
+
### Setting up alerts for Prometheus metrics **(ULTIMATE)**
#### Managed Prometheus instances
@@ -354,7 +360,7 @@ Prometheus server.
![Merge Request with Performance Impact](img/merge_request_performance.png)
-## Embedding metric charts within Gitlab Flavored Markdown
+## Embedding metric charts within GitLab Flavored Markdown
> [Introduced][ce-29691] in GitLab 12.2.
> Requires [Kubernetes](prometheus_library/kubernetes.md) metrics.
@@ -363,6 +369,10 @@ It is possible to display metrics charts within [GitLab Flavored Markdown](../..
To display a metric chart, include a link of the form `https://<root_url>/<project>/environments/<environment_id>/metrics`.
+A single chart may also be embedded. You can generate a link to the chart via the dropdown located on the right side of the chart:
+
+![Generate Link To Chart](img/generate_link_to_chart.png)
+
The following requirements must be met for the metric to unfurl:
- The `<environment_id>` must correspond to a real environment.
diff --git a/doc/user/project/members/img/access_requests_management.png b/doc/user/project/members/img/access_requests_management.png
index 8996d9564d7..9a1c9621e41 100644
--- a/doc/user/project/members/img/access_requests_management.png
+++ b/doc/user/project/members/img/access_requests_management.png
Binary files differ
diff --git a/doc/user/project/members/img/request_access_button.png b/doc/user/project/members/img/request_access_button.png
index e8b490b91b8..e693f9a9ac2 100644
--- a/doc/user/project/members/img/request_access_button.png
+++ b/doc/user/project/members/img/request_access_button.png
Binary files differ
diff --git a/doc/user/project/members/img/withdraw_access_request_button.png b/doc/user/project/members/img/withdraw_access_request_button.png
index 6a3172dfcdb..e5a8fe0b356 100644
--- a/doc/user/project/members/img/withdraw_access_request_button.png
+++ b/doc/user/project/members/img/withdraw_access_request_button.png
Binary files differ
diff --git a/doc/user/project/merge_requests/fast_forward_merge.md b/doc/user/project/merge_requests/fast_forward_merge.md
index 4f3c68090e4..2d59e535ce1 100644
--- a/doc/user/project/merge_requests/fast_forward_merge.md
+++ b/doc/user/project/merge_requests/fast_forward_merge.md
@@ -15,7 +15,7 @@ to accept merge requests without creating merge commits.
When the fast-forward merge
([`--ff-only`](https://git-scm.com/docs/git-merge#git-merge---ff-only)) setting
is enabled, no merge commits will be created and all merges are fast-forwarded,
-which means that merging is only allowed if the branch could be fast-forwarded.
+which means that merging is only allowed if the branch can be fast-forwarded.
When a fast-forward merge is not possible, the user is given the option to rebase.
@@ -28,9 +28,15 @@ When a fast-forward merge is not possible, the user is given the option to rebas
Now, when you visit the merge request page, you will be able to accept it
**only if a fast-forward merge is possible**.
+![Fast forward merge request](img/ff_merge_mr.png)
+
+If a fast-forward merge is not possible but a conflict free rebase is possible,
+a rebase button will be offered.
+
![Fast forward merge request](img/ff_merge_rebase.png)
-If the target branch is ahead of the source branch, you need to rebase the
+If the target branch is ahead of the source branch and a conflict free rebase is
+not possible, you need to rebase the
source branch locally before you will be able to do a fast-forward merge.
![Fast forward merge rebase locally](img/ff_merge_rebase_locally.png)
diff --git a/doc/user/project/merge_requests/img/ff_merge_mr.png b/doc/user/project/merge_requests/img/ff_merge_mr.png
new file mode 100644
index 00000000000..241cc990343
--- /dev/null
+++ b/doc/user/project/merge_requests/img/ff_merge_mr.png
Binary files differ
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index 2794cbc0f39..04db54872d3 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -41,7 +41,7 @@ With **[GitLab Enterprise Edition][ee]**, you can also:
- View the deployment process across projects with [Multi-Project Pipelines](../../../ci/multi_project_pipelines.md) **(PREMIUM)**
- Request [approvals](merge_request_approvals.md) from your managers **(STARTER)**
- Analyze the impact of your changes with [Code Quality reports](code_quality.md) **(STARTER)**
-- Manage the licenses of your dependencies with [License Management](../../application_security/license_management/index.md) **(ULTIMATE)**
+- Manage the licenses of your dependencies with [License Compliance](../../application_security/license_management/index.md) **(ULTIMATE)**
- Analyze your source code for vulnerabilities with [Static Application Security Testing](../../application_security/sast/index.md) **(ULTIMATE)**
- Analyze your running web applications for vulnerabilities with [Dynamic Application Security Testing](../../application_security/dast/index.md) **(ULTIMATE)**
- Analyze your dependencies for vulnerabilities with [Dependency Scanning](../../application_security/dependency_scanning/index.md) **(ULTIMATE)**
@@ -57,7 +57,7 @@ A. Consider you are a software developer working in a team:
1. You gather feedback from your team
1. You work on the implementation optimizing code with [Code Quality reports](code_quality.md) **(STARTER)**
1. You verify your changes with [JUnit test reports](../../../ci/junit_test_reports.md) in GitLab CI/CD
-1. You avoid using dependencies whose license is not compatible with your project with [License Management reports](license_management.md) **(ULTIMATE)**
+1. You avoid using dependencies whose license is not compatible with your project with [License Compliance reports](license_management.md) **(ULTIMATE)**
1. You request the [approval](#merge-request-approvals-starter) from your manager
1. Your manager pushes a commit with their final review, [approves the merge request](merge_request_approvals.md), and set it to [merge when pipeline succeeds](#merge-when-pipeline-succeeds) (Merge Request Approvals are available in GitLab Starter)
1. Your changes get deployed to production with [manual actions](../../../ci/yaml/README.md#whenmanual) for GitLab CI/CD
diff --git a/doc/user/project/repository/img/repository_languages.png b/doc/user/project/repository/img/repository_languages.png
deleted file mode 100644
index 5977ad7faae..00000000000
--- a/doc/user/project/repository/img/repository_languages.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/repository/img/repository_languages_v12_2.gif b/doc/user/project/repository/img/repository_languages_v12_2.gif
new file mode 100644
index 00000000000..d0a0e57c919
--- /dev/null
+++ b/doc/user/project/repository/img/repository_languages_v12_2.gif
Binary files differ
diff --git a/doc/user/project/repository/index.md b/doc/user/project/repository/index.md
index 84d63d29929..bd966185c94 100644
--- a/doc/user/project/repository/index.md
+++ b/doc/user/project/repository/index.md
@@ -182,7 +182,7 @@ were used and display this on the projects pages. If this information is missing
be added after updating the default branch on the project. This process can take up to 5
minutes.
-![Repository Languages bar](img/repository_languages.png)
+![Repository Languages bar](img/repository_languages_v12_2.gif)
Not all files are detected, among others; documentation,
vendored code, and most markup languages are excluded. This behaviour can be
diff --git a/doc/user/project/wiki/index.md b/doc/user/project/wiki/index.md
index e3c6cd6d6ff..d1bab47de25 100644
--- a/doc/user/project/wiki/index.md
+++ b/doc/user/project/wiki/index.md
@@ -28,11 +28,12 @@ NOTE: **Note:**
Requires Developer [permissions](../../permissions.md).
Create a new page by clicking the **New page** button that can be found
-in all wiki pages. You will be asked to fill in the page name from which GitLab
-will create the path to the page. You can specify a full path for the new file
-and any missing directories will be created automatically.
+in all wiki pages.
-![New page modal](img/wiki_create_new_page_modal.png)
+You will be asked to fill in a title for your new wiki page. Wiki titles
+also determine the path to the wiki page. You can specify a full path
+(using "`/`" for subdirectories) for the new title and any missing
+directories will be created automatically.
Once you enter the page name, it's time to fill in its content. GitLab wikis
support Markdown, RDoc and AsciiDoc. For Markdown based pages, all the
diff --git a/doc/workflow/git_annex.md b/doc/workflow/git_annex.md
index 9543859f86f..cdc38a5b7eb 100644
--- a/doc/workflow/git_annex.md
+++ b/doc/workflow/git_annex.md
@@ -200,7 +200,7 @@ searching for your distribution.
Although there is no general guide for `git-annex` errors, there are a few tips
on how to go around the warnings.
-### git-annex-shell: Not a git-annex or gcrypt repository.
+### git-annex-shell: Not a git-annex or gcrypt repository
This warning can appear on the initial `git annex sync --content` and is caused
by differences in `git-annex-shell`. You can read more about it
diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md
index ccb8844aea3..5abae929355 100644
--- a/doc/workflow/notifications.md
+++ b/doc/workflow/notifications.md
@@ -53,7 +53,7 @@ These settings can be configured on group page under the name of the group. It w
The group owner can disable email notifications for a group, which also includes
it's subgroups and projects. If this is the case, you will not receive any corresponding notifications,
-and the notification button will be disabled with an explanatory tooltip.
+and the notification button will be disabled with an explanatory tooltip.
### Project Settings
@@ -66,7 +66,7 @@ These settings can be configured on project page under the name of the project.
The project owner (or it's group owner) can disable email notifications for the project.
If this is the case, you will not receive any corresponding notifications, and the notification
-button will be disabled with an explanatory tooltip.
+button will be disabled with an explanatory tooltip.
## Notification events
diff --git a/lib/api/api.rb b/lib/api/api.rb
index e500a93b31e..219ed45eff6 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -18,7 +18,7 @@ module API
formatter: Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp.new,
include: [
GrapeLogging::Loggers::FilterParameters.new(LOG_FILTERS),
- GrapeLogging::Loggers::ClientEnv.new,
+ Gitlab::GrapeLogging::Loggers::ClientEnvLogger.new,
Gitlab::GrapeLogging::Loggers::RouteLogger.new,
Gitlab::GrapeLogging::Loggers::UserLogger.new,
Gitlab::GrapeLogging::Loggers::QueueDurationLogger.new,
diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb
index a1851ba3627..89b7e5c5e4b 100644
--- a/lib/api/award_emoji.rb
+++ b/lib/api/award_emoji.rb
@@ -69,12 +69,12 @@ module API
post endpoint do
not_found!('Award Emoji') unless can_read_awardable? && can_award_awardable?
- award = awardable.create_award_emoji(params[:name], current_user)
+ service = AwardEmojis::AddService.new(awardable, params[:name], current_user).execute
- if award.persisted?
- present award, with: Entities::AwardEmoji
+ if service[:status] == :success
+ present service[:award], with: Entities::AwardEmoji
else
- not_found!("Award Emoji #{award.errors.messages}")
+ not_found!("Award Emoji #{service[:message]}")
end
end
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 996205d4b7b..3073c14b341 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -489,11 +489,13 @@ module API
end
params do
optional :search, type: String, desc: 'Return list of users matching the search criteria'
+ optional :skip_users, type: Array[Integer], desc: 'Filter out users with the specified IDs'
use :pagination
end
get ':id/users' do
users = DeclarativePolicy.subject_scope { user_project.team.users }
users = users.search(params[:search]) if params[:search].present?
+ users = users.where_not_in(params[:skip_users]) if params[:skip_users].present?
present paginate(users), with: Entities::UserBasic
end
diff --git a/lib/feature/gitaly.rb b/lib/feature/gitaly.rb
index 9ded1aed4e3..be397a478cd 100644
--- a/lib/feature/gitaly.rb
+++ b/lib/feature/gitaly.rb
@@ -6,8 +6,10 @@ class Feature
class Gitaly
# Server feature flags should use '_' to separate words.
SERVER_FEATURE_FLAGS =
- [
- # 'get_commit_signatures'.freeze
+ %w[
+ get_commit_signatures
+ cache_invalidator
+ inforef_uploadpack_cache
].freeze
DEFAULT_ON_FLAGS = Set.new([]).freeze
diff --git a/lib/gitlab/action_rate_limiter.rb b/lib/gitlab/action_rate_limiter.rb
index fdb06d00548..0e8707af631 100644
--- a/lib/gitlab/action_rate_limiter.rb
+++ b/lib/gitlab/action_rate_limiter.rb
@@ -49,9 +49,9 @@ module Gitlab
request_information = {
message: 'Action_Rate_Limiter_Request',
env: type,
- ip: request.ip,
+ remote_ip: request.ip,
request_method: request.request_method,
- fullpath: request.fullpath
+ path: request.fullpath
}
if current_user
diff --git a/lib/gitlab/ci/build/policy/variables.rb b/lib/gitlab/ci/build/policy/variables.rb
index 0698136166a..e9c8864123f 100644
--- a/lib/gitlab/ci/build/policy/variables.rb
+++ b/lib/gitlab/ci/build/policy/variables.rb
@@ -10,7 +10,7 @@ module Gitlab
end
def satisfied_by?(pipeline, seed)
- variables = seed.to_resource.scoped_variables_hash
+ variables = seed.scoped_variables_hash
statements = @expressions.map do |statement|
::Gitlab::Ci::Pipeline::Expression::Statement
diff --git a/lib/gitlab/ci/build/rules.rb b/lib/gitlab/ci/build/rules.rb
new file mode 100644
index 00000000000..89623a809c9
--- /dev/null
+++ b/lib/gitlab/ci/build/rules.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Build
+ class Rules
+ include ::Gitlab::Utils::StrongMemoize
+
+ Result = Struct.new(:when, :start_in)
+
+ def initialize(rule_hashes, default_when = 'on_success')
+ @rule_list = Rule.fabricate_list(rule_hashes)
+ @default_when = default_when
+ end
+
+ def evaluate(pipeline, build)
+ if @rule_list.nil?
+ Result.new(@default_when)
+ elsif matched_rule = match_rule(pipeline, build)
+ Result.new(
+ matched_rule.attributes[:when] || @default_when,
+ matched_rule.attributes[:start_in]
+ )
+ else
+ Result.new('never')
+ end
+ end
+
+ private
+
+ def match_rule(pipeline, build)
+ @rule_list.find { |rule| rule.matches?(pipeline, build) }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/rules/rule.rb b/lib/gitlab/ci/build/rules/rule.rb
new file mode 100644
index 00000000000..8d52158c8d2
--- /dev/null
+++ b/lib/gitlab/ci/build/rules/rule.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Build
+ class Rules::Rule
+ attr_accessor :attributes
+
+ def self.fabricate_list(list)
+ list.map(&method(:new)) if list
+ end
+
+ def initialize(spec)
+ @clauses = []
+ @attributes = {}
+
+ spec.each do |type, value|
+ if clause = Clause.fabricate(type, value)
+ @clauses << clause
+ else
+ @attributes.merge!(type => value)
+ end
+ end
+ end
+
+ def matches?(pipeline, build)
+ @clauses.all? { |clause| clause.satisfied_by?(pipeline, build) }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/rules/rule/clause.rb b/lib/gitlab/ci/build/rules/rule/clause.rb
new file mode 100644
index 00000000000..ff0baf3348c
--- /dev/null
+++ b/lib/gitlab/ci/build/rules/rule/clause.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Build
+ class Rules::Rule::Clause
+ ##
+ # Abstract class that defines an interface of a single
+ # job rule specification.
+ #
+ # Used for job's inclusion rules configuration.
+ #
+ UnknownClauseError = Class.new(StandardError)
+
+ def self.fabricate(type, value)
+ type = type.to_s.camelize
+
+ self.const_get(type).new(value) if self.const_defined?(type)
+ end
+
+ def initialize(spec)
+ @spec = spec
+ end
+
+ def satisfied_by?(pipeline, seed = nil)
+ raise NotImplementedError
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/rules/rule/clause/changes.rb b/lib/gitlab/ci/build/rules/rule/clause/changes.rb
new file mode 100644
index 00000000000..81d2ee6c24c
--- /dev/null
+++ b/lib/gitlab/ci/build/rules/rule/clause/changes.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Build
+ class Rules::Rule::Clause::Changes < Rules::Rule::Clause
+ def initialize(globs)
+ @globs = Array(globs)
+ end
+
+ def satisfied_by?(pipeline, seed)
+ return true if pipeline.modified_paths.nil?
+
+ pipeline.modified_paths.any? do |path|
+ @globs.any? do |glob|
+ File.fnmatch?(glob, path, File::FNM_PATHNAME | File::FNM_DOTMATCH | File::FNM_EXTGLOB)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/rules/rule/clause/if.rb b/lib/gitlab/ci/build/rules/rule/clause/if.rb
new file mode 100644
index 00000000000..18c3b450f95
--- /dev/null
+++ b/lib/gitlab/ci/build/rules/rule/clause/if.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Build
+ class Rules::Rule::Clause::If < Rules::Rule::Clause
+ def initialize(expression)
+ @expression = expression
+ end
+
+ def satisfied_by?(pipeline, seed)
+ variables = seed.scoped_variables_hash
+
+ ::Gitlab::Ci::Pipeline::Expression::Statement.new(@expression, variables).truthful?
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb
index 29a52b9da17..6e11c582750 100644
--- a/lib/gitlab/ci/config/entry/job.rb
+++ b/lib/gitlab/ci/config/entry/job.rb
@@ -11,7 +11,8 @@ module Gitlab
include ::Gitlab::Config::Entry::Configurable
include ::Gitlab::Config::Entry::Attributable
- ALLOWED_KEYS = %i[tags script only except type image services
+ ALLOWED_WHEN = %w[on_success on_failure always manual delayed].freeze
+ ALLOWED_KEYS = %i[tags script only except rules type image services
allow_failure type stage when start_in artifacts cache
dependencies needs before_script after_script variables
environment coverage retry parallel extends].freeze
@@ -19,12 +20,19 @@ module Gitlab
REQUIRED_BY_NEEDS = %i[stage].freeze
validations do
+ validates :config, type: Hash
validates :config, allowed_keys: ALLOWED_KEYS
validates :config, required_keys: REQUIRED_BY_NEEDS, if: :has_needs?
validates :config, presence: true
validates :script, presence: true
validates :name, presence: true
validates :name, type: Symbol
+ validates :config,
+ disallowed_keys: {
+ in: %i[only except when start_in],
+ message: 'key may not be used with `rules`'
+ },
+ if: :has_rules?
with_options allow_nil: true do
validates :tags, array_of_strings: true
@@ -32,17 +40,19 @@ module Gitlab
validates :parallel, numericality: { only_integer: true,
greater_than_or_equal_to: 2,
less_than_or_equal_to: 50 }
- validates :when,
- inclusion: { in: %w[on_success on_failure always manual delayed],
- message: 'should be on_success, on_failure, ' \
- 'always, manual or delayed' }
+ validates :when, inclusion: {
+ in: ALLOWED_WHEN,
+ message: "should be one of: #{ALLOWED_WHEN.join(', ')}"
+ }
+
validates :dependencies, array_of_strings: true
validates :needs, array_of_strings: true
validates :extends, array_of_strings_or_string: true
+ validates :rules, array_of_hashes: true
end
validates :start_in, duration: { limit: '1 day' }, if: :delayed?
- validates :start_in, absence: true, unless: :delayed?
+ validates :start_in, absence: true, if: -> { has_rules? || !delayed? }
validate do
next unless dependencies.present?
@@ -91,6 +101,9 @@ module Gitlab
entry :except, Entry::Policy,
description: 'Refs policy this job will be executed for.'
+ entry :rules, Entry::Rules,
+ description: 'List of evaluable Rules to determine job inclusion.'
+
entry :variables, Entry::Variables,
description: 'Environment variables available for this job.'
@@ -112,7 +125,7 @@ module Gitlab
:parallel, :needs
attributes :script, :tags, :allow_failure, :when, :dependencies,
- :needs, :retry, :parallel, :extends, :start_in
+ :needs, :retry, :parallel, :extends, :start_in, :rules
def self.matching?(name, config)
!name.to_s.start_with?('.') &&
@@ -151,6 +164,10 @@ module Gitlab
self.when == 'delayed'
end
+ def has_rules?
+ @config.try(:key?, :rules)
+ end
+
def ignored?
allow_failure.nil? ? manual_action? : allow_failure
end
diff --git a/lib/gitlab/ci/config/entry/rules.rb b/lib/gitlab/ci/config/entry/rules.rb
new file mode 100644
index 00000000000..65cad0880f5
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/rules.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ class Rules < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+
+ validations do
+ validates :config, presence: true
+ validates :config, type: Array
+ end
+
+ def compose!(deps = nil)
+ super(deps) do
+ @config.each_with_index do |rule, index|
+ @entries[index] = ::Gitlab::Config::Entry::Factory.new(Entry::Rules::Rule)
+ .value(rule)
+ .with(key: "rule", parent: self, description: "rule definition.") # rubocop:disable CodeReuse/ActiveRecord
+ .create!
+ end
+
+ @entries.each_value do |entry|
+ entry.compose!(deps)
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/rules/rule.rb b/lib/gitlab/ci/config/entry/rules/rule.rb
new file mode 100644
index 00000000000..1f2a34ec90e
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/rules/rule.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ class Rules::Rule < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+ include ::Gitlab::Config::Entry::Attributable
+
+ CLAUSES = %i[if changes].freeze
+ ALLOWED_KEYS = %i[if changes when start_in].freeze
+ ALLOWED_WHEN = %w[on_success on_failure always never manual delayed].freeze
+
+ attributes :if, :changes, :when, :start_in
+
+ validations do
+ validates :config, presence: true
+ validates :config, type: { with: Hash }
+ validates :config, allowed_keys: ALLOWED_KEYS
+ validates :config, disallowed_keys: %i[start_in], unless: :specifies_delay?
+ validates :start_in, presence: true, if: :specifies_delay?
+ validates :start_in, duration: { limit: '1 day' }, if: :specifies_delay?
+
+ with_options allow_nil: true do
+ validates :if, expression: true
+ validates :changes, array_of_strings: true
+ validates :when, allowed_values: { in: ALLOWED_WHEN }
+ end
+ end
+
+ def specifies_delay?
+ self.when == 'delayed'
+ end
+
+ def default
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb b/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb
index 942e4e55323..f7b0720d4a9 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb
@@ -11,8 +11,9 @@ module Gitlab
def evaluate(variables = {})
text = @left.evaluate(variables)
regexp = @right.evaluate(variables)
+ return false unless regexp
- regexp.scan(text.to_s).any?
+ regexp.scan(text.to_s).present?
end
def self.build(_value, behind, ahead)
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb b/lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb
index 831c27fa0ea..02479ed28a4 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb
@@ -11,8 +11,9 @@ module Gitlab
def evaluate(variables = {})
text = @left.evaluate(variables)
regexp = @right.evaluate(variables)
+ return true unless regexp
- regexp.scan(text.to_s).none?
+ regexp.scan(text.to_s).empty?
end
def self.build(_value, behind, ahead)
diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb
index 7ec03d132c0..1066331062b 100644
--- a/lib/gitlab/ci/pipeline/seed/build.rb
+++ b/lib/gitlab/ci/pipeline/seed/build.rb
@@ -7,7 +7,7 @@ module Gitlab
class Build < Seed::Base
include Gitlab::Utils::StrongMemoize
- delegate :dig, to: :@attributes
+ delegate :dig, to: :@seed_attributes
# When the `ci_dag_limit_needs` is enabled it uses the lower limit
LOW_NEEDS_LIMIT = 5
@@ -15,14 +15,20 @@ module Gitlab
def initialize(pipeline, attributes, previous_stages)
@pipeline = pipeline
- @attributes = attributes
+ @seed_attributes = attributes
@previous_stages = previous_stages
@needs_attributes = dig(:needs_attributes)
+ @using_rules = attributes.key?(:rules)
+ @using_only = attributes.key?(:only)
+ @using_except = attributes.key?(:except)
+
@only = Gitlab::Ci::Build::Policy
.fabricate(attributes.delete(:only))
@except = Gitlab::Ci::Build::Policy
.fabricate(attributes.delete(:except))
+ @rules = Gitlab::Ci::Build::Rules
+ .new(attributes.delete(:rules))
end
def name
@@ -31,8 +37,13 @@ module Gitlab
def included?
strong_memoize(:inclusion) do
- all_of_only? &&
- none_of_except?
+ if @using_rules
+ included_by_rules?
+ elsif @using_only || @using_except
+ all_of_only? && none_of_except?
+ else
+ true
+ end
end
end
@@ -45,19 +56,13 @@ module Gitlab
end
def attributes
- @attributes.merge(
- pipeline: @pipeline,
- project: @pipeline.project,
- user: @pipeline.user,
- ref: @pipeline.ref,
- tag: @pipeline.tag,
- trigger_request: @pipeline.legacy_trigger,
- protected: @pipeline.protected_ref?
- )
+ @seed_attributes
+ .deep_merge(pipeline_attributes)
+ .deep_merge(rules_attributes)
end
def bridge?
- attributes_hash = @attributes.to_h
+ attributes_hash = @seed_attributes.to_h
attributes_hash.dig(:options, :trigger).present? ||
(attributes_hash.dig(:options, :bridge_needs).instance_of?(Hash) &&
attributes_hash.dig(:options, :bridge_needs, :pipeline).present?)
@@ -73,6 +78,18 @@ module Gitlab
end
end
+ def scoped_variables_hash
+ strong_memoize(:scoped_variables_hash) do
+ # This is a temporary piece of technical debt to allow us access
+ # to the CI variables to evaluate rules before we persist a Build
+ # with the result. We should refactor away the extra Build.new,
+ # but be able to get CI Variables directly from the Seed::Build.
+ ::Ci::Build.new(
+ @seed_attributes.merge(pipeline_attributes)
+ ).scoped_variables_hash
+ end
+ end
+
private
def all_of_only?
@@ -109,6 +126,28 @@ module Gitlab
HARD_NEEDS_LIMIT
end
end
+
+ def pipeline_attributes
+ {
+ pipeline: @pipeline,
+ project: @pipeline.project,
+ user: @pipeline.user,
+ ref: @pipeline.ref,
+ tag: @pipeline.tag,
+ trigger_request: @pipeline.legacy_trigger,
+ protected: @pipeline.protected_ref?
+ }
+ end
+
+ def included_by_rules?
+ rules_attributes[:when] != 'never'
+ end
+
+ def rules_attributes
+ strong_memoize(:rules_attributes) do
+ @using_rules ? @rules.evaluate(@pipeline, self).to_h.compact : {}
+ end
+ end
end
end
end
diff --git a/lib/gitlab/config/entry/validators.rb b/lib/gitlab/config/entry/validators.rb
index 0289e675c6b..374f929878e 100644
--- a/lib/gitlab/config/entry/validators.rb
+++ b/lib/gitlab/config/entry/validators.rb
@@ -20,8 +20,10 @@ module Gitlab
present_keys = value.try(:keys).to_a & options[:in]
if present_keys.any?
- record.errors.add(attribute, "contains disallowed keys: " +
- present_keys.join(', '))
+ message = options[:message] || "contains disallowed keys"
+ message += ": #{present_keys.join(', ')}"
+
+ record.errors.add(attribute, message)
end
end
end
@@ -65,6 +67,16 @@ module Gitlab
end
end
+ class ArrayOfHashesValidator < ActiveModel::EachValidator
+ include LegacyValidationHelpers
+
+ def validate_each(record, attribute, value)
+ unless value.is_a?(Array) && value.map { |hsh| hsh.is_a?(Hash) }.all?
+ record.errors.add(attribute, 'should be an array of hashes')
+ end
+ end
+ end
+
class ArrayOrStringValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless value.is_a?(Array) || value.is_a?(String)
@@ -231,6 +243,14 @@ module Gitlab
end
end
+ class ExpressionValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ unless value.is_a?(String) && ::Gitlab::Ci::Pipeline::Expression::Statement.new(value).valid?
+ record.errors.add(attribute, 'Invalid expression syntax')
+ end
+ end
+ end
+
class PortNamePresentAndUniqueValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return unless value.is_a?(Array)
diff --git a/lib/gitlab/daemon.rb b/lib/gitlab/daemon.rb
index 6d5fc4219fb..2f4ae010e74 100644
--- a/lib/gitlab/daemon.rb
+++ b/lib/gitlab/daemon.rb
@@ -46,7 +46,10 @@ module Gitlab
if thread
thread.wakeup if thread.alive?
- thread.join unless Thread.current == thread
+ begin
+ thread.join unless Thread.current == thread
+ rescue Exception # rubocop:disable Lint/RescueException
+ end
@thread = nil
end
end
diff --git a/lib/gitlab/email/hook/smime_signature_interceptor.rb b/lib/gitlab/email/hook/smime_signature_interceptor.rb
new file mode 100644
index 00000000000..e48041d9218
--- /dev/null
+++ b/lib/gitlab/email/hook/smime_signature_interceptor.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Email
+ module Hook
+ class SmimeSignatureInterceptor
+ # Sign emails with SMIME if enabled
+ class << self
+ def delivering_email(message)
+ signed_message = Gitlab::Email::Smime::Signer.sign(
+ cert: certificate.cert,
+ key: certificate.key,
+ data: message.encoded)
+ signed_email = Mail.new(signed_message)
+
+ overwrite_body(message, signed_email)
+ overwrite_headers(message, signed_email)
+ end
+
+ private
+
+ def certificate
+ @certificate ||= Gitlab::Email::Smime::Certificate.from_files(key_path, cert_path)
+ end
+
+ def key_path
+ Gitlab.config.gitlab.email_smime.key_file
+ end
+
+ def cert_path
+ Gitlab.config.gitlab.email_smime.cert_file
+ end
+
+ def overwrite_body(message, signed_email)
+ # since this is a multipart email, assignment to nil is important,
+ # otherwise Message#body will add a new mail part
+ message.body = nil
+ message.body = signed_email.body.encoded
+ end
+
+ def overwrite_headers(message, signed_email)
+ message.content_disposition = signed_email.content_disposition
+ message.content_transfer_encoding = signed_email.content_transfer_encoding
+ message.content_type = signed_email.content_type
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/email/smime/certificate.rb b/lib/gitlab/email/smime/certificate.rb
new file mode 100644
index 00000000000..b331c4ca19c
--- /dev/null
+++ b/lib/gitlab/email/smime/certificate.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Email
+ module Smime
+ class Certificate
+ include OpenSSL
+
+ attr_reader :key, :cert
+
+ def key_string
+ @key.to_s
+ end
+
+ def cert_string
+ @cert.to_pem
+ end
+
+ def self.from_strings(key_string, cert_string)
+ key = PKey::RSA.new(key_string)
+ cert = X509::Certificate.new(cert_string)
+ new(key, cert)
+ end
+
+ def self.from_files(key_path, cert_path)
+ from_strings(File.read(key_path), File.read(cert_path))
+ end
+
+ def initialize(key, cert)
+ @key = key
+ @cert = cert
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/email/smime/signer.rb b/lib/gitlab/email/smime/signer.rb
new file mode 100644
index 00000000000..2fa83014003
--- /dev/null
+++ b/lib/gitlab/email/smime/signer.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'openssl'
+
+module Gitlab
+ module Email
+ module Smime
+ # Tooling for signing and verifying data with SMIME
+ class Signer
+ include OpenSSL
+
+ def self.sign(cert:, key:, data:)
+ signed_data = PKCS7.sign(cert, key, data, nil, PKCS7::DETACHED)
+ PKCS7.write_smime(signed_data)
+ end
+
+ # return nil if data cannot be verified, otherwise the signed content data
+ def self.verify_signature(cert:, ca_cert: nil, signed_data:)
+ store = X509::Store.new
+ store.set_default_paths
+ store.add_cert(ca_cert) if ca_cert
+
+ signed_smime = PKCS7.read_smime(signed_data)
+ signed_smime if signed_smime.verify([cert], store)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/grape_logging/loggers/client_env_logger.rb b/lib/gitlab/grape_logging/loggers/client_env_logger.rb
new file mode 100644
index 00000000000..3acc6f6a2ef
--- /dev/null
+++ b/lib/gitlab/grape_logging/loggers/client_env_logger.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+# This is a fork of
+# https://github.com/aserafin/grape_logging/blob/master/lib/grape_logging/loggers/client_env.rb
+# to use remote_ip instead of ip.
+module Gitlab
+ module GrapeLogging
+ module Loggers
+ class ClientEnvLogger < ::GrapeLogging::Loggers::Base
+ def parameters(request, _)
+ { remote_ip: request.env["HTTP_X_FORWARDED_FOR"] || request.env["REMOTE_ADDR"], ua: request.env["HTTP_USER_AGENT"] }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sidekiq_logging/structured_logger.rb b/lib/gitlab/sidekiq_logging/structured_logger.rb
index 60782306ade..48b1524f9c7 100644
--- a/lib/gitlab/sidekiq_logging/structured_logger.rb
+++ b/lib/gitlab/sidekiq_logging/structured_logger.rb
@@ -8,16 +8,16 @@ module Gitlab
MAXIMUM_JOB_ARGUMENTS_LENGTH = 10.kilobytes
def call(job, queue)
- started_at = current_time
+ started_time = get_time
base_payload = parse_job(job)
- Sidekiq.logger.info log_job_start(started_at, base_payload)
+ Sidekiq.logger.info log_job_start(base_payload)
yield
- Sidekiq.logger.info log_job_done(job, started_at, base_payload)
+ Sidekiq.logger.info log_job_done(job, started_time, base_payload)
rescue => job_exception
- Sidekiq.logger.warn log_job_done(job, started_at, base_payload, job_exception)
+ Sidekiq.logger.warn log_job_done(job, started_time, base_payload, job_exception)
raise
end
@@ -32,7 +32,7 @@ module Gitlab
output_payload.merge!(job.slice(*::Gitlab::InstrumentationHelper::KEYS))
end
- def log_job_start(started_at, payload)
+ def log_job_start(payload)
payload['message'] = "#{base_message(payload)}: start"
payload['job_status'] = 'start'
@@ -45,11 +45,12 @@ module Gitlab
payload
end
- def log_job_done(job, started_at, payload, job_exception = nil)
+ def log_job_done(job, started_time, payload, job_exception = nil)
payload = payload.dup
add_instrumentation_keys!(job, payload)
- payload['duration'] = elapsed(started_at)
- payload['completed_at'] = Time.now.utc
+
+ elapsed_time = elapsed(started_time)
+ add_time_keys!(elapsed_time, payload)
message = base_message(payload)
@@ -69,6 +70,14 @@ module Gitlab
payload
end
+ def add_time_keys!(time, payload)
+ payload['duration'] = time[:duration].round(3)
+ payload['system_s'] = time[:stime].round(3)
+ payload['user_s'] = time[:utime].round(3)
+ payload['child_s'] = time[:ctime].round(3) if time[:ctime] > 0
+ payload['completed_at'] = Time.now.utc
+ end
+
def parse_job(job)
job = job.dup
@@ -93,8 +102,25 @@ module Gitlab
(Time.now.utc - start).to_f.round(3)
end
- def elapsed(start)
- (current_time - start).round(3)
+ def elapsed(t0)
+ t1 = get_time
+ {
+ duration: t1[:now] - t0[:now],
+ stime: t1[:times][:stime] - t0[:times][:stime],
+ utime: t1[:times][:utime] - t0[:times][:utime],
+ ctime: ctime(t1[:times]) - ctime(t0[:times])
+ }
+ end
+
+ def get_time
+ {
+ now: current_time,
+ times: Process.times
+ }
+ end
+
+ def ctime(times)
+ times[:cstime] + times[:cutime]
end
def current_time
diff --git a/lib/gitlab/sidekiq_middleware/monitor.rb b/lib/gitlab/sidekiq_middleware/monitor.rb
new file mode 100644
index 00000000000..53a6132edac
--- /dev/null
+++ b/lib/gitlab/sidekiq_middleware/monitor.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SidekiqMiddleware
+ class Monitor
+ def call(worker, job, queue)
+ Gitlab::SidekiqMonitor.instance.within_job(job['jid'], queue) do
+ yield
+ end
+ rescue Gitlab::SidekiqMonitor::CancelledError
+ # push job to DeadSet
+ payload = ::Sidekiq.dump_json(job)
+ ::Sidekiq::DeadSet.new.kill(payload, notify_failure: false)
+
+ # ignore retries
+ raise ::Sidekiq::JobRetry::Skip
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sidekiq_monitor.rb b/lib/gitlab/sidekiq_monitor.rb
new file mode 100644
index 00000000000..9842f1f53f7
--- /dev/null
+++ b/lib/gitlab/sidekiq_monitor.rb
@@ -0,0 +1,182 @@
+# frozen_string_literal: true
+
+module Gitlab
+ class SidekiqMonitor < Daemon
+ include ::Gitlab::Utils::StrongMemoize
+
+ NOTIFICATION_CHANNEL = 'sidekiq:cancel:notifications'.freeze
+ CANCEL_DEADLINE = 24.hours.seconds
+ RECONNECT_TIME = 3.seconds
+
+ # We use exception derived from `Exception`
+ # to consider this as an very low-level exception
+ # that should not be caught by application
+ CancelledError = Class.new(Exception) # rubocop:disable Lint/InheritException
+
+ attr_reader :jobs_thread
+ attr_reader :jobs_mutex
+
+ def initialize
+ super
+
+ @jobs_thread = {}
+ @jobs_mutex = Mutex.new
+ end
+
+ def within_job(jid, queue)
+ jobs_mutex.synchronize do
+ jobs_thread[jid] = Thread.current
+ end
+
+ if cancelled?(jid)
+ Sidekiq.logger.warn(
+ class: self.class.to_s,
+ action: 'run',
+ queue: queue,
+ jid: jid,
+ canceled: true
+ )
+ raise CancelledError
+ end
+
+ yield
+ ensure
+ jobs_mutex.synchronize do
+ jobs_thread.delete(jid)
+ end
+ end
+
+ def self.cancel_job(jid)
+ payload = {
+ action: 'cancel',
+ jid: jid
+ }.to_json
+
+ ::Gitlab::Redis::SharedState.with do |redis|
+ redis.setex(cancel_job_key(jid), CANCEL_DEADLINE, 1)
+ redis.publish(NOTIFICATION_CHANNEL, payload)
+ end
+ end
+
+ private
+
+ def start_working
+ Sidekiq.logger.info(
+ class: self.class.to_s,
+ action: 'start',
+ message: 'Starting Monitor Daemon'
+ )
+
+ while enabled?
+ process_messages
+ sleep(RECONNECT_TIME)
+ end
+
+ ensure
+ Sidekiq.logger.warn(
+ class: self.class.to_s,
+ action: 'stop',
+ message: 'Stopping Monitor Daemon'
+ )
+ end
+
+ def stop_working
+ thread.raise(Interrupt) if thread.alive?
+ end
+
+ def process_messages
+ ::Gitlab::Redis::SharedState.with do |redis|
+ redis.subscribe(NOTIFICATION_CHANNEL) do |on|
+ on.message do |channel, message|
+ process_message(message)
+ end
+ end
+ end
+ rescue Exception => e # rubocop:disable Lint/RescueException
+ Sidekiq.logger.warn(
+ class: self.class.to_s,
+ action: 'exception',
+ message: e.message
+ )
+
+ # we re-raise system exceptions
+ raise e unless e.is_a?(StandardError)
+ end
+
+ def process_message(message)
+ Sidekiq.logger.info(
+ class: self.class.to_s,
+ channel: NOTIFICATION_CHANNEL,
+ message: 'Received payload on channel',
+ payload: message
+ )
+
+ message = safe_parse(message)
+ return unless message
+
+ case message['action']
+ when 'cancel'
+ process_job_cancel(message['jid'])
+ else
+ # unknown message
+ end
+ end
+
+ def safe_parse(message)
+ JSON.parse(message)
+ rescue JSON::ParserError
+ end
+
+ def process_job_cancel(jid)
+ return unless jid
+
+ # try to find thread without lock
+ return unless find_thread_unsafe(jid)
+
+ Thread.new do
+ # try to find a thread, but with guaranteed
+ # that handle for thread corresponds to actually
+ # running job
+ find_thread_with_lock(jid) do |thread|
+ Sidekiq.logger.warn(
+ class: self.class.to_s,
+ action: 'cancel',
+ message: 'Canceling thread with CancelledError',
+ jid: jid,
+ thread_id: thread.object_id
+ )
+
+ thread&.raise(CancelledError)
+ end
+ end
+ end
+
+ # This method needs to be thread-safe
+ # This is why it passes thread in block,
+ # to ensure that we do process this thread
+ def find_thread_unsafe(jid)
+ jobs_thread[jid]
+ end
+
+ def find_thread_with_lock(jid)
+ # don't try to lock if we cannot find the thread
+ return unless find_thread_unsafe(jid)
+
+ jobs_mutex.synchronize do
+ find_thread_unsafe(jid).tap do |thread|
+ yield(thread) if thread
+ end
+ end
+ end
+
+ def cancelled?(jid)
+ ::Gitlab::Redis::SharedState.with do |redis|
+ redis.exists(self.class.cancel_job_key(jid))
+ end
+ end
+
+ def self.cancel_job_key(jid)
+ "sidekiq:cancel:#{jid}"
+ end
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 26e6cb524bd..0568bdbdfca 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -128,9 +128,6 @@ msgstr[1] ""
msgid "%{actionText} & %{openOrClose} %{noteable}"
msgstr ""
-msgid "%{canMergeCount}/%{assigneesCount} can merge"
-msgstr ""
-
msgid "%{commit_author_link} authored %{commit_timeago}"
msgstr ""
@@ -202,6 +199,9 @@ msgstr ""
msgid "%{lock_path} is locked by GitLab User %{lock_user_id}"
msgstr ""
+msgid "%{mergeLength}/%{usersLength} can merge"
+msgstr ""
+
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
@@ -279,6 +279,9 @@ msgstr ""
msgid "%{usage_ping_link_start}Learn more%{usage_ping_link_end} about what information is shared with GitLab Inc."
msgstr ""
+msgid "%{userName} (cannot merge)"
+msgstr ""
+
msgid "%{userName}'s avatar"
msgstr ""
@@ -306,6 +309,9 @@ msgstr ""
msgid "(external source)"
msgstr ""
+msgid "+ %{amount} more"
+msgstr ""
+
msgid "+ %{count} more"
msgstr ""
@@ -6496,7 +6502,7 @@ msgstr ""
msgid "Latest changes"
msgstr ""
-msgid "Latest pipeline for this branch"
+msgid "Latest pipeline for the most recent commit on this branch"
msgstr ""
msgid "Lead"
@@ -7439,9 +7445,6 @@ msgstr ""
msgid "No milestones to show"
msgstr ""
-msgid "No one can merge"
-msgstr ""
-
msgid "No other labels with such name or description"
msgstr ""
@@ -12888,15 +12891,9 @@ msgstr ""
msgid "WikiMarkdownTip|To link to a (new) page, simply type %{link_example}"
msgstr ""
-msgid "WikiNewPagePlaceholder|how-to-setup"
-msgstr ""
-
msgid "WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories."
msgstr ""
-msgid "WikiNewPageTitle|New Wiki Page"
-msgstr ""
-
msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
msgstr ""
@@ -12912,19 +12909,16 @@ msgstr ""
msgid "WikiPageConflictMessage|the page"
msgstr ""
-msgid "WikiPageCreate|Create %{page_title}"
-msgstr ""
-
-msgid "WikiPageEdit|Update %{page_title}"
+msgid "WikiPageCreate|Create %{pageTitle}"
msgstr ""
-msgid "WikiPage|Page slug"
+msgid "WikiPageEdit|Update %{pageTitle}"
msgstr ""
msgid "WikiPage|Write your content or drag files here…"
msgstr ""
-msgid "Wiki|Create Page"
+msgid "Wiki|Create New Page"
msgstr ""
msgid "Wiki|Create page"
@@ -12945,6 +12939,9 @@ msgstr ""
msgid "Wiki|Page history"
msgstr ""
+msgid "Wiki|Page title"
+msgstr ""
+
msgid "Wiki|Page version"
msgstr ""
@@ -13401,18 +13398,15 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
-msgid "a Zoom call was added to this issue"
-msgstr ""
-
-msgid "a Zoom call was removed from this issue"
-msgstr ""
-
msgid "a deleted user"
msgstr ""
msgid "added %{created_at_timeago}"
msgstr ""
+msgid "added a Zoom call to this issue"
+msgstr ""
+
msgid "ago"
msgstr ""
@@ -13452,6 +13446,9 @@ msgstr ""
msgid "cannot include leading slash or directory traversal."
msgstr ""
+msgid "cannot merge"
+msgstr ""
+
msgid "comment"
msgstr ""
@@ -13879,6 +13876,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no one can merge"
+msgstr ""
+
msgid "none"
msgstr ""
@@ -13949,6 +13949,9 @@ msgstr ""
msgid "remove due date"
msgstr ""
+msgid "removed a Zoom call from this issue"
+msgstr ""
+
msgid "rendered diff"
msgstr ""
diff --git a/package.json b/package.json
index 2b9a00d1cbd..df5f758fa20 100644
--- a/package.json
+++ b/package.json
@@ -30,16 +30,16 @@
"webpack-vrt": "NODE_OPTIONS=\"--max-old-space-size=3584\" NODE_ENV=production webpack --config config/webpack.config.review_toolbar.js"
},
"dependencies": {
- "@babel/core": "^7.4.4",
- "@babel/plugin-proposal-class-properties": "^7.4.4",
+ "@babel/core": "^7.5.5",
+ "@babel/plugin-proposal-class-properties": "^7.5.5",
"@babel/plugin-proposal-json-strings": "^7.2.0",
"@babel/plugin-proposal-private-methods": "^7.4.4",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-syntax-import-meta": "^7.2.0",
- "@babel/preset-env": "^7.4.4",
+ "@babel/preset-env": "^7.5.5",
"@gitlab/csslab": "^1.9.0",
"@gitlab/svgs": "^1.68.0",
- "@gitlab/ui": "5.15.0",
+ "@gitlab/ui": "5.18.0",
"apollo-cache-inmemory": "^1.5.1",
"apollo-client": "^2.5.1",
"apollo-link": "^1.2.11",
@@ -145,7 +145,7 @@
"xterm": "^3.5.0"
},
"devDependencies": {
- "@babel/plugin-transform-modules-commonjs": "^7.2.0",
+ "@babel/plugin-transform-modules-commonjs": "^7.5.0",
"@gitlab/eslint-config": "^1.6.0",
"@gitlab/eslint-plugin-i18n": "^1.1.0",
"@gitlab/eslint-plugin-vue-i18n": "^1.2.0",
diff --git a/qa/README.md b/qa/README.md
index 97555f8d0c2..27e11c04b3e 100644
--- a/qa/README.md
+++ b/qa/README.md
@@ -30,7 +30,7 @@ and corresponding views / partials / selectors in CE / EE.
Whenever `qa:selectors` job fails in your merge request, you are supposed to
fix [page objects](../doc/development/testing_guide/end_to_end/page_objects.md). You should also trigger end-to-end tests
-using `package-and-qa` manual action, to test if everything works fine.
+using `package-and-qa-manual` manual action, to test if everything works fine.
## How can I use it?
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb
index f51c16f472c..1c1f552e224 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb
@@ -19,7 +19,7 @@ module QA
page.add_member(user.username)
end
- expect(page).to have_content(/#{user.name} (. )?@#{user.username} Given access/)
+ expect(page).to have_content(/@#{user.username}(\n| )?Given access/)
end
end
end
diff --git a/spec/config/smime_signature_settings_spec.rb b/spec/config/smime_signature_settings_spec.rb
new file mode 100644
index 00000000000..4f0c227d866
--- /dev/null
+++ b/spec/config/smime_signature_settings_spec.rb
@@ -0,0 +1,56 @@
+require 'fast_spec_helper'
+
+describe SmimeSignatureSettings do
+ describe '.parse' do
+ let(:default_smime_key) { Rails.root.join('.gitlab_smime_key') }
+ let(:default_smime_cert) { Rails.root.join('.gitlab_smime_cert') }
+
+ it 'sets correct default values to disabled' do
+ parsed_settings = described_class.parse(nil)
+
+ expect(parsed_settings['enabled']).to be(false)
+ expect(parsed_settings['key_file']).to eq(default_smime_key)
+ expect(parsed_settings['cert_file']).to eq(default_smime_cert)
+ end
+
+ context 'when providing custom values' do
+ it 'sets correct default values to disabled' do
+ custom_settings = Settingslogic.new({})
+
+ parsed_settings = described_class.parse(custom_settings)
+
+ expect(parsed_settings['enabled']).to be(false)
+ expect(parsed_settings['key_file']).to eq(default_smime_key)
+ expect(parsed_settings['cert_file']).to eq(default_smime_cert)
+ end
+
+ it 'enables smime with default key and cert' do
+ custom_settings = Settingslogic.new({
+ 'enabled' => true
+ })
+
+ parsed_settings = described_class.parse(custom_settings)
+
+ expect(parsed_settings['enabled']).to be(true)
+ expect(parsed_settings['key_file']).to eq(default_smime_key)
+ expect(parsed_settings['cert_file']).to eq(default_smime_cert)
+ end
+
+ it 'enables smime with custom key and cert' do
+ custom_key = '/custom/key'
+ custom_cert = '/custom/cert'
+ custom_settings = Settingslogic.new({
+ 'enabled' => true,
+ 'key_file' => custom_key,
+ 'cert_file' => custom_cert
+ })
+
+ parsed_settings = described_class.parse(custom_settings)
+
+ expect(parsed_settings['enabled']).to be(true)
+ expect(parsed_settings['key_file']).to eq(custom_key)
+ expect(parsed_settings['cert_file']).to eq(custom_cert)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/concerns/issuable_collections_spec.rb b/spec/controllers/concerns/issuable_collections_spec.rb
index f210537aad5..7bdf5c49425 100644
--- a/spec/controllers/concerns/issuable_collections_spec.rb
+++ b/spec/controllers/concerns/issuable_collections_spec.rb
@@ -24,78 +24,6 @@ describe IssuableCollections do
controller
end
- describe '#set_sort_order_from_user_preference' do
- describe 'when sort param given' do
- let(:params) { { sort: 'updated_desc' } }
-
- context 'when issuable_sorting_field is defined' do
- before do
- controller.class.define_method(:issuable_sorting_field) { :issues_sort}
- end
-
- it 'sets user_preference with the right value' do
- controller.send(:set_sort_order_from_user_preference)
-
- expect(user.user_preference.reload.issues_sort).to eq('updated_desc')
- end
- end
-
- context 'when no issuable_sorting_field is defined on the controller' do
- it 'does not touch user_preference' do
- allow(user).to receive(:user_preference)
-
- controller.send(:set_sort_order_from_user_preference)
-
- expect(user).not_to have_received(:user_preference)
- end
- end
- end
-
- context 'when a user sorting preference exists' do
- let(:params) { {} }
-
- before do
- controller.class.define_method(:issuable_sorting_field) { :issues_sort }
- end
-
- it 'returns the set preference' do
- user.user_preference.update(issues_sort: 'updated_asc')
-
- sort_preference = controller.send(:set_sort_order_from_user_preference)
-
- expect(sort_preference).to eq('updated_asc')
- end
- end
- end
-
- describe '#set_set_order_from_cookie' do
- describe 'when sort param given' do
- let(:cookies) { {} }
- let(:params) { { sort: 'downvotes_asc' } }
-
- it 'sets the cookie with the right values and flags' do
- allow(controller).to receive(:cookies).and_return(cookies)
-
- controller.send(:set_sort_order_from_cookie)
-
- expect(cookies['issue_sort']).to eq({ value: 'popularity', secure: false, httponly: false })
- end
- end
-
- describe 'when cookie exists' do
- let(:cookies) { { 'issue_sort' => 'id_asc' } }
- let(:params) { {} }
-
- it 'sets the cookie with the right values and flags' do
- allow(controller).to receive(:cookies).and_return(cookies)
-
- controller.send(:set_sort_order_from_cookie)
-
- expect(cookies['issue_sort']).to eq({ value: 'created_asc', secure: false, httponly: false })
- end
- end
- end
-
describe '#page_count_for_relation' do
let(:params) { { state: 'opened' } }
diff --git a/spec/controllers/concerns/sorting_preference_spec.rb b/spec/controllers/concerns/sorting_preference_spec.rb
new file mode 100644
index 00000000000..a36124c6776
--- /dev/null
+++ b/spec/controllers/concerns/sorting_preference_spec.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe SortingPreference do
+ let(:user) { create(:user) }
+
+ let(:controller_class) do
+ Class.new do
+ def self.helper_method(name); end
+
+ include SortingPreference
+ include SortingHelper
+ end
+ end
+
+ let(:controller) { controller_class.new }
+
+ before do
+ allow(controller).to receive(:params).and_return(ActionController::Parameters.new(params))
+ allow(controller).to receive(:current_user).and_return(user)
+ allow(controller).to receive(:legacy_sort_cookie_name).and_return('issuable_sort')
+ allow(controller).to receive(:sorting_field).and_return(:issues_sort)
+ end
+
+ describe '#set_sort_order_from_user_preference' do
+ subject { controller.send(:set_sort_order_from_user_preference) }
+
+ context 'when sort param given' do
+ let(:params) { { sort: 'updated_desc' } }
+
+ context 'when sorting_field is defined' do
+ it 'sets user_preference with the right value' do
+ is_expected.to eq('updated_desc')
+ end
+ end
+
+ context 'when no sorting_field is defined on the controller' do
+ before do
+ allow(controller).to receive(:sorting_field).and_return(nil)
+ end
+
+ it 'does not touch user_preference' do
+ expect(user).not_to receive(:user_preference)
+
+ subject
+ end
+ end
+ end
+
+ context 'when a user sorting preference exists' do
+ let(:params) { {} }
+
+ before do
+ user.user_preference.update!(issues_sort: 'updated_asc')
+ end
+
+ it 'returns the set preference' do
+ is_expected.to eq('updated_asc')
+ end
+ end
+ end
+
+ describe '#set_set_order_from_cookie' do
+ subject { controller.send(:set_sort_order_from_cookie) }
+
+ before do
+ allow(controller).to receive(:cookies).and_return(cookies)
+ end
+
+ context 'when sort param given' do
+ let(:cookies) { {} }
+ let(:params) { { sort: 'downvotes_asc' } }
+
+ it 'sets the cookie with the right values and flags' do
+ subject
+
+ expect(cookies['issue_sort']).to eq(value: 'popularity', secure: false, httponly: false)
+ end
+ end
+
+ context 'when cookie exists' do
+ let(:cookies) { { 'issue_sort' => 'id_asc' } }
+ let(:params) { {} }
+
+ it 'sets the cookie with the right values and flags' do
+ subject
+
+ expect(cookies['issue_sort']).to eq(value: 'created_asc', secure: false, httponly: false)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/dashboard/projects_controller_spec.rb b/spec/controllers/dashboard/projects_controller_spec.rb
index 6591901a9dc..8b95c9f2496 100644
--- a/spec/controllers/dashboard/projects_controller_spec.rb
+++ b/spec/controllers/dashboard/projects_controller_spec.rb
@@ -40,6 +40,14 @@ describe Dashboard::ProjectsController do
expect(assigns(:projects)).to eq([project, project2])
end
+
+ context 'project sorting' do
+ let(:project) { create(:project) }
+
+ it_behaves_like 'set sort order from user preference' do
+ let(:sorting_param) { 'created_asc' }
+ end
+ end
end
end
diff --git a/spec/controllers/explore/projects_controller_spec.rb b/spec/controllers/explore/projects_controller_spec.rb
index 463586ee422..6752d2b8ebd 100644
--- a/spec/controllers/explore/projects_controller_spec.rb
+++ b/spec/controllers/explore/projects_controller_spec.rb
@@ -3,56 +3,91 @@
require 'spec_helper'
describe Explore::ProjectsController do
- describe 'GET #index.json' do
- render_views
+ shared_examples 'explore projects' do
+ describe 'GET #index.json' do
+ render_views
- before do
- get :index, format: :json
+ before do
+ get :index, format: :json
+ end
+
+ it { is_expected.to respond_with(:success) }
end
- it { is_expected.to respond_with(:success) }
- end
+ describe 'GET #trending.json' do
+ render_views
- describe 'GET #trending.json' do
- render_views
+ before do
+ get :trending, format: :json
+ end
- before do
- get :trending, format: :json
+ it { is_expected.to respond_with(:success) }
+ end
+
+ describe 'GET #starred.json' do
+ render_views
+
+ before do
+ get :starred, format: :json
+ end
+
+ it { is_expected.to respond_with(:success) }
end
- it { is_expected.to respond_with(:success) }
+ describe 'GET #trending' do
+ context 'sorting by update date' do
+ let(:project1) { create(:project, :public, updated_at: 3.days.ago) }
+ let(:project2) { create(:project, :public, updated_at: 1.day.ago) }
+
+ before do
+ create(:trending_project, project: project1)
+ create(:trending_project, project: project2)
+ end
+
+ it 'sorts by last updated' do
+ get :trending, params: { sort: 'updated_desc' }
+
+ expect(assigns(:projects)).to eq [project2, project1]
+ end
+
+ it 'sorts by oldest updated' do
+ get :trending, params: { sort: 'updated_asc' }
+
+ expect(assigns(:projects)).to eq [project1, project2]
+ end
+ end
+ end
end
- describe 'GET #starred.json' do
- render_views
+ context 'when user is signed in' do
+ let(:user) { create(:user) }
before do
- get :starred, format: :json
+ sign_in(user)
end
- it { is_expected.to respond_with(:success) }
- end
+ include_examples 'explore projects'
- describe 'GET #trending' do
- context 'sorting by update date' do
- let(:project1) { create(:project, :public, updated_at: 3.days.ago) }
- let(:project2) { create(:project, :public, updated_at: 1.day.ago) }
+ context 'user preference sorting' do
+ let(:project) { create(:project) }
- before do
- create(:trending_project, project: project1)
- create(:trending_project, project: project2)
+ it_behaves_like 'set sort order from user preference' do
+ let(:sorting_param) { 'created_asc' }
end
+ end
+ end
- it 'sorts by last updated' do
- get :trending, params: { sort: 'updated_desc' }
+ context 'when user is not signed in' do
+ include_examples 'explore projects'
- expect(assigns(:projects)).to eq [project2, project1]
- end
+ context 'user preference sorting' do
+ let(:project) { create(:project) }
+ let(:sorting_param) { 'created_asc' }
- it 'sorts by oldest updated' do
- get :trending, params: { sort: 'updated_asc' }
+ it 'does not set sort order from user preference' do
+ expect_any_instance_of(UserPreference).not_to receive(:update)
- expect(assigns(:projects)).to eq [project1, project2]
+ get :index, params: { sort: sorting_param }
end
end
end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index fab47aa4701..187c7864ad7 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -1104,18 +1104,39 @@ describe Projects::IssuesController do
project.add_developer(user)
end
+ subject do
+ post(:toggle_award_emoji, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: issue.iid,
+ name: emoji_name
+ })
+ end
+ let(:emoji_name) { 'thumbsup' }
+
it "toggles the award emoji" do
expect do
- post(:toggle_award_emoji, params: {
- namespace_id: project.namespace,
- project_id: project,
- id: issue.iid,
- name: "thumbsup"
- })
+ subject
end.to change { issue.award_emoji.count }.by(1)
expect(response).to have_gitlab_http_status(200)
end
+
+ it "removes the already awarded emoji" do
+ create(:award_emoji, awardable: issue, name: emoji_name, user: user)
+
+ expect { subject }.to change { AwardEmoji.count }.by(-1)
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'marks Todos on the Issue as done' do
+ todo = create(:todo, target: issue, project: project, user: user)
+
+ subject
+
+ expect(todo.reload).to be_done
+ end
end
describe 'POST create_merge_request' do
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index 9ab565dc2e8..4500c412521 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -543,23 +543,32 @@ describe Projects::NotesController do
project.add_developer(user)
end
+ subject { post(:toggle_award_emoji, params: request_params.merge(name: emoji_name)) }
+ let(:emoji_name) { 'thumbsup' }
+
it "toggles the award emoji" do
expect do
- post(:toggle_award_emoji, params: request_params.merge(name: "thumbsup"))
+ subject
end.to change { note.award_emoji.count }.by(1)
expect(response).to have_gitlab_http_status(200)
end
it "removes the already awarded emoji" do
- post(:toggle_award_emoji, params: request_params.merge(name: "thumbsup"))
+ create(:award_emoji, awardable: note, name: emoji_name, user: user)
- expect do
- post(:toggle_award_emoji, params: request_params.merge(name: "thumbsup"))
- end.to change { AwardEmoji.count }.by(-1)
+ expect { subject }.to change { AwardEmoji.count }.by(-1)
expect(response).to have_gitlab_http_status(200)
end
+
+ it 'marks Todos on the Noteable as done' do
+ todo = create(:todo, target: note.noteable, project: project, user: user)
+
+ subject
+
+ expect(todo.reload).to be_done
+ end
end
describe "resolving and unresolving" do
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index 089d06e11eb..212d8b15252 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -397,4 +397,69 @@ describe Projects::PipelinesController do
end
end
end
+
+ describe 'GET latest' do
+ let(:branch_main) { project.repository.branches[0] }
+ let(:branch_secondary) { project.repository.branches[1] }
+
+ let!(:pipeline_master) do
+ create(:ci_pipeline,
+ ref: branch_main.name,
+ sha: branch_main.target,
+ project: project)
+ end
+
+ let!(:pipeline_secondary) do
+ create(:ci_pipeline,
+ ref: branch_secondary.name,
+ sha: branch_secondary.target,
+ project: project)
+ end
+
+ before do
+ project.change_head(branch_main.name)
+ project.reload_default_branch
+ end
+
+ context 'no ref provided' do
+ it 'shows latest pipeline for the default project branch' do
+ get :show, params: { namespace_id: project.namespace, project_id: project, latest: true, ref: nil }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(assigns(:pipeline)).to have_attributes(id: pipeline_master.id)
+ end
+ end
+
+ context 'ref provided' do
+ before do
+ create(:ci_pipeline, ref: 'master', project: project)
+ end
+
+ it 'shows the latest pipeline for the provided ref' do
+ get :show, params: { namespace_id: project.namespace, project_id: project, latest: true, ref: branch_secondary.name }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(assigns(:pipeline)).to have_attributes(id: pipeline_secondary.id)
+ end
+
+ context 'newer pipeline exists for older sha' do
+ before do
+ create(:ci_pipeline, ref: branch_secondary.name, sha: project.commit(branch_secondary.name).parent, project: project)
+ end
+
+ it 'shows the provided ref with the last sha/pipeline combo' do
+ get :show, params: { namespace_id: project.namespace, project_id: project, latest: true, ref: branch_secondary.name }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(assigns(:pipeline)).to have_attributes(id: pipeline_secondary.id)
+ end
+ end
+ end
+
+ it 'renders a 404 if no pipeline is found for the ref' do
+ get :show, params: { namespace_id: project.namespace, project_id: project, ref: 'no-branch' }
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
end
diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb
index b958f419a19..8b43d1264b2 100644
--- a/spec/controllers/projects/raw_controller_spec.rb
+++ b/spec/controllers/projects/raw_controller_spec.rb
@@ -67,9 +67,9 @@ describe Projects::RawController do
attributes = {
message: 'Action_Rate_Limiter_Request',
env: :raw_blob_request_limit,
- ip: '0.0.0.0',
+ remote_ip: '0.0.0.0',
request_method: 'GET',
- fullpath: "/#{project.full_path}/raw/#{file_path}"
+ path: "/#{project.full_path}/raw/#{file_path}"
}
expect(Gitlab::AuthLogger).to receive(:error).with(attributes).once
diff --git a/spec/controllers/projects/wikis_controller_spec.rb b/spec/controllers/projects/wikis_controller_spec.rb
index fbca1d5740f..6fea6bca4f2 100644
--- a/spec/controllers/projects/wikis_controller_spec.rb
+++ b/spec/controllers/projects/wikis_controller_spec.rb
@@ -3,11 +3,11 @@
require 'spec_helper'
describe Projects::WikisController do
- let(:project) { create(:project, :public, :repository) }
- let(:user) { project.owner }
+ set(:project) { create(:project, :public, :repository) }
+ set(:user) { project.owner }
let(:project_wiki) { ProjectWiki.new(project, user) }
let(:wiki) { project_wiki.wiki }
- let(:wiki_title) { 'page-title-test' }
+ let(:wiki_title) { 'page title test' }
before do
create_page(wiki_title, 'hello world')
@@ -19,6 +19,21 @@ describe Projects::WikisController do
destroy_page(wiki_title)
end
+ describe 'GET #new' do
+ subject { get :new, params: { namespace_id: project.namespace, project_id: project } }
+
+ it 'redirects to #show and appends a `random_title` param' do
+ subject
+
+ expect(response).to have_http_status(302)
+ expect(Rails.application.routes.recognize_path(response.redirect_url)).to include(
+ controller: 'projects/wikis',
+ action: 'show'
+ )
+ expect(response.redirect_url).to match(/\?random_title=true\Z/)
+ end
+ end
+
describe 'GET #pages' do
subject { get :pages, params: { namespace_id: project.namespace, project_id: project, id: wiki_title } }
@@ -75,40 +90,62 @@ describe Projects::WikisController do
describe 'GET #show' do
render_views
- subject { get :show, params: { namespace_id: project.namespace, project_id: project, id: wiki_title } }
+ let(:random_title) { nil }
- it 'limits the retrieved pages for the sidebar' do
- expect(controller).to receive(:load_wiki).and_return(project_wiki)
+ subject { get :show, params: { namespace_id: project.namespace, project_id: project, id: id, random_title: random_title } }
- # empty? call
- expect(project_wiki).to receive(:list_pages).with(limit: 1).and_call_original
- # Sidebar entries
- expect(project_wiki).to receive(:list_pages).with(limit: 15).and_call_original
+ context 'when page exists' do
+ let(:id) { wiki_title }
- subject
+ it 'limits the retrieved pages for the sidebar' do
+ expect(controller).to receive(:load_wiki).and_return(project_wiki)
+ expect(project_wiki).to receive(:list_pages).with(limit: 15).and_call_original
+
+ subject
+
+ expect(response).to have_http_status(:ok)
+ expect(assigns(:page).title).to eq(wiki_title)
+ end
+
+ context 'when page content encoding is invalid' do
+ it 'sets flash error' do
+ allow(controller).to receive(:valid_encoding?).and_return(false)
- expect(response).to have_http_status(:ok)
- expect(response.body).to include(wiki_title)
+ subject
+
+ expect(response).to have_http_status(:ok)
+ expect(flash[:notice]).to eq('The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository.')
+ end
+ end
end
- context 'when page content encoding is invalid' do
- it 'sets flash error' do
- allow(controller).to receive(:valid_encoding?).and_return(false)
+ context 'when the page does not exist' do
+ let(:id) { 'does not exist' }
+ before do
subject
+ end
- expect(response).to have_http_status(:ok)
- expect(flash[:notice]).to eq 'The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository.'
+ it 'builds a new wiki page with the id as the title' do
+ expect(assigns(:page).title).to eq(id)
+ end
+
+ context 'when a random_title param is present' do
+ let(:random_title) { true }
+
+ it 'builds a new wiki page with no title' do
+ expect(assigns(:page).title).to be_empty
+ end
end
end
context 'when page is a file' do
include WikiHelpers
- let(:path) { upload_file_to_wiki(project, user, file_name) }
+ let(:id) { upload_file_to_wiki(project, user, file_name) }
before do
- get :show, params: { namespace_id: project.namespace, project_id: project, id: path }
+ subject
end
context 'when file is an image' do
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index fed4fc810f2..35487682462 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -129,9 +129,9 @@ describe RegistrationsController do
{
message: auth_log_message,
env: :invisible_captcha_signup_bot_detected,
- ip: '0.0.0.0',
+ remote_ip: '0.0.0.0',
request_method: 'POST',
- fullpath: '/users'
+ path: '/users'
}
end
diff --git a/spec/controllers/snippets/notes_controller_spec.rb b/spec/controllers/snippets/notes_controller_spec.rb
index 652533ac49f..fd4b95ce226 100644
--- a/spec/controllers/snippets/notes_controller_spec.rb
+++ b/spec/controllers/snippets/notes_controller_spec.rb
@@ -288,11 +288,13 @@ describe Snippets::NotesController do
describe 'POST toggle_award_emoji' do
let(:note) { create(:note_on_personal_snippet, noteable: public_snippet) }
+ let(:emoji_name) { 'thumbsup'}
+
before do
sign_in(user)
end
- subject { post(:toggle_award_emoji, params: { snippet_id: public_snippet, id: note.id, name: "thumbsup" }) }
+ subject { post(:toggle_award_emoji, params: { snippet_id: public_snippet, id: note.id, name: emoji_name }) }
it "toggles the award emoji" do
expect { subject }.to change { note.award_emoji.count }.by(1)
@@ -301,7 +303,7 @@ describe Snippets::NotesController do
end
it "removes the already awarded emoji when it exists" do
- note.toggle_award_emoji('thumbsup', user) # create award emoji before
+ create(:award_emoji, awardable: note, name: emoji_name, user: user)
expect { subject }.to change { AwardEmoji.count }.by(-1)
diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb
index 7b511c4d3d5..5c6b04a7141 100644
--- a/spec/features/projects/wiki/markdown_preview_spec.rb
+++ b/spec/features/projects/wiki/markdown_preview_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe 'Projects > Wiki > User previews markdown changes', :js do
- let(:user) { create(:user) }
+ set(:user) { create(:user) }
let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
let(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'home', content: '[some link](other-page)' }) }
let(:wiki_content) do
@@ -20,23 +20,12 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
project.add_maintainer(user)
sign_in(user)
-
- visit project_wiki_path(project, wiki_page)
end
context "while creating a new wiki page" do
context "when there are no spaces or hyphens in the page name" do
it "rewrites relative links as expected" do
- find('.add-new-wiki').click
- page.within '#modal-new-wiki' do
- fill_in :new_wiki_path, with: 'a/b/c/d'
- click_button 'Create page'
- end
-
- page.within '.wiki-form' do
- fill_in :wiki_content, with: wiki_content
- click_on "Preview"
- end
+ create_wiki_page('a/b/c/d', content: wiki_content)
expect(page).to have_content("regular link")
@@ -50,16 +39,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
context "when there are spaces in the page name" do
it "rewrites relative links as expected" do
- click_link 'New page'
- page.within '#modal-new-wiki' do
- fill_in :new_wiki_path, with: 'a page/b page/c page/d page'
- click_button 'Create page'
- end
-
- page.within '.wiki-form' do
- fill_in :wiki_content, with: wiki_content
- click_on "Preview"
- end
+ create_wiki_page('a page/b page/c page/d page', content: wiki_content)
expect(page).to have_content("regular link")
@@ -73,16 +53,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
context "when there are hyphens in the page name" do
it "rewrites relative links as expected" do
- click_link 'New page'
- page.within '#modal-new-wiki' do
- fill_in :new_wiki_path, with: 'a-page/b-page/c-page/d-page'
- click_button 'Create page'
- end
-
- page.within '.wiki-form' do
- fill_in :wiki_content, with: wiki_content
- click_on "Preview"
- end
+ create_wiki_page('a-page/b-page/c-page/d-page', content: wiki_content)
expect(page).to have_content("regular link")
@@ -96,23 +67,9 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
end
context "while editing a wiki page" do
- def create_wiki_page(path)
- find('.add-new-wiki').click
-
- page.within '#modal-new-wiki' do
- fill_in :new_wiki_path, with: path
- click_button 'Create page'
- end
-
- page.within '.wiki-form' do
- fill_in :wiki_content, with: 'content'
- click_on "Create page"
- end
- end
-
context "when there are no spaces or hyphens in the page name" do
it "rewrites relative links as expected" do
- create_wiki_page 'a/b/c/d'
+ create_wiki_page('a/b/c/d')
click_link 'Edit'
fill_in :wiki_content, with: wiki_content
@@ -130,7 +87,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
context "when there are spaces in the page name" do
it "rewrites relative links as expected" do
- create_wiki_page 'a page/b page/c page/d page'
+ create_wiki_page('a page/b page/c page/d page')
click_link 'Edit'
fill_in :wiki_content, with: wiki_content
@@ -148,7 +105,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
context "when there are hyphens in the page name" do
it "rewrites relative links as expected" do
- create_wiki_page 'a-page/b-page/c-page/d-page'
+ create_wiki_page('a-page/b-page/c-page/d-page')
click_link 'Edit'
fill_in :wiki_content, with: wiki_content
@@ -166,7 +123,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
context 'when rendering the preview' do
it 'renders content with CommonMark' do
- create_wiki_page 'a-page/b-page/c-page/common-mark'
+ create_wiki_page('a-page/b-page/c-page/common-mark')
click_link 'Edit'
fill_in :wiki_content, with: "1. one\n - sublist\n"
@@ -180,25 +137,31 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
end
it "does not linkify double brackets inside code blocks as expected" do
- click_link 'New page'
- page.within '#modal-new-wiki' do
- fill_in :new_wiki_path, with: 'linkify_test'
- click_button 'Create page'
- end
+ wiki_content = <<-HEREDOC
+ `[[do_not_linkify]]`
+ ```
+ [[also_do_not_linkify]]
+ ```
+ HEREDOC
- page.within '.wiki-form' do
- fill_in :wiki_content, with: <<-HEREDOC
- `[[do_not_linkify]]`
- ```
- [[also_do_not_linkify]]
- ```
- HEREDOC
- click_on "Preview"
- end
+ create_wiki_page('linkify_test', wiki_content)
expect(page).to have_content("do_not_linkify")
expect(page.html).to include('[[do_not_linkify]]')
expect(page.html).to include('[[also_do_not_linkify]]')
end
+
+ private
+
+ def create_wiki_page(path, content = 'content')
+ visit project_wiki_path(project, wiki_page)
+
+ click_link 'New page'
+
+ fill_in :wiki_title, with: path
+ fill_in :wiki_content, with: content
+
+ click_button 'Create page'
+ end
end
diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
index cc6dbaa6eb8..56d0518015d 100644
--- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
@@ -42,10 +42,10 @@ describe "User creates wiki page" do
click_link("link test")
- expect(page).to have_content("Create Page")
+ expect(page).to have_content("Create New Page")
end
- it "shows non-escaped link in the pages list", :js, :quarantine do
+ it "shows non-escaped link in the pages list", :quarantine do
fill_in(:wiki_title, with: "one/two/three-test")
page.within(".wiki-form") do
@@ -58,7 +58,9 @@ describe "User creates wiki page" do
expect(page).to have_xpath("//a[@href='/#{project.full_path}/wikis/one/two/three-test']")
end
- it "has `Create home` as a commit message" do
+ it "has `Create home` as a commit message", :js do
+ wait_for_requests
+
expect(page).to have_field("wiki[message]", with: "Create home")
end
@@ -81,7 +83,7 @@ describe "User creates wiki page" do
expect(current_path).to eq(project_wiki_path(project, "test"))
page.within(:css, ".nav-text") do
- expect(page).to have_content("test").and have_content("Create Page")
+ expect(page).to have_content("Create New Page")
end
click_link("Home")
@@ -93,7 +95,7 @@ describe "User creates wiki page" do
expect(current_path).to eq(project_wiki_path(project, "api"))
page.within(:css, ".nav-text") do
- expect(page).to have_content("Create").and have_content("api")
+ expect(page).to have_content("Create")
end
click_link("Home")
@@ -105,7 +107,7 @@ describe "User creates wiki page" do
expect(current_path).to eq(project_wiki_path(project, "raketasks"))
page.within(:css, ".nav-text") do
- expect(page).to have_content("Create").and have_content("rake")
+ expect(page).to have_content("Create")
end
end
@@ -150,6 +152,8 @@ describe "User creates wiki page" do
let(:project) { create(:project, :wiki_repo, namespace: create(:group, :public)) }
it "has `Create home` as a commit message" do
+ wait_for_requests
+
expect(page).to have_field("wiki[message]", with: "Create home")
end
@@ -181,20 +185,15 @@ describe "User creates wiki page" do
it "creates a page with a single word" do
click_link("New page")
- page.within("#modal-new-wiki") do
- fill_in(:new_wiki_path, with: "foo")
-
- click_button("Create page")
+ page.within(".wiki-form") do
+ fill_in(:wiki_title, with: "foo")
+ fill_in(:wiki_content, with: "My awesome wiki!")
end
# Commit message field should have correct value.
expect(page).to have_field("wiki[message]", with: "Create foo")
- page.within(".wiki-form") do
- fill_in(:wiki_content, with: "My awesome wiki!")
-
- click_button("Create page")
- end
+ click_button("Create page")
expect(page).to have_content("foo")
.and have_content("Last edited by #{user.name}")
@@ -204,20 +203,15 @@ describe "User creates wiki page" do
it "creates a page with spaces in the name" do
click_link("New page")
- page.within("#modal-new-wiki") do
- fill_in(:new_wiki_path, with: "Spaces in the name")
-
- click_button("Create page")
+ page.within(".wiki-form") do
+ fill_in(:wiki_title, with: "Spaces in the name")
+ fill_in(:wiki_content, with: "My awesome wiki!")
end
# Commit message field should have correct value.
expect(page).to have_field("wiki[message]", with: "Create Spaces in the name")
- page.within(".wiki-form") do
- fill_in(:wiki_content, with: "My awesome wiki!")
-
- click_button("Create page")
- end
+ click_button("Create page")
expect(page).to have_content("Spaces in the name")
.and have_content("Last edited by #{user.name}")
@@ -227,10 +221,9 @@ describe "User creates wiki page" do
it "creates a page with hyphens in the name" do
click_link("New page")
- page.within("#modal-new-wiki") do
- fill_in(:new_wiki_path, with: "hyphens-in-the-name")
-
- click_button("Create page")
+ page.within(".wiki-form") do
+ fill_in(:wiki_title, with: "hyphens-in-the-name")
+ fill_in(:wiki_content, with: "My awesome wiki!")
end
# Commit message field should have correct value.
@@ -251,12 +244,6 @@ describe "User creates wiki page" do
it "shows the emoji autocompletion dropdown" do
click_link("New page")
- page.within("#modal-new-wiki") do
- fill_in(:new_wiki_path, with: "test-autocomplete")
-
- click_button("Create page")
- end
-
page.within(".wiki-form") do
find("#wiki_content").native.send_keys("")
@@ -274,20 +261,15 @@ describe "User creates wiki page" do
it "creates a page" do
click_link("New page")
- page.within("#modal-new-wiki") do
- fill_in(:new_wiki_path, with: "foo")
-
- click_button("Create page")
+ page.within(".wiki-form") do
+ fill_in(:wiki_title, with: "foo")
+ fill_in(:wiki_content, with: "My awesome wiki!")
end
# Commit message field should have correct value.
expect(page).to have_field("wiki[message]", with: "Create foo")
- page.within(".wiki-form") do
- fill_in(:wiki_content, with: "My awesome wiki!")
-
- click_button("Create page")
- end
+ click_button("Create page")
expect(page).to have_content("foo")
.and have_content("Last edited by #{user.name}")
diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
index 2aab8fda62d..3f3711f9eb8 100644
--- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
@@ -70,7 +70,7 @@ describe 'User updates wiki page' do
context 'in a user namespace' do
let(:project) { create(:project, :wiki_repo) }
- it 'updates a page' do
+ it 'updates a page', :js do
# Commit message field should have correct value.
expect(page).to have_field('wiki[message]', with: 'Update home')
@@ -82,6 +82,18 @@ describe 'User updates wiki page' do
expect(page).to have_content('My awesome wiki!')
end
+ it 'updates the commit message as the title is changed', :js do
+ fill_in(:wiki_title, with: 'Wiki title')
+
+ expect(page).to have_field('wiki[message]', with: 'Update Wiki title')
+ end
+
+ it 'does not allow XSS', :js do
+ fill_in(:wiki_title, with: '<script>')
+
+ expect(page).to have_field('wiki[message]', with: 'Update &lt;script&gt;')
+ end
+
it 'shows a validation error message' do
fill_in(:wiki_content, with: '')
click_button('Save changes')
@@ -129,7 +141,7 @@ describe 'User updates wiki page' do
context 'in a group namespace' do
let(:project) { create(:project, :wiki_repo, namespace: create(:group, :public)) }
- it 'updates a page' do
+ it 'updates a page', :js do
# Commit message field should have correct value.
expect(page).to have_field('wiki[message]', with: 'Update home')
diff --git a/spec/features/projects/wiki/user_views_wiki_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_page_spec.rb
index 05742b63c43..77e725e7f11 100644
--- a/spec/features/projects/wiki/user_views_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_views_wiki_page_spec.rb
@@ -101,8 +101,7 @@ describe 'User views a wiki page' do
click_on('image')
expect(current_path).to match("wikis/#{path}")
- expect(page).to have_content('New Wiki Page')
- expect(page).to have_content('Create page')
+ expect(page).to have_content('Create New Page')
end
end
@@ -156,6 +155,6 @@ describe 'User views a wiki page' do
find('.shortcuts-wiki').click
click_link "Create your first page"
- expect(page).to have_content('Home · Create Page')
+ expect(page).to have_content('Create New Page')
end
end
diff --git a/spec/finders/award_emojis_finder_spec.rb b/spec/finders/award_emojis_finder_spec.rb
new file mode 100644
index 00000000000..ccac475daad
--- /dev/null
+++ b/spec/finders/award_emojis_finder_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AwardEmojisFinder do
+ set(:issue_1) { create(:issue) }
+ set(:issue_1_thumbsup) { create(:award_emoji, name: 'thumbsup', awardable: issue_1) }
+ set(:issue_1_thumbsdown) { create(:award_emoji, name: 'thumbsdown', awardable: issue_1) }
+ # Create a matching set of emoji for a second issue.
+ # These should never appear in our finder results
+ set(:issue_2) { create(:issue) }
+ set(:issue_2_thumbsup) { create(:award_emoji, name: 'thumbsup', awardable: issue_2) }
+ set(:issue_2_thumbsdown) { create(:award_emoji, name: 'thumbsdown', awardable: issue_2) }
+
+ describe 'param validation' do
+ it 'raises an error if `name` is invalid' do
+ expect { described_class.new(issue_1, { name: 'invalid' }).execute }.to raise_error(
+ ArgumentError,
+ 'Invalid name param'
+ )
+ end
+
+ it 'raises an error if `awarded_by` is invalid' do
+ expectation = [ArgumentError, 'Invalid awarded_by param']
+
+ expect { described_class.new(issue_1, { awarded_by: issue_2 }).execute }.to raise_error(*expectation)
+ expect { described_class.new(issue_1, { awarded_by: 'not-an-id' }).execute }.to raise_error(*expectation)
+ expect { described_class.new(issue_1, { awarded_by: 1.123 }).execute }.to raise_error(*expectation)
+ end
+ end
+
+ describe '#execute' do
+ it 'scopes to the awardable' do
+ expect(described_class.new(issue_1).execute).to contain_exactly(
+ issue_1_thumbsup, issue_1_thumbsdown
+ )
+ end
+
+ it 'filters by emoji name' do
+ expect(described_class.new(issue_1, { name: 'thumbsup' }).execute).to contain_exactly(issue_1_thumbsup)
+ expect(described_class.new(issue_1, { name: '8ball' }).execute).to be_empty
+ end
+
+ it 'filters by user' do
+ expect(described_class.new(issue_1, { awarded_by: issue_1_thumbsup.user }).execute).to contain_exactly(issue_1_thumbsup)
+ expect(described_class.new(issue_1, { awarded_by: issue_2_thumbsup.user }).execute).to be_empty
+ end
+ end
+end
diff --git a/spec/fixtures/api/schemas/deployment.json b/spec/fixtures/api/schemas/deployment.json
index 9216ad0060b..fe725b97c21 100644
--- a/spec/fixtures/api/schemas/deployment.json
+++ b/spec/fixtures/api/schemas/deployment.json
@@ -3,7 +3,7 @@
"required": [
"sha",
"created_at",
- "finished_at",
+ "deployed_at",
"iid",
"tag",
"last?",
@@ -12,7 +12,7 @@
],
"properties": {
"created_at": { "type": "string" },
- "finished_at": { "type": ["string", "null"] },
+ "deployed_at": { "type": ["string", "null"] },
"id": { "type": "integer" },
"iid": { "type": "integer" },
"last?": { "type": "boolean" },
diff --git a/spec/fixtures/api/schemas/entities/merge_request_sidebar.json b/spec/fixtures/api/schemas/entities/merge_request_sidebar.json
index 214b67a9a0f..9945de8a856 100644
--- a/spec/fixtures/api/schemas/entities/merge_request_sidebar.json
+++ b/spec/fixtures/api/schemas/entities/merge_request_sidebar.json
@@ -2,6 +2,7 @@
"type": "object",
"properties" : {
"id": { "type": "integer" },
+ "iid": { "type": "integer" },
"type": { "type": "string" },
"author_id": { "type": "integer" },
"project_id": { "type": "integer" },
diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js
index 7004373be0e..62ba0d36982 100644
--- a/spec/frontend/api_spec.js
+++ b/spec/frontend/api_spec.js
@@ -151,6 +151,28 @@ describe('Api', () => {
});
});
+ describe('projectUsers', () => {
+ it('fetches all users of a particular project', done => {
+ const query = 'dummy query';
+ const options = { unused: 'option' };
+ const projectPath = 'gitlab-org%2Fgitlab-ce';
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/users`;
+ mock.onGet(expectedUrl).reply(200, [
+ {
+ name: 'test',
+ },
+ ]);
+
+ Api.projectUsers('gitlab-org/gitlab-ce', query, options)
+ .then(response => {
+ expect(response.length).toBe(1);
+ expect(response[0].name).toBe('test');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
describe('projectMergeRequests', () => {
const projectPath = 'abc';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests`;
diff --git a/spec/frontend/autosave_spec.js b/spec/frontend/autosave_spec.js
index 4d9c8f96d62..33d402388c9 100644
--- a/spec/frontend/autosave_spec.js
+++ b/spec/frontend/autosave_spec.js
@@ -63,12 +63,15 @@ describe('Autosave', () => {
expect(field.trigger).toHaveBeenCalled();
});
- it('triggers native event', done => {
- autosave.field.get(0).addEventListener('change', () => {
- done();
- });
+ it('triggers native event', () => {
+ const fieldElement = autosave.field.get(0);
+ const eventHandler = jest.fn();
+ fieldElement.addEventListener('change', eventHandler);
Autosave.prototype.restore.call(autosave);
+
+ expect(eventHandler).toHaveBeenCalledTimes(1);
+ fieldElement.removeEventListener('change', eventHandler);
});
});
diff --git a/spec/frontend/mocks/mocks_helper_spec.js b/spec/frontend/mocks/mocks_helper_spec.js
index 34be110a7e3..b8bb02c2f43 100644
--- a/spec/frontend/mocks/mocks_helper_spec.js
+++ b/spec/frontend/mocks/mocks_helper_spec.js
@@ -46,7 +46,9 @@ describe('mocks_helper.js', () => {
readdir.sync.mockReturnValue([]);
setupManualMocks();
- readdir.mock.calls.forEach(call => {
+ const readdirSpy = readdir.sync;
+ expect(readdirSpy).toHaveBeenCalled();
+ readdirSpy.mock.calls.forEach(call => {
expect(call[1].deep).toBeLessThan(100);
});
});
diff --git a/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
new file mode 100644
index 00000000000..452d4cd07cc
--- /dev/null
+++ b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
@@ -0,0 +1,85 @@
+import { shallowMount } from '@vue/test-utils';
+import { joinPaths } from '~/lib/utils/url_utility';
+import { TEST_HOST } from 'helpers/test_constants';
+import AssigneeAvatarLink from '~/sidebar/components/assignees/assignee_avatar_link.vue';
+import AssigneeAvatar from '~/sidebar/components/assignees/assignee_avatar.vue';
+import userDataMock from '../../user_data_mock';
+
+const TOOLTIP_PLACEMENT = 'bottom';
+const { name: USER_NAME, username: USER_USERNAME } = userDataMock();
+const TEST_ISSUABLE_TYPE = 'merge_request';
+
+describe('AssigneeAvatarLink component', () => {
+ let wrapper;
+
+ function createComponent(props = {}) {
+ const propsData = {
+ user: userDataMock(),
+ showLess: true,
+ rootPath: TEST_HOST,
+ tooltipPlacement: TOOLTIP_PLACEMENT,
+ singleUser: false,
+ issuableType: TEST_ISSUABLE_TYPE,
+ ...props,
+ };
+
+ wrapper = shallowMount(AssigneeAvatarLink, {
+ propsData,
+ sync: false,
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findTooltipText = () => wrapper.attributes('data-original-title');
+
+ it('has the root url present in the assigneeUrl method', () => {
+ createComponent();
+ const assigneeUrl = joinPaths(TEST_HOST, USER_USERNAME);
+
+ expect(wrapper.attributes().href).toEqual(assigneeUrl);
+ });
+
+ it('renders assignee avatar', () => {
+ createComponent();
+
+ expect(wrapper.find(AssigneeAvatar).props()).toEqual(
+ expect.objectContaining({
+ issuableType: TEST_ISSUABLE_TYPE,
+ user: userDataMock(),
+ }),
+ );
+ });
+
+ describe.each`
+ issuableType | tooltipHasName | canMerge | expected
+ ${'merge_request'} | ${true} | ${true} | ${USER_NAME}
+ ${'merge_request'} | ${true} | ${false} | ${`${USER_NAME} (cannot merge)`}
+ ${'merge_request'} | ${false} | ${true} | ${''}
+ ${'merge_request'} | ${false} | ${false} | ${'Cannot merge'}
+ ${'issue'} | ${true} | ${true} | ${USER_NAME}
+ ${'issue'} | ${true} | ${false} | ${USER_NAME}
+ ${'issue'} | ${false} | ${true} | ${''}
+ ${'issue'} | ${false} | ${false} | ${''}
+ `(
+ 'with $issuableType and tooltipHasName=$tooltipHasName and canMerge=$canMerge',
+ ({ issuableType, tooltipHasName, canMerge, expected }) => {
+ beforeEach(() => {
+ createComponent({
+ issuableType,
+ tooltipHasName,
+ user: {
+ ...userDataMock(),
+ can_merge: canMerge,
+ },
+ });
+ });
+
+ it('sets tooltip', () => {
+ expect(findTooltipText()).toBe(expected);
+ });
+ },
+ );
+});
diff --git a/spec/frontend/sidebar/components/assignees/assignee_avatar_spec.js b/spec/frontend/sidebar/components/assignees/assignee_avatar_spec.js
new file mode 100644
index 00000000000..d60ae17733b
--- /dev/null
+++ b/spec/frontend/sidebar/components/assignees/assignee_avatar_spec.js
@@ -0,0 +1,78 @@
+import { shallowMount } from '@vue/test-utils';
+import AssigneeAvatar from '~/sidebar/components/assignees/assignee_avatar.vue';
+import { TEST_HOST } from 'helpers/test_constants';
+import userDataMock from '../../user_data_mock';
+
+const TEST_AVATAR = `${TEST_HOST}/avatar.png`;
+const TEST_DEFAULT_AVATAR_URL = `${TEST_HOST}/default/avatar/url.png`;
+
+describe('AssigneeAvatar', () => {
+ let origGon;
+ let wrapper;
+
+ function createComponent(props = {}) {
+ const propsData = {
+ user: userDataMock(),
+ imgSize: 24,
+ issuableType: 'merge_request',
+ ...props,
+ };
+
+ wrapper = shallowMount(AssigneeAvatar, {
+ propsData,
+ sync: false,
+ });
+ }
+
+ beforeEach(() => {
+ origGon = window.gon;
+ window.gon = { default_avatar_url: TEST_DEFAULT_AVATAR_URL };
+ });
+
+ afterEach(() => {
+ window.gon = origGon;
+ wrapper.destroy();
+ });
+
+ const findImg = () => wrapper.find('img');
+
+ it('does not show warning icon if assignee can merge', () => {
+ createComponent();
+
+ expect(wrapper.find('.merge-icon').exists()).toBe(false);
+ });
+
+ it('shows warning icon if assignee cannot merge', () => {
+ createComponent({
+ user: {
+ can_merge: false,
+ },
+ });
+
+ expect(wrapper.find('.merge-icon').exists()).toBe(true);
+ });
+
+ it('does not show warning icon for issuableType = "issue"', () => {
+ createComponent({
+ issuableType: 'issue',
+ });
+
+ expect(wrapper.find('.merge-icon').exists()).toBe(false);
+ });
+
+ it.each`
+ avatar | avatar_url | expected | desc
+ ${TEST_AVATAR} | ${null} | ${TEST_AVATAR} | ${'with avatar'}
+ ${null} | ${TEST_AVATAR} | ${TEST_AVATAR} | ${'with avatar_url'}
+ ${null} | ${null} | ${TEST_DEFAULT_AVATAR_URL} | ${'with no avatar'}
+ `('$desc', ({ avatar, avatar_url, expected }) => {
+ createComponent({
+ user: {
+ avatar,
+ avatar_url,
+ },
+ });
+
+ expect(findImg().attributes('src')).toEqual(expected);
+ });
+});
diff --git a/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js b/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js
new file mode 100644
index 00000000000..ff0c8d181b5
--- /dev/null
+++ b/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js
@@ -0,0 +1,189 @@
+import { shallowMount } from '@vue/test-utils';
+import CollapsedAssigneeList from '~/sidebar/components/assignees/collapsed_assignee_list.vue';
+import CollapsedAssignee from '~/sidebar/components/assignees/collapsed_assignee.vue';
+import UsersMockHelper from 'helpers/user_mock_data_helper';
+
+const DEFAULT_MAX_COUNTER = 99;
+
+describe('CollapsedAssigneeList component', () => {
+ let wrapper;
+
+ function createComponent(props = {}) {
+ const propsData = {
+ users: [],
+ issuableType: 'merge_request',
+ ...props,
+ };
+
+ wrapper = shallowMount(CollapsedAssigneeList, {
+ propsData,
+ sync: false,
+ });
+ }
+
+ const findNoUsersIcon = () => wrapper.find('i[aria-label=None]');
+ const findAvatarCounter = () => wrapper.find('.avatar-counter');
+ const findAssignees = () => wrapper.findAll(CollapsedAssignee);
+ const getTooltipTitle = () => wrapper.attributes('data-original-title');
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('No assignees/users', () => {
+ beforeEach(() => {
+ createComponent({
+ users: [],
+ });
+ });
+
+ it('has no users', () => {
+ expect(findNoUsersIcon().exists()).toBe(true);
+ });
+ });
+
+ describe('One assignee/user', () => {
+ let users;
+
+ beforeEach(() => {
+ users = UsersMockHelper.createNumberRandomUsers(1);
+ });
+
+ it('should not show no users icon', () => {
+ createComponent({ users });
+
+ expect(findNoUsersIcon().exists()).toBe(false);
+ });
+
+ it('has correct "cannot merge" tooltip when user cannot merge', () => {
+ users[0].can_merge = false;
+
+ createComponent({ users });
+
+ expect(getTooltipTitle()).toContain('cannot merge');
+ });
+
+ it('does not have "merge" word in tooltip if user can merge', () => {
+ users[0].can_merge = true;
+
+ createComponent({ users });
+
+ expect(getTooltipTitle()).not.toContain('merge');
+ });
+ });
+
+ describe('More than one assignees/users', () => {
+ let users;
+
+ beforeEach(() => {
+ users = UsersMockHelper.createNumberRandomUsers(2);
+
+ createComponent({ users });
+ });
+
+ it('has multiple-users class', () => {
+ expect(wrapper.classes('multiple-users')).toBe(true);
+ });
+
+ it('does not display an avatar count', () => {
+ expect(findAvatarCounter().exists()).toBe(false);
+ });
+
+ it('returns just two collapsed users', () => {
+ expect(findAssignees().length).toBe(2);
+ });
+ });
+
+ describe('More than two assignees/users', () => {
+ let users;
+ let userNames;
+
+ beforeEach(() => {
+ users = UsersMockHelper.createNumberRandomUsers(3);
+ userNames = users.map(x => x.name).join(', ');
+ });
+
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent({ users });
+ });
+
+ it('does display an avatar count', () => {
+ expect(findAvatarCounter().exists()).toBe(true);
+ expect(findAvatarCounter().text()).toEqual('+2');
+ });
+
+ it('returns one collapsed users', () => {
+ expect(findAssignees().length).toBe(1);
+ });
+ });
+
+ it('has corrent "no one can merge" tooltip when no one can merge', () => {
+ users[0].can_merge = false;
+ users[1].can_merge = false;
+ users[2].can_merge = false;
+
+ createComponent({
+ users,
+ });
+
+ expect(getTooltipTitle()).toEqual(`${userNames} (no one can merge)`);
+ });
+
+ it('has correct "cannot merge" tooltip when one user can merge', () => {
+ users[0].can_merge = true;
+ users[1].can_merge = false;
+ users[2].can_merge = false;
+
+ createComponent({
+ users,
+ });
+
+ expect(getTooltipTitle()).toEqual(`${userNames} (1/3 can merge)`);
+ });
+
+ it('has correct "cannot merge" tooltip when more than one user can merge', () => {
+ users[0].can_merge = false;
+ users[1].can_merge = true;
+ users[2].can_merge = true;
+
+ createComponent({
+ users,
+ });
+
+ expect(getTooltipTitle()).toEqual(`${userNames} (2/3 can merge)`);
+ });
+
+ it('does not have "merge" in tooltip if everyone can merge', () => {
+ users[0].can_merge = true;
+ users[1].can_merge = true;
+ users[2].can_merge = true;
+
+ createComponent({
+ users,
+ });
+
+ expect(getTooltipTitle()).toEqual(userNames);
+ });
+
+ it('displays the correct avatar count', () => {
+ users = UsersMockHelper.createNumberRandomUsers(5);
+
+ createComponent({
+ users,
+ });
+
+ expect(findAvatarCounter().text()).toEqual(`+${users.length - 1}`);
+ });
+
+ it('displays the correct avatar count via a computed property if more than default max counter', () => {
+ users = UsersMockHelper.createNumberRandomUsers(100);
+
+ createComponent({
+ users,
+ });
+
+ expect(findAvatarCounter().text()).toEqual(`${DEFAULT_MAX_COUNTER}+`);
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js b/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js
new file mode 100644
index 00000000000..f9ca7bc1ecb
--- /dev/null
+++ b/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js
@@ -0,0 +1,49 @@
+import { shallowMount } from '@vue/test-utils';
+import CollapsedAssignee from '~/sidebar/components/assignees/collapsed_assignee.vue';
+import AssigneeAvatar from '~/sidebar/components/assignees/assignee_avatar.vue';
+import userDataMock from '../../user_data_mock';
+
+const TEST_USER = userDataMock();
+const TEST_ISSUABLE_TYPE = 'merge_request';
+
+describe('CollapsedAssignee assignee component', () => {
+ let wrapper;
+
+ function createComponent(props = {}) {
+ const propsData = {
+ user: userDataMock(),
+ issuableType: TEST_ISSUABLE_TYPE,
+ ...props,
+ };
+
+ wrapper = shallowMount(CollapsedAssignee, {
+ propsData,
+ sync: false,
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('has author name', () => {
+ createComponent();
+
+ expect(
+ wrapper
+ .find('.author')
+ .text()
+ .trim(),
+ ).toEqual(TEST_USER.name);
+ });
+
+ it('has assignee avatar', () => {
+ createComponent();
+
+ expect(wrapper.find(AssigneeAvatar).props()).toEqual({
+ imgSize: 24,
+ user: TEST_USER,
+ issuableType: TEST_ISSUABLE_TYPE,
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js b/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js
new file mode 100644
index 00000000000..6398351834c
--- /dev/null
+++ b/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js
@@ -0,0 +1,103 @@
+import { mount } from '@vue/test-utils';
+import UncollapsedAssigneeList from '~/sidebar/components/assignees/uncollapsed_assignee_list.vue';
+import AssigneeAvatarLink from '~/sidebar/components/assignees/assignee_avatar_link.vue';
+import { TEST_HOST } from 'helpers/test_constants';
+import userDataMock from '../../user_data_mock';
+import UsersMockHelper from '../../../helpers/user_mock_data_helper';
+
+const DEFAULT_RENDER_COUNT = 5;
+
+describe('UncollapsedAssigneeList component', () => {
+ let wrapper;
+
+ function createComponent(props = {}) {
+ const propsData = {
+ users: [],
+ rootPath: TEST_HOST,
+ ...props,
+ };
+
+ wrapper = mount(UncollapsedAssigneeList, {
+ sync: false,
+ propsData,
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findMoreButton = () => wrapper.find('.user-list-more button');
+
+ describe('One assignee/user', () => {
+ let user;
+
+ beforeEach(() => {
+ user = userDataMock();
+
+ createComponent({
+ users: [user],
+ });
+ });
+
+ it('only has one user', () => {
+ expect(wrapper.findAll(AssigneeAvatarLink).length).toBe(1);
+ });
+
+ it('calls the AssigneeAvatarLink with the proper props', () => {
+ expect(wrapper.find(AssigneeAvatarLink).exists()).toBe(true);
+ expect(wrapper.find(AssigneeAvatarLink).props().tooltipPlacement).toEqual('left');
+ });
+
+ it('Shows one user with avatar, username and author name', () => {
+ expect(wrapper.text()).toContain(user.name);
+ expect(wrapper.text()).toContain(`@${user.username}`);
+ });
+ });
+
+ describe('n+ more label', () => {
+ describe('when users count is rendered users', () => {
+ beforeEach(() => {
+ createComponent({
+ users: UsersMockHelper.createNumberRandomUsers(DEFAULT_RENDER_COUNT),
+ });
+ });
+
+ it('does not show more label', () => {
+ expect(findMoreButton().exists()).toBe(false);
+ });
+ });
+
+ describe('when more than rendered users', () => {
+ beforeEach(() => {
+ createComponent({
+ users: UsersMockHelper.createNumberRandomUsers(DEFAULT_RENDER_COUNT + 1),
+ });
+ });
+
+ it('shows "+1 more" label', () => {
+ expect(findMoreButton().text()).toBe('+ 1 more');
+ });
+
+ it('shows truncated users', () => {
+ expect(wrapper.findAll(AssigneeAvatarLink).length).toBe(DEFAULT_RENDER_COUNT);
+ });
+
+ describe('when more button is clicked', () => {
+ beforeEach(() => {
+ findMoreButton().trigger('click');
+
+ return wrapper.vm.$nextTick();
+ });
+
+ it('shows "show less" label', () => {
+ expect(findMoreButton().text()).toBe('- show less');
+ });
+
+ it('shows all users', () => {
+ expect(wrapper.findAll(AssigneeAvatarLink).length).toBe(DEFAULT_RENDER_COUNT + 1);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/user_data_mock.js b/spec/frontend/sidebar/user_data_mock.js
new file mode 100644
index 00000000000..8ad70bb3499
--- /dev/null
+++ b/spec/frontend/sidebar/user_data_mock.js
@@ -0,0 +1,9 @@
+export default () => ({
+ avatar_url: 'mock_path',
+ id: 1,
+ name: 'Root',
+ state: 'active',
+ username: 'root',
+ web_url: '',
+ can_merge: true,
+});
diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js
index df8a625319b..d52aeb1fe6b 100644
--- a/spec/frontend/test_setup.js
+++ b/spec/frontend/test_setup.js
@@ -93,3 +93,9 @@ Object.assign(global, {
clearTimeout(id);
},
});
+
+// make sure that each test actually tests something
+// see https://jestjs.io/docs/en/expect#expecthasassertions
+beforeEach(() => {
+ expect.hasAssertions();
+});
diff --git a/spec/frontend/wikis_spec.js b/spec/frontend/wikis_spec.js
new file mode 100644
index 00000000000..b2475488d97
--- /dev/null
+++ b/spec/frontend/wikis_spec.js
@@ -0,0 +1,74 @@
+import Wikis from '~/pages/projects/wikis/wikis';
+import { setHTMLFixture } from './helpers/fixtures';
+
+describe('Wikis', () => {
+ describe('setting the commit message when the title changes', () => {
+ const editFormHtmlFixture = args => `<form class="wiki-form ${
+ args.newPage ? 'js-new-wiki-page' : ''
+ }">
+ <input type="text" id="wiki_title" value="My title" />
+ <input type="text" id="wiki_message" />
+ </form>`;
+
+ let wikis;
+ let titleInput;
+ let messageInput;
+
+ describe('when the wiki page is being created', () => {
+ const formHtmlFixture = editFormHtmlFixture({ newPage: true });
+
+ beforeEach(() => {
+ setHTMLFixture(formHtmlFixture);
+
+ titleInput = document.getElementById('wiki_title');
+ messageInput = document.getElementById('wiki_message');
+ wikis = new Wikis();
+ });
+
+ it('binds an event listener to the title input', () => {
+ wikis.handleWikiTitleChange = jest.fn();
+
+ titleInput.dispatchEvent(new Event('keyup'));
+
+ expect(wikis.handleWikiTitleChange).toHaveBeenCalled();
+ });
+
+ it('sets the commit message when title changes', () => {
+ titleInput.value = 'My title';
+ messageInput.value = '';
+
+ titleInput.dispatchEvent(new Event('keyup'));
+
+ expect(messageInput.value).toEqual('Create My title');
+ });
+
+ it('replaces hyphens with spaces', () => {
+ titleInput.value = 'my-hyphenated-title';
+ titleInput.dispatchEvent(new Event('keyup'));
+
+ expect(messageInput.value).toEqual('Create my hyphenated title');
+ });
+ });
+
+ describe('when the wiki page is being updated', () => {
+ const formHtmlFixture = editFormHtmlFixture({ newPage: false });
+
+ beforeEach(() => {
+ setHTMLFixture(formHtmlFixture);
+
+ titleInput = document.getElementById('wiki_title');
+ messageInput = document.getElementById('wiki_message');
+ wikis = new Wikis();
+ });
+
+ it('sets the commit message when title changes, prefixing with "Update"', () => {
+ titleInput.value = 'My title';
+ messageInput.value = '';
+
+ titleInput.dispatchEvent(new Event('keyup'));
+
+ expect(messageInput.value).toEqual('Update My title');
+ });
+ });
+ });
+});
diff --git a/spec/initializers/action_mailer_hooks_spec.rb b/spec/initializers/action_mailer_hooks_spec.rb
new file mode 100644
index 00000000000..3826ed9b00a
--- /dev/null
+++ b/spec/initializers/action_mailer_hooks_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe 'ActionMailer hooks' do
+ describe 'smime signature interceptor' do
+ before do
+ class_spy(ActionMailer::Base).as_stubbed_const
+ end
+
+ it 'is disabled by default' do
+ load Rails.root.join('config/initializers/action_mailer_hooks.rb')
+
+ expect(ActionMailer::Base).not_to(
+ have_received(:register_interceptor).with(Gitlab::Email::Hook::SmimeSignatureInterceptor))
+ end
+
+ describe 'interceptor testbed' do
+ where(:email_enabled, :email_smime_enabled, :smime_interceptor_enabled) do
+ [
+ [false, false, false],
+ [false, true, false],
+ [true, false, false],
+ [true, true, true]
+ ]
+ end
+
+ with_them do
+ before do
+ stub_config_setting(email_enabled: email_enabled)
+ stub_config_setting(email_smime: { enabled: email_smime_enabled })
+ end
+
+ it 'is enabled depending on settings' do
+ load Rails.root.join('config/initializers/action_mailer_hooks.rb')
+
+ if smime_interceptor_enabled
+ expect(ActionMailer::Base).to(
+ have_received(:register_interceptor).with(Gitlab::Email::Hook::SmimeSignatureInterceptor))
+ else
+ expect(ActionMailer::Base).not_to(
+ have_received(:register_interceptor).with(Gitlab::Email::Hook::SmimeSignatureInterceptor))
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/javascripts/diffs/components/diff_file_header_spec.js b/spec/javascripts/diffs/components/diff_file_header_spec.js
index d4280d3ec2c..356e7a8f1fe 100644
--- a/spec/javascripts/diffs/components/diff_file_header_spec.js
+++ b/spec/javascripts/diffs/components/diff_file_header_spec.js
@@ -372,7 +372,7 @@ describe('diff_file_header', () => {
});
it('displays old and new path if the file was renamed', () => {
- props.diffFile.viewer.name = diffViewerModes.renamed;
+ props.diffFile.renamed_file = true;
vm = mountComponentWithStore(Component, { props, store });
diff --git a/spec/javascripts/issue_show/components/description_spec.js b/spec/javascripts/issue_show/components/description_spec.js
index 7e00fbf2745..e10426a9858 100644
--- a/spec/javascripts/issue_show/components/description_spec.js
+++ b/spec/javascripts/issue_show/components/description_spec.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import Vue from 'vue';
+import '~/behaviors/markdown/render_gfm';
import Description from '~/issue_show/components/description.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
@@ -91,6 +92,7 @@ describe('Description component', () => {
let TaskList;
beforeEach(() => {
+ vm.$destroy();
vm = mountComponent(
DescriptionComponent,
Object.assign({}, props, {
diff --git a/spec/javascripts/monitoring/charts/time_series_spec.js b/spec/javascripts/monitoring/charts/time_series_spec.js
new file mode 100644
index 00000000000..d145a64e8d0
--- /dev/null
+++ b/spec/javascripts/monitoring/charts/time_series_spec.js
@@ -0,0 +1,335 @@
+import { shallowMount } from '@vue/test-utils';
+import { createStore } from '~/monitoring/stores';
+import { GlLink } from '@gitlab/ui';
+import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
+import { shallowWrapperContainsSlotText } from 'spec/helpers/vue_test_utils_helper';
+import TimeSeries from '~/monitoring/components/charts/time_series.vue';
+import * as types from '~/monitoring/stores/mutation_types';
+import { TEST_HOST } from 'spec/test_constants';
+import MonitoringMock, { deploymentData, mockProjectPath } from '../mock_data';
+
+describe('Time series component', () => {
+ const mockSha = 'mockSha';
+ const mockWidgets = 'mockWidgets';
+ const mockSvgPathContent = 'mockSvgPathContent';
+ const projectPath = `${TEST_HOST}${mockProjectPath}`;
+ const commitUrl = `${projectPath}/commit/${mockSha}`;
+ let mockGraphData;
+ let makeTimeSeriesChart;
+ let spriteSpy;
+ let store;
+
+ beforeEach(() => {
+ store = createStore();
+ store.commit(`monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, MonitoringMock.data);
+ store.commit(`monitoringDashboard/${types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS}`, deploymentData);
+ store.dispatch('monitoringDashboard/setFeatureFlags', { exportMetricsToCsvEnabled: true });
+ [mockGraphData] = store.state.monitoringDashboard.groups[0].metrics;
+
+ makeTimeSeriesChart = (graphData, type) =>
+ shallowMount(TimeSeries, {
+ propsData: {
+ graphData: { ...graphData, type },
+ containerWidth: 0,
+ deploymentData: store.state.monitoringDashboard.deploymentData,
+ projectPath,
+ },
+ slots: {
+ default: mockWidgets,
+ },
+ sync: false,
+ store,
+ });
+
+ spriteSpy = spyOnDependency(TimeSeries, 'getSvgIconPathContent').and.callFake(
+ () => new Promise(resolve => resolve(mockSvgPathContent)),
+ );
+ });
+
+ describe('general functions', () => {
+ let timeSeriesChart;
+
+ beforeEach(() => {
+ timeSeriesChart = makeTimeSeriesChart(mockGraphData, 'area-chart');
+ });
+
+ it('renders chart title', () => {
+ expect(timeSeriesChart.find('.js-graph-title').text()).toBe(mockGraphData.title);
+ });
+
+ it('contains graph widgets from slot', () => {
+ expect(timeSeriesChart.find('.js-graph-widgets').text()).toBe(mockWidgets);
+ });
+
+ describe('when exportMetricsToCsvEnabled is disabled', () => {
+ beforeEach(() => {
+ store.dispatch('monitoringDashboard/setFeatureFlags', { exportMetricsToCsvEnabled: false });
+ });
+
+ it('does not render the Download CSV button', done => {
+ timeSeriesChart.vm.$nextTick(() => {
+ expect(timeSeriesChart.contains('glbutton-stub')).toBe(false);
+ done();
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('formatTooltipText', () => {
+ const mockDate = deploymentData[0].created_at;
+ const mockCommitUrl = deploymentData[0].commitUrl;
+ const generateSeriesData = type => ({
+ seriesData: [
+ {
+ seriesName: timeSeriesChart.vm.chartData[0].name,
+ componentSubType: type,
+ value: [mockDate, 5.55555],
+ seriesIndex: 0,
+ },
+ ],
+ value: mockDate,
+ });
+
+ describe('when series is of line type', () => {
+ beforeEach(done => {
+ timeSeriesChart.vm.formatTooltipText(generateSeriesData('line'));
+ timeSeriesChart.vm.$nextTick(done);
+ });
+
+ it('formats tooltip title', () => {
+ expect(timeSeriesChart.vm.tooltip.title).toBe('31 May 2017, 9:23PM');
+ });
+
+ it('formats tooltip content', () => {
+ const name = 'Core Usage';
+ const value = '5.556';
+ const seriesLabel = timeSeriesChart.find(GlChartSeriesLabel);
+
+ expect(seriesLabel.vm.color).toBe('');
+ expect(shallowWrapperContainsSlotText(seriesLabel, 'default', name)).toBe(true);
+ expect(timeSeriesChart.vm.tooltip.content).toEqual([{ name, value, color: undefined }]);
+ expect(
+ shallowWrapperContainsSlotText(
+ timeSeriesChart.find(GlAreaChart),
+ 'tooltipContent',
+ value,
+ ),
+ ).toBe(true);
+ });
+ });
+
+ describe('when series is of scatter type', () => {
+ beforeEach(() => {
+ timeSeriesChart.vm.formatTooltipText(generateSeriesData('scatter'));
+ });
+
+ it('formats tooltip title', () => {
+ expect(timeSeriesChart.vm.tooltip.title).toBe('31 May 2017, 9:23PM');
+ });
+
+ it('formats tooltip sha', () => {
+ expect(timeSeriesChart.vm.tooltip.sha).toBe('f5bcd1d9');
+ });
+
+ it('formats tooltip commit url', () => {
+ expect(timeSeriesChart.vm.tooltip.commitUrl).toBe(mockCommitUrl);
+ });
+ });
+ });
+
+ describe('setSvg', () => {
+ const mockSvgName = 'mockSvgName';
+
+ beforeEach(done => {
+ timeSeriesChart.vm.setSvg(mockSvgName);
+ timeSeriesChart.vm.$nextTick(done);
+ });
+
+ it('gets svg path content', () => {
+ expect(spriteSpy).toHaveBeenCalledWith(mockSvgName);
+ });
+
+ it('sets svg path content', () => {
+ timeSeriesChart.vm.$nextTick(() => {
+ expect(timeSeriesChart.vm.svgs[mockSvgName]).toBe(`path://${mockSvgPathContent}`);
+ });
+ });
+ });
+
+ describe('onResize', () => {
+ const mockWidth = 233;
+
+ beforeEach(() => {
+ spyOn(Element.prototype, 'getBoundingClientRect').and.callFake(() => ({
+ width: mockWidth,
+ }));
+ timeSeriesChart.vm.onResize();
+ });
+
+ it('sets area chart width', () => {
+ expect(timeSeriesChart.vm.width).toBe(mockWidth);
+ });
+ });
+ });
+
+ describe('computed', () => {
+ describe('chartData', () => {
+ let chartData;
+ const seriesData = () => chartData[0];
+
+ beforeEach(() => {
+ ({ chartData } = timeSeriesChart.vm);
+ });
+
+ it('utilizes all data points', () => {
+ const { values } = mockGraphData.queries[0].result[0];
+
+ expect(chartData.length).toBe(1);
+ expect(seriesData().data.length).toBe(values.length);
+ });
+
+ it('creates valid data', () => {
+ const { data } = seriesData();
+
+ expect(
+ data.filter(
+ ([time, value]) => new Date(time).getTime() > 0 && typeof value === 'number',
+ ).length,
+ ).toBe(data.length);
+ });
+
+ it('formats line width correctly', () => {
+ expect(chartData[0].lineStyle.width).toBe(2);
+ });
+ });
+
+ describe('chartOptions', () => {
+ describe('yAxis formatter', () => {
+ let format;
+
+ beforeEach(() => {
+ format = timeSeriesChart.vm.chartOptions.yAxis.axisLabel.formatter;
+ });
+
+ it('rounds to 3 decimal places', () => {
+ expect(format(0.88888)).toBe('0.889');
+ });
+ });
+ });
+
+ describe('scatterSeries', () => {
+ it('utilizes deployment data', () => {
+ expect(timeSeriesChart.vm.scatterSeries.data).toEqual([
+ ['2017-05-31T21:23:37.881Z', 0],
+ ['2017-05-30T20:08:04.629Z', 0],
+ ['2017-05-30T17:42:38.409Z', 0],
+ ]);
+
+ expect(timeSeriesChart.vm.scatterSeries.symbolSize).toBe(14);
+ });
+ });
+
+ describe('yAxisLabel', () => {
+ it('constructs a label for the chart y-axis', () => {
+ expect(timeSeriesChart.vm.yAxisLabel).toBe('CPU');
+ });
+ });
+
+ describe('csvText', () => {
+ it('converts data from json to csv', () => {
+ const header = `timestamp,${mockGraphData.y_label}`;
+ const data = mockGraphData.queries[0].result[0].values;
+ const firstRow = `${data[0][0]},${data[0][1]}`;
+
+ expect(timeSeriesChart.vm.csvText).toMatch(`^${header}\r\n${firstRow}`);
+ });
+ });
+
+ describe('downloadLink', () => {
+ it('produces a link to download metrics as csv', () => {
+ const link = timeSeriesChart.vm.downloadLink;
+
+ expect(link).toContain('blob:');
+ });
+ });
+ });
+
+ afterEach(() => {
+ timeSeriesChart.destroy();
+ });
+ });
+
+ describe('wrapped components', () => {
+ const glChartComponents = [
+ {
+ chartType: 'area-chart',
+ component: GlAreaChart,
+ },
+ {
+ chartType: 'line-chart',
+ component: GlLineChart,
+ },
+ ];
+
+ glChartComponents.forEach(dynamicComponent => {
+ describe(`GitLab UI: ${dynamicComponent.chartType}`, () => {
+ let timeSeriesAreaChart;
+ let glChart;
+
+ beforeEach(done => {
+ timeSeriesAreaChart = makeTimeSeriesChart(mockGraphData, dynamicComponent.chartType);
+ glChart = timeSeriesAreaChart.find(dynamicComponent.component);
+ timeSeriesAreaChart.vm.$nextTick(done);
+ });
+
+ it('is a Vue instance', () => {
+ expect(glChart.exists()).toBe(true);
+ expect(glChart.isVueInstance()).toBe(true);
+ });
+
+ it('receives data properties needed for proper chart render', () => {
+ const props = glChart.props();
+
+ expect(props.data).toBe(timeSeriesAreaChart.vm.chartData);
+ expect(props.option).toBe(timeSeriesAreaChart.vm.chartOptions);
+ expect(props.formatTooltipText).toBe(timeSeriesAreaChart.vm.formatTooltipText);
+ expect(props.thresholds).toBe(timeSeriesAreaChart.vm.thresholds);
+ });
+
+ it('recieves a tooltip title', done => {
+ const mockTitle = 'mockTitle';
+ timeSeriesAreaChart.vm.tooltip.title = mockTitle;
+
+ timeSeriesAreaChart.vm.$nextTick(() => {
+ expect(shallowWrapperContainsSlotText(glChart, 'tooltipTitle', mockTitle)).toBe(true);
+ done();
+ });
+ });
+
+ describe('when tooltip is showing deployment data', () => {
+ beforeEach(done => {
+ timeSeriesAreaChart.vm.tooltip.isDeployment = true;
+ timeSeriesAreaChart.vm.$nextTick(done);
+ });
+
+ it('uses deployment title', () => {
+ expect(shallowWrapperContainsSlotText(glChart, 'tooltipTitle', 'Deployed')).toBe(true);
+ });
+
+ it('renders clickable commit sha in tooltip content', done => {
+ timeSeriesAreaChart.vm.tooltip.sha = mockSha;
+ timeSeriesAreaChart.vm.tooltip.commitUrl = commitUrl;
+
+ timeSeriesAreaChart.vm.$nextTick(() => {
+ const commitLink = timeSeriesAreaChart.find(GlLink);
+
+ expect(shallowWrapperContainsSlotText(commitLink, 'default', mockSha)).toBe(true);
+ expect(commitLink.attributes('href')).toEqual(commitUrl);
+ done();
+ });
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/components/dashboard_spec.js
index 02c3f303912..f3ec7520c6f 100644
--- a/spec/javascripts/monitoring/dashboard_spec.js
+++ b/spec/javascripts/monitoring/components/dashboard_spec.js
@@ -13,7 +13,7 @@ import MonitoringMock, {
environmentData,
singleGroupResponse,
dashboardGitResponse,
-} from './mock_data';
+} from '../mock_data';
const localVue = createLocalVue();
const propsData = {
diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js
index 85e660d3925..17e7314e214 100644
--- a/spec/javascripts/monitoring/mock_data.js
+++ b/spec/javascripts/monitoring/mock_data.js
@@ -1,5 +1,7 @@
export const mockApiEndpoint = `${gl.TEST_HOST}/monitoring/mock`;
+export const mockProjectPath = '/frontend-fixtures/environments-project';
+
export const metricsGroupsAPIResponse = {
success: true,
data: [
@@ -902,7 +904,7 @@ export const metricsDashboardResponse = {
},
{
title: 'Memory Usage (Pod average)',
- type: 'area-chart',
+ type: 'line-chart',
y_label: 'Memory Used per Pod',
weight: 2,
metrics: [
diff --git a/spec/javascripts/notes/stores/getters_spec.js b/spec/javascripts/notes/stores/getters_spec.js
index 71dcba114a9..d69f469c7c7 100644
--- a/spec/javascripts/notes/stores/getters_spec.js
+++ b/spec/javascripts/notes/stores/getters_spec.js
@@ -14,6 +14,13 @@ import {
const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json';
+// Helper function to ensure that we're using the same schema across tests.
+const createDiscussionNeighborParams = (discussionId, diffOrder, step) => ({
+ discussionId,
+ diffOrder,
+ step,
+});
+
describe('Getters Notes Store', () => {
let state;
@@ -25,7 +32,6 @@ describe('Getters Notes Store', () => {
targetNoteHash: 'hash',
lastFetchedAt: 'timestamp',
isNotesFetched: false,
-
notesData: notesDataMock,
userData: userDataMock,
noteableData: noteableDataMock,
@@ -244,62 +250,104 @@ describe('Getters Notes Store', () => {
});
});
- describe('nextUnresolvedDiscussionId', () => {
- const localGetters = {
- unresolvedDiscussionsIdsOrdered: () => ['123', '456', '789'],
- };
+ describe('findUnresolvedDiscussionIdNeighbor', () => {
+ let localGetters;
+ beforeEach(() => {
+ localGetters = {
+ unresolvedDiscussionsIdsOrdered: () => ['123', '456', '789'],
+ };
+ });
- it('should return the ID of the discussion after the ID provided', () => {
- expect(getters.nextUnresolvedDiscussionId(state, localGetters)('123')).toBe('456');
- expect(getters.nextUnresolvedDiscussionId(state, localGetters)('456')).toBe('789');
- expect(getters.nextUnresolvedDiscussionId(state, localGetters)('789')).toBe('123');
+ [
+ { step: 1, id: '123', expected: '456' },
+ { step: 1, id: '456', expected: '789' },
+ { step: 1, id: '789', expected: '123' },
+ { step: -1, id: '123', expected: '789' },
+ { step: -1, id: '456', expected: '123' },
+ { step: -1, id: '789', expected: '456' },
+ ].forEach(({ step, id, expected }) => {
+ it(`with step ${step} and id ${id}, returns next value`, () => {
+ const params = createDiscussionNeighborParams(id, true, step);
+
+ expect(getters.findUnresolvedDiscussionIdNeighbor(state, localGetters)(params)).toBe(
+ expected,
+ );
+ });
});
- });
- describe('previousUnresolvedDiscussionId', () => {
- describe('with unresolved discussions', () => {
- const localGetters = {
- unresolvedDiscussionsIdsOrdered: () => ['123', '456', '789'],
- };
+ describe('with 1 unresolved discussion', () => {
+ beforeEach(() => {
+ localGetters = {
+ unresolvedDiscussionsIdsOrdered: () => ['123'],
+ };
+ });
+
+ [{ step: 1, id: '123', expected: '123' }, { step: -1, id: '123', expected: '123' }].forEach(
+ ({ step, id, expected }) => {
+ it(`with step ${step} and match, returns only value`, () => {
+ const params = createDiscussionNeighborParams(id, true, step);
- it('with bogus returns falsey', () => {
- expect(getters.previousUnresolvedDiscussionId(state, localGetters)('bogus')).toBe('456');
+ expect(getters.findUnresolvedDiscussionIdNeighbor(state, localGetters)(params)).toBe(
+ expected,
+ );
+ });
+ },
+ );
+
+ it('with no match, returns only value', () => {
+ const params = createDiscussionNeighborParams('bogus', true, 1);
+
+ expect(getters.findUnresolvedDiscussionIdNeighbor(state, localGetters)(params)).toBe('123');
});
+ });
- [
- { id: '123', expected: '789' },
- { id: '456', expected: '123' },
- { id: '789', expected: '456' },
- ].forEach(({ id, expected }) => {
- it(`with ${id}, returns previous value`, () => {
- expect(getters.previousUnresolvedDiscussionId(state, localGetters)(id)).toBe(expected);
+ describe('with 0 unresolved discussions', () => {
+ beforeEach(() => {
+ localGetters = {
+ unresolvedDiscussionsIdsOrdered: () => [],
+ };
+ });
+
+ [{ step: 1 }, { step: -1 }].forEach(({ step }) => {
+ it(`with step ${step}, returns undefined`, () => {
+ const params = createDiscussionNeighborParams('bogus', true, step);
+
+ expect(
+ getters.findUnresolvedDiscussionIdNeighbor(state, localGetters)(params),
+ ).toBeUndefined();
});
});
});
+ });
- describe('with 1 unresolved discussion', () => {
- const localGetters = {
- unresolvedDiscussionsIdsOrdered: () => ['123'],
- };
+ describe('findUnresolvedDiscussionIdNeighbor aliases', () => {
+ let neighbor;
+ let findUnresolvedDiscussionIdNeighbor;
+ let localGetters;
- it('with bogus returns id', () => {
- expect(getters.previousUnresolvedDiscussionId(state, localGetters)('bogus')).toBe('123');
- });
+ beforeEach(() => {
+ neighbor = {};
+ findUnresolvedDiscussionIdNeighbor = jasmine.createSpy().and.returnValue(neighbor);
+ localGetters = { findUnresolvedDiscussionIdNeighbor };
+ });
- it('with match, returns value', () => {
- expect(getters.previousUnresolvedDiscussionId(state, localGetters)('123')).toEqual('123');
+ describe('nextUnresolvedDiscussionId', () => {
+ it('should return result of find neighbor', () => {
+ const expectedParams = createDiscussionNeighborParams('123', true, 1);
+ const result = getters.nextUnresolvedDiscussionId(state, localGetters)('123', true);
+
+ expect(findUnresolvedDiscussionIdNeighbor).toHaveBeenCalledWith(expectedParams);
+ expect(result).toBe(neighbor);
});
});
- describe('with 0 unresolved discussions', () => {
- const localGetters = {
- unresolvedDiscussionsIdsOrdered: () => [],
- };
+ describe('previosuUnresolvedDiscussionId', () => {
+ it('should return result of find neighbor', () => {
+ const expectedParams = createDiscussionNeighborParams('123', true, -1);
+ const result = getters.previousUnresolvedDiscussionId(state, localGetters)('123', true);
- it('returns undefined', () => {
- expect(
- getters.previousUnresolvedDiscussionId(state, localGetters)('bogus'),
- ).toBeUndefined();
+ expect(findUnresolvedDiscussionIdNeighbor).toHaveBeenCalledWith(expectedParams);
+ expect(result).toBe(neighbor);
});
});
});
diff --git a/spec/javascripts/registry/components/app_spec.js b/spec/javascripts/registry/components/app_spec.js
index e7675669f7a..5ea3f85a247 100644
--- a/spec/javascripts/registry/components/app_spec.js
+++ b/spec/javascripts/registry/components/app_spec.js
@@ -84,12 +84,7 @@ describe('Registry List', () => {
it('should render empty message', done => {
setTimeout(() => {
- expect(
- vm.$el
- .querySelector('p')
- .textContent.trim()
- .replace(/[\r\n]+/g, ' '),
- ).toEqual(
+ expect(vm.$el.querySelector('.js-no-container-images-text').textContent).toEqual(
'With the Container Registry, every project can have its own space to store its Docker images. More Information',
);
done();
@@ -124,7 +119,9 @@ describe('Registry List', () => {
it('should render invalid characters error message', done => {
setTimeout(() => {
- expect(vm.$el.querySelector('.container-message')).not.toBe(null);
+ expect(vm.$el.querySelector('p')).not.toContain(
+ 'We are having trouble connecting to Docker, which could be due to an issue with your project name or path. More information',
+ );
done();
});
});
diff --git a/spec/javascripts/sidebar/assignee_title_spec.js b/spec/javascripts/sidebar/assignee_title_spec.js
index 509edba2036..7fff7c075d9 100644
--- a/spec/javascripts/sidebar/assignee_title_spec.js
+++ b/spec/javascripts/sidebar/assignee_title_spec.js
@@ -4,8 +4,10 @@ import AssigneeTitle from '~/sidebar/components/assignees/assignee_title.vue';
describe('AssigneeTitle component', () => {
let component;
let AssigneeTitleComponent;
+ let statsSpy;
beforeEach(() => {
+ statsSpy = spyOnDependency(AssigneeTitle, 'trackEvent');
AssigneeTitleComponent = Vue.extend(AssigneeTitle);
});
@@ -102,4 +104,16 @@ describe('AssigneeTitle component', () => {
expect(component.$el.querySelector('.edit-link')).not.toBeNull();
});
+
+ it('calls trackEvent when edit is clicked', () => {
+ component = new AssigneeTitleComponent({
+ propsData: {
+ numberOfAssignees: 0,
+ editable: true,
+ },
+ }).$mount();
+ component.$el.querySelector('.js-sidebar-dropdown-toggle').click();
+
+ expect(statsSpy).toHaveBeenCalled();
+ });
});
diff --git a/spec/javascripts/sidebar/assignees_spec.js b/spec/javascripts/sidebar/assignees_spec.js
index 4ae2141d5f0..a1df5389a38 100644
--- a/spec/javascripts/sidebar/assignees_spec.js
+++ b/spec/javascripts/sidebar/assignees_spec.js
@@ -94,115 +94,9 @@ describe('Assignee component', () => {
expect(assignee.querySelector('.author').innerText.trim()).toEqual(UsersMock.user.name);
});
-
- it('Shows one user with avatar, username and author name', () => {
- component = new AssigneeComponent({
- propsData: {
- rootPath: 'http://localhost:3000/',
- users: [UsersMock.user],
- editable: true,
- },
- }).$mount();
-
- expect(component.$el.querySelector('.author-link')).not.toBeNull();
- // The image
- expect(component.$el.querySelector('.author-link img').getAttribute('src')).toEqual(
- UsersMock.user.avatar,
- );
- // Author name
- expect(component.$el.querySelector('.author-link .author').innerText.trim()).toEqual(
- UsersMock.user.name,
- );
- // Username
- expect(component.$el.querySelector('.author-link .username').innerText.trim()).toEqual(
- `@${UsersMock.user.username}`,
- );
- });
-
- it('has the root url present in the assigneeUrl method', () => {
- component = new AssigneeComponent({
- propsData: {
- rootPath: 'http://localhost:3000/',
- users: [UsersMock.user],
- editable: true,
- },
- }).$mount();
-
- expect(component.assigneeUrl(UsersMock.user).indexOf('http://localhost:3000/')).not.toEqual(
- -1,
- );
- });
-
- it('has correct "cannot merge" tooltip when user cannot merge', () => {
- const user = Object.assign({}, UsersMock.user, { can_merge: false });
-
- component = new AssigneeComponent({
- propsData: {
- rootPath: 'http://localhost:3000/',
- users: [user],
- editable: true,
- issuableType: 'merge_request',
- },
- }).$mount();
-
- expect(component.mergeNotAllowedTooltipMessage).toEqual('Cannot merge');
- });
});
describe('Two or more assignees/users', () => {
- it('has correct "cannot merge" tooltip when one user can merge', () => {
- const users = UsersMockHelper.createNumberRandomUsers(3);
- users[0].can_merge = true;
- users[1].can_merge = false;
- users[2].can_merge = false;
-
- component = new AssigneeComponent({
- propsData: {
- rootPath: 'http://localhost:3000/',
- users,
- editable: true,
- issuableType: 'merge_request',
- },
- }).$mount();
-
- expect(component.mergeNotAllowedTooltipMessage).toEqual('1/3 can merge');
- });
-
- it('has correct "cannot merge" tooltip when no user can merge', () => {
- const users = UsersMockHelper.createNumberRandomUsers(2);
- users[0].can_merge = false;
- users[1].can_merge = false;
-
- component = new AssigneeComponent({
- propsData: {
- rootPath: 'http://localhost:3000/',
- users,
- editable: true,
- issuableType: 'merge_request',
- },
- }).$mount();
-
- expect(component.mergeNotAllowedTooltipMessage).toEqual('No one can merge');
- });
-
- it('has correct "cannot merge" tooltip when more than one user can merge', () => {
- const users = UsersMockHelper.createNumberRandomUsers(3);
- users[0].can_merge = false;
- users[1].can_merge = true;
- users[2].can_merge = true;
-
- component = new AssigneeComponent({
- propsData: {
- rootPath: 'http://localhost:3000/',
- users,
- editable: true,
- issuableType: 'merge_request',
- },
- }).$mount();
-
- expect(component.mergeNotAllowedTooltipMessage).toEqual('2/3 can merge');
- });
-
it('has no "cannot merge" tooltip when every user can merge', () => {
const users = UsersMockHelper.createNumberRandomUsers(2);
users[0].can_merge = true;
@@ -217,7 +111,7 @@ describe('Assignee component', () => {
},
}).$mount();
- expect(component.mergeNotAllowedTooltipMessage).toEqual(null);
+ expect(component.collapsedTooltipTitle).not.toContain('cannot merge');
});
it('displays two assignee icons when collapsed', () => {
@@ -295,8 +189,12 @@ describe('Assignee component', () => {
expect(component.$el.querySelector('.user-list-more')).toBe(null);
});
- it('sets tooltip container to body', () => {
- const users = UsersMockHelper.createNumberRandomUsers(2);
+ it('shows sorted assignee where "can merge" users are sorted first', () => {
+ const users = UsersMockHelper.createNumberRandomUsers(3);
+ users[0].can_merge = false;
+ users[1].can_merge = false;
+ users[2].can_merge = true;
+
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000',
@@ -305,98 +203,46 @@ describe('Assignee component', () => {
},
}).$mount();
- expect(component.$el.querySelector('.user-link').getAttribute('data-container')).toBe('body');
+ expect(component.sortedAssigness[0].can_merge).toBe(true);
});
- it('Shows the "show-less" assignees label', done => {
- const users = UsersMockHelper.createNumberRandomUsers(6);
+ it('passes the sorted assignees to the uncollapsed-assignee-list', () => {
+ const users = UsersMockHelper.createNumberRandomUsers(3);
+ users[0].can_merge = false;
+ users[1].can_merge = false;
+ users[2].can_merge = true;
+
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000',
users,
- editable: true,
+ editable: false,
},
}).$mount();
- expect(component.$el.querySelectorAll('.user-item').length).toEqual(
- component.defaultRenderCount,
- );
-
- expect(component.$el.querySelector('.user-list-more')).not.toBe(null);
- const usersLabelExpectation = users.length - component.defaultRenderCount;
+ const userItems = component.$el.querySelectorAll('.user-list .user-item a');
- expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).not.toBe(
- `+${usersLabelExpectation} more`,
- );
- component.toggleShowLess();
- Vue.nextTick(() => {
- expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe(
- '- show less',
- );
- done();
- });
+ expect(userItems.length).toBe(3);
+ expect(userItems[0].dataset.originalTitle).toBe(users[2].name);
});
- it('Shows the "show-less" when "n+ more " label is clicked', done => {
- const users = UsersMockHelper.createNumberRandomUsers(6);
- component = new AssigneeComponent({
- propsData: {
- rootPath: 'http://localhost:3000',
- users,
- editable: true,
- },
- }).$mount();
-
- component.$el.querySelector('.user-list-more .btn-link').click();
- Vue.nextTick(() => {
- expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe(
- '- show less',
- );
- done();
- });
- });
+ it('passes the sorted assignees to the collapsed-assignee-list', () => {
+ const users = UsersMockHelper.createNumberRandomUsers(3);
+ users[0].can_merge = false;
+ users[1].can_merge = false;
+ users[2].can_merge = true;
- it('gets the count of avatar via a computed property ', () => {
- const users = UsersMockHelper.createNumberRandomUsers(6);
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000',
users,
- editable: true,
+ editable: false,
},
}).$mount();
- expect(component.sidebarAvatarCounter).toEqual(`+${users.length - 1}`);
- });
+ const collapsedButton = component.$el.querySelector('.sidebar-collapsed-user button');
- describe('n+ more label', () => {
- beforeEach(() => {
- const users = UsersMockHelper.createNumberRandomUsers(6);
- component = new AssigneeComponent({
- propsData: {
- rootPath: 'http://localhost:3000',
- users,
- editable: true,
- },
- }).$mount();
- });
-
- it('shows "+1 more" label', () => {
- expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe(
- '+ 1 more',
- );
- });
-
- it('shows "show less" label', done => {
- component.toggleShowLess();
-
- Vue.nextTick(() => {
- expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe(
- '- show less',
- );
- done();
- });
- });
+ expect(collapsedButton.innerText.trim()).toBe(users[2].name);
});
});
});
diff --git a/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js b/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js
index 486a7241e33..ea9e5677bc5 100644
--- a/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js
+++ b/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js
@@ -4,8 +4,10 @@ import confidentialIssueSidebar from '~/sidebar/components/confidential/confiden
describe('Confidential Issue Sidebar Block', () => {
let vm1;
let vm2;
+ let statsSpy;
beforeEach(() => {
+ statsSpy = spyOnDependency(confidentialIssueSidebar, 'trackEvent');
const Component = Vue.extend(confidentialIssueSidebar);
const service = {
update: () => Promise.resolve(true),
@@ -67,4 +69,10 @@ describe('Confidential Issue Sidebar Block', () => {
done();
});
});
+
+ it('calls trackEvent when "Edit" is clicked', () => {
+ vm1.$el.querySelector('.confidential-edit').click();
+
+ expect(statsSpy).toHaveBeenCalled();
+ });
});
diff --git a/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js b/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js
index ca882032bdf..2d930428230 100644
--- a/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js
+++ b/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js
@@ -4,8 +4,10 @@ import lockIssueSidebar from '~/sidebar/components/lock/lock_issue_sidebar.vue';
describe('LockIssueSidebar', () => {
let vm1;
let vm2;
+ let statsSpy;
beforeEach(() => {
+ statsSpy = spyOnDependency(lockIssueSidebar, 'trackEvent');
const Component = Vue.extend(lockIssueSidebar);
const mediator = {
@@ -59,6 +61,12 @@ describe('LockIssueSidebar', () => {
});
});
+ it('calls trackEvent when "Edit" is clicked', () => {
+ vm1.$el.querySelector('.lock-edit').click();
+
+ expect(statsSpy).toHaveBeenCalled();
+ });
+
it('displays the edit form when opened from collapsed state', done => {
expect(vm1.isLockDialogOpen).toBe(false);
diff --git a/spec/javascripts/sidebar/subscriptions_spec.js b/spec/javascripts/sidebar/subscriptions_spec.js
index 32728e58b06..2efa13f3fe8 100644
--- a/spec/javascripts/sidebar/subscriptions_spec.js
+++ b/spec/javascripts/sidebar/subscriptions_spec.js
@@ -6,8 +6,10 @@ import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Subscriptions', function() {
let vm;
let Subscriptions;
+ let statsSpy;
beforeEach(() => {
+ statsSpy = spyOnDependency(subscriptions, 'trackEvent');
Subscriptions = Vue.extend(subscriptions);
});
@@ -58,6 +60,13 @@ describe('Subscriptions', function() {
expect(vm.$emit).toHaveBeenCalledWith('toggleSubscription', jasmine.any(Object));
});
+ it('calls trackEvent when toggled', () => {
+ vm = mountComponent(Subscriptions, { subscribed: true });
+ vm.toggleSubscription();
+
+ expect(statsSpy).toHaveBeenCalled();
+ });
+
it('onClickCollapsedIcon method emits `toggleSidebar` event on component', () => {
vm = mountComponent(Subscriptions, { subscribed: true });
spyOn(vm, '$emit');
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js
index 212519743aa..7216ad00cc1 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js
@@ -83,6 +83,24 @@ describe('Merge request widget rebase component', () => {
expect(text).toContain('foo');
expect(text.replace(/\s\s+/g, ' ')).toContain('to allow this merge request to be merged.');
});
+
+ it('should render the correct target branch name', () => {
+ const targetBranch = 'fake-branch-to-test-with';
+ vm = mountComponent(Component, {
+ mr: {
+ rebaseInProgress: false,
+ canPushToSourceBranch: false,
+ targetBranch,
+ },
+ service: {},
+ });
+
+ const elem = vm.$el.querySelector('.rebase-state-find-class-convention span');
+
+ expect(elem.innerHTML).toContain(
+ `Fast-forward merge is not possible. Rebase the source branch onto <span class="label-branch">${targetBranch}</span> to allow this merge request to be merged.`,
+ );
+ });
});
describe('methods', () => {
diff --git a/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js b/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js
index e27a506f426..e2cd0f084fd 100644
--- a/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js
+++ b/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js
@@ -82,47 +82,5 @@ describe('MergeRequestStore', () => {
expect(store.isNothingToMergeState).toEqual(false);
});
});
-
- describe('mergePipelinesEnabled', () => {
- it('should set mergePipelinesEnabled = true when merge_pipelines_enabled is true', () => {
- store.setData({ ...mockData, merge_pipelines_enabled: true });
-
- expect(store.mergePipelinesEnabled).toBe(true);
- });
-
- it('should set mergePipelinesEnabled = false when merge_pipelines_enabled is not provided', () => {
- store.setData({ ...mockData, merge_pipelines_enabled: undefined });
-
- expect(store.mergePipelinesEnabled).toBe(false);
- });
- });
-
- describe('mergeTrainsCount', () => {
- it('should set mergeTrainsCount when merge_trains_count is provided', () => {
- store.setData({ ...mockData, merge_trains_count: 3 });
-
- expect(store.mergeTrainsCount).toBe(3);
- });
-
- it('should set mergeTrainsCount = 0 when merge_trains_count is not provided', () => {
- store.setData({ ...mockData, merge_trains_count: undefined });
-
- expect(store.mergeTrainsCount).toBe(0);
- });
- });
-
- describe('mergeTrainIndex', () => {
- it('should set mergeTrainIndex when merge_train_index is provided', () => {
- store.setData({ ...mockData, merge_train_index: 3 });
-
- expect(store.mergeTrainIndex).toBe(3);
- });
-
- it('should not set mergeTrainIndex when merge_train_index is not provided', () => {
- store.setData({ ...mockData, merge_train_index: undefined });
-
- expect(store.mergeTrainIndex).toBeUndefined();
- });
- });
});
});
diff --git a/spec/lib/gitlab/action_rate_limiter_spec.rb b/spec/lib/gitlab/action_rate_limiter_spec.rb
index 8dbad32dfb4..8b510a475d2 100644
--- a/spec/lib/gitlab/action_rate_limiter_spec.rb
+++ b/spec/lib/gitlab/action_rate_limiter_spec.rb
@@ -74,9 +74,9 @@ describe Gitlab::ActionRateLimiter, :clean_gitlab_redis_cache do
{
message: 'Action_Rate_Limiter_Request',
env: type,
- ip: '127.0.0.1',
+ remote_ip: '127.0.0.1',
request_method: 'GET',
- fullpath: fullpath
+ path: fullpath
}
end
diff --git a/spec/lib/gitlab/ci/build/policy/variables_spec.rb b/spec/lib/gitlab/ci/build/policy/variables_spec.rb
index f712f47a558..7140c14facb 100644
--- a/spec/lib/gitlab/ci/build/policy/variables_spec.rb
+++ b/spec/lib/gitlab/ci/build/policy/variables_spec.rb
@@ -13,7 +13,12 @@ describe Gitlab::Ci::Build::Policy::Variables do
build(:ci_build, pipeline: pipeline, project: project, ref: 'master')
end
- let(:seed) { double('build seed', to_resource: ci_build) }
+ let(:seed) do
+ double('build seed',
+ to_resource: ci_build,
+ scoped_variables_hash: ci_build.scoped_variables_hash
+ )
+ end
before do
pipeline.variables.build(key: 'CI_PROJECT_NAME', value: '')
@@ -83,7 +88,12 @@ describe Gitlab::Ci::Build::Policy::Variables do
build(:ci_bridge, pipeline: pipeline, project: project, ref: 'master')
end
- let(:seed) { double('bridge seed', to_resource: bridge) }
+ let(:seed) do
+ double('bridge seed',
+ to_resource: bridge,
+ scoped_variables_hash: ci_build.scoped_variables_hash
+ )
+ end
it 'is satisfied by a matching expression for a bridge job' do
policy = described_class.new(['$MY_VARIABLE'])
diff --git a/spec/lib/gitlab/ci/build/rules/rule_spec.rb b/spec/lib/gitlab/ci/build/rules/rule_spec.rb
new file mode 100644
index 00000000000..99852bd4228
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/rules/rule_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Build::Rules::Rule do
+ let(:seed) do
+ double('build seed',
+ to_resource: ci_build,
+ scoped_variables_hash: ci_build.scoped_variables_hash
+ )
+ end
+
+ let(:pipeline) { create(:ci_pipeline) }
+ let(:ci_build) { build(:ci_build, pipeline: pipeline) }
+ let(:rule) { described_class.new(rule_hash) }
+
+ describe '#matches?' do
+ subject { rule.matches?(pipeline, seed) }
+
+ context 'with one matching clause' do
+ let(:rule_hash) do
+ { if: '$VAR == null', when: 'always' }
+ end
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'with two matching clauses' do
+ let(:rule_hash) do
+ { if: '$VAR == null', changes: '**/*', when: 'always' }
+ end
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'with a matching and non-matching clause' do
+ let(:rule_hash) do
+ { if: '$VAR != null', changes: '$VAR == null', when: 'always' }
+ end
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'with two non-matching clauses' do
+ let(:rule_hash) do
+ { if: '$VAR != null', changes: 'README', when: 'always' }
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/build/rules_spec.rb b/spec/lib/gitlab/ci/build/rules_spec.rb
new file mode 100644
index 00000000000..d7793ebc806
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/rules_spec.rb
@@ -0,0 +1,168 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Build::Rules do
+ let(:pipeline) { create(:ci_pipeline) }
+ let(:ci_build) { build(:ci_build, pipeline: pipeline) }
+
+ let(:seed) do
+ double('build seed',
+ to_resource: ci_build,
+ scoped_variables_hash: ci_build.scoped_variables_hash
+ )
+ end
+
+ let(:rules) { described_class.new(rule_list) }
+
+ describe '.new' do
+ let(:rules_ivar) { rules.instance_variable_get :@rule_list }
+ let(:default_when) { rules.instance_variable_get :@default_when }
+
+ context 'with no rules' do
+ let(:rule_list) { [] }
+
+ it 'sets @rule_list to an empty array' do
+ expect(rules_ivar).to eq([])
+ end
+
+ it 'sets @default_when to "on_success"' do
+ expect(default_when).to eq('on_success')
+ end
+ end
+
+ context 'with one rule' do
+ let(:rule_list) { [{ if: '$VAR == null', when: 'always' }] }
+
+ it 'sets @rule_list to an array of a single rule' do
+ expect(rules_ivar).to be_an(Array)
+ end
+
+ it 'sets @default_when to "on_success"' do
+ expect(default_when).to eq('on_success')
+ end
+ end
+
+ context 'with multiple rules' do
+ let(:rule_list) do
+ [
+ { if: '$VAR == null', when: 'always' },
+ { if: '$VAR == null', when: 'always' }
+ ]
+ end
+
+ it 'sets @rule_list to an array of a single rule' do
+ expect(rules_ivar).to be_an(Array)
+ end
+
+ it 'sets @default_when to "on_success"' do
+ expect(default_when).to eq('on_success')
+ end
+ end
+
+ context 'with a specified default when:' do
+ let(:rule_list) { [{ if: '$VAR == null', when: 'always' }] }
+ let(:rules) { described_class.new(rule_list, 'manual') }
+
+ it 'sets @rule_list to an array of a single rule' do
+ expect(rules_ivar).to be_an(Array)
+ end
+
+ it 'sets @default_when to "manual"' do
+ expect(default_when).to eq('manual')
+ end
+ end
+ end
+
+ describe '#evaluate' do
+ subject { rules.evaluate(pipeline, seed) }
+
+ context 'with nil rules' do
+ let(:rule_list) { nil }
+
+ it { is_expected.to eq(described_class::Result.new('on_success')) }
+
+ context 'and when:manual set as the default' do
+ let(:rules) { described_class.new(rule_list, 'manual') }
+
+ it { is_expected.to eq(described_class::Result.new('manual')) }
+ end
+ end
+
+ context 'with no rules' do
+ let(:rule_list) { [] }
+
+ it { is_expected.to eq(described_class::Result.new('never')) }
+
+ context 'and when:manual set as the default' do
+ let(:rules) { described_class.new(rule_list, 'manual') }
+
+ it { is_expected.to eq(described_class::Result.new('never')) }
+ end
+ end
+
+ context 'with one rule without any clauses' do
+ let(:rule_list) { [{ when: 'manual' }] }
+
+ it { is_expected.to eq(described_class::Result.new('manual')) }
+ end
+
+ context 'with one matching rule' do
+ let(:rule_list) { [{ if: '$VAR == null', when: 'always' }] }
+
+ it { is_expected.to eq(described_class::Result.new('always')) }
+ end
+
+ context 'with two matching rules' do
+ let(:rule_list) do
+ [
+ { if: '$VAR == null', when: 'delayed', start_in: '1 day' },
+ { if: '$VAR == null', when: 'always' }
+ ]
+ end
+
+ it 'returns the value of the first matched rule in the list' do
+ expect(subject).to eq(described_class::Result.new('delayed', '1 day'))
+ end
+ end
+
+ context 'with a non-matching and matching rule' do
+ let(:rule_list) do
+ [
+ { if: '$VAR =! null', when: 'delayed', start_in: '1 day' },
+ { if: '$VAR == null', when: 'always' }
+ ]
+ end
+
+ it { is_expected.to eq(described_class::Result.new('always')) }
+ end
+
+ context 'with a matching and non-matching rule' do
+ let(:rule_list) do
+ [
+ { if: '$VAR == null', when: 'delayed', start_in: '1 day' },
+ { if: '$VAR != null', when: 'always' }
+ ]
+ end
+
+ it { is_expected.to eq(described_class::Result.new('delayed', '1 day')) }
+ end
+
+ context 'with non-matching rules' do
+ let(:rule_list) do
+ [
+ { if: '$VAR != null', when: 'delayed', start_in: '1 day' },
+ { if: '$VAR != null', when: 'always' }
+ ]
+ end
+
+ it { is_expected.to eq(described_class::Result.new('never')) }
+
+ context 'and when:manual set as the default' do
+ let(:rules) { described_class.new(rule_list, 'manual') }
+
+ it 'does not return the default when:' do
+ expect(subject).to eq(described_class::Result.new('never'))
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb
index 415ade7a096..1853efde350 100644
--- a/spec/lib/gitlab/ci/config/entry/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb
@@ -11,7 +11,7 @@ describe Gitlab::Ci::Config::Entry::Job do
let(:result) do
%i[before_script script stage type after_script cache
- image services only except variables artifacts
+ image services only except rules variables artifacts
environment coverage retry]
end
@@ -201,6 +201,21 @@ describe Gitlab::Ci::Config::Entry::Job do
expect(entry.errors).to include 'job parallel must be an integer'
end
end
+
+ context 'when it uses both "when:" and "rules:"' do
+ let(:config) do
+ {
+ script: 'echo',
+ when: 'on_failure',
+ rules: [{ if: '$VARIABLE', when: 'on_success' }]
+ }
+ end
+
+ it 'returns an error about when: being combined with rules' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'job config key may not be used with `rules`: when'
+ end
+ end
end
context 'when delayed job' do
@@ -240,6 +255,100 @@ describe Gitlab::Ci::Config::Entry::Job do
end
end
+ context 'when only: is used with rules:' do
+ let(:config) { { only: ['merge_requests'], rules: [{ if: '$THIS' }] } }
+
+ it 'returns error about mixing only: with rules:' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include /may not be used with `rules`/
+ end
+
+ context 'and only: is blank' do
+ let(:config) { { only: nil, rules: [{ if: '$THIS' }] } }
+
+ it 'returns error about mixing only: with rules:' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include /may not be used with `rules`/
+ end
+ end
+
+ context 'and rules: is blank' do
+ let(:config) { { only: ['merge_requests'], rules: nil } }
+
+ it 'returns error about mixing only: with rules:' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include /may not be used with `rules`/
+ end
+ end
+ end
+
+ context 'when except: is used with rules:' do
+ let(:config) { { except: { refs: %w[master] }, rules: [{ if: '$THIS' }] } }
+
+ it 'returns error about mixing except: with rules:' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include /may not be used with `rules`/
+ end
+
+ context 'and except: is blank' do
+ let(:config) { { except: nil, rules: [{ if: '$THIS' }] } }
+
+ it 'returns error about mixing except: with rules:' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include /may not be used with `rules`/
+ end
+ end
+
+ context 'and rules: is blank' do
+ let(:config) { { except: { refs: %w[master] }, rules: nil } }
+
+ it 'returns error about mixing except: with rules:' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include /may not be used with `rules`/
+ end
+ end
+ end
+
+ context 'when only: and except: are both used with rules:' do
+ let(:config) do
+ {
+ only: %w[merge_requests],
+ except: { refs: %w[master] },
+ rules: [{ if: '$THIS' }]
+ }
+ end
+
+ it 'returns errors about mixing both only: and except: with rules:' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include /may not be used with `rules`/
+ expect(entry.errors).to include /may not be used with `rules`/
+ end
+
+ context 'when only: and except: as both blank' do
+ let(:config) do
+ { only: nil, except: nil, rules: [{ if: '$THIS' }] }
+ end
+
+ it 'returns errors about mixing both only: and except: with rules:' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include /may not be used with `rules`/
+ expect(entry.errors).to include /may not be used with `rules`/
+ end
+ end
+
+ context 'when rules: is blank' do
+ let(:config) do
+ { only: %w[merge_requests], except: { refs: %w[master] }, rules: nil }
+ end
+
+ it 'returns errors about mixing both only: and except: with rules:' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include /may not be used with `rules`/
+ expect(entry.errors).to include /may not be used with `rules`/
+ end
+ end
+ end
+
context 'when start_in specified without delayed specification' do
let(:config) { { start_in: '1 day' } }
diff --git a/spec/lib/gitlab/ci/config/entry/policy_spec.rb b/spec/lib/gitlab/ci/config/entry/policy_spec.rb
index 266a27c1e47..a606eb303e7 100644
--- a/spec/lib/gitlab/ci/config/entry/policy_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/policy_spec.rb
@@ -51,8 +51,6 @@ describe Gitlab::Ci::Config::Entry::Policy do
let(:config) { ['/^(?!master).+/'] }
- subject { described_class.new([regexp]) }
-
context 'when allow_unsafe_ruby_regexp is disabled' do
before do
stub_feature_flags(allow_unsafe_ruby_regexp: false)
@@ -113,8 +111,6 @@ describe Gitlab::Ci::Config::Entry::Policy do
let(:config) { { refs: ['/^(?!master).+/'] } }
- subject { described_class.new([regexp]) }
-
context 'when allow_unsafe_ruby_regexp is disabled' do
before do
stub_feature_flags(allow_unsafe_ruby_regexp: false)
@@ -204,6 +200,14 @@ describe Gitlab::Ci::Config::Entry::Policy do
end
context 'when changes policy is invalid' do
+ let(:config) { { changes: 'some/*' } }
+
+ it 'returns errors' do
+ expect(entry.errors).to include /changes should be an array of strings/
+ end
+ end
+
+ context 'when changes policy is invalid' do
let(:config) { { changes: [1, 2] } }
it 'returns errors' do
diff --git a/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb
new file mode 100644
index 00000000000..c25344ec1a4
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb
@@ -0,0 +1,208 @@
+require 'fast_spec_helper'
+require 'chronic_duration'
+require 'support/helpers/stub_feature_flags'
+require_dependency 'active_model'
+
+describe Gitlab::Ci::Config::Entry::Rules::Rule do
+ let(:entry) { described_class.new(config) }
+
+ describe '.new' do
+ subject { entry }
+
+ context 'with a when: value but no clauses' do
+ let(:config) { { when: 'manual' } }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'when specifying an if: clause' do
+ let(:config) { { if: '$THIS || $THAT', when: 'manual' } }
+
+ it { is_expected.to be_valid }
+
+ describe '#when' do
+ subject { entry.when }
+
+ it { is_expected.to eq('manual') }
+ end
+ end
+
+ context 'using a list of multiple expressions' do
+ let(:config) { { if: ['$MY_VAR == "this"', '$YOUR_VAR == "that"'] } }
+
+ it { is_expected.not_to be_valid }
+
+ it 'reports an error about invalid format' do
+ expect(subject.errors).to include(/invalid expression syntax/)
+ end
+ end
+
+ context 'when specifying an invalid if: clause expression' do
+ let(:config) { { if: ['$MY_VAR =='] } }
+
+ it { is_expected.not_to be_valid }
+
+ it 'reports an error about invalid statement' do
+ expect(subject.errors).to include(/invalid expression syntax/)
+ end
+ end
+
+ context 'when specifying an if: clause expression with an invalid token' do
+ let(:config) { { if: ['$MY_VAR == 123'] } }
+
+ it { is_expected.not_to be_valid }
+
+ it 'reports an error about invalid statement' do
+ expect(subject.errors).to include(/invalid expression syntax/)
+ end
+ end
+
+ context 'when using invalid regex in an if: clause' do
+ let(:config) { { if: ['$MY_VAR =~ /some ( thing/'] } }
+
+ it 'reports an error about invalid expression' do
+ expect(subject.errors).to include(/invalid expression syntax/)
+ end
+ end
+
+ context 'when using an if: clause with lookahead regex character "?"' do
+ let(:config) { { if: '$CI_COMMIT_REF =~ /^(?!master).+/' } }
+
+ context 'when allow_unsafe_ruby_regexp is disabled' do
+ it { is_expected.not_to be_valid }
+
+ it 'reports an error about invalid expression syntax' do
+ expect(subject.errors).to include(/invalid expression syntax/)
+ end
+ end
+ end
+
+ context 'when using a changes: clause' do
+ let(:config) { { changes: %w[app/ lib/ spec/ other/* paths/**/*.rb] } }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'when using a string as an invalid changes: clause' do
+ let(:config) { { changes: 'a regular string' } }
+
+ it { is_expected.not_to be_valid }
+
+ it 'reports an error about invalid policy' do
+ expect(subject.errors).to include(/should be an array of strings/)
+ end
+ end
+
+ context 'when using a list as an invalid changes: clause' do
+ let(:config) { { changes: [1, 2] } }
+
+ it { is_expected.not_to be_valid }
+
+ it 'returns errors' do
+ expect(subject.errors).to include(/changes should be an array of strings/)
+ end
+ end
+
+ context 'specifying a delayed job' do
+ let(:config) { { if: '$THIS || $THAT', when: 'delayed', start_in: '15 minutes' } }
+
+ it { is_expected.to be_valid }
+
+ it 'sets attributes for the job delay' do
+ expect(entry.when).to eq('delayed')
+ expect(entry.start_in).to eq('15 minutes')
+ end
+
+ context 'without a when: key' do
+ let(:config) { { if: '$THIS || $THAT', start_in: '15 minutes' } }
+
+ it { is_expected.not_to be_valid }
+
+ it 'returns an error about the disallowed key' do
+ expect(entry.errors).to include(/disallowed keys: start_in/)
+ end
+ end
+
+ context 'without a start_in: key' do
+ let(:config) { { if: '$THIS || $THAT', when: 'delayed' } }
+
+ it { is_expected.not_to be_valid }
+
+ it 'returns an error about tstart_in being blank' do
+ expect(entry.errors).to include(/start in can't be blank/)
+ end
+ end
+ end
+
+ context 'when specifying unknown policy' do
+ let(:config) { { invalid: :something } }
+
+ it { is_expected.not_to be_valid }
+
+ it 'returns error about invalid key' do
+ expect(entry.errors).to include(/unknown keys: invalid/)
+ end
+ end
+
+ context 'when clause is empty' do
+ let(:config) { {} }
+
+ it { is_expected.not_to be_valid }
+
+ it 'is not a valid configuration' do
+ expect(entry.errors).to include(/can't be blank/)
+ end
+ end
+
+ context 'when policy strategy does not match' do
+ let(:config) { 'string strategy' }
+
+ it { is_expected.not_to be_valid }
+
+ it 'returns information about errors' do
+ expect(entry.errors)
+ .to include(/should be a hash/)
+ end
+ end
+ end
+
+ describe '#value' do
+ subject { entry.value }
+
+ context 'when specifying an if: clause' do
+ let(:config) { { if: '$THIS || $THAT', when: 'manual' } }
+
+ it 'stores the expression as "if"' do
+ expect(subject).to eq(if: '$THIS || $THAT', when: 'manual')
+ end
+ end
+
+ context 'when using a changes: clause' do
+ let(:config) { { changes: %w[app/ lib/ spec/ other/* paths/**/*.rb] } }
+
+ it { is_expected.to eq(config) }
+ end
+
+ context 'when default value has been provided' do
+ let(:config) { { changes: %w[app/**/*.rb] } }
+
+ before do
+ entry.default = { changes: %w[**/*] }
+ end
+
+ it 'does not set a default value' do
+ expect(entry.default).to eq(nil)
+ end
+
+ it 'does not add to provided configuration' do
+ expect(entry.value).to eq(config)
+ end
+ end
+ end
+
+ describe '.default' do
+ it 'does not have default value' do
+ expect(described_class.default).to be_nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/rules_spec.rb b/spec/lib/gitlab/ci/config/entry/rules_spec.rb
new file mode 100644
index 00000000000..291e7373daf
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/rules_spec.rb
@@ -0,0 +1,135 @@
+require 'fast_spec_helper'
+require 'support/helpers/stub_feature_flags'
+require_dependency 'active_model'
+
+describe Gitlab::Ci::Config::Entry::Rules do
+ let(:entry) { described_class.new(config) }
+
+ describe '.new' do
+ subject { entry }
+
+ context 'with a list of rule rule' do
+ let(:config) do
+ [{ if: '$THIS == "that"', when: 'never' }]
+ end
+
+ it { is_expected.to be_a(described_class) }
+ it { is_expected.to be_valid }
+
+ context 'after #compose!' do
+ before do
+ subject.compose!
+ end
+
+ it { is_expected.to be_valid }
+ end
+ end
+
+ context 'with a list of two rules' do
+ let(:config) do
+ [
+ { if: '$THIS == "that"', when: 'always' },
+ { if: '$SKIP', when: 'never' }
+ ]
+ end
+
+ it { is_expected.to be_a(described_class) }
+ it { is_expected.to be_valid }
+
+ context 'after #compose!' do
+ before do
+ subject.compose!
+ end
+
+ it { is_expected.to be_valid }
+ end
+ end
+
+ context 'with a single rule object' do
+ let(:config) do
+ { if: '$SKIP', when: 'never' }
+ end
+
+ it { is_expected.not_to be_valid }
+ end
+
+ context 'with an invalid boolean when:' do
+ let(:config) do
+ [{ if: '$THIS == "that"', when: false }]
+ end
+
+ it { is_expected.to be_a(described_class) }
+ it { is_expected.to be_valid }
+
+ context 'after #compose!' do
+ before do
+ subject.compose!
+ end
+
+ it { is_expected.not_to be_valid }
+
+ it 'returns an error about invalid when:' do
+ expect(subject.errors).to include(/when unknown value: false/)
+ end
+ end
+ end
+
+ context 'with an invalid string when:' do
+ let(:config) do
+ [{ if: '$THIS == "that"', when: 'explode' }]
+ end
+
+ it { is_expected.to be_a(described_class) }
+ it { is_expected.to be_valid }
+
+ context 'after #compose!' do
+ before do
+ subject.compose!
+ end
+
+ it { is_expected.not_to be_valid }
+
+ it 'returns an error about invalid when:' do
+ expect(subject.errors).to include(/when unknown value: explode/)
+ end
+ end
+ end
+ end
+
+ describe '#value' do
+ subject { entry.value }
+
+ context 'with a list of rule rule' do
+ let(:config) do
+ [{ if: '$THIS == "that"', when: 'never' }]
+ end
+
+ it { is_expected.to eq(config) }
+ end
+
+ context 'with a list of two rules' do
+ let(:config) do
+ [
+ { if: '$THIS == "that"', when: 'always' },
+ { if: '$SKIP', when: 'never' }
+ ]
+ end
+
+ it { is_expected.to eq(config) }
+ end
+
+ context 'with a single rule object' do
+ let(:config) do
+ { if: '$SKIP', when: 'never' }
+ end
+
+ it { is_expected.to eq(config) }
+ end
+ end
+
+ describe '.default' do
+ it 'does not have default policy' do
+ expect(described_class.default).to be_nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb
index 4e4f1bf6ad3..a527783ffac 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb
@@ -69,6 +69,34 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::Matches do
it { is_expected.to eq(false) }
end
+ context 'when right is nil' do
+ let(:left_value) { 'my-awesome-string' }
+ let(:right_value) { nil }
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when left and right are nil' do
+ let(:left_value) { nil }
+ let(:right_value) { nil }
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when left is an empty string' do
+ let(:left_value) { '' }
+ let(:right_value) { Gitlab::UntrustedRegexp.new('pattern') }
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when left and right are empty strings' do
+ let(:left_value) { '' }
+ let(:right_value) { Gitlab::UntrustedRegexp.new('') }
+
+ it { is_expected.to eq(true) }
+ end
+
context 'when left is a multiline string and matches right' do
let(:left_value) do
<<~TEXT
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb
index 6b81008ffb1..fb4238ecaf3 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb
@@ -69,6 +69,34 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::NotMatches do
it { is_expected.to eq(true) }
end
+ context 'when right is nil' do
+ let(:left_value) { 'my-awesome-string' }
+ let(:right_value) { nil }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when left and right are nil' do
+ let(:left_value) { nil }
+ let(:right_value) { nil }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when left is an empty string' do
+ let(:left_value) { '' }
+ let(:right_value) { Gitlab::UntrustedRegexp.new('pattern') }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when left and right are empty strings' do
+ let(:left_value) { '' }
+ let(:right_value) { Gitlab::UntrustedRegexp.new('') }
+
+ it { is_expected.to eq(false) }
+ end
+
context 'when left is a multiline string and matches right' do
let(:left_value) do
<<~TEXT
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
index 1a9350d68bd..89431b80be3 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
@@ -15,6 +15,60 @@ describe Gitlab::Ci::Pipeline::Seed::Build do
it { is_expected.to be_a(Hash) }
it { is_expected.to include(:name, :project, :ref) }
+
+ context 'with job:when' do
+ let(:attributes) { { name: 'rspec', ref: 'master', when: 'on_failure' } }
+
+ it { is_expected.to include(when: 'on_failure') }
+ end
+
+ context 'with job:when:delayed' do
+ let(:attributes) { { name: 'rspec', ref: 'master', when: 'delayed', start_in: '3 hours' } }
+
+ it { is_expected.to include(when: 'delayed', start_in: '3 hours') }
+ end
+
+ context 'with job:rules:[when:]' do
+ context 'is matched' do
+ let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR == null', when: 'always' }] } }
+
+ it { is_expected.to include(when: 'always') }
+ end
+
+ context 'is not matched' do
+ let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR != null', when: 'always' }] } }
+
+ it { is_expected.to include(when: 'never') }
+ end
+ end
+
+ context 'with job:rules:[when:delayed]' do
+ context 'is matched' do
+ let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR == null', when: 'delayed', start_in: '3 hours' }] } }
+
+ it { is_expected.to include(when: 'delayed', start_in: '3 hours') }
+ end
+
+ context 'is not matched' do
+ let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR != null', when: 'delayed', start_in: '3 hours' }] } }
+
+ it { is_expected.to include(when: 'never') }
+ end
+ end
+
+ context 'with job:rules but no explicit when:' do
+ context 'is matched' do
+ let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR == null' }] } }
+
+ it { is_expected.to include(when: 'on_success') }
+ end
+
+ context 'is not matched' do
+ let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR != null' }] } }
+
+ it { is_expected.to include(when: 'never') }
+ end
+ end
end
describe '#bridge?' do
@@ -366,9 +420,25 @@ describe Gitlab::Ci::Pipeline::Seed::Build do
it { is_expected.not_to be_included }
end
+
+ context 'when using both only and except policies' do
+ let(:attributes) do
+ {
+ name: 'rspec',
+ only: {
+ refs: ["branches@#{pipeline.project_full_path}"]
+ },
+ except: {
+ refs: ["branches@#{pipeline.project_full_path}"]
+ }
+ }
+ end
+
+ it { is_expected.not_to be_included }
+ end
end
- context 'when repository path does not matches' do
+ context 'when repository path does not match' do
context 'when using only' do
let(:attributes) do
{ name: 'rspec', only: { refs: %w[branches@fork] } }
@@ -397,6 +467,215 @@ describe Gitlab::Ci::Pipeline::Seed::Build do
it { is_expected.not_to be_included }
end
end
+
+ context 'using rules:' do
+ using RSpec::Parameterized
+
+ let(:attributes) { { name: 'rspec', rules: rule_set } }
+
+ context 'with a matching if: rule' do
+ context 'with an explicit `when: never`' do
+ where(:rule_set) do
+ [
+ [[{ if: '$VARIABLE == null', when: 'never' }]],
+ [[{ if: '$VARIABLE == null', when: 'never' }, { if: '$VARIABLE == null', when: 'always' }]],
+ [[{ if: '$VARIABLE != "the wrong value"', when: 'never' }, { if: '$VARIABLE == null', when: 'always' }]]
+ ]
+ end
+
+ with_them do
+ it { is_expected.not_to be_included }
+
+ it 'correctly populates when:' do
+ expect(seed_build.attributes).to include(when: 'never')
+ end
+ end
+ end
+
+ context 'with an explicit `when: always`' do
+ where(:rule_set) do
+ [
+ [[{ if: '$VARIABLE == null', when: 'always' }]],
+ [[{ if: '$VARIABLE == null', when: 'always' }, { if: '$VARIABLE == null', when: 'never' }]],
+ [[{ if: '$VARIABLE != "the wrong value"', when: 'always' }, { if: '$VARIABLE == null', when: 'never' }]]
+ ]
+ end
+
+ with_them do
+ it { is_expected.to be_included }
+
+ it 'correctly populates when:' do
+ expect(seed_build.attributes).to include(when: 'always')
+ end
+ end
+ end
+
+ context 'with an explicit `when: on_failure`' do
+ where(:rule_set) do
+ [
+ [[{ if: '$CI_JOB_NAME == "rspec" && $VAR == null', when: 'on_failure' }]],
+ [[{ if: '$VARIABLE != null', when: 'delayed', start_in: '1 day' }, { if: '$CI_JOB_NAME == "rspec"', when: 'on_failure' }]],
+ [[{ if: '$VARIABLE == "the wrong value"', when: 'delayed', start_in: '1 day' }, { if: '$CI_BUILD_NAME == "rspec"', when: 'on_failure' }]]
+ ]
+ end
+
+ with_them do
+ it { is_expected.to be_included }
+
+ it 'correctly populates when:' do
+ expect(seed_build.attributes).to include(when: 'on_failure')
+ end
+ end
+ end
+
+ context 'with an explicit `when: delayed`' do
+ where(:rule_set) do
+ [
+ [[{ if: '$VARIABLE == null', when: 'delayed', start_in: '1 day' }]],
+ [[{ if: '$VARIABLE == null', when: 'delayed', start_in: '1 day' }, { if: '$VARIABLE == null', when: 'never' }]],
+ [[{ if: '$VARIABLE != "the wrong value"', when: 'delayed', start_in: '1 day' }, { if: '$VARIABLE == null', when: 'never' }]]
+ ]
+ end
+
+ with_them do
+ it { is_expected.to be_included }
+
+ it 'correctly populates when:' do
+ expect(seed_build.attributes).to include(when: 'delayed', start_in: '1 day')
+ end
+ end
+ end
+
+ context 'without an explicit when: value' do
+ where(:rule_set) do
+ [
+ [[{ if: '$VARIABLE == null' }]],
+ [[{ if: '$VARIABLE == null' }, { if: '$VARIABLE == null' }]],
+ [[{ if: '$VARIABLE != "the wrong value"' }, { if: '$VARIABLE == null' }]]
+ ]
+ end
+
+ with_them do
+ it { is_expected.to be_included }
+
+ it 'correctly populates when:' do
+ expect(seed_build.attributes).to include(when: 'on_success')
+ end
+ end
+ end
+ end
+
+ context 'with a matching changes: rule' do
+ let(:pipeline) do
+ create(:ci_pipeline, project: project).tap do |pipeline|
+ stub_pipeline_modified_paths(pipeline, %w[app/models/ci/pipeline.rb spec/models/ci/pipeline_spec.rb .gitlab-ci.yml])
+ end
+ end
+
+ context 'with an explicit `when: never`' do
+ where(:rule_set) do
+ [
+ [[{ changes: %w[*/**/*.rb], when: 'never' }, { changes: %w[*/**/*.rb], when: 'always' }]],
+ [[{ changes: %w[app/models/ci/pipeline.rb], when: 'never' }, { changes: %w[app/models/ci/pipeline.rb], when: 'always' }]],
+ [[{ changes: %w[spec/**/*.rb], when: 'never' }, { changes: %w[spec/**/*.rb], when: 'always' }]],
+ [[{ changes: %w[*.yml], when: 'never' }, { changes: %w[*.yml], when: 'always' }]],
+ [[{ changes: %w[.*.yml], when: 'never' }, { changes: %w[.*.yml], when: 'always' }]],
+ [[{ changes: %w[**/*], when: 'never' }, { changes: %w[**/*], when: 'always' }]],
+ [[{ changes: %w[*/**/*.rb *.yml], when: 'never' }, { changes: %w[*/**/*.rb *.yml], when: 'always' }]],
+ [[{ changes: %w[.*.yml **/*], when: 'never' }, { changes: %w[.*.yml **/*], when: 'always' }]]
+ ]
+ end
+
+ with_them do
+ it { is_expected.not_to be_included }
+
+ it 'correctly populates when:' do
+ expect(seed_build.attributes).to include(when: 'never')
+ end
+ end
+ end
+
+ context 'with an explicit `when: always`' do
+ where(:rule_set) do
+ [
+ [[{ changes: %w[*/**/*.rb], when: 'always' }, { changes: %w[*/**/*.rb], when: 'never' }]],
+ [[{ changes: %w[app/models/ci/pipeline.rb], when: 'always' }, { changes: %w[app/models/ci/pipeline.rb], when: 'never' }]],
+ [[{ changes: %w[spec/**/*.rb], when: 'always' }, { changes: %w[spec/**/*.rb], when: 'never' }]],
+ [[{ changes: %w[*.yml], when: 'always' }, { changes: %w[*.yml], when: 'never' }]],
+ [[{ changes: %w[.*.yml], when: 'always' }, { changes: %w[.*.yml], when: 'never' }]],
+ [[{ changes: %w[**/*], when: 'always' }, { changes: %w[**/*], when: 'never' }]],
+ [[{ changes: %w[*/**/*.rb *.yml], when: 'always' }, { changes: %w[*/**/*.rb *.yml], when: 'never' }]],
+ [[{ changes: %w[.*.yml **/*], when: 'always' }, { changes: %w[.*.yml **/*], when: 'never' }]]
+ ]
+ end
+
+ with_them do
+ it { is_expected.to be_included }
+
+ it 'correctly populates when:' do
+ expect(seed_build.attributes).to include(when: 'always')
+ end
+ end
+ end
+
+ context 'without an explicit when: value' do
+ where(:rule_set) do
+ [
+ [[{ changes: %w[*/**/*.rb] }]],
+ [[{ changes: %w[app/models/ci/pipeline.rb] }]],
+ [[{ changes: %w[spec/**/*.rb] }]],
+ [[{ changes: %w[*.yml] }]],
+ [[{ changes: %w[.*.yml] }]],
+ [[{ changes: %w[**/*] }]],
+ [[{ changes: %w[*/**/*.rb *.yml] }]],
+ [[{ changes: %w[.*.yml **/*] }]]
+ ]
+ end
+
+ with_them do
+ it { is_expected.to be_included }
+
+ it 'correctly populates when:' do
+ expect(seed_build.attributes).to include(when: 'on_success')
+ end
+ end
+ end
+ end
+
+ context 'with no matching rule' do
+ where(:rule_set) do
+ [
+ [[{ if: '$VARIABLE != null', when: 'never' }]],
+ [[{ if: '$VARIABLE != null', when: 'never' }, { if: '$VARIABLE != null', when: 'always' }]],
+ [[{ if: '$VARIABLE == "the wrong value"', when: 'never' }, { if: '$VARIABLE != null', when: 'always' }]],
+ [[{ if: '$VARIABLE != null', when: 'always' }]],
+ [[{ if: '$VARIABLE != null', when: 'always' }, { if: '$VARIABLE != null', when: 'never' }]],
+ [[{ if: '$VARIABLE == "the wrong value"', when: 'always' }, { if: '$VARIABLE != null', when: 'never' }]],
+ [[{ if: '$VARIABLE != null' }]],
+ [[{ if: '$VARIABLE != null' }, { if: '$VARIABLE != null' }]],
+ [[{ if: '$VARIABLE == "the wrong value"' }, { if: '$VARIABLE != null' }]]
+ ]
+ end
+
+ with_them do
+ it { is_expected.not_to be_included }
+
+ it 'correctly populates when:' do
+ expect(seed_build.attributes).to include(when: 'never')
+ end
+ end
+ end
+
+ context 'with no rules' do
+ let(:rule_set) { [] }
+
+ it { is_expected.not_to be_included }
+
+ it 'correctly populates when:' do
+ expect(seed_build.attributes).to include(when: 'never')
+ end
+ end
+ end
end
describe 'applying needs: dependency' do
@@ -476,4 +755,10 @@ describe Gitlab::Ci::Pipeline::Seed::Build do
end
end
end
+
+ describe '#scoped_variables_hash' do
+ subject { seed_build.scoped_variables_hash }
+
+ it { is_expected.to eq(seed_build.to_resource.scoped_variables_hash) }
+ end
end
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index d5567b4f166..91c559dcd9b 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -125,9 +125,11 @@ module Gitlab
describe 'delayed job entry' do
context 'when delayed is defined' do
let(:config) do
- YAML.dump(rspec: { script: 'rollout 10%',
- when: 'delayed',
- start_in: '1 day' })
+ YAML.dump(rspec: {
+ script: 'rollout 10%',
+ when: 'delayed',
+ start_in: '1 day'
+ })
end
it 'has the attributes' do
@@ -726,12 +728,12 @@ module Gitlab
end
end
- describe "When" do
- %w(on_success on_failure always).each do |when_state|
- it "returns #{when_state} when defined" do
+ describe 'when:' do
+ (Gitlab::Ci::Config::Entry::Job::ALLOWED_WHEN - %w[delayed]).each do |when_state|
+ it "#{when_state} creates one build and sets when:" do
config = YAML.dump({
- rspec: { script: "rspec", when: when_state }
- })
+ rspec: { script: 'rspec', when: when_state }
+ })
config_processor = Gitlab::Ci::YamlProcessor.new(config)
builds = config_processor.stage_builds_attributes("test")
@@ -740,6 +742,35 @@ module Gitlab
expect(builds.first[:when]).to eq(when_state)
end
end
+
+ context 'delayed' do
+ context 'with start_in' do
+ it 'creates one build and sets when:' do
+ config = YAML.dump({
+ rspec: { script: 'rspec', when: 'delayed', start_in: '1 hour' }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+ builds = config_processor.stage_builds_attributes("test")
+
+ expect(builds.size).to eq(1)
+ expect(builds.first[:when]).to eq('delayed')
+ expect(builds.first[:options][:start_in]).to eq('1 hour')
+ end
+ end
+
+ context 'without start_in' do
+ it 'raises an error' do
+ config = YAML.dump({
+ rspec: { script: 'rspec', when: 'delayed' }
+ })
+
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(YamlProcessor::ValidationError, /start in should be a duration/)
+ end
+ end
+ end
end
describe 'Parallel' do
@@ -1132,7 +1163,7 @@ module Gitlab
it { expect { subject }.not_to raise_error }
end
- context 'needs to builds' do
+ context 'needs two builds' do
let(:needs) { %w(build1 build2) }
it "does create jobs with valid specification" do
@@ -1169,7 +1200,7 @@ module Gitlab
end
end
- context 'needs to builds defined as symbols' do
+ context 'needs two builds defined as symbols' do
let(:needs) { [:build1, :build2] }
it { expect { subject }.not_to raise_error }
@@ -1195,6 +1226,67 @@ module Gitlab
end
end
+ describe 'rules' do
+ subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
+
+ let(:config) do
+ {
+ var_default: { stage: 'build', script: 'test', rules: [{ if: '$VAR == null' }] },
+ var_when: { stage: 'build', script: 'test', rules: [{ if: '$VAR == null', when: 'always' }] },
+ var_and_changes: { stage: 'build', script: 'test', rules: [{ if: '$VAR == null', changes: %w[README], when: 'always' }] },
+ changes_not_var: { stage: 'test', script: 'test', rules: [{ if: '$VAR != null', changes: %w[README] }] },
+ var_not_changes: { stage: 'test', script: 'test', rules: [{ if: '$VAR == null', changes: %w[other/file.rb], when: 'always' }] },
+ nothing: { stage: 'test', script: 'test', rules: [{ when: 'manual' }] },
+ var_never: { stage: 'deploy', script: 'test', rules: [{ if: '$VAR == null', when: 'never' }] },
+ var_delayed: { stage: 'deploy', script: 'test', rules: [{ if: '$VAR == null', when: 'delayed', start_in: '3 hours' }] },
+ two_rules: { stage: 'deploy', script: 'test', rules: [{ if: '$VAR == null', when: 'on_success' }, { changes: %w[README], when: 'manual' }] }
+ }
+ end
+
+ it 'raises no exceptions' do
+ expect { subject }.not_to raise_error
+ end
+
+ it 'returns all jobs regardless of their inclusion' do
+ expect(subject.builds.count).to eq(config.keys.count)
+ end
+
+ context 'used with job-level when' do
+ let(:config) do
+ {
+ var_default: {
+ stage: 'build',
+ script: 'test',
+ when: 'always',
+ rules: [{ if: '$VAR == null' }]
+ }
+ }
+ end
+
+ it 'raises a ValidationError' do
+ expect { subject }.to raise_error(YamlProcessor::ValidationError, /may not be used with `rules`: when/)
+ end
+ end
+
+ context 'used with job-level when:delayed' do
+ let(:config) do
+ {
+ var_default: {
+ stage: 'build',
+ script: 'test',
+ when: 'delayed',
+ start_in: '10 minutes',
+ rules: [{ if: '$VAR == null' }]
+ }
+ }
+ end
+
+ it 'raises a ValidationError' do
+ expect { subject }.to raise_error(YamlProcessor::ValidationError, /may not be used with `rules`: when, start_in/)
+ end
+ end
+ end
+
describe "Hidden jobs" do
let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config) }
subject { config_processor.stage_builds_attributes("test") }
@@ -1513,7 +1605,7 @@ module Gitlab
config = YAML.dump({ rspec: { script: "test", when: 1 } })
expect do
Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec when should be on_success, on_failure, always, manual or delayed")
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec when should be one of: #{Gitlab::Ci::Config::Entry::Job::ALLOWED_WHEN.join(', ')}")
end
it "returns errors if job artifacts:name is not an a string" do
diff --git a/spec/lib/gitlab/daemon_spec.rb b/spec/lib/gitlab/daemon_spec.rb
index d3e73314b87..0372b770844 100644
--- a/spec/lib/gitlab/daemon_spec.rb
+++ b/spec/lib/gitlab/daemon_spec.rb
@@ -34,12 +34,12 @@ describe Gitlab::Daemon do
end
end
- describe 'when Daemon is enabled' do
+ context 'when Daemon is enabled' do
before do
allow(subject).to receive(:enabled?).and_return(true)
end
- describe 'when Daemon is stopped' do
+ context 'when Daemon is stopped' do
describe '#start' do
it 'starts the Daemon' do
expect { subject.start.join }.to change { subject.thread? }.from(false).to(true)
@@ -57,14 +57,14 @@ describe Gitlab::Daemon do
end
end
- describe 'when Daemon is running' do
+ context 'when Daemon is running' do
before do
- subject.start.join
+ subject.start
end
describe '#start' do
it "doesn't start running Daemon" do
- expect { subject.start.join }.not_to change { subject.thread? }
+ expect { subject.start.join }.not_to change { subject.thread }
expect(subject).to have_received(:start_working).once
end
@@ -76,11 +76,29 @@ describe Gitlab::Daemon do
expect(subject).to have_received(:stop_working)
end
+
+ context 'when stop_working raises exception' do
+ before do
+ allow(subject).to receive(:start_working) do
+ sleep(1000)
+ end
+ end
+
+ it 'shutdowns Daemon' do
+ expect(subject).to receive(:stop_working) do
+ subject.thread.raise(Interrupt)
+ end
+
+ expect(subject.thread).to be_alive
+ expect { subject.stop }.not_to raise_error
+ expect(subject.thread).to be_nil
+ end
+ end
end
end
end
- describe 'when Daemon is disabled' do
+ context 'when Daemon is disabled' do
before do
allow(subject).to receive(:enabled?).and_return(false)
end
diff --git a/spec/lib/gitlab/email/hook/disable_email_interceptor_spec.rb b/spec/lib/gitlab/email/hook/disable_email_interceptor_spec.rb
index 0c58cf088cc..c8ed12523d0 100644
--- a/spec/lib/gitlab/email/hook/disable_email_interceptor_spec.rb
+++ b/spec/lib/gitlab/email/hook/disable_email_interceptor_spec.rb
@@ -13,9 +13,6 @@ describe Gitlab::Email::Hook::DisableEmailInterceptor do
end
after do
- # Removing interceptor from the list because unregister_interceptor is
- # implemented in later version of mail gem
- # See: https://github.com/mikel/mail/pull/705
Mail.unregister_interceptor(described_class)
end
diff --git a/spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb b/spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb
new file mode 100644
index 00000000000..35aa663b0a5
--- /dev/null
+++ b/spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe Gitlab::Email::Hook::SmimeSignatureInterceptor do
+ include SmimeHelper
+
+ # cert generation is an expensive operation and they are used read-only,
+ # so we share them as instance variables in all tests
+ before :context do
+ @root_ca = generate_root
+ @cert = generate_cert(root_ca: @root_ca)
+ end
+
+ let(:root_certificate) do
+ Gitlab::Email::Smime::Certificate.new(@root_ca[:key], @root_ca[:cert])
+ end
+
+ let(:certificate) do
+ Gitlab::Email::Smime::Certificate.new(@cert[:key], @cert[:cert])
+ end
+
+ let(:mail) do
+ ActionMailer::Base.mail(to: 'test@example.com', from: 'info@example.com', body: 'signed hello')
+ end
+
+ before do
+ allow(Gitlab::Email::Smime::Certificate).to receive_messages(from_files: certificate)
+
+ Mail.register_interceptor(described_class)
+ mail.deliver_now
+ end
+
+ after do
+ Mail.unregister_interceptor(described_class)
+ end
+
+ it 'signs the email appropriately with SMIME' do
+ expect(mail.header['To'].value).to eq('test@example.com')
+ expect(mail.header['From'].value).to eq('info@example.com')
+ expect(mail.header['Content-Type'].value).to match('multipart/signed').and match('protocol="application/x-pkcs7-signature"')
+
+ # verify signature and obtain pkcs7 encoded content
+ p7enc = Gitlab::Email::Smime::Signer.verify_signature(
+ cert: certificate.cert,
+ ca_cert: root_certificate.cert,
+ signed_data: mail.encoded)
+
+ # envelope in a Mail object and obtain the body
+ decoded_mail = Mail.new(p7enc.data)
+
+ expect(decoded_mail.body.encoded).to eq('signed hello')
+ end
+end
diff --git a/spec/lib/gitlab/email/smime/certificate_spec.rb b/spec/lib/gitlab/email/smime/certificate_spec.rb
new file mode 100644
index 00000000000..90b27602413
--- /dev/null
+++ b/spec/lib/gitlab/email/smime/certificate_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Email::Smime::Certificate do
+ include SmimeHelper
+
+ # cert generation is an expensive operation and they are used read-only,
+ # so we share them as instance variables in all tests
+ before :context do
+ @root_ca = generate_root
+ @cert = generate_cert(root_ca: @root_ca)
+ end
+
+ describe 'testing environment setup' do
+ describe 'generate_root' do
+ subject { @root_ca }
+
+ it 'generates a root CA that expires a long way in the future' do
+ expect(subject[:cert].not_after).to be > 999.years.from_now
+ end
+ end
+
+ describe 'generate_cert' do
+ subject { @cert }
+
+ it 'generates a cert properly signed by the root CA' do
+ expect(subject[:cert].issuer).to eq(@root_ca[:cert].subject)
+ end
+
+ it 'generates a cert that expires soon' do
+ expect(subject[:cert].not_after).to be < 60.minutes.from_now
+ end
+
+ it 'generates a cert intended for email signing' do
+ expect(subject[:cert].extensions).to include(an_object_having_attributes(oid: 'extendedKeyUsage', value: match('E-mail Protection')))
+ end
+
+ context 'passing in INFINITE_EXPIRY' do
+ subject { generate_cert(root_ca: @root_ca, expires_in: SmimeHelper::INFINITE_EXPIRY) }
+
+ it 'generates a cert that expires a long way in the future' do
+ expect(subject[:cert].not_after).to be > 999.years.from_now
+ end
+ end
+ end
+ end
+
+ describe '.from_strings' do
+ it 'parses correctly a certificate and key' do
+ parsed_cert = described_class.from_strings(@cert[:key].to_s, @cert[:cert].to_pem)
+
+ common_cert_tests(parsed_cert, @cert, @root_ca)
+ end
+ end
+
+ describe '.from_files' do
+ it 'parses correctly a certificate and key' do
+ allow(File).to receive(:read).with('a_key').and_return(@cert[:key].to_s)
+ allow(File).to receive(:read).with('a_cert').and_return(@cert[:cert].to_pem)
+
+ parsed_cert = described_class.from_files('a_key', 'a_cert')
+
+ common_cert_tests(parsed_cert, @cert, @root_ca)
+ end
+ end
+
+ def common_cert_tests(parsed_cert, cert, root_ca)
+ expect(parsed_cert.cert).to be_a(OpenSSL::X509::Certificate)
+ expect(parsed_cert.cert.subject).to eq(cert[:cert].subject)
+ expect(parsed_cert.cert.issuer).to eq(root_ca[:cert].subject)
+ expect(parsed_cert.cert.not_before).to eq(cert[:cert].not_before)
+ expect(parsed_cert.cert.not_after).to eq(cert[:cert].not_after)
+ expect(parsed_cert.cert.extensions).to include(an_object_having_attributes(oid: 'extendedKeyUsage', value: match('E-mail Protection')))
+ expect(parsed_cert.key).to be_a(OpenSSL::PKey::RSA)
+ end
+end
diff --git a/spec/lib/gitlab/email/smime/signer_spec.rb b/spec/lib/gitlab/email/smime/signer_spec.rb
new file mode 100644
index 00000000000..56048b7148c
--- /dev/null
+++ b/spec/lib/gitlab/email/smime/signer_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Email::Smime::Signer do
+ include SmimeHelper
+
+ it 'signs data appropriately with SMIME' do
+ root_certificate = generate_root
+ certificate = generate_cert(root_ca: root_certificate)
+
+ signed_content = described_class.sign(
+ cert: certificate[:cert],
+ key: certificate[:key],
+ data: 'signed content')
+ expect(signed_content).not_to be_nil
+
+ p7enc = described_class.verify_signature(
+ cert: certificate[:cert],
+ ca_cert: root_certificate[:cert],
+ signed_data: signed_content)
+
+ expect(p7enc).not_to be_nil
+ expect(p7enc.data).to eq('signed content')
+ end
+end
diff --git a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
index 5621d3d17d1..c09db4af8d8 100644
--- a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
+++ b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
@@ -36,7 +36,9 @@ describe Gitlab::SidekiqLogging::StructuredLogger do
'message' => 'TestWorker JID-da883554ee4fe414012f5f42: done: 0.0 sec',
'job_status' => 'done',
'duration' => 0.0,
- "completed_at" => timestamp.iso8601(3)
+ "completed_at" => timestamp.iso8601(3),
+ "system_s" => 0.0,
+ "user_s" => 0.0
)
end
let(:exception_payload) do
@@ -52,6 +54,13 @@ describe Gitlab::SidekiqLogging::StructuredLogger do
allow(Sidekiq).to receive(:logger).and_return(logger)
allow(subject).to receive(:current_time).and_return(timestamp.to_f)
+
+ allow(Process).to receive(:times).and_return(
+ stime: 0.0,
+ utime: 0.0,
+ cutime: 0.0,
+ cstime: 0.0
+ )
end
subject { described_class.new }
@@ -177,5 +186,31 @@ describe Gitlab::SidekiqLogging::StructuredLogger do
end
end
end
+
+ def ctime(times)
+ times[:cstime] + times[:cutime]
+ end
+
+ context 'with ctime value greater than 0' do
+ let(:times_start) { { stime: 0.04999, utime: 0.0483, cstime: 0.0188, cutime: 0.0188 } }
+ let(:times_end) { { stime: 0.0699, utime: 0.0699, cstime: 0.0399, cutime: 0.0399 } }
+
+ before do
+ end_payload['system_s'] = 0.02
+ end_payload['user_s'] = 0.022
+ end_payload['child_s'] = 0.042
+
+ allow(Process).to receive(:times).and_return(times_start, times_end)
+ end
+
+ it 'logs with ctime data and other cpu data' do
+ Timecop.freeze(timestamp) do
+ expect(logger).to receive(:info).with(start_payload.except('args')).ordered
+ expect(logger).to receive(:info).with(end_payload.except('args')).ordered
+
+ subject.call(job, 'test_queue') { }
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/sidekiq_middleware/monitor_spec.rb b/spec/lib/gitlab/sidekiq_middleware/monitor_spec.rb
new file mode 100644
index 00000000000..7319cdc2399
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_middleware/monitor_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::SidekiqMiddleware::Monitor do
+ let(:monitor) { described_class.new }
+
+ describe '#call' do
+ let(:worker) { double }
+ let(:job) { { 'jid' => 'job-id' } }
+ let(:queue) { 'my-queue' }
+
+ it 'calls SidekiqMonitor' do
+ expect(Gitlab::SidekiqMonitor.instance).to receive(:within_job)
+ .with('job-id', 'my-queue')
+ .and_call_original
+
+ expect { |blk| monitor.call(worker, job, queue, &blk) }.to yield_control
+ end
+
+ it 'passthroughs the return value' do
+ result = monitor.call(worker, job, queue) do
+ 'value'
+ end
+
+ expect(result).to eq('value')
+ end
+
+ context 'when cancel happens' do
+ subject do
+ monitor.call(worker, job, queue) do
+ raise Gitlab::SidekiqMonitor::CancelledError
+ end
+ end
+
+ it 'skips the job' do
+ expect { subject }.to raise_error(Sidekiq::JobRetry::Skip)
+ end
+
+ it 'puts job in DeadSet' do
+ ::Sidekiq::DeadSet.new.clear
+
+ expect do
+ subject rescue Sidekiq::JobRetry::Skip
+ end.to change { ::Sidekiq::DeadSet.new.size }.by(1)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sidekiq_monitor_spec.rb b/spec/lib/gitlab/sidekiq_monitor_spec.rb
new file mode 100644
index 00000000000..bbd7bf90217
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_monitor_spec.rb
@@ -0,0 +1,261 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::SidekiqMonitor do
+ let(:monitor) { described_class.new }
+
+ describe '#within_job' do
+ it 'tracks thread' do
+ blk = proc do
+ expect(monitor.jobs_thread['jid']).not_to be_nil
+
+ "OK"
+ end
+
+ expect(monitor.within_job('jid', 'queue', &blk)).to eq("OK")
+ end
+
+ context 'when job is canceled' do
+ let(:jid) { SecureRandom.hex }
+
+ before do
+ described_class.cancel_job(jid)
+ end
+
+ it 'does not execute a block' do
+ expect do |blk|
+ monitor.within_job(jid, 'queue', &blk)
+ rescue described_class::CancelledError
+ end.not_to yield_control
+ end
+
+ it 'raises exception' do
+ expect { monitor.within_job(jid, 'queue') }.to raise_error(
+ described_class::CancelledError)
+ end
+ end
+ end
+
+ describe '#start_working' do
+ subject { monitor.send(:start_working) }
+
+ before do
+ # we want to run at most once cycle
+ # we toggle `enabled?` flag after the first call
+ stub_const('Gitlab::SidekiqMonitor::RECONNECT_TIME', 0)
+ allow(monitor).to receive(:enabled?).and_return(true, false)
+
+ allow(Sidekiq.logger).to receive(:info)
+ allow(Sidekiq.logger).to receive(:warn)
+ end
+
+ context 'when structured logging is used' do
+ it 'logs start message' do
+ expect(Sidekiq.logger).to receive(:info)
+ .with(
+ class: described_class.to_s,
+ action: 'start',
+ message: 'Starting Monitor Daemon')
+
+ expect(::Gitlab::Redis::SharedState).to receive(:with)
+
+ subject
+ end
+
+ it 'logs stop message' do
+ expect(Sidekiq.logger).to receive(:warn)
+ .with(
+ class: described_class.to_s,
+ action: 'stop',
+ message: 'Stopping Monitor Daemon')
+
+ expect(::Gitlab::Redis::SharedState).to receive(:with)
+
+ subject
+ end
+
+ it 'logs StandardError message' do
+ expect(Sidekiq.logger).to receive(:warn)
+ .with(
+ class: described_class.to_s,
+ action: 'exception',
+ message: 'My Exception')
+
+ expect(::Gitlab::Redis::SharedState).to receive(:with)
+ .and_raise(StandardError, 'My Exception')
+
+ expect { subject }.not_to raise_error
+ end
+
+ it 'logs and raises Exception message' do
+ expect(Sidekiq.logger).to receive(:warn)
+ .with(
+ class: described_class.to_s,
+ action: 'exception',
+ message: 'My Exception')
+
+ expect(::Gitlab::Redis::SharedState).to receive(:with)
+ .and_raise(Exception, 'My Exception')
+
+ expect { subject }.to raise_error(Exception, 'My Exception')
+ end
+ end
+
+ context 'when StandardError is raised' do
+ it 'does retry connection' do
+ expect(::Gitlab::Redis::SharedState).to receive(:with)
+ .and_raise(StandardError, 'My Exception')
+
+ expect(::Gitlab::Redis::SharedState).to receive(:with)
+
+ # we expect to run `process_messages` twice
+ expect(monitor).to receive(:enabled?).and_return(true, true, false)
+
+ subject
+ end
+ end
+
+ context 'when message is published' do
+ let(:subscribed) { double }
+
+ before do
+ expect_any_instance_of(::Redis).to receive(:subscribe)
+ .and_yield(subscribed)
+
+ expect(subscribed).to receive(:message)
+ .and_yield(
+ described_class::NOTIFICATION_CHANNEL,
+ payload
+ )
+
+ expect(Sidekiq.logger).to receive(:info)
+ .with(
+ class: described_class.to_s,
+ action: 'start',
+ message: 'Starting Monitor Daemon')
+
+ expect(Sidekiq.logger).to receive(:info)
+ .with(
+ class: described_class.to_s,
+ channel: described_class::NOTIFICATION_CHANNEL,
+ message: 'Received payload on channel',
+ payload: payload
+ )
+ end
+
+ context 'and message is valid' do
+ let(:payload) { '{"action":"cancel","jid":"my-jid"}' }
+
+ it 'processes cancel' do
+ expect(monitor).to receive(:process_job_cancel).with('my-jid')
+
+ subject
+ end
+ end
+
+ context 'and message is not valid json' do
+ let(:payload) { '{"action"}' }
+
+ it 'skips processing' do
+ expect(monitor).not_to receive(:process_job_cancel)
+
+ subject
+ end
+ end
+ end
+ end
+
+ describe '#stop' do
+ let!(:monitor_thread) { monitor.start }
+
+ it 'does stop the thread' do
+ expect(monitor_thread).to be_alive
+
+ expect { monitor.stop }.not_to raise_error
+
+ expect(monitor_thread).not_to be_alive
+ expect { monitor_thread.value }.to raise_error(Interrupt)
+ end
+ end
+
+ describe '#process_job_cancel' do
+ subject { monitor.send(:process_job_cancel, jid) }
+
+ context 'when jid is missing' do
+ let(:jid) { nil }
+
+ it 'does not run thread' do
+ expect(subject).to be_nil
+ end
+ end
+
+ context 'when jid is provided' do
+ let(:jid) { 'my-jid' }
+
+ context 'when jid is not found' do
+ it 'does not log cancellation message' do
+ expect(Sidekiq.logger).not_to receive(:warn)
+ expect(subject).to be_nil
+ end
+ end
+
+ context 'when jid is found' do
+ let(:thread) { Thread.new { sleep 1000 } }
+
+ before do
+ monitor.jobs_thread[jid] = thread
+ end
+
+ after do
+ thread.kill
+ rescue
+ end
+
+ it 'does log cancellation message' do
+ expect(Sidekiq.logger).to receive(:warn)
+ .with(
+ class: described_class.to_s,
+ action: 'cancel',
+ message: 'Canceling thread with CancelledError',
+ jid: 'my-jid',
+ thread_id: thread.object_id)
+
+ expect(subject).to be_a(Thread)
+
+ subject.join
+ end
+
+ it 'does cancel the thread' do
+ expect(subject).to be_a(Thread)
+
+ subject.join
+
+ # we wait for the thread to be cancelled
+ # by `process_job_cancel`
+ expect { thread.join(5) }.to raise_error(described_class::CancelledError)
+ end
+ end
+ end
+ end
+
+ describe '.cancel_job' do
+ subject { described_class.cancel_job('my-jid') }
+
+ it 'sets a redis key' do
+ expect_any_instance_of(::Redis).to receive(:setex)
+ .with('sidekiq:cancel:my-jid', anything, 1)
+
+ subject
+ end
+
+ it 'notifies all workers' do
+ payload = '{"action":"cancel","jid":"my-jid"}'
+
+ expect_any_instance_of(::Redis).to receive(:publish)
+ .with('sidekiq:cancel:notifications', payload)
+
+ subject
+ end
+ end
+end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index dcc4b70a382..6cba7df114c 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -543,6 +543,73 @@ describe Notify do
end
end
+ describe '#mail_thread' do
+ set(:mail_thread_note) { create(:note) }
+
+ let(:headers) do
+ {
+ from: 'someone@test.com',
+ to: 'someone-else@test.com',
+ subject: 'something',
+ template_name: '_note_email' # re-use this for testing
+ }
+ end
+
+ let(:mailer) do
+ mailer = described_class.new
+ mailer.instance_variable_set(:@note, mail_thread_note)
+ mailer
+ end
+
+ context 'the model has no namespace' do
+ class TopLevelThing
+ include Referable
+ include Noteable
+
+ def to_reference(*_args)
+ 'tlt-ref'
+ end
+
+ def id
+ 'tlt-id'
+ end
+ end
+
+ subject do
+ mailer.send(:mail_thread, TopLevelThing.new, headers)
+ end
+
+ it 'has X-GitLab-Namespaced-Thing-ID header' do
+ expect(subject.header['X-GitLab-TopLevelThing-ID'].value).to eq('tlt-id')
+ end
+ end
+
+ context 'the model has a namespace' do
+ module Namespaced
+ class Thing
+ include Referable
+ include Noteable
+
+ def to_reference(*_args)
+ 'some-reference'
+ end
+
+ def id
+ 'some-id'
+ end
+ end
+ end
+
+ subject do
+ mailer.send(:mail_thread, Namespaced::Thing.new, headers)
+ end
+
+ it 'has X-GitLab-Namespaced-Thing-ID header' do
+ expect(subject.header['X-GitLab-Namespaced-Thing-ID'].value).to eq('some-id')
+ end
+ end
+ end
+
context 'for issue notes' do
let(:host) { Gitlab.config.gitlab.host }
diff --git a/spec/models/award_emoji_spec.rb b/spec/models/award_emoji_spec.rb
index 8452ac69734..b15b26b1630 100644
--- a/spec/models/award_emoji_spec.rb
+++ b/spec/models/award_emoji_spec.rb
@@ -44,6 +44,29 @@ describe AwardEmoji do
end
end
+ describe 'scopes' do
+ set(:thumbsup) { create(:award_emoji, name: 'thumbsup') }
+ set(:thumbsdown) { create(:award_emoji, name: 'thumbsdown') }
+
+ describe '.upvotes' do
+ it { expect(described_class.upvotes).to contain_exactly(thumbsup) }
+ end
+
+ describe '.downvotes' do
+ it { expect(described_class.downvotes).to contain_exactly(thumbsdown) }
+ end
+
+ describe '.named' do
+ it { expect(described_class.named('thumbsup')).to contain_exactly(thumbsup) }
+ it { expect(described_class.named(%w[thumbsup thumbsdown])).to contain_exactly(thumbsup, thumbsdown) }
+ end
+
+ describe '.awarded_by' do
+ it { expect(described_class.awarded_by(thumbsup.user)).to contain_exactly(thumbsup) }
+ it { expect(described_class.awarded_by([thumbsup.user, thumbsdown.user])).to contain_exactly(thumbsup, thumbsdown) }
+ end
+ end
+
describe 'expiring ETag cache' do
context 'on a note' do
let(:note) { create(:note_on_issue) }
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 4aac4b640f4..bc853d45085 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -149,6 +149,56 @@ describe Ci::Build do
end
end
+ describe '.with_stale_live_trace' do
+ subject { described_class.with_stale_live_trace }
+
+ context 'when build has a stale live trace' do
+ let!(:build) { create(:ci_build, :success, :trace_live, finished_at: 1.day.ago) }
+
+ it 'selects the build' do
+ is_expected.to eq([build])
+ end
+ end
+
+ context 'when build does not have a stale live trace' do
+ let!(:build) { create(:ci_build, :success, :trace_live, finished_at: 1.hour.ago) }
+
+ it 'does not select the build' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
+ describe '.finished_before' do
+ subject { described_class.finished_before(date) }
+
+ let(:date) { 1.hour.ago }
+
+ context 'when build has finished one day ago' do
+ let!(:build) { create(:ci_build, :success, finished_at: 1.day.ago) }
+
+ it 'selects the build' do
+ is_expected.to eq([build])
+ end
+ end
+
+ context 'when build has finished 30 minutes ago' do
+ let!(:build) { create(:ci_build, :success, finished_at: 30.minutes.ago) }
+
+ it 'returns an empty array' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when build is still running' do
+ let!(:build) { create(:ci_build, :running) }
+
+ it 'returns an empty array' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
describe '.with_reports' do
subject { described_class.with_reports(Ci::JobArtifact.test_reports) }
diff --git a/spec/models/concerns/awardable_spec.rb b/spec/models/concerns/awardable_spec.rb
index 9e7106281ee..76da42cf243 100644
--- a/spec/models/concerns/awardable_spec.rb
+++ b/spec/models/concerns/awardable_spec.rb
@@ -82,16 +82,6 @@ describe Awardable do
end
end
- describe "#toggle_award_emoji" do
- it "adds an emoji if it isn't awarded yet" do
- expect { issue.toggle_award_emoji("thumbsup", award_emoji.user) }.to change { AwardEmoji.count }.by(1)
- end
-
- it "toggles already awarded emoji" do
- expect { issue.toggle_award_emoji("thumbsdown", award_emoji.user) }.to change { AwardEmoji.count }.by(-1)
- end
- end
-
describe 'querying award_emoji on an Awardable' do
let(:issue) { create(:issue) }
diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb
index 6c67d84b59b..342fcfa1041 100644
--- a/spec/requests/api/award_emoji_spec.rb
+++ b/spec/requests/api/award_emoji_spec.rb
@@ -155,6 +155,14 @@ describe API::AwardEmoji do
expect(json_response['user']['username']).to eq(user.username)
end
+ it 'marks Todos on the Issue as done' do
+ todo = create(:todo, target: issue, project: project, user: user)
+
+ post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user), params: { name: '8ball' }
+
+ expect(todo.reload).to be_done
+ end
+
it "returns a 400 bad request error if the name is not given" do
post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user)
@@ -209,6 +217,14 @@ describe API::AwardEmoji do
expect(json_response['user']['username']).to eq(user.username)
end
+ it 'marks Todos on the Noteable as done' do
+ todo = create(:todo, target: note2.noteable, project: project, user: user)
+
+ post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user), params: { name: 'rocket' }
+
+ expect(todo.reload).to be_done
+ end
+
it "normalizes +1 as thumbsup award" do
post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user), params: { name: '+1' }
diff --git a/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb
index 3982125a38a..5b910d5bfe0 100644
--- a/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb
+++ b/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb
@@ -5,9 +5,9 @@ require 'spec_helper'
describe 'Adding an AwardEmoji' do
include GraphqlHelpers
- let(:current_user) { create(:user) }
- let(:awardable) { create(:note) }
- let(:project) { awardable.project }
+ set(:current_user) { create(:user) }
+ set(:project) { create(:project) }
+ set(:awardable) { create(:note, project: project) }
let(:emoji_name) { 'thumbsup' }
let(:mutation) do
variables = {
@@ -43,7 +43,7 @@ describe 'Adding an AwardEmoji' do
end
context 'when the given awardable is not an Awardable' do
- let(:awardable) { create(:label) }
+ let(:awardable) { create(:label, project: project) }
it_behaves_like 'a mutation that does not create an AwardEmoji'
@@ -52,7 +52,7 @@ describe 'Adding an AwardEmoji' do
end
context 'when the given awardable is an Awardable but still cannot be awarded an emoji' do
- let(:awardable) { create(:system_note) }
+ let(:awardable) { create(:system_note, project: project) }
it_behaves_like 'a mutation that does not create an AwardEmoji'
@@ -73,6 +73,13 @@ describe 'Adding an AwardEmoji' do
expect(mutation_response['awardEmoji']['name']).to eq(emoji_name)
end
+ describe 'marking Todos as done' do
+ let(:user) { current_user}
+ subject { post_graphql_mutation(mutation, current_user: user) }
+
+ include_examples 'creating award emojis marks Todos as done'
+ end
+
context 'when there were active record validation errors' do
before do
expect_next_instance_of(AwardEmoji) do |award|
diff --git a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
index 31145730f10..ae628d3e56c 100644
--- a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
+++ b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
@@ -5,9 +5,9 @@ require 'spec_helper'
describe 'Toggling an AwardEmoji' do
include GraphqlHelpers
- let(:current_user) { create(:user) }
- let(:awardable) { create(:note) }
- let(:project) { awardable.project }
+ set(:current_user) { create(:user) }
+ set(:project) { create(:project) }
+ set(:awardable) { create(:note, project: project) }
let(:emoji_name) { 'thumbsup' }
let(:mutation) do
variables = {
@@ -40,7 +40,7 @@ describe 'Toggling an AwardEmoji' do
end
context 'when the given awardable is not an Awardable' do
- let(:awardable) { create(:label) }
+ let(:awardable) { create(:label, project: project) }
it_behaves_like 'a mutation that does not create or destroy an AwardEmoji'
@@ -49,7 +49,7 @@ describe 'Toggling an AwardEmoji' do
end
context 'when the given awardable is an Awardable but still cannot be awarded an emoji' do
- let(:awardable) { create(:system_note) }
+ let(:awardable) { create(:system_note, project: project) }
it_behaves_like 'a mutation that does not create or destroy an AwardEmoji'
@@ -81,6 +81,13 @@ describe 'Toggling an AwardEmoji' do
expect(mutation_response['toggledOn']).to eq(true)
end
+ describe 'marking Todos as done' do
+ let(:user) { current_user}
+ subject { post_graphql_mutation(mutation, current_user: user) }
+
+ include_examples 'creating award emojis marks Todos as done'
+ end
+
context 'when there were active record validation errors' do
before do
expect_next_instance_of(AwardEmoji) do |award|
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 1d7ca85cdd2..5465fe0c366 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -1494,6 +1494,17 @@ describe API::Projects do
expect(response).to have_gitlab_http_status(404)
end
+
+ it 'filters out users listed in skip_users' do
+ other_user = create(:user)
+ project.team.add_developer(other_user)
+
+ get api("/projects/#{project.id}/users?skip_users=#{user.id}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response.size).to eq(1)
+ expect(json_response[0]['id']).to eq(other_user.id)
+ end
end
end
diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb
index d832963292c..478f09a7881 100644
--- a/spec/requests/rack_attack_global_spec.rb
+++ b/spec/requests/rack_attack_global_spec.rb
@@ -112,9 +112,9 @@ describe 'Rack Attack global throttles' do
arguments = {
message: 'Rack_Attack',
env: :throttle,
- ip: '127.0.0.1',
+ remote_ip: '127.0.0.1',
request_method: 'GET',
- fullpath: get_args.first,
+ path: get_args.first,
user_id: user.id,
username: user.username
}
@@ -213,9 +213,9 @@ describe 'Rack Attack global throttles' do
arguments = {
message: 'Rack_Attack',
env: :throttle,
- ip: '127.0.0.1',
+ remote_ip: '127.0.0.1',
request_method: 'GET',
- fullpath: '/users/sign_in'
+ path: '/users/sign_in'
}
expect(Gitlab::AuthLogger).to receive(:error).with(arguments)
@@ -377,9 +377,9 @@ describe 'Rack Attack global throttles' do
arguments = {
message: 'Rack_Attack',
env: :throttle,
- ip: '127.0.0.1',
+ remote_ip: '127.0.0.1',
request_method: 'GET',
- fullpath: '/dashboard/snippets',
+ path: '/dashboard/snippets',
user_id: user.id,
username: user.username
}
diff --git a/spec/serializers/deployment_entity_spec.rb b/spec/serializers/deployment_entity_spec.rb
index c0ea2b3c389..1b19eac9a97 100644
--- a/spec/serializers/deployment_entity_spec.rb
+++ b/spec/serializers/deployment_entity_spec.rb
@@ -32,8 +32,8 @@ describe DeploymentEntity do
expect(subject).to include(:created_at)
end
- it 'exposes finished_at' do
- expect(subject).to include(:finished_at)
+ it 'exposes deployed_at' do
+ expect(subject).to include(:deployed_at)
end
context 'when the pipeline has another manual action' do
diff --git a/spec/serializers/merge_request_sidebar_basic_entity_spec.rb b/spec/serializers/merge_request_sidebar_basic_entity_spec.rb
new file mode 100644
index 00000000000..b364b1a3306
--- /dev/null
+++ b/spec/serializers/merge_request_sidebar_basic_entity_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe MergeRequestSidebarBasicEntity do
+ let(:project) { create :project, :repository }
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:user) { create(:user) }
+
+ let(:request) { double('request', current_user: user, project: project) }
+
+ let(:entity) { described_class.new(merge_request, request: request).as_json }
+
+ describe '#current_user' do
+ it 'contains attributes related to the current user' do
+ expect(entity[:current_user].keys).to contain_exactly(
+ :id, :name, :username, :state, :avatar_url, :web_url, :todo,
+ :can_edit, :can_move, :can_admin_label, :can_merge
+ )
+ end
+ end
+end
diff --git a/spec/services/award_emojis/add_service_spec.rb b/spec/services/award_emojis/add_service_spec.rb
new file mode 100644
index 00000000000..037db39ba80
--- /dev/null
+++ b/spec/services/award_emojis/add_service_spec.rb
@@ -0,0 +1,103 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AwardEmojis::AddService do
+ set(:user) { create(:user) }
+ set(:project) { create(:project) }
+ set(:awardable) { create(:note, project: project) }
+ let(:name) { 'thumbsup' }
+ subject(:service) { described_class.new(awardable, name, user) }
+
+ describe '#execute' do
+ context 'when user is not authorized' do
+ it 'does not add an emoji' do
+ expect { service.execute }.not_to change { AwardEmoji.count }
+ end
+
+ it 'returns an error state' do
+ result = service.execute
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:http_status]).to eq(:forbidden)
+ end
+ end
+
+ context 'when user is authorized' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'creates an award emoji' do
+ expect { service.execute }.to change { AwardEmoji.count }.by(1)
+ end
+
+ it 'returns the award emoji' do
+ result = service.execute
+
+ expect(result[:award]).to be_kind_of(AwardEmoji)
+ end
+
+ it 'return a success status' do
+ result = service.execute
+
+ expect(result[:status]).to eq(:success)
+ end
+
+ it 'sets the correct properties on the award emoji' do
+ award = service.execute[:award]
+
+ expect(award.name).to eq(name)
+ expect(award.user).to eq(user)
+ end
+
+ describe 'marking Todos as done' do
+ subject { service.execute }
+
+ include_examples 'creating award emojis marks Todos as done'
+ end
+
+ context 'when the awardable cannot have emoji awarded to it' do
+ before do
+ expect(awardable).to receive(:emoji_awardable?).and_return(false)
+ end
+
+ it 'does not add an emoji' do
+ expect { service.execute }.not_to change { AwardEmoji.count }
+ end
+
+ it 'returns an error status' do
+ result = service.execute
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:http_status]).to eq(:unprocessable_entity)
+ end
+ end
+
+ context 'when the awardable is invalid' do
+ before do
+ expect_next_instance_of(AwardEmoji) do |award|
+ expect(award).to receive(:valid?).and_return(false)
+ expect(award).to receive_message_chain(:errors, :full_messages).and_return(['Error 1', 'Error 2'])
+ end
+ end
+
+ it 'does not add an emoji' do
+ expect { service.execute }.not_to change { AwardEmoji.count }
+ end
+
+ it 'returns an error status' do
+ result = service.execute
+
+ expect(result[:status]).to eq(:error)
+ end
+
+ it 'returns an error message' do
+ result = service.execute
+
+ expect(result[:message]).to eq('Error 1 and Error 2')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/finders/awarded_emoji_finder_spec.rb b/spec/services/award_emojis/collect_user_emoji_service_spec.rb
index d4479df7418..a0dea31b403 100644
--- a/spec/finders/awarded_emoji_finder_spec.rb
+++ b/spec/services/award_emojis/collect_user_emoji_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe AwardedEmojiFinder do
+describe AwardEmojis::CollectUserEmojiService do
describe '#execute' do
it 'returns an Array containing the awarded emoji names' do
user = create(:user)
diff --git a/spec/services/award_emojis/destroy_service_spec.rb b/spec/services/award_emojis/destroy_service_spec.rb
new file mode 100644
index 00000000000..c4a7d5ec20e
--- /dev/null
+++ b/spec/services/award_emojis/destroy_service_spec.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AwardEmojis::DestroyService do
+ set(:user) { create(:user) }
+ set(:awardable) { create(:note) }
+ set(:project) { awardable.project }
+ let(:name) { 'thumbsup' }
+ let!(:award_from_other_user) do
+ create(:award_emoji, name: name, awardable: awardable, user: create(:user))
+ end
+ subject(:service) { described_class.new(awardable, name, user) }
+
+ describe '#execute' do
+ shared_examples_for 'a service that does not authorize the user' do |error:|
+ it 'does not remove the emoji' do
+ expect { service.execute }.not_to change { AwardEmoji.count }
+ end
+
+ it 'returns an error state' do
+ result = service.execute
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:http_status]).to eq(:forbidden)
+ end
+
+ it 'returns a nil award' do
+ result = service.execute
+
+ expect(result).to have_key(:award)
+ expect(result[:award]).to be_nil
+ end
+
+ it 'returns the error' do
+ result = service.execute
+
+ expect(result[:message]).to eq(error)
+ expect(result[:errors]).to eq([error])
+ end
+ end
+
+ context 'when user is not authorized' do
+ it_behaves_like 'a service that does not authorize the user',
+ error: 'User cannot destroy emoji on the awardable'
+ end
+
+ context 'when the user is authorized' do
+ before do
+ project.add_developer(user)
+ end
+
+ context 'when user has not awarded an emoji to the awardable' do
+ let!(:award_from_user) { create(:award_emoji, name: name, user: user) }
+
+ it_behaves_like 'a service that does not authorize the user',
+ error: 'User has not awarded emoji of type thumbsup on the awardable'
+ end
+
+ context 'when user has awarded an emoji to the awardable' do
+ let!(:award_from_user) { create(:award_emoji, name: name, awardable: awardable, user: user) }
+
+ it 'removes the emoji' do
+ expect { service.execute }.to change { AwardEmoji.count }.by(-1)
+ end
+
+ it 'returns a success status' do
+ result = service.execute
+
+ expect(result[:status]).to eq(:success)
+ end
+
+ it 'returns no errors' do
+ result = service.execute
+
+ expect(result).not_to have_key(:error)
+ expect(result).not_to have_key(:errors)
+ end
+
+ it 'returns the destroyed award' do
+ result = service.execute
+
+ expect(result[:award]).to eq(award_from_user)
+ expect(result[:award]).to be_destroyed
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/award_emojis/toggle_service_spec.rb b/spec/services/award_emojis/toggle_service_spec.rb
new file mode 100644
index 00000000000..972a1d5fc06
--- /dev/null
+++ b/spec/services/award_emojis/toggle_service_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AwardEmojis::ToggleService do
+ set(:user) { create(:user) }
+ set(:project) { create(:project, :public) }
+ set(:awardable) { create(:note, project: project) }
+ let(:name) { 'thumbsup' }
+ subject(:service) { described_class.new(awardable, name, user) }
+
+ describe '#execute' do
+ context 'when user has awarded an emoji' do
+ let!(:award_from_other_user) { create(:award_emoji, name: name, awardable: awardable, user: create(:user)) }
+ let!(:award) { create(:award_emoji, name: name, awardable: awardable, user: user) }
+
+ it 'calls AwardEmojis::DestroyService' do
+ expect(AwardEmojis::AddService).not_to receive(:new)
+
+ expect_next_instance_of(AwardEmojis::DestroyService) do |service|
+ expect(service).to receive(:execute)
+ end
+
+ service.execute
+ end
+
+ it 'destroys an AwardEmoji' do
+ expect { service.execute }.to change { AwardEmoji.count }.by(-1)
+ end
+
+ it 'returns the result of DestroyService#execute' do
+ mock_result = double(foo: true)
+
+ expect_next_instance_of(AwardEmojis::DestroyService) do |service|
+ expect(service).to receive(:execute).and_return(mock_result)
+ end
+
+ result = service.execute
+
+ expect(result).to eq(mock_result)
+ end
+ end
+
+ context 'when user has not awarded an emoji' do
+ it 'calls AwardEmojis::AddService' do
+ expect_next_instance_of(AwardEmojis::AddService) do |service|
+ expect(service).to receive(:execute)
+ end
+
+ expect(AwardEmojis::DestroyService).not_to receive(:new)
+
+ service.execute
+ end
+
+ it 'creates an AwardEmoji' do
+ expect { service.execute }.to change { AwardEmoji.count }.by(1)
+ end
+
+ it 'returns the result of AddService#execute' do
+ mock_result = double(foo: true)
+
+ expect_next_instance_of(AwardEmojis::AddService) do |service|
+ expect(service).to receive(:execute).and_return(mock_result)
+ end
+
+ result = service.execute
+
+ expect(result).to eq(mock_result)
+ end
+ end
+ end
+end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index d9f35afee06..fd9a63b79cc 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -229,10 +229,10 @@ describe Issues::UpdateService, :mailer do
it 'creates zoom_link_added system note when a zoom link is added to the description' do
update_issue(description: 'Changed description https://zoom.us/j/5873603787')
- note = find_note('a Zoom call was added')
+ note = find_note('added a Zoom call')
expect(note).not_to be_nil
- expect(note.note).to eq('a Zoom call was added to this issue')
+ expect(note.note).to eq('added a Zoom call to this issue')
end
context 'when issue turns confidential' do
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index b0b74407812..d4fa62fa85d 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -78,6 +78,7 @@ describe Projects::CreateService, '#execute' do
expect(project).to be_valid
expect(project.owner).to eq(group)
expect(project.namespace).to eq(group)
+ expect(project.team.owners).to include(user)
expect(user.authorized_projects).to include(project)
end
end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 486d0ca0c56..f46f9633c1c 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -521,7 +521,7 @@ describe SystemNoteService do
end
it 'sets the zoom link added note text' do
- expect(subject.note).to eq('a Zoom call was added to this issue')
+ expect(subject.note).to eq('added a Zoom call to this issue')
end
end
@@ -533,7 +533,7 @@ describe SystemNoteService do
end
it 'sets the zoom link removed note text' do
- expect(subject.note).to eq('a Zoom call was removed from this issue')
+ expect(subject.note).to eq('removed a Zoom call from this issue')
end
end
diff --git a/spec/support/helpers/smime_helper.rb b/spec/support/helpers/smime_helper.rb
new file mode 100644
index 00000000000..656b3e196ba
--- /dev/null
+++ b/spec/support/helpers/smime_helper.rb
@@ -0,0 +1,55 @@
+module SmimeHelper
+ include OpenSSL
+
+ INFINITE_EXPIRY = 1000.years
+ SHORT_EXPIRY = 30.minutes
+
+ def generate_root
+ issue(signed_by: nil, expires_in: INFINITE_EXPIRY, certificate_authority: true)
+ end
+
+ def generate_cert(root_ca:, expires_in: SHORT_EXPIRY)
+ issue(signed_by: root_ca, expires_in: expires_in, certificate_authority: false)
+ end
+
+ # returns a hash { key:, cert: } containing a generated key, cert pair
+ def issue(email_address: 'test@example.com', signed_by:, expires_in:, certificate_authority:)
+ key = OpenSSL::PKey::RSA.new(4096)
+ public_key = key.public_key
+
+ subject = if certificate_authority
+ X509::Name.parse("/CN=EU")
+ else
+ X509::Name.parse("/CN=#{email_address}")
+ end
+
+ cert = X509::Certificate.new
+ cert.subject = subject
+
+ cert.issuer = signed_by&.fetch(:cert, nil)&.subject || subject
+
+ cert.not_before = Time.now
+ cert.not_after = expires_in.from_now
+ cert.public_key = public_key
+ cert.serial = 0x0
+ cert.version = 2
+
+ extension_factory = X509::ExtensionFactory.new
+ if certificate_authority
+ extension_factory.subject_certificate = cert
+ extension_factory.issuer_certificate = cert
+ cert.add_extension(extension_factory.create_extension('subjectKeyIdentifier', 'hash'))
+ cert.add_extension(extension_factory.create_extension('basicConstraints', 'CA:TRUE', true))
+ cert.add_extension(extension_factory.create_extension('keyUsage', 'cRLSign,keyCertSign', true))
+ else
+ cert.add_extension(extension_factory.create_extension('subjectAltName', "email:#{email_address}", false))
+ cert.add_extension(extension_factory.create_extension('basicConstraints', 'CA:FALSE', true))
+ cert.add_extension(extension_factory.create_extension('keyUsage', 'digitalSignature,keyEncipherment', true))
+ cert.add_extension(extension_factory.create_extension('extendedKeyUsage', 'clientAuth,emailProtection', false))
+ end
+
+ cert.sign(signed_by&.fetch(:key, nil) || key, Digest::SHA256.new)
+
+ { key: key, cert: cert }
+ end
+end
diff --git a/spec/support/helpers/stub_gitlab_calls.rb b/spec/support/helpers/stub_gitlab_calls.rb
index badea94352a..7d10cffe920 100644
--- a/spec/support/helpers/stub_gitlab_calls.rb
+++ b/spec/support/helpers/stub_gitlab_calls.rb
@@ -22,6 +22,10 @@ module StubGitlabCalls
allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file) { ci_yaml }
end
+ def stub_pipeline_modified_paths(pipeline, modified_paths)
+ allow(pipeline).to receive(:modified_paths).and_return(modified_paths)
+ end
+
def stub_repository_ci_yaml_file(sha:, path: '.gitlab-ci.yml')
allow_any_instance_of(Repository)
.to receive(:gitlab_ci_yml_for).with(sha, path)
diff --git a/spec/support/shared_examples/award_emoji_todo_shared_examples.rb b/spec/support/shared_examples/award_emoji_todo_shared_examples.rb
new file mode 100644
index 00000000000..88ad37d232f
--- /dev/null
+++ b/spec/support/shared_examples/award_emoji_todo_shared_examples.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+# Shared examples to that test code that creates AwardEmoji also mark Todos
+# as done.
+#
+# The examples expect these to be defined in the calling spec:
+# - `subject` the callable code that executes the creation of an AwardEmoji
+# - `user`
+# - `project`
+RSpec.shared_examples 'creating award emojis marks Todos as done' do
+ using RSpec::Parameterized::TableSyntax
+
+ before do
+ project.add_developer(user)
+ end
+
+ where(:type, :expectation) do
+ :issue | true
+ :merge_request | true
+ :project_snippet | false
+ end
+
+ with_them do
+ let(:project) { awardable.project }
+ let(:awardable) { create(type) }
+ let!(:todo) { create(:todo, target: awardable, project: project, user: user) }
+
+ it do
+ subject
+
+ expect(todo.reload.done?).to eq(expectation)
+ end
+ end
+
+ # Notes have more complicated rules than other Todoables
+ describe 'for notes' do
+ let!(:todo) { create(:todo, target: awardable.noteable, project: project, user: user) }
+
+ context 'regular Notes' do
+ let(:awardable) { create(:note, project: project) }
+
+ it 'marks the Todo as done' do
+ subject
+
+ expect(todo.reload.done?).to eq(true)
+ end
+ end
+
+ context 'PersonalSnippet Notes' do
+ let(:awardable) { create(:note, noteable: create(:personal_snippet, author: user)) }
+
+ it 'does not mark the Todo as done' do
+ subject
+
+ expect(todo.reload.done?).to eq(false)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb b/spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb
index 1cd14ea2251..d89eded6e69 100644
--- a/spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb
@@ -2,14 +2,14 @@
shared_examples 'set sort order from user preference' do
describe '#set_sort_order_from_user_preference' do
- # There is no issuable_sorting_field defined in any CE controllers yet,
+ # There is no sorting_field defined in any CE controllers yet,
# however any other field present in user_preferences table can be used for testing.
context 'when database is in read-only mode' do
it 'does not update user preference' do
allow(Gitlab::Database).to receive(:read_only?).and_return(true)
- expect_any_instance_of(UserPreference).not_to receive(:update).with({ controller.send(:issuable_sorting_field) => sorting_param })
+ expect_any_instance_of(UserPreference).not_to receive(:update).with({ controller.send(:sorting_field) => sorting_param })
get :index, params: { namespace_id: project.namespace, project_id: project, sort: sorting_param }
end
@@ -19,7 +19,7 @@ shared_examples 'set sort order from user preference' do
it 'updates user preference' do
allow(Gitlab::Database).to receive(:read_only?).and_return(false)
- expect_any_instance_of(UserPreference).to receive(:update).with({ controller.send(:issuable_sorting_field) => sorting_param })
+ expect_any_instance_of(UserPreference).to receive(:update).with({ controller.send(:sorting_field) => sorting_param })
get :index, params: { namespace_id: project.namespace, project_id: project, sort: sorting_param }
end
diff --git a/spec/workers/ci/archive_traces_cron_worker_spec.rb b/spec/workers/ci/archive_traces_cron_worker_spec.rb
index 28381fdc3be..01232e2a58b 100644
--- a/spec/workers/ci/archive_traces_cron_worker_spec.rb
+++ b/spec/workers/ci/archive_traces_cron_worker_spec.rb
@@ -5,6 +5,8 @@ require 'spec_helper'
describe Ci::ArchiveTracesCronWorker do
subject { described_class.new.perform }
+ let(:finished_at) { 1.day.ago }
+
before do
stub_feature_flags(ci_enable_live_trace: true)
end
@@ -28,7 +30,7 @@ describe Ci::ArchiveTracesCronWorker do
end
context 'when a job succeeded' do
- let!(:build) { create(:ci_build, :success, :trace_live) }
+ let!(:build) { create(:ci_build, :success, :trace_live, finished_at: finished_at) }
it_behaves_like 'archives trace'
@@ -39,9 +41,15 @@ describe Ci::ArchiveTracesCronWorker do
subject
end
+ context 'when the job finished recently' do
+ let(:finished_at) { 1.hour.ago }
+
+ it_behaves_like 'does not archive trace'
+ end
+
context 'when a trace had already been archived' do
let!(:build) { create(:ci_build, :success, :trace_live, :trace_artifact) }
- let!(:build2) { create(:ci_build, :success, :trace_live) }
+ let!(:build2) { create(:ci_build, :success, :trace_live, finished_at: finished_at) }
it 'continues to archive live traces' do
subject
@@ -52,7 +60,7 @@ describe Ci::ArchiveTracesCronWorker do
end
context 'when an unexpected exception happened during archiving' do
- let!(:build) { create(:ci_build, :success, :trace_live) }
+ let!(:build) { create(:ci_build, :success, :trace_live, finished_at: finished_at) }
before do
allow(Gitlab::Sentry).to receive(:track_exception)
@@ -71,7 +79,7 @@ describe Ci::ArchiveTracesCronWorker do
end
context 'when a job was cancelled' do
- let!(:build) { create(:ci_build, :canceled, :trace_live) }
+ let!(:build) { create(:ci_build, :canceled, :trace_live, finished_at: finished_at) }
it_behaves_like 'archives trace'
end
diff --git a/yarn.lock b/yarn.lock
index a295039ec54..4489b10815a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9,7 +9,7 @@
dependencies:
"@babel/highlight" "^7.0.0"
-"@babel/core@>=7.2.2", "@babel/core@^7.1.0", "@babel/core@^7.1.2", "@babel/core@^7.4.4":
+"@babel/core@>=7.2.2", "@babel/core@^7.1.0", "@babel/core@^7.1.2", "@babel/core@^7.5.5":
version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.5.5.tgz#17b2686ef0d6bc58f963dddd68ab669755582c30"
integrity sha512-i4qoSr2KTtce0DmkuuQBV4AuQgGPUcPXMr9L5MyYAtk06z068lQ10a4O009fe5OB/DfNV+h+qqT7ddNV8UnRjg==
@@ -139,16 +139,16 @@
"@babel/types" "^7.0.0"
"@babel/helper-module-transforms@^7.1.0", "@babel/helper-module-transforms@^7.4.4":
- version "7.4.4"
- resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.4.4.tgz#96115ea42a2f139e619e98ed46df6019b94414b8"
- integrity sha512-3Z1yp8TVQf+B4ynN7WoHPKS8EkdTbgAEy0nU0rs/1Kw4pDgmvYH3rz3aI11KgxKCba2cn7N+tqzV1mY2HMN96w==
+ version "7.5.5"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.5.5.tgz#f84ff8a09038dcbca1fd4355661a500937165b4a"
+ integrity sha512-jBeCvETKuJqeiaCdyaheF40aXnnU1+wkSiUs/IQg3tB85up1LyL8x77ClY8qJpuRJUcXQo+ZtdNESmZl4j56Pw==
dependencies:
"@babel/helper-module-imports" "^7.0.0"
"@babel/helper-simple-access" "^7.1.0"
"@babel/helper-split-export-declaration" "^7.4.4"
"@babel/template" "^7.4.4"
- "@babel/types" "^7.4.4"
- lodash "^4.17.11"
+ "@babel/types" "^7.5.5"
+ lodash "^4.17.13"
"@babel/helper-optimise-call-expression@^7.0.0":
version "7.0.0"
@@ -163,11 +163,11 @@
integrity sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==
"@babel/helper-regex@^7.0.0", "@babel/helper-regex@^7.4.4":
- version "7.4.4"
- resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.4.4.tgz#a47e02bc91fb259d2e6727c2a30013e3ac13c4a2"
- integrity sha512-Y5nuB/kESmR3tKjU8Nkn1wMGEx1tjJX076HBMeL3XLQCu6vA/YRzuTW0bbb+qRnXvQGn+d6Rx953yffl8vEy7Q==
+ version "7.5.5"
+ resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.5.5.tgz#0aa6824f7100a2e0e89c1527c23936c152cab351"
+ integrity sha512-CkCYQLkfkiugbRDO8eZn6lRuR8kzZoGXCg3149iTk5se7g6qykSpy3+hELSwquhu+TgHn8nkLiBwHvNX8Hofcw==
dependencies:
- lodash "^4.17.11"
+ lodash "^4.17.13"
"@babel/helper-remap-async-to-generator@^7.1.0":
version "7.1.0"
@@ -225,9 +225,9 @@
"@babel/types" "^7.5.5"
"@babel/highlight@^7.0.0":
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0.tgz#f710c38c8d458e6dd9a201afb637fcb781ce99e4"
- integrity sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==
+ version "7.5.0"
+ resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.5.0.tgz#56d11312bd9248fa619591d02472be6e8cb32540"
+ integrity sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==
dependencies:
chalk "^2.0.0"
esutils "^2.0.2"
@@ -252,7 +252,7 @@
"@babel/helper-remap-async-to-generator" "^7.1.0"
"@babel/plugin-syntax-async-generators" "^7.2.0"
-"@babel/plugin-proposal-class-properties@^7.1.0", "@babel/plugin-proposal-class-properties@^7.4.4":
+"@babel/plugin-proposal-class-properties@^7.1.0", "@babel/plugin-proposal-class-properties@^7.5.5":
version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.5.5.tgz#a974cfae1e37c3110e71f3c6a2e48b8e71958cd4"
integrity sha512-AF79FsnWFxjlaosgdi421vmYG6/jg79bVD0dpD44QdgobzHKuLZ6S3vl8la9qIeSwGi8i1fS0O1mfuDAAdo1/A==
@@ -833,7 +833,7 @@
"@babel/helper-regex" "^7.4.4"
regexpu-core "^4.5.4"
-"@babel/preset-env@^7.1.0", "@babel/preset-env@^7.4.4":
+"@babel/preset-env@^7.1.0", "@babel/preset-env@^7.5.5":
version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.5.5.tgz#bc470b53acaa48df4b8db24a570d6da1fef53c9a"
integrity sha512-GMZQka/+INwsMz1A5UEql8tG015h5j/qjptpKY2gJ7giy8ohzU710YciJB5rcKsWGWHiW3RUnHib0E5/m3Tp3A==
@@ -914,9 +914,9 @@
integrity sha512-FBMd0IiARPtH5aaOFUVki6evHiJQiY0pFy7fizyRF7dtwc+el3nwpzvhb9qBNzceG1OIJModG1xpE0DDFjPXwA==
"@babel/standalone@^7.0.0":
- version "7.3.4"
- resolved "https://registry.yarnpkg.com/@babel/standalone/-/standalone-7.3.4.tgz#b622c1e522acef91b2a14f22bdcdd4f935a1a474"
- integrity sha512-4L9c5i4WlGqbrjOVX0Yp8TIR5cEiw1/tPYYZENW/iuO2uI6viY38U7zALidzNfGdZIwNc+A/AWqMEWKeScWkBg==
+ version "7.5.5"
+ resolved "https://registry.yarnpkg.com/@babel/standalone/-/standalone-7.5.5.tgz#9d3143f6078ff408db694a4254bd6f03c5c33962"
+ integrity sha512-YIp5taErC4uvp4d5urJtWMui3cpvZt83x57l4oVJNvFtDzumf3pMgRmoTSpGuEzh1yzo7jHhg3mbQmMhmKPbjA==
"@babel/template@^7.1.0", "@babel/template@^7.4.0", "@babel/template@^7.4.4":
version "7.4.4"
@@ -1003,10 +1003,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.68.0.tgz#d631bd84ea7907592240d8417e82ba66d6a54c0c"
integrity sha512-3JmIq0bHg4InjLooM+kQFPfg3d7B1Pye67pN9+12kZXIa0nRGuwKEq3WSbcS+ACdg5jcVPNPYqStItEO4teHdw==
-"@gitlab/ui@5.15.0":
- version "5.15.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-5.15.0.tgz#1ce92cfed77dcd63a90d751043b42b19e64431c9"
- integrity sha512-YrYgERcmTxC+oP4GaY7onqvYgvTsyGCiiegQbZbXdNRLGGAmvfxWPzQRz8Ci9yIYkLvi0X2AV7BT8RTVOPQgXg==
+"@gitlab/ui@5.18.0":
+ version "5.18.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-5.18.0.tgz#f7f93ad41673f5ae081cd2246dda5456d3c956a2"
+ integrity sha512-umB7JCKVDZOajed0N563JBYSpiyK+VDL2eCdkinW+twWhMct8onoW4CLTpkgBi6Z9Y1Bq47aqnFtGf+1IKNIGw==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.2.1"