summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.flayignore4
-rw-r--r--.gitlab-ci.yml91
-rw-r--r--CHANGELOG.md198
-rw-r--r--CONTRIBUTING.md21
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile5
-rw-r--r--Gemfile.lock5
-rw-r--r--VERSION2
-rw-r--r--app/assets/javascripts/api.js16
-rw-r--r--app/assets/javascripts/environments/components/environments_app.vue3
-rw-r--r--app/assets/javascripts/environments/mixins/environments_mixin.js26
-rw-r--r--app/assets/javascripts/environments/services/environments_service.js17
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/form.vue6
-rw-r--r--app/assets/javascripts/ide/components/file_finder/index.vue10
-rw-r--r--app/assets/javascripts/ide/components/ide.vue5
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue9
-rw-r--r--app/assets/javascripts/ide/components/repo_file.vue4
-rw-r--r--app/assets/javascripts/ide/constants.js2
-rw-r--r--app/assets/javascripts/ide/ide_router.js20
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js4
-rw-r--r--app/assets/javascripts/ide/stores/index.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js16
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/actions.js49
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/getters.js7
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/index.js12
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js7
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/mutations.js53
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/state.js6
-rw-r--r--app/assets/javascripts/ide/stores/workers/files_decorator_worker.js4
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js99
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_store.js2
-rw-r--r--app/assets/javascripts/pages/ldap/omniauth_callbacks/index.js3
-rw-r--r--app/assets/javascripts/pipelines/components/graph/action_component.vue55
-rw-r--r--app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue13
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue11
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_component.vue12
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue13
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table_row.vue3
-rw-r--r--app/assets/javascripts/pipelines/components/stage.vue19
-rw-r--r--app/assets/javascripts/pipelines/constants.js2
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines.js2
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js30
-rw-r--r--app/assets/javascripts/shortcuts_navigation.js5
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue4
-rw-r--r--app/assets/stylesheets/framework/forms.scss8
-rw-r--r--app/assets/stylesheets/framework/lists.scss237
-rw-r--r--app/assets/stylesheets/framework/secondary_navigation_elements.scss12
-rw-r--r--app/assets/stylesheets/framework/variables.scss2
-rw-r--r--app/assets/stylesheets/pages/commits.scss4
-rw-r--r--app/assets/stylesheets/pages/groups.scss232
-rw-r--r--app/assets/stylesheets/pages/issuable.scss18
-rw-r--r--app/assets/stylesheets/pages/members.scss10
-rw-r--r--app/assets/stylesheets/pages/merge_conflicts.scss10
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss58
-rw-r--r--app/assets/stylesheets/pages/projects.scss6
-rw-r--r--app/assets/stylesheets/pages/repo.scss114
-rw-r--r--app/controllers/admin/application_settings_controller.rb2
-rw-r--r--app/controllers/boards/issues_controller.rb2
-rw-r--r--app/controllers/groups/boards_controller.rb11
-rw-r--r--app/controllers/groups/settings/badges_controller.rb4
-rw-r--r--app/controllers/profiles/keys_controller.rb2
-rw-r--r--app/controllers/projects/boards_controller.rb9
-rw-r--r--app/controllers/projects/pipelines_controller.rb25
-rw-r--r--app/controllers/projects/prometheus/metrics_controller.rb2
-rw-r--r--app/controllers/projects/settings/badges_controller.rb4
-rw-r--r--app/controllers/projects/settings/integrations_controller.rb9
-rw-r--r--app/finders/issuable_finder.rb9
-rw-r--r--app/finders/issues_finder.rb2
-rw-r--r--app/finders/merge_requests_finder.rb2
-rw-r--r--app/finders/personal_projects_finder.rb2
-rw-r--r--app/helpers/application_helper.rb5
-rw-r--r--app/helpers/blob_helper.rb5
-rw-r--r--app/models/appearance.rb15
-rw-r--r--app/models/application_setting.rb36
-rw-r--r--app/models/ci/build.rb5
-rw-r--r--app/models/ci/pipeline.rb20
-rw-r--r--app/models/ci/runner.rb2
-rw-r--r--app/models/clusters/applications/prometheus.rb5
-rw-r--r--app/models/commit.rb28
-rw-r--r--app/models/commit_status.rb1
-rw-r--r--app/models/concerns/cacheable_attributes.rb54
-rw-r--r--app/models/concerns/redis_cacheable.rb19
-rw-r--r--app/models/concerns/sortable.rb4
-rw-r--r--app/models/concerns/time_trackable.rb4
-rw-r--r--app/models/project.rb17
-rw-r--r--app/models/project_services/gemnasium_service.rb13
-rw-r--r--app/models/repository.rb20
-rw-r--r--app/models/service.rb1
-rw-r--r--app/models/user.rb29
-rw-r--r--app/presenters/ci/build_presenter.rb25
-rw-r--r--app/presenters/commit_status_presenter.rb24
-rw-r--r--app/presenters/generic_commit_status_presenter.rb2
-rw-r--r--app/serializers/pipeline_entity.rb6
-rw-r--r--app/services/boards/issues/create_service.rb6
-rw-r--r--app/services/keys/base_service.rb2
-rw-r--r--app/services/keys/destroy_service.rb12
-rw-r--r--app/services/lfs/unlock_file_service.rb8
-rw-r--r--app/services/milestones/base_service.rb1
-rw-r--r--app/uploaders/object_storage.rb1
-rw-r--r--app/views/admin/application_settings/_ci_cd.html.haml2
-rw-r--r--app/views/admin/broadcast_messages/_form.html.haml4
-rw-r--r--app/views/admin/dashboard/index.html.haml9
-rw-r--r--app/views/admin/runners/_runner.html.haml2
-rw-r--r--app/views/admin/runners/index.html.haml2
-rw-r--r--app/views/help/_shortcuts.html.haml25
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml6
-rw-r--r--app/views/projects/empty.html.haml2
-rw-r--r--[-rwxr-xr-x]app/views/projects/forks/new.html.haml0
-rw-r--r--app/views/projects/merge_requests/conflicts/_commit_stats.html.haml4
-rw-r--r--app/views/projects/merge_requests/conflicts/_submit_form.html.haml16
-rw-r--r--app/views/projects/registry/repositories/index.html.haml2
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml2
-rw-r--r--app/views/projects/tree/_tree_header.html.haml2
-rw-r--r--app/views/shared/_auto_devops_callout.html.haml2
-rw-r--r--app/views/shared/runners/show.html.haml2
-rw-r--r--changelogs/unreleased/10244-add-project-ci-cd-settings.yml5
-rw-r--r--changelogs/unreleased/16957-issue-due-email.yml5
-rw-r--r--changelogs/unreleased/18524-fix-double-brackets-in-wiki-markdown.yml5
-rw-r--r--changelogs/unreleased/19861-expand-api-render-an-arbitrary-markdown-document.yml5
-rw-r--r--changelogs/unreleased/21677-run-pipeline-word.yml5
-rw-r--r--changelogs/unreleased/22846-notifications-broken-during-email-address-change-before-email-confirmed.yml6
-rw-r--r--changelogs/unreleased/25010-collapsed-sidebar-tooltips.yml5
-rw-r--r--changelogs/unreleased/30739-fix-joined-information-on-project-members-page.yml5
-rw-r--r--changelogs/unreleased/32617-fix-template-selector-menu-visibility.yml6
-rw-r--r--changelogs/unreleased/33697-pipelines-json-endpoint.yml5
-rw-r--r--changelogs/unreleased/33697-remove-ujs-action-big-graph.yml5
-rw-r--r--changelogs/unreleased/34262-show-current-labels-when-editing.yml5
-rw-r--r--changelogs/unreleased/36762-reconcile-project-templates-with-auto-devops.yml5
-rw-r--r--changelogs/unreleased/36983-osw-heading-labels-color-fix.yml5
-rw-r--r--changelogs/unreleased/39584-nesting-depth-5-pages-pipelines.yml5
-rw-r--r--changelogs/unreleased/39710-search-placeholder-cut-off.yml5
-rw-r--r--changelogs/unreleased/40402-time-estimate-system-notes-can-be-confusing.yml5
-rw-r--r--changelogs/unreleased/40487-axios-pipelines.yml4
-rw-r--r--changelogs/unreleased/41059-calculate-artifact-size-more-efficiently.yml5
-rw-r--r--changelogs/unreleased/41082-make-deploykeys-table-more-clearly-structured.yml5
-rw-r--r--changelogs/unreleased/41748-vertical-misalignment-login-box.yml5
-rw-r--r--changelogs/unreleased/41981-allow-group-owner-to-enable-runners-from-subgroups.yml5
-rw-r--r--changelogs/unreleased/42099-port-push-mirroring-to-ce-ce-port-v-2.yml5
-rw-r--r--changelogs/unreleased/42543-hide-divergence-graph-on-branches-for-mobile.yml5
-rw-r--r--changelogs/unreleased/42803-show-new-branch-mr-button.yml5
-rw-r--r--changelogs/unreleased/42889-avoid-return-inside-block.yml5
-rw-r--r--changelogs/unreleased/42936-improve-ns-factory-avoid-duplicates.yml6
-rw-r--r--changelogs/unreleased/43111-controller-projects-mergerequestscontroller-index-executes-more-than-100-sql-queries.yml5
-rw-r--r--changelogs/unreleased/43404-pipelines-commit.yml5
-rw-r--r--changelogs/unreleased/43466-make-auto-devops-settings-first-class.yml5
-rw-r--r--changelogs/unreleased/43469-gcp-account-offer.yml5
-rw-r--r--changelogs/unreleased/43557-osw-present-merge-sha-commit.yml5
-rw-r--r--changelogs/unreleased/43567-replace-gke.yml5
-rw-r--r--changelogs/unreleased/43617-mailsig.yml5
-rw-r--r--changelogs/unreleased/44059-specify-variables-when-executing-a-manual-pipeline-from-the-ui.yml5
-rw-r--r--changelogs/unreleased/44224-remove-gl.yml5
-rw-r--r--changelogs/unreleased/44296-commit-path.yml6
-rw-r--r--changelogs/unreleased/44447-expose-deploy-token-to-ci-cd.yml5
-rw-r--r--changelogs/unreleased/44541-fix-file-tree-commit-status-cache.yml5
-rw-r--r--changelogs/unreleased/44582-clear-pipeline-status-cache.yml5
-rw-r--r--changelogs/unreleased/44697-prevue.yml5
-rw-r--r--changelogs/unreleased/44799-api-naming-issue-scope.yml5
-rw-r--r--changelogs/unreleased/44833-ide-clean-up-status-bar.yml5
-rw-r--r--changelogs/unreleased/44834-ide-remove-branch-from-bottom-bar.yml5
-rw-r--r--changelogs/unreleased/44879.yml5
-rw-r--r--changelogs/unreleased/44985-fix-protected-branch-delete-modal.yml5
-rw-r--r--changelogs/unreleased/45065-users-projects-json-sort.yml5
-rw-r--r--changelogs/unreleased/45159-fix-illustration.yml5
-rw-r--r--changelogs/unreleased/45397-update-faraday_middleware-to-0-12-2.yml5
-rw-r--r--changelogs/unreleased/45398-fix-rss-button.yml5
-rw-r--r--changelogs/unreleased/45436-markdown-is-not-rendering-error-loading-viewer-undefined-method-html_escape.yml5
-rw-r--r--changelogs/unreleased/45451-user-deletion-modal-with-same-info-for-delete-user-or-delete-user-and-contributions.yml6
-rw-r--r--changelogs/unreleased/45481-sane-pages-artifacts.yml6
-rw-r--r--changelogs/unreleased/45572-members-invitations-scheduled-before-commit.yml5
-rw-r--r--changelogs/unreleased/45576-fix-create-project-for-user-endpoint.yml5
-rw-r--r--changelogs/unreleased/45666-project-ci-lint-links.yml5
-rw-r--r--changelogs/unreleased/45761-replace-actionview-time_ago_in_words.yml5
-rw-r--r--changelogs/unreleased/45827-expose_readme_url_in_project_api.yml5
-rw-r--r--changelogs/unreleased/45934-ide-firefox-scroll-md-preview.yml5
-rw-r--r--changelogs/unreleased/46049-import-export-import-is-broken-due-to-the-addition-of-a-ci-table.yml5
-rw-r--r--changelogs/unreleased/46082-runner-contacted_at-is-not-always-a-time-type.yml5
-rw-r--r--changelogs/unreleased/46193-fix-big-estimate.yml5
-rw-r--r--changelogs/unreleased/46210-terms-acceptance-dropdown-menu.yml5
-rw-r--r--changelogs/unreleased/46286-fix-ingress-rbac-default-value.yml5
-rw-r--r--changelogs/unreleased/46303_copy_button_fix_embedded_snippets.yml5
-rw-r--r--changelogs/unreleased/46345-kubernetes-popover-illustration-skewed.yml5
-rw-r--r--changelogs/unreleased/46354-deprecate_gemnasium_service.yml5
-rw-r--r--changelogs/unreleased/46427-add-keyboard-shortcut-environments.yml5
-rw-r--r--changelogs/unreleased/46427-add-keyboard-shortcut-kubernetes.yml5
-rw-r--r--changelogs/unreleased/46427-change-keyboard-shortcut-of-activity-feed.yml (renamed from changelogs/unreleased/winh-new-mergerequest-branch-picker.yml)4
-rw-r--r--changelogs/unreleased/46427-remove-outdated-todos-shortcut.yml5
-rw-r--r--changelogs/unreleased/4950-unassign-slash-command-preview-fix.yml5
-rw-r--r--changelogs/unreleased/5750-backport-checksum-git-commanderror-exit-status-128.yml6
-rw-r--r--changelogs/unreleased/5794-we-should-failover-gracefully-when-we-can-t-connect-to-geo-tracking-database-ce.yml5
-rw-r--r--changelogs/unreleased/8088_embedded_snippets_support.yml5
-rw-r--r--changelogs/unreleased/ab-43706-composite-primary-keys.yml5
-rw-r--r--changelogs/unreleased/ab-44259-atomic-internal-ids-for-all-models.yml5
-rw-r--r--changelogs/unreleased/ab-46530-mediumtext-for-gpg-keys.yml5
-rwxr-xr-xchangelogs/unreleased/accessible-text.yml6
-rw-r--r--changelogs/unreleased/add-copy-metadata-command.yml5
-rw-r--r--changelogs/unreleased/add-git-commit-message-predefined-variable.yml5
-rw-r--r--changelogs/unreleased/add-loading-icon-padding-for-pipeline-environments.yml5
-rw-r--r--changelogs/unreleased/add-padding-to-profile-description.yml5
-rw-r--r--changelogs/unreleased/align-project-avatar-on-small-viewports.yml5
-rw-r--r--changelogs/unreleased/ash-mckenzie-include-sha-with-version.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-add-missing-changelog-type-to-docs.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-builds-artifacts-feature.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-commits-branches-feature.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-commits-comments-feature.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-issues-milestones-feature.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-source-markdown-render-feature.yml5
-rw-r--r--changelogs/unreleased/break-issue-title-for-board-card-title-and-issueable-header-text.yml5
-rw-r--r--changelogs/unreleased/bvl-enforce-terms.yml5
-rw-r--r--changelogs/unreleased/bvl-restrict-api-git-for-terms.yml6
-rw-r--r--changelogs/unreleased/bvl-shared-groups-on-group-page.yml5
-rw-r--r--changelogs/unreleased/bw-add-console-message.yml5
-rw-r--r--changelogs/unreleased/ccr-incoming-email-regex-anchor.yml3
-rw-r--r--changelogs/unreleased/change-font-for-tables-inside-diff-discussions.yml5
-rw-r--r--changelogs/unreleased/collapsed-contextual-nav-update.yml6
-rw-r--r--changelogs/unreleased/create-live-trace-only-if-job-is-complete.yml5
-rw-r--r--changelogs/unreleased/deprecation-warning-for-dynamic-milestones.yml5
-rw-r--r--changelogs/unreleased/dm-webhook-catch-blocked-url-exception.yml6
-rw-r--r--changelogs/unreleased/docs-use-variables-deploy-policy-for-staging-and-production.yml6
-rw-r--r--changelogs/unreleased/dz-add-2fa-filter-admin-api.yml5
-rw-r--r--changelogs/unreleased/feature-add-language-in-repository-to-api.yml5
-rw-r--r--changelogs/unreleased/feature-add_target_to_tags.yml5
-rw-r--r--changelogs/unreleased/feature-display-active-sessions.yml5
-rw-r--r--changelogs/unreleased/feature-gb-add-regexp-variables-expression.yml5
-rw-r--r--changelogs/unreleased/feature-runner-per-group.yml5
-rw-r--r--changelogs/unreleased/feature-show-only-groups-user-is-member-of-in-dashboard.yml5
-rw-r--r--changelogs/unreleased/fix-assignee-name-wrap.yml5
-rw-r--r--changelogs/unreleased/fix-devops-remove-beta.yml5
-rw-r--r--changelogs/unreleased/fix-gb-add-pipeline-builds-foreign-key.yml5
-rw-r--r--changelogs/unreleased/fix-gb-exclude-persisted-variables-from-environment-name.yml5
-rw-r--r--changelogs/unreleased/fix-gb-not-allow-to-trigger-skipped-manual-actions.yml5
-rw-r--r--changelogs/unreleased/fix-inconsistent-protected-branch-pill-baseline.yml5
-rw-r--r--changelogs/unreleased/fix-metrics-content-types.yml5
-rw-r--r--changelogs/unreleased/fix-project-mirror-data-schema.yml6
-rw-r--r--changelogs/unreleased/fix-shortcut-close-screen-with-key.yml5
-rw-r--r--changelogs/unreleased/fix-unverified-hover-state.yml5
-rw-r--r--changelogs/unreleased/fix-wiki-find-page-invalid-encoding.yml5
-rw-r--r--changelogs/unreleased/fj-42354-custom-hooks-not-triggered-by-UI-wiki-edit.yml5
-rw-r--r--changelogs/unreleased/fj-45057-improve-ssrf-documentation.yml5
-rw-r--r--changelogs/unreleased/fj-46411-fix-badge-api-endpoint-route-with-relative-url.yml5
-rw-r--r--changelogs/unreleased/fj-46459-fix-expose-url-when-base-url-set.yml5
-rw-r--r--changelogs/unreleased/fj-change-gollum-gems-to-custom-ones.yml5
-rw-r--r--changelogs/unreleased/fl-pipelines-details-axios.yml5
-rw-r--r--changelogs/unreleased/helm-add-alpine-mirrors.yml5
-rw-r--r--changelogs/unreleased/ide-file-finder.yml5
-rw-r--r--changelogs/unreleased/ide-improve-commit-panel.yml5
-rw-r--r--changelogs/unreleased/improve-commit-message-body-rendering.yml5
-rw-r--r--changelogs/unreleased/improve-jobs-queuing-time-metric.yml5
-rw-r--r--changelogs/unreleased/improve-quick-actions-summary-preview.yml5
-rw-r--r--changelogs/unreleased/increase-new-issue-metadata-form-margin.yml5
-rw-r--r--changelogs/unreleased/inform-the-user-when-there-are-no-project-import-options-available.yml5
-rw-r--r--changelogs/unreleased/issue-25256.yml5
-rw-r--r--changelogs/unreleased/issue_43660.yml5
-rw-r--r--changelogs/unreleased/jivl-refactor-activity-calendar.yml5
-rw-r--r--changelogs/unreleased/jprovazn-commit-notes-api.yml5
-rw-r--r--changelogs/unreleased/jprovazn-generic-error.yml6
-rw-r--r--changelogs/unreleased/jr-33320-lfs-settings-interface.yml5
-rw-r--r--changelogs/unreleased/jr-46209-web-ide-copy.yml5
-rw-r--r--changelogs/unreleased/jr-web-ide-shortcuts.yml5
-rw-r--r--changelogs/unreleased/jramsay-44880-filter-pipelines-by-sha.yml5
-rw-r--r--changelogs/unreleased/label-links-on-project-transfer.yml5
-rw-r--r--changelogs/unreleased/live-trace-v2-efficient-destroy-all.yml5
-rw-r--r--changelogs/unreleased/live-trace-v2.yml5
-rw-r--r--changelogs/unreleased/memoize-database-version.yml5
-rw-r--r--changelogs/unreleased/move-board-blank-state-vue-component.yml5
-rw-r--r--changelogs/unreleased/move-estimate-only-pane-vue-component.yml5
-rw-r--r--changelogs/unreleased/move-help-state-vue-component.yml5
-rw-r--r--changelogs/unreleased/move-notification-service-calls-to-sidekiq.yml5
-rw-r--r--changelogs/unreleased/move-pipeline-failed-vue-component.yml5
-rw-r--r--changelogs/unreleased/move-time-tracking-spent-only-pane-vue-component.yml5
-rw-r--r--changelogs/unreleased/osw-use-cached-highlighted-content-for-discussions.yml5
-rw-r--r--changelogs/unreleased/performance-gb-improve-pipeline-creation-service.yml5
-rw-r--r--changelogs/unreleased/pipelines-index-performance.yml5
-rw-r--r--changelogs/unreleased/rd-44635-error-500-when-clicking-ghost-user-or-gitlab-support-bot-in-ui.yml5
-rw-r--r--changelogs/unreleased/rd-45502-uploading-project-export-with-lfs-file-locks-fails.yml5
-rw-r--r--changelogs/unreleased/refactor-move-mr-widget-ready-to-merge-vue-component.yml5
-rw-r--r--changelogs/unreleased/refactor-move-mr-widget-wip-vue-component.yml5
-rw-r--r--changelogs/unreleased/refactor-move-no-tracking-pane-vue-component.yml5
-rw-r--r--changelogs/unreleased/refactor-move-sidebar-time-tracking-vue-component.yml5
-rw-r--r--changelogs/unreleased/rename-merge-request-widget-author-component.yml5
-rw-r--r--changelogs/unreleased/rename-overview-project-sidenav.yml5
-rw-r--r--changelogs/unreleased/restore-label-underline-color.yml5
-rw-r--r--changelogs/unreleased/restore-size-and-position-for-fork-icon.yml5
-rw-r--r--changelogs/unreleased/revert-discussion-counter-height.yml5
-rw-r--r--changelogs/unreleased/security-45689-fix-archive-cache-bug.yml5
-rw-r--r--changelogs/unreleased/security_issue_42029.yml5
-rw-r--r--changelogs/unreleased/sh-bump-lograge.yml5
-rw-r--r--changelogs/unreleased/sh-fix-blocked-user-account-ldap.yml5
-rw-r--r--changelogs/unreleased/sh-fix-grape-logging-status-code.yml5
-rw-r--r--changelogs/unreleased/sh-move-delete-groups-api-async.yml5
-rw-r--r--changelogs/unreleased/show-group-id-in-group-settings.yml5
-rw-r--r--changelogs/unreleased/show-runners-description-on-jobs-page.yml5
-rw-r--r--changelogs/unreleased/tc-repo-verify-mails.yml5
-rw-r--r--changelogs/unreleased/tz-upgrade-underscore.yml5
-rw-r--r--changelogs/unreleased/unresolved-discussions-vue-component-i18n-and-tests.yml5
-rw-r--r--changelogs/unreleased/update-environment-item-action-buttons-icons.yml5
-rw-r--r--changelogs/unreleased/update-timeline-icon-for-description-edit.yml5
-rw-r--r--changelogs/unreleased/use-case-insensitive-ordering-for-dashboard-filters.yml5
-rw-r--r--changelogs/unreleased/winh-dashboard-any-milestone.yml5
-rw-r--r--changelogs/unreleased/winh-dropdown-entry-unlocking.yml5
-rw-r--r--changelogs/unreleased/winh-make-it-right-now.yml5
-rw-r--r--changelogs/unreleased/zj-branch-containing-sha-opt-out.yml5
-rw-r--r--changelogs/unreleased/zj-calculate-checksum-mandator.yml5
-rw-r--r--changelogs/unreleased/zj-find-license-opt-out.yml5
-rw-r--r--changelogs/unreleased/zj-fork-opt-out.yml5
-rw-r--r--changelogs/unreleased/zj-namespace-service-mandatory.yml5
-rw-r--r--changelogs/unreleased/zj-ref-exists-opt-out.yml5
-rw-r--r--changelogs/unreleased/zj-repo-checksum-opt-out.yml5
-rw-r--r--changelogs/unreleased/zj-repository-exist-mandatory.yml5
-rw-r--r--changelogs/unreleased/zj-tag-containing-sha-opt-out.yml5
-rw-r--r--changelogs/unreleased/zj-workhorse-archive-mandatory.yml5
-rw-r--r--config/initializers/1_settings.rb3
-rw-r--r--config/initializers/deprecations.rb2
-rw-r--r--config/initializers/flipper.rb21
-rw-r--r--config/initializers/kubeclient.rb16
-rw-r--r--config/webpack.config.js211
-rw-r--r--db/migrate/20180503175053_ensure_missing_columns_to_project_mirror_data.rb15
-rw-r--r--db/migrate/20180504195842_project_name_lower_index.rb32
-rw-r--r--db/migrate/20180517082340_add_not_null_constraints_to_project_authorizations.rb38
-rw-r--r--db/migrate/20180521171529_increase_mysql_text_limit_for_gpg_keys.rb2
-rw-r--r--db/migrate/gpg_keys_limits_to_mysql.rb15
-rw-r--r--db/optional_migrations/composite_primary_keys.rb63
-rw-r--r--db/post_migrate/20180514161336_remove_gemnasium_service.rb15
-rw-r--r--db/schema.rb8
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_metrics.md4
-rw-r--r--doc/administration/monitoring/prometheus/index.md2
-rw-r--r--doc/api/README.md1
-rw-r--r--doc/api/groups.md3
-rw-r--r--doc/api/issues.md9
-rw-r--r--doc/api/markdown.md29
-rw-r--r--doc/api/merge_requests.md9
-rw-r--r--doc/api/projects.md11
-rw-r--r--doc/api/services.md7
-rw-r--r--doc/ci/environments.md1
-rw-r--r--doc/ci/variables/README.md11
-rw-r--r--doc/ci/yaml/README.md13
-rw-r--r--doc/development/ee_features.md38
-rw-r--r--doc/development/fe_guide/vuex.md8
-rw-r--r--doc/downgrade_ee_to_ce/README.md21
-rw-r--r--doc/raketasks/backup_restore.md35
-rw-r--r--doc/topics/autodevops/img/rollout_enabled.pngbin0 -> 19536 bytes
-rw-r--r--doc/topics/autodevops/img/rollout_staging_disabled.pngbin0 -> 13837 bytes
-rw-r--r--doc/topics/autodevops/img/rollout_staging_enabled.pngbin0 -> 17306 bytes
-rw-r--r--doc/topics/autodevops/img/staging_enabled.pngbin0 -> 17929 bytes
-rw-r--r--doc/topics/autodevops/index.md78
-rw-r--r--doc/topics/autodevops/quick_start_guide.md2
-rw-r--r--[-rwxr-xr-x]doc/user/admin_area/settings/img/enforce_terms.pngbin51979 -> 51979 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/admin_area/settings/img/respond_to_terms.pngbin205994 -> 205994 bytes
-rw-r--r--doc/user/project/deploy_tokens/index.md2
-rw-r--r--doc/user/project/integrations/jira.md2
-rw-r--r--doc/user/project/integrations/project_services.md2
-rw-r--r--doc/user/project/merge_requests/resolve_conflicts.md2
-rw-r--r--doc/workflow/shortcuts.md22
-rw-r--r--lib/api/api.rb18
-rw-r--r--lib/api/entities.rb2
-rw-r--r--lib/api/groups.rb4
-rw-r--r--lib/api/helpers/pagination.rb253
-rw-r--r--lib/api/helpers/related_resources_helpers.rb9
-rw-r--r--lib/api/issues.rb9
-rw-r--r--lib/api/markdown.rb33
-rw-r--r--lib/api/merge_requests.rb9
-rw-r--r--lib/api/runner.rb1
-rw-r--r--lib/api/settings.rb2
-rw-r--r--lib/api/v3/groups.rb5
-rw-r--r--lib/api/v3/settings.rb2
-rw-r--r--lib/banzai/filter/gollum_tags_filter.rb3
-rw-r--r--lib/banzai/filter/plantuml_filter.rb2
-rw-r--r--lib/banzai/filter/reference_filter.rb2
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb4
-rw-r--r--lib/feature.rb17
-rw-r--r--lib/gitlab.rb4
-rw-r--r--lib/gitlab/auth/ldap/access.rb41
-rw-r--r--lib/gitlab/auth/ldap/config.rb18
-rw-r--r--lib/gitlab/auth/saml/config.rb4
-rw-r--r--lib/gitlab/auth/saml/user.rb6
-rw-r--r--lib/gitlab/auth/user_auth_finders.rb10
-rw-r--r--lib/gitlab/ci/pipeline/expression.rb10
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/matches.rb29
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb33
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexer.rb8
-rw-r--r--lib/gitlab/ci/pipeline/expression/statement.rb9
-rw-r--r--lib/gitlab/ci/pipeline/preloader.rb28
-rw-r--r--lib/gitlab/current_settings.rb44
-rw-r--r--lib/gitlab/database.rb2
-rw-r--r--lib/gitlab/email/handler/create_issue_handler.rb2
-rw-r--r--lib/gitlab/email/handler/reply_processing.rb8
-rw-r--r--lib/gitlab/email/reply_parser.rb7
-rw-r--r--lib/gitlab/file_detector.rb2
-rw-r--r--lib/gitlab/git/blob.rb20
-rw-r--r--lib/gitlab/git/commit.rb64
-rw-r--r--lib/gitlab/git/gitlab_projects.rb2
-rw-r--r--lib/gitlab/git/path_helper.rb2
-rw-r--r--lib/gitlab/git/repository.rb87
-rw-r--r--lib/gitlab/git/repository_mirroring.rb6
-rw-r--r--lib/gitlab/git/tag.rb88
-rw-r--r--lib/gitlab/git/wiki.rb2
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb16
-rw-r--r--lib/gitlab/gitaly_client/operation_service.rb2
-rw-r--r--lib/gitlab/gitaly_client/ref_service.rb18
-rw-r--r--lib/gitlab/gitaly_client/repository_service.rb10
-rw-r--r--lib/gitlab/gitaly_client/util.rb14
-rw-r--r--lib/gitlab/incoming_email.rb2
-rw-r--r--lib/gitlab/serializer/pagination.rb2
-rw-r--r--lib/gitlab/untrusted_regexp.rb26
-rw-r--r--lib/gitlab/workhorse.rb7
-rw-r--r--lib/tasks/migrate/add_limits_mysql.rake2
-rw-r--r--lib/tasks/migrate/composite_primary_keys.rake15
-rw-r--r--lib/tasks/migrate/setup_postgresql.rake2
-rw-r--r--locale/gitlab.pot33
-rw-r--r--package.json3
-rw-r--r--qa/qa/specs/features/merge_request/create_spec.rb2
-rw-r--r--qa/qa/specs/features/project/deploy_key_clone_spec.rb12
-rwxr-xr-xscripts/no-ee-check7
-rw-r--r--scripts/prepare_build.sh2
-rwxr-xr-xscripts/trigger-build-cloud-native61
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb14
-rw-r--r--spec/controllers/projects/prometheus/metrics_controller_spec.rb73
-rw-r--r--spec/factories/gitaly/tag.rb9
-rw-r--r--spec/features/admin/admin_settings_spec.rb2
-rw-r--r--spec/features/merge_request/user_resolves_conflicts_spec.rb6
-rw-r--r--spec/features/projects/actve_tabs_spec.rb4
-rw-r--r--spec/features/projects/blobs/blob_show_spec.rb18
-rw-r--r--spec/features/projects/environments/environments_spec.rb16
-rw-r--r--spec/features/projects/user_sees_sidebar_spec.rb2
-rw-r--r--spec/features/projects/user_uses_shortcuts_spec.rb41
-rw-r--r--spec/features/projects/wiki/markdown_preview_spec.rb23
-rw-r--r--spec/features/users/user_browses_projects_on_user_page_spec.rb8
-rw-r--r--spec/finders/issues_finder_spec.rb2
-rw-r--r--spec/finders/personal_projects_finder_spec.rb12
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/projects.json1
-rw-r--r--spec/fixtures/emails/valid_new_issue_with_quote.eml25
-rw-r--r--spec/helpers/blob_helper_spec.rb10
-rw-r--r--spec/javascripts/environments/environments_app_spec.js212
-rw-r--r--spec/javascripts/environments/folder/environments_folder_view_spec.js81
-rw-r--r--spec/javascripts/environments/mock_data.js1
-rw-r--r--spec/javascripts/helpers/vuex_action_helper.js2
-rw-r--r--spec/javascripts/ide/components/ide_spec.js6
-rw-r--r--spec/javascripts/ide/components/repo_editor_spec.js20
-rw-r--r--spec/javascripts/ide/mock_data.js47
-rw-r--r--spec/javascripts/ide/stores/modules/pipelines/actions_spec.js289
-rw-r--r--spec/javascripts/ide/stores/modules/pipelines/getters_spec.js71
-rw-r--r--spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js120
-rw-r--r--spec/javascripts/pipelines/graph/action_component_spec.js71
-rw-r--r--spec/javascripts/pipelines/stage_spec.js27
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js6
-rw-r--r--spec/lib/api/helpers/pagination_spec.rb212
-rw-r--r--spec/lib/api/helpers/related_resources_helpers_spec.rb16
-rw-r--r--spec/lib/banzai/filter/gollum_tags_filter_spec.rb6
-rw-r--r--spec/lib/gitlab/auth/ldap/access_spec.rb13
-rw-r--r--spec/lib/gitlab/auth/ldap/config_spec.rb36
-rw-r--r--spec/lib/gitlab/auth/ldap/user_spec.rb5
-rw-r--r--spec/lib/gitlab/ci/config/entry/policy_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb80
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb96
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb99
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/token_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/preloader_spec.rb20
-rw-r--r--spec/lib/gitlab/current_settings_spec.rb101
-rw-r--r--spec/lib/gitlab/database_spec.rb14
-rw-r--r--spec/lib/gitlab/email/handler/create_issue_handler_spec.rb14
-rw-r--r--spec/lib/gitlab/email/reply_parser_spec.rb18
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb56
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb80
-rw-r--r--spec/lib/gitlab/git/tag_spec.rb52
-rw-r--r--spec/lib/gitlab/import_export/project.json20
-rw-r--r--spec/lib/gitlab/incoming_email_spec.rb4
-rw-r--r--spec/lib/gitlab/untrusted_regexp_spec.rb45
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb12
-rw-r--r--spec/models/appearance_spec.rb27
-rw-r--r--spec/models/application_setting_spec.rb30
-rw-r--r--spec/models/ci/build_spec.rb52
-rw-r--r--spec/models/ci/pipeline_spec.rb55
-rw-r--r--spec/models/clusters/applications/prometheus_spec.rb12
-rw-r--r--spec/models/commit_spec.rb84
-rw-r--r--spec/models/commit_status_spec.rb6
-rw-r--r--spec/models/concerns/cacheable_attributes_spec.rb153
-rw-r--r--spec/models/concerns/issuable_spec.rb13
-rw-r--r--spec/models/concerns/redis_cacheable_spec.rb83
-rw-r--r--spec/models/concerns/sortable_spec.rb108
-rw-r--r--spec/models/generic_commit_status_spec.rb6
-rw-r--r--spec/models/project_services/gemnasium_service_spec.rb33
-rw-r--r--spec/models/project_spec.rb28
-rw-r--r--spec/models/repository_spec.rb28
-rw-r--r--spec/models/user_spec.rb88
-rw-r--r--spec/presenters/ci/build_presenter_spec.rb2
-rw-r--r--spec/presenters/commit_status_presenter_spec.rb15
-rw-r--r--spec/requests/api/environments_spec.rb2
-rw-r--r--spec/requests/api/groups_spec.rb9
-rw-r--r--spec/requests/api/issues_spec.rb9
-rw-r--r--spec/requests/api/markdown_spec.rb112
-rw-r--r--spec/requests/api/merge_requests_spec.rb225
-rw-r--r--spec/requests/api/projects_spec.rb5
-rw-r--r--spec/requests/api/runner_spec.rb16
-rw-r--r--spec/requests/api/users_spec.rb27
-rw-r--r--spec/requests/api/v3/groups_spec.rb8
-rw-r--r--spec/requests/api/v3/projects_spec.rb2
-rw-r--r--spec/serializers/pipeline_entity_spec.rb7
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb22
-rw-r--r--spec/services/keys/destroy_service_spec.rb13
-rw-r--r--spec/services/projects/update_remote_mirror_service_spec.rb6
-rw-r--r--spec/support/helpers/rake_helpers.rb4
-rw-r--r--spec/support/shared_examples/malicious_regexp_shared_examples.rb2
-rw-r--r--yarn.lock156
507 files changed, 5960 insertions, 2748 deletions
diff --git a/.flayignore b/.flayignore
index 0c4eee10ffa..7faa6c7bb90 100644
--- a/.flayignore
+++ b/.flayignore
@@ -10,3 +10,7 @@ lib/gitlab/background_migration/*
app/models/project_services/kubernetes_service.rb
lib/gitlab/workhorse.rb
lib/gitlab/ci/trace/chunked_io.rb
+lib/gitlab/gitaly_client/ref_service.rb
+lib/gitlab/gitaly_client/commit_service.rb
+lib/gitlab/git/commit.rb
+lib/gitlab/git/tag.rb
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 84d8e69b84e..ef263a3f106 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -126,6 +126,23 @@ stages:
<<: *dedicated-no-docs-pull-cache-job
<<: *except-docs-and-qa
+.single-script-job: &single-script-job
+ image: ruby:2.4-alpine
+ before_script: []
+ stage: build
+ cache: {}
+ dependencies: []
+ variables: &single-script-job-variables
+ GIT_STRATEGY: none
+ before_script:
+ # We need to download the script rather than clone the repo since the
+ # package-and-qa job will not be able to run when the branch gets
+ # deleted (when merging the MR).
+ - export SCRIPT_NAME="${SCRIPT_NAME:-$CI_JOB_NAME}"
+ - apk add --update openssl
+ - wget $CI_PROJECT_URL/raw/$CI_COMMIT_SHA/scripts/$SCRIPT_NAME
+ - chmod 755 $SCRIPT_NAME
+
.rake-exec: &rake-exec
<<: *dedicated-no-docs-no-db-pull-cache-job
script:
@@ -207,19 +224,10 @@ stages:
.review-docs: &review-docs
<<: *dedicated-runner
<<: *except-qa
- image: ruby:2.4-alpine
- before_script:
- - gem install gitlab --no-doc
- # We need to download the script rather than clone the repo since the
- # review-docs-cleanup job will not be able to run when the branch gets
- # deleted (when merging the MR).
- - apk add --update openssl
- - wget https://gitlab.com/gitlab-org/gitlab-ce/raw/master/scripts/trigger-build-docs
- - chmod 755 trigger-build-docs
- cache: {}
- dependencies: []
+ <<: *single-script-job
variables:
- GIT_STRATEGY: none
+ <<: *single-script-job-variables
+ SCRIPT_NAME: trigger-build-docs
when: manual
only:
- branches
@@ -253,23 +261,14 @@ stages:
# Trigger a package build in omnibus-gitlab repository
#
package-and-qa:
- image: ruby:2.4-alpine
- before_script: []
- stage: build
- cache: {}
- when: manual
+ <<: *single-script-job
variables:
- GIT_STRATEGY: none
+ <<: *single-script-job-variables
+ SCRIPT_NAME: trigger-build-omnibus
retry: 0
- before_script:
- # We need to download the script rather than clone the repo since the
- # package-and-qa job will not be able to run when the branch gets
- # deleted (when merging the MR).
- - apk add --update openssl
- - wget https://gitlab.com/$CI_PROJECT_PATH/raw/$CI_COMMIT_SHA/scripts/trigger-build-omnibus
- - chmod 755 trigger-build-omnibus
script:
- - ./trigger-build-omnibus
+ - ./$SCRIPT_NAME
+ when: manual
only:
- //@gitlab-org/gitlab-ce
- //@gitlab-org/gitlab-ee
@@ -286,7 +285,8 @@ review-docs-deploy:
url: http://$DOCS_GITLAB_REPO_SUFFIX-$CI_COMMIT_REF_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX
on_stop: review-docs-cleanup
script:
- - ./trigger-build-docs deploy
+ - gem install gitlab --no-ri --no-rdoc
+ - ./$SCRIPT_NAME deploy
# Cleanup remote environment of gitlab-docs
review-docs-cleanup:
@@ -296,7 +296,28 @@ review-docs-cleanup:
name: review-docs/$CI_COMMIT_REF_NAME
action: stop
script:
- - ./trigger-build-docs cleanup
+ - gem install gitlab --no-ri --no-rdoc
+ - ./SCRIPT_NAME cleanup
+
+##
+# Trigger a docker image build in CNG (Cloud Native GitLab) repository
+#
+cloud-native-image:
+ image: ruby:2.4-alpine
+ before_script: []
+ stage: build
+ allow_failure: true
+ variables:
+ GIT_DEPTH: "1"
+ cache: {}
+ before_script:
+ - gem install gitlab --no-rdoc --no-ri
+ - chmod 755 ./scripts/trigger-build-cloud-native
+ script:
+ - ./scripts/trigger-build-cloud-native
+ only:
+ - tags@gitlab-org/gitlab-ce
+ - tags@gitlab-org/gitlab-ee
# Retrieve knapsack and rspec_flaky reports
retrieve-tests-metadata:
@@ -325,7 +346,7 @@ update-tests-metadata:
- rspec_flaky/
policy: push
script:
- - retry gem install fog-aws mime-types activesupport
+ - retry gem install fog-aws mime-types activesupport --no-ri --no-rdoc
- scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec-pg_node_*.json
- scripts/merge-reports ${FLAKY_RSPEC_SUITE_REPORT_PATH} rspec_flaky/all_*_*.json
- FLAKY_RSPEC_GENERATE_REPORT=1 scripts/prune-old-flaky-specs ${FLAKY_RSPEC_SUITE_REPORT_PATH}
@@ -836,3 +857,15 @@ gitlab_git_test:
cache: {}
script:
- spec/support/prepare-gitlab-git-test-for-commit --check-for-changes
+
+no_ee_check:
+ <<: *dedicated-runner
+ <<: *except-docs-and-qa
+ variables:
+ SETUP_DB: "false"
+ before_script: []
+ cache: {}
+ script:
+ - scripts/no-ee-check
+ only:
+ - //@gitlab-org/gitlab-ce
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 29047c3ad65..99cf96035d9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,204 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 10.8.1 (2018-05-23)
+
+### Fixed (9 changes)
+
+- Allow CommitStatus class to use presentable methods. !18979
+- Fix corrupted environment pages with unathorized proxy url. !18989
+- Fixes deploy token variables on Ci::Build. !19047
+- Fix project mirror database inconsistencies when upgrading from EE to CE. !19109
+- Render 404 when prometheus adapter is disabled in Prometheus metrics controller. !19110
+- Fix error when deleting an empty list of refs.
+- Fixed U2F login when used with LDAP.
+- Bump prometheus-client-mmap to 0.9.3 to fix nil exception error.
+- Fix system hook not firing for blocked users when LDAP sign-in is used.
+
+
+## 10.8.0 (2018-05-22)
+
+### Security (3 changes, 1 of them is from the community)
+
+- Update faraday_middlewar to 0.12.2. !18397 (Takuya Noguchi)
+- Serve archive requests with the correct file in all cases.
+- Sanitizes user name to avoid XSS attacks.
+
+### Fixed (47 changes, 11 of them are from the community)
+
+- Refactor CSS to eliminate vertical misalignment of login nav. !16275 (Takuya Noguchi)
+- Fix pipeline status in branch/tag tree page. !17995
+- Allow group owner to enable runners from subgroups (#41981). !18009
+- Fix template selector menu visibility when toggling preview mode in file edit view. !18118 (Fabian Schneider)
+- Fix confirmation modal for deleting a protected branch. !18176 (Paul Bonaud @PaulRbR)
+- Triggering custom hooks by Wiki UI edit. !18251
+- Now `rake cache:clear` will also clear pipeline status cache. !18257
+- Fix `joined` information on project members page. !18290 (Fabian Schneider)
+- Fix missing namespace for some internal users. !18357
+- Show shared projects on group page. !18390
+- Restore label underline color. !18407 (George Tsiolis)
+- Fix undefined `html_escape` method during markdown rendering. !18418
+- Fix unassign slash command preview. !18447
+- Correct text and functionality for delete user / delete user and contributions modal. !18463 (Marc Schwede)
+- Fix discussions API setting created_at for notable in a group or notable in a project in a group with owners. !18464
+- Don't include lfs_file_locks data in export bundle. !18495
+- Reset milestone filter when clicking "Any Milestone" in dashboard. !18531
+- Ensure member notifications are sent after the member actual creation/update in the DB. !18538
+- Update links to /ci/lint with ones to project ci/lint. !18539 (Takuya Noguchi)
+- Fix tabs container styles to make RSS button clickable. !18559
+- Raise NoRepository error for non-valid repositories when calculating repository checksum. !18594
+- Don't automatically remove artifacts for pages jobs after pages:deploy has run. !18628
+- Increase new issue metadata form margin. !18630 (George Tsiolis)
+- Add loading icon padding for pipeline environments. !18631 (George Tsiolis)
+- ShaAttribute no longer stops startup if database is missing. !18726
+- Fix close keyboard shortcuts dialog using the keyboard shortcut. !18783 (Lars Greiss)
+- Fixes database inconsistencies between Community and Enterprise Edition on import state. !18811
+- Add database foreign key constraint between pipelines and build. !18822
+- Fix finding wiki pages when they have invalidly-encoded content. !18856
+- Fix outdated Web IDE welcome copy. !18861
+- fixed copy to blipboard button in embed bar of snippets. !18923 (haseebeqx)
+- Disables RBAC on nginx-ingress. !18947
+- Correct skewed Kubernetes popover illustration. !18949
+- Resolve Import/Export ci_cd_settings error updating the project. !46049
+- Fix project creation for user endpoint when jobs_enabled parameter supplied.
+- 46210 Display logo and user dropdown on mobile for terms page and fix styling.
+- Adds illustration for when job log was erased.
+- Ensure web hook 'blocked URL' errors are stored in web hook logs and properly surfaced to the user.
+- Make toggle markdown preview shortcut only toggle selected field.
+- Verifiy if pipeline has commit idetails and render information in MR widget when branch is deleted.
+- Fixed inconsistent protected branch pill baseline.
+- Fix setting Gitlab metrics content types.
+- Display only generic message on merge error to avoid exposing any potentially sensitive or user unfriendly backend messages.
+- Fix label links update on project transfer.
+- Breaks commit not found message in pipelines table.
+- Adjust issue boards list header label text color.
+- Prevent pipeline actions in dropdown to redirct to a new page.
+
+### Changed (35 changes, 15 of them are from the community)
+
+- Improve tooltips in collapsed right sidebar. !17714
+- Partition job_queue_duration_seconds with jobs_running_for_project. !17730
+- For group dashboard, we no longer show groups which the visitor is not a member of (this applies to admins and auditors). !17884 (Roger Rüttimann)
+- Use RFC 3676 mail signature delimiters. !17979 (Enrico Scholz)
+- Add sha filter to pipelines list API. !18125
+- New CI Job live-trace architecture. !18169
+- Make project deploy keys table more clearly structured. !18279
+- Remove green background from unlock button in admin area. !18288
+- Renamed Overview to Project in the contextual navigation at a project level. !18295 (Constance Okoghenun)
+- Load branches on new merge request page asynchronously. !18315
+- Create settings section for autodevops. !18321
+- Add a comma to the time estimate system notes. !18326
+- Enable specifying variables when executing a manual pipeline. !18440
+- Fix size and position for fork icon. !18449 (George Tsiolis)
+- Refactored activity calendar. !18469 (Enrico Scholz)
+- Small improvements to repository checks. !18484
+- Add 2FA filter to users API for admins only. !18503
+- Align project avatar on small viewports. !18513 (George Tsiolis)
+- Show group and project LFS settings in the interface to Owners and Masters. !18562
+- Update environment item action buttons icons. !18632 (George Tsiolis)
+- Update timeline icon for description edit. !18633 (George Tsiolis)
+- Revert discussion counter height. !18656 (George Tsiolis)
+- Improve quick actions summary preview. !18659 (George Tsiolis)
+- Change font for tables inside diff discussions. !18660 (George Tsiolis)
+- Add padding to profile description. !18663 (George Tsiolis)
+- Break issue title for board card title and issuable header text. !18674 (George Tsiolis)
+- Adds push mirrors to GitLab Community Edition. !18715
+- Inform the user when there are no project import options available. !18716 (George Tsiolis)
+- Improve commit message body rendering and fix responsive compare panels. !18725 (Constance Okoghenun)
+- Reconcile project templates with Auto DevOps. !18737
+- Remove branch name from the status bar of WebIDE.
+- Clean up WebIDE status bar and add useful info.
+- Improve interaction on WebIDE commit panel.
+- Keep current labels visible when editing them in the sidebar.
+- Use VueJS for rendering pipeline stages.
+
+### Performance (26 changes, 11 of them are from the community)
+
+- Move WorkInProgress vue component. !17536 (George Tsiolis)
+- Move ReadyToMerge vue component. !17545 (George Tsiolis)
+- Move BoardBlankState vue component. !17666 (George Tsiolis)
+- Improve DB performance of calculating total artifacts size. !17839
+- Add i18n and update specs for UnresolvedDiscussions vue component. !17866 (George Tsiolis)
+- Introduce new ProjectCiCdSetting model with group_runners_enabled. !18144
+- Move PipelineFailed vue component. !18277 (George Tsiolis)
+- Move TimeTrackingEstimateOnlyPane vue component. !18318 (George Tsiolis)
+- Move TimeTrackingHelpState vue component. !18319 (George Tsiolis)
+- Reduce queries on merge requests list page for merge requests from forks. !18561
+- Destroy build_chunks efficiently with FastDestroyAll module. !18575
+- Improve performance of a service responsible for creating a pipeline. !18582
+- Replace time_ago_in_words with JS-based one. !18607 (Takuya Noguchi)
+- Move TimeTrackingNoTrackingPane vue component. !18676 (George Tsiolis)
+- Move SidebarTimeTracking vue component. !18677 (George Tsiolis)
+- Move TimeTrackingSpentOnlyPane vue component. !18710 (George Tsiolis)
+- Detecting tags containing a commit uses Gitaly by default.
+- Increase cluster applications installer availability using alpine linux mirrors.
+- Compute notification recipients in background jobs.
+- Use persisted diff data instead fetching Git on discussions.
+- Detecting branchnames containing a commit uses Gitaly by default.
+- Detect repository license on Gitaly by default.
+- Finish NamespaceService migration to Gitaly.
+- Check if a ref exists is done by Gitaly by default.
+- Compute Gitlab::Git::Repository#checksum on Gitaly by default.
+- Repository#exists? is always executed through Gitaly.
+
+### Added (22 changes, 10 of them are from the community)
+
+- Allow group masters to configure runners for groups. !9646 (Alexis Reigel)
+- Adds Embedded Snippets Support. !15695 (haseebeqx)
+- Add Copy metadata quick action. !16473 (Mateusz Bajorski)
+- Show Runner's description on job's page. !17321
+- Add deprecation message to dynamic milestone pages. !17505
+- Show new branch/mr button even when branch exists. !17712 (Jacopo Beschi @jacopo-beschi)
+- API: add languages of project GET /projects/:id/languages. !17770 (Roger Rüttimann)
+- Display active sessions and allow the user to revoke any of it. !17867 (Alexis Reigel)
+- Add cron job to email users on issue due date. !17985 (Stuart Nelson)
+- Rubocop rule to avoid returning from a block. !18000 (Jacopo Beschi @jacopo-beschi)
+- Add the signature verfication badge to the compare view. !18245 (Marc Shaw)
+- Expose Deploy Token data as environment varialbes on CI/CD jobs. !18414
+- Show group id in group settings. !18482 (George Tsiolis)
+- Allow admins to enforce accepting Terms of Service on an instance. !18570
+- Add CI_COMMIT_MESSAGE, CI_COMMIT_TITLE and CI_COMMIT_DESCRIPTION predefined variables. !18672
+- Add GCP signup offer to cluster index / create pages. !18684
+- Output some useful information when running the rails console. !18697
+- Display merge commit SHA in merge widget after merge. !18722
+- git SHA is now displayed alongside the GitLab version on the Admin Dashboard.
+- Expose the target commit ID through the tag API.
+- Added fuzzy file finder to web IDE.
+- Add discussion API for merge requests and commits.
+
+### Other (22 changes, 8 of them are from the community)
+
+- Replace the `project/issues/milestones.feature` spinach test with an rspec analog. !18300 (@blackst0ne)
+- Replace the `project/commits/branches.feature` spinach test with an rspec analog. !18302 (@blackst0ne)
+- Replacing gollum libraries for gitlab custom libs. !18343
+- Replace the `project/commits/comments.feature` spinach test with an rspec analog. !18356 (@blackst0ne)
+- Replace "Click" with "Select" to be more inclusive of people with accessibility requirements. !18386 (Mark Lapierre)
+- Remove ahead/behind graphs on project branches on mobile. !18415 (Takuya Noguchi)
+- Replace the `project/source/markdown_render.feature` spinach test with an rspec analog. !18525 (@blackst0ne)
+- Add missing changelog type to docs. !18526 (@blackst0ne)
+- Added Webhook SSRF prevention to documentation. !18532
+- Upgrade underscore.js to 1.9.0. !18578
+- Add documentation about how to use variables to define deploy policies for staging/production environments. !18675
+- Replace the `project/builds/artifacts.feature` spinach test with an rspec analog. !18729 (@blackst0ne)
+- Block access to the API & git for users that did not accept enforced Terms of Service. !18816
+- Transition to atomic internal ids for all models. !44259
+- Removes modal boards store and mixins from global scope.
+- Replace GKE acronym with Google Kubernetes Engine.
+- Replace vue resource with axios for pipelines details page.
+- Enable prometheus monitoring by default.
+- Replace vue resource with axios in pipelines table.
+- Bump lograge to 0.10.0 and remove monkey patch.
+- Improves wording in new pipeline page.
+- Gitaly handles repository forks by default.
+
+
+## 10.7.4 (2018-05-21)
+
+### Fixed (1 change)
+
+- Fix error when deleting an empty list of refs.
+
+
## 10.7.3 (2018-05-02)
### Fixed (8 changes)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 5447ebbdd8c..383d13656e2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -168,7 +168,7 @@ hits. They are not always necessary, but very convenient.
If you are an expert in a particular area, it makes it easier to find issues to
work on. You can also subscribe to those labels to receive an email each time an
-issue is labelled with a subject label corresponding to your expertise.
+issue is labeled with a subject label corresponding to your expertise.
Examples of subject labels are ~wiki, ~"container registry", ~ldap, ~api,
~issues, ~"merge requests", ~labels, and ~"container registry".
@@ -296,7 +296,24 @@ any potential community contributor to @-mention per above.
## Implement design & UI elements
-Please see the [UX Guide for GitLab].
+For guidance on UX implementation at GitLab, please refer to our [Design System](https://design.gitlab.com/).
+
+The UX team uses labels to manage their workflow.
+
+The ~"UX" label on an issue is a signal to the UX team that it will need UX attention.
+To better understand the priority by which UX tackles issues, see the [UX section](https://about.gitlab.com/handbook/ux/) of the handbook.
+
+Once an issue has been worked on and is ready for development, a UXer applies the ~"UX ready" label to that issue.
+
+The UX team has a special type label called ~"design artifact". This label indicates that the final output
+for an issue is a UX solution/design. The solution will be developed by frontend and/or backend in a subsequent milestone.
+Any issue labeled ~"design artifact" should not also be labeled ~"frontend" or ~"backend" since no development is
+needed until the solution has been decided.
+
+~"design artifact" issues are like any other issue and should contain a milestone label, ~"Deliverable" or ~"Stretch", when scheduled in the current milestone.
+
+Once the ~"design artifact" issue has been completed, the UXer removes the ~"design artifact" label and applies the ~"UX ready" label. The Product Manager can use the
+existing issue or decide to create a whole new issue for the purpose of development.
## Issue tracker
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 897e21587ed..7bb21aff834 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.100.0
+0.102.0
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 6aba2b245a8..fae6e3d04b2 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-4.2.0
+4.2.1
diff --git a/Gemfile b/Gemfile
index a6cd0ca980a..6730ee9c164 100644
--- a/Gemfile
+++ b/Gemfile
@@ -174,6 +174,9 @@ gem 'httparty', '~> 0.13.3'
# Colored output to console
gem 'rainbow', '~> 2.2'
+# Progress bar
+gem 'ruby-progressbar'
+
# GitLab settings
gem 'settingslogic', '~> 2.0.9'
@@ -294,7 +297,7 @@ group :metrics do
gem 'influxdb', '~> 0.2', require: false
# Prometheus
- gem 'prometheus-client-mmap', '~> 0.9.2'
+ gem 'prometheus-client-mmap', '~> 0.9.3'
gem 'raindrops', '~> 0.18'
end
diff --git a/Gemfile.lock b/Gemfile.lock
index 18c25cc34b6..9c2ef9dfa91 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -629,7 +629,7 @@ GEM
parser
unparser
procto (0.0.3)
- prometheus-client-mmap (0.9.2)
+ prometheus-client-mmap (0.9.3)
pry (0.10.4)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
@@ -1115,7 +1115,7 @@ DEPENDENCIES
peek-sidekiq (~> 1.0.3)
pg (~> 0.18.2)
premailer-rails (~> 1.9.7)
- prometheus-client-mmap (~> 0.9.2)
+ prometheus-client-mmap (~> 0.9.3)
pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1)
@@ -1150,6 +1150,7 @@ DEPENDENCIES
rubocop-rspec (~> 1.22.1)
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.17.0)
+ ruby-progressbar
ruby_parser (~> 3.8)
rufus-scheduler (~> 3.4)
rugged (~> 0.27)
diff --git a/VERSION b/VERSION
index 60919325d67..8ca9077d87b 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-10.8.0-pre
+11.0.0-pre
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 8ad3d18b302..eb919241318 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -23,6 +23,8 @@ const Api = {
commitPath: '/api/:version/projects/:id/repository/commits',
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
createBranchPath: '/api/:version/projects/:id/repository/branches',
+ pipelinesPath: '/api/:version/projects/:id/pipelines',
+ pipelineJobsPath: '/api/:version/projects/:id/pipelines/:pipeline_id/jobs',
group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
@@ -222,6 +224,20 @@ const Api = {
});
},
+ pipelines(projectPath, params = {}) {
+ const url = Api.buildUrl(this.pipelinesPath).replace(':id', encodeURIComponent(projectPath));
+
+ return axios.get(url, { params });
+ },
+
+ pipelineJobs(projectPath, pipelineId, params = {}) {
+ const url = Api.buildUrl(this.pipelineJobsPath)
+ .replace(':id', encodeURIComponent(projectPath))
+ .replace(':pipeline_id', pipelineId);
+
+ return axios.get(url, { params });
+ },
+
buildUrl(url) {
let urlRoot = '';
if (gon.relative_url_root != null) {
diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue
index c0be72f7401..3da762446c9 100644
--- a/app/assets/javascripts/environments/components/environments_app.vue
+++ b/app/assets/javascripts/environments/components/environments_app.vue
@@ -68,8 +68,7 @@
this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', showLoader);
this.service.getFolderContent(folder.folder_path)
- .then(resp => resp.json())
- .then(response => this.store.setfolderContent(folder, response.environments))
+ .then(response => this.store.setfolderContent(folder, response.data.environments))
.then(() => this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false))
.catch(() => {
Flash(s__('Environments|An error occurred while fetching the environments.'));
diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js
index 34d18d55120..a7a79dbca70 100644
--- a/app/assets/javascripts/environments/mixins/environments_mixin.js
+++ b/app/assets/javascripts/environments/mixins/environments_mixin.js
@@ -6,7 +6,6 @@ import Visibility from 'visibilityjs';
import Poll from '../../lib/utils/poll';
import {
getParameterByName,
- parseQueryStringIntoObject,
} from '../../lib/utils/common_utils';
import { s__ } from '../../locale';
import Flash from '../../flash';
@@ -46,17 +45,14 @@ export default {
methods: {
saveData(resp) {
- const headers = resp.headers;
- return resp.json().then((response) => {
- this.isLoading = false;
-
- if (_.isEqual(parseQueryStringIntoObject(resp.url.split('?')[1]), this.requestData)) {
- this.store.storeAvailableCount(response.available_count);
- this.store.storeStoppedCount(response.stopped_count);
- this.store.storeEnvironments(response.environments);
- this.store.setPagination(headers);
- }
- });
+ this.isLoading = false;
+
+ if (_.isEqual(resp.config.params, this.requestData)) {
+ this.store.storeAvailableCount(resp.data.available_count);
+ this.store.storeStoppedCount(resp.data.stopped_count);
+ this.store.storeEnvironments(resp.data.environments);
+ this.store.setPagination(resp.headers);
+ }
},
/**
@@ -70,7 +66,7 @@ export default {
updateContent(parameters) {
this.updateInternalState(parameters);
// fetch new data
- return this.service.get(this.requestData)
+ return this.service.fetchEnvironments(this.requestData)
.then(response => this.successCallback(response))
.then(() => {
// restart polling
@@ -105,7 +101,7 @@ export default {
fetchEnvironments() {
this.isLoading = true;
- return this.service.get(this.requestData)
+ return this.service.fetchEnvironments(this.requestData)
.then(this.successCallback)
.catch(this.errorCallback);
},
@@ -141,7 +137,7 @@ export default {
this.poll = new Poll({
resource: this.service,
- method: 'get',
+ method: 'fetchEnvironments',
data: this.requestData,
successCallback: this.successCallback,
errorCallback: this.errorCallback,
diff --git a/app/assets/javascripts/environments/services/environments_service.js b/app/assets/javascripts/environments/services/environments_service.js
index 03ab74b3338..3b121551aca 100644
--- a/app/assets/javascripts/environments/services/environments_service.js
+++ b/app/assets/javascripts/environments/services/environments_service.js
@@ -1,25 +1,22 @@
-/* eslint-disable class-methods-use-this */
-import Vue from 'vue';
-import VueResource from 'vue-resource';
-
-Vue.use(VueResource);
+import axios from '~/lib/utils/axios_utils';
export default class EnvironmentsService {
constructor(endpoint) {
- this.environments = Vue.resource(endpoint);
+ this.environmentsEndpoint = endpoint;
this.folderResults = 3;
}
- get(options = {}) {
+ fetchEnvironments(options = {}) {
const { scope, page } = options;
- return this.environments.get({ scope, page });
+ return axios.get(this.environmentsEndpoint, { params: { scope, page } });
}
+ // eslint-disable-next-line class-methods-use-this
postAction(endpoint) {
- return Vue.http.post(endpoint, {}, { emulateJSON: true });
+ return axios.post(endpoint, {}, { emulateJSON: true });
}
getFolderContent(folderUrl) {
- return Vue.http.get(`${folderUrl}.json?per_page=${this.folderResults}`);
+ return axios.get(`${folderUrl}.json?per_page=${this.folderResults}`);
}
}
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
index 4a645c827ad..81961fe3c57 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
@@ -5,7 +5,7 @@ import LoadingButton from '~/vue_shared/components/loading_button.vue';
import CommitMessageField from './message_field.vue';
import Actions from './actions.vue';
import SuccessMessage from './success_message.vue';
-import { activityBarViews, MAX_WINDOW_HEIGHT_COMPACT, COMMIT_ITEM_PADDING } from '../../constants';
+import { activityBarViews, MAX_WINDOW_HEIGHT_COMPACT } from '../../constants';
export default {
components: {
@@ -70,7 +70,7 @@ export default {
? this.$refs.formEl && this.$refs.formEl.offsetHeight
: this.$refs.compactEl && this.$refs.compactEl.offsetHeight;
- this.componentHeight = elHeight + COMMIT_ITEM_PADDING;
+ this.componentHeight = elHeight;
},
enterTransition() {
this.$nextTick(() => {
@@ -78,7 +78,7 @@ export default {
? this.$refs.compactEl && this.$refs.compactEl.offsetHeight
: this.$refs.formEl && this.$refs.formEl.offsetHeight;
- this.componentHeight = elHeight + COMMIT_ITEM_PADDING;
+ this.componentHeight = elHeight;
});
},
afterEndTransition() {
diff --git a/app/assets/javascripts/ide/components/file_finder/index.vue b/app/assets/javascripts/ide/components/file_finder/index.vue
index ea2b13a8b21..cabb3f59b17 100644
--- a/app/assets/javascripts/ide/components/file_finder/index.vue
+++ b/app/assets/javascripts/ide/components/file_finder/index.vue
@@ -39,12 +39,10 @@ export default {
return this.allBlobs.slice(0, MAX_FILE_FINDER_RESULTS);
}
- return fuzzaldrinPlus
- .filter(this.allBlobs, searchText, {
- key: 'path',
- maxResults: MAX_FILE_FINDER_RESULTS,
- })
- .sort((a, b) => b.lastOpenedAt - a.lastOpenedAt);
+ return fuzzaldrinPlus.filter(this.allBlobs, searchText, {
+ key: 'path',
+ maxResults: MAX_FILE_FINDER_RESULTS,
+ });
},
filteredBlobsLength() {
return this.filteredBlobs.length;
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index 6c373a92776..1ec69adce09 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -52,7 +52,10 @@ export default {
methods: {
...mapActions(['toggleFileFinder']),
mousetrapStopCallback(e, el, combo) {
- if (combo === 't' && el.classList.contains('dropdown-input-field')) {
+ if (
+ (combo === 't' && el.classList.contains('dropdown-input-field')) ||
+ el.classList.contains('inputarea')
+ ) {
return true;
} else if (combo === 'command+p' || combo === 'ctrl+p') {
return false;
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index f8678b602ac..f2178c06c10 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -43,9 +43,13 @@ export default {
},
},
watch: {
- file(oldVal, newVal) {
+ file(newVal, oldVal) {
+ if (oldVal.pending) {
+ this.removePendingTab(oldVal);
+ }
+
// Compare key to allow for files opened in review mode to be cached differently
- if (newVal.key !== this.file.key) {
+ if (oldVal.key !== this.file.key) {
this.initMonaco();
if (this.currentActivityView !== activityBarViews.edit) {
@@ -99,6 +103,7 @@ export default {
'setFileViewMode',
'setFileEOL',
'updateViewer',
+ 'removePendingTab',
]),
initMonaco() {
if (this.shouldHideEditor) return;
diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue
index 14946f8c9fa..7bc865058c6 100644
--- a/app/assets/javascripts/ide/components/repo_file.vue
+++ b/app/assets/javascripts/ide/components/repo_file.vue
@@ -122,11 +122,11 @@ export default {
<div
class="file"
:class="fileClass"
+ @click="clickFile"
+ role="button"
>
<div
class="file-name"
- @click="clickFile"
- role="button"
>
<span
class="ide-file-name str-truncated"
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
index 48d4cc43198..83fe22f40a4 100644
--- a/app/assets/javascripts/ide/constants.js
+++ b/app/assets/javascripts/ide/constants.js
@@ -5,8 +5,6 @@ export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33;
export const MAX_WINDOW_HEIGHT_COMPACT = 750;
-export const COMMIT_ITEM_PADDING = 32;
-
// Commit message textarea
export const MAX_TITLE_LENGTH = 50;
export const MAX_BODY_LENGTH = 72;
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index adca85dc65b..a21cec4e8d8 100644
--- a/app/assets/javascripts/ide/ide_router.js
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -41,7 +41,7 @@ const router = new VueRouter({
component: EmptyRouterComponent,
children: [
{
- path: ':targetmode(edit|tree|blob)/:branch/*',
+ path: ':targetmode(edit|tree|blob)/*',
component: EmptyRouterComponent,
},
{
@@ -63,23 +63,27 @@ router.beforeEach((to, from, next) => {
.then(() => {
const fullProjectId = `${to.params.namespace}/${to.params.project}`;
- if (to.params.branch) {
- store.dispatch('setCurrentBranchId', to.params.branch);
+ const baseSplit = to.params[0].split('/-/');
+ const branchId = baseSplit[0].slice(-1) === '/' ? baseSplit[0].slice(0, -1) : baseSplit[0];
+
+ if (branchId) {
+ const basePath = baseSplit.length > 1 ? baseSplit[1] : '';
+
+ store.dispatch('setCurrentBranchId', branchId);
store.dispatch('getBranchData', {
projectId: fullProjectId,
- branchId: to.params.branch,
+ branchId,
});
store
.dispatch('getFiles', {
projectId: fullProjectId,
- branchId: to.params.branch,
+ branchId,
})
.then(() => {
- if (to.params[0]) {
- const path =
- to.params[0].slice(-1) === '/' ? to.params[0].slice(0, -1) : to.params[0];
+ if (basePath) {
+ const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath;
const treeEntryKey = Object.keys(store.state.entries).find(
key => key === path && !store.state.entries[key].pending,
);
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index b6baa693104..13aea91d8ba 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -63,7 +63,9 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive
const file = state.entries[path];
commit(types.TOGGLE_LOADING, { entry: file });
return service
- .getFileData(`${gon.relative_url_root ? gon.relative_url_root : ''}${file.url}`)
+ .getFileData(
+ `${gon.relative_url_root ? gon.relative_url_root : ''}${file.url.replace('/-/', '/')}`,
+ )
.then(res => {
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
setPageTitle(pageTitle);
diff --git a/app/assets/javascripts/ide/stores/index.js b/app/assets/javascripts/ide/stores/index.js
index 7c82ce7976b..699710055e3 100644
--- a/app/assets/javascripts/ide/stores/index.js
+++ b/app/assets/javascripts/ide/stores/index.js
@@ -5,6 +5,7 @@ import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import commitModule from './modules/commit';
+import pipelines from './modules/pipelines';
Vue.use(Vuex);
@@ -15,5 +16,6 @@ export default new Vuex.Store({
getters,
modules: {
commit: commitModule,
+ pipelines,
},
});
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index b85246b2502..cd25c3060f2 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -204,17 +204,23 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
dispatch('updateViewer', 'editor', { root: true });
router.push(
- `/project/${rootState.currentProjectId}/blob/${getters.branchName}/${
+ `/project/${rootState.currentProjectId}/blob/${getters.branchName}/-/${
rootGetters.activeFile.path
}`,
);
}
})
.then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH))
- .then(() => dispatch('refreshLastCommitData', {
- projectId: rootState.currentProjectId,
- branchId: rootState.currentBranchId,
- }, { root: true }));
+ .then(() =>
+ dispatch(
+ 'refreshLastCommitData',
+ {
+ projectId: rootState.currentProjectId,
+ branchId: rootState.currentBranchId,
+ },
+ { root: true },
+ ),
+ );
})
.catch(err => {
let errMsg = __('Error committing changes. Please try again.');
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
new file mode 100644
index 00000000000..07f7b201f2e
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
@@ -0,0 +1,49 @@
+import { __ } from '../../../../locale';
+import Api from '../../../../api';
+import flash from '../../../../flash';
+import * as types from './mutation_types';
+
+export const requestLatestPipeline = ({ commit }) => commit(types.REQUEST_LATEST_PIPELINE);
+export const receiveLatestPipelineError = ({ commit }) => {
+ flash(__('There was an error loading latest pipeline'));
+ commit(types.RECEIVE_LASTEST_PIPELINE_ERROR);
+};
+export const receiveLatestPipelineSuccess = ({ commit }, pipeline) =>
+ commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, pipeline);
+
+export const fetchLatestPipeline = ({ dispatch, rootState }, sha) => {
+ dispatch('requestLatestPipeline');
+
+ return Api.pipelines(rootState.currentProjectId, { sha, per_page: '1' })
+ .then(({ data }) => {
+ dispatch('receiveLatestPipelineSuccess', data.pop());
+ })
+ .catch(() => dispatch('receiveLatestPipelineError'));
+};
+
+export const requestJobs = ({ commit }) => commit(types.REQUEST_JOBS);
+export const receiveJobsError = ({ commit }) => {
+ flash(__('There was an error loading jobs'));
+ commit(types.RECEIVE_JOBS_ERROR);
+};
+export const receiveJobsSuccess = ({ commit }, data) => commit(types.RECEIVE_JOBS_SUCCESS, data);
+
+export const fetchJobs = ({ dispatch, state, rootState }, page = '1') => {
+ dispatch('requestJobs');
+
+ Api.pipelineJobs(rootState.currentProjectId, state.latestPipeline.id, {
+ page,
+ })
+ .then(({ data, headers }) => {
+ const nextPage = headers && headers['x-next-page'];
+
+ dispatch('receiveJobsSuccess', data);
+
+ if (nextPage) {
+ dispatch('fetchJobs', nextPage);
+ }
+ })
+ .catch(() => dispatch('receiveJobsError'));
+};
+
+export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/getters.js b/app/assets/javascripts/ide/stores/modules/pipelines/getters.js
new file mode 100644
index 00000000000..d6c91f5b64d
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/getters.js
@@ -0,0 +1,7 @@
+export const hasLatestPipeline = state => !state.isLoadingPipeline && !!state.latestPipeline;
+
+export const failedJobs = state =>
+ state.stages.reduce(
+ (acc, stage) => acc.concat(stage.jobs.filter(job => job.status === 'failed')),
+ [],
+ );
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/index.js b/app/assets/javascripts/ide/stores/modules/pipelines/index.js
new file mode 100644
index 00000000000..b44c3141b81
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/index.js
@@ -0,0 +1,12 @@
+import state from './state';
+import * as actions from './actions';
+import mutations from './mutations';
+import * as getters from './getters';
+
+export default {
+ namespaced: true,
+ state: state(),
+ actions,
+ mutations,
+ getters,
+};
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js
new file mode 100644
index 00000000000..6b5701670a6
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js
@@ -0,0 +1,7 @@
+export const REQUEST_LATEST_PIPELINE = 'REQUEST_LATEST_PIPELINE';
+export const RECEIVE_LASTEST_PIPELINE_ERROR = 'RECEIVE_LASTEST_PIPELINE_ERROR';
+export const RECEIVE_LASTEST_PIPELINE_SUCCESS = 'RECEIVE_LASTEST_PIPELINE_SUCCESS';
+
+export const REQUEST_JOBS = 'REQUEST_JOBS';
+export const RECEIVE_JOBS_ERROR = 'RECEIVE_JOBS_ERROR';
+export const RECEIVE_JOBS_SUCCESS = 'RECEIVE_JOBS_SUCCESS';
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js
new file mode 100644
index 00000000000..2b16e57b386
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js
@@ -0,0 +1,53 @@
+/* eslint-disable no-param-reassign */
+import * as types from './mutation_types';
+
+export default {
+ [types.REQUEST_LATEST_PIPELINE](state) {
+ state.isLoadingPipeline = true;
+ },
+ [types.RECEIVE_LASTEST_PIPELINE_ERROR](state) {
+ state.isLoadingPipeline = false;
+ },
+ [types.RECEIVE_LASTEST_PIPELINE_SUCCESS](state, pipeline) {
+ state.isLoadingPipeline = false;
+
+ if (pipeline) {
+ state.latestPipeline = {
+ id: pipeline.id,
+ status: pipeline.status,
+ };
+ }
+ },
+ [types.REQUEST_JOBS](state) {
+ state.isLoadingJobs = true;
+ },
+ [types.RECEIVE_JOBS_ERROR](state) {
+ state.isLoadingJobs = false;
+ },
+ [types.RECEIVE_JOBS_SUCCESS](state, jobs) {
+ state.isLoadingJobs = false;
+
+ state.stages = jobs.reduce((acc, job) => {
+ let stage = acc.find(s => s.title === job.stage);
+
+ if (!stage) {
+ stage = {
+ title: job.stage,
+ jobs: [],
+ };
+
+ acc.push(stage);
+ }
+
+ stage.jobs = stage.jobs.concat({
+ id: job.id,
+ name: job.name,
+ status: job.status,
+ stage: job.stage,
+ duration: job.duration,
+ });
+
+ return acc;
+ }, state.stages);
+ },
+};
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/state.js b/app/assets/javascripts/ide/stores/modules/pipelines/state.js
new file mode 100644
index 00000000000..6f22542aaea
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/state.js
@@ -0,0 +1,6 @@
+export default () => ({
+ isLoadingPipeline: false,
+ isLoadingJobs: false,
+ latestPipeline: null,
+ stages: [],
+});
diff --git a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js
index d249b05f47c..0a1c253c637 100644
--- a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js
+++ b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js
@@ -26,7 +26,7 @@ self.addEventListener('message', e => {
id: folderPath,
name: folderName,
path: folderPath,
- url: `/${projectId}/tree/${branchId}/${folderPath}/`,
+ url: `/${projectId}/tree/${branchId}/-/${folderPath}/`,
type: 'tree',
parentTreeUrl: parentFolder ? parentFolder.url : `/${projectId}/tree/${branchId}/`,
tempFile,
@@ -64,7 +64,7 @@ self.addEventListener('message', e => {
id: path,
name: blobName,
path,
- url: `/${projectId}/blob/${branchId}/${path}`,
+ url: `/${projectId}/blob/${branchId}/-/${path}`,
type: 'blob',
parentTreeUrl: fileFolder ? fileFolder.url : `/${projectId}/blob/${branchId}`,
tempFile,
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index c3d94d63c13..b7624cf490a 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -2,10 +2,7 @@ import $ from 'jquery';
import timeago from 'timeago.js';
import dateFormat from 'vendor/date.format';
import { pluralize } from './text_utility';
-import {
- languageCode,
- s__,
-} from '../../locale';
+import { languageCode, s__ } from '../../locale';
window.timeago = timeago;
window.dateFormat = dateFormat;
@@ -17,11 +14,37 @@ window.dateFormat = dateFormat;
*
* @param {Boolean} abbreviated
*/
-const getMonthNames = (abbreviated) => {
+const getMonthNames = abbreviated => {
if (abbreviated) {
- return [s__('Jan'), s__('Feb'), s__('Mar'), s__('Apr'), s__('May'), s__('Jun'), s__('Jul'), s__('Aug'), s__('Sep'), s__('Oct'), s__('Nov'), s__('Dec')];
+ return [
+ s__('Jan'),
+ s__('Feb'),
+ s__('Mar'),
+ s__('Apr'),
+ s__('May'),
+ s__('Jun'),
+ s__('Jul'),
+ s__('Aug'),
+ s__('Sep'),
+ s__('Oct'),
+ s__('Nov'),
+ s__('Dec'),
+ ];
}
- return [s__('January'), s__('February'), s__('March'), s__('April'), s__('May'), s__('June'), s__('July'), s__('August'), s__('September'), s__('October'), s__('November'), s__('December')];
+ return [
+ s__('January'),
+ s__('February'),
+ s__('March'),
+ s__('April'),
+ s__('May'),
+ s__('June'),
+ s__('July'),
+ s__('August'),
+ s__('September'),
+ s__('October'),
+ s__('November'),
+ s__('December'),
+ ];
};
/**
@@ -29,7 +52,8 @@ const getMonthNames = (abbreviated) => {
* @param {date} date
* @returns {String}
*/
-export const getDayName = date => ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][date.getDay()];
+export const getDayName = date =>
+ ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][date.getDay()];
/**
* @example
@@ -55,7 +79,7 @@ export function getTimeago() {
if (!timeagoInstance) {
const localeRemaining = function getLocaleRemaining(number, index) {
return [
- [s__('Timeago|less than a minute ago'), s__('Timeago|in a while')],
+ [s__('Timeago|less than a minute ago'), s__('Timeago|right now')],
[s__('Timeago|less than a minute ago'), s__('Timeago|%s seconds remaining')],
[s__('Timeago|about a minute ago'), s__('Timeago|1 minute remaining')],
[s__('Timeago|%s minutes ago'), s__('Timeago|%s minutes remaining')],
@@ -73,7 +97,7 @@ export function getTimeago() {
};
const locale = function getLocale(number, index) {
return [
- [s__('Timeago|less than a minute ago'), s__('Timeago|in a while')],
+ [s__('Timeago|less than a minute ago'), s__('Timeago|right now')],
[s__('Timeago|less than a minute ago'), s__('Timeago|in %s seconds')],
[s__('Timeago|about a minute ago'), s__('Timeago|in 1 minute')],
[s__('Timeago|%s minutes ago'), s__('Timeago|in %s minutes')],
@@ -102,7 +126,7 @@ export function getTimeago() {
* For the given element, renders a timeago instance.
* @param {jQuery} $els
*/
-export const renderTimeago = ($els) => {
+export const renderTimeago = $els => {
const timeagoEls = $els || document.querySelectorAll('.js-timeago-render');
// timeago.js sets timeouts internally for each timeago value to be updated in real time
@@ -119,7 +143,8 @@ export const localTimeAgo = ($timeagoEls, setTimeago = true) => {
if (setTimeago) {
// Recreate with custom template
$(el).tooltip({
- template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
+ template:
+ '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
});
}
@@ -141,7 +166,9 @@ export const timeFor = (time, expiredLabel) => {
if (new Date(time) < new Date()) {
return expiredLabel || s__('Timeago|Past due');
}
- return getTimeago().format(time, `${timeagoLanguageCode}-remaining`).trim();
+ return getTimeago()
+ .format(time, `${timeagoLanguageCode}-remaining`)
+ .trim();
};
export const getDayDifference = (a, b) => {
@@ -161,7 +188,7 @@ export const getDayDifference = (a, b) => {
export function timeIntervalInWords(intervalInSeconds) {
const secondsInteger = parseInt(intervalInSeconds, 10);
const minutes = Math.floor(secondsInteger / 60);
- const seconds = secondsInteger - (minutes * 60);
+ const seconds = secondsInteger - minutes * 60;
let text = '';
if (minutes >= 1) {
@@ -178,8 +205,34 @@ export function dateInWords(date, abbreviated = false, hideYear = false) {
const month = date.getMonth();
const year = date.getFullYear();
- const monthNames = [s__('January'), s__('February'), s__('March'), s__('April'), s__('May'), s__('June'), s__('July'), s__('August'), s__('September'), s__('October'), s__('November'), s__('December')];
- const monthNamesAbbr = [s__('Jan'), s__('Feb'), s__('Mar'), s__('Apr'), s__('May'), s__('Jun'), s__('Jul'), s__('Aug'), s__('Sep'), s__('Oct'), s__('Nov'), s__('Dec')];
+ const monthNames = [
+ s__('January'),
+ s__('February'),
+ s__('March'),
+ s__('April'),
+ s__('May'),
+ s__('June'),
+ s__('July'),
+ s__('August'),
+ s__('September'),
+ s__('October'),
+ s__('November'),
+ s__('December'),
+ ];
+ const monthNamesAbbr = [
+ s__('Jan'),
+ s__('Feb'),
+ s__('Mar'),
+ s__('Apr'),
+ s__('May'),
+ s__('Jun'),
+ s__('Jul'),
+ s__('Aug'),
+ s__('Sep'),
+ s__('Oct'),
+ s__('Nov'),
+ s__('Dec'),
+ ];
const monthName = abbreviated ? monthNamesAbbr[month] : monthNames[month];
@@ -210,7 +263,7 @@ export const monthInWords = (date, abbreviated = false) => {
*
* @param {Date} date
*/
-export const totalDaysInMonth = (date) => {
+export const totalDaysInMonth = date => {
if (!date) {
return 0;
}
@@ -223,12 +276,20 @@ export const totalDaysInMonth = (date) => {
*
* @param {Date} date
*/
-export const getSundays = (date) => {
+export const getSundays = date => {
if (!date) {
return [];
}
- const daysToSunday = ['Saturday', 'Friday', 'Thursday', 'Wednesday', 'Tuesday', 'Monday', 'Sunday'];
+ const daysToSunday = [
+ 'Saturday',
+ 'Friday',
+ 'Thursday',
+ 'Wednesday',
+ 'Tuesday',
+ 'Monday',
+ 'Sunday',
+ ];
const month = date.getMonth();
const year = date.getFullYear();
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
index db1d09eb2f2..70f185e3656 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
@@ -351,7 +351,7 @@ import Cookies from 'js-cookie';
},
getCommitButtonText() {
- const initial = 'Commit conflict resolution';
+ const initial = 'Commit to source branch';
const inProgress = 'Committing...';
return this.state ? this.state.isSubmitting ? inProgress : initial : initial;
diff --git a/app/assets/javascripts/pages/ldap/omniauth_callbacks/index.js b/app/assets/javascripts/pages/ldap/omniauth_callbacks/index.js
new file mode 100644
index 00000000000..edd7e38471b
--- /dev/null
+++ b/app/assets/javascripts/pages/ldap/omniauth_callbacks/index.js
@@ -0,0 +1,3 @@
+import initU2F from '../../../shared/sessions/u2f';
+
+document.addEventListener('DOMContentLoaded', initU2F);
diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue
index fd3491c7fe0..82b4ce083fb 100644
--- a/app/assets/javascripts/pipelines/components/graph/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue
@@ -1,11 +1,21 @@
<script>
import $ from 'jquery';
-import tooltip from '../../../vue_shared/directives/tooltip';
-import Icon from '../../../vue_shared/components/icon.vue';
-import { dasherize } from '../../../lib/utils/text_utility';
-import eventHub from '../../event_hub';
+import axios from '~/lib/utils/axios_utils';
+import { dasherize } from '~/lib/utils/text_utility';
+import { __ } from '~/locale';
+import createFlash from '~/flash';
+import tooltip from '~/vue_shared/directives/tooltip';
+import Icon from '~/vue_shared/components/icon.vue';
+
/**
- * Renders either a cancel, retry or play icon pointing to the given path.
+ * Renders either a cancel, retry or play icon button and handles the post request
+ *
+ * Used in:
+ * - mr widget mini pipeline graph: `mr_widget_pipeline.vue`
+ * - pipelines table
+ * - pipelines table in merge request page
+ * - pipelines table in commit page
+ * - pipelines detail page in big graph
*/
export default {
components: {
@@ -32,16 +42,10 @@ export default {
required: true,
},
- requestFinishedFor: {
- type: String,
- required: false,
- default: '',
- },
},
data() {
return {
isDisabled: false,
- linkRequested: '',
};
},
@@ -51,19 +55,28 @@ export default {
return `${actionIconDash} js-icon-${actionIconDash}`;
},
},
- watch: {
- requestFinishedFor() {
- if (this.requestFinishedFor === this.linkRequested) {
- this.isDisabled = false;
- }
- },
- },
methods: {
+ /**
+ * The request should not be handled here.
+ * However due to this component being used in several
+ * different apps it avoids repetition & complexity.
+ *
+ */
onClickAction() {
$(this.$el).tooltip('hide');
- eventHub.$emit('postAction', this.link);
- this.linkRequested = this.link;
+
this.isDisabled = true;
+
+ axios.post(`${this.link}.json`)
+ .then(() => {
+ this.isDisabled = false;
+ this.$emit('pipelineActionRequestComplete');
+ })
+ .catch(() => {
+ this.isDisabled = false;
+
+ createFlash(__('An error occurred while making the request.'));
+ });
},
},
};
@@ -80,6 +93,6 @@ btn-transparent ci-action-icon-container ci-action-icon-wrapper"
data-container="body"
:disabled="isDisabled"
>
- <icon :name="actionIcon" />
+ <icon :name="actionIcon"/>
</button>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
index 4027d26098f..7bfe11ab8cd 100644
--- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
@@ -42,11 +42,6 @@ export default {
type: Object,
required: true,
},
- requestFinishedFor: {
- type: String,
- required: false,
- default: '',
- },
},
computed: {
@@ -76,11 +71,15 @@ export default {
e.stopPropagation();
});
},
+
+ pipelineActionRequestComplete() {
+ this.$emit('pipelineActionRequestComplete');
+ },
},
};
</script>
<template>
- <div class="ci-job-dropdown-container">
+ <div class="ci-job-dropdown-container dropdown">
<button
v-tooltip
type="button"
@@ -110,7 +109,7 @@ export default {
<job-component
:job="item"
css-class-job-name="mini-pipeline-graph-dropdown-item"
- :request-finished-for="requestFinishedFor"
+ @pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
</li>
</ul>
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index 7b8a5edcbff..4ec67f6c01b 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -16,11 +16,6 @@ export default {
type: Object,
required: true,
},
- requestFinishedFor: {
- type: String,
- required: false,
- default: '',
- },
},
computed: {
@@ -51,6 +46,10 @@ export default {
return className;
},
+
+ refreshPipelineGraph() {
+ this.$emit('refreshPipelineGraph');
+ },
},
};
</script>
@@ -74,7 +73,7 @@ export default {
:key="stage.name"
:stage-connector-class="stageConnectorClass(index, stage)"
:is-first-column="isFirstColumn(index)"
- :request-finished-for="requestFinishedFor"
+ @refreshPipelineGraph="refreshPipelineGraph"
/>
</ul>
</div>
diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue
index c1f0f051b63..27b938c4985 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue
@@ -46,11 +46,6 @@ export default {
required: false,
default: '',
},
- requestFinishedFor: {
- type: String,
- required: false,
- default: '',
- },
},
computed: {
status() {
@@ -84,6 +79,11 @@ export default {
return this.job.status && this.job.status.action && this.job.status.action.path;
},
},
+ methods: {
+ pipelineActionRequestComplete() {
+ this.$emit('pipelineActionRequestComplete');
+ },
+ },
};
</script>
<template>
@@ -126,7 +126,7 @@ export default {
:tooltip-text="status.action.title"
:link="status.action.path"
:action-icon="status.action.icon"
- :request-finished-for="requestFinishedFor"
+ @pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
index 5461fdbbadd..f32368947e8 100644
--- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
@@ -29,12 +29,6 @@ export default {
required: false,
default: '',
},
-
- requestFinishedFor: {
- type: String,
- required: false,
- default: '',
- },
},
methods: {
@@ -49,6 +43,10 @@ export default {
buildConnnectorClass(index) {
return index === 0 && !this.isFirstColumn ? 'left-connector' : '';
},
+
+ pipelineActionRequestComplete() {
+ this.$emit('refreshPipelineGraph');
+ },
},
};
</script>
@@ -75,12 +73,13 @@ export default {
v-if="job.size === 1"
:job="job"
css-class-job-name="build-content"
+ @pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
<dropdown-job-component
v-if="job.size > 1"
:job="job"
- :request-finished-for="requestFinishedFor"
+ @pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
</li>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
index 498a97851fa..0f671ceea21 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
@@ -9,6 +9,7 @@
import CommitComponent from '../../vue_shared/components/commit.vue';
import LoadingButton from '../../vue_shared/components/loading_button.vue';
import Icon from '../../vue_shared/components/icon.vue';
+ import { PIPELINES_TABLE } from '../constants';
/**
* Pipeline table row.
@@ -46,6 +47,7 @@
required: true,
},
},
+ pipelinesTable: PIPELINES_TABLE,
data() {
return {
isRetrying: false,
@@ -297,6 +299,7 @@
v-for="(stage, index) in pipeline.details.stages"
:key="index">
<pipeline-stage
+ :type="$options.pipelinesTable"
:stage="stage"
:update-dropdown="updateGraphDropdown"
/>
diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue
index a65485c05eb..f9769815796 100644
--- a/app/assets/javascripts/pipelines/components/stage.vue
+++ b/app/assets/javascripts/pipelines/components/stage.vue
@@ -21,6 +21,7 @@ import Icon from '../../vue_shared/components/icon.vue';
import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
import JobComponent from './graph/job_component.vue';
import tooltip from '../../vue_shared/directives/tooltip';
+import { PIPELINES_TABLE } from '../constants';
export default {
components: {
@@ -44,6 +45,12 @@ export default {
required: false,
default: false,
},
+
+ type: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
@@ -133,6 +140,16 @@ export default {
isDropdownOpen() {
return this.$el.classList.contains('open');
},
+
+ pipelineActionRequestComplete() {
+ if (this.type === PIPELINES_TABLE) {
+ // warn the table to update
+ eventHub.$emit('refreshPipelinesTable');
+ } else {
+ // close the dropdown in mr widget
+ $(this.$refs.dropdown).dropdown('toggle');
+ }
+ },
},
};
</script>
@@ -151,6 +168,7 @@ export default {
id="stageDropdown"
aria-haspopup="true"
aria-expanded="false"
+ ref="dropdown"
>
<span
@@ -188,6 +206,7 @@ export default {
<job-component
:job="job"
css-class-job-name="mini-pipeline-graph-dropdown-item"
+ @pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
</li>
</ul>
diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js
index b384c7500e7..eaa11a84cb9 100644
--- a/app/assets/javascripts/pipelines/constants.js
+++ b/app/assets/javascripts/pipelines/constants.js
@@ -1,2 +1,2 @@
-// eslint-disable-next-line import/prefer-default-export
export const CANCEL_REQUEST = 'CANCEL_REQUEST';
+export const PIPELINES_TABLE = 'PIPELINES_TABLE';
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js
index de0faf181e5..30b1eee186d 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines.js
@@ -55,11 +55,13 @@ export default {
eventHub.$on('postAction', this.postAction);
eventHub.$on('retryPipeline', this.postAction);
eventHub.$on('clickedDropdown', this.updateTable);
+ eventHub.$on('refreshPipelinesTable', this.fetchPipelines);
},
beforeDestroy() {
eventHub.$off('postAction', this.postAction);
eventHub.$off('retryPipeline', this.postAction);
eventHub.$off('clickedDropdown', this.updateTable);
+ eventHub.$off('refreshPipelinesTable', this.fetchPipelines);
},
destroyed() {
this.poll.stop();
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index 04fe7958fe6..b49a16a87e6 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -25,30 +25,14 @@ export default () => {
data() {
return {
mediator,
- requestFinishedFor: null,
};
},
- created() {
- eventHub.$on('postAction', this.postAction);
- },
- beforeDestroy() {
- eventHub.$off('postAction', this.postAction);
- },
methods: {
- postAction(action) {
- // Click was made, reset this variable
- this.requestFinishedFor = null;
-
- this.mediator.service
- .postAction(action)
- .then(() => {
- this.mediator.refreshPipeline();
- this.requestFinishedFor = action;
- })
- .catch(() => {
- this.requestFinishedFor = action;
- Flash(__('An error occurred while making the request.'));
- });
+ requestRefreshPipelineGraph() {
+ // When an action is clicked
+ // (wether in the dropdown or in the main nodes, we refresh the big graph)
+ this.mediator.refreshPipeline()
+ .catch(() => Flash(__('An error occurred while making the request.')));
},
},
render(createElement) {
@@ -56,7 +40,9 @@ export default () => {
props: {
isLoading: this.mediator.state.isLoading,
pipeline: this.mediator.store.state.pipeline,
- requestFinishedFor: this.requestFinishedFor,
+ },
+ on: {
+ refreshPipelineGraph: this.requestRefreshPipelineGraph,
},
});
},
diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js
index a4d10850471..78f7353eb0d 100644
--- a/app/assets/javascripts/shortcuts_navigation.js
+++ b/app/assets/javascripts/shortcuts_navigation.js
@@ -7,7 +7,7 @@ export default class ShortcutsNavigation extends Shortcuts {
super();
Mousetrap.bind('g p', () => findAndFollowLink('.shortcuts-project'));
- Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-project-activity'));
+ Mousetrap.bind('g v', () => findAndFollowLink('.shortcuts-project-activity'));
Mousetrap.bind('g f', () => findAndFollowLink('.shortcuts-tree'));
Mousetrap.bind('g c', () => findAndFollowLink('.shortcuts-commits'));
Mousetrap.bind('g j', () => findAndFollowLink('.shortcuts-builds'));
@@ -16,9 +16,10 @@ export default class ShortcutsNavigation extends Shortcuts {
Mousetrap.bind('g i', () => findAndFollowLink('.shortcuts-issues'));
Mousetrap.bind('g b', () => findAndFollowLink('.shortcuts-issue-boards'));
Mousetrap.bind('g m', () => findAndFollowLink('.shortcuts-merge_requests'));
- Mousetrap.bind('g t', () => findAndFollowLink('.shortcuts-todos'));
Mousetrap.bind('g w', () => findAndFollowLink('.shortcuts-wiki'));
Mousetrap.bind('g s', () => findAndFollowLink('.shortcuts-snippets'));
+ Mousetrap.bind('g k', () => findAndFollowLink('.shortcuts-kubernetes'));
+ Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-environments'));
Mousetrap.bind('i', () => findAndFollowLink('.shortcuts-new-issue'));
this.enabledHelp.push('.hidden-shortcut.project');
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue
index cb6e9858736..8338fde61c7 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue
@@ -2,7 +2,7 @@
import tooltip from '../../vue_shared/directives/tooltip';
export default {
- name: 'MRWidgetAuthor',
+ name: 'MrWidgetAuthor',
directives: {
tooltip,
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue
index 8f1fd809a81..644e4b7d81a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue
@@ -1,10 +1,10 @@
<script>
- import mrWidgetAuthor from './mr_widget_author.vue';
+ import MrWidgetAuthor from './mr_widget_author.vue';
export default {
name: 'MRWidgetAuthorTime',
components: {
- mrWidgetAuthor,
+ MrWidgetAuthor,
},
props: {
actionText: {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue
index 7ff7fc7988a..84be9327443 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue
@@ -1,13 +1,13 @@
<script>
import Flash from '../../../flash';
import statusIcon from '../mr_widget_status_icon.vue';
- import mrWidgetAuthor from '../../components/mr_widget_author.vue';
+ import MrWidgetAuthor from '../../components/mr_widget_author.vue';
import eventHub from '../../event_hub';
export default {
name: 'MRWidgetMergeWhenPipelineSucceeds',
components: {
- mrWidgetAuthor,
+ MrWidgetAuthor,
statusIcon,
},
props: {
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 2c30311b1c1..1fb4e3ec761 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -120,6 +120,14 @@ label {
@include box-shadow(none);
border-radius: 2px;
padding: $gl-vert-padding $gl-input-padding;
+
+ &.input-short {
+ width: $input-short-width;
+
+ @media (min-width: $screen-md-min) {
+ width: $input-short-md-width;
+ }
+ }
}
.select-wrapper {
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index f1a8a46dda4..45517416e93 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -279,251 +279,14 @@ ul.indent-list {
padding: 10px 0 0 30px;
}
-
// Specific styles for tree list
@keyframes spin-avatar {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
-.groups-list-tree-container {
- .has-no-search-results {
- text-align: center;
- padding: $gl-padding;
- font-style: italic;
- color: $well-light-text-color;
- }
-
- > .group-list-tree > .group-row.has-children:first-child {
- border-top: 0;
- }
-}
-
-.group-list-tree {
- .avatar-container.content-loading {
- position: relative;
-
- > a,
- > a .avatar {
- height: 100%;
- border-radius: 50%;
- }
-
- > a {
- padding: 2px;
-
- .avatar {
- border: 2px solid $white-normal;
-
- &.identicon {
- line-height: 15px;
- }
- }
- }
-
- &::after {
- content: "";
- position: absolute;
- height: 100%;
- width: 100%;
- background-color: transparent;
- border: 2px outset $kdb-border;
- border-radius: 50%;
- animation: spin-avatar 3s infinite linear;
- }
- }
-
- .folder-toggle-wrap {
- float: left;
- line-height: $list-text-height;
- font-size: 0;
-
- span {
- font-size: $gl-font-size;
- }
- }
-
- .folder-caret,
- .item-type-icon {
- display: inline-block;
- }
-
- .folder-caret {
- width: 15px;
-
- svg {
- margin-bottom: 2px;
- }
- }
-
- .item-type-icon {
- margin-top: 2px;
- width: 20px;
- }
-
- > .group-row:not(.has-children) {
- .folder-caret {
- opacity: 0;
- }
- }
-
- .content-list li:last-child {
- padding-bottom: 0;
- }
-
- .group-list-tree {
- margin-bottom: 0;
- margin-left: 30px;
- position: relative;
-
- &::before {
- content: '';
- display: block;
- width: 0;
- position: absolute;
- top: 5px;
- bottom: 0;
- left: -16px;
- border-left: 2px solid $border-white-normal;
- }
-
- .group-row {
- position: relative;
-
- &::before {
- content: "";
- display: block;
- width: 10px;
- height: 0;
- border-top: 2px solid $border-white-normal;
- position: absolute;
- top: 30px;
- left: -16px;
- }
-
- &:last-child::before {
- background: $white-light;
- height: auto;
- top: 30px;
- bottom: 0;
- }
-
- &.being-removed {
- opacity: 0.5;
- }
- }
- }
-
- .group-row {
- padding: 0;
-
- &.has-children {
- border-top: 0;
- }
-
- &:first-child {
- border-top: 1px solid $white-normal;
- }
-
- &:last-of-type {
- .group-row-contents:not(:hover) {
- border-bottom: 1px solid transparent;
- }
- }
- }
-
- .group-row-contents {
- padding: 10px 10px 8px;
- border-top: solid 1px transparent;
- border-bottom: solid 1px $white-normal;
-
- &:hover {
- border-color: $row-hover-border;
- background-color: $row-hover;
- cursor: pointer;
- }
-
- .avatar-container > a {
- width: 100%;
- text-decoration: none;
- }
-
- &.has-more-items {
- display: block;
- padding: 20px 10px;
- }
-
- .stats {
- position: relative;
- line-height: 46px;
-
- > span {
- display: inline-flex;
- align-items: center;
- height: 16px;
- min-width: 30px;
- }
-
- > span:last-child {
- margin-right: 0;
- }
-
- .stat-value {
- margin: 2px 0 0 5px;
- }
- }
-
- .controls {
- margin-left: 5px;
-
- > .btn {
- margin-right: $btn-xs-side-margin;
- }
- }
- }
-
- .project-row-contents .stats {
- line-height: inherit;
-
- > span:first-child {
- margin-left: 25px;
- }
-
- .item-visibility {
- margin-right: 0;
- }
-
- .last-updated {
- position: absolute;
- right: 12px;
- min-width: 250px;
- text-align: right;
- color: $gl-text-color-secondary;
- }
- }
-}
-
.namespace-title {
.tooltip-inner {
max-width: 350px;
}
}
-
-ul.group-list-tree {
- li.group-row {
- > .group-row-contents .title {
- line-height: $list-text-height;
- }
-
- &.has-description > .group-row-contents .title {
- line-height: inherit;
- }
- }
-}
-
-.js-groups-list-holder {
- .groups-list-loading {
- font-size: 34px;
- text-align: center;
- }
-}
diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
index 66dbe403385..937638d50e8 100644
--- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss
+++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
@@ -128,14 +128,6 @@
/* Large devices (large desktops, 1200px and up) */
@media (min-width: $screen-lg-min) { width: 250px; }
-
- &.input-short {
- /* Medium devices (desktops, 992px and up) */
- @media (min-width: $screen-md-min) { width: 170px; }
-
- /* Large devices (large desktops, 1200px and up) */
- @media (min-width: $screen-lg-min) { width: 210px; }
- }
}
@media (max-width: $screen-xs-max) {
@@ -164,10 +156,6 @@
}
}
- .input-short {
- width: 100%;
- }
-
.icon-label {
display: inline-block;
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index b5505538541..b8ea8ee14c5 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -554,6 +554,8 @@ $input-danger-border: $red-400;
$input-group-addon-bg: #f7f8fa;
$gl-field-focus-shadow: rgba(0, 0, 0, 0.075);
$gl-field-focus-shadow-error: rgba($red-500, 0.6);
+$input-short-width: 200px;
+$input-short-md-width: 280px;
/*
* Help
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 944996159d7..0a27c6e0a25 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -329,6 +329,10 @@
&.invalid {
@include status-color($gray-dark, $gray, $gray-darkest);
border-color: $gray-darkest;
+
+ &:not(span):hover {
+ color: $gray;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 6ee8b33bd39..409b7285f82 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -18,6 +18,10 @@
.group-row {
@include basic-list-stats;
+
+ .description p {
+ margin-bottom: 0;
+ }
}
.ldap-group-links {
@@ -237,3 +241,231 @@
overflow-y: unset;
}
}
+
+.groups-list-tree-container {
+ .has-no-search-results {
+ text-align: center;
+ padding: $gl-padding;
+ font-style: italic;
+ color: $well-light-text-color;
+ }
+
+ > .group-list-tree > .group-row.has-children:first-child {
+ border-top: 0;
+ }
+}
+
+.group-list-tree {
+ .avatar-container.content-loading {
+ position: relative;
+
+ > a,
+ > a .avatar {
+ height: 100%;
+ border-radius: 50%;
+ }
+
+ > a {
+ padding: 2px;
+
+ .avatar {
+ border: 2px solid $white-normal;
+
+ &.identicon {
+ line-height: 15px;
+ }
+ }
+ }
+
+ &::after {
+ content: "";
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ background-color: transparent;
+ border: 2px outset $kdb-border;
+ border-radius: 50%;
+ animation: spin-avatar 3s infinite linear;
+ }
+ }
+
+ .folder-toggle-wrap {
+ float: left;
+ line-height: $list-text-height;
+ font-size: 0;
+
+ span {
+ font-size: $gl-font-size;
+ }
+ }
+
+ .folder-caret,
+ .item-type-icon {
+ display: inline-block;
+ }
+
+ .folder-caret {
+ width: 15px;
+
+ svg {
+ margin-bottom: 2px;
+ }
+ }
+
+ .item-type-icon {
+ margin-top: 2px;
+ width: 20px;
+ }
+
+ > .group-row:not(.has-children) {
+ .folder-caret {
+ opacity: 0;
+ }
+ }
+
+ .content-list li:last-child {
+ padding-bottom: 0;
+ }
+
+ .group-list-tree {
+ margin-bottom: 0;
+ margin-left: 30px;
+ position: relative;
+
+ &::before {
+ content: '';
+ display: block;
+ width: 0;
+ position: absolute;
+ top: 5px;
+ bottom: 0;
+ left: -16px;
+ border-left: 2px solid $border-white-normal;
+ }
+
+ .group-row {
+ position: relative;
+
+ &::before {
+ content: "";
+ display: block;
+ width: 10px;
+ height: 0;
+ border-top: 2px solid $border-white-normal;
+ position: absolute;
+ top: 30px;
+ left: -16px;
+ }
+
+ &:last-child::before {
+ background: $white-light;
+ height: auto;
+ top: 30px;
+ bottom: 0;
+ }
+
+ &.being-removed {
+ opacity: 0.5;
+ }
+ }
+ }
+
+ .group-row {
+ padding: 0;
+
+ &.has-children {
+ border-top: 0;
+ }
+
+ &:first-child {
+ border-top: 1px solid $white-normal;
+ }
+ }
+
+ .group-row-contents {
+ padding: $gl-padding-top;
+
+ &:hover {
+ border-color: $row-hover-border;
+ background-color: $row-hover;
+ cursor: pointer;
+ }
+
+ .avatar-container > a {
+ width: 100%;
+ text-decoration: none;
+ }
+
+ &.has-more-items {
+ display: block;
+ padding: 20px 10px;
+ }
+
+ .stats {
+ position: relative;
+ line-height: 46px;
+
+ > span {
+ display: inline-flex;
+ align-items: center;
+ height: 16px;
+ min-width: 30px;
+ }
+
+ > span:last-child {
+ margin-right: 0;
+ }
+
+ .stat-value {
+ margin: 2px 0 0 5px;
+ }
+ }
+
+ .controls {
+ margin-left: 5px;
+
+ > .btn {
+ margin-right: $btn-xs-side-margin;
+ }
+ }
+ }
+
+ .project-row-contents .stats {
+ line-height: inherit;
+
+ > span:first-child {
+ margin-left: 25px;
+ }
+
+ .item-visibility {
+ margin-right: 0;
+ }
+
+ .last-updated {
+ position: absolute;
+ right: 12px;
+ min-width: 250px;
+ text-align: right;
+ color: $gl-text-color-secondary;
+ }
+ }
+}
+
+ul.group-list-tree {
+ li.group-row {
+ > .group-row-contents .title {
+ line-height: $list-text-height;
+ }
+
+ &.has-description > .group-row-contents .title {
+ line-height: inherit;
+ }
+ }
+}
+
+.js-groups-list-holder {
+ .groups-list-loading {
+ font-size: 34px;
+ text-align: center;
+ }
+}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index b2dad4a358a..a8110f069d4 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -197,9 +197,21 @@
}
&.assignee {
- .author_link:hover {
- .author {
- text-decoration: underline;
+ .author_link {
+ display: block;
+ padding-left: 42px;
+ position: relative;
+
+ &:hover {
+ .author {
+ text-decoration: underline;
+ }
+ }
+
+ .avatar {
+ left: 0;
+ position: absolute;
+ top: 0;
}
}
}
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
index 3422829de58..d5ae141dd7e 100644
--- a/app/assets/stylesheets/pages/members.scss
+++ b/app/assets/stylesheets/pages/members.scss
@@ -99,16 +99,6 @@
@media (min-width: $screen-sm-min) {
width: 250px;
}
-
- &.input-short {
- @media (min-width: $screen-md-min) {
- width: 170px;
- }
-
- @media (min-width: $screen-lg-min) {
- width: 210px;
- }
- }
}
}
diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss
index 04bde64c752..3d5ed9ef3c5 100644
--- a/app/assets/stylesheets/pages/merge_conflicts.scss
+++ b/app/assets/stylesheets/pages/merge_conflicts.scss
@@ -286,6 +286,14 @@ $colors: (
}
.resolve-conflicts-form {
- padding-top: $gl-padding;
+ h4 {
+ margin-top: 0;
+ }
+
+ .resolve-info {
+ @media (max-width: $screen-md-max) {
+ margin-bottom: $gl-padding;
+ }
+ }
}
}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 02803e7b040..1264d977b2f 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -66,13 +66,9 @@
}
}
- .btn-group {
- &.open {
- .btn-default {
- background-color: $white-normal;
- border-color: $border-white-normal;
- }
- }
+ .btn-group.open .btn-default {
+ background-color: $white-normal;
+ border-color: $border-white-normal;
}
.btn .text-center {
@@ -361,16 +357,14 @@
&:not(:first-child) {
margin-left: 44px;
- .left-connector {
- &::before {
- content: '';
- position: absolute;
- top: 48%;
- left: -44px;
- border-top: 2px solid $border-color;
- width: 44px;
- height: 1px;
- }
+ .left-connector::before {
+ content: '';
+ position: absolute;
+ top: 48%;
+ left: -44px;
+ border-top: 2px solid $border-color;
+ width: 44px;
+ height: 1px;
}
}
}
@@ -386,22 +380,16 @@
&:last-child {
.build {
// Remove right connecting horizontal line from first build in last stage
- &:first-child {
- &::after {
- border: 0;
- }
+ &:first-child::after {
+ border: 0;
}
// Remove right curved connectors from all builds in last stage
- &:not(:first-child) {
- &::after {
- border: 0;
- }
+ &:not(:first-child)::after {
+ border: 0;
}
// Remove opposite curve
- .curve {
- &::before {
- display: none;
- }
+ .curve::before {
+ display: none;
}
}
}
@@ -409,16 +397,12 @@
&:first-child {
.build {
// Remove left curved connectors from all builds in first stage
- &:not(:first-child) {
- &::before {
- border: 0;
- }
+ &:not(:first-child)::before {
+ border: 0;
}
// Remove opposite curve
- .curve {
- &::after {
- display: none;
- }
+ .curve::after {
+ display: none;
}
}
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index dd0cb2c2613..53d021086bf 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -871,12 +871,6 @@ pre.light-well {
margin: 0;
}
-.commits-search-form {
- .input-short {
- min-width: 200px;
- }
-}
-
.git-clone-holder {
width: 380px;
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 00457717f00..175d2779bb7 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -39,12 +39,15 @@
.ide-file-list {
flex: 1;
+ padding-left: $gl-padding;
+ padding-right: $gl-padding;
+ padding-bottom: $grid-size;
.file {
cursor: pointer;
&.file-open {
- background: $link-active-background;
+ background: $white-normal;
}
&.file-active {
@@ -84,12 +87,11 @@
.ide-new-btn {
display: none;
- margin-right: -8px;
}
&:hover,
&:focus {
- background: $link-active-background;
+ background: $white-normal;
.ide-new-btn {
display: block;
@@ -111,12 +113,11 @@
}
}
-.file-name,
-.file-col-commit-message {
+.file-name {
display: flex;
overflow: visible;
align-items: center;
- padding: 6px 12px;
+ width: 100%;
}
.multi-file-loading-container {
@@ -306,8 +307,18 @@
}
.preview-container {
- height: 100%;
- overflow: auto;
+ flex-grow: 1;
+ position: relative;
+
+ .md-previewer {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ overflow: auto;
+ padding: $gl-padding;
+ }
.file-container {
background-color: $gray-darker;
@@ -347,10 +358,6 @@
color: $diff-image-info-color;
}
}
-
- .md-previewer {
- padding: $gl-padding;
- }
}
.ide-mode-tabs {
@@ -501,7 +508,7 @@
align-items: center;
margin-bottom: 0;
border-bottom: 1px solid $white-dark;
- padding: $gl-btn-padding $gl-padding;
+ padding: 12px 0;
}
.multi-file-commit-panel-header-title {
@@ -523,32 +530,31 @@
.multi-file-commit-list {
flex: 1;
overflow: auto;
- padding: $gl-padding;
+ padding: $grid-size 0;
+ margin-left: -$grid-size;
+ margin-right: -$grid-size;
min-height: 60px;
+
+ .multi-file-commit-list-item {
+ margin-left: 0;
+ margin-right: 0;
+ }
+
+ &.help-block {
+ margin-left: 0;
+ right: 0;
+ }
}
.multi-file-commit-list-item {
- display: flex;
- padding: 0;
- align-items: center;
- border-radius: $border-radius-default;
-
.multi-file-discard-btn {
display: none;
margin-top: -2px;
margin-left: auto;
- margin-right: $grid-size;
color: $gl-link-color;
-
- &:focus,
- &:hover {
- text-decoration: underline;
- }
}
&:hover {
- background: $white-normal;
-
.multi-file-discard-btn {
display: flex;
}
@@ -584,25 +590,39 @@
}
}
+.multi-file-commit-list-item,
+.ide-file-list .file {
+ display: flex;
+ align-items: center;
+ margin-left: -$grid-size;
+ margin-right: -$grid-size;
+ padding: $grid-size / 2 $grid-size;
+ border-radius: $border-radius-default;
+ text-align: left;
+
+ &:hover,
+ &:focus {
+ background: $white-normal;
+ }
+}
+
.multi-file-commit-list-path {
- padding: $grid-size / 2;
- padding-left: $grid-size;
+ padding: 0;
background: none;
border: 0;
text-align: left;
width: 100%;
- min-width: 0;
+
+ &:hover,
+ &:focus {
+ outline: 0;
+ }
svg {
min-width: 16px;
vertical-align: middle;
display: inline-block;
}
-
- &:hover,
- &:focus {
- outline: 0;
- }
}
.multi-file-commit-list-file-path {
@@ -619,12 +639,18 @@
.multi-file-commit-form {
position: relative;
- padding: $gl-padding;
background-color: $white-light;
- border-top: 1px solid $white-dark;
border-left: 1px solid $white-dark;
transition: all 0.3s ease;
+ > form,
+ > .commit-form-compact {
+ padding: $gl-padding 0;
+ margin-left: $gl-padding;
+ margin-right: $gl-padding;
+ border-top: 1px solid $white-dark;
+ }
+
.btn {
font-size: $gl-font-size;
}
@@ -787,8 +813,9 @@
display: flex;
flex: 1;
flex-direction: column;
- width: 100%;
min-height: 140px;
+ margin-left: $gl-padding;
+ margin-right: $gl-padding;
&.is-first {
border-bottom: 1px solid $white-dark;
@@ -979,9 +1006,8 @@
.ide-tree-header {
display: flex;
align-items: center;
- padding: 10px 0;
- margin-left: 10px;
- margin-right: 10px;
+ margin-bottom: 8px;
+ padding: 12px 0;
border-bottom: 1px solid $white-dark;
.ide-new-btn {
@@ -1012,9 +1038,9 @@
.commit-form-slide-up-enter-active,
.commit-form-slide-up-leave-active {
position: absolute;
- top: 16px;
- left: 16px;
- right: 16px;
+ top: 0;
+ left: 0;
+ right: 0;
transition: all 0.3s ease;
}
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 8958eab0423..cdfe3d6ab1e 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -52,7 +52,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
private
def set_application_setting
- @application_setting = ApplicationSetting.current
+ @application_setting = ApplicationSetting.current_without_cache
end
def application_setting_params
diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb
index 7d7ff217e5d..09e143c23e8 100644
--- a/app/controllers/boards/issues_controller.rb
+++ b/app/controllers/boards/issues_controller.rb
@@ -94,7 +94,7 @@ module Boards
def serialize_as_json(resource)
resource.as_json(
- only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position],
+ only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position, :weight],
labels: true,
issue_endpoints: true,
include_full_project_path: board.group_board?,
diff --git a/app/controllers/groups/boards_controller.rb b/app/controllers/groups/boards_controller.rb
index 7c2016f0326..e892d1f8dbf 100644
--- a/app/controllers/groups/boards_controller.rb
+++ b/app/controllers/groups/boards_controller.rb
@@ -2,19 +2,24 @@ class Groups::BoardsController < Groups::ApplicationController
include BoardsResponses
before_action :assign_endpoint_vars
+ before_action :boards, only: :index
def index
- @boards = Boards::ListService.new(group, current_user).execute
-
respond_with_boards
end
def show
- @board = group.boards.find(params[:id])
+ @board = boards.find(params[:id])
respond_with_board
end
+ private
+
+ def boards
+ @boards ||= Boards::ListService.new(group, current_user).execute
+ end
+
def assign_endpoint_vars
@boards_endpoint = group_boards_url(group)
@namespace_path = group.to_param
diff --git a/app/controllers/groups/settings/badges_controller.rb b/app/controllers/groups/settings/badges_controller.rb
index edb334a3d88..ccbd0a3bc02 100644
--- a/app/controllers/groups/settings/badges_controller.rb
+++ b/app/controllers/groups/settings/badges_controller.rb
@@ -1,12 +1,12 @@
module Groups
module Settings
class BadgesController < Groups::ApplicationController
- include GrapeRouteHelpers::NamedRouteMatcher
+ include API::Helpers::RelatedResourcesHelpers
before_action :authorize_admin_group!
def index
- @badge_api_endpoint = api_v4_groups_badges_path(id: @group.id)
+ @badge_api_endpoint = expose_url(api_v4_groups_badges_path(id: @group.id))
end
end
end
diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb
index f0e5d2aa94e..12a6cd11f80 100644
--- a/app/controllers/profiles/keys_controller.rb
+++ b/app/controllers/profiles/keys_controller.rb
@@ -23,7 +23,7 @@ class Profiles::KeysController < Profiles::ApplicationController
def destroy
@key = current_user.keys.find(params[:id])
- @key.destroy
+ Keys::DestroyService.new(current_user).execute(@key)
respond_to do |format|
format.html { redirect_to profile_keys_url, status: 302 }
diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb
index 949e54ff819..e7354a9e1f7 100644
--- a/app/controllers/projects/boards_controller.rb
+++ b/app/controllers/projects/boards_controller.rb
@@ -4,22 +4,25 @@ class Projects::BoardsController < Projects::ApplicationController
before_action :check_issues_available!
before_action :authorize_read_board!, only: [:index, :show]
+ before_action :boards, only: :index
before_action :assign_endpoint_vars
def index
- @boards = Boards::ListService.new(project, current_user).execute
-
respond_with_boards
end
def show
- @board = project.boards.find(params[:id])
+ @board = boards.find(params[:id])
respond_with_board
end
private
+ def boards
+ @boards ||= Boards::ListService.new(project, current_user).execute
+ end
+
def assign_endpoint_vars
@boards_endpoint = project_boards_path(project)
@bulk_issues_path = bulk_update_project_issues_path(project)
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index f7417a6a5aa..6b40fc2fe68 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -18,19 +18,12 @@ class Projects::PipelinesController < Projects::ApplicationController
.page(params[:page])
.per(30)
- @running_count = PipelinesFinder
- .new(project, scope: 'running').execute.count
+ @running_count = limited_pipelines_count(project, 'running')
+ @pending_count = limited_pipelines_count(project, 'pending')
+ @finished_count = limited_pipelines_count(project, 'finished')
+ @pipelines_count = limited_pipelines_count(project)
- @pending_count = PipelinesFinder
- .new(project, scope: 'pending').execute.count
-
- @finished_count = PipelinesFinder
- .new(project, scope: 'finished').execute.count
-
- @pipelines_count = PipelinesFinder
- .new(project).execute.count
-
- @pipelines.map(&:commit) # List commits for batch loading
+ Gitlab::Ci::Pipeline::Preloader.preload(@pipelines)
respond_to do |format|
format.html
@@ -41,7 +34,7 @@ class Projects::PipelinesController < Projects::ApplicationController
pipelines: PipelineSerializer
.new(project: @project, current_user: @current_user)
.with_pagination(request, response)
- .represent(@pipelines),
+ .represent(@pipelines, disable_coverage: true),
count: {
all: @pipelines_count,
running: @running_count,
@@ -185,4 +178,10 @@ class Projects::PipelinesController < Projects::ApplicationController
def authorize_update_pipeline!
return access_denied! unless can?(current_user, :update_pipeline, @pipeline)
end
+
+ def limited_pipelines_count(project, scope = nil)
+ finder = PipelinesFinder.new(project, scope: scope)
+
+ view_context.limited_counter_with_delimiter(finder.execute)
+ end
end
diff --git a/app/controllers/projects/prometheus/metrics_controller.rb b/app/controllers/projects/prometheus/metrics_controller.rb
index 1dd886409a5..c6b6243b553 100644
--- a/app/controllers/projects/prometheus/metrics_controller.rb
+++ b/app/controllers/projects/prometheus/metrics_controller.rb
@@ -25,7 +25,7 @@ module Projects
end
def require_prometheus_metrics!
- render_404 unless prometheus_adapter.can_query?
+ render_404 unless prometheus_adapter&.can_query?
end
end
end
diff --git a/app/controllers/projects/settings/badges_controller.rb b/app/controllers/projects/settings/badges_controller.rb
index f7b70dd4b7b..7887bee49c5 100644
--- a/app/controllers/projects/settings/badges_controller.rb
+++ b/app/controllers/projects/settings/badges_controller.rb
@@ -1,12 +1,12 @@
module Projects
module Settings
class BadgesController < Projects::ApplicationController
- include GrapeRouteHelpers::NamedRouteMatcher
+ include API::Helpers::RelatedResourcesHelpers
before_action :authorize_admin_project!
def index
- @badge_api_endpoint = api_v4_projects_badges_path(id: @project.id)
+ @badge_api_endpoint = expose_url(api_v4_projects_badges_path(id: @project.id))
end
end
end
diff --git a/app/controllers/projects/settings/integrations_controller.rb b/app/controllers/projects/settings/integrations_controller.rb
index 1ff08cce8cb..d9fecfecc40 100644
--- a/app/controllers/projects/settings/integrations_controller.rb
+++ b/app/controllers/projects/settings/integrations_controller.rb
@@ -11,7 +11,14 @@ module Projects
@hook = ProjectHook.new
# Services
- @services = @project.find_or_initialize_services
+ @services = @project.find_or_initialize_services(exceptions: service_exceptions)
+ end
+
+ private
+
+ # Returns a list of services that should be hidden from the list
+ def service_exceptions
+ @project.disabled_services.dup
end
end
end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 7ed9b1fc6d0..c6ef79ce15e 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -6,7 +6,7 @@
# klass - actual class like Issue or MergeRequest
# current_user - which user use
# params:
-# scope: 'created-by-me' or 'assigned-to-me' or 'all'
+# scope: 'created_by_me' or 'assigned_to_me' or 'all'
# state: 'opened' or 'closed' or 'all'
# group_id: integer
# project_id: integer
@@ -282,9 +282,9 @@ class IssuableFinder
return items.none if current_user_related? && !current_user
case params[:scope]
- when 'created-by-me', 'authored'
+ when 'created_by_me', 'authored'
items.where(author_id: current_user.id)
- when 'assigned-to-me'
+ when 'assigned_to_me'
items.assigned_to(current_user)
else
items
@@ -426,6 +426,7 @@ class IssuableFinder
end
def current_user_related?
- params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me'
+ scope = params[:scope]
+ scope == 'created_by_me' || scope == 'authored' || scope == 'assigned_to_me'
end
end
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index 2a27ff0e386..1787b4899cd 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -5,7 +5,7 @@
# Arguments:
# current_user - which user use
# params:
-# scope: 'created-by-me' or 'assigned-to-me' or 'all'
+# scope: 'created_by_me' or 'assigned_to_me' or 'all'
# state: 'open' or 'closed' or 'all'
# group_id: integer
# project_id: integer
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index 64dc1e6af0f..e2240e5e0d8 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -5,7 +5,7 @@
# Arguments:
# current_user - which user use
# params:
-# scope: 'created-by-me' or 'assigned-to-me' or 'all'
+# scope: 'created_by_me' or 'assigned_to_me' or 'all'
# state: 'open', 'closed', 'merged', or 'all'
# group_id: integer
# project_id: integer
diff --git a/app/finders/personal_projects_finder.rb b/app/finders/personal_projects_finder.rb
index 3ad4bd5f066..5aea0cb8192 100644
--- a/app/finders/personal_projects_finder.rb
+++ b/app/finders/personal_projects_finder.rb
@@ -13,7 +13,7 @@ class PersonalProjectsFinder < UnionFinder
def execute(current_user = nil)
segments = all_projects(current_user)
- find_union(segments, Project).includes(:namespace).order_id_desc
+ find_union(segments, Project).includes(:namespace).order_updated_desc
end
private
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index aa4569500b8..f5d94ad96a1 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -2,6 +2,11 @@ require 'digest/md5'
require 'uri'
module ApplicationHelper
+ # See https://docs.gitlab.com/ee/development/ee_features.html#code-in-app-views
+ def render_if_exists(partial, locals = {})
+ render(partial, locals) if lookup_context.exists?(partial, [], true)
+ end
+
# Check if a particular controller is the current one
#
# args - One or more controller names to check
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index e7a36e20050..3db28fd6da3 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -17,7 +17,9 @@ module BlobHelper
end
def ide_edit_path(project = @project, ref = @ref, path = @path, options = {})
- "#{ide_path}/project#{url_for([project, "edit", "blob", id: [ref, path], script_name: "/"])}"
+ segments = [ide_path, 'project', project.full_path, 'edit', ref]
+ segments.concat(['-', path]) if path.present?
+ File.join(segments)
end
def edit_blob_button(project = @project, ref = @ref, path = @path, options = {})
@@ -331,7 +333,6 @@ module BlobHelper
if !on_top_of_branch?(project, ref)
edit_disabled_button_tag(text, common_classes)
# This condition only applies to users who are logged in
- # Web IDE (Beta) requires the user to have this feature enabled
elsif !current_user || (current_user && can_modify_blob?(blob, project, ref))
edit_link_tag(text, edit_path, common_classes)
elsif can?(current_user, :fork_project, project) && can?(current_user, :create_merge_request_in, project)
diff --git a/app/models/appearance.rb b/app/models/appearance.rb
index f8713138a93..67cc84a9140 100644
--- a/app/models/appearance.rb
+++ b/app/models/appearance.rb
@@ -1,6 +1,6 @@
class Appearance < ActiveRecord::Base
+ include CacheableAttributes
include CacheMarkdownField
- include AfterCommitQueue
include ObjectStorage::BackgroundMove
include WithUploads
@@ -15,16 +15,9 @@ class Appearance < ActiveRecord::Base
mount_uploader :logo, AttachmentUploader
mount_uploader :header_logo, AttachmentUploader
- CACHE_KEY = "current_appearance:#{Gitlab::VERSION}".freeze
-
- after_commit :flush_redis_cache
-
- def self.current
- Rails.cache.fetch(CACHE_KEY) { first }
- end
-
- def flush_redis_cache
- Rails.cache.delete(CACHE_KEY)
+ # Overrides CacheableAttributes.current_without_cache
+ def self.current_without_cache
+ first
end
def single_appearance_row
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 451e512aef7..e8ccb320fae 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -1,11 +1,11 @@
class ApplicationSetting < ActiveRecord::Base
+ include CacheableAttributes
include CacheMarkdownField
include TokenAuthenticatable
add_authentication_token_field :runners_registration_token
add_authentication_token_field :health_check_access_token
- CACHE_KEY = 'application_setting.last'.freeze
DOMAIN_LIST_SEPARATOR = %r{\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace
| # or
\s # any whitespace character
@@ -229,40 +229,6 @@ class ApplicationSetting < ActiveRecord::Base
after_commit do
reset_memoized_terms
- Rails.cache.write(CACHE_KEY, self)
- end
-
- def self.current
- ensure_cache_setup
-
- Rails.cache.fetch(CACHE_KEY) do
- ApplicationSetting.last.tap do |settings|
- # do not cache nils
- raise 'missing settings' unless settings
- end
- end
- rescue
- # Fall back to an uncached value if there are any problems (e.g. redis down)
- ApplicationSetting.last
- end
-
- def self.expire
- Rails.cache.delete(CACHE_KEY)
- rescue
- # Gracefully handle when Redis is not available. For example,
- # omnibus may fail here during gitlab:assets:compile.
- end
-
- def self.cached
- value = Rails.cache.read(CACHE_KEY)
- ensure_cache_setup if value.present?
- value
- end
-
- def self.ensure_cache_setup
- # This is a workaround for a Rails bug that causes attribute methods not
- # to be loaded when read from cache: https://github.com/rails/rails/issues/27348
- ApplicationSetting.define_attribute_methods
end
def self.defaults
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 61c10c427dd..495430931aa 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -184,7 +184,7 @@ module Ci
end
def playable?
- action? && (manual? || complete?)
+ action? && (manual? || retryable?)
end
def action?
@@ -599,6 +599,7 @@ module Ci
break variables unless persisted?
variables
+ .concat(pipeline.persisted_variables)
.append(key: 'CI_JOB_ID', value: id.to_s)
.append(key: 'CI_JOB_TOKEN', value: token, public: false)
.append(key: 'CI_BUILD_ID', value: id.to_s)
@@ -661,7 +662,7 @@ module Ci
Gitlab::Ci::Variables::Collection.new.tap do |variables|
break variables unless gitlab_deploy_token
- variables.append(key: 'CI_DEPLOY_USER', value: gitlab_deploy_token.name)
+ variables.append(key: 'CI_DEPLOY_USER', value: gitlab_deploy_token.username)
variables.append(key: 'CI_DEPLOY_PASSWORD', value: gitlab_deploy_token.token, public: false)
end
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 1f49764e7cc..53af87a271a 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -406,7 +406,18 @@ module Ci
end
def has_warnings?
- builds.latest.failed_but_allowed.any?
+ number_of_warnings.positive?
+ end
+
+ def number_of_warnings
+ BatchLoader.for(id).batch(default_value: 0) do |pipeline_ids, loader|
+ Build.where(commit_id: pipeline_ids)
+ .latest
+ .failed_but_allowed
+ .group(:commit_id)
+ .count
+ .each { |id, amount| loader.call(id, amount) }
+ end
end
def set_config_source
@@ -512,9 +523,14 @@ module Ci
strong_memoize(:legacy_trigger) { trigger_requests.first }
end
+ def persisted_variables
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ variables.append(key: 'CI_PIPELINE_ID', value: id.to_s) if persisted?
+ end
+ end
+
def predefined_variables
Gitlab::Ci::Variables::Collection.new
- .append(key: 'CI_PIPELINE_ID', value: id.to_s)
.append(key: 'CI_CONFIG_PATH', value: ci_yaml_file_path)
.append(key: 'CI_PIPELINE_SOURCE', value: source.to_s)
.append(key: 'CI_COMMIT_MESSAGE', value: git_commit_message)
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index e6f1ed519be..530eacf4be0 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -75,7 +75,7 @@ module Ci
project_type: 3
}
- cached_attr_reader :version, :revision, :platform, :architecture, :contacted_at, :ip_address
+ cached_attr_reader :version, :revision, :platform, :architecture, :ip_address, :contacted_at
chronic_duration_attr :maximum_timeout_human_readable, :maximum_timeout
diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb
index 7b25d8c4089..c702c4ee807 100644
--- a/app/models/clusters/applications/prometheus.rb
+++ b/app/models/clusters/applications/prometheus.rb
@@ -49,6 +49,11 @@ module Clusters
# ensures headers containing auth data are appended to original k8s client options
options = kube_client.rest_client.options.merge(headers: kube_client.headers)
RestClient::Resource.new(proxy_url, options)
+ rescue Kubeclient::HttpError
+ # If users have mistakenly set parameters or removed the depended clusters,
+ # `proxy_url` could raise an exception because gitlab can not communicate with the cluster.
+ # Since `PrometheusAdapter#can_query?` is eargely loaded on environement pages in gitlab,
+ # we need to silence the exceptions
end
private
diff --git a/app/models/commit.rb b/app/models/commit.rb
index b46f9f34689..56d4c86774e 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -224,8 +224,34 @@ class Commit
Gitlab::ClosingIssueExtractor.new(project, current_user).closed_by_message(safe_message)
end
+ def lazy_author
+ BatchLoader.for(author_email.downcase).batch do |emails, loader|
+ # A Hash that maps user Emails to the corresponding User objects. The
+ # Emails at this point are the _primary_ Emails of the Users.
+ users_for_emails = User
+ .by_any_email(emails)
+ .each_with_object({}) { |user, hash| hash[user.email] = user }
+
+ users_for_ids = users_for_emails
+ .values
+ .each_with_object({}) { |user, hash| hash[user.id] = user }
+
+ # Some commits may have used an alternative Email address. In this case we
+ # need to query the "emails" table to map those addresses to User objects.
+ Email
+ .where(email: emails - users_for_emails.keys)
+ .pluck(:email, :user_id)
+ .each { |(email, id)| users_for_emails[email] = users_for_ids[id] }
+
+ users_for_emails.each { |email, user| loader.call(email, user) }
+ end
+ end
+
def author
- User.find_by_any_email(author_email.downcase)
+ # We use __sync so that we get the actual objects back (including an actual
+ # nil), instead of a wrapper, as returning a wrapped nil breaks a lot of
+ # code.
+ lazy_author.__sync
end
request_cache(:author) { author_email.downcase }
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 97d89422594..a7d05722287 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -2,6 +2,7 @@ class CommitStatus < ActiveRecord::Base
include HasStatus
include Importable
include AfterCommitQueue
+ include Presentable
self.table_name = 'ci_builds'
diff --git a/app/models/concerns/cacheable_attributes.rb b/app/models/concerns/cacheable_attributes.rb
new file mode 100644
index 00000000000..b32459fdabf
--- /dev/null
+++ b/app/models/concerns/cacheable_attributes.rb
@@ -0,0 +1,54 @@
+module CacheableAttributes
+ extend ActiveSupport::Concern
+
+ included do
+ after_commit { self.class.expire }
+ end
+
+ class_methods do
+ # Can be overriden
+ def current_without_cache
+ last
+ end
+
+ def cache_key
+ "#{name}:#{Gitlab::VERSION}:#{Gitlab.migrations_hash}:json".freeze
+ end
+
+ def defaults
+ {}
+ end
+
+ def build_from_defaults(attributes = {})
+ new(defaults.merge(attributes))
+ end
+
+ def cached
+ json_attributes = Rails.cache.read(cache_key)
+ return nil unless json_attributes.present?
+
+ build_from_defaults(JSON.parse(json_attributes))
+ end
+
+ def current
+ cached_record = cached
+ return cached_record if cached_record.present?
+
+ current_without_cache.tap { |current_record| current_record&.cache! }
+ rescue
+ # Fall back to an uncached value if there are any problems (e.g. Redis down)
+ current_without_cache
+ end
+
+ def expire
+ Rails.cache.delete(cache_key)
+ rescue
+ # Gracefully handle when Redis is not available. For example,
+ # omnibus may fail here during gitlab:assets:compile.
+ end
+ end
+
+ def cache!
+ Rails.cache.write(self.class.cache_key, attributes.to_json)
+ end
+end
diff --git a/app/models/concerns/redis_cacheable.rb b/app/models/concerns/redis_cacheable.rb
index b889f4202dc..b5425295130 100644
--- a/app/models/concerns/redis_cacheable.rb
+++ b/app/models/concerns/redis_cacheable.rb
@@ -7,7 +7,11 @@ module RedisCacheable
class_methods do
def cached_attr_reader(*attributes)
attributes.each do |attribute|
- define_method("#{attribute}") do
+ define_method(attribute) do
+ unless self.has_attribute?(attribute)
+ raise ArgumentError, "`cached_attr_reader` requires the #{self.class.name}\##{attribute} attribute to have a database column"
+ end
+
cached_attribute(attribute) || read_attribute(attribute)
end
end
@@ -15,13 +19,16 @@ module RedisCacheable
end
def cached_attribute(attribute)
- (cached_attributes || {})[attribute]
+ cached_value = (cached_attributes || {})[attribute]
+ cast_value_from_cache(attribute, cached_value) if cached_value
end
def cache_attributes(values)
Gitlab::Redis::SharedState.with do |redis|
redis.set(cache_attribute_key, values.to_json, ex: CACHED_ATTRIBUTES_EXPIRY_TIME)
end
+
+ clear_memoization(:cached_attributes)
end
private
@@ -38,4 +45,12 @@ module RedisCacheable
end
end
end
+
+ def cast_value_from_cache(attribute, value)
+ if Gitlab.rails5?
+ self.class.type_for_attribute(attribute).cast(value)
+ else
+ self.class.column_for_attribute(attribute).type_cast_from_database(value)
+ end
+ end
end
diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb
index cefa5c13c5f..db7254c27e0 100644
--- a/app/models/concerns/sortable.rb
+++ b/app/models/concerns/sortable.rb
@@ -12,8 +12,8 @@ module Sortable
scope :order_created_asc, -> { reorder(created_at: :asc) }
scope :order_updated_desc, -> { reorder(updated_at: :desc) }
scope :order_updated_asc, -> { reorder(updated_at: :asc) }
- scope :order_name_asc, -> { reorder(name: :asc) }
- scope :order_name_desc, -> { reorder(name: :desc) }
+ scope :order_name_asc, -> { reorder("lower(name) asc") }
+ scope :order_name_desc, -> { reorder("lower(name) desc") }
end
module ClassMethods
diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb
index 73fc5048dcf..1caf47072bc 100644
--- a/app/models/concerns/time_trackable.rb
+++ b/app/models/concerns/time_trackable.rb
@@ -53,6 +53,10 @@ module TimeTrackable
Gitlab::TimeTrackingFormatter.output(time_estimate)
end
+ def time_estimate=(val)
+ val.is_a?(Integer) ? super([val, Gitlab::Database::MAX_INT_VALUE].min) : super(val)
+ end
+
private
def touchable?
diff --git a/app/models/project.rb b/app/models/project.rb
index 0975e64e995..0e727664d39 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -894,6 +894,13 @@ class Project < ActiveRecord::Base
Gitlab::Routing.url_helpers.project_url(self)
end
+ def readme_url
+ readme = repository.readme
+ if readme
+ Gitlab::Routing.url_helpers.project_blob_url(self, File.join(default_branch, readme.path))
+ end
+ end
+
def new_issuable_address(author, address_type)
return unless Gitlab::IncomingEmail.supports_issue_creation? && author
@@ -997,7 +1004,7 @@ class Project < ActiveRecord::Base
available_services_names = Service.available_services_names - exceptions
- available_services_names.map do |service_name|
+ available_services = available_services_names.map do |service_name|
service = find_service(services, service_name)
if service
@@ -1014,6 +1021,14 @@ class Project < ActiveRecord::Base
end
end
end
+
+ available_services.reject do |service|
+ disabled_services.include?(service.to_param)
+ end
+ end
+
+ def disabled_services
+ []
end
def find_or_initialize_service(name)
diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb
index 26cbfd784ad..84248f9590b 100644
--- a/app/models/project_services/gemnasium_service.rb
+++ b/app/models/project_services/gemnasium_service.rb
@@ -3,6 +3,7 @@ require "gemnasium/gitlab_service"
class GemnasiumService < Service
prop_accessor :token, :api_key
validates :token, :api_key, presence: true, if: :activated?
+ validate :deprecation_validation
def title
'Gemnasium'
@@ -27,6 +28,18 @@ class GemnasiumService < Service
%w(push)
end
+ def deprecated?
+ true
+ end
+
+ def deprecation_message
+ "Gemnasium has been acquired by GitLab in January 2018. Since May 15, 2018, the service provided by Gemnasium is no longer available."
+ end
+
+ def deprecation_validation
+ errors[:base] << deprecation_message
+ end
+
def execute(data)
return unless supported_events.include?(data[:object_kind])
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 44c6bff6b66..0e1bf11d7c0 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -596,7 +596,7 @@ class Repository
cache_method :gitlab_ci_yml
def xcode_project?
- file_on_head(:xcode_config).present?
+ file_on_head(:xcode_config, :tree).present?
end
cache_method :xcode_project?
@@ -920,11 +920,21 @@ class Repository
end
end
- def file_on_head(type)
- if head = tree(:head)
- head.blobs.find do |blob|
- Gitlab::FileDetector.type_of(blob.path) == type
+ def file_on_head(type, object_type = :blob)
+ return unless head = tree(:head)
+
+ objects =
+ case object_type
+ when :blob
+ head.blobs
+ when :tree
+ head.trees
+ else
+ raise ArgumentError, "Object type #{object_type} is not supported"
end
+
+ objects.find do |object|
+ Gitlab::FileDetector.type_of(object.path) == type
end
end
diff --git a/app/models/service.rb b/app/models/service.rb
index f7e3f7590ad..831c2ea1141 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -253,7 +253,6 @@ class Service < ActiveRecord::Base
emails_on_push
external_wiki
flowdock
- gemnasium
hipchat
irker
jira
diff --git a/app/models/user.rb b/app/models/user.rb
index 8ef3c3ceff0..0a838d34054 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -109,7 +109,7 @@ class User < ActiveRecord::Base
has_many :created_projects, foreign_key: :creator_id, class_name: 'Project'
has_many :users_star_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :starred_projects, through: :users_star_projects, source: :project
- has_many :project_authorizations
+ has_many :project_authorizations, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :authorized_projects, through: :project_authorizations, source: :project
has_many :user_interacted_projects
@@ -165,8 +165,7 @@ class User < ActiveRecord::Base
validate :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id }
before_validation :sanitize_attrs
- before_validation :set_notification_email, if: :email_changed?
- before_save :set_notification_email, if: :email_changed? # in case validation is skipped
+ before_validation :set_notification_email, if: :new_record?
before_validation :set_public_email, if: :public_email_changed?
before_save :set_public_email, if: :public_email_changed? # in case validation is skipped
before_save :ensure_incoming_email_token
@@ -179,8 +178,21 @@ class User < ActiveRecord::Base
after_update :username_changed_hook, if: :username_changed?
after_destroy :post_destroy_hook
after_destroy :remove_key_cache
- after_commit :update_emails_with_primary_email, on: :update, if: -> { previous_changes.key?('email') }
- after_commit :update_invalid_gpg_signatures, on: :update, if: -> { previous_changes.key?('email') }
+ after_commit(on: :update) do
+ if previous_changes.key?('email')
+ # Grab previous_email here since previous_changes changes after
+ # #update_emails_with_primary_email and #update_notification_email are called
+ previous_email = previous_changes[:email][0]
+
+ update_emails_with_primary_email(previous_email)
+ update_invalid_gpg_signatures
+
+ if previous_email == notification_email
+ self.notification_email = email
+ save
+ end
+ end
+ end
after_initialize :set_projects_limit
@@ -546,8 +558,7 @@ class User < ActiveRecord::Base
# hash and `_was` variables getting munged.
# By using an `after_commit` instead of `after_update`, we avoid the recursive callback
# scenario, though it then requires us to use the `previous_changes` hash
- def update_emails_with_primary_email
- previous_email = previous_changes[:email][0] # grab this before the DestroyService is called
+ def update_emails_with_primary_email(previous_email)
primary_email_record = emails.find_by(email: email)
Emails::DestroyService.new(self, user: self).execute(primary_email_record) if primary_email_record
@@ -772,13 +783,13 @@ class User < ActiveRecord::Base
end
def set_notification_email
- if notification_email.blank? || !all_emails.include?(notification_email)
+ if notification_email.blank? || all_emails.exclude?(notification_email)
self.notification_email = email
end
end
def set_public_email
- if public_email.blank? || !all_emails.include?(public_email)
+ if public_email.blank? || all_emails.exclude?(public_email)
self.public_email = ''
end
end
diff --git a/app/presenters/ci/build_presenter.rb b/app/presenters/ci/build_presenter.rb
index 4873d7ce662..e0aaa5cb736 100644
--- a/app/presenters/ci/build_presenter.rb
+++ b/app/presenters/ci/build_presenter.rb
@@ -1,16 +1,5 @@
module Ci
- class BuildPresenter < Gitlab::View::Presenter::Delegated
- CALLOUT_FAILURE_MESSAGES = {
- unknown_failure: 'There is an unknown failure, please try again',
- script_failure: 'There has been a script failure. Check the job log for more information',
- api_failure: 'There has been an API failure, please try again',
- stuck_or_timeout_failure: 'There has been a timeout failure or the job got stuck. Check your timeout limits or try again',
- runner_system_failure: 'There has been a runner system failure, please try again',
- missing_dependency_failure: 'There has been a missing dependency failure, check the job log for more information'
- }.freeze
-
- presents :build
-
+ class BuildPresenter < CommitStatusPresenter
def erased_by_user?
# Build can be erased through API, therefore it does not have
# `erased_by` user assigned in that case.
@@ -44,14 +33,6 @@ module Ci
"#{subject.name} - #{detailed_status.status_tooltip}"
end
- def callout_failure_message
- CALLOUT_FAILURE_MESSAGES[failure_reason.to_sym]
- end
-
- def recoverable?
- failed? && !unrecoverable?
- end
-
private
def tooltip_for_badge
@@ -61,9 +42,5 @@ module Ci
def detailed_status
@detailed_status ||= subject.detailed_status(user)
end
-
- def unrecoverable?
- script_failure? || missing_dependency_failure?
- end
end
end
diff --git a/app/presenters/commit_status_presenter.rb b/app/presenters/commit_status_presenter.rb
new file mode 100644
index 00000000000..c7f7aa836bd
--- /dev/null
+++ b/app/presenters/commit_status_presenter.rb
@@ -0,0 +1,24 @@
+class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
+ CALLOUT_FAILURE_MESSAGES = {
+ unknown_failure: 'There is an unknown failure, please try again',
+ script_failure: 'There has been a script failure. Check the job log for more information',
+ api_failure: 'There has been an API failure, please try again',
+ stuck_or_timeout_failure: 'There has been a timeout failure or the job got stuck. Check your timeout limits or try again',
+ runner_system_failure: 'There has been a runner system failure, please try again',
+ missing_dependency_failure: 'There has been a missing dependency failure, check the job log for more information'
+ }.freeze
+
+ presents :build
+
+ def callout_failure_message
+ CALLOUT_FAILURE_MESSAGES[failure_reason.to_sym]
+ end
+
+ def recoverable?
+ failed? && !unrecoverable?
+ end
+
+ def unrecoverable?
+ script_failure? || missing_dependency_failure?
+ end
+end
diff --git a/app/presenters/generic_commit_status_presenter.rb b/app/presenters/generic_commit_status_presenter.rb
new file mode 100644
index 00000000000..da09df29a37
--- /dev/null
+++ b/app/presenters/generic_commit_status_presenter.rb
@@ -0,0 +1,2 @@
+class GenericCommitStatusPresenter < CommitStatusPresenter
+end
diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb
index 6457294b285..f782b411b84 100644
--- a/app/serializers/pipeline_entity.rb
+++ b/app/serializers/pipeline_entity.rb
@@ -4,7 +4,11 @@ class PipelineEntity < Grape::Entity
expose :id
expose :user, using: UserEntity
expose :active?, as: :active
- expose :coverage
+
+ # Coverage isn't always necessary (e.g. when displaying project pipelines in
+ # the UI). Instead of creating an entirely different entity we just allow the
+ # disabling of this specific field whenever necessary.
+ expose :coverage, unless: proc { options[:disable_coverage] }
expose :source
expose :created_at, :updated_at
diff --git a/app/services/boards/issues/create_service.rb b/app/services/boards/issues/create_service.rb
index 7c4a79f555e..3025029755c 100644
--- a/app/services/boards/issues/create_service.rb
+++ b/app/services/boards/issues/create_service.rb
@@ -10,11 +10,15 @@ module Boards
end
def execute
- create_issue(params.merge(label_ids: [list.label_id]))
+ create_issue(params.merge(issue_params))
end
private
+ def issue_params
+ { label_ids: [list.label_id] }
+ end
+
def board
@board ||= parent.boards.find(params.delete(:board_id))
end
diff --git a/app/services/keys/base_service.rb b/app/services/keys/base_service.rb
index f78791932a7..df8e82f5f60 100644
--- a/app/services/keys/base_service.rb
+++ b/app/services/keys/base_service.rb
@@ -2,7 +2,7 @@ module Keys
class BaseService
attr_accessor :user, :params
- def initialize(user, params)
+ def initialize(user, params = {})
@user, @params = user, params
@ip_address = @params.delete(:ip_address)
end
diff --git a/app/services/keys/destroy_service.rb b/app/services/keys/destroy_service.rb
new file mode 100644
index 00000000000..785cfa3a1d8
--- /dev/null
+++ b/app/services/keys/destroy_service.rb
@@ -0,0 +1,12 @@
+module Keys
+ class DestroyService < ::Keys::BaseService
+ def execute(key)
+ key.destroy if destroy_possible?(key)
+ end
+
+ # overriden in EE::Keys::DestroyService
+ def destroy_possible?(key)
+ true
+ end
+ end
+end
diff --git a/app/services/lfs/unlock_file_service.rb b/app/services/lfs/unlock_file_service.rb
index 6c93dc69bb0..7eb89339a92 100644
--- a/app/services/lfs/unlock_file_service.rb
+++ b/app/services/lfs/unlock_file_service.rb
@@ -2,14 +2,14 @@ module Lfs
class UnlockFileService < BaseService
def execute
unless can?(current_user, :push_code, project)
- raise Gitlab::GitAccess::UnauthorizedError, 'You have no permissions'
+ raise Gitlab::GitAccess::UnauthorizedError, _('You have no permissions')
end
unlock_file
rescue Gitlab::GitAccess::UnauthorizedError => ex
error(ex.message, 403)
rescue ActiveRecord::RecordNotFound
- error('Lock not found', 404)
+ error(_('Lock not found'), 404)
rescue => ex
error(ex.message, 500)
end
@@ -24,9 +24,9 @@ module Lfs
success(lock: lock, http_status: :ok)
elsif forced
- error('You must have master access to force delete a lock', 403)
+ error(_('You must have master access to force delete a lock'), 403)
else
- error("#{lock.path} is locked by GitLab User #{lock.user_id}", 403)
+ error(_("%{lock_path} is locked by GitLab User %{lock_user_id}") % { lock_path: lock.path, lock_user_id: lock.user_id }, 403)
end
end
diff --git a/app/services/milestones/base_service.rb b/app/services/milestones/base_service.rb
index 4963601ea8b..cce0863d611 100644
--- a/app/services/milestones/base_service.rb
+++ b/app/services/milestones/base_service.rb
@@ -5,6 +5,7 @@ module Milestones
def initialize(parent, user, params = {})
@parent, @current_user, @params = parent, user, params.dup
+ super
end
end
end
diff --git a/app/uploaders/object_storage.rb b/app/uploaders/object_storage.rb
index a3549cada95..f2a8afccdeb 100644
--- a/app/uploaders/object_storage.rb
+++ b/app/uploaders/object_storage.rb
@@ -103,6 +103,7 @@ module ObjectStorage
end
included do
+ include AfterCommitQueue
after_save on: [:create, :update] do
background_upload(changed_mounts)
end
diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml
index b4d2a789df0..079f371e0ba 100644
--- a/app/views/admin/application_settings/_ci_cd.html.haml
+++ b/app/views/admin/application_settings/_ci_cd.html.haml
@@ -7,7 +7,7 @@
.checkbox
= f.label :auto_devops_enabled do
= f.check_box :auto_devops_enabled
- Enabled Auto DevOps (Beta) for projects by default
+ Enabled Auto DevOps for projects by default
.help-block
It will automatically build, test, and deploy applications based on a predefined CI/CD configuration
= link_to icon('question-circle'), help_page_path('topics/autodevops/index.md')
diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml
index 5a4ed1c3a2a..813ad451b44 100644
--- a/app/views/admin/broadcast_messages/_form.html.haml
+++ b/app/views/admin/broadcast_messages/_form.html.haml
@@ -27,11 +27,11 @@
.col-sm-10
= f.color_field :font, class: "form-control"
.form-group
- = f.label :starts_at, class: 'control-label'
+ = f.label :starts_at, _("Starts at (UTC)"), class: 'control-label'
.col-sm-10.datetime-controls
= f.datetime_select :starts_at, {}, class: 'form-control form-control-inline'
.form-group
- = f.label :ends_at, class: 'control-label'
+ = f.label :ends_at, _("Ends at (UTC)"), class: 'control-label'
.col-sm-10.datetime-controls
= f.datetime_select :ends_at, {}, class: 'form-control form-control-inline'
.form-actions
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 41ef646fc0e..cfa1d9d0f0c 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -2,6 +2,8 @@
- breadcrumb_title "Dashboard"
%div{ class: container_class }
+ = render_if_exists "admin/licenses/breakdown", license: @license
+
.admin-dashboard.prepend-top-default
.row
.col-sm-4
@@ -20,6 +22,7 @@
%h3.text-center
Users:
= approximate_count_with_delimiters(User)
+ = render_if_exists 'users_statistics'
%hr
= link_to 'New user', new_admin_user_path, class: "btn btn-new"
.col-sm-4
@@ -97,6 +100,9 @@
= reply_email
%span.light.pull-right
= boolean_to_icon Gitlab::IncomingEmail.enabled?
+
+ = render_if_exists 'elastic_and_geo'
+
- container_reg = "Container Registry"
%p{ "aria-label" => "#{container_reg}: status " + (Gitlab.config.registry.enabled ? "on" : "off") }
= container_reg
@@ -144,6 +150,9 @@
GitLab Pages
%span.pull-right
= Gitlab::Pages::VERSION
+
+ = render_if_exists 'geo'
+
%p
Ruby
%span.pull-right
diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml
index 6e76e7c2768..99fbbaec487 100644
--- a/app/views/admin/runners/_runner.html.haml
+++ b/app/views/admin/runners/_runner.html.haml
@@ -33,7 +33,7 @@
= tag
%td
- if runner.contacted_at
- #{time_ago_in_words(runner.contacted_at)} ago
+ = time_ago_with_tooltip runner.contacted_at
- else
Never
%td.admin-runner-btn-group-cell
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index 1a3b5e58ed5..e49bf1cffc8 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -45,7 +45,7 @@
.pull-left
= form_tag admin_runners_path, id: 'runners-search', class: 'form-inline', method: :get do
.form-group
- = search_field_tag :search, params[:search], class: 'form-control', placeholder: 'Runner description or token', spellcheck: false
+ = search_field_tag :search, params[:search], class: 'form-control input-short', placeholder: 'Runner description or token', spellcheck: false
= submit_tag 'Search', class: 'btn'
.pull-right.light
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index 1c5b4aecabb..9a3a03a7671 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -121,7 +121,7 @@
%tr
%td.shortcut
.key g
- .key e
+ .key v
%td
Go to the project's activity feed
%tr
@@ -175,6 +175,18 @@
%tr
%td.shortcut
.key g
+ .key e
+ %td
+ Go to environments
+ %tr
+ %td.shortcut
+ .key g
+ .key k
+ %td
+ Go to kubernetes
+ %tr
+ %td.shortcut
+ .key g
.key s
%td
Go to snippets
@@ -219,6 +231,17 @@
%td.shortcut
.key y
%td Go to file permalink
+ %tbody
+ %tr
+ %th
+ %th Web IDE
+ %tr
+ %td.shortcut
+ - if browser.platform.mac?
+ .key &#8984; p
+ - else
+ .key ctrl p
+ %td Go to file
.col-lg-4
%table.shortcut-mappings
%tbody.hidden-shortcut.network{ style: 'display:none' }
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index c3ea592a6b5..f3af15d771d 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -19,7 +19,7 @@
= nav_link(path: 'projects#show', html_options: { class: "fly-out-top-item" } ) do
= link_to project_path(@project) do
%strong.fly-out-top-item-name
- = _('Overview')
+ = _('Project')
%li.divider.fly-out-top-item
= nav_link(path: 'projects#show') do
= link_to project_path(@project), title: _('Project details'), class: 'shortcuts-project' do
@@ -154,7 +154,7 @@
= nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts]) do
= link_to project_pipelines_path(@project), class: 'shortcuts-pipelines' do
.nav-icon-container
- = sprite_icon('pipeline')
+ = sprite_icon('rocket')
%span.nav-item-name
= _('CI / CD')
@@ -212,7 +212,7 @@
- if project_nav_tab? :clusters
- show_cluster_hint = show_gke_cluster_integration_callout?(@project)
= nav_link(controller: [:clusters, :user, :gcp]) do
- = link_to project_clusters_path(@project), title: _('Kubernetes'), class: 'shortcuts-cluster' do
+ = link_to project_clusters_path(@project), title: _('Kubernetes'), class: 'shortcuts-kubernetes' do
%span
= _('Kubernetes')
- if show_cluster_hint
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index a066f9f4cca..63fc5c6d05a 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -22,7 +22,7 @@
%hr
%p
- - link_to_auto_devops_settings = link_to(s_('AutoDevOps|enable Auto DevOps (Beta)'), project_settings_ci_cd_path(@project, anchor: 'autodevops-settings'))
+ - link_to_auto_devops_settings = link_to(s_('AutoDevOps|enable Auto DevOps'), project_settings_ci_cd_path(@project, anchor: 'autodevops-settings'))
- link_to_add_kubernetes_cluster = link_to(s_('AutoDevOps|add a Kubernetes cluster'), new_project_cluster_path(@project))
= s_('AutoDevOps|You can automatically build and test your application if you %{link_to_auto_devops_settings} for this project. You can automatically deploy it as well, if you %{link_to_add_kubernetes_cluster}.').html_safe % { link_to_auto_devops_settings: link_to_auto_devops_settings, link_to_add_kubernetes_cluster: link_to_add_kubernetes_cluster }
diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml
index a603b1024eb..a603b1024eb 100755..100644
--- a/app/views/projects/forks/new.html.haml
+++ b/app/views/projects/forks/new.html.haml
diff --git a/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml b/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml
index 964dc40a213..e6205f24ae6 100644
--- a/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml
+++ b/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml
@@ -11,6 +11,6 @@
Showing
%strong.cred {{conflictsCountText}}
between
- %strong {{conflictsData.sourceBranch}}
+ %strong.ref-name {{conflictsData.sourceBranch}}
and
- %strong {{conflictsData.targetBranch}}
+ %strong.ref-name {{conflictsData.targetBranch}}
diff --git a/app/views/projects/merge_requests/conflicts/_submit_form.html.haml b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml
index 13026b7566a..b86a87a1fc6 100644
--- a/app/views/projects/merge_requests/conflicts/_submit_form.html.haml
+++ b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml
@@ -1,13 +1,21 @@
+- branch_name = link_to @merge_request.source_branch, project_tree_path(@merge_request.project, @merge_request.source_branch), class: "ref-name"
+- translation =_('You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}') % { use_ours: '<code>Use Ours</code>', use_theirs: '<code>Use Theirs</code>', branch_name: branch_name }
+
+%hr
.form-horizontal.resolve-conflicts-form
.form-group
- %label.col-sm-2.control-label{ "for" => "commit-message" }
- #{ _('Commit message') }
- .col-sm-10
+ .col-md-4
+ %h4= _('Resolve conflicts on source branch')
+ .resolve-info
+ = translation.html_safe
+ .col-md-8
+ %label.label-light{ "for" => "commit-message" }
+ #{ _('Commit message') }
.commit-message-container
.max-width-marker
%textarea.form-control.js-commit-message#commit-message{ "v-model" => "conflictsData.commitMessage", "rows" => "5" }
.form-group
- .col-sm-offset-2.col-sm-10
+ .col-md-offset-4.col-md-8
.row
.col-xs-6
%button.btn.btn-success.js-submit-button{ type: "button", "@click" => "commit()", ":disabled" => "!readyToCommit" }
diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml
index 76f57320f99..2a683d5be0f 100644
--- a/app/views/projects/registry/repositories/index.html.haml
+++ b/app/views/projects/registry/repositories/index.html.haml
@@ -30,7 +30,7 @@
%br
%p
- deploy_token = link_to(_('deploy token'), help_page_path('user/project/deploy_tokens/index', anchor: 'read-container-registry-images'), target: '_blank')
- = s_('ContainerRegistry|You can also %{deploy_token} for read-only access to the registry images.').html_safe % { deploy_token: deploy_token }
+ = s_('ContainerRegistry|You can also use a %{deploy_token} for read-only access to the registry images.').html_safe % { deploy_token: deploy_token }
%br
%p
= s_('ContainerRegistry|Once you log in, you&rsquo;re free to create and upload a container image using the common %{build} and %{push} commands').html_safe % { build: "<code>build</code>".html_safe, push: "<code>push</code>".html_safe }
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 5f596a019f7..7d8dd58e7e0 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -19,7 +19,7 @@
%section.settings#autodevops-settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
- = s_('CICD|Auto DevOps (Beta)')
+ = s_('CICD|Auto DevOps')
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index 8587d3b0c0d..fc8ebfa1fb1 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -82,7 +82,7 @@
- if can_collaborate
= succeed " " do
- = link_to ide_edit_path(@project, @id, ""), class: 'btn btn-default' do
+ = link_to ide_edit_path(@project, @ref, @path), class: 'btn btn-default' do
= _('Web IDE')
= render 'projects/buttons/download', project: @project, ref: @ref
diff --git a/app/views/shared/_auto_devops_callout.html.haml b/app/views/shared/_auto_devops_callout.html.haml
index d3fa324e460..084d295f2c1 100644
--- a/app/views/shared/_auto_devops_callout.html.haml
+++ b/app/views/shared/_auto_devops_callout.html.haml
@@ -3,7 +3,7 @@
= custom_icon('icon_autodevops')
.banner-body.prepend-left-10.append-bottom-10
- %h5.banner-title= s_('AutoDevOps|Auto DevOps (Beta)')
+ %h5.banner-title= s_('AutoDevOps|Auto DevOps')
%p= s_('AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.')
%p
- link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer')
diff --git a/app/views/shared/runners/show.html.haml b/app/views/shared/runners/show.html.haml
index 480a224b6d5..93c6ba86640 100644
--- a/app/views/shared/runners/show.html.haml
+++ b/app/views/shared/runners/show.html.haml
@@ -66,6 +66,6 @@
%td Last contact
%td
- if @runner.contacted_at
- #{time_ago_in_words(@runner.contacted_at)} ago
+ = time_ago_with_tooltip @runner.contacted_at
- else
Never
diff --git a/changelogs/unreleased/10244-add-project-ci-cd-settings.yml b/changelogs/unreleased/10244-add-project-ci-cd-settings.yml
deleted file mode 100644
index 89f9a0fe03c..00000000000
--- a/changelogs/unreleased/10244-add-project-ci-cd-settings.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Introduce new ProjectCiCdSetting model with group_runners_enabled
-merge_request: 18144
-author:
-type: performance
diff --git a/changelogs/unreleased/16957-issue-due-email.yml b/changelogs/unreleased/16957-issue-due-email.yml
deleted file mode 100644
index 83944ca4f73..00000000000
--- a/changelogs/unreleased/16957-issue-due-email.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add cron job to email users on issue due date
-merge_request: 17985
-author: Stuart Nelson
-type: added
diff --git a/changelogs/unreleased/18524-fix-double-brackets-in-wiki-markdown.yml b/changelogs/unreleased/18524-fix-double-brackets-in-wiki-markdown.yml
new file mode 100644
index 00000000000..9287243a7e3
--- /dev/null
+++ b/changelogs/unreleased/18524-fix-double-brackets-in-wiki-markdown.yml
@@ -0,0 +1,5 @@
+---
+title: Fix double-brackets being linkified in wiki markdown
+merge_request: 18524
+author: brewingcode
+type: fixed
diff --git a/changelogs/unreleased/19861-expand-api-render-an-arbitrary-markdown-document.yml b/changelogs/unreleased/19861-expand-api-render-an-arbitrary-markdown-document.yml
new file mode 100644
index 00000000000..a97e8a2b5cc
--- /dev/null
+++ b/changelogs/unreleased/19861-expand-api-render-an-arbitrary-markdown-document.yml
@@ -0,0 +1,5 @@
+---
+title: Add API endpoint to render markdown text
+merge_request: 18926
+author: "@blackst0ne"
+type: added
diff --git a/changelogs/unreleased/21677-run-pipeline-word.yml b/changelogs/unreleased/21677-run-pipeline-word.yml
deleted file mode 100644
index 9cc280244e4..00000000000
--- a/changelogs/unreleased/21677-run-pipeline-word.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improves wording in new pipeline page
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/22846-notifications-broken-during-email-address-change-before-email-confirmed.yml b/changelogs/unreleased/22846-notifications-broken-during-email-address-change-before-email-confirmed.yml
new file mode 100644
index 00000000000..2b4727c5f03
--- /dev/null
+++ b/changelogs/unreleased/22846-notifications-broken-during-email-address-change-before-email-confirmed.yml
@@ -0,0 +1,6 @@
+---
+title: Fix an issue where the notification email address would be set to an unconfirmed
+ email address
+merge_request: 18474
+author:
+type: fixed
diff --git a/changelogs/unreleased/25010-collapsed-sidebar-tooltips.yml b/changelogs/unreleased/25010-collapsed-sidebar-tooltips.yml
deleted file mode 100644
index 1226fb4eefd..00000000000
--- a/changelogs/unreleased/25010-collapsed-sidebar-tooltips.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improve tooltips in collapsed right sidebar
-merge_request: 17714
-author:
-type: changed
diff --git a/changelogs/unreleased/30739-fix-joined-information-on-project-members-page.yml b/changelogs/unreleased/30739-fix-joined-information-on-project-members-page.yml
deleted file mode 100644
index f2d5b503661..00000000000
--- a/changelogs/unreleased/30739-fix-joined-information-on-project-members-page.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix `joined` information on project members page
-merge_request: 18290
-author: Fabian Schneider
-type: fixed
diff --git a/changelogs/unreleased/32617-fix-template-selector-menu-visibility.yml b/changelogs/unreleased/32617-fix-template-selector-menu-visibility.yml
deleted file mode 100644
index c73be5a901e..00000000000
--- a/changelogs/unreleased/32617-fix-template-selector-menu-visibility.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Fix template selector menu visibility when toggling preview mode in file edit
- view
-merge_request: 18118
-author: Fabian Schneider
-type: fixed
diff --git a/changelogs/unreleased/33697-pipelines-json-endpoint.yml b/changelogs/unreleased/33697-pipelines-json-endpoint.yml
deleted file mode 100644
index d44e2729415..00000000000
--- a/changelogs/unreleased/33697-pipelines-json-endpoint.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use VueJS for rendering pipeline stages
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/33697-remove-ujs-action-big-graph.yml b/changelogs/unreleased/33697-remove-ujs-action-big-graph.yml
deleted file mode 100644
index 43ce52c1209..00000000000
--- a/changelogs/unreleased/33697-remove-ujs-action-big-graph.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Prevent pipeline actions in dropdown to redirct to a new page
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/34262-show-current-labels-when-editing.yml b/changelogs/unreleased/34262-show-current-labels-when-editing.yml
deleted file mode 100644
index d3b15b9ddd1..00000000000
--- a/changelogs/unreleased/34262-show-current-labels-when-editing.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Keep current labels visible when editing them in the sidebar
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/36762-reconcile-project-templates-with-auto-devops.yml b/changelogs/unreleased/36762-reconcile-project-templates-with-auto-devops.yml
deleted file mode 100644
index 8169b18f875..00000000000
--- a/changelogs/unreleased/36762-reconcile-project-templates-with-auto-devops.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Reconcile project templates with Auto DevOps
-merge_request: 18737
-author:
-type: changed
diff --git a/changelogs/unreleased/36983-osw-heading-labels-color-fix.yml b/changelogs/unreleased/36983-osw-heading-labels-color-fix.yml
deleted file mode 100644
index 082e0544dea..00000000000
--- a/changelogs/unreleased/36983-osw-heading-labels-color-fix.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adjust issue boards list header label text color
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/39584-nesting-depth-5-pages-pipelines.yml b/changelogs/unreleased/39584-nesting-depth-5-pages-pipelines.yml
new file mode 100644
index 00000000000..9f07fcdfa0b
--- /dev/null
+++ b/changelogs/unreleased/39584-nesting-depth-5-pages-pipelines.yml
@@ -0,0 +1,5 @@
+---
+title: Apply NestingDepth (level 5) (pages/pipelines.scss)
+merge_request: 18830
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/39710-search-placeholder-cut-off.yml b/changelogs/unreleased/39710-search-placeholder-cut-off.yml
new file mode 100644
index 00000000000..59290768c6a
--- /dev/null
+++ b/changelogs/unreleased/39710-search-placeholder-cut-off.yml
@@ -0,0 +1,5 @@
+---
+title: 'Fixes: Runners search input placeholder is cut off'
+merge_request: 19015
+author: Jacopo Beschi @jacopo-beschi
+type: fixed
diff --git a/changelogs/unreleased/40402-time-estimate-system-notes-can-be-confusing.yml b/changelogs/unreleased/40402-time-estimate-system-notes-can-be-confusing.yml
deleted file mode 100644
index e47577f9058..00000000000
--- a/changelogs/unreleased/40402-time-estimate-system-notes-can-be-confusing.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add a comma to the time estimate system notes
-merge_request: 18326
-author:
-type: changed
diff --git a/changelogs/unreleased/40487-axios-pipelines.yml b/changelogs/unreleased/40487-axios-pipelines.yml
deleted file mode 100644
index 437d5e87e1a..00000000000
--- a/changelogs/unreleased/40487-axios-pipelines.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-title: Replace vue resource with axios in pipelines table
-merge_request:
-author:
-type: other \ No newline at end of file
diff --git a/changelogs/unreleased/41059-calculate-artifact-size-more-efficiently.yml b/changelogs/unreleased/41059-calculate-artifact-size-more-efficiently.yml
deleted file mode 100644
index e3f94bbf081..00000000000
--- a/changelogs/unreleased/41059-calculate-artifact-size-more-efficiently.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improve DB performance of calculating total artifacts size
-merge_request: 17839
-author:
-type: performance
diff --git a/changelogs/unreleased/41082-make-deploykeys-table-more-clearly-structured.yml b/changelogs/unreleased/41082-make-deploykeys-table-more-clearly-structured.yml
deleted file mode 100644
index 23704c2b37b..00000000000
--- a/changelogs/unreleased/41082-make-deploykeys-table-more-clearly-structured.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make project deploy keys table more clearly structured
-merge_request: 18279
-author:
-type: changed
diff --git a/changelogs/unreleased/41748-vertical-misalignment-login-box.yml b/changelogs/unreleased/41748-vertical-misalignment-login-box.yml
deleted file mode 100644
index 77a97400323..00000000000
--- a/changelogs/unreleased/41748-vertical-misalignment-login-box.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Refactor CSS to eliminate vertical misalignment of login nav
-merge_request: 16275
-author: Takuya Noguchi
-type: fixed
diff --git a/changelogs/unreleased/41981-allow-group-owner-to-enable-runners-from-subgroups.yml b/changelogs/unreleased/41981-allow-group-owner-to-enable-runners-from-subgroups.yml
deleted file mode 100644
index 30481e7af84..00000000000
--- a/changelogs/unreleased/41981-allow-group-owner-to-enable-runners-from-subgroups.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Allow group owner to enable runners from subgroups (#41981)'
-merge_request: 18009
-author:
-type: fixed
diff --git a/changelogs/unreleased/42099-port-push-mirroring-to-ce-ce-port-v-2.yml b/changelogs/unreleased/42099-port-push-mirroring-to-ce-ce-port-v-2.yml
deleted file mode 100644
index f23521ea416..00000000000
--- a/changelogs/unreleased/42099-port-push-mirroring-to-ce-ce-port-v-2.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adds push mirrors to GitLab Community Edition
-merge_request: 18715
-author:
-type: changed
diff --git a/changelogs/unreleased/42543-hide-divergence-graph-on-branches-for-mobile.yml b/changelogs/unreleased/42543-hide-divergence-graph-on-branches-for-mobile.yml
deleted file mode 100644
index 7452a264bfd..00000000000
--- a/changelogs/unreleased/42543-hide-divergence-graph-on-branches-for-mobile.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove ahead/behind graphs on project branches on mobile
-merge_request: 18415
-author: Takuya Noguchi
-type: other
diff --git a/changelogs/unreleased/42803-show-new-branch-mr-button.yml b/changelogs/unreleased/42803-show-new-branch-mr-button.yml
deleted file mode 100644
index d689ff7f001..00000000000
--- a/changelogs/unreleased/42803-show-new-branch-mr-button.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Show new branch/mr button even when branch exists
-merge_request: 17712
-author: Jacopo Beschi @jacopo-beschi
-type: added
diff --git a/changelogs/unreleased/42889-avoid-return-inside-block.yml b/changelogs/unreleased/42889-avoid-return-inside-block.yml
deleted file mode 100644
index e3e1341ddcc..00000000000
--- a/changelogs/unreleased/42889-avoid-return-inside-block.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Rubocop rule to avoid returning from a block
-merge_request: 18000
-author: Jacopo Beschi @jacopo-beschi
-type: added
diff --git a/changelogs/unreleased/42936-improve-ns-factory-avoid-duplicates.yml b/changelogs/unreleased/42936-improve-ns-factory-avoid-duplicates.yml
deleted file mode 100644
index 54b523b65a1..00000000000
--- a/changelogs/unreleased/42936-improve-ns-factory-avoid-duplicates.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Fix discussions API setting created_at for notable in a group or notable in
- a project in a group with owners
-merge_request: 18464
-author:
-type: fixed
diff --git a/changelogs/unreleased/43111-controller-projects-mergerequestscontroller-index-executes-more-than-100-sql-queries.yml b/changelogs/unreleased/43111-controller-projects-mergerequestscontroller-index-executes-more-than-100-sql-queries.yml
deleted file mode 100644
index 120e319acfb..00000000000
--- a/changelogs/unreleased/43111-controller-projects-mergerequestscontroller-index-executes-more-than-100-sql-queries.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Reduce queries on merge requests list page for merge requests from forks
-merge_request: 18561
-author:
-type: performance
diff --git a/changelogs/unreleased/43404-pipelines-commit.yml b/changelogs/unreleased/43404-pipelines-commit.yml
deleted file mode 100644
index 0b9a4a6451f..00000000000
--- a/changelogs/unreleased/43404-pipelines-commit.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Breaks commit not found message in pipelines table
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/43466-make-auto-devops-settings-first-class.yml b/changelogs/unreleased/43466-make-auto-devops-settings-first-class.yml
deleted file mode 100644
index f5c5415159c..00000000000
--- a/changelogs/unreleased/43466-make-auto-devops-settings-first-class.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Create settings section for autodevops
-merge_request: 18321
-author:
-type: changed
diff --git a/changelogs/unreleased/43469-gcp-account-offer.yml b/changelogs/unreleased/43469-gcp-account-offer.yml
deleted file mode 100644
index 323a4b81731..00000000000
--- a/changelogs/unreleased/43469-gcp-account-offer.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add GCP signup offer to cluster index / create pages
-merge_request: 18684
-author:
-type: added
diff --git a/changelogs/unreleased/43557-osw-present-merge-sha-commit.yml b/changelogs/unreleased/43557-osw-present-merge-sha-commit.yml
deleted file mode 100644
index a7128f7481e..00000000000
--- a/changelogs/unreleased/43557-osw-present-merge-sha-commit.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Display merge commit SHA in merge widget after merge
-merge_request: 18722
-author:
-type: added
diff --git a/changelogs/unreleased/43567-replace-gke.yml b/changelogs/unreleased/43567-replace-gke.yml
deleted file mode 100644
index 8ec79fc3d4d..00000000000
--- a/changelogs/unreleased/43567-replace-gke.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replace GKE acronym with Google Kubernetes Engine
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/43617-mailsig.yml b/changelogs/unreleased/43617-mailsig.yml
deleted file mode 100644
index 2c7568e32ca..00000000000
--- a/changelogs/unreleased/43617-mailsig.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use RFC 3676 mail signature delimiters
-merge_request: 17979
-author: Enrico Scholz
-type: changed
diff --git a/changelogs/unreleased/44059-specify-variables-when-executing-a-manual-pipeline-from-the-ui.yml b/changelogs/unreleased/44059-specify-variables-when-executing-a-manual-pipeline-from-the-ui.yml
deleted file mode 100644
index 8854eeb5fba..00000000000
--- a/changelogs/unreleased/44059-specify-variables-when-executing-a-manual-pipeline-from-the-ui.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Enable specifying variables when executing a manual pipeline
-merge_request: 18440
-author:
-type: changed
diff --git a/changelogs/unreleased/44224-remove-gl.yml b/changelogs/unreleased/44224-remove-gl.yml
deleted file mode 100644
index 1c792883f09..00000000000
--- a/changelogs/unreleased/44224-remove-gl.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Removes modal boards store and mixins from global scope
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/44296-commit-path.yml b/changelogs/unreleased/44296-commit-path.yml
deleted file mode 100644
index b658178f8dc..00000000000
--- a/changelogs/unreleased/44296-commit-path.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Verifiy if pipeline has commit idetails and render information in MR widget
- when branch is deleted
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/44447-expose-deploy-token-to-ci-cd.yml b/changelogs/unreleased/44447-expose-deploy-token-to-ci-cd.yml
deleted file mode 100644
index d01b797b1ff..00000000000
--- a/changelogs/unreleased/44447-expose-deploy-token-to-ci-cd.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Expose Deploy Token data as environment varialbes on CI/CD jobs
-merge_request: 18414
-author:
-type: added
diff --git a/changelogs/unreleased/44541-fix-file-tree-commit-status-cache.yml b/changelogs/unreleased/44541-fix-file-tree-commit-status-cache.yml
deleted file mode 100644
index ff734fe0c05..00000000000
--- a/changelogs/unreleased/44541-fix-file-tree-commit-status-cache.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix pipeline status in branch/tag tree page
-merge_request: 17995
-author:
-type: fixed
diff --git a/changelogs/unreleased/44582-clear-pipeline-status-cache.yml b/changelogs/unreleased/44582-clear-pipeline-status-cache.yml
deleted file mode 100644
index 1777f2ffaab..00000000000
--- a/changelogs/unreleased/44582-clear-pipeline-status-cache.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Now `rake cache:clear` will also clear pipeline status cache
-merge_request: 18257
-author:
-type: fixed
diff --git a/changelogs/unreleased/44697-prevue.yml b/changelogs/unreleased/44697-prevue.yml
deleted file mode 100644
index 9fdce5869ae..00000000000
--- a/changelogs/unreleased/44697-prevue.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make toggle markdown preview shortcut only toggle selected field
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/44799-api-naming-issue-scope.yml b/changelogs/unreleased/44799-api-naming-issue-scope.yml
new file mode 100644
index 00000000000..75c6ea4cd0d
--- /dev/null
+++ b/changelogs/unreleased/44799-api-naming-issue-scope.yml
@@ -0,0 +1,5 @@
+---
+title: Rename issue scope created-by-me to created_by_me, and assigned-to-me to assigned_to_me
+merge_request: 44799
+author:
+type: deprecated
diff --git a/changelogs/unreleased/44833-ide-clean-up-status-bar.yml b/changelogs/unreleased/44833-ide-clean-up-status-bar.yml
deleted file mode 100644
index 4c827e57195..00000000000
--- a/changelogs/unreleased/44833-ide-clean-up-status-bar.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Clean up WebIDE status bar and add useful info
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/44834-ide-remove-branch-from-bottom-bar.yml b/changelogs/unreleased/44834-ide-remove-branch-from-bottom-bar.yml
deleted file mode 100644
index d3f838ad362..00000000000
--- a/changelogs/unreleased/44834-ide-remove-branch-from-bottom-bar.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove branch name from the status bar of WebIDE
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/44879.yml b/changelogs/unreleased/44879.yml
deleted file mode 100644
index b51e057bb7b..00000000000
--- a/changelogs/unreleased/44879.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add the signature verfication badge to the compare view
-merge_request: 18245
-author: Marc Shaw
-type: added
diff --git a/changelogs/unreleased/44985-fix-protected-branch-delete-modal.yml b/changelogs/unreleased/44985-fix-protected-branch-delete-modal.yml
deleted file mode 100644
index 4af2af2a561..00000000000
--- a/changelogs/unreleased/44985-fix-protected-branch-delete-modal.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix confirmation modal for deleting a protected branch
-merge_request: 18176
-author: Paul Bonaud @PaulRbR
-type: fixed
diff --git a/changelogs/unreleased/45065-users-projects-json-sort.yml b/changelogs/unreleased/45065-users-projects-json-sort.yml
new file mode 100644
index 00000000000..89a1d7eb36f
--- /dev/null
+++ b/changelogs/unreleased/45065-users-projects-json-sort.yml
@@ -0,0 +1,5 @@
+---
+title: Order UsersController#projects.json by updated_at
+merge_request: 18227
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/45159-fix-illustration.yml b/changelogs/unreleased/45159-fix-illustration.yml
deleted file mode 100644
index 3b9cb45b916..00000000000
--- a/changelogs/unreleased/45159-fix-illustration.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adds illustration for when job log was erased
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/45397-update-faraday_middleware-to-0-12-2.yml b/changelogs/unreleased/45397-update-faraday_middleware-to-0-12-2.yml
deleted file mode 100644
index 3370ec3feba..00000000000
--- a/changelogs/unreleased/45397-update-faraday_middleware-to-0-12-2.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update faraday_middlewar to 0.12.2
-merge_request: 18397
-author: Takuya Noguchi
-type: security
diff --git a/changelogs/unreleased/45398-fix-rss-button.yml b/changelogs/unreleased/45398-fix-rss-button.yml
deleted file mode 100644
index 2c8ee6f0564..00000000000
--- a/changelogs/unreleased/45398-fix-rss-button.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix tabs container styles to make RSS button clickable
-merge_request: 18559
-author:
-type: fixed
diff --git a/changelogs/unreleased/45436-markdown-is-not-rendering-error-loading-viewer-undefined-method-html_escape.yml b/changelogs/unreleased/45436-markdown-is-not-rendering-error-loading-viewer-undefined-method-html_escape.yml
deleted file mode 100644
index 0f1d111ca58..00000000000
--- a/changelogs/unreleased/45436-markdown-is-not-rendering-error-loading-viewer-undefined-method-html_escape.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix undefined `html_escape` method during markdown rendering
-merge_request: 18418
-author:
-type: fixed
diff --git a/changelogs/unreleased/45451-user-deletion-modal-with-same-info-for-delete-user-or-delete-user-and-contributions.yml b/changelogs/unreleased/45451-user-deletion-modal-with-same-info-for-delete-user-or-delete-user-and-contributions.yml
deleted file mode 100644
index 707a18745c8..00000000000
--- a/changelogs/unreleased/45451-user-deletion-modal-with-same-info-for-delete-user-or-delete-user-and-contributions.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Correct text and functionality for delete user / delete user and contributions
- modal.
-merge_request: 18463
-author: Marc Schwede
-type: fixed
diff --git a/changelogs/unreleased/45481-sane-pages-artifacts.yml b/changelogs/unreleased/45481-sane-pages-artifacts.yml
deleted file mode 100644
index b9c68b70012..00000000000
--- a/changelogs/unreleased/45481-sane-pages-artifacts.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Don't automatically remove artifacts for pages jobs after pages:deploy has
- run
-merge_request: 18628
-author:
-type: fixed
diff --git a/changelogs/unreleased/45572-members-invitations-scheduled-before-commit.yml b/changelogs/unreleased/45572-members-invitations-scheduled-before-commit.yml
deleted file mode 100644
index 7cdea436d47..00000000000
--- a/changelogs/unreleased/45572-members-invitations-scheduled-before-commit.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Ensure member notifications are sent after the member actual creation/update in the DB
-merge_request: 18538
-author:
-type: fixed
diff --git a/changelogs/unreleased/45576-fix-create-project-for-user-endpoint.yml b/changelogs/unreleased/45576-fix-create-project-for-user-endpoint.yml
deleted file mode 100644
index 12631c75b44..00000000000
--- a/changelogs/unreleased/45576-fix-create-project-for-user-endpoint.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix project creation for user endpoint when jobs_enabled parameter supplied
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/45666-project-ci-lint-links.yml b/changelogs/unreleased/45666-project-ci-lint-links.yml
deleted file mode 100644
index dbf803c0921..00000000000
--- a/changelogs/unreleased/45666-project-ci-lint-links.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update links to /ci/lint with ones to project ci/lint
-merge_request: 18539
-author: Takuya Noguchi
-type: fixed
diff --git a/changelogs/unreleased/45761-replace-actionview-time_ago_in_words.yml b/changelogs/unreleased/45761-replace-actionview-time_ago_in_words.yml
deleted file mode 100644
index adf4db90407..00000000000
--- a/changelogs/unreleased/45761-replace-actionview-time_ago_in_words.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replace time_ago_in_words with JS-based one
-merge_request: 18607
-author: Takuya Noguchi
-type: performance
diff --git a/changelogs/unreleased/45827-expose_readme_url_in_project_api.yml b/changelogs/unreleased/45827-expose_readme_url_in_project_api.yml
new file mode 100644
index 00000000000..7c495cf4dc0
--- /dev/null
+++ b/changelogs/unreleased/45827-expose_readme_url_in_project_api.yml
@@ -0,0 +1,5 @@
+---
+title: Expose readme url in Project API
+merge_request: 18960
+author: Imre Farkas
+type: changed
diff --git a/changelogs/unreleased/45934-ide-firefox-scroll-md-preview.yml b/changelogs/unreleased/45934-ide-firefox-scroll-md-preview.yml
new file mode 100644
index 00000000000..b9e70bc5679
--- /dev/null
+++ b/changelogs/unreleased/45934-ide-firefox-scroll-md-preview.yml
@@ -0,0 +1,5 @@
+---
+title: Fix unscrollable Markdown preview of WebIDE on Firefox
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/46049-import-export-import-is-broken-due-to-the-addition-of-a-ci-table.yml b/changelogs/unreleased/46049-import-export-import-is-broken-due-to-the-addition-of-a-ci-table.yml
deleted file mode 100644
index 77e4bb50082..00000000000
--- a/changelogs/unreleased/46049-import-export-import-is-broken-due-to-the-addition-of-a-ci-table.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Resolve Import/Export ci_cd_settings error updating the project
-merge_request: 46049
-author:
-type: fixed
diff --git a/changelogs/unreleased/46082-runner-contacted_at-is-not-always-a-time-type.yml b/changelogs/unreleased/46082-runner-contacted_at-is-not-always-a-time-type.yml
new file mode 100644
index 00000000000..07f67251b24
--- /dev/null
+++ b/changelogs/unreleased/46082-runner-contacted_at-is-not-always-a-time-type.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Runner contacted at tooltip cache.
+merge_request: 18810
+author:
+type: fixed
diff --git a/changelogs/unreleased/46193-fix-big-estimate.yml b/changelogs/unreleased/46193-fix-big-estimate.yml
new file mode 100644
index 00000000000..d0da0c10033
--- /dev/null
+++ b/changelogs/unreleased/46193-fix-big-estimate.yml
@@ -0,0 +1,5 @@
+---
+title: Fixes 500 error on /estimate BIG_VALUE
+merge_request: 18964
+author: Jacopo Beschi @jacopo-beschi
+type: fixed
diff --git a/changelogs/unreleased/46210-terms-acceptance-dropdown-menu.yml b/changelogs/unreleased/46210-terms-acceptance-dropdown-menu.yml
deleted file mode 100644
index 8a7c549e356..00000000000
--- a/changelogs/unreleased/46210-terms-acceptance-dropdown-menu.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 46210 Display logo and user dropdown on mobile for terms page and fix styling
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/46286-fix-ingress-rbac-default-value.yml b/changelogs/unreleased/46286-fix-ingress-rbac-default-value.yml
deleted file mode 100644
index e9cd8977394..00000000000
--- a/changelogs/unreleased/46286-fix-ingress-rbac-default-value.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Disables RBAC on nginx-ingress
-merge_request: 18947
-author:
-type: fixed
diff --git a/changelogs/unreleased/46303_copy_button_fix_embedded_snippets.yml b/changelogs/unreleased/46303_copy_button_fix_embedded_snippets.yml
deleted file mode 100644
index c8cdf3672b3..00000000000
--- a/changelogs/unreleased/46303_copy_button_fix_embedded_snippets.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: fixed copy to blipboard button in embed bar of snippets
-merge_request: 18923
-author: haseebeqx
-type: fixed
diff --git a/changelogs/unreleased/46345-kubernetes-popover-illustration-skewed.yml b/changelogs/unreleased/46345-kubernetes-popover-illustration-skewed.yml
deleted file mode 100644
index a0e6b39fef6..00000000000
--- a/changelogs/unreleased/46345-kubernetes-popover-illustration-skewed.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Correct skewed Kubernetes popover illustration
-merge_request: 18949
-author:
-type: fixed
diff --git a/changelogs/unreleased/46354-deprecate_gemnasium_service.yml b/changelogs/unreleased/46354-deprecate_gemnasium_service.yml
new file mode 100644
index 00000000000..c5ead45d883
--- /dev/null
+++ b/changelogs/unreleased/46354-deprecate_gemnasium_service.yml
@@ -0,0 +1,5 @@
+---
+title: Deprecate Gemnasium project service
+merge_request: 18954
+author:
+type: deprecated
diff --git a/changelogs/unreleased/46427-add-keyboard-shortcut-environments.yml b/changelogs/unreleased/46427-add-keyboard-shortcut-environments.yml
new file mode 100644
index 00000000000..609968f3230
--- /dev/null
+++ b/changelogs/unreleased/46427-add-keyboard-shortcut-environments.yml
@@ -0,0 +1,5 @@
+---
+title: Adds keyboard shortcut `g e` for Environments on Project pages
+merge_request: 19002
+author:
+type: added
diff --git a/changelogs/unreleased/46427-add-keyboard-shortcut-kubernetes.yml b/changelogs/unreleased/46427-add-keyboard-shortcut-kubernetes.yml
new file mode 100644
index 00000000000..48e51b2615e
--- /dev/null
+++ b/changelogs/unreleased/46427-add-keyboard-shortcut-kubernetes.yml
@@ -0,0 +1,5 @@
+---
+title: Adds keyboard shortcut `g k` for Kubernetes on Project pages
+merge_request: 19002
+author:
+type: added
diff --git a/changelogs/unreleased/winh-new-mergerequest-branch-picker.yml b/changelogs/unreleased/46427-change-keyboard-shortcut-of-activity-feed.yml
index 401ecd09ef2..9a7cf0d6944 100644
--- a/changelogs/unreleased/winh-new-mergerequest-branch-picker.yml
+++ b/changelogs/unreleased/46427-change-keyboard-shortcut-of-activity-feed.yml
@@ -1,5 +1,5 @@
---
-title: Load branches on new merge request page asynchronously
-merge_request: 18315
+title: Changes keyboard shortcut of Activity feed to `g v`
+merge_request: 19002
author:
type: changed
diff --git a/changelogs/unreleased/46427-remove-outdated-todos-shortcut.yml b/changelogs/unreleased/46427-remove-outdated-todos-shortcut.yml
new file mode 100644
index 00000000000..f416e35030e
--- /dev/null
+++ b/changelogs/unreleased/46427-remove-outdated-todos-shortcut.yml
@@ -0,0 +1,5 @@
+---
+title: Removes outdated `g t` shortcut for TODO in favor of `Shift+T`
+merge_request: 19002
+author:
+type: removed
diff --git a/changelogs/unreleased/4950-unassign-slash-command-preview-fix.yml b/changelogs/unreleased/4950-unassign-slash-command-preview-fix.yml
deleted file mode 100644
index 0b8c14ae699..00000000000
--- a/changelogs/unreleased/4950-unassign-slash-command-preview-fix.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix unassign slash command preview
-merge_request: 18447
-author:
-type: fixed
diff --git a/changelogs/unreleased/5750-backport-checksum-git-commanderror-exit-status-128.yml b/changelogs/unreleased/5750-backport-checksum-git-commanderror-exit-status-128.yml
deleted file mode 100644
index d778b44c110..00000000000
--- a/changelogs/unreleased/5750-backport-checksum-git-commanderror-exit-status-128.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Raise NoRepository error for non-valid repositories when calculating repository
- checksum
-merge_request: 18594
-author:
-type: fixed
diff --git a/changelogs/unreleased/5794-we-should-failover-gracefully-when-we-can-t-connect-to-geo-tracking-database-ce.yml b/changelogs/unreleased/5794-we-should-failover-gracefully-when-we-can-t-connect-to-geo-tracking-database-ce.yml
deleted file mode 100644
index 4db0ff4f3a0..00000000000
--- a/changelogs/unreleased/5794-we-should-failover-gracefully-when-we-can-t-connect-to-geo-tracking-database-ce.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: ShaAttribute no longer stops startup if database is missing
-merge_request: 18726
-author:
-type: fixed
diff --git a/changelogs/unreleased/8088_embedded_snippets_support.yml b/changelogs/unreleased/8088_embedded_snippets_support.yml
deleted file mode 100644
index 7bd77a69dbd..00000000000
--- a/changelogs/unreleased/8088_embedded_snippets_support.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adds Embedded Snippets Support
-merge_request: 15695
-author: haseebeqx
-type: added
diff --git a/changelogs/unreleased/ab-43706-composite-primary-keys.yml b/changelogs/unreleased/ab-43706-composite-primary-keys.yml
new file mode 100644
index 00000000000..b17050a64c8
--- /dev/null
+++ b/changelogs/unreleased/ab-43706-composite-primary-keys.yml
@@ -0,0 +1,5 @@
+---
+title: Add NOT NULL constraints to project_authorizations.
+merge_request: 18980
+author:
+type: other
diff --git a/changelogs/unreleased/ab-44259-atomic-internal-ids-for-all-models.yml b/changelogs/unreleased/ab-44259-atomic-internal-ids-for-all-models.yml
deleted file mode 100644
index e154f7dbc85..00000000000
--- a/changelogs/unreleased/ab-44259-atomic-internal-ids-for-all-models.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Transition to atomic internal ids for all models.
-merge_request: 44259
-author:
-type: other
diff --git a/changelogs/unreleased/ab-46530-mediumtext-for-gpg-keys.yml b/changelogs/unreleased/ab-46530-mediumtext-for-gpg-keys.yml
new file mode 100644
index 00000000000..88ef62ebc0e
--- /dev/null
+++ b/changelogs/unreleased/ab-46530-mediumtext-for-gpg-keys.yml
@@ -0,0 +1,5 @@
+---
+title: Increase text limit for GPG keys (mysql only).
+merge_request: 19069
+author:
+type: other
diff --git a/changelogs/unreleased/accessible-text.yml b/changelogs/unreleased/accessible-text.yml
deleted file mode 100755
index d39d5a9eb2c..00000000000
--- a/changelogs/unreleased/accessible-text.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Replace "Click" with "Select" to be more inclusive of people with accessibility
- requirements
-merge_request: 18386
-author: Mark Lapierre
-type: other
diff --git a/changelogs/unreleased/add-copy-metadata-command.yml b/changelogs/unreleased/add-copy-metadata-command.yml
deleted file mode 100644
index 3bf25ae6ce0..00000000000
--- a/changelogs/unreleased/add-copy-metadata-command.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add Copy metadata quick action
-merge_request: 16473
-author: Mateusz Bajorski
-type: added
diff --git a/changelogs/unreleased/add-git-commit-message-predefined-variable.yml b/changelogs/unreleased/add-git-commit-message-predefined-variable.yml
deleted file mode 100644
index 183fe69936e..00000000000
--- a/changelogs/unreleased/add-git-commit-message-predefined-variable.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add CI_COMMIT_MESSAGE, CI_COMMIT_TITLE and CI_COMMIT_DESCRIPTION predefined variables
-merge_request: 18672
-author:
-type: added
diff --git a/changelogs/unreleased/add-loading-icon-padding-for-pipeline-environments.yml b/changelogs/unreleased/add-loading-icon-padding-for-pipeline-environments.yml
deleted file mode 100644
index a6304418517..00000000000
--- a/changelogs/unreleased/add-loading-icon-padding-for-pipeline-environments.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add loading icon padding for pipeline environments
-merge_request: 18631
-author: George Tsiolis
-type: fixed
diff --git a/changelogs/unreleased/add-padding-to-profile-description.yml b/changelogs/unreleased/add-padding-to-profile-description.yml
deleted file mode 100644
index 4628a10eb3f..00000000000
--- a/changelogs/unreleased/add-padding-to-profile-description.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add padding to profile description
-merge_request: 18663
-author: George Tsiolis
-type: changed
diff --git a/changelogs/unreleased/align-project-avatar-on-small-viewports.yml b/changelogs/unreleased/align-project-avatar-on-small-viewports.yml
deleted file mode 100644
index 320e7fbc294..00000000000
--- a/changelogs/unreleased/align-project-avatar-on-small-viewports.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Align project avatar on small viewports
-merge_request: 18513
-author: George Tsiolis
-type: changed
diff --git a/changelogs/unreleased/ash-mckenzie-include-sha-with-version.yml b/changelogs/unreleased/ash-mckenzie-include-sha-with-version.yml
deleted file mode 100644
index b49c48e0fe1..00000000000
--- a/changelogs/unreleased/ash-mckenzie-include-sha-with-version.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: git SHA is now displayed alongside the GitLab version on the Admin Dashboard
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/blackst0ne-add-missing-changelog-type-to-docs.yml b/changelogs/unreleased/blackst0ne-add-missing-changelog-type-to-docs.yml
deleted file mode 100644
index f8790fa45aa..00000000000
--- a/changelogs/unreleased/blackst0ne-add-missing-changelog-type-to-docs.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add missing changelog type to docs
-merge_request: 18526
-author: "@blackst0ne"
-type: other
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-builds-artifacts-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-builds-artifacts-feature.yml
deleted file mode 100644
index 98c56cf2b57..00000000000
--- a/changelogs/unreleased/blackst0ne-replace-spinach-project-builds-artifacts-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Replace the `project/builds/artifacts.feature` spinach test with an rspec analog'
-merge_request: 18729
-author: '@blackst0ne'
-type: other
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-commits-branches-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-commits-branches-feature.yml
deleted file mode 100644
index bcfba4ae70d..00000000000
--- a/changelogs/unreleased/blackst0ne-replace-spinach-project-commits-branches-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: "Replace the `project/commits/branches.feature` spinach test with an rspec analog"
-merge_request: 18302
-author: "@blackst0ne"
-type: other
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-commits-comments-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-commits-comments-feature.yml
deleted file mode 100644
index e7077f27555..00000000000
--- a/changelogs/unreleased/blackst0ne-replace-spinach-project-commits-comments-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replace the `project/commits/comments.feature` spinach test with an rspec analog
-merge_request: 18356
-author: "@blackst0ne"
-type: other
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-milestones-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-milestones-feature.yml
deleted file mode 100644
index 0dcac0a80eb..00000000000
--- a/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-milestones-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replace the `project/issues/milestones.feature` spinach test with an rspec analog
-merge_request: 18300
-author: "@blackst0ne"
-type: other
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-source-markdown-render-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-source-markdown-render-feature.yml
deleted file mode 100644
index 657ed782880..00000000000
--- a/changelogs/unreleased/blackst0ne-replace-spinach-project-source-markdown-render-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replace the `project/source/markdown_render.feature` spinach test with an rspec analog
-merge_request: 18525
-author: "@blackst0ne"
-type: other
diff --git a/changelogs/unreleased/break-issue-title-for-board-card-title-and-issueable-header-text.yml b/changelogs/unreleased/break-issue-title-for-board-card-title-and-issueable-header-text.yml
deleted file mode 100644
index 7acde223962..00000000000
--- a/changelogs/unreleased/break-issue-title-for-board-card-title-and-issueable-header-text.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Break issue title for board card title and issuable header text
-merge_request: 18674
-author: George Tsiolis
-type: changed
diff --git a/changelogs/unreleased/bvl-enforce-terms.yml b/changelogs/unreleased/bvl-enforce-terms.yml
deleted file mode 100644
index 1bb1ecdf623..00000000000
--- a/changelogs/unreleased/bvl-enforce-terms.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow admins to enforce accepting Terms of Service on an instance
-merge_request: 18570
-author:
-type: added
diff --git a/changelogs/unreleased/bvl-restrict-api-git-for-terms.yml b/changelogs/unreleased/bvl-restrict-api-git-for-terms.yml
deleted file mode 100644
index 49cd04b065b..00000000000
--- a/changelogs/unreleased/bvl-restrict-api-git-for-terms.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Block access to the API & git for users that did not accept enforced Terms
- of Service
-merge_request: 18816
-author:
-type: other
diff --git a/changelogs/unreleased/bvl-shared-groups-on-group-page.yml b/changelogs/unreleased/bvl-shared-groups-on-group-page.yml
deleted file mode 100644
index 6c0703fd138..00000000000
--- a/changelogs/unreleased/bvl-shared-groups-on-group-page.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Show shared projects on group page
-merge_request: 18390
-author:
-type: fixed
diff --git a/changelogs/unreleased/bw-add-console-message.yml b/changelogs/unreleased/bw-add-console-message.yml
deleted file mode 100644
index 7994f7caced..00000000000
--- a/changelogs/unreleased/bw-add-console-message.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Output some useful information when running the rails console
-merge_request: 18697
-author:
-type: added
diff --git a/changelogs/unreleased/ccr-incoming-email-regex-anchor.yml b/changelogs/unreleased/ccr-incoming-email-regex-anchor.yml
new file mode 100644
index 00000000000..a0d787e570e
--- /dev/null
+++ b/changelogs/unreleased/ccr-incoming-email-regex-anchor.yml
@@ -0,0 +1,3 @@
+title: Add anchor for incoming email regex
+merge_request: !18917
+type: added
diff --git a/changelogs/unreleased/change-font-for-tables-inside-diff-discussions.yml b/changelogs/unreleased/change-font-for-tables-inside-diff-discussions.yml
deleted file mode 100644
index f2810fab208..00000000000
--- a/changelogs/unreleased/change-font-for-tables-inside-diff-discussions.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Change font for tables inside diff discussions
-merge_request: 18660
-author: George Tsiolis
-type: changed
diff --git a/changelogs/unreleased/collapsed-contextual-nav-update.yml b/changelogs/unreleased/collapsed-contextual-nav-update.yml
new file mode 100644
index 00000000000..31a32a9e1e9
--- /dev/null
+++ b/changelogs/unreleased/collapsed-contextual-nav-update.yml
@@ -0,0 +1,6 @@
+---
+title: Renamed 'Overview' to 'Project' in collapsed contextual navigation at a project
+ level
+merge_request: 18996
+author: Constance Okoghenun
+type: fixed
diff --git a/changelogs/unreleased/create-live-trace-only-if-job-is-complete.yml b/changelogs/unreleased/create-live-trace-only-if-job-is-complete.yml
new file mode 100644
index 00000000000..f32c70cf884
--- /dev/null
+++ b/changelogs/unreleased/create-live-trace-only-if-job-is-complete.yml
@@ -0,0 +1,5 @@
+---
+title: Forbid to patch traces for finished jobs
+merge_request: 18969
+author:
+type: fixed
diff --git a/changelogs/unreleased/deprecation-warning-for-dynamic-milestones.yml b/changelogs/unreleased/deprecation-warning-for-dynamic-milestones.yml
deleted file mode 100644
index 3e1ac7b795d..00000000000
--- a/changelogs/unreleased/deprecation-warning-for-dynamic-milestones.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add deprecation message to dynamic milestone pages
-merge_request: 17505
-author:
-type: added
diff --git a/changelogs/unreleased/dm-webhook-catch-blocked-url-exception.yml b/changelogs/unreleased/dm-webhook-catch-blocked-url-exception.yml
deleted file mode 100644
index c4f8f7acca6..00000000000
--- a/changelogs/unreleased/dm-webhook-catch-blocked-url-exception.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Ensure web hook 'blocked URL' errors are stored in web hook logs and properly
- surfaced to the user
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/docs-use-variables-deploy-policy-for-staging-and-production.yml b/changelogs/unreleased/docs-use-variables-deploy-policy-for-staging-and-production.yml
deleted file mode 100644
index aa23a89a175..00000000000
--- a/changelogs/unreleased/docs-use-variables-deploy-policy-for-staging-and-production.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Add documentation about how to use variables to define deploy policies for
- staging/production environments
-merge_request: 18675
-author:
-type: other
diff --git a/changelogs/unreleased/dz-add-2fa-filter-admin-api.yml b/changelogs/unreleased/dz-add-2fa-filter-admin-api.yml
deleted file mode 100644
index df479e69380..00000000000
--- a/changelogs/unreleased/dz-add-2fa-filter-admin-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add 2FA filter to users API for admins only
-merge_request: 18503
-author:
-type: changed
diff --git a/changelogs/unreleased/feature-add-language-in-repository-to-api.yml b/changelogs/unreleased/feature-add-language-in-repository-to-api.yml
deleted file mode 100644
index bd9bd377212..00000000000
--- a/changelogs/unreleased/feature-add-language-in-repository-to-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'API: add languages of project GET /projects/:id/languages'
-merge_request: 17770
-author: Roger Rüttimann
-type: added
diff --git a/changelogs/unreleased/feature-add_target_to_tags.yml b/changelogs/unreleased/feature-add_target_to_tags.yml
deleted file mode 100644
index 75816005e1f..00000000000
--- a/changelogs/unreleased/feature-add_target_to_tags.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Expose the target commit ID through the tag API
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/feature-display-active-sessions.yml b/changelogs/unreleased/feature-display-active-sessions.yml
deleted file mode 100644
index 14cfa66953e..00000000000
--- a/changelogs/unreleased/feature-display-active-sessions.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Display active sessions and allow the user to revoke any of it
-merge_request: 17867
-author: Alexis Reigel
-type: added
diff --git a/changelogs/unreleased/feature-gb-add-regexp-variables-expression.yml b/changelogs/unreleased/feature-gb-add-regexp-variables-expression.yml
new file mode 100644
index 00000000000..d77c5b42497
--- /dev/null
+++ b/changelogs/unreleased/feature-gb-add-regexp-variables-expression.yml
@@ -0,0 +1,5 @@
+---
+title: Add support for variables expression pattern matching syntax
+merge_request: 18902
+author:
+type: added
diff --git a/changelogs/unreleased/feature-runner-per-group.yml b/changelogs/unreleased/feature-runner-per-group.yml
deleted file mode 100644
index 162a5fae0a4..00000000000
--- a/changelogs/unreleased/feature-runner-per-group.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow group masters to configure runners for groups
-merge_request: 9646
-author: Alexis Reigel
-type: added
diff --git a/changelogs/unreleased/feature-show-only-groups-user-is-member-of-in-dashboard.yml b/changelogs/unreleased/feature-show-only-groups-user-is-member-of-in-dashboard.yml
deleted file mode 100644
index 6e2273ed9af..00000000000
--- a/changelogs/unreleased/feature-show-only-groups-user-is-member-of-in-dashboard.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: For group dashboard, we no longer show groups which the visitor is not a member of (this applies to admins and auditors)
-merge_request: 17884
-author: Roger Rüttimann
-type: changed
diff --git a/changelogs/unreleased/fix-assignee-name-wrap.yml b/changelogs/unreleased/fix-assignee-name-wrap.yml
new file mode 100644
index 00000000000..2407288785f
--- /dev/null
+++ b/changelogs/unreleased/fix-assignee-name-wrap.yml
@@ -0,0 +1,5 @@
+---
+title: Wrapping problem on the issues page has been fixed
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-devops-remove-beta.yml b/changelogs/unreleased/fix-devops-remove-beta.yml
new file mode 100644
index 00000000000..326003eb956
--- /dev/null
+++ b/changelogs/unreleased/fix-devops-remove-beta.yml
@@ -0,0 +1,5 @@
+---
+title: Removed "(Beta)" from "Auto DevOps" messages
+merge_request: 18759
+author:
+type: changed
diff --git a/changelogs/unreleased/fix-gb-add-pipeline-builds-foreign-key.yml b/changelogs/unreleased/fix-gb-add-pipeline-builds-foreign-key.yml
deleted file mode 100644
index bded7bb7cc4..00000000000
--- a/changelogs/unreleased/fix-gb-add-pipeline-builds-foreign-key.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add database foreign key constraint between pipelines and build
-merge_request: 18822
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-gb-exclude-persisted-variables-from-environment-name.yml b/changelogs/unreleased/fix-gb-exclude-persisted-variables-from-environment-name.yml
new file mode 100644
index 00000000000..92426832f30
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-exclude-persisted-variables-from-environment-name.yml
@@ -0,0 +1,5 @@
+---
+title: Exclude CI_PIPELINE_ID from variables supported in dynamic environment name
+merge_request: 19032
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-gb-not-allow-to-trigger-skipped-manual-actions.yml b/changelogs/unreleased/fix-gb-not-allow-to-trigger-skipped-manual-actions.yml
new file mode 100644
index 00000000000..c2a788d6ad0
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-not-allow-to-trigger-skipped-manual-actions.yml
@@ -0,0 +1,5 @@
+---
+title: Do not allow to trigger manual actions that were skipped
+merge_request: 18985
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-inconsistent-protected-branch-pill-baseline.yml b/changelogs/unreleased/fix-inconsistent-protected-branch-pill-baseline.yml
deleted file mode 100644
index fb6dffaf226..00000000000
--- a/changelogs/unreleased/fix-inconsistent-protected-branch-pill-baseline.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed inconsistent protected branch pill baseline
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-metrics-content-types.yml b/changelogs/unreleased/fix-metrics-content-types.yml
deleted file mode 100644
index a418dccffc3..00000000000
--- a/changelogs/unreleased/fix-metrics-content-types.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix setting Gitlab metrics content types
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-project-mirror-data-schema.yml b/changelogs/unreleased/fix-project-mirror-data-schema.yml
deleted file mode 100644
index 107f1fe3b9c..00000000000
--- a/changelogs/unreleased/fix-project-mirror-data-schema.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Fixes database inconsistencies between Community and Enterprise Edition on
- import state
-merge_request: 18811
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-shortcut-close-screen-with-key.yml b/changelogs/unreleased/fix-shortcut-close-screen-with-key.yml
deleted file mode 100644
index 9cbc856a075..00000000000
--- a/changelogs/unreleased/fix-shortcut-close-screen-with-key.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix close keyboard shortcuts dialog using the keyboard shortcut
-merge_request: 18783
-author: Lars Greiss
-type: fixed
diff --git a/changelogs/unreleased/fix-unverified-hover-state.yml b/changelogs/unreleased/fix-unverified-hover-state.yml
new file mode 100644
index 00000000000..003138f9821
--- /dev/null
+++ b/changelogs/unreleased/fix-unverified-hover-state.yml
@@ -0,0 +1,5 @@
+---
+title: Unverified hover state color changed to black
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-wiki-find-page-invalid-encoding.yml b/changelogs/unreleased/fix-wiki-find-page-invalid-encoding.yml
deleted file mode 100644
index f003bef8671..00000000000
--- a/changelogs/unreleased/fix-wiki-find-page-invalid-encoding.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix finding wiki pages when they have invalidly-encoded content
-merge_request: 18856
-author:
-type: fixed
diff --git a/changelogs/unreleased/fj-42354-custom-hooks-not-triggered-by-UI-wiki-edit.yml b/changelogs/unreleased/fj-42354-custom-hooks-not-triggered-by-UI-wiki-edit.yml
deleted file mode 100644
index 9fe458aba4a..00000000000
--- a/changelogs/unreleased/fj-42354-custom-hooks-not-triggered-by-UI-wiki-edit.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Triggering custom hooks by Wiki UI edit
-merge_request: 18251
-author:
-type: fixed
diff --git a/changelogs/unreleased/fj-45057-improve-ssrf-documentation.yml b/changelogs/unreleased/fj-45057-improve-ssrf-documentation.yml
deleted file mode 100644
index b923f442b26..00000000000
--- a/changelogs/unreleased/fj-45057-improve-ssrf-documentation.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Added Webhook SSRF prevention to documentation
-merge_request: 18532
-author:
-type: other
diff --git a/changelogs/unreleased/fj-46411-fix-badge-api-endpoint-route-with-relative-url.yml b/changelogs/unreleased/fj-46411-fix-badge-api-endpoint-route-with-relative-url.yml
new file mode 100644
index 00000000000..bd4e5a43352
--- /dev/null
+++ b/changelogs/unreleased/fj-46411-fix-badge-api-endpoint-route-with-relative-url.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed badge api endpoint route when relative url is set
+merge_request: 19004
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-46459-fix-expose-url-when-base-url-set.yml b/changelogs/unreleased/fj-46459-fix-expose-url-when-base-url-set.yml
new file mode 100644
index 00000000000..16b0ee06898
--- /dev/null
+++ b/changelogs/unreleased/fj-46459-fix-expose-url-when-base-url-set.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed bug where generated api urls didn't add the base url if set
+merge_request: 19003
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-change-gollum-gems-to-custom-ones.yml b/changelogs/unreleased/fj-change-gollum-gems-to-custom-ones.yml
deleted file mode 100644
index 53883e8d907..00000000000
--- a/changelogs/unreleased/fj-change-gollum-gems-to-custom-ones.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replacing gollum libraries for gitlab custom libs
-merge_request: 18343
-author:
-type: other
diff --git a/changelogs/unreleased/fl-pipelines-details-axios.yml b/changelogs/unreleased/fl-pipelines-details-axios.yml
deleted file mode 100644
index 0b72e54cba3..00000000000
--- a/changelogs/unreleased/fl-pipelines-details-axios.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replace vue resource with axios for pipelines details page
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/helm-add-alpine-mirrors.yml b/changelogs/unreleased/helm-add-alpine-mirrors.yml
deleted file mode 100644
index 656c4f911d0..00000000000
--- a/changelogs/unreleased/helm-add-alpine-mirrors.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Increase cluster applications installer availability using alpine linux mirrors
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/ide-file-finder.yml b/changelogs/unreleased/ide-file-finder.yml
deleted file mode 100644
index 252dd3a30c4..00000000000
--- a/changelogs/unreleased/ide-file-finder.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Added fuzzy file finder to web IDE
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/ide-improve-commit-panel.yml b/changelogs/unreleased/ide-improve-commit-panel.yml
deleted file mode 100644
index f6237db3039..00000000000
--- a/changelogs/unreleased/ide-improve-commit-panel.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improve interaction on WebIDE commit panel
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/improve-commit-message-body-rendering.yml b/changelogs/unreleased/improve-commit-message-body-rendering.yml
deleted file mode 100644
index 3fb9b03725e..00000000000
--- a/changelogs/unreleased/improve-commit-message-body-rendering.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improve commit message body rendering and fix responsive compare panels
-merge_request: 18725
-author: Constance Okoghenun
-type: changed
diff --git a/changelogs/unreleased/improve-jobs-queuing-time-metric.yml b/changelogs/unreleased/improve-jobs-queuing-time-metric.yml
deleted file mode 100644
index cee8b8523fd..00000000000
--- a/changelogs/unreleased/improve-jobs-queuing-time-metric.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Partition job_queue_duration_seconds with jobs_running_for_project
-merge_request: 17730
-author:
-type: changed
diff --git a/changelogs/unreleased/improve-quick-actions-summary-preview.yml b/changelogs/unreleased/improve-quick-actions-summary-preview.yml
deleted file mode 100644
index bc75c169ad7..00000000000
--- a/changelogs/unreleased/improve-quick-actions-summary-preview.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improve quick actions summary preview
-merge_request: 18659
-author: George Tsiolis
-type: changed
diff --git a/changelogs/unreleased/increase-new-issue-metadata-form-margin.yml b/changelogs/unreleased/increase-new-issue-metadata-form-margin.yml
deleted file mode 100644
index a7196f67969..00000000000
--- a/changelogs/unreleased/increase-new-issue-metadata-form-margin.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Increase new issue metadata form margin
-merge_request: 18630
-author: George Tsiolis
-type: fixed
diff --git a/changelogs/unreleased/inform-the-user-when-there-are-no-project-import-options-available.yml b/changelogs/unreleased/inform-the-user-when-there-are-no-project-import-options-available.yml
deleted file mode 100644
index c14f21fc644..00000000000
--- a/changelogs/unreleased/inform-the-user-when-there-are-no-project-import-options-available.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Inform the user when there are no project import options available
-merge_request: 18716
-author: George Tsiolis
-type: changed
diff --git a/changelogs/unreleased/issue-25256.yml b/changelogs/unreleased/issue-25256.yml
new file mode 100644
index 00000000000..e981b5f9a8f
--- /dev/null
+++ b/changelogs/unreleased/issue-25256.yml
@@ -0,0 +1,5 @@
+---
+title: Don't trim incoming emails that create new issues
+merge_request:
+author: Cameron Crockett
+type: fixed
diff --git a/changelogs/unreleased/issue_43660.yml b/changelogs/unreleased/issue_43660.yml
deleted file mode 100644
index d83c0ebcbb5..00000000000
--- a/changelogs/unreleased/issue_43660.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Enable prometheus monitoring by default
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/jivl-refactor-activity-calendar.yml b/changelogs/unreleased/jivl-refactor-activity-calendar.yml
deleted file mode 100644
index 0702ede4af9..00000000000
--- a/changelogs/unreleased/jivl-refactor-activity-calendar.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Refactored activity calendar
-merge_request: 18469
-author: Enrico Scholz
-type: changed
diff --git a/changelogs/unreleased/jprovazn-commit-notes-api.yml b/changelogs/unreleased/jprovazn-commit-notes-api.yml
deleted file mode 100644
index 4665d800ccf..00000000000
--- a/changelogs/unreleased/jprovazn-commit-notes-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add discussion API for merge requests and commits
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/jprovazn-generic-error.yml b/changelogs/unreleased/jprovazn-generic-error.yml
deleted file mode 100644
index ced3b84fe02..00000000000
--- a/changelogs/unreleased/jprovazn-generic-error.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Display only generic message on merge error to avoid exposing any potentially
- sensitive or user unfriendly backend messages.
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/jr-33320-lfs-settings-interface.yml b/changelogs/unreleased/jr-33320-lfs-settings-interface.yml
deleted file mode 100644
index b39308f5474..00000000000
--- a/changelogs/unreleased/jr-33320-lfs-settings-interface.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Show group and project LFS settings in the interface to Owners and Masters
-merge_request: 18562
-author:
-type: changed
diff --git a/changelogs/unreleased/jr-46209-web-ide-copy.yml b/changelogs/unreleased/jr-46209-web-ide-copy.yml
deleted file mode 100644
index 87ccae6ced0..00000000000
--- a/changelogs/unreleased/jr-46209-web-ide-copy.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix outdated Web IDE welcome copy
-merge_request: 18861
-author:
-type: fixed
diff --git a/changelogs/unreleased/jr-web-ide-shortcuts.yml b/changelogs/unreleased/jr-web-ide-shortcuts.yml
new file mode 100644
index 00000000000..a895eab432a
--- /dev/null
+++ b/changelogs/unreleased/jr-web-ide-shortcuts.yml
@@ -0,0 +1,5 @@
+---
+title: Add shortcuts to Web IDE docs and modal
+merge_request: 19044
+author:
+type: changed
diff --git a/changelogs/unreleased/jramsay-44880-filter-pipelines-by-sha.yml b/changelogs/unreleased/jramsay-44880-filter-pipelines-by-sha.yml
deleted file mode 100644
index 3654aa28ff4..00000000000
--- a/changelogs/unreleased/jramsay-44880-filter-pipelines-by-sha.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add sha filter to pipelines list API
-merge_request: 18125
-author:
-type: changed
diff --git a/changelogs/unreleased/label-links-on-project-transfer.yml b/changelogs/unreleased/label-links-on-project-transfer.yml
deleted file mode 100644
index fabedb77cb3..00000000000
--- a/changelogs/unreleased/label-links-on-project-transfer.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix label links update on project transfer
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/live-trace-v2-efficient-destroy-all.yml b/changelogs/unreleased/live-trace-v2-efficient-destroy-all.yml
deleted file mode 100644
index ab22739b73d..00000000000
--- a/changelogs/unreleased/live-trace-v2-efficient-destroy-all.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Destroy build_chunks efficiently with FastDestroyAll module
-merge_request: 18575
-author:
-type: performance
diff --git a/changelogs/unreleased/live-trace-v2.yml b/changelogs/unreleased/live-trace-v2.yml
deleted file mode 100644
index 875a66bc565..00000000000
--- a/changelogs/unreleased/live-trace-v2.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: New CI Job live-trace architecture
-merge_request: 18169
-author:
-type: changed
diff --git a/changelogs/unreleased/memoize-database-version.yml b/changelogs/unreleased/memoize-database-version.yml
new file mode 100644
index 00000000000..575348a53a1
--- /dev/null
+++ b/changelogs/unreleased/memoize-database-version.yml
@@ -0,0 +1,5 @@
+---
+title: Memoize Gitlab::Database.version
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/move-board-blank-state-vue-component.yml b/changelogs/unreleased/move-board-blank-state-vue-component.yml
deleted file mode 100644
index 0a278a8c009..00000000000
--- a/changelogs/unreleased/move-board-blank-state-vue-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move BoardBlankState vue component
-merge_request: 17666
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/move-estimate-only-pane-vue-component.yml b/changelogs/unreleased/move-estimate-only-pane-vue-component.yml
deleted file mode 100644
index b6c538f70b3..00000000000
--- a/changelogs/unreleased/move-estimate-only-pane-vue-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move TimeTrackingEstimateOnlyPane vue component
-merge_request: 18318
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/move-help-state-vue-component.yml b/changelogs/unreleased/move-help-state-vue-component.yml
deleted file mode 100644
index 6108368cde0..00000000000
--- a/changelogs/unreleased/move-help-state-vue-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move TimeTrackingHelpState vue component
-merge_request: 18319
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/move-notification-service-calls-to-sidekiq.yml b/changelogs/unreleased/move-notification-service-calls-to-sidekiq.yml
deleted file mode 100644
index b2517884d3c..00000000000
--- a/changelogs/unreleased/move-notification-service-calls-to-sidekiq.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Compute notification recipients in background jobs
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/move-pipeline-failed-vue-component.yml b/changelogs/unreleased/move-pipeline-failed-vue-component.yml
deleted file mode 100644
index 38d42134876..00000000000
--- a/changelogs/unreleased/move-pipeline-failed-vue-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move PipelineFailed vue component
-merge_request: 18277
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/move-time-tracking-spent-only-pane-vue-component.yml b/changelogs/unreleased/move-time-tracking-spent-only-pane-vue-component.yml
deleted file mode 100644
index d2db0df5a04..00000000000
--- a/changelogs/unreleased/move-time-tracking-spent-only-pane-vue-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move TimeTrackingSpentOnlyPane vue component
-merge_request: 18710
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/osw-use-cached-highlighted-content-for-discussions.yml b/changelogs/unreleased/osw-use-cached-highlighted-content-for-discussions.yml
deleted file mode 100644
index 03a11a3038a..00000000000
--- a/changelogs/unreleased/osw-use-cached-highlighted-content-for-discussions.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use persisted diff data instead fetching Git on discussions
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/performance-gb-improve-pipeline-creation-service.yml b/changelogs/unreleased/performance-gb-improve-pipeline-creation-service.yml
deleted file mode 100644
index bd308f37bec..00000000000
--- a/changelogs/unreleased/performance-gb-improve-pipeline-creation-service.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improve performance of a service responsible for creating a pipeline
-merge_request: 18582
-author:
-type: performance
diff --git a/changelogs/unreleased/pipelines-index-performance.yml b/changelogs/unreleased/pipelines-index-performance.yml
new file mode 100644
index 00000000000..928c2ddab72
--- /dev/null
+++ b/changelogs/unreleased/pipelines-index-performance.yml
@@ -0,0 +1,5 @@
+---
+title: Improve performance of project pipelines pages
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/rd-44635-error-500-when-clicking-ghost-user-or-gitlab-support-bot-in-ui.yml b/changelogs/unreleased/rd-44635-error-500-when-clicking-ghost-user-or-gitlab-support-bot-in-ui.yml
deleted file mode 100644
index a08a75ceda6..00000000000
--- a/changelogs/unreleased/rd-44635-error-500-when-clicking-ghost-user-or-gitlab-support-bot-in-ui.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix missing namespace for some internal users
-merge_request: 18357
-author:
-type: fixed
diff --git a/changelogs/unreleased/rd-45502-uploading-project-export-with-lfs-file-locks-fails.yml b/changelogs/unreleased/rd-45502-uploading-project-export-with-lfs-file-locks-fails.yml
deleted file mode 100644
index e3266dda629..00000000000
--- a/changelogs/unreleased/rd-45502-uploading-project-export-with-lfs-file-locks-fails.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Don't include lfs_file_locks data in export bundle
-merge_request: 18495
-author:
-type: fixed
diff --git a/changelogs/unreleased/refactor-move-mr-widget-ready-to-merge-vue-component.yml b/changelogs/unreleased/refactor-move-mr-widget-ready-to-merge-vue-component.yml
deleted file mode 100644
index 90192fae030..00000000000
--- a/changelogs/unreleased/refactor-move-mr-widget-ready-to-merge-vue-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move ReadyToMerge vue component
-merge_request: 17545
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/refactor-move-mr-widget-wip-vue-component.yml b/changelogs/unreleased/refactor-move-mr-widget-wip-vue-component.yml
deleted file mode 100644
index 0f045431aae..00000000000
--- a/changelogs/unreleased/refactor-move-mr-widget-wip-vue-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move WorkInProgress vue component
-merge_request: 17536
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/refactor-move-no-tracking-pane-vue-component.yml b/changelogs/unreleased/refactor-move-no-tracking-pane-vue-component.yml
deleted file mode 100644
index 4bb088a1e58..00000000000
--- a/changelogs/unreleased/refactor-move-no-tracking-pane-vue-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move TimeTrackingNoTrackingPane vue component
-merge_request: 18676
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/refactor-move-sidebar-time-tracking-vue-component.yml b/changelogs/unreleased/refactor-move-sidebar-time-tracking-vue-component.yml
deleted file mode 100644
index 4f578bfcf26..00000000000
--- a/changelogs/unreleased/refactor-move-sidebar-time-tracking-vue-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move SidebarTimeTracking vue component
-merge_request: 18677
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/rename-merge-request-widget-author-component.yml b/changelogs/unreleased/rename-merge-request-widget-author-component.yml
new file mode 100644
index 00000000000..15e6eafd826
--- /dev/null
+++ b/changelogs/unreleased/rename-merge-request-widget-author-component.yml
@@ -0,0 +1,5 @@
+---
+title: Rename merge request widget author component
+merge_request: 19079
+author: George Tsiolis
+type: changed
diff --git a/changelogs/unreleased/rename-overview-project-sidenav.yml b/changelogs/unreleased/rename-overview-project-sidenav.yml
deleted file mode 100644
index 3632ef25c00..00000000000
--- a/changelogs/unreleased/rename-overview-project-sidenav.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Renamed Overview to Project in the contextual navigation at a project level
-merge_request: 18295
-author: Constance Okoghenun
-type: changed
diff --git a/changelogs/unreleased/restore-label-underline-color.yml b/changelogs/unreleased/restore-label-underline-color.yml
deleted file mode 100644
index 2e24ece5c36..00000000000
--- a/changelogs/unreleased/restore-label-underline-color.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Restore label underline color
-merge_request: 18407
-author: George Tsiolis
-type: fixed
diff --git a/changelogs/unreleased/restore-size-and-position-for-fork-icon.yml b/changelogs/unreleased/restore-size-and-position-for-fork-icon.yml
deleted file mode 100644
index dd8dad0b17d..00000000000
--- a/changelogs/unreleased/restore-size-and-position-for-fork-icon.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix size and position for fork icon
-merge_request: 18449
-author: George Tsiolis
-type: changed
diff --git a/changelogs/unreleased/revert-discussion-counter-height.yml b/changelogs/unreleased/revert-discussion-counter-height.yml
deleted file mode 100644
index 331ff997009..00000000000
--- a/changelogs/unreleased/revert-discussion-counter-height.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Revert discussion counter height
-merge_request: 18656
-author: George Tsiolis
-type: changed
diff --git a/changelogs/unreleased/security-45689-fix-archive-cache-bug.yml b/changelogs/unreleased/security-45689-fix-archive-cache-bug.yml
deleted file mode 100644
index 0103a7fc430..00000000000
--- a/changelogs/unreleased/security-45689-fix-archive-cache-bug.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Serve archive requests with the correct file in all cases
-merge_request:
-author:
-type: security
diff --git a/changelogs/unreleased/security_issue_42029.yml b/changelogs/unreleased/security_issue_42029.yml
deleted file mode 100644
index 0772e33f930..00000000000
--- a/changelogs/unreleased/security_issue_42029.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Sanitizes user name to avoid XSS attacks
-merge_request:
-author:
-type: security
diff --git a/changelogs/unreleased/sh-bump-lograge.yml b/changelogs/unreleased/sh-bump-lograge.yml
deleted file mode 100644
index 65b15153a35..00000000000
--- a/changelogs/unreleased/sh-bump-lograge.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Bump lograge to 0.10.0 and remove monkey patch
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/sh-fix-blocked-user-account-ldap.yml b/changelogs/unreleased/sh-fix-blocked-user-account-ldap.yml
deleted file mode 100644
index f7abe763ea8..00000000000
--- a/changelogs/unreleased/sh-fix-blocked-user-account-ldap.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix system hook not firing for blocked users when LDAP sign-in is used
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-fix-grape-logging-status-code.yml b/changelogs/unreleased/sh-fix-grape-logging-status-code.yml
new file mode 100644
index 00000000000..aabf9a84bfb
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-grape-logging-status-code.yml
@@ -0,0 +1,5 @@
+---
+title: Fix api_json.log not always reporting the right HTTP status code
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-move-delete-groups-api-async.yml b/changelogs/unreleased/sh-move-delete-groups-api-async.yml
new file mode 100644
index 00000000000..1b200cac5c5
--- /dev/null
+++ b/changelogs/unreleased/sh-move-delete-groups-api-async.yml
@@ -0,0 +1,5 @@
+---
+title: Move API group deletion to Sidekiq
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/show-group-id-in-group-settings.yml b/changelogs/unreleased/show-group-id-in-group-settings.yml
deleted file mode 100644
index b975fe8c71d..00000000000
--- a/changelogs/unreleased/show-group-id-in-group-settings.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Show group id in group settings
-merge_request: 18482
-author: George Tsiolis
-type: added
diff --git a/changelogs/unreleased/show-runners-description-on-jobs-page.yml b/changelogs/unreleased/show-runners-description-on-jobs-page.yml
deleted file mode 100644
index d9414a3d021..00000000000
--- a/changelogs/unreleased/show-runners-description-on-jobs-page.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Show Runner's description on job's page
-merge_request: 17321
-author:
-type: added
diff --git a/changelogs/unreleased/tc-repo-verify-mails.yml b/changelogs/unreleased/tc-repo-verify-mails.yml
deleted file mode 100644
index b4d3c4b1596..00000000000
--- a/changelogs/unreleased/tc-repo-verify-mails.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Small improvements to repository checks
-merge_request: 18484
-author:
-type: changed
diff --git a/changelogs/unreleased/tz-upgrade-underscore.yml b/changelogs/unreleased/tz-upgrade-underscore.yml
deleted file mode 100644
index 5dfd8154ecd..00000000000
--- a/changelogs/unreleased/tz-upgrade-underscore.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Upgrade underscore.js to 1.9.0
-merge_request: 18578
-author:
-type: other
diff --git a/changelogs/unreleased/unresolved-discussions-vue-component-i18n-and-tests.yml b/changelogs/unreleased/unresolved-discussions-vue-component-i18n-and-tests.yml
deleted file mode 100644
index d99a9c93c0b..00000000000
--- a/changelogs/unreleased/unresolved-discussions-vue-component-i18n-and-tests.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add i18n and update specs for UnresolvedDiscussions vue component
-merge_request: 17866
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/update-environment-item-action-buttons-icons.yml b/changelogs/unreleased/update-environment-item-action-buttons-icons.yml
deleted file mode 100644
index 7f06022be3e..00000000000
--- a/changelogs/unreleased/update-environment-item-action-buttons-icons.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update environment item action buttons icons
-merge_request: 18632
-author: George Tsiolis
-type: changed
diff --git a/changelogs/unreleased/update-timeline-icon-for-description-edit.yml b/changelogs/unreleased/update-timeline-icon-for-description-edit.yml
deleted file mode 100644
index 560db00e503..00000000000
--- a/changelogs/unreleased/update-timeline-icon-for-description-edit.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update timeline icon for description edit
-merge_request: 18633
-author: George Tsiolis
-type: changed
diff --git a/changelogs/unreleased/use-case-insensitive-ordering-for-dashboard-filters.yml b/changelogs/unreleased/use-case-insensitive-ordering-for-dashboard-filters.yml
new file mode 100644
index 00000000000..098e4b1d5fa
--- /dev/null
+++ b/changelogs/unreleased/use-case-insensitive-ordering-for-dashboard-filters.yml
@@ -0,0 +1,5 @@
+---
+title: "Use case in-sensitive ordering by name for dashboard"
+merge_request: 18553
+author: "@vedharish"
+type: fixed
diff --git a/changelogs/unreleased/winh-dashboard-any-milestone.yml b/changelogs/unreleased/winh-dashboard-any-milestone.yml
deleted file mode 100644
index 49eecd3da2b..00000000000
--- a/changelogs/unreleased/winh-dashboard-any-milestone.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Reset milestone filter when clicking "Any Milestone" in dashboard
-merge_request: 18531
-author:
-type: fixed
diff --git a/changelogs/unreleased/winh-dropdown-entry-unlocking.yml b/changelogs/unreleased/winh-dropdown-entry-unlocking.yml
deleted file mode 100644
index fc669af1f57..00000000000
--- a/changelogs/unreleased/winh-dropdown-entry-unlocking.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove green background from unlock button in admin area
-merge_request: 18288
-author:
-type: changed
diff --git a/changelogs/unreleased/winh-make-it-right-now.yml b/changelogs/unreleased/winh-make-it-right-now.yml
new file mode 100644
index 00000000000..7b386c0b332
--- /dev/null
+++ b/changelogs/unreleased/winh-make-it-right-now.yml
@@ -0,0 +1,5 @@
+---
+title: Use "right now" for short time periods
+merge_request: 19095
+author:
+type: changed
diff --git a/changelogs/unreleased/zj-branch-containing-sha-opt-out.yml b/changelogs/unreleased/zj-branch-containing-sha-opt-out.yml
deleted file mode 100644
index 3d11ee588ae..00000000000
--- a/changelogs/unreleased/zj-branch-containing-sha-opt-out.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Detecting branchnames containing a commit uses Gitaly by default
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/zj-calculate-checksum-mandator.yml b/changelogs/unreleased/zj-calculate-checksum-mandator.yml
new file mode 100644
index 00000000000..83315a3c5dd
--- /dev/null
+++ b/changelogs/unreleased/zj-calculate-checksum-mandator.yml
@@ -0,0 +1,5 @@
+---
+title: Remove shellout implementation for Repository checksums
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/zj-find-license-opt-out.yml b/changelogs/unreleased/zj-find-license-opt-out.yml
deleted file mode 100644
index be2656601a9..00000000000
--- a/changelogs/unreleased/zj-find-license-opt-out.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Detect repository license on Gitaly by default
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/zj-fork-opt-out.yml b/changelogs/unreleased/zj-fork-opt-out.yml
deleted file mode 100644
index 56bf6b8b0f6..00000000000
--- a/changelogs/unreleased/zj-fork-opt-out.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Gitaly handles repository forks by default
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/zj-namespace-service-mandatory.yml b/changelogs/unreleased/zj-namespace-service-mandatory.yml
deleted file mode 100644
index d890741c51b..00000000000
--- a/changelogs/unreleased/zj-namespace-service-mandatory.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Finish NamespaceService migration to Gitaly
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/zj-ref-exists-opt-out.yml b/changelogs/unreleased/zj-ref-exists-opt-out.yml
deleted file mode 100644
index cdffecb0d0a..00000000000
--- a/changelogs/unreleased/zj-ref-exists-opt-out.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Check if a ref exists is done by Gitaly by default
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/zj-repo-checksum-opt-out.yml b/changelogs/unreleased/zj-repo-checksum-opt-out.yml
deleted file mode 100644
index 98dfedf7475..00000000000
--- a/changelogs/unreleased/zj-repo-checksum-opt-out.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Compute Gitlab::Git::Repository#checksum on Gitaly by default
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/zj-repository-exist-mandatory.yml b/changelogs/unreleased/zj-repository-exist-mandatory.yml
deleted file mode 100644
index 7d83446e90f..00000000000
--- a/changelogs/unreleased/zj-repository-exist-mandatory.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Repository#exists? is always executed through Gitaly
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/zj-tag-containing-sha-opt-out.yml b/changelogs/unreleased/zj-tag-containing-sha-opt-out.yml
deleted file mode 100644
index 4774c7811d1..00000000000
--- a/changelogs/unreleased/zj-tag-containing-sha-opt-out.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Detecting tags containing a commit uses Gitaly by default
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/zj-workhorse-archive-mandatory.yml b/changelogs/unreleased/zj-workhorse-archive-mandatory.yml
new file mode 100644
index 00000000000..3a4a351a2b9
--- /dev/null
+++ b/changelogs/unreleased/zj-workhorse-archive-mandatory.yml
@@ -0,0 +1,5 @@
+---
+title: Workhorse will use Gitaly to create archives
+merge_request:
+author:
+type: other
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 5248bd858a0..dd36700964a 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -470,6 +470,3 @@ if Rails.env.test?
Settings.gitlab['default_can_create_group'] = true
Settings.gitlab['default_can_create_team'] = false
end
-
-# Force a refresh of application settings at startup
-ApplicationSetting.expire
diff --git a/config/initializers/deprecations.rb b/config/initializers/deprecations.rb
index c8d7f742bb1..14616e726d9 100644
--- a/config/initializers/deprecations.rb
+++ b/config/initializers/deprecations.rb
@@ -1,4 +1,4 @@
-if Gitlab.dev_env_or_com?
+if Rails.env.development? || ENV['GITLAB_LEGACY_PATH_LOG_MESSAGE']
deprecator = ActiveSupport::Deprecation.new('11.0', 'GitLab')
deprecator.behavior = -> (message, callstack) {
diff --git a/config/initializers/flipper.rb b/config/initializers/flipper.rb
index c60ad535fd5..80cab7273e5 100644
--- a/config/initializers/flipper.rb
+++ b/config/initializers/flipper.rb
@@ -1,22 +1 @@
-require 'flipper/adapters/active_record'
-require 'flipper/adapters/active_support_cache_store'
-
-Flipper.configure do |config|
- config.default do
- adapter = Flipper::Adapters::ActiveRecord.new(
- feature_class: Feature::FlipperFeature, gate_class: Feature::FlipperGate)
- cached_adapter = Flipper::Adapters::ActiveSupportCacheStore.new(
- adapter,
- Rails.cache,
- expires_in: 1.hour)
-
- Flipper.new(cached_adapter)
- end
-end
-
Feature.register_feature_groups
-
-unless Rails.env.test?
- require 'flipper/middleware/memoizer'
- Rails.application.config.middleware.use Flipper::Middleware::Memoizer
-end
diff --git a/config/initializers/kubeclient.rb b/config/initializers/kubeclient.rb
new file mode 100644
index 00000000000..7f115268b37
--- /dev/null
+++ b/config/initializers/kubeclient.rb
@@ -0,0 +1,16 @@
+class Kubeclient::Client
+ # We need to monkey patch this method until
+ # https://github.com/abonas/kubeclient/pull/323 is merged
+ def proxy_url(kind, name, port, namespace = '')
+ discover unless @discovered
+ entity_name_plural =
+ if %w[services pods nodes].include?(kind.to_s)
+ kind.to_s
+ else
+ @entities[kind.to_s].resource_name
+ end
+
+ ns_prefix = build_namespace_prefix(namespace)
+ rest_client["#{ns_prefix}#{entity_name_plural}/#{name}:#{port}/proxy"].url
+ end
+end
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 5096f35e808..27050e7069d 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -2,20 +2,25 @@ const fs = require('fs');
const path = require('path');
const glob = require('glob');
const webpack = require('webpack');
+const VueLoaderPlugin = require('vue-loader/lib/plugin');
const StatsWriterPlugin = require('webpack-stats-plugin').StatsWriterPlugin;
const CopyWebpackPlugin = require('copy-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const ROOT_PATH = path.resolve(__dirname, '..');
+const CACHE_PATH = path.join(ROOT_PATH, 'tmp/cache');
const IS_PRODUCTION = process.env.NODE_ENV === 'production';
const IS_DEV_SERVER = process.argv.join(' ').indexOf('webpack-dev-server') !== -1;
const DEV_SERVER_HOST = process.env.DEV_SERVER_HOST || 'localhost';
const DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808;
-const DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false';
+const DEV_SERVER_LIVERELOAD = IS_DEV_SERVER && process.env.DEV_SERVER_LIVERELOAD !== 'false';
const WEBPACK_REPORT = process.env.WEBPACK_REPORT;
const NO_COMPRESSION = process.env.NO_COMPRESSION;
+const VUE_VERSION = require('vue/package.json').version;
+const VUE_LOADER_VERSION = require('vue-loader/package.json').version;
+
let autoEntriesCount = 0;
let watchAutoEntries = [];
const defaultEntries = ['./main'];
@@ -61,7 +66,7 @@ function generateEntries() {
return Object.assign(manualEntries, autoEntries);
}
-const config = {
+module.exports = {
mode: IS_PRODUCTION ? 'production' : 'development',
context: path.join(ROOT_PATH, 'app/assets/javascripts'),
@@ -76,46 +81,43 @@ const config = {
globalObject: 'this', // allow HMR and web workers to play nice
},
- optimization: {
- nodeEnv: false,
- runtimeChunk: 'single',
- splitChunks: {
- maxInitialRequests: 4,
- cacheGroups: {
- default: false,
- common: () => ({
- priority: 20,
- name: 'main',
- chunks: 'initial',
- minChunks: autoEntriesCount * 0.9,
- }),
- vendors: {
- priority: 10,
- chunks: 'async',
- test: /[\\/](node_modules|vendor[\\/]assets[\\/]javascripts)[\\/]/,
- },
- commons: {
- chunks: 'all',
- minChunks: 2,
- reuseExistingChunk: true,
- },
- },
+ resolve: {
+ extensions: ['.js'],
+ alias: {
+ '~': path.join(ROOT_PATH, 'app/assets/javascripts'),
+ emojis: path.join(ROOT_PATH, 'fixtures/emojis'),
+ empty_states: path.join(ROOT_PATH, 'app/views/shared/empty_states'),
+ icons: path.join(ROOT_PATH, 'app/views/shared/icons'),
+ images: path.join(ROOT_PATH, 'app/assets/images'),
+ vendor: path.join(ROOT_PATH, 'vendor/assets/javascripts'),
+ vue$: 'vue/dist/vue.esm.js',
+ spec: path.join(ROOT_PATH, 'spec/javascripts'),
},
},
module: {
+ strictExportPresence: true,
rules: [
{
test: /\.js$/,
- exclude: /(node_modules|vendor\/assets)/,
+ exclude: path => /node_modules|vendor[\\/]assets/.test(path) && !/\.vue\.js/.test(path),
loader: 'babel-loader',
options: {
- cacheDirectory: path.join(ROOT_PATH, 'tmp/cache/babel-loader'),
+ cacheDirectory: path.join(CACHE_PATH, 'babel-loader'),
},
},
{
test: /\.vue$/,
loader: 'vue-loader',
+ options: {
+ cacheDirectory: path.join(CACHE_PATH, 'vue-loader'),
+ cacheIdentifier: [
+ process.env.NODE_ENV || 'development',
+ webpack.version,
+ VUE_VERSION,
+ VUE_LOADER_VERSION,
+ ].join('|'),
+ },
},
{
test: /\.svg$/,
@@ -147,10 +149,9 @@ const config = {
},
},
{
- test: /katex.min.css$/,
- include: /node_modules\/katex\/dist/,
+ test: /.css$/,
use: [
- { loader: 'style-loader' },
+ 'vue-style-loader',
{
loader: 'css-loader',
options: {
@@ -175,9 +176,34 @@ const config = {
],
},
],
-
noParse: [/monaco-editor\/\w+\/vs\//],
- strictExportPresence: true,
+ },
+
+ optimization: {
+ nodeEnv: false,
+ runtimeChunk: 'single',
+ splitChunks: {
+ maxInitialRequests: 4,
+ cacheGroups: {
+ default: false,
+ common: () => ({
+ priority: 20,
+ name: 'main',
+ chunks: 'initial',
+ minChunks: autoEntriesCount * 0.9,
+ }),
+ vendors: {
+ priority: 10,
+ chunks: 'async',
+ test: /[\\/](node_modules|vendor[\\/]assets[\\/]javascripts)[\\/]/,
+ },
+ commons: {
+ chunks: 'all',
+ minChunks: 2,
+ reuseExistingChunk: true,
+ },
+ },
+ },
},
plugins: [
@@ -197,6 +223,9 @@ const config = {
},
}),
+ // enable vue-loader to use existing loader rules for other module types
+ new VueLoaderPlugin(),
+
// prevent pikaday from including moment.js
new webpack.IgnorePlugin(/moment/, /pikaday/),
@@ -228,40 +257,52 @@ const config = {
},
},
]),
- ],
- resolve: {
- extensions: ['.js'],
- alias: {
- '~': path.join(ROOT_PATH, 'app/assets/javascripts'),
- emojis: path.join(ROOT_PATH, 'fixtures/emojis'),
- empty_states: path.join(ROOT_PATH, 'app/views/shared/empty_states'),
- icons: path.join(ROOT_PATH, 'app/views/shared/icons'),
- images: path.join(ROOT_PATH, 'app/assets/images'),
- vendor: path.join(ROOT_PATH, 'vendor/assets/javascripts'),
- vue$: 'vue/dist/vue.esm.js',
- spec: path.join(ROOT_PATH, 'spec/javascripts'),
- },
- },
+ // compression can require a lot of compute time and is disabled in CI
+ IS_PRODUCTION && !NO_COMPRESSION && new CompressionPlugin(),
- // sqljs requires fs
- node: {
- fs: 'empty',
- },
-};
+ // WatchForChangesPlugin
+ // TODO: publish this as a separate plugin
+ IS_DEV_SERVER && {
+ apply(compiler) {
+ compiler.hooks.emit.tapAsync('WatchForChangesPlugin', (compilation, callback) => {
+ const missingDeps = Array.from(compilation.missingDependencies);
+ const nodeModulesPath = path.join(ROOT_PATH, 'node_modules');
+ const hasMissingNodeModules = missingDeps.some(
+ file => file.indexOf(nodeModulesPath) !== -1
+ );
-if (IS_PRODUCTION) {
- config.devtool = 'source-map';
+ // watch for changes to missing node_modules
+ if (hasMissingNodeModules) compilation.contextDependencies.add(nodeModulesPath);
- // compression can require a lot of compute time and is disabled in CI
- if (!NO_COMPRESSION) {
- config.plugins.push(new CompressionPlugin());
- }
-}
+ // watch for changes to automatic entrypoints
+ watchAutoEntries.forEach(watchPath => compilation.contextDependencies.add(watchPath));
+
+ // report our auto-generated bundle count
+ console.log(
+ `${autoEntriesCount} entries from '/pages' automatically added to webpack output.`
+ );
-if (IS_DEV_SERVER) {
- config.devtool = 'cheap-module-eval-source-map';
- config.devServer = {
+ callback();
+ });
+ },
+ },
+
+ // enable HMR only in webpack-dev-server
+ DEV_SERVER_LIVERELOAD && new webpack.HotModuleReplacementPlugin(),
+
+ // optionally generate webpack bundle analysis
+ WEBPACK_REPORT &&
+ new BundleAnalyzerPlugin({
+ analyzerMode: 'static',
+ generateStatsFile: true,
+ openAnalyzer: false,
+ reportFilename: path.join(ROOT_PATH, 'webpack-report/index.html'),
+ statsFilename: path.join(ROOT_PATH, 'webpack-report/stats.json'),
+ }),
+ ].filter(Boolean),
+
+ devServer: {
host: DEV_SERVER_HOST,
port: DEV_SERVER_PORT,
disableHostCheck: true,
@@ -269,46 +310,10 @@ if (IS_DEV_SERVER) {
stats: 'errors-only',
hot: DEV_SERVER_LIVERELOAD,
inline: DEV_SERVER_LIVERELOAD,
- };
- config.plugins.push({
- apply(compiler) {
- compiler.hooks.emit.tapAsync('WatchForChangesPlugin', (compilation, callback) => {
- const missingDeps = Array.from(compilation.missingDependencies);
- const nodeModulesPath = path.join(ROOT_PATH, 'node_modules');
- const hasMissingNodeModules = missingDeps.some(
- file => file.indexOf(nodeModulesPath) !== -1
- );
-
- // watch for changes to missing node_modules
- if (hasMissingNodeModules) compilation.contextDependencies.add(nodeModulesPath);
-
- // watch for changes to automatic entrypoints
- watchAutoEntries.forEach(watchPath => compilation.contextDependencies.add(watchPath));
-
- // report our auto-generated bundle count
- console.log(
- `${autoEntriesCount} entries from '/pages' automatically added to webpack output.`
- );
-
- callback();
- });
- },
- });
- if (DEV_SERVER_LIVERELOAD) {
- config.plugins.push(new webpack.HotModuleReplacementPlugin());
- }
-}
+ },
-if (WEBPACK_REPORT) {
- config.plugins.push(
- new BundleAnalyzerPlugin({
- analyzerMode: 'static',
- generateStatsFile: true,
- openAnalyzer: false,
- reportFilename: path.join(ROOT_PATH, 'webpack-report/index.html'),
- statsFilename: path.join(ROOT_PATH, 'webpack-report/stats.json'),
- })
- );
-}
+ devtool: IS_PRODUCTION ? 'source-map' : 'cheap-module-eval-source-map',
-module.exports = config;
+ // sqljs requires fs
+ node: { fs: 'empty' },
+};
diff --git a/db/migrate/20180503175053_ensure_missing_columns_to_project_mirror_data.rb b/db/migrate/20180503175053_ensure_missing_columns_to_project_mirror_data.rb
new file mode 100644
index 00000000000..970a53d68d0
--- /dev/null
+++ b/db/migrate/20180503175053_ensure_missing_columns_to_project_mirror_data.rb
@@ -0,0 +1,15 @@
+class EnsureMissingColumnsToProjectMirrorData < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ add_column :project_mirror_data, :status, :string unless column_exists?(:project_mirror_data, :status)
+ add_column :project_mirror_data, :jid, :string unless column_exists?(:project_mirror_data, :jid)
+ add_column :project_mirror_data, :last_error, :text unless column_exists?(:project_mirror_data, :last_error)
+ end
+
+ def down
+ # db/migrate/20180502122856_create_project_mirror_data.rb will remove the table
+ end
+end
diff --git a/db/migrate/20180504195842_project_name_lower_index.rb b/db/migrate/20180504195842_project_name_lower_index.rb
new file mode 100644
index 00000000000..d6f25d3d4ab
--- /dev/null
+++ b/db/migrate/20180504195842_project_name_lower_index.rb
@@ -0,0 +1,32 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class ProjectNameLowerIndex < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+ INDEX_NAME = 'index_projects_on_lower_name'
+
+ disable_ddl_transaction!
+
+ def up
+ return unless Gitlab::Database.postgresql?
+
+ disable_statement_timeout
+
+ execute "CREATE INDEX CONCURRENTLY #{INDEX_NAME} ON projects (LOWER(name))"
+ end
+
+ def down
+ return unless Gitlab::Database.postgresql?
+
+ disable_statement_timeout
+
+ if supports_drop_index_concurrently?
+ execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME}"
+ else
+ execute "DROP INDEX IF EXISTS #{INDEX_NAME}"
+ end
+ end
+end
diff --git a/db/migrate/20180517082340_add_not_null_constraints_to_project_authorizations.rb b/db/migrate/20180517082340_add_not_null_constraints_to_project_authorizations.rb
new file mode 100644
index 00000000000..3b7b877232b
--- /dev/null
+++ b/db/migrate/20180517082340_add_not_null_constraints_to_project_authorizations.rb
@@ -0,0 +1,38 @@
+class AddNotNullConstraintsToProjectAuthorizations < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def up
+ if Gitlab::Database.postgresql?
+ # One-pass version for PostgreSQL
+ execute <<~SQL
+ ALTER TABLE project_authorizations
+ ALTER COLUMN user_id SET NOT NULL,
+ ALTER COLUMN project_id SET NOT NULL,
+ ALTER COLUMN access_level SET NOT NULL
+ SQL
+ else
+ change_column_null :project_authorizations, :user_id, false
+ change_column_null :project_authorizations, :project_id, false
+ change_column_null :project_authorizations, :access_level, false
+ end
+ end
+
+ def down
+ if Gitlab::Database.postgresql?
+ # One-pass version for PostgreSQL
+ execute <<~SQL
+ ALTER TABLE project_authorizations
+ ALTER COLUMN user_id DROP NOT NULL,
+ ALTER COLUMN project_id DROP NOT NULL,
+ ALTER COLUMN access_level DROP NOT NULL
+ SQL
+ else
+ change_column_null :project_authorizations, :user_id, true
+ change_column_null :project_authorizations, :project_id, true
+ change_column_null :project_authorizations, :access_level, true
+ end
+ end
+end
diff --git a/db/migrate/20180521171529_increase_mysql_text_limit_for_gpg_keys.rb b/db/migrate/20180521171529_increase_mysql_text_limit_for_gpg_keys.rb
new file mode 100644
index 00000000000..df84898003f
--- /dev/null
+++ b/db/migrate/20180521171529_increase_mysql_text_limit_for_gpg_keys.rb
@@ -0,0 +1,2 @@
+# rubocop:disable all
+require_relative 'gpg_keys_limits_to_mysql'
diff --git a/db/migrate/gpg_keys_limits_to_mysql.rb b/db/migrate/gpg_keys_limits_to_mysql.rb
new file mode 100644
index 00000000000..780340d0564
--- /dev/null
+++ b/db/migrate/gpg_keys_limits_to_mysql.rb
@@ -0,0 +1,15 @@
+class IncreaseMysqlTextLimitForGpgKeys < ActiveRecord::Migration
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def up
+ return unless Gitlab::Database.mysql?
+
+ change_column :gpg_keys, :key, :text, limit: 16.megabytes - 1
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/optional_migrations/composite_primary_keys.rb b/db/optional_migrations/composite_primary_keys.rb
new file mode 100644
index 00000000000..0fd3fca52dd
--- /dev/null
+++ b/db/optional_migrations/composite_primary_keys.rb
@@ -0,0 +1,63 @@
+# This migration adds a primary key constraint to tables
+# that only have a composite unique key.
+#
+# This is not strictly relevant to Rails (v4 does not
+# support composite primary keys). However this becomes
+# useful for e.g. PostgreSQL's logical replication (pglogical)
+# which requires all tables to have a primary key constraint.
+#
+# In that sense, the migration is optional and not strictly needed.
+class CompositePrimaryKeysMigration < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ Index = Struct.new(:table, :name, :columns)
+
+ TABLES = [
+ Index.new(:issue_assignees, 'index_issue_assignees_on_issue_id_and_user_id', %i(issue_id user_id)),
+ Index.new(:user_interacted_projects, 'index_user_interacted_projects_on_project_id_and_user_id', %i(project_id user_id)),
+ Index.new(:merge_request_diff_files, 'index_merge_request_diff_files_on_mr_diff_id_and_order', %i(merge_request_diff_id relative_order)),
+ Index.new(:merge_request_diff_commits, 'index_merge_request_diff_commits_on_mr_diff_id_and_order', %i(merge_request_diff_id relative_order)),
+ Index.new(:project_authorizations, 'index_project_authorizations_on_user_id_project_id_access_level', %i(user_id project_id access_level)),
+ Index.new(:push_event_payloads, 'index_push_event_payloads_on_event_id', %i(event_id)),
+ Index.new(:schema_migrations, 'unique_schema_migrations', %(version)),
+ ]
+
+ disable_ddl_transaction!
+
+ def up
+ return unless Gitlab::Database.postgresql?
+
+ disable_statement_timeout
+ TABLES.each do |index|
+ add_primary_key(index)
+ end
+ end
+
+ def down
+ return unless Gitlab::Database.postgresql?
+
+ disable_statement_timeout
+ TABLES.each do |index|
+ remove_primary_key(index)
+ end
+ end
+
+ private
+ def add_primary_key(index)
+ execute "ALTER TABLE #{index.table} ADD PRIMARY KEY USING INDEX #{index.name}"
+ end
+
+ def remove_primary_key(index)
+ temp_index_name = "#{index.name[0..58]}_old"
+ rename_index index.table, index.name, temp_index_name if index_exists_by_name?(index.table, index.name)
+
+ # re-create unique key index
+ add_concurrent_index index.table, index.columns, unique: true, name: index.name
+
+ # This also drops the `temp_index_name` as this is owned by the constraint
+ execute "ALTER TABLE #{index.table} DROP CONSTRAINT IF EXISTS #{temp_index_name}"
+ end
+end
+
diff --git a/db/post_migrate/20180514161336_remove_gemnasium_service.rb b/db/post_migrate/20180514161336_remove_gemnasium_service.rb
new file mode 100644
index 00000000000..6d7806e8daa
--- /dev/null
+++ b/db/post_migrate/20180514161336_remove_gemnasium_service.rb
@@ -0,0 +1,15 @@
+class RemoveGemnasiumService < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ disable_statement_timeout
+
+ execute("DELETE FROM services WHERE type='GemnasiumService';")
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index ed29d202f91..37d336b9928 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20180512061621) do
+ActiveRecord::Schema.define(version: 20180521171529) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -1449,9 +1449,9 @@ ActiveRecord::Schema.define(version: 20180512061621) do
add_index "personal_access_tokens", ["user_id"], name: "index_personal_access_tokens_on_user_id", using: :btree
create_table "project_authorizations", id: false, force: :cascade do |t|
- t.integer "user_id"
- t.integer "project_id"
- t.integer "access_level"
+ t.integer "user_id", null: false
+ t.integer "project_id", null: false
+ t.integer "access_level", null: false
end
add_index "project_authorizations", ["project_id"], name: "index_project_authorizations_on_project_id", using: :btree
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index 69600cad25c..411a0fae93f 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -1,7 +1,7 @@
# GitLab Prometheus metrics
>**Note:**
-Available since [Omnibus GitLab 9.3][29118]. Currently experimental. For
+Available since [Omnibus GitLab 9.3][29118]. For
installations from source you'll have to configure it yourself.
To enable the GitLab Prometheus metrics:
@@ -24,7 +24,7 @@ server, because the embedded server configuration is overwritten once every
## Metrics available
-In this experimental phase, only a few metrics are available:
+The following metrics are available:
| Metric | Type | Since | Description |
|:--------------------------------- |:--------- |:----- |:----------- |
diff --git a/doc/administration/monitoring/prometheus/index.md b/doc/administration/monitoring/prometheus/index.md
index 3d24812c66a..f47add48345 100644
--- a/doc/administration/monitoring/prometheus/index.md
+++ b/doc/administration/monitoring/prometheus/index.md
@@ -120,7 +120,7 @@ To disable the monitoring of Kubernetes:
## GitLab Prometheus metrics
-> Introduced as an experimental feature in GitLab 9.3.
+> Introduced in GitLab 9.3.
GitLab monitors its own internal service metrics, and makes them available at the `/-/metrics` endpoint. Unlike other exporters, this endpoint requires authentication as it is available on the same URL and port as user traffic.
diff --git a/doc/api/README.md b/doc/api/README.md
index e777fc63d2b..194907accc7 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -33,6 +33,7 @@ following locations:
- [Jobs](jobs.md)
- [Keys](keys.md)
- [Labels](labels.md)
+- [Markdown](markdown.md)
- [Merge Requests](merge_requests.md)
- [Project milestones](milestones.md)
- [Group milestones](group_milestones.md)
diff --git a/doc/api/groups.md b/doc/api/groups.md
index 923fd662a5b..96842ef330f 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -487,6 +487,9 @@ Parameters:
- `id` (required) - The ID or path of a user group
+This will queue a background job to delete all projects in the group. The
+response will be a 202 Accepted if the user has authorization.
+
## Search for group
Get all groups that match your string in their name or path.
diff --git a/doc/api/issues.md b/doc/api/issues.md
index 7479c1d2f93..d0063e0b8a2 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -38,8 +38,8 @@ GET /issues?my_reaction_emoji=star
| `state` | string | no | Return all issues or just those that are `opened` or `closed` |
| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels |
| `milestone` | string | no | The milestone title |
-| `scope` | string | no | Return issues for the given scope: `created-by-me`, `assigned-to-me` or `all`. Defaults to `created-by-me` _([Introduced][ce-13004] in GitLab 9.5)_ |
-| `author_id` | integer | no | Return issues created by the given user `id`. Combine with `scope=all` or `scope=assigned-to-me`. _([Introduced][ce-13004] in GitLab 9.5)_ |
+| `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`. Defaults to `created_by_me`<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ |
+| `author_id` | integer | no | Return issues created by the given user `id`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ |
| `assignee_id` | integer | no | Return issues assigned to the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ |
| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ |
| `iids[]` | Array[integer] | no | Return only the issues having the given `iid` |
@@ -152,7 +152,7 @@ GET /groups/:id/issues?my_reaction_emoji=star
| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels |
| `iids[]` | Array[integer] | no | Return only the issues having the given `iid` |
| `milestone` | string | no | The milestone title |
-| `scope` | string | no | Return issues for the given scope: `created-by-me`, `assigned-to-me` or `all` _([Introduced][ce-13004] in GitLab 9.5)_ |
+| `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ |
| `author_id` | integer | no | Return issues created by the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ |
| `assignee_id` | integer | no | Return issues assigned to the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ |
| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ |
@@ -266,7 +266,7 @@ GET /projects/:id/issues?my_reaction_emoji=star
| `state` | string | no | Return all issues or just those that are `opened` or `closed` |
| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels |
| `milestone` | string | no | The milestone title |
-| `scope` | string | no | Return issues for the given scope: `created-by-me`, `assigned-to-me` or `all` _([Introduced][ce-13004] in GitLab 9.5)_ |
+| `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ |
| `author_id` | integer | no | Return issues created by the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ |
| `assignee_id` | integer | no | Return issues assigned to the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ |
| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ |
@@ -1254,3 +1254,4 @@ Example response:
[ce-13004]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13004
[ce-14016]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14016
[ce-17042]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17042
+[ce-18935]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18935
diff --git a/doc/api/markdown.md b/doc/api/markdown.md
new file mode 100644
index 00000000000..f406838e887
--- /dev/null
+++ b/doc/api/markdown.md
@@ -0,0 +1,29 @@
+# Markdown API
+
+> [Introduced][ce-18926] in GitLab 11.0.
+
+Available only in APIv4.
+
+## Render an arbitrary Markdown document
+
+```
+POST /api/v4/markdown
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ------- | ------------- | ------------------------------------------ |
+| `text` | string | yes | The markdown text to render |
+| `gfm` | boolean | no (optional) | Render text using GitLab Flavored Markdown. Default is `false` |
+| `project` | string | no (optional) | Use `project` as a context when creating references using GitLab Flavored Markdown. [Authentication](README.html#authentication) is required if a project is not public. |
+
+```bash
+curl --header Content-Type:application/json --data '{"text":"Hello world! :tada:", "gfm":true, "project":"group_example/project_example"}' https://gitlab.example.com/api/v4/markdown
+```
+
+Response example:
+
+```json
+{ "html": "<p dir=\"auto\">Hello world! <gl-emoji title=\"party popper\" data-name=\"tada\" data-unicode-version=\"6.0\">🎉</gl-emoji></p>" }
+```
+
+[ce-18926]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18926
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index b9a4f661777..cbd51c9870c 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -28,7 +28,7 @@ GET /merge_requests?milestone=release
GET /merge_requests?labels=bug,reproduced
GET /merge_requests?author_id=5
GET /merge_requests?my_reaction_emoji=star
-GET /merge_requests?scope=assigned-to-me
+GET /merge_requests?scope=assigned_to_me
```
Parameters:
@@ -45,8 +45,8 @@ Parameters:
| `created_before` | datetime | no | Return merge requests created on or before the given time |
| `updated_after` | datetime | no | Return merge requests updated on or after the given time |
| `updated_before` | datetime | no | Return merge requests updated on or before the given time |
-| `scope` | string | no | Return merge requests for the given scope: `created-by-me`, `assigned-to-me` or `all`. Defaults to `created-by-me` |
-| `author_id` | integer | no | Returns merge requests created by the given user `id`. Combine with `scope=all` or `scope=assigned-to-me` |
+| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`. Defaults to `created_by_me`<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead. |
+| `author_id` | integer | no | Returns merge requests created by the given user `id`. Combine with `scope=all` or `scope=assigned_to_me` |
| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id` |
| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ |
| `source_branch` | string | no | Return merge requests with the given source branch |
@@ -164,7 +164,7 @@ Parameters:
| `created_before` | datetime | no | Return merge requests created on or before the given time |
| `updated_after` | datetime | no | Return merge requests updated on or after the given time |
| `updated_before` | datetime | no | Return merge requests updated on or before the given time |
-| `scope` | string | no | Return merge requests for the given scope: `created-by-me`, `assigned-to-me` or `all` _([Introduced][ce-13060] in GitLab 9.5)_ |
+| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13060] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ |
| `author_id` | integer | no | Returns merge requests created by the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ |
| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ |
| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ |
@@ -1460,3 +1460,4 @@ Example response:
[ce-13060]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13060
[ce-14016]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14016
[ce-15454]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15454
+[ce-18935]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18935
diff --git a/doc/api/projects.md b/doc/api/projects.md
index fe21dfe23f9..79cf5e1cc10 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -66,6 +66,7 @@ GET /projects
"ssh_url_to_repo": "git@example.com:diaspora/diaspora-client.git",
"http_url_to_repo": "http://example.com/diaspora/diaspora-client.git",
"web_url": "http://example.com/diaspora/diaspora-client",
+ "readme_url": "http://example.com/diaspora/diaspora-client/blob/master/README.md",
"tag_list": [
"example",
"disapora client"
@@ -135,6 +136,7 @@ GET /projects
"ssh_url_to_repo": "git@example.com:brightbox/puppet.git",
"http_url_to_repo": "http://example.com/brightbox/puppet.git",
"web_url": "http://example.com/brightbox/puppet",
+ "readme_url": "http://example.com/brightbox/puppet/blob/master/README.md",
"tag_list": [
"example",
"puppet"
@@ -252,6 +254,7 @@ GET /users/:user_id/projects
"ssh_url_to_repo": "git@example.com:diaspora/diaspora-client.git",
"http_url_to_repo": "http://example.com/diaspora/diaspora-client.git",
"web_url": "http://example.com/diaspora/diaspora-client",
+ "readme_url": "http://example.com/diaspora/diaspora-client/blob/master/README.md",
"tag_list": [
"example",
"disapora client"
@@ -321,6 +324,7 @@ GET /users/:user_id/projects
"ssh_url_to_repo": "git@example.com:brightbox/puppet.git",
"http_url_to_repo": "http://example.com/brightbox/puppet.git",
"web_url": "http://example.com/brightbox/puppet",
+ "readme_url": "http://example.com/brightbox/puppet/blob/master/README.md",
"tag_list": [
"example",
"puppet"
@@ -420,6 +424,7 @@ GET /projects/:id
"ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git",
"http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
"web_url": "http://example.com/diaspora/diaspora-project-site",
+ "readme_url": "http://example.com/diaspora/diaspora-project-site/blob/master/README.md",
"tag_list": [
"example",
"disapora project"
@@ -710,6 +715,7 @@ Example responses:
"ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git",
"http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
"web_url": "http://example.com/diaspora/diaspora-project-site",
+ "readme_url": "http://example.com/diaspora/diaspora-project-site/blob/master/README.md",
"tag_list": [
"example",
"disapora project"
@@ -788,6 +794,7 @@ Example response:
"ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git",
"http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
"web_url": "http://example.com/diaspora/diaspora-project-site",
+ "readme_url": "http://example.com/diaspora/diaspora-project-site/blob/master/README.md",
"tag_list": [
"example",
"disapora project"
@@ -865,6 +872,7 @@ Example response:
"ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git",
"http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
"web_url": "http://example.com/diaspora/diaspora-project-site",
+ "readme_url": "http://example.com/diaspora/diaspora-project-site/blob/master/README.md",
"tag_list": [
"example",
"disapora project"
@@ -966,6 +974,7 @@ Example response:
"ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git",
"http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
"web_url": "http://example.com/diaspora/diaspora-project-site",
+ "readme_url": "http://example.com/diaspora/diaspora-project-site/blob/master/README.md",
"tag_list": [
"example",
"disapora project"
@@ -1061,6 +1070,7 @@ Example response:
"ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git",
"http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
"web_url": "http://example.com/diaspora/diaspora-project-site",
+ "readme_url": "http://example.com/diaspora/diaspora-project-site/blob/master/README.md",
"tag_list": [
"example",
"disapora project"
@@ -1421,4 +1431,3 @@ GET /projects/:id/snapshot
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `wiki` | boolean | no | Whether to download the wiki, rather than project, repository |
-
diff --git a/doc/api/services.md b/doc/api/services.md
index ec632125325..f23303ef836 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -405,6 +405,13 @@ GET /projects/:id/services/flowdock
Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities.
+CAUTION: **Warning:**
+Gemnasium service integration has been deprecated in GitLab 11.0. Gemnasium has been
+[acquired by GitLab](https://about.gitlab.com/press/releases/2018-01-30-gemnasium-acquisition.html)
+in January 2018 and since May 15, 2018, the service provided by Gemnasium is no longer available.
+You can [migrate from Gemnasium to GitLab](https://docs.gitlab.com/ee/user/project/import/gemnasium.html)
+to keep monitoring your dependencies.
+
### Create/Edit Gemnasium service
Set Gemnasium service for a project.
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index 517e25f00f7..3a491f0073c 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -252,6 +252,7 @@ including predefined, secure variables and `.gitlab-ci.yml`
[`variables`](yaml/README.md#variables). You however cannot use variables
defined under `script` or on the Runner's side. There are other variables that
are unsupported in environment name context:
+- `CI_PIPELINE_ID`
- `CI_JOB_ID`
- `CI_JOB_TOKEN`
- `CI_BUILD_ID`
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 42367bf13f7..aedf7958c8a 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -530,6 +530,16 @@ Below you can find supported syntax reference:
`$STAGING` value needs to a string, with length higher than zero.
Variable that contains only whitespace characters is not an empty variable.
+1. Pattern matching _(added in 11.0)_
+
+ > Example: `$VARIABLE =~ /^content.*/`
+
+ It is possible perform pattern matching against a variable and regular
+ expression. Expression like this evaluates to truth if matches are found.
+
+ Pattern matching is case-sensitive by default. Use `i` flag modifier, like
+ `/pattern/i` to make a pattern case-insensitive.
+
### Unsupported predefined variables
Because GitLab evaluates variables before creating jobs, we do not support a
@@ -543,6 +553,7 @@ We do not support variables containing tokens because of security reasons.
You can find a full list of unsupported variables below:
+- `CI_PIPELINE_ID`
- `CI_JOB_ID`
- `CI_JOB_TOKEN`
- `CI_BUILD_ID`
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 2a17a51d7f8..3e77a6f58b7 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -344,10 +344,11 @@ job:
kubernetes: active
```
-Example of using variables expressions:
+Examples of using variables expressions:
```yaml
deploy:
+ script: cap staging deploy
only:
refs:
- branches
@@ -356,6 +357,16 @@ deploy:
- $STAGING
```
+Another use case is exluding jobs depending on a commit message _(added in 11.0)_:
+
+```yaml
+end-to-end:
+ script: rake test:end-to-end
+ except:
+ variables:
+ - $CI_COMMIT_MESSAGE =~ /skip-end-to-end-tests/
+```
+
Learn more about variables expressions on [a separate page][variables-expressions].
## `tags`
diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md
index 4873090a2d4..057a4094aed 100644
--- a/doc/development/ee_features.md
+++ b/doc/development/ee_features.md
@@ -53,6 +53,9 @@ stub_licensed_features(variable_environment_scope: true)
EE-specific comments should not be backported to CE.
+**Note:** This is only meant as a workaround, we should follow up and
+resolve this soon.
+
### Detection of EE-only files
For each commit (except on `master`), the `ee-files-location-check` CI job tries
@@ -105,11 +108,14 @@ is applied not only to models. Here's a list of other examples:
- `ee/app/services/foo/create_service.rb`
- `ee/app/validators/foo_attr_validator.rb`
- `ee/app/workers/foo_worker.rb`
+- `ee/app/views/foo.html.haml`
+- `ee/app/views/foo/_bar.html.haml`
This works because for every path that are present in CE's eager-load/auto-load
paths, we add the same `ee/`-prepended path in [`config/application.rb`].
+This also applies to views.
-[`config/application.rb`]: https://gitlab.com/gitlab-org/gitlab-ee/blob/d278b76d6600a0e27d8019a0be27971ba23ab640/config/application.rb#L41-51
+[`config/application.rb`]: https://gitlab.com/gitlab-org/gitlab-ee/blob/925d3d4ebc7a2c72964ce97623ae41b8af12538d/config/application.rb#L42-52
### EE features based on CE features
@@ -359,9 +365,37 @@ Blocks of code that are EE-specific should be moved to partials. This
avoids conflicts with big chunks of HAML code that that are not fun to
resolve when you add the indentation to the equation.
-EE-specific views should be placed in `ee/app/views/ee/`, using extra
+EE-specific views should be placed in `ee/app/views/`, using extra
sub-directories if appropriate.
+Instead of using regular `render`, we should use `render_if_exists`, which
+will not render anything if it cannot find the specific partial. We use this
+so that we could put `render_if_exists` in CE, keeping code the same between
+CE and EE.
+
+Also, it should search for the EE partial first, and then CE partial, and
+then if nothing found, render nothing.
+
+This has two uses:
+
+- CE renders nothing, and EE renders its EE partial.
+- CE renders its CE partial, and EE renders its EE partial, while the view
+ file stays the same.
+
+The advantages of this:
+
+- Minimal code difference between CE and EE.
+- Very clear hints about where we're extending EE views while reading CE codes.
+- Whenever we want to show something different in CE, we could just add CE
+ partials. Same applies the other way around. If we just use
+ `render_if_exists`, it would be very easy to change the content in EE.
+
+The disadvantage of this:
+
+- Slightly more work while developing EE features, because now we need to
+ port `render_if_exists` to CE.
+- If we have typos in the partial name, it would be silently ignored.
+
### Code in `lib/`
Place EE-specific logic in the top-level `EE` module namespace. Namespace the
diff --git a/doc/development/fe_guide/vuex.md b/doc/development/fe_guide/vuex.md
index 8997a5889dc..858b03c60bf 100644
--- a/doc/development/fe_guide/vuex.md
+++ b/doc/development/fe_guide/vuex.md
@@ -37,12 +37,13 @@ import state from './state';
Vue.use(Vuex);
-export default new Vuex.Store({
+export const createStore = () => new Vuex.Store({
actions,
getters,
mutations,
state,
});
+export default createStore();
```
### `state.js`
@@ -320,10 +321,11 @@ In order to write unit tests for those components, we need to include the store
```javascript
//component_spec.js
import Vue from 'vue';
-import store from './store';
+import { createStore } from './store';
import component from './component.vue'
describe('component', () => {
+ let store;
let vm;
let Component;
@@ -340,6 +342,8 @@ describe('component', () => {
name: 'Foo',
age: '30',
};
+
+ store = createStore();
// populate the store
store.dispatch('addUser', user);
diff --git a/doc/downgrade_ee_to_ce/README.md b/doc/downgrade_ee_to_ce/README.md
index f656057e3da..236408762e3 100644
--- a/doc/downgrade_ee_to_ce/README.md
+++ b/doc/downgrade_ee_to_ce/README.md
@@ -15,9 +15,9 @@ Kerberos and Atlassian Crowd are only available on the Enterprise Edition, so
you should disable these mechanisms before downgrading and you should provide
alternative authentication methods to your users.
-### Remove Jenkins CI Service entries from the database
+### Remove Service Integration entries from the database
-The `JenkinsService` class is only available on the Enterprise Edition codebase,
+The `JenkinsService` and `GithubService` classes are only available in the Enterprise Edition codebase,
so if you downgrade to the Community Edition, you'll come across the following
error:
@@ -30,20 +30,31 @@ column if you didn't intend it to be used for storing the inheritance class or o
use another column for that information.)
```
+or
+
+```
+Completed 500 Internal Server Error in 497ms (ActiveRecord: 32.2ms)
+
+ActionView::Template::Error (The single-table inheritance mechanism failed to locate the subclass: 'GithubService'. This
+error is raised because the column 'type' is reserved for storing the class in case of inheritance. Please rename this
+column if you didn't intend it to be used for storing the inheritance class or overwrite Service.inheritance_column to
+use another column for that information.)
+```
+
All services are created automatically for every project you have, so in order
to avoid getting this error, you need to remove all instances of the
-`JenkinsService` from your database:
+`JenkinsService` and `GithubService` from your database:
**Omnibus Installation**
```
-$ sudo gitlab-rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService']).delete_all"
+$ sudo gitlab-rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService', 'GithubService']).delete_all"
```
**Source Installation**
```
-$ bundle exec rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService']).delete_all" production
+$ bundle exec rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService', 'GithubService']).delete_all" production
```
### Secret variables environment scopes
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 785cc32d590..77139c50d07 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -64,6 +64,13 @@ If you are running GitLab within a Docker container, you can run the backup from
docker exec -t <container name> gitlab-rake gitlab:backup:create
```
+If you are using the gitlab-omnibus helm chart on a Kubernetes cluster, you can
+run the backup task on the gitlab application pod using kubectl
+
+```
+kubectl exec -it <gitlab-gitlab pod> gitlab-rake gitlab:backup:create
+```
+
Example output:
```
@@ -601,6 +608,34 @@ If there is a GitLab version mismatch between your backup tar file and the insta
version of GitLab, the restore command will abort with an error. Install the
[correct GitLab version](https://packages.gitlab.com/gitlab/) and try again.
+### Restore for Docker image and gitlab-omnibus helm chart
+
+For GitLab installations using docker image or the gitlab-omnibus helm chart on
+a Kubernetes cluster, restore task expects the restore directories to be empty.
+However, with docker and Kubernetes volume mounts, some system level directories
+may be created at the volume roots, like `lost+found` directory found in Linux
+operating systems. These directories are usually owned by `root`, which can
+cause access permission errors since the restore rake task runs as `git` user.
+So, to restore a GitLab installation, users have to confirm the restore target
+directories are empty.
+
+For both these installation types, the backup tarball has to be available in the
+backup location (default location is `/var/opt/gitlab/backups`).
+
+For docker installations, the restore task can be run from host using the
+command
+
+```
+docker exec -it <name of container> gitlab-rake gitlab:backup:restore
+```
+
+Similarly, for gitlab-omnibus helm chart, the restore task can be run on the
+gitlab application pod using kubectl
+
+```
+kubectl exec -it <gitlab-gitlab pod> gitlab-rake gitlab:backup:restore
+```
+
## Alternative backup strategies
If your GitLab server contains a lot of Git repository data you may find the GitLab backup script to be too slow.
diff --git a/doc/topics/autodevops/img/rollout_enabled.png b/doc/topics/autodevops/img/rollout_enabled.png
new file mode 100644
index 00000000000..d6e7d790cf2
--- /dev/null
+++ b/doc/topics/autodevops/img/rollout_enabled.png
Binary files differ
diff --git a/doc/topics/autodevops/img/rollout_staging_disabled.png b/doc/topics/autodevops/img/rollout_staging_disabled.png
new file mode 100644
index 00000000000..71e36b440f0
--- /dev/null
+++ b/doc/topics/autodevops/img/rollout_staging_disabled.png
Binary files differ
diff --git a/doc/topics/autodevops/img/rollout_staging_enabled.png b/doc/topics/autodevops/img/rollout_staging_enabled.png
new file mode 100644
index 00000000000..d0d1d356627
--- /dev/null
+++ b/doc/topics/autodevops/img/rollout_staging_enabled.png
Binary files differ
diff --git a/doc/topics/autodevops/img/staging_enabled.png b/doc/topics/autodevops/img/staging_enabled.png
new file mode 100644
index 00000000000..0ef1a67d641
--- /dev/null
+++ b/doc/topics/autodevops/img/staging_enabled.png
Binary files differ
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 5254e6e3d9a..f80c82acfa3 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -1,7 +1,5 @@
# Auto DevOps
-DANGER: Auto DevOps is currently in **Beta** and _not recommended for production use_.
-
> [Introduced][ce-37115] in GitLab 10.0.
Auto DevOps automatically detects, builds, tests, deploys, and monitors your
@@ -496,6 +494,16 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac
| `POSTGRES_DB` | The PostgreSQL database name; defaults to the value of [`$CI_ENVIRONMENT_SLUG`](../../ci/variables/README.md#predefined-variables-environment-variables). Set it to use a custom database name. |
| `BUILDPACK_URL` | The buildpack's full URL. It can point to either Git repositories or a tarball URL. For Git repositories, it is possible to point to a specific `ref`, for example `https://github.com/heroku/heroku-buildpack-ruby.git#v142` |
| `STAGING_ENABLED` | From GitLab 10.8, this variable can be used to define a [deploy policy for staging and production environments](#deploy-policy-for-staging-and-production-environments). |
+| `CANARY_ENABLED` | From GitLab 11.0, this variable can be used to define a [deploy policy for canary environments](#deploy-policy-for-canary-environments). |
+| `INCREMENTAL_ROLLOUT_ENABLED`| From GitLab 10.8, this variable can be used to enable an [incremental rollout](#incremental-rollout-to-production) of your application for the production environment. |
+| `TEST_DISABLED` | From GitLab 11.0, this variable can be used to disable the `test` job. If the variable is present, the job will not be created. |
+| `CODEQUALITY_DISABLED` | From GitLab 11.0, this variable can be used to disable the `codequality` job. If the variable is present, the job will not be created. |
+| `SAST_DISABLED` | From GitLab 11.0, this variable can be used to disable the `sast` job. If the variable is present, the job will not be created. |
+| `DEPENDENCY_SCANNING_DISABLED` | From GitLab 11.0, this variable can be used to disable the `dependency_scanning` job. If the variable is present, the job will not be created. |
+| `CONTAINER_SCANNING_DISABLED` | From GitLab 11.0, this variable can be used to disable the `sast:container` job. If the variable is present, the job will not be created. |
+| `REVIEW_DISABLED` | From GitLab 11.0, this variable can be used to disable the `review` and the manual `review:stop` job. If the variable is present, these jobs will not be created. |
+| `DAST_DISABLED` | From GitLab 11.0, this variable can be used to disable the `dast` job. If the variable is present, the job will not be created. |
+| `PERFORMANCE_DISABLED` | From GitLab 11.0, this variable can be used to disable the `performance` job. If the variable is present, the job will not be created. |
TIP: **Tip:**
Set up the replica variables using a
@@ -578,6 +586,72 @@ If `STAGING_ENABLED` is defined in your project (e.g., set `STAGING_ENABLED` to
to a `staging` environment, and a `production_manual` job will be created for
you when you're ready to manually deploy to production.
+#### Deploy policy for canary environments **[PREMIUM]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ci-yml/merge_requests/171)
+in GitLab 11.0.
+
+A [canary environment](https://docs.gitlab.com/ee/user/project/canary_deployments.html) can be used
+before any changes are deployed to production.
+
+If `CANARY_ENABLED` is defined in your project (e.g., set `CANARY_ENABLED` to
+`1` as a secret variable) then two manual jobs will be created:
+
+- `canary` which will deploy the application to the canary environment
+- `production_manual` which is to be used by you when you're ready to manually
+ deploy to production.
+
+#### Incremental rollout to production **[PREMIUM]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5415) in GitLab 10.8.
+
+When you have a new version of your app to deploy in production, you may want
+to use an incremental rollout to replace just a few pods with the latest code.
+This will allow you to first check how the app is behaving, and later manually
+increasing the rollout up to 100%.
+
+If `INCREMENTAL_ROLLOUT_ENABLED` is defined in your project (e.g., set
+`INCREMENTAL_ROLLOUT_ENABLED` to `1` as a secret variable), then instead of the
+standard `production` job, 4 different
+[manual jobs](../../ci/pipelines.md#manual-actions-from-the-pipeline-graph)
+will be created:
+
+1. `rollout 10%`
+1. `rollout 25%`
+1. `rollout 50%`
+1. `rollout 100%`
+
+The percentage is based on the `REPLICAS` variable and defines the number of
+pods you want to have for your deployment. If you say `10`, and then you run
+the `10%` rollout job, there will be `1` new pod + `9` old ones.
+
+To start a job, click on the play icon next to the job's name. You are not
+required to go from `10%` to `100%`, you can jump to whatever job you want.
+You can also scale down by running a lower percentage job, just before hitting
+`100%`. Once you get to `100%`, you cannot scale down, and you'd have to roll
+back by redeploying the old version using the
+[rollback button](../../ci/environments.md#rolling-back-changes) in the
+environment page.
+
+Below, you can see how the pipeline will look if the rollout or staging
+variables are defined.
+
+- **Without `INCREMENTAL_ROLLOUT_ENABLED` and without `STAGING_ENABLED`**
+
+ ![Staging and rollout disabled](img/rollout_staging_disabled.png)
+
+- **Without `INCREMENTAL_ROLLOUT_ENABLED` and with `STAGING_ENABLED`**
+
+ ![Staging enabled](img/staging_enabled.png)
+
+- **With `INCREMENTAL_ROLLOUT_ENABLED` and without `STAGING_ENABLED`**
+
+ ![Rollout enabled](img/rollout_enabled.png)
+
+- **With `INCREMENTAL_ROLLOUT_ENABLED` and with `STAGING_ENABLED`**
+
+ ![Rollout and staging enabled](img/rollout_staging_enabled.png)
+
## Currently supported languages
NOTE: **Note:**
diff --git a/doc/topics/autodevops/quick_start_guide.md b/doc/topics/autodevops/quick_start_guide.md
index 0b16af2953b..8989a4c8956 100644
--- a/doc/topics/autodevops/quick_start_guide.md
+++ b/doc/topics/autodevops/quick_start_guide.md
@@ -1,7 +1,5 @@
# Auto DevOps: quick start guide
-DANGER: Auto DevOps is currently in **Beta** and _not recommended for production use_.
-
> [Introduced][ce-37115] in GitLab 10.0.
This is a step-by-step guide to deploying a project hosted on GitLab.com to
diff --git a/doc/user/admin_area/settings/img/enforce_terms.png b/doc/user/admin_area/settings/img/enforce_terms.png
index e5f0a2683b5..e5f0a2683b5 100755..100644
--- a/doc/user/admin_area/settings/img/enforce_terms.png
+++ b/doc/user/admin_area/settings/img/enforce_terms.png
Binary files differ
diff --git a/doc/user/admin_area/settings/img/respond_to_terms.png b/doc/user/admin_area/settings/img/respond_to_terms.png
index d0d086c3498..d0d086c3498 100755..100644
--- a/doc/user/admin_area/settings/img/respond_to_terms.png
+++ b/doc/user/admin_area/settings/img/respond_to_terms.png
Binary files differ
diff --git a/doc/user/project/deploy_tokens/index.md b/doc/user/project/deploy_tokens/index.md
index 7a8b3c75690..c09d5aeba8e 100644
--- a/doc/user/project/deploy_tokens/index.md
+++ b/doc/user/project/deploy_tokens/index.md
@@ -76,7 +76,7 @@ pull images from your Container Registry.
> [Introduced][ce-18414] in GitLab 10.8.
There's a special case when it comes to Deploy Tokens, if a user creates one
-named `gitlab-deploy-token`, the name and token of the Deploy Token will be
+named `gitlab-deploy-token`, the username and token of the Deploy Token will be
automatically exposed to the CI/CD jobs as environment variables: `CI_DEPLOY_USER` and
`CI_DEPLOY_PASSWORD`, respectively.
diff --git a/doc/user/project/integrations/jira.md b/doc/user/project/integrations/jira.md
index 5933bcedc8b..4d5b2c97291 100644
--- a/doc/user/project/integrations/jira.md
+++ b/doc/user/project/integrations/jira.md
@@ -111,7 +111,7 @@ in the table below.
| ----- | ----------- |
| `Web URL` | The base URL to the JIRA instance web interface which is being linked to this GitLab project. E.g., `https://jira.example.com`. |
| `JIRA API URL` | The base URL to the JIRA instance API. Web URL value will be used if not set. E.g., `https://jira-api.example.com`. |
-| `Username` | The user name created in [configuring JIRA step](#configuring-jira). |
+| `Username` | The user name created in [configuring JIRA step](#configuring-jira). Using the email address will cause `401 unauthorized`. |
| `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). |
| `Transition ID` | This is the ID of a transition that moves issues to the desired state. **Closing JIRA issues via commits or Merge Requests won't work if you don't set the ID correctly.** |
diff --git a/doc/user/project/integrations/project_services.md b/doc/user/project/integrations/project_services.md
index 9496d6f2ce0..074eeb729e3 100644
--- a/doc/user/project/integrations/project_services.md
+++ b/doc/user/project/integrations/project_services.md
@@ -34,7 +34,7 @@ Click on the service links to see further configuration instructions and details
| [Emails on push](emails_on_push.md) | Email the commits and diff of each push to a list of recipients |
| External Wiki | Replaces the link to the internal wiki with a link to an external wiki |
| Flowdock | Flowdock is a collaboration web app for technical teams |
-| Gemnasium | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities |
+| Gemnasium _(Has been deprecated in GitLab 11.0)_ | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities |
| [HipChat](hipchat.md) | Private group chat and IM |
| [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway |
| [JIRA](jira.md) | JIRA issue tracker |
diff --git a/doc/user/project/merge_requests/resolve_conflicts.md b/doc/user/project/merge_requests/resolve_conflicts.md
index 68c49054e47..ecbc8534eea 100644
--- a/doc/user/project/merge_requests/resolve_conflicts.md
+++ b/doc/user/project/merge_requests/resolve_conflicts.md
@@ -29,7 +29,7 @@ The merge conflict resolution editor allows for more complex merge conflicts,
which require the user to manually modify a file in order to resolve a conflict,
to be solved right form the GitLab interface. Use the **Edit inline** button
to open the editor. Once you're sure about your changes, hit the
-**Commit conflict resolution** button.
+**Commit to source branch** button.
![Merge conflict editor](img/merge_conflict_editor.png)
diff --git a/doc/workflow/shortcuts.md b/doc/workflow/shortcuts.md
index 2e1bd6bfe5c..b2f1cbec204 100644
--- a/doc/workflow/shortcuts.md
+++ b/doc/workflow/shortcuts.md
@@ -11,7 +11,7 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?'
| <kbd>f</kbd> | Focus filter |
| <kbd>p</kbd> + <kbd>b</kbd> | Show/hide the Performance Bar |
| <kbd>?</kbd> | Show/hide this dialog |
-| <kbd>⌘</kbd> + <kbd>shift</kbd> + <kbd>p</kbd> | Toggle markdown preview |
+| <kbd>Cmd</kbd>/<kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>p</kbd> | Toggle markdown preview |
| <kbd>↑</kbd> | Edit last comment (when focused on an empty textarea) |
## Project Files Browsing
@@ -46,15 +46,19 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?'
| Keyboard Shortcut | Description |
| ----------------- | ----------- |
| <kbd>g</kbd> + <kbd>p</kbd> | Go to the project's home page |
-| <kbd>g</kbd> + <kbd>e</kbd> | Go to the project's activity feed |
+| <kbd>g</kbd> + <kbd>v</kbd> | Go to the project's activity feed |
| <kbd>g</kbd> + <kbd>f</kbd> | Go to files |
| <kbd>g</kbd> + <kbd>c</kbd> | Go to commits |
-| <kbd>g</kbd> + <kbd>b</kbd> | Go to jobs |
+| <kbd>g</kbd> + <kbd>j</kbd> | Go to jobs |
| <kbd>g</kbd> + <kbd>n</kbd> | Go to network graph |
-| <kbd>g</kbd> + <kbd>g</kbd> | Go to repository charts |
+| <kbd>g</kbd> + <kbd>d</kbd> | Go to repository charts |
| <kbd>g</kbd> + <kbd>i</kbd> | Go to issues |
+| <kbd>g</kbd> + <kbd>b</kbd> | Go to issue boards |
| <kbd>g</kbd> + <kbd>m</kbd> | Go to merge requests |
+| <kbd>g</kbd> + <kbd>e</kbd> | Go to environments |
+| <kbd>g</kbd> + <kbd>k</kbd> | Go to kubernetes |
| <kbd>g</kbd> + <kbd>s</kbd> | Go to snippets |
+| <kbd>g</kbd> + <kbd>w</kbd> | Go to wiki |
| <kbd>t</kbd> | Go to finding file |
| <kbd>i</kbd> | New issue |
@@ -66,8 +70,8 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?'
| <kbd>→</kbd> or <kbd>l</kbd> | Scroll right |
| <kbd>↑</kbd> or <kbd>k</kbd> | Scroll up |
| <kbd>↓</kbd> or <kbd>j</kbd> | Scroll down |
-| <kbd>shift</kbd> + <kbd>↑</kbd> or <kbd>shift</kbd> + <kbd>k</kbd> | Scroll to top |
-| <kbd>shift</kbd> + <kbd>↓</kbd> or <kbd>shift</kbd> + <kbd>j</kbd> | Scroll to bottom |
+| <kbd>Shift</kbd> + <kbd>↑</kbd> or <kbd>Shift</kbd> + <kbd>k</kbd> | Scroll to top |
+| <kbd>Shift</kbd> + <kbd>↓</kbd> or <kbd>Shift</kbd> + <kbd>j</kbd> | Scroll to bottom |
## Issues and Merge Requests
@@ -84,3 +88,9 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?'
| Keyboard Shortcut | Description |
| ----------------- | ----------- |
| <kbd>e</kbd> | Edit wiki page|
+
+## Web IDE
+
+| Keyboard Shortcut | Description |
+| ----------------- | ----------- |
+| <kbd>⌘</kbd> + <kbd>p</kbd> | Go to file |
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 5139e869c71..de20b2b8e67 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -8,14 +8,15 @@ module API
PROJECT_ENDPOINT_REQUIREMENTS = { id: NO_SLASH_URL_PART_REGEX }.freeze
COMMIT_ENDPOINT_REQUIREMENTS = PROJECT_ENDPOINT_REQUIREMENTS.merge(sha: NO_SLASH_URL_PART_REGEX).freeze
- use GrapeLogging::Middleware::RequestLogger,
- logger: Logger.new(LOG_FILENAME),
- formatter: Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp.new,
- include: [
- GrapeLogging::Loggers::FilterParameters.new,
- GrapeLogging::Loggers::ClientEnv.new,
- Gitlab::GrapeLogging::Loggers::UserLogger.new
- ]
+ insert_before Grape::Middleware::Error,
+ GrapeLogging::Middleware::RequestLogger,
+ logger: Logger.new(LOG_FILENAME),
+ formatter: Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp.new,
+ include: [
+ GrapeLogging::Loggers::FilterParameters.new,
+ GrapeLogging::Loggers::ClientEnv.new,
+ Gitlab::GrapeLogging::Loggers::UserLogger.new
+ ]
allow_access_with_scope :api
prefix :api
@@ -139,6 +140,7 @@ module API
mount ::API::Keys
mount ::API::Labels
mount ::API::Lint
+ mount ::API::Markdown
mount ::API::Members
mount ::API::MergeRequestDiffs
mount ::API::MergeRequests
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 25d78fc761d..174c5af91d5 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -125,7 +125,7 @@ module API
# (fixed in https://github.com/rails/rails/pull/25976).
project.tags.map(&:name).sort
end
- expose :ssh_url_to_repo, :http_url_to_repo, :web_url
+ expose :ssh_url_to_repo, :http_url_to_repo, :web_url, :readme_url
expose :avatar_url do |project, options|
project.avatar_url(only_path: false)
end
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 0d125cd7831..03b6b30a0d8 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -167,8 +167,10 @@ module API
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/46285')
destroy_conditionally!(group) do |group|
- ::Groups::DestroyService.new(group, current_user).execute
+ ::Groups::DestroyService.new(group, current_user).async_execute
end
+
+ accepted!
end
desc 'Get a list of projects in this group.' do
diff --git a/lib/api/helpers/pagination.rb b/lib/api/helpers/pagination.rb
index 09805049169..3308212216e 100644
--- a/lib/api/helpers/pagination.rb
+++ b/lib/api/helpers/pagination.rb
@@ -2,67 +2,240 @@ module API
module Helpers
module Pagination
def paginate(relation)
- relation = add_default_order(relation)
+ strategy = if params[:pagination] == 'keyset' && Feature.enabled?('api_keyset_pagination')
+ KeysetPaginationStrategy
+ else
+ DefaultPaginationStrategy
+ end
- relation.page(params[:page]).per(params[:per_page]).tap do |data|
- add_pagination_headers(data)
- end
+ strategy.new(self).paginate(relation)
end
- private
+ class KeysetPaginationInfo
+ attr_reader :relation, :request_context
- def add_pagination_headers(paginated_data)
- header 'X-Per-Page', paginated_data.limit_value.to_s
- header 'X-Page', paginated_data.current_page.to_s
- header 'X-Next-Page', paginated_data.next_page.to_s
- header 'X-Prev-Page', paginated_data.prev_page.to_s
- header 'Link', pagination_links(paginated_data)
+ def initialize(relation, request_context)
+ # This is because it's rather complex to support multiple values with possibly different sort directions
+ # (and we don't need this in the API)
+ if relation.order_values.size > 1
+ raise "Pagination only supports ordering by a single column." \
+ "The following columns were given: #{relation.order_values.map { |v| v.expr.name }}"
+ end
- return if data_without_counts?(paginated_data)
+ @relation = relation
+ @request_context = request_context
+ end
- header 'X-Total', paginated_data.total_count.to_s
- header 'X-Total-Pages', total_pages(paginated_data).to_s
- end
+ def fields
+ keys.zip(values).reject { |_, v| v.nil? }.to_h
+ end
- def pagination_links(paginated_data)
- request_url = request.url.split('?').first
- request_params = params.clone
- request_params[:per_page] = paginated_data.limit_value
+ def column_for_order_by(relation)
+ relation.order_values.first&.expr&.name
+ end
- links = []
+ # Sort direction (`:asc` or `:desc`)
+ def sort
+ @sort ||= if order_by_primary_key?
+ # Default order is by id DESC
+ :desc
+ else
+ # API defaults to DESC order if param `sort` not present
+ request_context.params[:sort]&.to_sym || :desc
+ end
+ end
- request_params[:page] = paginated_data.prev_page
- links << %(<#{request_url}?#{request_params.to_query}>; rel="prev") if request_params[:page]
+ # Do we only sort by primary key?
+ def order_by_primary_key?
+ keys.size == 1 && keys.first == primary_key
+ end
- request_params[:page] = paginated_data.next_page
- links << %(<#{request_url}?#{request_params.to_query}>; rel="next") if request_params[:page]
+ def primary_key
+ relation.model.primary_key.to_sym
+ end
- request_params[:page] = 1
- links << %(<#{request_url}?#{request_params.to_query}>; rel="first")
+ def sort_ascending?
+ sort == :asc
+ end
- unless data_without_counts?(paginated_data)
- request_params[:page] = total_pages(paginated_data)
- links << %(<#{request_url}?#{request_params.to_query}>; rel="last")
+ # Build hash of request parameters for a given record (relevant to pagination)
+ def params_for(record)
+ return {} unless record
+
+ keys.each_with_object({}) do |key, h|
+ h["ks_prev_#{key}".to_sym] = record.attributes[key.to_s]
+ end
end
- links.join(', ')
- end
+ private
+
+ # All values present in request parameters that correspond to #keys.
+ def values
+ @values ||= keys.map do |key|
+ request_context.params["ks_prev_#{key}".to_sym]
+ end
+ end
- def total_pages(paginated_data)
- # Ensure there is in total at least 1 page
- [paginated_data.total_pages, 1].max
+ # All keys relevant to pagination.
+ # This always includes the primary key. Optionally, the `order_by` key is prepended.
+ def keys
+ @keys ||= [column_for_order_by(relation), primary_key].compact.uniq
+ end
end
- def add_default_order(relation)
- if relation.is_a?(ActiveRecord::Relation) && relation.order_values.empty?
- relation = relation.order(:id)
+ class KeysetPaginationStrategy
+ attr_reader :request_context
+ delegate :params, :header, :request, to: :request_context
+
+ def initialize(request_context)
+ @request_context = request_context
+ end
+
+ def paginate(relation)
+ pagination = KeysetPaginationInfo.new(relation, request_context)
+
+ paged_relation = relation.limit(per_page)
+
+ if conds = conditions(pagination)
+ paged_relation = paged_relation.where(*conds)
+ end
+
+ # In all cases: sort by primary key (possibly in addition to another sort column)
+ paged_relation = paged_relation.order(pagination.primary_key => pagination.sort)
+
+ add_default_pagination_headers
+
+ if last_record = paged_relation.last
+ next_page_params = pagination.params_for(last_record)
+ add_navigation_links(next_page_params)
+ end
+
+ paged_relation
+ end
+
+ private
+
+ def conditions(pagination)
+ fields = pagination.fields
+
+ return nil if fields.empty?
+
+ placeholder = fields.map { '?' }
+
+ comp = if pagination.sort_ascending?
+ '>'
+ else
+ '<'
+ end
+
+ [
+ # Row value comparison:
+ # (A, B) < (a, b) <=> (A < a) OR (A = a AND B < b)
+ # <=> A <= a AND ((A < a) OR (A = a AND B < b))
+ "(#{fields.keys.join(',')}) #{comp} (#{placeholder.join(',')})",
+ *fields.values
+ ]
+ end
+
+ def per_page
+ params[:per_page]
+ end
+
+ def add_default_pagination_headers
+ header 'X-Per-Page', per_page.to_s
+ end
+
+ def add_navigation_links(next_page_params)
+ header 'X-Next-Page', page_href(next_page_params)
+ header 'Link', link_for('next', next_page_params)
end
- relation
+ def page_href(next_page_params)
+ request_url = request.url.split('?').first
+ request_params = params.dup
+ request_params[:per_page] = per_page
+
+ request_params.merge!(next_page_params) if next_page_params
+
+ "#{request_url}?#{request_params.to_query}"
+ end
+
+ def link_for(rel, next_page_params)
+ %(<#{page_href(next_page_params)}>; rel="#{rel}")
+ end
end
- def data_without_counts?(paginated_data)
- paginated_data.is_a?(Kaminari::PaginatableWithoutCount)
+ class DefaultPaginationStrategy
+ attr_reader :request_context
+ delegate :params, :header, :request, to: :request_context
+
+ def initialize(request_context)
+ @request_context = request_context
+ end
+
+ def paginate(relation)
+ relation = add_default_order(relation)
+
+ relation.page(params[:page]).per(params[:per_page]).tap do |data|
+ add_pagination_headers(data)
+ end
+ end
+
+ private
+
+ def add_default_order(relation)
+ if relation.is_a?(ActiveRecord::Relation) && relation.order_values.empty?
+ relation = relation.order(:id)
+ end
+
+ relation
+ end
+
+ def add_pagination_headers(paginated_data)
+ header 'X-Per-Page', paginated_data.limit_value.to_s
+ header 'X-Page', paginated_data.current_page.to_s
+ header 'X-Next-Page', paginated_data.next_page.to_s
+ header 'X-Prev-Page', paginated_data.prev_page.to_s
+ header 'Link', pagination_links(paginated_data)
+
+ return if data_without_counts?(paginated_data)
+
+ header 'X-Total', paginated_data.total_count.to_s
+ header 'X-Total-Pages', total_pages(paginated_data).to_s
+ end
+
+ def pagination_links(paginated_data)
+ request_url = request.url.split('?').first
+ request_params = params.clone
+ request_params[:per_page] = paginated_data.limit_value
+
+ links = []
+
+ request_params[:page] = paginated_data.prev_page
+ links << %(<#{request_url}?#{request_params.to_query}>; rel="prev") if request_params[:page]
+
+ request_params[:page] = paginated_data.next_page
+ links << %(<#{request_url}?#{request_params.to_query}>; rel="next") if request_params[:page]
+
+ request_params[:page] = 1
+ links << %(<#{request_url}?#{request_params.to_query}>; rel="first")
+
+ unless data_without_counts?(paginated_data)
+ request_params[:page] = total_pages(paginated_data)
+ links << %(<#{request_url}?#{request_params.to_query}>; rel="last")
+ end
+
+ links.join(', ')
+ end
+
+ def total_pages(paginated_data)
+ # Ensure there is in total at least 1 page
+ [paginated_data.total_pages, 1].max
+ end
+
+ def data_without_counts?(paginated_data)
+ paginated_data.is_a?(Kaminari::PaginatableWithoutCount)
+ end
end
end
end
diff --git a/lib/api/helpers/related_resources_helpers.rb b/lib/api/helpers/related_resources_helpers.rb
index 7f4d6e58b34..a2cbed30229 100644
--- a/lib/api/helpers/related_resources_helpers.rb
+++ b/lib/api/helpers/related_resources_helpers.rb
@@ -13,9 +13,14 @@ module API
def expose_url(path)
url_options = Gitlab::Application.routes.default_url_options
- protocol, host, port = url_options.slice(:protocol, :host, :port).values
+ protocol, host, port, script_name = url_options.values_at(:protocol, :host, :port, :script_name)
- URI::Generic.build(scheme: protocol, host: host, port: port, path: path).to_s
+ # Using a blank component at the beginning of the join we ensure
+ # that the resulted path will start with '/'. If the resulted path
+ # does not start with '/', URI::Generic#build will fail
+ path_with_script_name = File.join('', [script_name, path].select(&:present?))
+
+ URI::Generic.build(scheme: protocol, host: host, port: port, path: path_with_script_name).to_s
end
private
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 2f50f94c897..6d75e8817c4 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -13,6 +13,7 @@ module API
args.delete(:id)
args[:milestone_title] = args.delete(:milestone)
args[:label_name] = args.delete(:labels)
+ args[:scope] = args[:scope].underscore if args[:scope]
issues = IssuesFinder.new(current_user, args).execute
.preload(:assignees, :labels, :notes, :timelogs)
@@ -36,8 +37,8 @@ module API
optional :updated_before, type: DateTime, desc: 'Return issues updated before the specified time'
optional :author_id, type: Integer, desc: 'Return issues which are authored by the user with the given ID'
optional :assignee_id, type: Integer, desc: 'Return issues which are assigned to the user with the given ID'
- optional :scope, type: String, values: %w[created-by-me assigned-to-me all],
- desc: 'Return issues for the given scope: `created-by-me`, `assigned-to-me` or `all`'
+ optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all],
+ desc: 'Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`'
optional :my_reaction_emoji, type: String, desc: 'Return issues reacted by the authenticated user by the given emoji'
use :pagination
end
@@ -66,8 +67,8 @@ module API
optional :state, type: String, values: %w[opened closed all], default: 'all',
desc: 'Return opened, closed, or all issues'
use :issues_params
- optional :scope, type: String, values: %w[created-by-me assigned-to-me all], default: 'created-by-me',
- desc: 'Return issues for the given scope: `created-by-me`, `assigned-to-me` or `all`'
+ optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all], default: 'created_by_me',
+ desc: 'Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`'
end
get do
authenticate! unless params[:scope] == 'all'
diff --git a/lib/api/markdown.rb b/lib/api/markdown.rb
new file mode 100644
index 00000000000..b9ed68aa584
--- /dev/null
+++ b/lib/api/markdown.rb
@@ -0,0 +1,33 @@
+module API
+ class Markdown < Grape::API
+ params do
+ requires :text, type: String, desc: "The markdown text to render"
+ optional :gfm, type: Boolean, desc: "Render text using GitLab Flavored Markdown"
+ optional :project, type: String, desc: "The full path of a project to use as the context when creating references using GitLab Flavored Markdown"
+ end
+ resource :markdown do
+ desc "Render markdown text" do
+ detail "This feature was introduced in GitLab 11.0."
+ end
+ post do
+ # Explicitly set CommonMark as markdown engine to use.
+ # Remove this set when https://gitlab.com/gitlab-org/gitlab-ce/issues/43011 is done.
+ context = { markdown_engine: :common_mark, only_path: false }
+
+ if params[:project]
+ project = Project.find_by_full_path(params[:project])
+
+ not_found!("Project") unless can?(current_user, :read_project, project)
+
+ context[:project] = project
+ else
+ context[:skip_project_check] = true
+ end
+
+ context[:pipeline] = params[:gfm] ? :full : :plain_markdown
+
+ { html: Banzai.render(params[:text], context) }
+ end
+ end
+ end
+end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index d4cc18f622b..bc4df16e3a8 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -38,6 +38,7 @@ module API
args[:milestone_title] = args.delete(:milestone)
args[:label_name] = args.delete(:labels)
+ args[:scope] = args[:scope].underscore if args[:scope]
merge_requests = MergeRequestsFinder.new(current_user, args).execute
.reorder(args[:order_by] => args[:sort])
@@ -79,8 +80,8 @@ module API
optional :view, type: String, values: %w[simple], desc: 'If simple, returns the `iid`, URL, title, description, and basic state of merge request'
optional :author_id, type: Integer, desc: 'Return merge requests which are authored by the user with the given ID'
optional :assignee_id, type: Integer, desc: 'Return merge requests which are assigned to the user with the given ID'
- optional :scope, type: String, values: %w[created-by-me assigned-to-me all],
- desc: 'Return merge requests for the given scope: `created-by-me`, `assigned-to-me` or `all`'
+ optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all],
+ desc: 'Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`'
optional :my_reaction_emoji, type: String, desc: 'Return issues reacted by the authenticated user by the given emoji'
optional :source_branch, type: String, desc: 'Return merge requests with the given source branch'
optional :target_branch, type: String, desc: 'Return merge requests with the given target branch'
@@ -95,8 +96,8 @@ module API
end
params do
use :merge_requests_params
- optional :scope, type: String, values: %w[created-by-me assigned-to-me all], default: 'created-by-me',
- desc: 'Return merge requests for the given scope: `created-by-me`, `assigned-to-me` or `all`'
+ optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all], default: 'created_by_me',
+ desc: 'Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`'
end
get do
authenticate! unless params[:scope] == 'all'
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
index 649feba1036..a7f1cb1131f 100644
--- a/lib/api/runner.rb
+++ b/lib/api/runner.rb
@@ -149,6 +149,7 @@ module API
end
patch '/:id/trace' do
job = authenticate_job!
+ forbidden!('Job is not running') unless job.running?
error!('400 Missing header Content-Range', 400) unless request.headers.key?('Content-Range')
content_range = request.headers['Content-Range']
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index 152df23a327..e31c332b6e4 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -5,7 +5,7 @@ module API
helpers do
def current_settings
@current_setting ||=
- (ApplicationSetting.current || ApplicationSetting.create_from_defaults)
+ (ApplicationSetting.current_without_cache || ApplicationSetting.create_from_defaults)
end
end
diff --git a/lib/api/v3/groups.rb b/lib/api/v3/groups.rb
index 3844fd4810d..4fa7d196e50 100644
--- a/lib/api/v3/groups.rb
+++ b/lib/api/v3/groups.rb
@@ -131,8 +131,9 @@ module API
delete ":id" do
group = find_group!(params[:id])
authorize! :admin_group, group
- Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/46285')
- present ::Groups::DestroyService.new(group, current_user).execute, with: Entities::GroupDetail, current_user: current_user
+ ::Groups::DestroyService.new(group, current_user).async_execute
+
+ accepted!
end
desc 'Get a list of projects in this group.' do
diff --git a/lib/api/v3/settings.rb b/lib/api/v3/settings.rb
index 9b4ab7630fb..fc56495c8b1 100644
--- a/lib/api/v3/settings.rb
+++ b/lib/api/v3/settings.rb
@@ -6,7 +6,7 @@ module API
helpers do
def current_settings
@current_setting ||=
- (ApplicationSetting.current || ApplicationSetting.create_from_defaults)
+ (ApplicationSetting.current_without_cache || ApplicationSetting.create_from_defaults)
end
end
diff --git a/lib/banzai/filter/gollum_tags_filter.rb b/lib/banzai/filter/gollum_tags_filter.rb
index f2e9a5a1116..4bc82ecb4d6 100644
--- a/lib/banzai/filter/gollum_tags_filter.rb
+++ b/lib/banzai/filter/gollum_tags_filter.rb
@@ -58,6 +58,9 @@ module Banzai
def call
doc.search(".//text()").each do |node|
+ # Do not perform linking inside <code> blocks
+ next unless node.ancestors('code').empty?
+
# A Gollum ToC tag is `[[_TOC_]]`, but due to MarkdownFilter running
# before this one, it will be converted into `[[<em>TOC</em>]]`, so it
# needs special-case handling
diff --git a/lib/banzai/filter/plantuml_filter.rb b/lib/banzai/filter/plantuml_filter.rb
index 5325819d828..28933c78966 100644
--- a/lib/banzai/filter/plantuml_filter.rb
+++ b/lib/banzai/filter/plantuml_filter.rb
@@ -23,7 +23,7 @@ module Banzai
private
def settings
- ApplicationSetting.current || ApplicationSetting.create_from_defaults
+ Gitlab::CurrentSettings.current_application_settings
end
def plantuml_setup
diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb
index b9d5ecf70ec..2f023f4f242 100644
--- a/lib/banzai/filter/reference_filter.rb
+++ b/lib/banzai/filter/reference_filter.rb
@@ -73,7 +73,7 @@ module Banzai
#
# Note that while the key might exist, its value could be nil!
def validate
- needs :project
+ needs :project unless skip_project_check?
end
# Iterates over all <a> and text() nodes in a document.
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index 8b2f05fffec..a1f24e8b093 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -42,9 +42,9 @@ module Banzai
end
def self.transform_context(context)
- context.merge(
- only_path: true,
+ context[:only_path] = true unless context.key?(:only_path)
+ context.merge(
# EmojiFilter
asset_host: Gitlab::Application.config.asset_host,
asset_root: Gitlab.config.gitlab.base_url
diff --git a/lib/feature.rb b/lib/feature.rb
index 8e9ba5c530a..6474de6e56d 100644
--- a/lib/feature.rb
+++ b/lib/feature.rb
@@ -1,3 +1,6 @@
+require 'flipper/adapters/active_record'
+require 'flipper/adapters/active_support_cache_store'
+
class Feature
# Classes to override flipper table names
class FlipperFeature < Flipper::Adapters::ActiveRecord::Feature
@@ -60,7 +63,8 @@ class Feature
end
def flipper
- @flipper ||= Flipper.instance
+ Thread.current[:flipper] ||=
+ Flipper.new(flipper_adapter).tap { |flip| flip.memoize = true }
end
# This method is called from config/initializers/flipper.rb and can be used
@@ -68,5 +72,16 @@ class Feature
# See https://docs.gitlab.com/ee/development/feature_flags.html#feature-groups
def register_feature_groups
end
+
+ def flipper_adapter
+ active_record_adapter = Flipper::Adapters::ActiveRecord.new(
+ feature_class: FlipperFeature,
+ gate_class: FlipperGate)
+
+ Flipper::Adapters::ActiveSupportCacheStore.new(
+ active_record_adapter,
+ Rails.cache,
+ expires_in: 1.hour)
+ end
end
end
diff --git a/lib/gitlab.rb b/lib/gitlab.rb
index c5498d0da1a..31119471b8e 100644
--- a/lib/gitlab.rb
+++ b/lib/gitlab.rb
@@ -9,6 +9,10 @@ module Gitlab
Settings
end
+ def self.migrations_hash
+ @_migrations_hash ||= Digest::MD5.hexdigest(ActiveRecord::Migrator.get_all_versions.to_s)
+ end
+
COM_URL = 'https://gitlab.com'.freeze
APP_DIRS_PATTERN = %r{^/?(app|config|ee|lib|spec|\(\w*\))}
SUBDOMAIN_REGEX = %r{\Ahttps://[a-z0-9]+\.gitlab\.com\z}
diff --git a/lib/gitlab/auth/ldap/access.rb b/lib/gitlab/auth/ldap/access.rb
index 34286900e72..865185eb5db 100644
--- a/lib/gitlab/auth/ldap/access.rb
+++ b/lib/gitlab/auth/ldap/access.rb
@@ -6,7 +6,7 @@ module Gitlab
module Auth
module LDAP
class Access
- attr_reader :provider, :user
+ attr_reader :provider, :user, :ldap_identity
def self.open(user, &block)
Gitlab::Auth::LDAP::Adapter.open(user.ldap_identity.provider) do |adapter|
@@ -14,9 +14,12 @@ module Gitlab
end
end
- def self.allowed?(user)
+ def self.allowed?(user, options = {})
self.open(user) do |access|
+ # Whether user is allowed, or not, we should update
+ # permissions to keep things clean
if access.allowed?
+ access.update_user
Users::UpdateService.new(user, user: user, last_credential_check_at: Time.now).execute
true
@@ -29,7 +32,8 @@ module Gitlab
def initialize(user, adapter = nil)
@adapter = adapter
@user = user
- @provider = user.ldap_identity.provider
+ @ldap_identity = user.ldap_identity
+ @provider = adapter&.provider || ldap_identity&.provider
end
def allowed?
@@ -40,7 +44,7 @@ module Gitlab
end
# Block user in GitLab if he/she was blocked in AD
- if Gitlab::Auth::LDAP::Person.disabled_via_active_directory?(user.ldap_identity.extern_uid, adapter)
+ if Gitlab::Auth::LDAP::Person.disabled_via_active_directory?(ldap_identity.extern_uid, adapter)
block_user(user, 'is disabled in Active Directory')
false
else
@@ -64,27 +68,44 @@ module Gitlab
Gitlab::Auth::LDAP::Config.new(provider)
end
+ def find_ldap_user
+ Gitlab::Auth::LDAP::Person.find_by_dn(ldap_identity.extern_uid, adapter)
+ end
+
def ldap_user
- @ldap_user ||= Gitlab::Auth::LDAP::Person.find_by_dn(user.ldap_identity.extern_uid, adapter)
+ return unless provider
+
+ @ldap_user ||= find_ldap_user
end
def block_user(user, reason)
user.ldap_block
- Gitlab::AppLogger.info(
- "LDAP account \"#{user.ldap_identity.extern_uid}\" #{reason}, " \
- "blocking Gitlab user \"#{user.name}\" (#{user.email})"
- )
+ if provider
+ Gitlab::AppLogger.info(
+ "LDAP account \"#{ldap_identity.extern_uid}\" #{reason}, " \
+ "blocking Gitlab user \"#{user.name}\" (#{user.email})"
+ )
+ else
+ Gitlab::AppLogger.info(
+ "Account is not provided by LDAP, " \
+ "blocking Gitlab user \"#{user.name}\" (#{user.email})"
+ )
+ end
end
def unblock_user(user, reason)
user.activate
Gitlab::AppLogger.info(
- "LDAP account \"#{user.ldap_identity.extern_uid}\" #{reason}, " \
+ "LDAP account \"#{ldap_identity.extern_uid}\" #{reason}, " \
"unblocking Gitlab user \"#{user.name}\" (#{user.email})"
)
end
+
+ def update_user
+ # no-op in CE
+ end
end
end
end
diff --git a/lib/gitlab/auth/ldap/config.rb b/lib/gitlab/auth/ldap/config.rb
index 77185f52ced..d4415eaa6dc 100644
--- a/lib/gitlab/auth/ldap/config.rb
+++ b/lib/gitlab/auth/ldap/config.rb
@@ -11,6 +11,8 @@ module Gitlab
attr_accessor :provider, :options
+ InvalidProvider = Class.new(StandardError)
+
def self.enabled?
Gitlab.config.ldap.enabled
end
@@ -22,6 +24,10 @@ module Gitlab
def self.available_servers
return [] unless enabled?
+ _available_servers
+ end
+
+ def self._available_servers
Array.wrap(servers.first)
end
@@ -34,7 +40,7 @@ module Gitlab
end
def self.invalid_provider(provider)
- raise "Unknown provider (#{provider}). Available providers: #{providers}"
+ raise InvalidProvider.new("Unknown provider (#{provider}). Available providers: #{providers}")
end
def initialize(provider)
@@ -84,13 +90,17 @@ module Gitlab
end
def base
- options['base']
+ @base ||= Person.normalize_dn(options['base'])
end
def uid
options['uid']
end
+ def label
+ options['label']
+ end
+
def sync_ssh_keys?
sync_ssh_keys.present?
end
@@ -132,6 +142,10 @@ module Gitlab
options['timeout'].to_i
end
+ def external_groups
+ options['external_groups'] || []
+ end
+
def has_auth?
options['password'] || options['bind_dn']
end
diff --git a/lib/gitlab/auth/saml/config.rb b/lib/gitlab/auth/saml/config.rb
index 2760b1a3247..5fa9581f837 100644
--- a/lib/gitlab/auth/saml/config.rb
+++ b/lib/gitlab/auth/saml/config.rb
@@ -14,6 +14,10 @@ module Gitlab
def external_groups
options[:external_groups]
end
+
+ def admin_groups
+ options[:admin_groups]
+ end
end
end
end
diff --git a/lib/gitlab/auth/saml/user.rb b/lib/gitlab/auth/saml/user.rb
index cb01cd8004c..b8c84c37cd5 100644
--- a/lib/gitlab/auth/saml/user.rb
+++ b/lib/gitlab/auth/saml/user.rb
@@ -20,10 +20,8 @@ module Gitlab
user ||= find_or_build_ldap_user if auto_link_ldap_user?
user ||= build_new_user if signup_enabled?
- if external_users_enabled? && user
- # Check if there is overlap between the user's groups and the external groups
- # setting then set user as external or internal.
- user.external = !(auth_hash.groups & saml_config.external_groups).empty?
+ if user
+ user.external = !(auth_hash.groups & saml_config.external_groups).empty? if external_users_enabled?
end
user
diff --git a/lib/gitlab/auth/user_auth_finders.rb b/lib/gitlab/auth/user_auth_finders.rb
index cf02030c577..4dc23f977da 100644
--- a/lib/gitlab/auth/user_auth_finders.rb
+++ b/lib/gitlab/auth/user_auth_finders.rb
@@ -1,9 +1,5 @@
module Gitlab
module Auth
- #
- # Exceptions
- #
-
AuthenticationError = Class.new(StandardError)
MissingTokenError = Class.new(AuthenticationError)
TokenNotFoundError = Class.new(AuthenticationError)
@@ -61,6 +57,12 @@ module Gitlab
private
+ def route_authentication_setting
+ return {} unless respond_to?(:route_setting)
+
+ route_setting(:authentication) || {}
+ end
+
def access_token
strong_memoize(:access_token) do
find_oauth_access_token || find_personal_access_token
diff --git a/lib/gitlab/ci/pipeline/expression.rb b/lib/gitlab/ci/pipeline/expression.rb
new file mode 100644
index 00000000000..f57df7c5637
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/expression.rb
@@ -0,0 +1,10 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Expression
+ ExpressionError = Class.new(StandardError)
+ RuntimeError = Class.new(ExpressionError)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb b/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb
new file mode 100644
index 00000000000..10957598f76
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb
@@ -0,0 +1,29 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Expression
+ module Lexeme
+ class Matches < Lexeme::Operator
+ PATTERN = /=~/.freeze
+
+ def initialize(left, right)
+ @left = left
+ @right = right
+ end
+
+ def evaluate(variables = {})
+ text = @left.evaluate(variables)
+ regexp = @right.evaluate(variables)
+
+ regexp.scan(text.to_s).any?
+ end
+
+ def self.build(_value, behind, ahead)
+ new(behind, ahead)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb b/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb
new file mode 100644
index 00000000000..9b239c29ea4
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb
@@ -0,0 +1,33 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Expression
+ module Lexeme
+ require_dependency 're2'
+
+ class Pattern < Lexeme::Value
+ PATTERN = %r{^/.+/[ismU]*$}.freeze
+
+ def initialize(regexp)
+ @value = regexp
+
+ unless Gitlab::UntrustedRegexp.valid?(@value)
+ raise Lexer::SyntaxError, 'Invalid regular expression!'
+ end
+ end
+
+ def evaluate(variables = {})
+ Gitlab::UntrustedRegexp.fabricate(@value)
+ rescue RegexpError
+ raise Expression::RuntimeError, 'Invalid regular expression!'
+ end
+
+ def self.build(string)
+ new(string)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/expression/lexer.rb b/lib/gitlab/ci/pipeline/expression/lexer.rb
index e1c68b7c3c2..4cacb1e62c9 100644
--- a/lib/gitlab/ci/pipeline/expression/lexer.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexer.rb
@@ -5,15 +5,17 @@ module Gitlab
class Lexer
include ::Gitlab::Utils::StrongMemoize
+ SyntaxError = Class.new(Expression::ExpressionError)
+
LEXEMES = [
Expression::Lexeme::Variable,
Expression::Lexeme::String,
+ Expression::Lexeme::Pattern,
Expression::Lexeme::Null,
- Expression::Lexeme::Equals
+ Expression::Lexeme::Equals,
+ Expression::Lexeme::Matches
].freeze
- SyntaxError = Class.new(Statement::StatementError)
-
MAX_TOKENS = 100
def initialize(statement, max_tokens: MAX_TOKENS)
diff --git a/lib/gitlab/ci/pipeline/expression/statement.rb b/lib/gitlab/ci/pipeline/expression/statement.rb
index 09a7c98464b..b36f1e0f865 100644
--- a/lib/gitlab/ci/pipeline/expression/statement.rb
+++ b/lib/gitlab/ci/pipeline/expression/statement.rb
@@ -3,15 +3,16 @@ module Gitlab
module Pipeline
module Expression
class Statement
- StatementError = Class.new(StandardError)
+ StatementError = Class.new(Expression::ExpressionError)
GRAMMAR = [
+ %w[variable],
%w[variable equals string],
%w[variable equals variable],
%w[variable equals null],
%w[string equals variable],
%w[null equals variable],
- %w[variable]
+ %w[variable matches pattern]
].freeze
def initialize(statement, variables = {})
@@ -35,11 +36,13 @@ module Gitlab
def truthful?
evaluate.present?
+ rescue Expression::ExpressionError
+ false
end
def valid?
parse_tree.is_a?(Lexeme::Base)
- rescue StatementError
+ rescue Expression::ExpressionError
false
end
end
diff --git a/lib/gitlab/ci/pipeline/preloader.rb b/lib/gitlab/ci/pipeline/preloader.rb
new file mode 100644
index 00000000000..e7a2e5511cf
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/preloader.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Pipeline
+ # Class for preloading data associated with pipelines such as commit
+ # authors.
+ module Preloader
+ def self.preload(pipelines)
+ # This ensures that all the pipeline commits are eager loaded before we
+ # start using them.
+ pipelines.each(&:commit)
+
+ pipelines.each do |pipeline|
+ # This preloads the author of every commit. We're using "lazy_author"
+ # here since "author" immediately loads the data on the first call.
+ pipeline.commit.try(:lazy_author)
+
+ # This preloads the number of warnings for every pipeline, ensuring
+ # that Ci::Pipeline#has_warnings? doesn't execute any additional
+ # queries.
+ pipeline.number_of_warnings
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index e392a015b91..6cf7aa1bf0d 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -9,8 +9,8 @@ module Gitlab
end
end
- def fake_application_settings(defaults = ::ApplicationSetting.defaults)
- Gitlab::FakeApplicationSettings.new(defaults)
+ def fake_application_settings(attributes = {})
+ Gitlab::FakeApplicationSettings.new(::ApplicationSetting.defaults.merge(attributes || {}))
end
def method_missing(name, *args, &block)
@@ -25,43 +25,35 @@ module Gitlab
def ensure_application_settings!
return in_memory_application_settings if ENV['IN_MEMORY_APPLICATION_SETTINGS'] == 'true'
-
- cached_application_settings || uncached_application_settings
- end
-
- def cached_application_settings
- begin
- ::ApplicationSetting.cached
- rescue ::Redis::BaseError, ::Errno::ENOENT, ::Errno::EADDRNOTAVAIL
- # In case Redis isn't running or the Redis UNIX socket file is not available
- end
- end
-
- def uncached_application_settings
return fake_application_settings unless connect_to_db?
- db_settings = ::ApplicationSetting.current
-
+ current_settings = ::ApplicationSetting.current
# If there are pending migrations, it's possible there are columns that
# need to be added to the application settings. To prevent Rake tasks
# and other callers from failing, use any loaded settings and return
# defaults for missing columns.
if ActiveRecord::Migrator.needs_migration?
- defaults = ::ApplicationSetting.defaults
- defaults.merge!(db_settings.attributes.symbolize_keys) if db_settings.present?
- return fake_application_settings(defaults)
+ return fake_application_settings(current_settings&.attributes)
end
- return db_settings if db_settings.present?
+ return current_settings if current_settings.present?
- ::ApplicationSetting.create_from_defaults || in_memory_application_settings
+ with_fallback_to_fake_application_settings do
+ ::ApplicationSetting.create_from_defaults || in_memory_application_settings
+ end
end
def in_memory_application_settings
- @in_memory_application_settings ||= ::ApplicationSetting.new(::ApplicationSetting.defaults) # rubocop:disable Gitlab/ModuleWithInstanceVariables
- rescue ActiveRecord::StatementInvalid, ActiveRecord::UnknownAttributeError
- # In case migrations the application_settings table is not created yet,
- # we fallback to a simple OpenStruct
+ with_fallback_to_fake_application_settings do
+ @in_memory_application_settings ||= ::ApplicationSetting.build_from_defaults # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ end
+ end
+
+ def with_fallback_to_fake_application_settings(&block)
+ yield
+ rescue
+ # In case the application_settings table is not created yet, or if a new
+ # ApplicationSetting column is not yet migrated we fallback to a simple OpenStruct
fake_application_settings
end
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index 76501dd50e8..d49d055c3f2 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -43,7 +43,7 @@ module Gitlab
end
def self.version
- database_version.match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1]
+ @version ||= database_version.match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1]
end
def self.join_lateral_supported?
diff --git a/lib/gitlab/email/handler/create_issue_handler.rb b/lib/gitlab/email/handler/create_issue_handler.rb
index 05a60deb7d3..764f93f6d3d 100644
--- a/lib/gitlab/email/handler/create_issue_handler.rb
+++ b/lib/gitlab/email/handler/create_issue_handler.rb
@@ -47,7 +47,7 @@ module Gitlab
project,
author,
title: mail.subject,
- description: message
+ description: message_including_reply
).execute
end
end
diff --git a/lib/gitlab/email/handler/reply_processing.rb b/lib/gitlab/email/handler/reply_processing.rb
index da5ff350549..38b1425364f 100644
--- a/lib/gitlab/email/handler/reply_processing.rb
+++ b/lib/gitlab/email/handler/reply_processing.rb
@@ -16,8 +16,12 @@ module Gitlab
@message ||= process_message
end
- def process_message
- message = ReplyParser.new(mail).execute.strip
+ def message_including_reply
+ @message_with_reply ||= process_message(trim_reply: false)
+ end
+
+ def process_message(**kwargs)
+ message = ReplyParser.new(mail, **kwargs).execute.strip
add_attachments(message)
end
diff --git a/lib/gitlab/email/reply_parser.rb b/lib/gitlab/email/reply_parser.rb
index 01c28d051ee..ae6b84607d6 100644
--- a/lib/gitlab/email/reply_parser.rb
+++ b/lib/gitlab/email/reply_parser.rb
@@ -4,8 +4,9 @@ module Gitlab
class ReplyParser
attr_accessor :message
- def initialize(message)
+ def initialize(message, trim_reply: true)
@message = message
+ @trim_reply = trim_reply
end
def execute
@@ -13,7 +14,9 @@ module Gitlab
encoding = body.encoding
- body = EmailReplyTrimmer.trim(body)
+ if @trim_reply
+ body = EmailReplyTrimmer.trim(body)
+ end
return '' unless body
diff --git a/lib/gitlab/file_detector.rb b/lib/gitlab/file_detector.rb
index 77af7a868d0..49bc9c0b671 100644
--- a/lib/gitlab/file_detector.rb
+++ b/lib/gitlab/file_detector.rb
@@ -14,7 +14,7 @@ module Gitlab
avatar: /\Alogo\.(png|jpg|gif)\z/,
issue_template: %r{\A\.gitlab/issue_templates/[^/]+\.md\z},
merge_request_template: %r{\A\.gitlab/merge_request_templates/[^/]+\.md\z},
- xcode_config: %r{\A[^/]*\.(xcodeproj|xcworkspace)\z},
+ xcode_config: %r{\A[^/]*\.(xcodeproj|xcworkspace)(/.+)?\z},
# Configuration files
gitignore: '.gitignore',
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index d78d29b7ac6..156d077a69c 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -104,25 +104,22 @@ module Gitlab
# file.rb # oid: 4a
#
#
- # Blob.find_entry_by_path(repo, '1a', 'app/file.rb') # => '4a'
+ # Blob.find_entry_by_path(repo, '1a', 'blog', 'app', 'file.rb') # => '4a'
#
- def find_entry_by_path(repository, root_id, path)
+ def find_entry_by_path(repository, root_id, *path_parts)
root_tree = repository.lookup(root_id)
- # Strip leading slashes
- path[%r{^/*}] = ''
- path_arr = path.split('/')
entry = root_tree.find do |entry|
- entry[:name] == path_arr[0]
+ entry[:name] == path_parts[0]
end
return nil unless entry
- if path_arr.size > 1
+ if path_parts.size > 1
return nil unless entry[:type] == :tree
- path_arr.shift
- find_entry_by_path(repository, entry[:oid], path_arr.join('/'))
+ path_parts.shift
+ find_entry_by_path(repository, entry[:oid], *path_parts)
else
[:blob, :commit].include?(entry[:type]) ? entry : nil
end
@@ -185,10 +182,13 @@ module Gitlab
def find_by_rugged(repository, sha, path, limit:)
return unless path
+ # Strip any leading / characters from the path
+ path = path.sub(%r{\A/*}, '')
+
rugged_commit = repository.lookup(sha)
root_tree = rugged_commit.tree
- blob_entry = find_entry_by_path(repository, root_tree.oid, path)
+ blob_entry = find_entry_by_path(repository, root_tree.oid, *path.split('/'))
return nil unless blob_entry
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index d79a4dbeee4..89f761dd515 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -6,6 +6,7 @@ module Gitlab
attr_accessor :raw_commit, :head
+ MAX_COMMIT_MESSAGE_DISPLAY_SIZE = 10.megabytes
MIN_SHA_LENGTH = 7
SERIALIZE_KEYS = [
:id, :message, :parent_ids,
@@ -63,9 +64,7 @@ module Gitlab
if is_enabled
repo.gitaly_commit_client.find_commit(commit_id)
else
- obj = repo.rev_parse_target(commit_id)
-
- obj.is_a?(Rugged::Commit) ? obj : nil
+ rugged_find(repo, commit_id)
end
end
@@ -76,6 +75,12 @@ module Gitlab
nil
end
+ def rugged_find(repo, commit_id)
+ obj = repo.rev_parse_target(commit_id)
+
+ obj.is_a?(Rugged::Commit) ? obj : nil
+ end
+
# Get last commit for HEAD
#
# Ex.
@@ -297,11 +302,40 @@ module Gitlab
nil
end
end
+
+ def get_message(repository, commit_id)
+ BatchLoader.for({ repository: repository, commit_id: commit_id }).batch do |items, loader|
+ items_by_repo = items.group_by { |i| i[:repository] }
+
+ items_by_repo.each do |repo, items|
+ commit_ids = items.map { |i| i[:commit_id] }
+
+ messages = get_messages(repository, commit_ids)
+
+ messages.each do |commit_sha, message|
+ loader.call({ repository: repository, commit_id: commit_sha }, message)
+ end
+ end
+ end
+ end
+
+ def get_messages(repository, commit_ids)
+ repository.gitaly_migrate(:commit_messages) do |is_enabled|
+ if is_enabled
+ repository.gitaly_commit_client.get_commit_messages(commit_ids)
+ else
+ commit_ids.map { |id| [id, rugged_find(repository, id).message] }.to_h
+ end
+ end
+ end
end
def initialize(repository, raw_commit, head = nil)
raise "Nil as raw commit passed" unless raw_commit
+ @repository = repository
+ @head = head
+
case raw_commit
when Hash
init_from_hash(raw_commit)
@@ -312,9 +346,6 @@ module Gitlab
else
raise "Invalid raw commit type: #{raw_commit.class}"
end
-
- @repository = repository
- @head = head
end
def sha
@@ -518,7 +549,7 @@ module Gitlab
# TODO: Once gitaly "takes over" Rugged consider separating the
# subject from the message to make it clearer when there's one
# available but not the other.
- @message = (commit.body.presence || commit.subject).dup
+ @message = message_from_gitaly_body
@authored_date = Time.at(commit.author.date.seconds).utc
@author_name = commit.author.name.dup
@author_email = commit.author.email.dup
@@ -570,6 +601,25 @@ module Gitlab
def refs(repo)
repo.refs_hash[id]
end
+
+ def message_from_gitaly_body
+ return @raw_commit.subject.dup if @raw_commit.body_size.zero?
+ return @raw_commit.body.dup if full_body_fetched_from_gitaly?
+
+ if @raw_commit.body_size > MAX_COMMIT_MESSAGE_DISPLAY_SIZE
+ "#{@raw_commit.subject}\n\n--commit message is too big".strip
+ else
+ fetch_body_from_gitaly
+ end
+ end
+
+ def full_body_fetched_from_gitaly?
+ @raw_commit.body.bytesize == @raw_commit.body_size
+ end
+
+ def fetch_body_from_gitaly
+ self.class.get_message(@repository, id)
+ end
end
end
end
diff --git a/lib/gitlab/git/gitlab_projects.rb b/lib/gitlab/git/gitlab_projects.rb
index 68373460d23..00c943fdb25 100644
--- a/lib/gitlab/git/gitlab_projects.rb
+++ b/lib/gitlab/git/gitlab_projects.rb
@@ -53,7 +53,7 @@ module Gitlab
# Import project via git clone --bare
# URL must be publicly cloneable
def import_project(source, timeout)
- Gitlab::GitalyClient.migrate(:import_repository) do |is_enabled|
+ Gitlab::GitalyClient.migrate(:import_repository, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
gitaly_import_repository(source)
else
diff --git a/lib/gitlab/git/path_helper.rb b/lib/gitlab/git/path_helper.rb
index 155cf52f050..57b82a37d6c 100644
--- a/lib/gitlab/git/path_helper.rb
+++ b/lib/gitlab/git/path_helper.rb
@@ -6,7 +6,7 @@ module Gitlab
class << self
def normalize_path(filename)
# Strip all leading slashes so that //foo -> foo
- filename[%r{^/*}] = ''
+ filename = filename.sub(%r{\A/*}, '')
# Expand relative paths (e.g. foo/../bar)
filename = Pathname.new(filename)
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 061865a7acf..1a21625a322 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -403,10 +403,10 @@ module Gitlab
prefix = archive_prefix(ref, commit.id, append_sha: append_sha)
{
- 'RepoPath' => path,
'ArchivePrefix' => prefix,
'ArchivePath' => archive_file_path(storage_path, commit.id, prefix, format),
- 'CommitId' => commit.id
+ 'CommitId' => commit.id,
+ 'GitalyRepository' => gitaly_repository.to_h
}
end
@@ -1048,7 +1048,7 @@ module Gitlab
return @info_attributes if @info_attributes
content =
- gitaly_migrate(:get_info_attributes) do |is_enabled|
+ gitaly_migrate(:get_info_attributes, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
gitaly_repository_client.info_attributes
else
@@ -1334,7 +1334,7 @@ module Gitlab
end
def squash_in_progress?(squash_id)
- gitaly_migrate(:squash_in_progress) do |is_enabled|
+ gitaly_migrate(:squash_in_progress, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
gitaly_repository_client.squash_in_progress?(squash_id)
else
@@ -1473,10 +1473,19 @@ module Gitlab
def search_files_by_content(query, ref)
return [] if empty? || query.blank?
- offset = 2
- args = %W(grep -i -I -n -z --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref})
+ safe_query = Regexp.escape(query)
+ ref ||= root_ref
+
+ gitaly_migrate(:search_files_by_content) do |is_enabled|
+ if is_enabled
+ gitaly_repository_client.search_files_by_content(ref, safe_query)
+ else
+ offset = 2
+ args = %W(grep -i -I -n -z --before-context #{offset} --after-context #{offset} -E -e #{safe_query} #{ref})
- run_git(args).first.scrub.split(/^--\n/)
+ run_git(args).first.scrub.split(/^--\n/)
+ end
+ end
end
def can_be_merged?(source_sha, target_branch)
@@ -1491,12 +1500,19 @@ module Gitlab
def search_files_by_name(query, ref)
safe_query = Regexp.escape(query.sub(%r{^/*}, ""))
+ ref ||= root_ref
return [] if empty? || safe_query.blank?
- args = %W(ls-tree -r --name-status --full-tree #{ref || root_ref} -- #{safe_query})
+ gitaly_migrate(:search_files_by_name) do |is_enabled|
+ if is_enabled
+ gitaly_repository_client.search_files_by_name(ref, safe_query)
+ else
+ args = %W(ls-tree -r --name-status --full-tree #{ref} -- #{safe_query})
- run_git(args).first.lines.map(&:strip)
+ run_git(args).first.lines.map(&:strip)
+ end
+ end
end
def find_commits_by_message(query, ref, path, limit, offset)
@@ -1572,14 +1588,12 @@ module Gitlab
end
def checksum
- gitaly_migrate(:calculate_checksum,
- status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
- if is_enabled
- gitaly_repository_client.calculate_checksum
- else
- calculate_checksum_by_shelling_out
- end
- end
+ # The exists? RPC is much cheaper, so we perform this request first
+ raise NoRepository, "Repository does not exists" unless exists?
+
+ gitaly_repository_client.calculate_checksum
+ rescue GRPC::NotFound
+ raise NoRepository # Guard against data races.
end
private
@@ -1948,7 +1962,12 @@ module Gitlab
end
target_commit = Gitlab::Git::Commit.find(self, ref.target)
- Gitlab::Git::Tag.new(self, ref.name, ref.target, target_commit, message)
+ Gitlab::Git::Tag.new(self, {
+ name: ref.name,
+ target: ref.target,
+ target_commit: target_commit,
+ message: message
+ })
end.sort_by(&:name)
end
@@ -2348,7 +2367,7 @@ module Gitlab
end
def gitaly_delete_refs(*ref_names)
- gitaly_ref_client.delete_refs(refs: ref_names)
+ gitaly_ref_client.delete_refs(refs: ref_names) if ref_names.any?
end
def rugged_remove_remote(remote_name)
@@ -2478,36 +2497,6 @@ module Gitlab
rev_parse_target(ref).oid
end
- def calculate_checksum_by_shelling_out
- raise NoRepository unless exists?
-
- args = %W(--git-dir=#{path} show-ref --heads --tags)
- output, status = run_git(args)
-
- if status.nil? || !status.zero?
- # Non-valid git repositories return 128 as the status code and an error output
- raise InvalidRepository if status == 128 && output.to_s.downcase =~ /not a git repository/
- # Empty repositories returns with a non-zero status and an empty output.
- raise ChecksumError, output unless output.blank?
-
- return EMPTY_REPOSITORY_CHECKSUM
- end
-
- refs = output.split("\n")
-
- result = refs.inject(nil) do |checksum, ref|
- value = Digest::SHA1.hexdigest(ref).hex
-
- if checksum.nil?
- value
- else
- checksum ^ value
- end
- end
-
- result.to_s(16)
- end
-
def build_git_cmd(*args)
object_directories = alternate_object_directories.join(File::PATH_SEPARATOR)
diff --git a/lib/gitlab/git/repository_mirroring.rb b/lib/gitlab/git/repository_mirroring.rb
index 8a01f92e2af..e35ea5762eb 100644
--- a/lib/gitlab/git/repository_mirroring.rb
+++ b/lib/gitlab/git/repository_mirroring.rb
@@ -35,7 +35,11 @@ module Gitlab
next if name =~ /\^\{\}\Z/
target_commit = Gitlab::Git::Commit.find(self, target)
- Gitlab::Git::Tag.new(self, name, target, target_commit)
+ Gitlab::Git::Tag.new(self, {
+ name: name,
+ target: target,
+ target_commit: target_commit
+ })
end.compact
end
diff --git a/lib/gitlab/git/tag.rb b/lib/gitlab/git/tag.rb
index 8a8f7b051ed..e44284572fd 100644
--- a/lib/gitlab/git/tag.rb
+++ b/lib/gitlab/git/tag.rb
@@ -1,17 +1,99 @@
module Gitlab
module Git
class Tag < Ref
- attr_reader :object_sha
+ extend Gitlab::EncodingHelper
+
+ attr_reader :object_sha, :repository
+
+ MAX_TAG_MESSAGE_DISPLAY_SIZE = 10.megabytes
+ SERIALIZE_KEYS = %i[name target target_commit message].freeze
+
+ attr_accessor *SERIALIZE_KEYS # rubocop:disable Lint/AmbiguousOperator
+
+ class << self
+ def get_message(repository, tag_id)
+ BatchLoader.for({ repository: repository, tag_id: tag_id }).batch do |items, loader|
+ items_by_repo = items.group_by { |i| i[:repository] }
+
+ items_by_repo.each do |repo, items|
+ tag_ids = items.map { |i| i[:tag_id] }
+
+ messages = get_messages(repository, tag_ids)
+
+ messages.each do |id, message|
+ loader.call({ repository: repository, tag_id: id }, message)
+ end
+ end
+ end
+ end
+
+ def get_messages(repository, tag_ids)
+ repository.gitaly_migrate(:tag_messages) do |is_enabled|
+ if is_enabled
+ repository.gitaly_ref_client.get_tag_messages(tag_ids)
+ else
+ tag_ids.map do |id|
+ tag = repository.rugged.lookup(id)
+ message = tag.is_a?(Rugged::Commit) ? "" : tag.message
+
+ [id, message]
+ end.to_h
+ end
+ end
+ end
+ end
+
+ def initialize(repository, raw_tag)
+ @repository = repository
+ @raw_tag = raw_tag
+
+ case raw_tag
+ when Hash
+ init_from_hash
+ when Gitaly::Tag
+ init_from_gitaly
+ end
- def initialize(repository, name, target, target_commit, message = nil)
super(repository, name, target, target_commit)
+ end
+
+ def init_from_hash
+ raw_tag = @raw_tag.symbolize_keys
+
+ SERIALIZE_KEYS.each do |key|
+ send("#{key}=", raw_tag[key]) # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+
+ def init_from_gitaly
+ @name = encode!(@raw_tag.name.dup)
+ @target = @raw_tag.id
+ @message = message_from_gitaly_tag
- @message = message
+ if @raw_tag.target_commit.present?
+ @target_commit = Gitlab::Git::Commit.decorate(repository, @raw_tag.target_commit)
+ end
end
def message
encode! @message
end
+
+ private
+
+ def message_from_gitaly_tag
+ return @raw_tag.message.dup if full_message_fetched_from_gitaly?
+
+ if @raw_tag.message_size > MAX_TAG_MESSAGE_DISPLAY_SIZE
+ '--tag message is too big'
+ else
+ self.class.get_message(@repository, target)
+ end
+ end
+
+ def full_message_fetched_from_gitaly?
+ @raw_tag.message.bytesize == @raw_tag.message_size
+ end
end
end
end
diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb
index d75a5f15c29..1ab8c4e0229 100644
--- a/lib/gitlab/git/wiki.rb
+++ b/lib/gitlab/git/wiki.rb
@@ -131,7 +131,7 @@ module Gitlab
def page_formatted_data(title:, dir: nil, version: nil)
version = version&.id
- @repository.gitaly_migrate(:wiki_page_formatted_data) do |is_enabled|
+ @repository.gitaly_migrate(:wiki_page_formatted_data, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
gitaly_wiki_client.get_formatted_data(title: title, dir: dir, version: version)
else
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index a36e6c822f9..1f5f88bf792 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -334,6 +334,22 @@ module Gitlab
signatures
end
+ def get_commit_messages(commit_ids)
+ request = Gitaly::GetCommitMessagesRequest.new(repository: @gitaly_repo, commit_ids: commit_ids)
+ response = GitalyClient.call(@repository.storage, :commit_service, :get_commit_messages, request)
+
+ messages = Hash.new { |h, k| h[k] = ''.b }
+ current_commit_id = nil
+
+ response.each do |rpc_message|
+ current_commit_id = rpc_message.commit_id if rpc_message.commit_id.present?
+
+ messages[current_commit_id] << rpc_message.message
+ end
+
+ messages
+ end
+
private
def call_commit_diff(request_params, options = {})
diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb
index 831cfd1e014..44b0e517bf0 100644
--- a/lib/gitlab/gitaly_client/operation_service.rb
+++ b/lib/gitlab/gitaly_client/operation_service.rb
@@ -40,7 +40,7 @@ module Gitlab
raise Gitlab::Git::Repository::TagExistsError
end
- Util.gitlab_tag_from_gitaly_tag(@repository, response.tag)
+ Gitlab::Git::Tag.new(@repository, response.tag)
rescue GRPC::FailedPrecondition => e
raise Gitlab::Git::Repository::InvalidRef, e
end
diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb
index ba6b577fd17..3ac46be6208 100644
--- a/lib/gitlab/gitaly_client/ref_service.rb
+++ b/lib/gitlab/gitaly_client/ref_service.rb
@@ -171,6 +171,22 @@ module Gitlab
consume_ref_contains_sha_response(stream, :branch_names)
end
+ def get_tag_messages(tag_ids)
+ request = Gitaly::GetTagMessagesRequest.new(repository: @gitaly_repo, tag_ids: tag_ids)
+ response = GitalyClient.call(@repository.storage, :ref_service, :get_tag_messages, request)
+
+ messages = Hash.new { |h, k| h[k] = ''.b }
+ current_tag_id = nil
+
+ response.each do |rpc_message|
+ current_tag_id = rpc_message.tag_id if rpc_message.tag_id.present?
+
+ messages[current_tag_id] << rpc_message.message
+ end
+
+ messages
+ end
+
private
def consume_refs_response(response)
@@ -210,7 +226,7 @@ module Gitlab
def consume_tags_response(response)
response.flat_map do |message|
- message.tags.map { |gitaly_tag| Util.gitlab_tag_from_gitaly_tag(@repository, gitaly_tag) }
+ message.tags.map { |gitaly_tag| Gitlab::Git::Tag.new(@repository, gitaly_tag) }
end
end
diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb
index 132a5947f17..ee01f5a5bd9 100644
--- a/lib/gitlab/gitaly_client/repository_service.rb
+++ b/lib/gitlab/gitaly_client/repository_service.rb
@@ -301,6 +301,16 @@ module Gitlab
GitalyClient.call(@storage, :repository_service, :get_raw_changes, request)
end
+
+ def search_files_by_name(ref, query)
+ request = Gitaly::SearchFilesByNameRequest.new(repository: @gitaly_repo, ref: ref, query: query)
+ GitalyClient.call(@storage, :repository_service, :search_files_by_name, request).flat_map(&:files)
+ end
+
+ def search_files_by_content(ref, query)
+ request = Gitaly::SearchFilesByContentRequest.new(repository: @gitaly_repo, ref: ref, query: query)
+ GitalyClient.call(@storage, :repository_service, :search_files_by_content, request).flat_map(&:matches)
+ end
end
end
end
diff --git a/lib/gitlab/gitaly_client/util.rb b/lib/gitlab/gitaly_client/util.rb
index 405567db94a..9c19c51d412 100644
--- a/lib/gitlab/gitaly_client/util.rb
+++ b/lib/gitlab/gitaly_client/util.rb
@@ -21,20 +21,6 @@ module Gitlab
gitaly_repository.relative_path,
gitaly_repository.gl_repository)
end
-
- def gitlab_tag_from_gitaly_tag(repository, gitaly_tag)
- if gitaly_tag.target_commit.present?
- commit = Gitlab::Git::Commit.decorate(repository, gitaly_tag.target_commit)
- end
-
- Gitlab::Git::Tag.new(
- repository,
- Gitlab::EncodingHelper.encode!(gitaly_tag.name.dup),
- gitaly_tag.id,
- commit,
- Gitlab::EncodingHelper.encode!(gitaly_tag.message.chomp)
- )
- end
end
end
end
diff --git a/lib/gitlab/incoming_email.rb b/lib/gitlab/incoming_email.rb
index c9122a23568..d323cb9dadf 100644
--- a/lib/gitlab/incoming_email.rb
+++ b/lib/gitlab/incoming_email.rb
@@ -57,7 +57,7 @@ module Gitlab
regex = Regexp.escape(wildcard_address)
regex = regex.sub(Regexp.escape(WILDCARD_PLACEHOLDER), '(.+)')
- Regexp.new(regex).freeze
+ Regexp.new(/\A#{regex}\z/).freeze
end
end
end
diff --git a/lib/gitlab/serializer/pagination.rb b/lib/gitlab/serializer/pagination.rb
index 9c92b83dddc..6bb00d8ae21 100644
--- a/lib/gitlab/serializer/pagination.rb
+++ b/lib/gitlab/serializer/pagination.rb
@@ -17,8 +17,6 @@ module Gitlab
end
end
- private
-
# Methods needed by `API::Helpers::Pagination`
#
diff --git a/lib/gitlab/untrusted_regexp.rb b/lib/gitlab/untrusted_regexp.rb
index 75ba0799058..dc2d91dfa23 100644
--- a/lib/gitlab/untrusted_regexp.rb
+++ b/lib/gitlab/untrusted_regexp.rb
@@ -9,7 +9,9 @@ module Gitlab
# there is a strict limit on total execution time. See the RE2 documentation
# at https://github.com/google/re2/wiki/Syntax for more details.
class UntrustedRegexp
- delegate :===, to: :regexp
+ require_dependency 're2'
+
+ delegate :===, :source, to: :regexp
def initialize(pattern, multiline: false)
if multiline
@@ -35,6 +37,10 @@ module Gitlab
RE2.Replace(text, regexp, rewrite)
end
+ def ==(other)
+ self.source == other.source
+ end
+
# Handles regular expressions with the preferred RE2 library where possible
# via UntustedRegex. Falls back to Ruby's built-in regular expression library
# when the syntax would be invalid in RE2.
@@ -48,6 +54,24 @@ module Gitlab
Regexp.new(pattern)
end
+ def self.valid?(pattern)
+ !!self.fabricate(pattern)
+ rescue RegexpError
+ false
+ end
+
+ def self.fabricate(pattern)
+ matches = pattern.match(%r{^/(?<regexp>.+)/(?<flags>[ismU]*)$})
+
+ raise RegexpError, 'Invalid regular expression!' if matches.nil?
+
+ expression = matches[:regexp]
+ flags = matches[:flags]
+ expression.prepend("(?#{flags})") if flags.present?
+
+ self.new(expression, multiline: false)
+ end
+
private
attr_reader :regexp
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index 1f060de657d..e893e46ee86 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -65,12 +65,7 @@ module Gitlab
params = repository.archive_metadata(ref, Gitlab.config.gitlab.repository_downloads_path, format, append_sha: append_sha)
raise "Repository or ref not found" if params.empty?
- if Gitlab::GitalyClient.feature_enabled?(:workhorse_archive, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT)
- params.merge!(
- 'GitalyServer' => gitaly_server_hash(repository),
- 'GitalyRepository' => repository.gitaly_repository.to_h
- )
- end
+ params['GitalyServer'] = gitaly_server_hash(repository)
# If present DisableCache must be a Boolean. Otherwise workhorse ignores it.
params['DisableCache'] = true if git_archive_cache_disabled?
diff --git a/lib/tasks/migrate/add_limits_mysql.rake b/lib/tasks/migrate/add_limits_mysql.rake
index c6204f89de4..9b05876034c 100644
--- a/lib/tasks/migrate/add_limits_mysql.rake
+++ b/lib/tasks/migrate/add_limits_mysql.rake
@@ -2,6 +2,7 @@ require Rails.root.join('db/migrate/limits_to_mysql')
require Rails.root.join('db/migrate/markdown_cache_limits_to_mysql')
require Rails.root.join('db/migrate/merge_request_diff_file_limits_to_mysql')
require Rails.root.join('db/migrate/limits_ci_build_trace_chunks_raw_data_for_mysql')
+require Rails.root.join('db/migrate/gpg_keys_limits_to_mysql')
desc "GitLab | Add limits to strings in mysql database"
task add_limits_mysql: :environment do
@@ -10,4 +11,5 @@ task add_limits_mysql: :environment do
MarkdownCacheLimitsToMysql.new.up
MergeRequestDiffFileLimitsToMysql.new.up
LimitsCiBuildTraceChunksRawDataForMysql.new.up
+ IncreaseMysqlTextLimitForGpgKeys.new.up
end
diff --git a/lib/tasks/migrate/composite_primary_keys.rake b/lib/tasks/migrate/composite_primary_keys.rake
new file mode 100644
index 00000000000..eb112434dd9
--- /dev/null
+++ b/lib/tasks/migrate/composite_primary_keys.rake
@@ -0,0 +1,15 @@
+namespace :gitlab do
+ namespace :db do
+ desc 'GitLab | Adds primary keys to tables that only have composite unique keys'
+ task composite_primary_keys_add: :environment do
+ require Rails.root.join('db/optional_migrations/composite_primary_keys')
+ CompositePrimaryKeysMigration.new.up
+ end
+
+ desc 'GitLab | Removes previously added composite primary keys'
+ task composite_primary_keys_drop: :environment do
+ require Rails.root.join('db/optional_migrations/composite_primary_keys')
+ CompositePrimaryKeysMigration.new.down
+ end
+ end
+end
diff --git a/lib/tasks/migrate/setup_postgresql.rake b/lib/tasks/migrate/setup_postgresql.rake
index af30ecb0e9b..e7aab50e42a 100644
--- a/lib/tasks/migrate/setup_postgresql.rake
+++ b/lib/tasks/migrate/setup_postgresql.rake
@@ -8,6 +8,7 @@ task setup_postgresql: :environment do
require Rails.root.join('db/migrate/20170503185032_index_redirect_routes_path_for_like')
require Rails.root.join('db/migrate/20171220191323_add_index_on_namespaces_lower_name.rb')
require Rails.root.join('db/migrate/20180215181245_users_name_lower_index.rb')
+ require Rails.root.join('db/migrate/20180504195842_project_name_lower_index.rb')
require Rails.root.join('db/post_migrate/20180306164012_add_path_index_to_redirect_routes.rb')
NamespacesProjectsPathLowerIndexes.new.up
@@ -18,5 +19,6 @@ task setup_postgresql: :environment do
IndexRedirectRoutesPathForLike.new.up
AddIndexOnNamespacesLowerName.new.up
UsersNameLowerIndex.new.up
+ ProjectNameLowerIndex.new.up
AddPathIndexToRedirectRoutes.new.up
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 90cdfd0dd03..608d2a584ba 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-05-15 15:05+0200\n"
-"PO-Revision-Date: 2018-05-15 15:05+0200\n"
+"POT-Creation-Date: 2018-05-21 12:38-0700\n"
+"PO-Revision-Date: 2018-05-21 12:38-0700\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
@@ -82,6 +82,9 @@ msgstr[1] ""
msgid "%{loadingIcon} Started"
msgstr ""
+msgid "%{lock_path} is locked by GitLab User %{lock_user_id}"
+msgstr ""
+
msgid "%{nip_domain} can be used as an alternative to a custom domain."
msgstr ""
@@ -1447,7 +1450,7 @@ msgstr ""
msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images."
msgstr ""
-msgid "ContainerRegistry|You can also %{deploy_token} for read-only access to the registry images."
+msgid "ContainerRegistry|You can also use a %{deploy_token} for read-only access to the registry images."
msgstr ""
msgid "Continuous Integration and Deployment"
@@ -1872,6 +1875,9 @@ msgstr ""
msgid "Enable the Performance Bar for a given group."
msgstr ""
+msgid "Ends at (UTC)"
+msgstr ""
+
msgid "Environments"
msgstr ""
@@ -2484,6 +2490,9 @@ msgstr ""
msgid "Lock %{issuableDisplayName}"
msgstr ""
+msgid "Lock not found"
+msgstr ""
+
msgid "Lock to current projects"
msgstr ""
@@ -3404,6 +3413,9 @@ msgstr ""
msgid "Reset runners registration token"
msgstr ""
+msgid "Resolve conflicts on source branch"
+msgstr ""
+
msgid "Resolve discussion"
msgstr ""
@@ -3780,6 +3792,9 @@ msgstr ""
msgid "Started"
msgstr ""
+msgid "Starts at (UTC)"
+msgstr ""
+
msgid "Status"
msgstr ""
@@ -4272,6 +4287,9 @@ msgstr ""
msgid "Toggle Sidebar"
msgstr ""
+msgid "Toggle discussion"
+msgstr ""
+
msgid "Toggle sidebar"
msgstr ""
@@ -4593,12 +4611,21 @@ msgstr ""
msgid "You can only edit files when you are on a branch"
msgstr ""
+msgid "You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}"
+msgstr ""
+
msgid "You cannot write to this read-only GitLab instance."
msgstr ""
+msgid "You have no permissions"
+msgstr ""
+
msgid "You have reached your project limit"
msgstr ""
+msgid "You must have master access to force delete a lock"
+msgstr ""
+
msgid "You must sign in to star a project"
msgstr ""
diff --git a/package.json b/package.json
index 59b4988b264..1382afd41d6 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
"blackst0ne-mermaid": "^7.1.0-fixed",
"bootstrap-sass": "^3.3.6",
"brace-expansion": "^1.1.8",
+ "cache-loader": "^1.2.2",
"chart.js": "1.0.2",
"classlist-polyfill": "^1.2.0",
"clipboard": "^1.7.1",
@@ -83,7 +84,7 @@
"url-loader": "^1.0.1",
"visibilityjs": "^1.2.4",
"vue": "^2.5.16",
- "vue-loader": "^14.1.1",
+ "vue-loader": "^15.2.0",
"vue-resource": "^1.5.0",
"vue-router": "^3.0.1",
"vue-template-compiler": "^2.5.16",
diff --git a/qa/qa/specs/features/merge_request/create_spec.rb b/qa/qa/specs/features/merge_request/create_spec.rb
index fbf9a4d17e5..0931e649e24 100644
--- a/qa/qa/specs/features/merge_request/create_spec.rb
+++ b/qa/qa/specs/features/merge_request/create_spec.rb
@@ -11,7 +11,7 @@ module QA
expect(page).to have_content('This is a merge request')
expect(page).to have_content('Great feature')
- expect(page).to have_content('Opened less than a minute ago')
+ expect(page).to have_content(/Opened [\w\s]+ a minute ago/)
end
end
end
diff --git a/qa/qa/specs/features/project/deploy_key_clone_spec.rb b/qa/qa/specs/features/project/deploy_key_clone_spec.rb
index bf8fa230244..442ac312b4d 100644
--- a/qa/qa/specs/features/project/deploy_key_clone_spec.rb
+++ b/qa/qa/specs/features/project/deploy_key_clone_spec.rb
@@ -33,13 +33,15 @@ module QA
end
keys = [
- Runtime::Key::RSA.new(8192),
- Runtime::Key::ECDSA.new(521),
- Runtime::Key::ED25519.new
+ [Runtime::Key::RSA, 8192],
+ [Runtime::Key::ECDSA, 521],
+ [Runtime::Key::ED25519]
]
- keys.each do |key|
- scenario "user sets up a deploy key with #{key.name}(#{key.bits}) to clone code using pipelines" do
+ keys.each do |(key_class, bits)|
+ scenario "user sets up a deploy key with #{key_class}(#{bits}) to clone code using pipelines" do
+ key = key_class.new(*bits)
+
login
Factory::Resource::DeployKey.fabricate! do |resource|
diff --git a/scripts/no-ee-check b/scripts/no-ee-check
new file mode 100755
index 00000000000..29d319dc822
--- /dev/null
+++ b/scripts/no-ee-check
@@ -0,0 +1,7 @@
+#!/usr/bin/env ruby
+ee_path = File.join(File.expand_path(__dir__), '../ee')
+
+if Dir.exist?(ee_path)
+ puts 'The repository contains /ee directory. There should be no /ee directory in CE repo.'
+ exit 1
+end
diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh
index d8bcc9f8191..75a3cea0448 100644
--- a/scripts/prepare_build.sh
+++ b/scripts/prepare_build.sh
@@ -11,7 +11,7 @@ fi
# Only install knapsack after bundle install! Otherwise oddly some native
# gems could not be found under some circumstance. No idea why, hours wasted.
-retry gem install knapsack
+retry gem install knapsack --no-ri --no-rdoc
cp config/gitlab.yml.example config/gitlab.yml
sed -i 's/bin_path: \/usr\/bin\/git/bin_path: \/usr\/local\/bin\/git/' config/gitlab.yml
diff --git a/scripts/trigger-build-cloud-native b/scripts/trigger-build-cloud-native
new file mode 100755
index 00000000000..b6ca75a588d
--- /dev/null
+++ b/scripts/trigger-build-cloud-native
@@ -0,0 +1,61 @@
+#!/usr/bin/env ruby
+
+require 'gitlab'
+
+#
+# Configure credentials to be used with gitlab gem
+#
+Gitlab.configure do |config|
+ config.endpoint = 'https://gitlab.com/api/v4'
+end
+
+#
+# The remote project
+#
+GITLAB_CNG_REPO = 'gitlab-org/build/CNG'.freeze
+
+def ee?
+ ENV['CI_PROJECT_NAME'] == 'gitlab-ee' || File.exist?('CHANGELOG-EE.md')
+end
+
+def read_file_version(filename)
+ raw_version = File.read(filename).strip
+
+ # if the version matches semver format, treat it as a tag and prepend `v`
+ if raw_version =~ Regexp.compile(/^\d+\.\d+\.\d+(-rc\d+)?(-ee)?$/)
+ "v#{raw_version}"
+ else
+ raw_version
+ end
+end
+
+def params
+ params = {
+ 'GITLAB_SHELL_VERSION' => read_file_version('GITLAB_SHELL_VERSION'),
+ 'GITALY_VERSION' => read_file_version('GITALY_SERVER_VERSION'),
+ 'TRIGGERED_USER' => ENV['GITLAB_USER_NAME'],
+ 'TRIGGER_SOURCE' => "https://gitlab.com/gitlab-org/#{ENV['CI_PROJECT_NAME']}/-/jobs/#{ENV['CI_JOB_ID']}"
+ }
+
+ if ee?
+ params['EE_PIPELINE'] = 'true'
+ params['GITLAB_EE_VERSION'] = ENV['CI_COMMIT_REF_NAME']
+ else
+ params['CE_PIPELINE'] = 'true'
+ params['GITLAB_CE_VERSION'] = ENV['CI_COMMIT_REF_NAME']
+ end
+
+ params
+end
+
+#
+# Trigger a pipeline
+#
+def trigger_pipeline
+ # Create the cross project pipeline using CI_JOB_TOKEN
+ pipeline = Gitlab.run_trigger(GITLAB_CNG_REPO, ENV['CI_JOB_TOKEN'], 'master', params)
+
+ puts "Triggered https://gitlab.com/#{GITLAB_CNG_REPO}/pipelines/#{pipeline.id}"
+end
+
+trigger_pipeline
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index a451bbb97b6..9e7bc20a6d1 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -35,10 +35,16 @@ describe Projects::PipelinesController do
expect(json_response).to include('pipelines')
expect(json_response['pipelines'].count).to eq 4
- expect(json_response['count']['all']).to eq 4
- expect(json_response['count']['running']).to eq 1
- expect(json_response['count']['pending']).to eq 1
- expect(json_response['count']['finished']).to eq 1
+ expect(json_response['count']['all']).to eq '4'
+ expect(json_response['count']['running']).to eq '1'
+ expect(json_response['count']['pending']).to eq '1'
+ expect(json_response['count']['finished']).to eq '1'
+ end
+
+ it 'does not include coverage data for the pipelines' do
+ subject
+
+ expect(json_response['pipelines'][0]).not_to include('coverage')
end
context 'when performing gitaly calls', :request_store do
diff --git a/spec/controllers/projects/prometheus/metrics_controller_spec.rb b/spec/controllers/projects/prometheus/metrics_controller_spec.rb
index b2b245dba90..871dcf5c796 100644
--- a/spec/controllers/projects/prometheus/metrics_controller_spec.rb
+++ b/spec/controllers/projects/prometheus/metrics_controller_spec.rb
@@ -12,44 +12,67 @@ describe Projects::Prometheus::MetricsController do
end
describe 'GET #active_common' do
- before do
- allow(controller).to receive(:prometheus_adapter).and_return(prometheus_adapter)
- end
+ context 'when prometheus_adapter can query' do
+ before do
+ allow(controller).to receive(:prometheus_adapter).and_return(prometheus_adapter)
+ end
- context 'when prometheus metrics are enabled' do
- context 'when data is not present' do
- before do
- allow(prometheus_adapter).to receive(:query).with(:matched_metrics).and_return({})
- end
+ context 'when prometheus metrics are enabled' do
+ context 'when data is not present' do
+ before do
+ allow(prometheus_adapter).to receive(:query).with(:matched_metrics).and_return({})
+ end
- it 'returns no content response' do
- get :active_common, project_params(format: :json)
+ it 'returns no content response' do
+ get :active_common, project_params(format: :json)
- expect(response).to have_gitlab_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
+ end
end
- end
- context 'when data is available' do
- let(:sample_response) { { some_data: 1 } }
+ context 'when data is available' do
+ let(:sample_response) { { some_data: 1 } }
+
+ before do
+ allow(prometheus_adapter).to receive(:query).with(:matched_metrics).and_return(sample_response)
+ end
- before do
- allow(prometheus_adapter).to receive(:query).with(:matched_metrics).and_return(sample_response)
+ it 'returns no content response' do
+ get :active_common, project_params(format: :json)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to eq(sample_response.deep_stringify_keys)
+ end
end
- it 'returns no content response' do
- get :active_common, project_params(format: :json)
+ context 'when requesting non json response' do
+ it 'returns not found response' do
+ get :active_common, project_params
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to eq(sample_response.deep_stringify_keys)
+ expect(response).to have_gitlab_http_status(404)
+ end
end
end
+ end
- context 'when requesting non json response' do
- it 'returns not found response' do
- get :active_common, project_params
+ context 'when prometheus_adapter cannot query' do
+ it 'renders 404' do
+ prometheus_adapter = double('prometheus_adapter', can_query?: false)
- expect(response).to have_gitlab_http_status(404)
- end
+ allow(controller).to receive(:prometheus_adapter).and_return(prometheus_adapter)
+ allow(prometheus_adapter).to receive(:query).with(:matched_metrics).and_return({})
+
+ get :active_common, project_params(format: :json)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when prometheus_adapter is disabled' do
+ it 'renders 404' do
+ get :active_common, project_params(format: :json)
+
+ expect(response).to have_gitlab_http_status(404)
end
end
end
diff --git a/spec/factories/gitaly/tag.rb b/spec/factories/gitaly/tag.rb
new file mode 100644
index 00000000000..e99776d524a
--- /dev/null
+++ b/spec/factories/gitaly/tag.rb
@@ -0,0 +1,9 @@
+FactoryBot.define do
+ factory :gitaly_tag, class: Gitaly::Tag do
+ skip_create
+
+ name { 'v3.1.4' }
+ message { 'Pie release' }
+ target_commit factory: :gitaly_commit
+ end
+end
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index f2f9b734c39..dc025d82937 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -152,7 +152,7 @@ feature 'Admin updates settings' do
scenario 'Change CI/CD settings' do
page.within('.as-ci-cd') do
- check 'Enabled Auto DevOps (Beta) for projects by default'
+ check 'Enabled Auto DevOps for projects by default'
fill_in 'Auto devops domain', with: 'domain.com'
click_button 'Save changes'
end
diff --git a/spec/features/merge_request/user_resolves_conflicts_spec.rb b/spec/features/merge_request/user_resolves_conflicts_spec.rb
index 19995559fae..25a57f7d379 100644
--- a/spec/features/merge_request/user_resolves_conflicts_spec.rb
+++ b/spec/features/merge_request/user_resolves_conflicts_spec.rb
@@ -27,7 +27,7 @@ describe 'Merge request > User resolves conflicts', :js do
end
end
- find_button('Commit conflict resolution').send_keys(:return)
+ find_button('Commit to source branch').send_keys(:return)
expect(page).to have_content('All merge conflicts were resolved')
merge_request.reload_diff
@@ -71,7 +71,7 @@ describe 'Merge request > User resolves conflicts', :js do
execute_script('ace.edit($(".files-wrapper .diff-file pre")[1]).setValue("Gregor Samsa woke from troubled dreams");')
end
- find_button('Commit conflict resolution').send_keys(:return)
+ find_button('Commit to source branch').send_keys(:return)
expect(page).to have_content('All merge conflicts were resolved')
merge_request.reload_diff
@@ -145,7 +145,7 @@ describe 'Merge request > User resolves conflicts', :js do
execute_script('ace.edit($(".files-wrapper .diff-file pre")[0]).setValue("Gregor Samsa woke from troubled dreams");')
end
- click_button 'Commit conflict resolution'
+ click_button 'Commit to source branch'
expect(page).to have_content('All merge conflicts were resolved')
diff --git a/spec/features/projects/actve_tabs_spec.rb b/spec/features/projects/actve_tabs_spec.rb
index 0bda68b83e7..ce5606b63ae 100644
--- a/spec/features/projects/actve_tabs_spec.rb
+++ b/spec/features/projects/actve_tabs_spec.rb
@@ -35,7 +35,7 @@ describe 'Project active tab' do
visit project_path(project)
end
- it_behaves_like 'page has active tab', 'Overview'
+ it_behaves_like 'page has active tab', 'Project'
it_behaves_like 'page has active sub tab', 'Details'
context 'on project Home/Activity' do
@@ -43,7 +43,7 @@ describe 'Project active tab' do
click_tab('Activity')
end
- it_behaves_like 'page has active tab', 'Overview'
+ it_behaves_like 'page has active tab', 'Project'
it_behaves_like 'page has active sub tab', 'Activity'
end
end
diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb
index ac82f869f0f..e7b305925f7 100644
--- a/spec/features/projects/blobs/blob_show_spec.rb
+++ b/spec/features/projects/blobs/blob_show_spec.rb
@@ -14,6 +14,8 @@ feature 'File blob', :js do
context 'Ruby file' do
before do
visit_blob('files/ruby/popen.rb')
+
+ wait_for_requests
end
it 'displays the blob' do
@@ -48,6 +50,8 @@ feature 'File blob', :js do
context 'visiting directly' do
before do
visit_blob('files/markdown/ruby-style-guide.md')
+
+ wait_for_requests
end
it 'displays the blob using the rich viewer' do
@@ -159,6 +163,8 @@ feature 'File blob', :js do
project.update_attribute(:lfs_enabled, true)
visit_blob('files/lfs/file.md')
+
+ wait_for_requests
end
it 'displays an error' do
@@ -207,6 +213,8 @@ feature 'File blob', :js do
context 'when LFS is disabled on the project' do
before do
visit_blob('files/lfs/file.md')
+
+ wait_for_requests
end
it 'displays the blob' do
@@ -242,6 +250,8 @@ feature 'File blob', :js do
).execute
visit_blob('files/test.pdf')
+
+ wait_for_requests
end
it 'displays the blob' do
@@ -268,6 +278,8 @@ feature 'File blob', :js do
project.update_attribute(:lfs_enabled, true)
visit_blob('files/lfs/lfs_object.iso')
+
+ wait_for_requests
end
it 'displays the blob' do
@@ -290,6 +302,8 @@ feature 'File blob', :js do
context 'when LFS is disabled on the project' do
before do
visit_blob('files/lfs/lfs_object.iso')
+
+ wait_for_requests
end
it 'displays the blob' do
@@ -313,6 +327,8 @@ feature 'File blob', :js do
context 'ZIP file' do
before do
visit_blob('Gemfile.zip')
+
+ wait_for_requests
end
it 'displays the blob' do
@@ -347,6 +363,8 @@ feature 'File blob', :js do
).execute
visit_blob('files/empty.md')
+
+ wait_for_requests
end
it 'displays an error' do
diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
index 5248a783db4..f9defa22d35 100644
--- a/spec/features/projects/environments/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -42,6 +42,22 @@ feature 'Environments page', :js do
expect(page).to have_content('You don\'t have any environments right now')
end
end
+
+ context 'when cluster is not reachable' do
+ let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+ let!(:application_prometheus) { create(:clusters_applications_prometheus, :installed, cluster: cluster) }
+
+ before do
+ allow_any_instance_of(Kubeclient::Client).to receive(:proxy_url).and_raise(Kubeclient::HttpError.new(401, 'Unauthorized', nil))
+ end
+
+ it 'should show one environment without error' do
+ visit_environments(project, scope: 'available')
+
+ expect(page).to have_css('.environments-container')
+ expect(page.all('.environment-name').length).to eq(1)
+ end
+ end
end
describe 'with one stopped environment' do
diff --git a/spec/features/projects/user_sees_sidebar_spec.rb b/spec/features/projects/user_sees_sidebar_spec.rb
index cf80517b934..8a9255db9e8 100644
--- a/spec/features/projects/user_sees_sidebar_spec.rb
+++ b/spec/features/projects/user_sees_sidebar_spec.rb
@@ -37,7 +37,7 @@ describe 'Projects > User sees sidebar' do
visit project_path(project)
within('.nav-sidebar') do
- expect(page).to have_content 'Overview'
+ expect(page).to have_content 'Project'
expect(page).to have_content 'Issues'
expect(page).to have_content 'Wiki'
diff --git a/spec/features/projects/user_uses_shortcuts_spec.rb b/spec/features/projects/user_uses_shortcuts_spec.rb
index 47c5a8161d9..495a010b32c 100644
--- a/spec/features/projects/user_uses_shortcuts_spec.rb
+++ b/spec/features/projects/user_uses_shortcuts_spec.rb
@@ -13,6 +13,8 @@ describe 'User uses shortcuts', :js do
context 'when navigating to the Project pages' do
it 'redirects to the details page' do
+ visit project_issues_path(project)
+
find('body').native.send_key('g')
find('body').native.send_key('p')
@@ -22,7 +24,7 @@ describe 'User uses shortcuts', :js do
it 'redirects to the activity page' do
find('body').native.send_key('g')
- find('body').native.send_key('e')
+ find('body').native.send_key('v')
expect(page).to have_active_navigation('Project')
expect(page).to have_active_sub_navigation('Activity')
@@ -72,10 +74,19 @@ describe 'User uses shortcuts', :js do
expect(page).to have_active_sub_navigation('List')
end
+ it 'redirects to the issue board page' do
+ find('body').native.send_key('g')
+ find('body').native.send_key('b')
+
+ expect(page).to have_active_navigation('Issues')
+ expect(page).to have_active_sub_navigation('Board')
+ end
+
it 'redirects to the new issue page' do
find('body').native.send_key('i')
expect(page).to have_content(project.title)
+ expect(page).to have_content('New Issue')
end
end
@@ -88,6 +99,34 @@ describe 'User uses shortcuts', :js do
end
end
+ context 'when navigating to the CI / CD pages' do
+ it 'redirects to the Jobs page' do
+ find('body').native.send_key('g')
+ find('body').native.send_key('j')
+
+ expect(page).to have_active_navigation('CI / CD')
+ expect(page).to have_active_sub_navigation('Jobs')
+ end
+ end
+
+ context 'when navigating to the Operations pages' do
+ it 'redirects to the Environments page' do
+ find('body').native.send_key('g')
+ find('body').native.send_key('e')
+
+ expect(page).to have_active_navigation('Operations')
+ expect(page).to have_active_sub_navigation('Environments')
+ end
+
+ it 'redirects to the Kubernetes page' do
+ find('body').native.send_key('g')
+ find('body').native.send_key('k')
+
+ expect(page).to have_active_navigation('Operations')
+ expect(page).to have_active_sub_navigation('Kubernetes')
+ end
+ end
+
context 'when navigating to the Snippets pages' do
it 'redirects to the snippets page' do
find('body').native.send_key('g')
diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb
index 6586ccaa400..e473739a6aa 100644
--- a/spec/features/projects/wiki/markdown_preview_spec.rb
+++ b/spec/features/projects/wiki/markdown_preview_spec.rb
@@ -155,4 +155,27 @@ feature 'Projects > Wiki > User previews markdown changes', :js do
end
end
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
+
+ page.within '.wiki-form' do
+ fill_in :wiki_content, with: <<-HEREDOC
+ `[[do_not_linkify]]`
+ ```
+ [[also_do_not_linkify]]
+ ```
+ HEREDOC
+ click_on "Preview"
+ end
+
+ 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
end
diff --git a/spec/features/users/user_browses_projects_on_user_page_spec.rb b/spec/features/users/user_browses_projects_on_user_page_spec.rb
index a70637c8370..7bede0b0d48 100644
--- a/spec/features/users/user_browses_projects_on_user_page_spec.rb
+++ b/spec/features/users/user_browses_projects_on_user_page_spec.rb
@@ -27,8 +27,8 @@ describe 'Users > User browses projects on user page', :js do
end
it 'paginates projects', :js do
- project = create(:project, namespace: user.namespace)
- project2 = create(:project, namespace: user.namespace)
+ project = create(:project, namespace: user.namespace, updated_at: 2.minutes.since)
+ project2 = create(:project, namespace: user.namespace, updated_at: 1.minute.since)
allow(Project).to receive(:default_per_page).and_return(1)
sign_in(user)
@@ -41,11 +41,11 @@ describe 'Users > User browses projects on user page', :js do
wait_for_requests
- expect(page).to have_content(project2.name)
+ expect(page).to have_content(project.name)
click_link('Next')
- expect(page).to have_content(project.name)
+ expect(page).to have_content(project2.name)
end
context 'when not signed in' do
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index 45439640ea3..74e91b02f0f 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -372,7 +372,7 @@ describe IssuesFinder do
end
context 'personal scope' do
- let(:scope) { 'assigned-to-me' }
+ let(:scope) { 'assigned_to_me' }
it 'returns issue assigned to the user' do
expect(issues).to contain_exactly(issue1, issue2)
diff --git a/spec/finders/personal_projects_finder_spec.rb b/spec/finders/personal_projects_finder_spec.rb
index 5e52898e9c0..00c551a1f65 100644
--- a/spec/finders/personal_projects_finder_spec.rb
+++ b/spec/finders/personal_projects_finder_spec.rb
@@ -4,14 +4,16 @@ describe PersonalProjectsFinder do
let(:source_user) { create(:user) }
let(:current_user) { create(:user) }
let(:finder) { described_class.new(source_user) }
- let!(:public_project) { create(:project, :public, namespace: source_user.namespace) }
+ let!(:public_project) do
+ create(:project, :public, namespace: source_user.namespace, updated_at: 1.hour.ago)
+ end
let!(:private_project) do
- create(:project, :private, namespace: source_user.namespace, path: 'mepmep')
+ create(:project, :private, namespace: source_user.namespace, updated_at: 3.hours.ago, path: 'mepmep')
end
let!(:internal_project) do
- create(:project, :internal, namespace: source_user.namespace, path: 'C')
+ create(:project, :internal, namespace: source_user.namespace, updated_at: 2.hours.ago, path: 'C')
end
before do
@@ -28,7 +30,7 @@ describe PersonalProjectsFinder do
subject { finder.execute(current_user) }
context 'normal user' do
- it { is_expected.to eq([internal_project, private_project, public_project]) }
+ it { is_expected.to eq([public_project, internal_project, private_project]) }
end
context 'external' do
@@ -36,7 +38,7 @@ describe PersonalProjectsFinder do
current_user.update_attributes(external: true)
end
- it { is_expected.to eq([private_project, public_project]) }
+ it { is_expected.to eq([public_project, private_project]) }
end
end
end
diff --git a/spec/fixtures/api/schemas/public_api/v4/projects.json b/spec/fixtures/api/schemas/public_api/v4/projects.json
index d89eeea89a5..17ad8d8c48d 100644
--- a/spec/fixtures/api/schemas/public_api/v4/projects.json
+++ b/spec/fixtures/api/schemas/public_api/v4/projects.json
@@ -20,6 +20,7 @@
"ssh_url_to_repo": { "type": "string" },
"http_url_to_repo": { "type": "string" },
"web_url": { "type": "string" },
+ "readme_url": { "type": ["string", "null"] },
"avatar_url": { "type": ["string", "null"] },
"star_count": { "type": "integer" },
"forks_count": { "type": "integer" },
diff --git a/spec/fixtures/emails/valid_new_issue_with_quote.eml b/spec/fixtures/emails/valid_new_issue_with_quote.eml
new file mode 100644
index 00000000000..0caf8ed4e9e
--- /dev/null
+++ b/spec/fixtures/emails/valid_new_issue_with_quote.eml
@@ -0,0 +1,25 @@
+Return-Path: <jake@adventuretime.ooo>
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog <jake@adventuretime.ooo>
+To: incoming+gitlabhq/gitlabhq+auth_token@appmail.adventuretime.ooo
+Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+Subject: New Issue by email
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
+
+The reply by email functionality should be extended to allow creating a new issue by email.
+even when the email is forwarded to the project which may include lines that begin with ">"
+
+there should be a quote below this line:
+
+> this is a quote \ No newline at end of file
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index 8de615ad8c2..a3e010c3206 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -258,13 +258,19 @@ describe BlobHelper do
it 'returns full IDE path' do
Rails.application.routes.default_url_options[:script_name] = nil
- expect(helper.ide_edit_path(project, "master", "")).to eq("/-/ide/project/#{project.namespace.path}/#{project.path}/edit/master/")
+ expect(helper.ide_edit_path(project, "master", "")).to eq("/-/ide/project/#{project.namespace.path}/#{project.path}/edit/master")
+ end
+
+ it 'returns full IDE path with second -' do
+ Rails.application.routes.default_url_options[:script_name] = nil
+
+ expect(helper.ide_edit_path(project, "testing/slashes", "readme.md")).to eq("/-/ide/project/#{project.namespace.path}/#{project.path}/edit/testing/slashes/-/readme.md")
end
it 'returns IDE path without relative_url_root' do
Rails.application.routes.default_url_options[:script_name] = "/gitlab"
- expect(helper.ide_edit_path(project, "master", "")).to eq("/gitlab/-/ide/project/#{project.namespace.path}/#{project.path}/edit/master/")
+ expect(helper.ide_edit_path(project, "master", "")).to eq("/gitlab/-/ide/project/#{project.namespace.path}/#{project.path}/edit/master")
end
end
end
diff --git a/spec/javascripts/environments/environments_app_spec.js b/spec/javascripts/environments/environments_app_spec.js
index e4c3bf2bef1..615168645b4 100644
--- a/spec/javascripts/environments/environments_app_spec.js
+++ b/spec/javascripts/environments/environments_app_spec.js
@@ -1,8 +1,8 @@
-import _ from 'underscore';
import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import environmentsComponent from '~/environments/components/environments_app.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { headersInterceptor } from 'spec/helpers/vue_resource_helper';
import { environment, folder } from './mock_data';
describe('Environment', () => {
@@ -18,103 +18,76 @@ describe('Environment', () => {
let EnvironmentsComponent;
let component;
+ let mock;
beforeEach(() => {
+ mock = new MockAdapter(axios);
+
EnvironmentsComponent = Vue.extend(environmentsComponent);
});
+ afterEach(() => {
+ component.$destroy();
+ mock.restore();
+ });
+
describe('successfull request', () => {
describe('without environments', () => {
- const environmentsEmptyResponseInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([]), {
- status: 200,
- }));
- };
-
- beforeEach(() => {
- Vue.http.interceptors.push(environmentsEmptyResponseInterceptor);
- Vue.http.interceptors.push(headersInterceptor);
- });
-
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors, environmentsEmptyResponseInterceptor,
- );
- Vue.http.interceptors = _.without(Vue.http.interceptors, headersInterceptor);
- });
+ beforeEach((done) => {
+ mock.onGet(mockData.endpoint).reply(200, { environments: [] });
- it('should render the empty state', (done) => {
component = mountComponent(EnvironmentsComponent, mockData);
setTimeout(() => {
- expect(
- component.$el.querySelector('.js-new-environment-button').textContent,
- ).toContain('New environment');
-
- expect(
- component.$el.querySelector('.js-blank-state-title').textContent,
- ).toContain('You don\'t have any environments right now.');
-
done();
}, 0);
});
+
+ it('should render the empty state', () => {
+ expect(
+ component.$el.querySelector('.js-new-environment-button').textContent,
+ ).toContain('New environment');
+
+ expect(
+ component.$el.querySelector('.js-blank-state-title').textContent,
+ ).toContain('You don\'t have any environments right now.');
+ });
});
describe('with paginated environments', () => {
- let backupInterceptors;
- const environmentsResponseInterceptor = (request, next) => {
- next((response) => {
- response.headers.set('X-nExt-pAge', '2');
- });
-
- next(request.respondWith(JSON.stringify({
+ beforeEach((done) => {
+ mock.onGet(mockData.endpoint).reply(200, {
environments: [environment],
stopped_count: 1,
available_count: 0,
- }), {
- status: 200,
- headers: {
- 'X-nExt-pAge': '2',
- 'x-page': '1',
- 'X-Per-Page': '1',
- 'X-Prev-Page': '',
- 'X-TOTAL': '37',
- 'X-Total-Pages': '2',
- },
- }));
- };
-
- beforeEach(() => {
- backupInterceptors = Vue.http.interceptors;
- Vue.http.interceptors = [
- environmentsResponseInterceptor,
- headersInterceptor,
- ];
- component = mountComponent(EnvironmentsComponent, mockData);
- });
+ }, {
+ 'X-nExt-pAge': '2',
+ 'x-page': '1',
+ 'X-Per-Page': '1',
+ 'X-Prev-Page': '',
+ 'X-TOTAL': '37',
+ 'X-Total-Pages': '2',
+ });
- afterEach(() => {
- Vue.http.interceptors = backupInterceptors;
- });
+ component = mountComponent(EnvironmentsComponent, mockData);
- it('should render a table with environments', (done) => {
setTimeout(() => {
- expect(component.$el.querySelectorAll('table')).not.toBeNull();
- expect(
- component.$el.querySelector('.environment-name').textContent.trim(),
- ).toEqual(environment.name);
done();
}, 0);
});
+ it('should render a table with environments', () => {
+ expect(component.$el.querySelectorAll('table')).not.toBeNull();
+ expect(
+ component.$el.querySelector('.environment-name').textContent.trim(),
+ ).toEqual(environment.name);
+ });
+
describe('pagination', () => {
- it('should render pagination', (done) => {
- setTimeout(() => {
- expect(
- component.$el.querySelectorAll('.gl-pagination li').length,
- ).toEqual(5);
- done();
- }, 0);
+ it('should render pagination', () => {
+ expect(
+ component.$el.querySelectorAll('.gl-pagination li').length,
+ ).toEqual(5);
});
it('should make an API request when page is clicked', (done) => {
@@ -133,50 +106,39 @@ describe('Environment', () => {
expect(component.updateContent).toHaveBeenCalledWith({ scope: 'stopped', page: '1' });
done();
- });
+ }, 0);
});
});
});
});
describe('unsuccessfull request', () => {
- const environmentsErrorResponseInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([]), {
- status: 500,
- }));
- };
-
- beforeEach(() => {
- Vue.http.interceptors.push(environmentsErrorResponseInterceptor);
- });
+ beforeEach((done) => {
+ mock.onGet(mockData.endpoint).reply(500, {});
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors, environmentsErrorResponseInterceptor,
- );
- });
-
- it('should render empty state', (done) => {
component = mountComponent(EnvironmentsComponent, mockData);
setTimeout(() => {
- expect(
- component.$el.querySelector('.js-blank-state-title').textContent,
- ).toContain('You don\'t have any environments right now.');
done();
}, 0);
});
+
+ it('should render empty state', () => {
+ expect(
+ component.$el.querySelector('.js-blank-state-title').textContent,
+ ).toContain('You don\'t have any environments right now.');
+ });
});
describe('expandable folders', () => {
- const environmentsResponseInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify({
- environments: [folder],
- stopped_count: 0,
- available_count: 1,
- }), {
- status: 200,
- headers: {
+ beforeEach(() => {
+ mock.onGet(mockData.endpoint).reply(200,
+ {
+ environments: [folder],
+ stopped_count: 0,
+ available_count: 1,
+ },
+ {
'X-nExt-pAge': '2',
'x-page': '1',
'X-Per-Page': '1',
@@ -184,18 +146,11 @@ describe('Environment', () => {
'X-TOTAL': '37',
'X-Total-Pages': '2',
},
- }));
- };
+ );
- beforeEach(() => {
- Vue.http.interceptors.push(environmentsResponseInterceptor);
- component = mountComponent(EnvironmentsComponent, mockData);
- });
+ mock.onGet(environment.folder_path).reply(200, { environments: [environment] });
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors, environmentsResponseInterceptor,
- );
+ component = mountComponent(EnvironmentsComponent, mockData);
});
it('should open a closed folder', (done) => {
@@ -211,7 +166,7 @@ describe('Environment', () => {
).not.toContain('display: none');
done();
});
- });
+ }, 0);
});
it('should close an opened folder', (done) => {
@@ -233,7 +188,7 @@ describe('Environment', () => {
done();
});
});
- });
+ }, 0);
});
it('should show children environments and a button to show all environments', (done) => {
@@ -242,49 +197,32 @@ describe('Environment', () => {
component.$el.querySelector('.folder-name').click();
Vue.nextTick(() => {
- const folderInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify({
- environments: [environment],
- }), { status: 200 }));
- };
-
- Vue.http.interceptors.push(folderInterceptor);
-
// wait for next async request
setTimeout(() => {
expect(component.$el.querySelectorAll('.js-child-row').length).toEqual(1);
expect(component.$el.querySelector('.text-center > a.btn').textContent).toContain('Show all');
-
- Vue.http.interceptors = _.without(Vue.http.interceptors, folderInterceptor);
done();
});
});
- });
+ }, 0);
});
});
describe('methods', () => {
- const environmentsEmptyResponseInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([]), {
- status: 200,
- }));
- };
-
beforeEach(() => {
- Vue.http.interceptors.push(environmentsEmptyResponseInterceptor);
- Vue.http.interceptors.push(headersInterceptor);
+ mock.onGet(mockData.endpoint).reply(200,
+ {
+ environments: [],
+ stopped_count: 0,
+ available_count: 1,
+ },
+ {},
+ );
component = mountComponent(EnvironmentsComponent, mockData);
spyOn(history, 'pushState').and.stub();
});
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors, environmentsEmptyResponseInterceptor,
- );
- Vue.http.interceptors = _.without(Vue.http.interceptors, headersInterceptor);
- });
-
describe('updateContent', () => {
it('should set given parameters', (done) => {
component.updateContent({ scope: 'stopped', page: '3' })
diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js
index 906a1116974..f5ce4df0bfe 100644
--- a/spec/javascripts/environments/folder/environments_folder_view_spec.js
+++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js
@@ -1,13 +1,15 @@
-import _ from 'underscore';
import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import environmentsFolderViewComponent from '~/environments/folder/environments_folder_view.vue';
-import { headersInterceptor } from 'spec/helpers/vue_resource_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { environmentsList } from '../mock_data';
describe('Environments Folder View', () => {
let Component;
let component;
+ let mock;
+
const mockData = {
endpoint: 'environments.json',
folderName: 'review',
@@ -17,46 +19,35 @@ describe('Environments Folder View', () => {
};
beforeEach(() => {
+ mock = new MockAdapter(axios);
+
Component = Vue.extend(environmentsFolderViewComponent);
});
afterEach(() => {
+ mock.restore();
+
component.$destroy();
});
describe('successfull request', () => {
- const environmentsResponseInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify({
+ beforeEach(() => {
+ mock.onGet(mockData.endpoint).reply(200, {
environments: environmentsList,
stopped_count: 1,
available_count: 0,
- }), {
- status: 200,
- headers: {
- 'X-nExt-pAge': '2',
- 'x-page': '1',
- 'X-Per-Page': '2',
- 'X-Prev-Page': '',
- 'X-TOTAL': '20',
- 'X-Total-Pages': '10',
- },
- }));
- };
-
- beforeEach(() => {
- Vue.http.interceptors.push(environmentsResponseInterceptor);
- Vue.http.interceptors.push(headersInterceptor);
+ }, {
+ 'X-nExt-pAge': '2',
+ 'x-page': '1',
+ 'X-Per-Page': '2',
+ 'X-Prev-Page': '',
+ 'X-TOTAL': '20',
+ 'X-Total-Pages': '10',
+ });
component = mountComponent(Component, mockData);
});
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors, environmentsResponseInterceptor,
- );
- Vue.http.interceptors = _.without(Vue.http.interceptors, headersInterceptor);
- });
-
it('should render a table with environments', (done) => {
setTimeout(() => {
expect(component.$el.querySelectorAll('table')).not.toBeNull();
@@ -135,25 +126,15 @@ describe('Environments Folder View', () => {
});
describe('unsuccessfull request', () => {
- const environmentsErrorResponseInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([]), {
- status: 500,
- }));
- };
-
beforeEach(() => {
- Vue.http.interceptors.push(environmentsErrorResponseInterceptor);
- });
+ mock.onGet(mockData.endpoint).reply(500, {
+ environments: [],
+ });
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors, environmentsErrorResponseInterceptor,
- );
+ component = mountComponent(Component, mockData);
});
it('should not render a table', (done) => {
- component = mountComponent(Component, mockData);
-
setTimeout(() => {
expect(
component.$el.querySelector('table'),
@@ -190,27 +171,15 @@ describe('Environments Folder View', () => {
});
describe('methods', () => {
- const environmentsEmptyResponseInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([]), {
- status: 200,
- }));
- };
-
beforeEach(() => {
- Vue.http.interceptors.push(environmentsEmptyResponseInterceptor);
- Vue.http.interceptors.push(headersInterceptor);
+ mock.onGet(mockData.endpoint).reply(200, {
+ environments: [],
+ });
component = mountComponent(Component, mockData);
spyOn(history, 'pushState').and.stub();
});
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors, environmentsEmptyResponseInterceptor,
- );
- Vue.http.interceptors = _.without(Vue.http.interceptors, headersInterceptor);
- });
-
describe('updateContent', () => {
it('should set given parameters', (done) => {
component.updateContent({ scope: 'stopped', page: '4' })
diff --git a/spec/javascripts/environments/mock_data.js b/spec/javascripts/environments/mock_data.js
index 15e11aa686b..8a1e26935d9 100644
--- a/spec/javascripts/environments/mock_data.js
+++ b/spec/javascripts/environments/mock_data.js
@@ -82,6 +82,7 @@ export const environment = {
stop_path: '/root/review-app/environments/7/stop',
created_at: '2017-01-31T10:53:46.894Z',
updated_at: '2017-01-31T10:53:46.894Z',
+ folder_path: '/root/review-app/environments/7',
},
};
diff --git a/spec/javascripts/helpers/vuex_action_helper.js b/spec/javascripts/helpers/vuex_action_helper.js
index 83f29d1b0c2..d6ab0aeeed7 100644
--- a/spec/javascripts/helpers/vuex_action_helper.js
+++ b/spec/javascripts/helpers/vuex_action_helper.js
@@ -55,7 +55,7 @@ export default (action, payload, state, expectedMutations, expectedActions, done
};
// call the action with mocked store and arguments
- action({ commit, state, dispatch }, payload);
+ action({ commit, state, dispatch, rootState: state }, payload);
// check if no mutations should have been dispatched
if (expectedMutations.length === 0) {
diff --git a/spec/javascripts/ide/components/ide_spec.js b/spec/javascripts/ide/components/ide_spec.js
index 6f580e1f7af..045a60e56a0 100644
--- a/spec/javascripts/ide/components/ide_spec.js
+++ b/spec/javascripts/ide/components/ide_spec.js
@@ -107,5 +107,11 @@ describe('ide component', () => {
vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 't'),
).toBe(true);
});
+
+ it('stops callback in monaco editor', () => {
+ setFixtures('<div class="inputarea"></div>');
+
+ expect(vm.mousetrapStopCallback(null, document.querySelector('.inputarea'), 't')).toBe(true);
+ });
});
});
diff --git a/spec/javascripts/ide/components/repo_editor_spec.js b/spec/javascripts/ide/components/repo_editor_spec.js
index ff500acd849..d3f80e6f9c0 100644
--- a/spec/javascripts/ide/components/repo_editor_spec.js
+++ b/spec/javascripts/ide/components/repo_editor_spec.js
@@ -346,4 +346,24 @@ describe('RepoEditor', () => {
});
});
});
+
+ it('calls removePendingTab when old file is pending', done => {
+ spyOnProperty(vm, 'shouldHideEditor').and.returnValue(true);
+ spyOn(vm, 'removePendingTab');
+
+ vm.file.pending = true;
+
+ vm
+ .$nextTick()
+ .then(() => {
+ vm.file = file('testing');
+
+ return vm.$nextTick();
+ })
+ .then(() => {
+ expect(vm.removePendingTab).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
});
diff --git a/spec/javascripts/ide/mock_data.js b/spec/javascripts/ide/mock_data.js
index c059862b9d1..7e641c7984b 100644
--- a/spec/javascripts/ide/mock_data.js
+++ b/spec/javascripts/ide/mock_data.js
@@ -1,4 +1,3 @@
-// eslint-disable-next-line import/prefer-default-export
export const projectData = {
id: 1,
name: 'abcproject',
@@ -14,3 +13,49 @@ export const projectData = {
mergeRequests: {},
merge_requests_enabled: true,
};
+
+export const pipelines = [
+ {
+ id: 1,
+ ref: 'master',
+ sha: '123',
+ status: 'failed',
+ },
+ {
+ id: 2,
+ ref: 'master',
+ sha: '213',
+ status: 'success',
+ },
+];
+
+export const jobs = [
+ {
+ id: 1,
+ name: 'test',
+ status: 'failed',
+ stage: 'test',
+ duration: 1,
+ },
+ {
+ id: 2,
+ name: 'test 2',
+ status: 'failed',
+ stage: 'test',
+ duration: 1,
+ },
+ {
+ id: 3,
+ name: 'test 3',
+ status: 'failed',
+ stage: 'test',
+ duration: 1,
+ },
+ {
+ id: 4,
+ name: 'test 3',
+ status: 'failed',
+ stage: 'build',
+ duration: 1,
+ },
+];
diff --git a/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js b/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js
new file mode 100644
index 00000000000..85fbcf8084b
--- /dev/null
+++ b/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js
@@ -0,0 +1,289 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import actions, {
+ requestLatestPipeline,
+ receiveLatestPipelineError,
+ receiveLatestPipelineSuccess,
+ fetchLatestPipeline,
+ requestJobs,
+ receiveJobsError,
+ receiveJobsSuccess,
+ fetchJobs,
+} from '~/ide/stores/modules/pipelines/actions';
+import state from '~/ide/stores/modules/pipelines/state';
+import * as types from '~/ide/stores/modules/pipelines/mutation_types';
+import testAction from '../../../../helpers/vuex_action_helper';
+import { pipelines, jobs } from '../../../mock_data';
+
+describe('IDE pipelines actions', () => {
+ let mockedState;
+ let mock;
+
+ beforeEach(() => {
+ mockedState = state();
+ mock = new MockAdapter(axios);
+
+ gon.api_version = 'v4';
+ mockedState.currentProjectId = 'test/project';
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('requestLatestPipeline', () => {
+ it('commits request', done => {
+ testAction(
+ requestLatestPipeline,
+ null,
+ mockedState,
+ [{ type: types.REQUEST_LATEST_PIPELINE }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveLatestPipelineError', () => {
+ it('commits error', done => {
+ testAction(
+ receiveLatestPipelineError,
+ null,
+ mockedState,
+ [{ type: types.RECEIVE_LASTEST_PIPELINE_ERROR }],
+ [],
+ done,
+ );
+ });
+
+ it('creates flash message', () => {
+ const flashSpy = spyOnDependency(actions, 'flash');
+
+ receiveLatestPipelineError({ commit() {} });
+
+ expect(flashSpy).toHaveBeenCalled();
+ });
+ });
+
+ describe('receiveLatestPipelineSuccess', () => {
+ it('commits pipeline', done => {
+ testAction(
+ receiveLatestPipelineSuccess,
+ pipelines[0],
+ mockedState,
+ [{ type: types.RECEIVE_LASTEST_PIPELINE_SUCCESS, payload: pipelines[0] }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('fetchLatestPipeline', () => {
+ describe('success', () => {
+ beforeEach(() => {
+ mock.onGet(/\/api\/v4\/projects\/(.*)\/pipelines(.*)/).replyOnce(200, pipelines);
+ });
+
+ it('dispatches request', done => {
+ testAction(
+ fetchLatestPipeline,
+ '123',
+ mockedState,
+ [],
+ [{ type: 'requestLatestPipeline' }, { type: 'receiveLatestPipelineSuccess' }],
+ done,
+ );
+ });
+
+ it('dispatches success with latest pipeline', done => {
+ testAction(
+ fetchLatestPipeline,
+ '123',
+ mockedState,
+ [],
+ [
+ { type: 'requestLatestPipeline' },
+ { type: 'receiveLatestPipelineSuccess', payload: pipelines[0] },
+ ],
+ done,
+ );
+ });
+
+ it('calls axios with correct params', () => {
+ const apiSpy = spyOn(axios, 'get').and.callThrough();
+
+ fetchLatestPipeline({ dispatch() {}, rootState: state }, '123');
+
+ expect(apiSpy).toHaveBeenCalledWith(jasmine.anything(), {
+ params: {
+ sha: '123',
+ per_page: '1',
+ },
+ });
+ });
+ });
+
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(/\/api\/v4\/projects\/(.*)\/pipelines(.*)/).replyOnce(500);
+ });
+
+ it('dispatches error', done => {
+ testAction(
+ fetchLatestPipeline,
+ '123',
+ mockedState,
+ [],
+ [{ type: 'requestLatestPipeline' }, { type: 'receiveLatestPipelineError' }],
+ done,
+ );
+ });
+ });
+ });
+
+ describe('requestJobs', () => {
+ it('commits request', done => {
+ testAction(requestJobs, null, mockedState, [{ type: types.REQUEST_JOBS }], [], done);
+ });
+ });
+
+ describe('receiveJobsError', () => {
+ it('commits error', done => {
+ testAction(
+ receiveJobsError,
+ null,
+ mockedState,
+ [{ type: types.RECEIVE_JOBS_ERROR }],
+ [],
+ done,
+ );
+ });
+
+ it('creates flash message', () => {
+ const flashSpy = spyOnDependency(actions, 'flash');
+
+ receiveJobsError({ commit() {} });
+
+ expect(flashSpy).toHaveBeenCalled();
+ });
+ });
+
+ describe('receiveJobsSuccess', () => {
+ it('commits jobs', done => {
+ testAction(
+ receiveJobsSuccess,
+ jobs,
+ mockedState,
+ [{ type: types.RECEIVE_JOBS_SUCCESS, payload: jobs }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('fetchJobs', () => {
+ let page = '';
+
+ beforeEach(() => {
+ mockedState.latestPipeline = pipelines[0];
+ });
+
+ describe('success', () => {
+ beforeEach(() => {
+ mock.onGet(/\/api\/v4\/projects\/(.*)\/pipelines\/(.*)\/jobs/).replyOnce(() => [
+ 200,
+ jobs,
+ {
+ 'x-next-page': page,
+ },
+ ]);
+ });
+
+ it('dispatches request', done => {
+ testAction(
+ fetchJobs,
+ null,
+ mockedState,
+ [],
+ [{ type: 'requestJobs' }, { type: 'receiveJobsSuccess' }],
+ done,
+ );
+ });
+
+ it('dispatches success with latest pipeline', done => {
+ testAction(
+ fetchJobs,
+ null,
+ mockedState,
+ [],
+ [{ type: 'requestJobs' }, { type: 'receiveJobsSuccess', payload: jobs }],
+ done,
+ );
+ });
+
+ it('dispatches twice for both pages', done => {
+ page = '2';
+
+ testAction(
+ fetchJobs,
+ null,
+ mockedState,
+ [],
+ [
+ { type: 'requestJobs' },
+ { type: 'receiveJobsSuccess', payload: jobs },
+ { type: 'fetchJobs', payload: '2' },
+ { type: 'requestJobs' },
+ { type: 'receiveJobsSuccess', payload: jobs },
+ ],
+ done,
+ );
+ });
+
+ it('calls axios with correct URL', () => {
+ const apiSpy = spyOn(axios, 'get').and.callThrough();
+
+ fetchJobs({ dispatch() {}, state: mockedState, rootState: mockedState });
+
+ expect(apiSpy).toHaveBeenCalledWith('/api/v4/projects/test%2Fproject/pipelines/1/jobs', {
+ params: { page: '1' },
+ });
+ });
+
+ it('calls axios with page next page', () => {
+ const apiSpy = spyOn(axios, 'get').and.callThrough();
+
+ fetchJobs({ dispatch() {}, state: mockedState, rootState: mockedState });
+
+ expect(apiSpy).toHaveBeenCalledWith('/api/v4/projects/test%2Fproject/pipelines/1/jobs', {
+ params: { page: '1' },
+ });
+
+ page = '2';
+
+ fetchJobs({ dispatch() {}, state: mockedState, rootState: mockedState }, page);
+
+ expect(apiSpy).toHaveBeenCalledWith('/api/v4/projects/test%2Fproject/pipelines/1/jobs', {
+ params: { page: '2' },
+ });
+ });
+ });
+
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(/\/api\/v4\/projects\/(.*)\/pipelines(.*)/).replyOnce(500);
+ });
+
+ it('dispatches error', done => {
+ testAction(
+ fetchJobs,
+ null,
+ mockedState,
+ [],
+ [{ type: 'requestJobs' }, { type: 'receiveJobsError' }],
+ done,
+ );
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/modules/pipelines/getters_spec.js b/spec/javascripts/ide/stores/modules/pipelines/getters_spec.js
new file mode 100644
index 00000000000..b2a7e8a9025
--- /dev/null
+++ b/spec/javascripts/ide/stores/modules/pipelines/getters_spec.js
@@ -0,0 +1,71 @@
+import * as getters from '~/ide/stores/modules/pipelines/getters';
+import state from '~/ide/stores/modules/pipelines/state';
+
+describe('IDE pipeline getters', () => {
+ let mockedState;
+
+ beforeEach(() => {
+ mockedState = state();
+ });
+
+ describe('hasLatestPipeline', () => {
+ it('returns false when loading is true', () => {
+ mockedState.isLoadingPipeline = true;
+
+ expect(getters.hasLatestPipeline(mockedState)).toBe(false);
+ });
+
+ it('returns false when pipelines is null', () => {
+ mockedState.latestPipeline = null;
+
+ expect(getters.hasLatestPipeline(mockedState)).toBe(false);
+ });
+
+ it('returns false when loading is true & pipelines is null', () => {
+ mockedState.latestPipeline = null;
+ mockedState.isLoadingPipeline = true;
+
+ expect(getters.hasLatestPipeline(mockedState)).toBe(false);
+ });
+
+ it('returns true when loading is false & pipelines is an object', () => {
+ mockedState.latestPipeline = {
+ id: 1,
+ };
+ mockedState.isLoadingPipeline = false;
+
+ expect(getters.hasLatestPipeline(mockedState)).toBe(true);
+ });
+ });
+
+ describe('failedJobs', () => {
+ it('returns array of failed jobs', () => {
+ mockedState.stages = [
+ {
+ title: 'test',
+ jobs: [{ id: 1, status: 'failed' }, { id: 2, status: 'success' }],
+ },
+ {
+ title: 'build',
+ jobs: [{ id: 3, status: 'failed' }, { id: 4, status: 'failed' }],
+ },
+ ];
+
+ expect(getters.failedJobs(mockedState).length).toBe(3);
+ expect(getters.failedJobs(mockedState)).toEqual([
+ {
+ id: 1,
+ status: jasmine.anything(),
+ },
+ {
+ id: 3,
+ status: jasmine.anything(),
+ },
+ {
+ id: 4,
+ status: jasmine.anything(),
+ },
+ ]);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js b/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js
new file mode 100644
index 00000000000..8262e916243
--- /dev/null
+++ b/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js
@@ -0,0 +1,120 @@
+import mutations from '~/ide/stores/modules/pipelines/mutations';
+import state from '~/ide/stores/modules/pipelines/state';
+import * as types from '~/ide/stores/modules/pipelines/mutation_types';
+import { pipelines, jobs } from '../../../mock_data';
+
+describe('IDE pipelines mutations', () => {
+ let mockedState;
+
+ beforeEach(() => {
+ mockedState = state();
+ });
+
+ describe(types.REQUEST_LATEST_PIPELINE, () => {
+ it('sets loading to true', () => {
+ mutations[types.REQUEST_LATEST_PIPELINE](mockedState);
+
+ expect(mockedState.isLoadingPipeline).toBe(true);
+ });
+ });
+
+ describe(types.RECEIVE_LASTEST_PIPELINE_ERROR, () => {
+ it('sets loading to false', () => {
+ mutations[types.RECEIVE_LASTEST_PIPELINE_ERROR](mockedState);
+
+ expect(mockedState.isLoadingPipeline).toBe(false);
+ });
+ });
+
+ describe(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, () => {
+ it('sets loading to false on success', () => {
+ mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](mockedState, pipelines[0]);
+
+ expect(mockedState.isLoadingPipeline).toBe(false);
+ });
+
+ it('sets latestPipeline', () => {
+ mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](mockedState, pipelines[0]);
+
+ expect(mockedState.latestPipeline).toEqual({
+ id: pipelines[0].id,
+ status: pipelines[0].status,
+ });
+ });
+
+ it('does not set latest pipeline if pipeline is null', () => {
+ mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](mockedState, null);
+
+ expect(mockedState.latestPipeline).toEqual(null);
+ });
+ });
+
+ describe(types.REQUEST_JOBS, () => {
+ it('sets jobs loading to true', () => {
+ mutations[types.REQUEST_JOBS](mockedState);
+
+ expect(mockedState.isLoadingJobs).toBe(true);
+ });
+ });
+
+ describe(types.RECEIVE_JOBS_ERROR, () => {
+ it('sets jobs loading to false', () => {
+ mutations[types.RECEIVE_JOBS_ERROR](mockedState);
+
+ expect(mockedState.isLoadingJobs).toBe(false);
+ });
+ });
+
+ describe(types.RECEIVE_JOBS_SUCCESS, () => {
+ it('sets jobs loading to false on success', () => {
+ mutations[types.RECEIVE_JOBS_SUCCESS](mockedState, jobs);
+
+ expect(mockedState.isLoadingJobs).toBe(false);
+ });
+
+ it('sets stages', () => {
+ mutations[types.RECEIVE_JOBS_SUCCESS](mockedState, jobs);
+
+ expect(mockedState.stages.length).toBe(2);
+ expect(mockedState.stages).toEqual([
+ {
+ title: 'test',
+ jobs: jasmine.anything(),
+ },
+ {
+ title: 'build',
+ jobs: jasmine.anything(),
+ },
+ ]);
+ });
+
+ it('sets jobs in stages', () => {
+ mutations[types.RECEIVE_JOBS_SUCCESS](mockedState, jobs);
+
+ expect(mockedState.stages[0].jobs.length).toBe(3);
+ expect(mockedState.stages[1].jobs.length).toBe(1);
+ expect(mockedState.stages).toEqual([
+ {
+ title: jasmine.anything(),
+ jobs: jobs.filter(job => job.stage === 'test').map(job => ({
+ id: job.id,
+ name: job.name,
+ status: job.status,
+ stage: job.stage,
+ duration: job.duration,
+ })),
+ },
+ {
+ title: jasmine.anything(),
+ jobs: jobs.filter(job => job.stage === 'build').map(job => ({
+ id: job.id,
+ name: job.name,
+ status: job.status,
+ stage: job.stage,
+ duration: job.duration,
+ })),
+ },
+ ]);
+ });
+ });
+});
diff --git a/spec/javascripts/pipelines/graph/action_component_spec.js b/spec/javascripts/pipelines/graph/action_component_spec.js
index d646bef96f5..568e679abe9 100644
--- a/spec/javascripts/pipelines/graph/action_component_spec.js
+++ b/spec/javascripts/pipelines/graph/action_component_spec.js
@@ -1,13 +1,19 @@
import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import actionComponent from '~/pipelines/components/graph/action_component.vue';
-import eventHub from '~/pipelines/event_hub';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('pipeline graph action component', () => {
let component;
+ let mock;
beforeEach(done => {
const ActionComponent = Vue.extend(actionComponent);
+ mock = new MockAdapter(axios);
+
+ mock.onPost('foo.json').reply(200);
+
component = mountComponent(ActionComponent, {
tooltipText: 'bar',
link: 'foo',
@@ -18,15 +24,10 @@ describe('pipeline graph action component', () => {
});
afterEach(() => {
+ mock.restore();
component.$destroy();
});
- it('should emit an event with the provided link', () => {
- eventHub.$on('postAction', link => {
- expect(link).toEqual('foo');
- });
- });
-
it('should render the provided title as a bootstrap tooltip', () => {
expect(component.$el.getAttribute('data-original-title')).toEqual('bar');
});
@@ -34,10 +35,12 @@ describe('pipeline graph action component', () => {
it('should update bootstrap tooltip when title changes', done => {
component.tooltipText = 'changed';
- setTimeout(() => {
+ component.$nextTick()
+ .then(() => {
expect(component.$el.getAttribute('data-original-title')).toBe('changed');
- done();
- });
+ })
+ .then(done)
+ .catch(done.fail);
});
it('should render an svg', () => {
@@ -45,44 +48,18 @@ describe('pipeline graph action component', () => {
expect(component.$el.querySelector('svg')).toBeDefined();
});
- it('disables the button when clicked', done => {
- component.$el.click();
+ describe('on click', () => {
+ it('emits `pipelineActionRequestComplete` after a successfull request', done => {
+ spyOn(component, '$emit');
- component.$nextTick(() => {
- expect(component.$el.getAttribute('disabled')).toEqual('disabled');
- done();
- });
- });
-
- it('re-enabled the button when `requestFinishedFor` matches `linkRequested`', done => {
- component.$el.click();
-
- component
- .$nextTick()
- .then(() => {
- expect(component.$el.getAttribute('disabled')).toEqual('disabled');
- component.requestFinishedFor = 'foo';
- })
- .then(() => {
- expect(component.$el.getAttribute('disabled')).toBeNull();
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('does not re-enable the button when `requestFinishedFor` does not matches `linkRequested`', done => {
- component.$el.click();
+ component.$el.click();
- component
- .$nextTick()
- .then(() => {
- expect(component.$el.getAttribute('disabled')).toEqual('disabled');
- component.requestFinishedFor = 'bar';
- })
- .then(() => {
- expect(component.$el.getAttribute('disabled')).toEqual('disabled');
- })
- .then(done)
- .catch(done.fail);
+ component.$nextTick()
+ .then(() => {
+ expect(component.$emit).toHaveBeenCalledWith('pipelineActionRequestComplete');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
});
});
diff --git a/spec/javascripts/pipelines/stage_spec.js b/spec/javascripts/pipelines/stage_spec.js
index 75156e7bdfd..16f6db39d6a 100644
--- a/spec/javascripts/pipelines/stage_spec.js
+++ b/spec/javascripts/pipelines/stage_spec.js
@@ -102,4 +102,31 @@ describe('Pipelines stage component', () => {
});
});
});
+
+ describe('pipelineActionRequestComplete', () => {
+ beforeEach(() => {
+ mock.onGet('path.json').reply(200, stageReply);
+
+ mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200);
+ });
+
+ describe('within pipeline table', () => {
+ it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', done => {
+ spyOn(eventHub, '$emit');
+
+ component.type = 'PIPELINES_TABLE';
+ component.$el.querySelector('button').click();
+
+ setTimeout(() => {
+ component.$el.querySelector('.js-ci-action').click();
+ component.$nextTick()
+ .then(() => {
+ expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable');
+ })
+ .then(done)
+ .catch(done.fail);
+ }, 0);
+ });
+ });
+ });
});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js
index db27aa144d6..00f4f2d7c39 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js
@@ -1,12 +1,12 @@
import Vue from 'vue';
-import authorComponent from '~/vue_merge_request_widget/components/mr_widget_author.vue';
+import MrWidgetAuthor from '~/vue_merge_request_widget/components/mr_widget_author.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
-describe('MRWidgetAuthor', () => {
+describe('MrWidgetAuthor', () => {
let vm;
beforeEach(() => {
- const Component = Vue.extend(authorComponent);
+ const Component = Vue.extend(MrWidgetAuthor);
vm = mountComponent(Component, {
author: {
diff --git a/spec/lib/api/helpers/pagination_spec.rb b/spec/lib/api/helpers/pagination_spec.rb
index a547988d631..c73c6023b60 100644
--- a/spec/lib/api/helpers/pagination_spec.rb
+++ b/spec/lib/api/helpers/pagination_spec.rb
@@ -7,7 +7,203 @@ describe API::Helpers::Pagination do
Class.new.include(described_class).new
end
- describe '#paginate' do
+ describe '#paginate (keyset pagination)' do
+ let(:value) { spy('return value') }
+
+ before do
+ allow(value).to receive(:to_query).and_return(value)
+
+ allow(subject).to receive(:header).and_return(value)
+ allow(subject).to receive(:params).and_return(value)
+ allow(subject).to receive(:request).and_return(value)
+ end
+
+ context 'when resource can be paginated' do
+ let!(:projects) do
+ [
+ create(:project, name: 'One'),
+ create(:project, name: 'Two'),
+ create(:project, name: 'Three')
+ ].sort_by { |e| -e.id } # sort by id desc (this is the default sort order for the API)
+ end
+
+ describe 'first page' do
+ before do
+ allow(subject).to receive(:params)
+ .and_return({ pagination: 'keyset', per_page: 2 })
+ end
+
+ it 'returns appropriate amount of resources' do
+ expect(subject.paginate(resource).count).to eq 2
+ end
+
+ it 'returns the first two records (by id desc)' do
+ expect(subject.paginate(resource)).to eq(projects[0..1])
+ end
+
+ it 'adds appropriate headers' do
+ expect_header('X-Per-Page', '2')
+ expect_header('X-Next-Page', "#{value}?ks_prev_id=#{projects[1].id}&pagination=keyset&per_page=2")
+
+ expect_header('Link', anything) do |_key, val|
+ expect(val).to include('rel="next"')
+ end
+
+ subject.paginate(resource)
+ end
+ end
+
+ describe 'second page' do
+ before do
+ allow(subject).to receive(:params)
+ .and_return({ pagination: 'keyset', per_page: 2, ks_prev_id: projects[1].id })
+ end
+
+ it 'returns appropriate amount of resources' do
+ expect(subject.paginate(resource).count).to eq 1
+ end
+
+ it 'returns the third record' do
+ expect(subject.paginate(resource)).to eq(projects[2..2])
+ end
+
+ it 'adds appropriate headers' do
+ expect_header('X-Per-Page', '2')
+ expect_header('X-Next-Page', "#{value}?ks_prev_id=#{projects[2].id}&pagination=keyset&per_page=2")
+
+ expect_header('Link', anything) do |_key, val|
+ expect(val).to include('rel="next"')
+ end
+
+ subject.paginate(resource)
+ end
+ end
+
+ describe 'third page' do
+ before do
+ allow(subject).to receive(:params)
+ .and_return({ pagination: 'keyset', per_page: 2, ks_prev_id: projects[2].id })
+ end
+
+ it 'returns appropriate amount of resources' do
+ expect(subject.paginate(resource).count).to eq 0
+ end
+
+ it 'adds appropriate headers' do
+ expect_header('X-Per-Page', '2')
+ expect(subject).not_to receive(:header).with('Link')
+
+ subject.paginate(resource)
+ end
+ end
+
+ context 'if order' do
+ context 'is not present' do
+ before do
+ allow(subject).to receive(:params)
+ .and_return({ pagination: 'keyset', per_page: 2 })
+ end
+
+ it 'is not present it adds default order(:id) desc' do
+ resource.order_values = []
+
+ paginated_relation = subject.paginate(resource)
+
+ expect(resource.order_values).to be_empty
+ expect(paginated_relation.order_values).to be_present
+ expect(paginated_relation.order_values.size).to eq(1)
+ expect(paginated_relation.order_values.first).to be_descending
+ expect(paginated_relation.order_values.first.expr.name).to eq :id
+ end
+ end
+
+ context 'is present' do
+ let(:resource) { Project.all.order(name: :desc) }
+ let!(:projects) do
+ [
+ create(:project, name: 'One'),
+ create(:project, name: 'Two'),
+ create(:project, name: 'Three'),
+ create(:project, name: 'Three'), # Note the duplicate name
+ create(:project, name: 'Four'),
+ create(:project, name: 'Five'),
+ create(:project, name: 'Six')
+ ]
+
+ # if we sort this by name descending, id descending, this yields:
+ # {
+ # 2 => "Two",
+ # 4 => "Three",
+ # 3 => "Three",
+ # 7 => "Six",
+ # 1 => "One",
+ # 5 => "Four",
+ # 6 => "Five"
+ # }
+ #
+ # (key is the id)
+ end
+
+ it 'it also orders by primary key' do
+ allow(subject).to receive(:params)
+ .and_return({ pagination: 'keyset', per_page: 2 })
+ paginated_relation = subject.paginate(resource)
+
+ expect(paginated_relation.order_values).to be_present
+ expect(paginated_relation.order_values.size).to eq(2)
+ expect(paginated_relation.order_values.first).to be_descending
+ expect(paginated_relation.order_values.first.expr.name).to eq :name
+ expect(paginated_relation.order_values.second).to be_descending
+ expect(paginated_relation.order_values.second.expr.name).to eq :id
+ end
+
+ it 'it returns the right records (first page)' do
+ allow(subject).to receive(:params)
+ .and_return({ pagination: 'keyset', per_page: 2 })
+ result = subject.paginate(resource)
+
+ expect(result.first).to eq(projects[1])
+ expect(result.second).to eq(projects[3])
+ end
+
+ it 'it returns the right records (second page)' do
+ allow(subject).to receive(:params)
+ .and_return({ pagination: 'keyset', ks_prev_id: projects[3].id, ks_prev_name: projects[3].name, per_page: 2 })
+ result = subject.paginate(resource)
+
+ expect(result.first).to eq(projects[2])
+ expect(result.second).to eq(projects[6])
+ end
+
+ it 'it returns the right records (third page), note increased per_page' do
+ allow(subject).to receive(:params)
+ .and_return({ pagination: 'keyset', ks_prev_id: projects[6].id, ks_prev_name: projects[6].name, per_page: 5 })
+ result = subject.paginate(resource)
+
+ expect(result.size).to eq(3)
+ expect(result.first).to eq(projects[0])
+ expect(result.second).to eq(projects[4])
+ expect(result.last).to eq(projects[5])
+ end
+
+ it 'it returns the right link to the next page' do
+ allow(subject).to receive(:params)
+ .and_return({ pagination: 'keyset', ks_prev_id: projects[3].id, ks_prev_name: projects[3].name, per_page: 2 })
+ expect_header('X-Per-Page', '2')
+ expect_header('X-Next-Page', "#{value}?ks_prev_id=#{projects[6].id}&ks_prev_name=#{projects[6].name}&pagination=keyset&per_page=2")
+
+ expect_header('Link', anything) do |_key, val|
+ expect(val).to include('rel="next"')
+ end
+
+ subject.paginate(resource)
+ end
+ end
+ end
+ end
+ end
+
+ describe '#paginate (default offset-based pagination)' do
let(:value) { spy('return value') }
before do
@@ -146,14 +342,14 @@ describe API::Helpers::Pagination do
end
end
end
+ end
- def expect_header(*args, &block)
- expect(subject).to receive(:header).with(*args, &block)
- end
+ def expect_header(*args, &block)
+ expect(subject).to receive(:header).with(*args, &block)
+ end
- def expect_message(method)
- expect(subject).to receive(method)
- .at_least(:once).and_return(value)
- end
+ def expect_message(method)
+ expect(subject).to receive(method)
+ .at_least(:once).and_return(value)
end
end
diff --git a/spec/lib/api/helpers/related_resources_helpers_spec.rb b/spec/lib/api/helpers/related_resources_helpers_spec.rb
index b918301f1cb..66af7f81535 100644
--- a/spec/lib/api/helpers/related_resources_helpers_spec.rb
+++ b/spec/lib/api/helpers/related_resources_helpers_spec.rb
@@ -9,9 +9,9 @@ describe API::Helpers::RelatedResourcesHelpers do
let(:path) { '/api/v4/awesome_endpoint' }
subject(:url) { helpers.expose_url(path) }
- def stub_default_url_options(protocol: 'http', host: 'example.com', port: nil)
+ def stub_default_url_options(protocol: 'http', host: 'example.com', port: nil, script_name: '')
expect(Gitlab::Application.routes).to receive(:default_url_options)
- .and_return(protocol: protocol, host: host, port: port)
+ .and_return(protocol: protocol, host: host, port: port, script_name: script_name)
end
it 'respects the protocol if it is HTTP' do
@@ -37,5 +37,17 @@ describe API::Helpers::RelatedResourcesHelpers do
is_expected.to start_with('http://example.com:8080/')
end
+
+ it 'includes the relative_url before the path if it is set' do
+ stub_default_url_options(script_name: '/gitlab')
+
+ is_expected.to start_with('http://example.com/gitlab/api/v4')
+ end
+
+ it 'includes the path after the host' do
+ stub_default_url_options
+
+ is_expected.to start_with('http://example.com/api/v4')
+ end
end
end
diff --git a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb
index ca76d6f0881..0e178b859c4 100644
--- a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb
+++ b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb
@@ -91,6 +91,12 @@ describe Banzai::Filter::GollumTagsFilter do
expect(doc.at_css('a').text).to eq 'link-text'
expect(doc.at_css('a')['href']).to eq expected_path
end
+
+ it "inside back ticks will be exempt from linkification" do
+ doc = filter('<code>[[link-in-backticks]]</code>', project_wiki: project_wiki)
+
+ expect(doc.at_css('code').text).to eq '[[link-in-backticks]]'
+ end
end
context 'table of contents' do
diff --git a/spec/lib/gitlab/auth/ldap/access_spec.rb b/spec/lib/gitlab/auth/ldap/access_spec.rb
index 6b251d824f7..eff21985108 100644
--- a/spec/lib/gitlab/auth/ldap/access_spec.rb
+++ b/spec/lib/gitlab/auth/ldap/access_spec.rb
@@ -8,6 +8,7 @@ describe Gitlab::Auth::LDAP::Access do
describe '.allowed?' do
it 'updates the users `last_credential_check_at' do
+ allow(access).to receive(:update_user)
expect(access).to receive(:allowed?) { true }
expect(described_class).to receive(:open).and_yield(access)
@@ -16,12 +17,21 @@ describe Gitlab::Auth::LDAP::Access do
end
end
+ describe '#find_ldap_user' do
+ it 'finds a user by dn first' do
+ expect(Gitlab::Auth::LDAP::Person).to receive(:find_by_dn).and_return(:ldap_user)
+
+ access.find_ldap_user
+ end
+ end
+
describe '#allowed?' do
subject { access.allowed? }
context 'when the user cannot be found' do
before do
allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_dn).and_return(nil)
+ allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_email).and_return(nil)
end
it { is_expected.to be_falsey }
@@ -54,7 +64,7 @@ describe Gitlab::Auth::LDAP::Access do
end
end
- context 'and has no disabled flag in active diretory' do
+ context 'and has no disabled flag in active directory' do
before do
allow(Gitlab::Auth::LDAP::Person).to receive(:disabled_via_active_directory?).and_return(false)
end
@@ -100,6 +110,7 @@ describe Gitlab::Auth::LDAP::Access do
context 'when user cannot be found' do
before do
allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_dn).and_return(nil)
+ allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_email).and_return(nil)
end
it { is_expected.to be_falsey }
diff --git a/spec/lib/gitlab/auth/ldap/config_spec.rb b/spec/lib/gitlab/auth/ldap/config_spec.rb
index 82587e2ba55..d3ab599d5a0 100644
--- a/spec/lib/gitlab/auth/ldap/config_spec.rb
+++ b/spec/lib/gitlab/auth/ldap/config_spec.rb
@@ -23,7 +23,7 @@ describe Gitlab::Auth::LDAP::Config do
end
it 'raises an error if a unknown provider is used' do
- expect { described_class.new 'unknown' }.to raise_error(RuntimeError)
+ expect { described_class.new 'unknown' }.to raise_error(described_class::InvalidProvider)
end
end
@@ -370,4 +370,38 @@ describe Gitlab::Auth::LDAP::Config do
})
end
end
+
+ describe '#base' do
+ context 'when the configured base is not normalized' do
+ it 'returns the normalized base' do
+ stub_ldap_config(options: { 'base' => 'DC=example, DC= com' })
+
+ expect(config.base).to eq('dc=example,dc=com')
+ end
+ end
+
+ context 'when the configured base is normalized' do
+ it 'returns the base unaltered' do
+ stub_ldap_config(options: { 'base' => 'dc=example,dc=com' })
+
+ expect(config.base).to eq('dc=example,dc=com')
+ end
+ end
+
+ context 'when the configured base is malformed' do
+ it 'returns the base unaltered' do
+ stub_ldap_config(options: { 'base' => 'invalid,dc=example,dc=com' })
+
+ expect(config.base).to eq('invalid,dc=example,dc=com')
+ end
+ end
+
+ context 'when the configured base is blank' do
+ it 'returns the base unaltered' do
+ stub_ldap_config(options: { 'base' => '' })
+
+ expect(config.base).to eq('')
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/auth/ldap/user_spec.rb b/spec/lib/gitlab/auth/ldap/user_spec.rb
index 653c19942ea..44bb9d20e47 100644
--- a/spec/lib/gitlab/auth/ldap/user_spec.rb
+++ b/spec/lib/gitlab/auth/ldap/user_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Gitlab::Auth::LDAP::User do
+ include LdapHelpers
+
let(:ldap_user) { described_class.new(auth_hash) }
let(:gl_user) { ldap_user.gl_user }
let(:info) do
@@ -177,8 +179,7 @@ describe Gitlab::Auth::LDAP::User do
describe 'blocking' do
def configure_block(value)
- allow_any_instance_of(Gitlab::Auth::LDAP::Config)
- .to receive(:block_auto_created_users).and_return(value)
+ stub_ldap_config(block_auto_created_users: value)
end
context 'signup' do
diff --git a/spec/lib/gitlab/ci/config/entry/policy_spec.rb b/spec/lib/gitlab/ci/config/entry/policy_spec.rb
index 08718c382b9..83d39b82068 100644
--- a/spec/lib/gitlab/ci/config/entry/policy_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/policy_spec.rb
@@ -111,7 +111,15 @@ describe Gitlab::Ci::Config::Entry::Policy do
context 'when specifying invalid variables expressions token' do
let(:config) { { variables: ['$MY_VAR == 123'] } }
- it 'reports an error about invalid statement' do
+ it 'reports an error about invalid expression' do
+ expect(entry.errors).to include /invalid expression syntax/
+ end
+ end
+
+ context 'when using invalid variables expressions regexp' do
+ let(:config) { { variables: ['$MY_VAR =~ /some ( thing/'] } }
+
+ it 'reports an error about invalid expression' do
expect(entry.errors).to include /invalid expression syntax/
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
new file mode 100644
index 00000000000..49e5af52f4d
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb
@@ -0,0 +1,80 @@
+require 'fast_spec_helper'
+require_dependency 're2'
+
+describe Gitlab::Ci::Pipeline::Expression::Lexeme::Matches do
+ let(:left) { double('left') }
+ let(:right) { double('right') }
+
+ describe '.build' do
+ it 'creates a new instance of the token' do
+ expect(described_class.build('=~', left, right))
+ .to be_a(described_class)
+ end
+ end
+
+ describe '.type' do
+ it 'is an operator' do
+ expect(described_class.type).to eq :operator
+ end
+ end
+
+ describe '#evaluate' do
+ it 'returns false when left and right do not match' do
+ allow(left).to receive(:evaluate).and_return('my-string')
+ allow(right).to receive(:evaluate)
+ .and_return(Gitlab::UntrustedRegexp.new('something'))
+
+ operator = described_class.new(left, right)
+
+ expect(operator.evaluate).to eq false
+ end
+
+ it 'returns true when left and right match' do
+ allow(left).to receive(:evaluate).and_return('my-awesome-string')
+ allow(right).to receive(:evaluate)
+ .and_return(Gitlab::UntrustedRegexp.new('awesome.string$'))
+
+ operator = described_class.new(left, right)
+
+ expect(operator.evaluate).to eq true
+ end
+
+ it 'supports matching against a nil value' do
+ allow(left).to receive(:evaluate).and_return(nil)
+ allow(right).to receive(:evaluate)
+ .and_return(Gitlab::UntrustedRegexp.new('pattern'))
+
+ operator = described_class.new(left, right)
+
+ expect(operator.evaluate).to eq false
+ end
+
+ it 'supports multiline strings' do
+ allow(left).to receive(:evaluate).and_return <<~TEXT
+ My awesome contents
+
+ My-text-string!
+ TEXT
+
+ allow(right).to receive(:evaluate)
+ .and_return(Gitlab::UntrustedRegexp.new('text-string'))
+
+ operator = described_class.new(left, right)
+
+ expect(operator.evaluate).to eq true
+ end
+
+ it 'supports regexp flags' do
+ allow(left).to receive(:evaluate).and_return <<~TEXT
+ My AWESOME content
+ TEXT
+
+ allow(right).to receive(:evaluate)
+ .and_return(Gitlab::UntrustedRegexp.new('(?i)awesome'))
+
+ operator = described_class.new(left, right)
+
+ expect(operator.evaluate).to eq true
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb
new file mode 100644
index 00000000000..3ebc2e94727
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb
@@ -0,0 +1,96 @@
+require 'fast_spec_helper'
+
+describe Gitlab::Ci::Pipeline::Expression::Lexeme::Pattern do
+ describe '.build' do
+ it 'creates a new instance of the token' do
+ expect(described_class.build('/.*/'))
+ .to be_a(described_class)
+ end
+
+ it 'raises an error if pattern is invalid' do
+ expect { described_class.build('/ some ( thin/i') }
+ .to raise_error(Gitlab::Ci::Pipeline::Expression::Lexer::SyntaxError)
+ end
+ end
+
+ describe '.type' do
+ it 'is a value lexeme' do
+ expect(described_class.type).to eq :value
+ end
+ end
+
+ describe '.scan' do
+ it 'correctly identifies a pattern token' do
+ scanner = StringScanner.new('/pattern/')
+
+ token = described_class.scan(scanner)
+
+ expect(token).not_to be_nil
+ expect(token.build.evaluate)
+ .to eq Gitlab::UntrustedRegexp.new('pattern')
+ end
+
+ it 'is a greedy scanner for regexp boundaries' do
+ scanner = StringScanner.new('/some .* / pattern/')
+
+ token = described_class.scan(scanner)
+
+ expect(token).not_to be_nil
+ expect(token.build.evaluate)
+ .to eq Gitlab::UntrustedRegexp.new('some .* / pattern')
+ end
+
+ it 'does not allow to use an empty pattern' do
+ scanner = StringScanner.new(%(//))
+
+ token = described_class.scan(scanner)
+
+ expect(token).to be_nil
+ end
+
+ it 'support single flag' do
+ scanner = StringScanner.new('/pattern/i')
+
+ token = described_class.scan(scanner)
+
+ expect(token).not_to be_nil
+ expect(token.build.evaluate)
+ .to eq Gitlab::UntrustedRegexp.new('(?i)pattern')
+ end
+
+ it 'support multiple flags' do
+ scanner = StringScanner.new('/pattern/im')
+
+ token = described_class.scan(scanner)
+
+ expect(token).not_to be_nil
+ expect(token.build.evaluate)
+ .to eq Gitlab::UntrustedRegexp.new('(?im)pattern')
+ end
+
+ it 'does not support arbitrary flags' do
+ scanner = StringScanner.new('/pattern/x')
+
+ token = described_class.scan(scanner)
+
+ expect(token).to be_nil
+ end
+ end
+
+ describe '#evaluate' do
+ it 'returns a regular expression' do
+ regexp = described_class.new('/abc/')
+
+ expect(regexp.evaluate).to eq Gitlab::UntrustedRegexp.new('abc')
+ end
+
+ it 'raises error if evaluated regexp is not valid' do
+ allow(Gitlab::UntrustedRegexp).to receive(:valid?).and_return(true)
+
+ regexp = described_class.new('/invalid ( .*/')
+
+ expect { regexp.evaluate }
+ .to raise_error(Gitlab::Ci::Pipeline::Expression::RuntimeError)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb
index 230ceeb07f8..3f11b3f7673 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::Ci::Pipeline::Expression::Lexer do
end
describe '#tokens' do
- it 'tokenss single value' do
+ it 'returns single value' do
tokens = described_class.new('$VARIABLE').tokens
expect(tokens).to be_one
@@ -20,14 +20,14 @@ describe Gitlab::Ci::Pipeline::Expression::Lexer do
expect(tokens).to all(be_an_instance_of(token_class))
end
- it 'tokenss multiple values of the same token' do
+ it 'returns multiple values of the same token' do
tokens = described_class.new("$VARIABLE1 $VARIABLE2").tokens
expect(tokens.size).to eq 2
expect(tokens).to all(be_an_instance_of(token_class))
end
- it 'tokenss multiple values with different tokens' do
+ it 'returns multiple values with different tokens' do
tokens = described_class.new('$VARIABLE "text" "value"').tokens
expect(tokens.size).to eq 3
@@ -36,7 +36,7 @@ describe Gitlab::Ci::Pipeline::Expression::Lexer do
expect(tokens.third.value).to eq '"value"'
end
- it 'tokenss tokens and operators' do
+ it 'returns tokens and operators' do
tokens = described_class.new('$VARIABLE == "text"').tokens
expect(tokens.size).to eq 3
diff --git a/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb
index e8e6f585310..2b78b1dd4a7 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb
@@ -1,4 +1,4 @@
-require 'spec_helper'
+require 'fast_spec_helper'
describe Gitlab::Ci::Pipeline::Expression::Parser do
describe '#tree' do
diff --git a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb
index 6685bf5385b..11e73294f18 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb
@@ -1,4 +1,5 @@
-require 'spec_helper'
+require 'fast_spec_helper'
+require 'rspec-parameterized'
describe Gitlab::Ci::Pipeline::Expression::Statement do
subject do
@@ -36,7 +37,7 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
'== "123"', # invalid left side
'"some string"', # only string provided
'$VAR ==', # invalid right side
- '12345', # unknown syntax
+ 'null', # missing lexemes
'' # empty statement
]
@@ -44,7 +45,7 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
context "when expression grammar is #{syntax.inspect}" do
let(:text) { syntax }
- it 'aises a statement error exception' do
+ it 'raises a statement error exception' do
expect { subject.parse_tree }
.to raise_error described_class::StatementError
end
@@ -82,48 +83,66 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
end
describe '#evaluate' do
- statements = [
- ['$PRESENT_VARIABLE == "my variable"', true],
- ["$PRESENT_VARIABLE == 'my variable'", true],
- ['"my variable" == $PRESENT_VARIABLE', true],
- ['$PRESENT_VARIABLE == null', false],
- ['$EMPTY_VARIABLE == null', false],
- ['"" == $EMPTY_VARIABLE', true],
- ['$EMPTY_VARIABLE', ''],
- ['$UNDEFINED_VARIABLE == null', true],
- ['null == $UNDEFINED_VARIABLE', true],
- ['$PRESENT_VARIABLE', 'my variable'],
- ['$UNDEFINED_VARIABLE', nil]
- ]
-
- statements.each do |expression, value|
- context "when using expression `#{expression}`" do
- let(:text) { expression }
-
- it "evaluates to `#{value.inspect}`" do
- expect(subject.evaluate).to eq value
- end
+ using RSpec::Parameterized::TableSyntax
+
+ where(:expression, :value) do
+ '$PRESENT_VARIABLE == "my variable"' | true
+ '"my variable" == $PRESENT_VARIABLE' | true
+ '$PRESENT_VARIABLE == null' | false
+ '$EMPTY_VARIABLE == null' | false
+ '"" == $EMPTY_VARIABLE' | true
+ '$EMPTY_VARIABLE' | ''
+ '$UNDEFINED_VARIABLE == null' | true
+ 'null == $UNDEFINED_VARIABLE' | true
+ '$PRESENT_VARIABLE' | 'my variable'
+ '$UNDEFINED_VARIABLE' | nil
+ "$PRESENT_VARIABLE =~ /var.*e$/" | true
+ "$PRESENT_VARIABLE =~ /^var.*/" | false
+ "$EMPTY_VARIABLE =~ /var.*/" | false
+ "$UNDEFINED_VARIABLE =~ /var.*/" | false
+ "$PRESENT_VARIABLE =~ /VAR.*/i" | true
+ end
+
+ with_them do
+ let(:text) { expression }
+
+ it "evaluates to `#{params[:value].inspect}`" do
+ expect(subject.evaluate).to eq value
end
end
end
describe '#truthful?' do
- statements = [
- ['$PRESENT_VARIABLE == "my variable"', true],
- ["$PRESENT_VARIABLE == 'no match'", false],
- ['$UNDEFINED_VARIABLE == null', true],
- ['$PRESENT_VARIABLE', true],
- ['$UNDEFINED_VARIABLE', false],
- ['$EMPTY_VARIABLE', false]
- ]
-
- statements.each do |expression, value|
- context "when using expression `#{expression}`" do
- let(:text) { expression }
-
- it "returns `#{value.inspect}`" do
- expect(subject.truthful?).to eq value
- end
+ using RSpec::Parameterized::TableSyntax
+
+ where(:expression, :value) do
+ '$PRESENT_VARIABLE == "my variable"' | true
+ "$PRESENT_VARIABLE == 'no match'" | false
+ '$UNDEFINED_VARIABLE == null' | true
+ '$PRESENT_VARIABLE' | true
+ '$UNDEFINED_VARIABLE' | false
+ '$EMPTY_VARIABLE' | false
+ '$INVALID = 1' | false
+ "$PRESENT_VARIABLE =~ /var.*/" | true
+ "$UNDEFINED_VARIABLE =~ /var.*/" | false
+ end
+
+ with_them do
+ let(:text) { expression }
+
+ it "returns `#{params[:value].inspect}`" do
+ expect(subject.truthful?).to eq value
+ end
+ end
+
+ context 'when evaluating expression raises an error' do
+ let(:text) { '$PRESENT_VARIABLE' }
+
+ it 'returns false' do
+ allow(subject).to receive(:evaluate)
+ .and_raise(described_class::StatementError)
+
+ expect(subject.truthful?).to be_falsey
end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/token_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/token_spec.rb
index 6d7453f0de5..cedfe270f9d 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/token_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/token_spec.rb
@@ -1,4 +1,4 @@
-require 'spec_helper'
+require 'fast_spec_helper'
describe Gitlab::Ci::Pipeline::Expression::Token do
let(:value) { '$VARIABLE' }
diff --git a/spec/lib/gitlab/ci/pipeline/preloader_spec.rb b/spec/lib/gitlab/ci/pipeline/preloader_spec.rb
new file mode 100644
index 00000000000..477c7477df0
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/preloader_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Preloader do
+ describe '.preload' do
+ it 'preloads the author of every pipeline commit' do
+ commit = double(:commit)
+ pipeline = double(:pipeline, commit: commit)
+
+ expect(commit)
+ .to receive(:lazy_author)
+
+ expect(pipeline)
+ .to receive(:number_of_warnings)
+
+ described_class.preload([pipeline])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/current_settings_spec.rb b/spec/lib/gitlab/current_settings_spec.rb
index 4ddcbd7eb66..19028495f52 100644
--- a/spec/lib/gitlab/current_settings_spec.rb
+++ b/spec/lib/gitlab/current_settings_spec.rb
@@ -1,17 +1,15 @@
require 'spec_helper'
describe Gitlab::CurrentSettings do
- include StubENV
-
before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
end
- describe '#current_application_settings' do
+ describe '#current_application_settings', :use_clean_rails_memory_store_caching do
it 'allows keys to be called directly' do
db_settings = create(:application_setting,
- home_page_url: 'http://mydomain.com',
- signup_enabled: false)
+ home_page_url: 'http://mydomain.com',
+ signup_enabled: false)
expect(described_class.home_page_url).to eq(db_settings.home_page_url)
expect(described_class.signup_enabled?).to be_falsey
@@ -19,46 +17,54 @@ describe Gitlab::CurrentSettings do
expect(described_class.metrics_sample_interval).to be(15)
end
- context 'with DB available' do
+ context 'when ENV["IN_MEMORY_APPLICATION_SETTINGS"] is true' do
before do
- # For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(true)` causes issues
- # during the initialization phase of the test suite, so instead let's mock the internals of it
- allow(ActiveRecord::Base.connection).to receive(:active?).and_return(true)
- allow(ActiveRecord::Base.connection).to receive(:table_exists?).and_call_original
- allow(ActiveRecord::Base.connection).to receive(:table_exists?).with('application_settings').and_return(true)
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'true')
end
- it 'attempts to use cached values first' do
- expect(ApplicationSetting).to receive(:cached)
+ it 'returns an in-memory ApplicationSetting object' do
+ expect(ApplicationSetting).not_to receive(:current)
expect(described_class.current_application_settings).to be_a(ApplicationSetting)
+ expect(described_class.current_application_settings).not_to be_persisted
end
+ end
- it 'falls back to DB if Redis returns an empty value' do
- expect(ApplicationSetting).to receive(:cached).and_return(nil)
- expect(ApplicationSetting).to receive(:last).and_call_original.twice
-
- expect(described_class.current_application_settings).to be_a(ApplicationSetting)
+ context 'with DB unavailable' do
+ before do
+ # For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(false)` causes issues
+ # during the initialization phase of the test suite, so instead let's mock the internals of it
+ allow(ActiveRecord::Base.connection).to receive(:active?).and_return(false)
end
- it 'falls back to DB if Redis fails' do
- db_settings = ApplicationSetting.create!(ApplicationSetting.defaults)
+ it 'returns an in-memory ApplicationSetting object' do
+ expect(ApplicationSetting).not_to receive(:current)
- expect(ApplicationSetting).to receive(:cached).and_raise(::Redis::BaseError)
- expect(Rails.cache).to receive(:fetch).with(ApplicationSetting::CACHE_KEY).and_raise(Redis::BaseError)
+ expect(described_class.current_application_settings).to be_a(Gitlab::FakeApplicationSettings)
+ end
+ end
- expect(described_class.current_application_settings).to eq(db_settings)
+ context 'with DB available' do
+ # This method returns the ::ApplicationSetting.defaults hash
+ # but with respect of custom attribute accessors of ApplicationSetting model
+ def settings_from_defaults
+ ar_wrapped_defaults = ::ApplicationSetting.build_from_defaults.attributes
+ ar_wrapped_defaults.slice(*::ApplicationSetting.defaults.keys)
end
- it 'creates default ApplicationSettings if none are present' do
- expect(ApplicationSetting).to receive(:cached).and_raise(::Redis::BaseError)
- expect(Rails.cache).to receive(:fetch).with(ApplicationSetting::CACHE_KEY).and_raise(Redis::BaseError)
+ before do
+ # For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(true)` causes issues
+ # during the initialization phase of the test suite, so instead let's mock the internals of it
+ allow(ActiveRecord::Base.connection).to receive(:active?).and_return(true)
+ allow(ActiveRecord::Base.connection).to receive(:cached_table_exists?).with('application_settings').and_return(true)
+ end
+ it 'creates default ApplicationSettings if none are present' do
settings = described_class.current_application_settings
expect(settings).to be_a(ApplicationSetting)
expect(settings).to be_persisted
- expect(settings).to have_attributes(ApplicationSetting.defaults)
+ expect(settings).to have_attributes(settings_from_defaults)
end
context 'with migrations pending' do
@@ -69,7 +75,7 @@ describe Gitlab::CurrentSettings do
it 'returns an in-memory ApplicationSetting object' do
settings = described_class.current_application_settings
- expect(settings).to be_a(OpenStruct)
+ expect(settings).to be_a(Gitlab::FakeApplicationSettings)
expect(settings.sign_in_enabled?).to eq(settings.sign_in_enabled)
expect(settings.sign_up_enabled?).to eq(settings.sign_up_enabled)
end
@@ -81,7 +87,7 @@ describe Gitlab::CurrentSettings do
settings = described_class.current_application_settings
app_defaults = ApplicationSetting.last
- expect(settings).to be_a(OpenStruct)
+ expect(settings).to be_a(Gitlab::FakeApplicationSettings)
expect(settings.home_page_url).to eq(db_settings.home_page_url)
expect(settings.signup_enabled?).to be_falsey
expect(settings.signup_enabled).to be_falsey
@@ -91,34 +97,29 @@ describe Gitlab::CurrentSettings do
settings.each { |key, _| expect(settings[key]).to eq(app_defaults[key]) }
end
end
- end
-
- context 'with DB unavailable' do
- before do
- # For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(false)` causes issues
- # during the initialization phase of the test suite, so instead let's mock the internals of it
- allow(ActiveRecord::Base.connection).to receive(:active?).and_return(false)
- end
- it 'returns an in-memory ApplicationSetting object' do
- expect(ApplicationSetting).not_to receive(:current)
- expect(ApplicationSetting).not_to receive(:last)
+ context 'when ApplicationSettings.current is present' do
+ it 'returns the existing application settings' do
+ expect(ApplicationSetting).to receive(:current).and_return(:current_settings)
- expect(described_class.current_application_settings).to be_a(OpenStruct)
+ expect(described_class.current_application_settings).to eq(:current_settings)
+ end
end
- end
- context 'when ENV["IN_MEMORY_APPLICATION_SETTINGS"] is true' do
- before do
- stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'true')
+ context 'when the application_settings table does not exists' do
+ it 'returns an in-memory ApplicationSetting object' do
+ expect(ApplicationSetting).to receive(:create_from_defaults).and_raise(ActiveRecord::StatementInvalid)
+
+ expect(described_class.current_application_settings).to be_a(Gitlab::FakeApplicationSettings)
+ end
end
- it 'returns an in-memory ApplicationSetting object' do
- expect(ApplicationSetting).not_to receive(:current)
- expect(ApplicationSetting).not_to receive(:last)
+ context 'when the application_settings table is not fully migrated' do
+ it 'returns an in-memory ApplicationSetting object' do
+ expect(ApplicationSetting).to receive(:create_from_defaults).and_raise(ActiveRecord::UnknownAttributeError)
- expect(described_class.current_application_settings).to be_a(ApplicationSetting)
- expect(described_class.current_application_settings).not_to be_persisted
+ expect(described_class.current_application_settings).to be_a(Gitlab::FakeApplicationSettings)
+ end
end
end
end
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index 1fe1d3926ad..8ac36ae8bab 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -32,6 +32,12 @@ describe Gitlab::Database do
end
describe '.version' do
+ around do |example|
+ described_class.instance_variable_set(:@version, nil)
+ example.run
+ described_class.instance_variable_set(:@version, nil)
+ end
+
context "on mysql" do
it "extracts the version number" do
allow(described_class).to receive(:database_version)
@@ -49,6 +55,14 @@ describe Gitlab::Database do
expect(described_class.version).to eq '9.4.4'
end
end
+
+ it 'memoizes the result' do
+ count = ActiveRecord::QueryRecorder
+ .new { 2.times { described_class.version } }
+ .count
+
+ expect(count).to eq(1)
+ end
end
describe '.join_lateral_supported?' do
diff --git a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
index 452249210b0..154ab4b3856 100644
--- a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
@@ -46,6 +46,20 @@ describe Gitlab::Email::Handler::CreateIssueHandler do
expect(issue.description).to eq('')
end
end
+
+ context "when there are quotes in email" do
+ let(:email_raw) { fixture_file("emails/valid_new_issue_with_quote.eml") }
+
+ it "creates a new issue" do
+ expect { receiver.execute }.to change { project.issues.count }.by(1)
+ issue = project.issues.last
+
+ expect(issue.author).to eq(user)
+ expect(issue.title).to eq('New Issue by email')
+ expect(issue.description).to include('reply by email')
+ expect(issue.description).to include('> this is a quote')
+ end
+ end
end
context "something is wrong" do
diff --git a/spec/lib/gitlab/email/reply_parser_spec.rb b/spec/lib/gitlab/email/reply_parser_spec.rb
index e21a998adfe..0989188f7ee 100644
--- a/spec/lib/gitlab/email/reply_parser_spec.rb
+++ b/spec/lib/gitlab/email/reply_parser_spec.rb
@@ -3,8 +3,8 @@ require "spec_helper"
# Inspired in great part by Discourse's Email::Receiver
describe Gitlab::Email::ReplyParser do
describe '#execute' do
- def test_parse_body(mail_string)
- described_class.new(Mail::Message.new(mail_string)).execute
+ def test_parse_body(mail_string, params = {})
+ described_class.new(Mail::Message.new(mail_string), params).execute
end
it "returns an empty string if the message is blank" do
@@ -212,5 +212,19 @@ describe Gitlab::Email::ReplyParser do
it "does not wrap links with no href in unnecessary brackets" do
expect(test_parse_body(fixture_file("emails/html_empty_link.eml"))).to eq("no brackets!")
end
+
+ it "does not trim reply if trim_reply option is false" do
+ expect(test_parse_body(fixture_file("emails/valid_new_issue_with_quote.eml"), { trim_reply: false }))
+ .to eq(
+ <<-BODY.strip_heredoc.chomp
+ The reply by email functionality should be extended to allow creating a new issue by email.
+ even when the email is forwarded to the project which may include lines that begin with ">"
+
+ there should be a quote below this line:
+
+ > this is a quote
+ BODY
+ )
+ end
end
end
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index 2e068584c2e..08c6d1e55e9 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -66,7 +66,8 @@ describe Gitlab::Git::Commit, seed_helper: true do
describe "Commit info from gitaly commit" do
let(:subject) { "My commit".force_encoding('ASCII-8BIT') }
let(:body) { subject + "My body".force_encoding('ASCII-8BIT') }
- let(:gitaly_commit) { build(:gitaly_commit, subject: subject, body: body) }
+ let(:body_size) { body.length }
+ let(:gitaly_commit) { build(:gitaly_commit, subject: subject, body: body, body_size: body_size) }
let(:id) { gitaly_commit.id }
let(:committer) { gitaly_commit.committer }
let(:author) { gitaly_commit.author }
@@ -83,10 +84,30 @@ describe Gitlab::Git::Commit, seed_helper: true do
it { expect(commit.committer_email).to eq(committer.email) }
it { expect(commit.parent_ids).to eq(gitaly_commit.parent_ids) }
- context 'no body' do
+ context 'body_size != body.size' do
let(:body) { "".force_encoding('ASCII-8BIT') }
- it { expect(commit.safe_message).to eq(subject) }
+ context 'zero body_size' do
+ it { expect(commit.safe_message).to eq(subject) }
+ end
+
+ context 'body_size less than threshold' do
+ let(:body_size) { 123 }
+
+ it 'fetches commit message seperately' do
+ expect(described_class).to receive(:get_message).with(repository, id)
+
+ commit.safe_message
+ end
+ end
+
+ context 'body_size greater than threshold' do
+ let(:body_size) { described_class::MAX_COMMIT_MESSAGE_DISPLAY_SIZE + 1 }
+
+ it 'returns the suject plus a notice about message size' do
+ expect(commit.safe_message).to eq("My commit\n\n--commit message is too big")
+ end
+ end
end
end
@@ -589,6 +610,35 @@ describe Gitlab::Git::Commit, seed_helper: true do
it { is_expected.not_to include("feature") }
end
+ describe '.get_message' do
+ let(:commit_ids) { %w[6d394385cf567f80a8fd85055db1ab4c5295806f cfe32cf61b73a0d5e9f13e774abde7ff789b1660] }
+
+ subject do
+ commit_ids.map { |id| described_class.get_message(repository, id) }
+ end
+
+ shared_examples 'getting commit messages' do
+ it 'gets commit messages' do
+ expect(subject).to contain_exactly(
+ "Added contributing guide\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
+ "Add submodule\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n"
+ )
+ end
+ end
+
+ context 'when Gitaly commit_messages feature is enabled' do
+ it_behaves_like 'getting commit messages'
+
+ it 'gets messages in one batch', :request_store do
+ expect { subject.map(&:itself) }.to change { Gitlab::GitalyClient.get_request_count }.by(1)
+ end
+ end
+
+ context 'when Gitaly commit_messages feature is disabled', :disable_gitaly do
+ it_behaves_like 'getting commit messages'
+ end
+ end
+
def sample_commit_hash
{
author_email: "dmitriy.zaporozhets@gmail.com",
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index fcb690d8aa3..af6a486ab20 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -249,10 +249,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
subject(:metadata) { repository.archive_metadata(ref, storage_path, format, append_sha: append_sha) }
- it 'sets RepoPath to the repository path' do
- expect(metadata['RepoPath']).to eq(repository.path)
- end
-
it 'sets CommitId to the commit SHA' do
expect(metadata['CommitId']).to eq(SeedRepo::LastCommit::ID)
end
@@ -600,6 +596,10 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
+ it 'does not fail when deleting an empty list of refs' do
+ expect { repo.delete_refs(*[]) }.not_to raise_error
+ end
+
it 'raises an error if it failed' do
expect { repo.delete_refs('refs\heads\fix') }.to raise_error(Gitlab::Git::Repository::GitError)
end
@@ -2247,66 +2247,42 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe '#checksum' do
- shared_examples 'calculating checksum' do
- it 'calculates the checksum for non-empty repo' do
- expect(repository.checksum).to eq '54f21be4c32c02f6788d72207fa03ad3bce725e4'
- end
-
- it 'returns 0000000000000000000000000000000000000000 for an empty repo' do
- FileUtils.rm_rf(File.join(storage_path, 'empty-repo.git'))
-
- system(git_env, *%W(#{Gitlab.config.git.bin_path} init --bare empty-repo.git),
- chdir: storage_path,
- out: '/dev/null',
- err: '/dev/null')
+ it 'calculates the checksum for non-empty repo' do
+ expect(repository.checksum).to eq '54f21be4c32c02f6788d72207fa03ad3bce725e4'
+ end
- empty_repo = described_class.new('default', 'empty-repo.git', '')
+ it 'returns 0000000000000000000000000000000000000000 for an empty repo' do
+ FileUtils.rm_rf(File.join(storage_path, 'empty-repo.git'))
- expect(empty_repo.checksum).to eq '0000000000000000000000000000000000000000'
- end
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} init --bare empty-repo.git),
+ chdir: storage_path,
+ out: '/dev/null',
+ err: '/dev/null')
- it 'raises Gitlab::Git::Repository::InvalidRepository error for non-valid git repo' do
- FileUtils.rm_rf(File.join(storage_path, 'non-valid.git'))
+ empty_repo = described_class.new('default', 'empty-repo.git', '')
- system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{TEST_REPO_PATH} non-valid.git),
- chdir: SEED_STORAGE_PATH,
- out: '/dev/null',
- err: '/dev/null')
+ expect(empty_repo.checksum).to eq '0000000000000000000000000000000000000000'
+ end
- File.truncate(File.join(storage_path, 'non-valid.git/HEAD'), 0)
+ it 'raises Gitlab::Git::Repository::InvalidRepository error for non-valid git repo' do
+ FileUtils.rm_rf(File.join(storage_path, 'non-valid.git'))
- non_valid = described_class.new('default', 'non-valid.git', '')
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{TEST_REPO_PATH} non-valid.git),
+ chdir: SEED_STORAGE_PATH,
+ out: '/dev/null',
+ err: '/dev/null')
- expect { non_valid.checksum }.to raise_error(Gitlab::Git::Repository::InvalidRepository)
- end
+ File.truncate(File.join(storage_path, 'non-valid.git/HEAD'), 0)
- it 'raises Gitlab::Git::Repository::NoRepository error when there is no repo' do
- broken_repo = described_class.new('default', 'a/path.git', '')
+ non_valid = described_class.new('default', 'non-valid.git', '')
- expect { broken_repo.checksum }.to raise_error(Gitlab::Git::Repository::NoRepository)
- end
- end
-
- context 'when calculate_checksum Gitaly feature is enabled' do
- it_behaves_like 'calculating checksum'
+ expect { non_valid.checksum }.to raise_error(Gitlab::Git::Repository::InvalidRepository)
end
- context 'when calculate_checksum Gitaly feature is disabled', :disable_gitaly do
- it_behaves_like 'calculating checksum'
-
- describe 'when storage is broken', :broken_storage do
- it 'raises a storage exception when storage is not available' do
- broken_repo = described_class.new('broken', 'a/path.git', '')
-
- expect { broken_repo.rugged }.to raise_error(Gitlab::Git::Storage::Inaccessible)
- end
- end
-
- it "raises a Gitlab::Git::Repository::Failure error if the `popen` call to git returns a non-zero exit code" do
- allow(repository).to receive(:popen).and_return(['output', nil])
+ it 'raises Gitlab::Git::Repository::NoRepository error when there is no repo' do
+ broken_repo = described_class.new('default', 'a/path.git', '')
- expect { repository.checksum }.to raise_error Gitlab::Git::Repository::ChecksumError
- end
+ expect { broken_repo.checksum }.to raise_error(Gitlab::Git::Repository::NoRepository)
end
end
diff --git a/spec/lib/gitlab/git/tag_spec.rb b/spec/lib/gitlab/git/tag_spec.rb
index 6c4f538bf01..be2f5bfb819 100644
--- a/spec/lib/gitlab/git/tag_spec.rb
+++ b/spec/lib/gitlab/git/tag_spec.rb
@@ -32,4 +32,56 @@ describe Gitlab::Git::Tag, seed_helper: true do
context 'when Gitaly tags feature is disabled', :skip_gitaly_mock do
it_behaves_like 'Gitlab::Git::Repository#tags'
end
+
+ describe '.get_message' do
+ let(:tag_ids) { %w[f4e6814c3e4e7a0de82a9e7cd20c626cc963a2f8 8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b] }
+
+ subject do
+ tag_ids.map { |id| described_class.get_message(repository, id) }
+ end
+
+ shared_examples 'getting tag messages' do
+ it 'gets tag messages' do
+ expect(subject[0]).to eq("Release\n")
+ expect(subject[1]).to eq("Version 1.1.0\n")
+ end
+ end
+
+ context 'when Gitaly tag_messages feature is enabled' do
+ it_behaves_like 'getting tag messages'
+
+ it 'gets messages in one batch', :request_store do
+ expect { subject.map(&:itself) }.to change { Gitlab::GitalyClient.get_request_count }.by(1)
+ end
+ end
+
+ context 'when Gitaly tag_messages feature is disabled', :disable_gitaly do
+ it_behaves_like 'getting tag messages'
+ end
+ end
+
+ describe 'tag into from Gitaly tag' do
+ context 'message_size != message.size' do
+ let(:gitaly_tag) { build(:gitaly_tag, message: ''.b, message_size: message_size) }
+ let(:tag) { described_class.new(repository, gitaly_tag) }
+
+ context 'message_size less than threshold' do
+ let(:message_size) { 123 }
+
+ it 'fetches tag message seperately' do
+ expect(described_class).to receive(:get_message).with(repository, gitaly_tag.id)
+
+ tag.message
+ end
+ end
+
+ context 'message_size greater than threshold' do
+ let(:message_size) { described_class::MAX_TAG_MESSAGE_DISPLAY_SIZE + 1 }
+
+ it 'returns a notice about message size' do
+ expect(tag.message).to eq("--tag message is too big")
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index 6d63749296e..4f64f2bd6b4 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -6760,26 +6760,6 @@
"wiki_page_events": true
},
{
- "id": 92,
- "title": "Gemnasium",
- "project_id": 5,
- "created_at": "2016-06-14T15:01:51.202Z",
- "updated_at": "2016-06-14T15:01:51.202Z",
- "active": false,
- "properties": {},
- "template": false,
- "push_events": true,
- "issues_events": true,
- "merge_requests_events": true,
- "tag_push_events": true,
- "note_events": true,
- "job_events": true,
- "type": "GemnasiumService",
- "category": "common",
- "default": false,
- "wiki_page_events": true
- },
- {
"id": 91,
"title": "Flowdock",
"project_id": 5,
diff --git a/spec/lib/gitlab/incoming_email_spec.rb b/spec/lib/gitlab/incoming_email_spec.rb
index ad087f42e06..4c0c3fcbcc7 100644
--- a/spec/lib/gitlab/incoming_email_spec.rb
+++ b/spec/lib/gitlab/incoming_email_spec.rb
@@ -83,6 +83,10 @@ describe Gitlab::IncomingEmail do
it "returns reply key" do
expect(described_class.key_from_address("replies+key@example.com")).to eq("key")
end
+
+ it 'does not match emails with extra bits' do
+ expect(described_class.key_from_address('somereplies+somekey@example.com.someotherdomain.com')).to be nil
+ end
end
context 'self.key_from_fallback_message_id' do
diff --git a/spec/lib/gitlab/untrusted_regexp_spec.rb b/spec/lib/gitlab/untrusted_regexp_spec.rb
index 0ee7fa1e570..0a6ac0aa294 100644
--- a/spec/lib/gitlab/untrusted_regexp_spec.rb
+++ b/spec/lib/gitlab/untrusted_regexp_spec.rb
@@ -1,6 +1,49 @@
-require 'spec_helper'
+require 'fast_spec_helper'
+require 'support/shared_examples/malicious_regexp_shared_examples'
describe Gitlab::UntrustedRegexp do
+ describe '.valid?' do
+ it 'returns true if regexp is valid' do
+ expect(described_class.valid?('/some ( thing/'))
+ .to be false
+ end
+
+ it 'returns true if regexp is invalid' do
+ expect(described_class.valid?('/some .* thing/'))
+ .to be true
+ end
+ end
+
+ describe '.fabricate' do
+ context 'when regexp is using /regexp/ scheme with flags' do
+ it 'fabricates regexp with a single flag' do
+ regexp = described_class.fabricate('/something/i')
+
+ expect(regexp).to eq described_class.new('(?i)something')
+ expect(regexp.scan('SOMETHING')).to be_one
+ end
+
+ it 'fabricates regexp with multiple flags' do
+ regexp = described_class.fabricate('/something/im')
+
+ expect(regexp).to eq described_class.new('(?im)something')
+ end
+
+ it 'fabricates regexp without flags' do
+ regexp = described_class.fabricate('/something/')
+
+ expect(regexp).to eq described_class.new('something')
+ end
+ end
+
+ context 'when regexp is a raw pattern' do
+ it 'raises an error' do
+ expect { described_class.fabricate('some .* thing') }
+ .to raise_error(RegexpError)
+ end
+ end
+ end
+
describe '#initialize' do
subject { described_class.new(pattern) }
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index e732b089d44..660671cefaf 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::Workhorse do
- let(:project) { create(:project, :repository) }
+ set(:project) { create(:project, :repository) }
let(:repository) { project.repository }
def decode_workhorse_header(array)
@@ -55,16 +55,6 @@ describe Gitlab::Workhorse do
end
end
- context 'when Gitaly workhorse_archive feature is disabled', :disable_gitaly do
- it 'sets the header correctly' do
- key, command, params = decode_workhorse_header(subject)
-
- expect(key).to eq('Gitlab-Workhorse-Send-Data')
- expect(command).to eq('git-archive')
- expect(params).to eq(base_params)
- end
- end
-
context "when the repository doesn't have an archive file path" do
before do
allow(project.repository).to receive(:archive_metadata).and_return(Hash.new)
diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb
index 5489c17bd82..77b07cf1ac9 100644
--- a/spec/models/appearance_spec.rb
+++ b/spec/models/appearance_spec.rb
@@ -3,34 +3,11 @@ require 'rails_helper'
describe Appearance do
subject { build(:appearance) }
- it { is_expected.to be_valid }
+ it { include(CacheableAttributes) }
+ it { expect(described_class.current_without_cache).to eq(described_class.first) }
it { is_expected.to have_many(:uploads) }
- describe '.current', :use_clean_rails_memory_store_caching do
- let!(:appearance) { create(:appearance) }
-
- it 'returns the current appearance row' do
- expect(described_class.current).to eq(appearance)
- end
-
- it 'caches the result' do
- expect(described_class).to receive(:first).once
-
- 2.times { described_class.current }
- end
- end
-
- describe '#flush_redis_cache' do
- it 'flushes the cache in Redis' do
- appearance = create(:appearance)
-
- expect(Rails.cache).to receive(:delete).with(described_class::CACHE_KEY)
-
- appearance.flush_redis_cache
- end
- end
-
describe '#single_appearance_row' do
it 'adds an error when more than 1 row exists' do
create(:appearance)
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 10d6109cae7..7e47043a1cb 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -3,6 +3,9 @@ require 'spec_helper'
describe ApplicationSetting do
let(:setting) { described_class.create_from_defaults }
+ it { include(CacheableAttributes) }
+ it { expect(described_class.current_without_cache).to eq(described_class.last) }
+
it { expect(setting).to be_valid }
it { expect(setting.uuid).to be_present }
it { expect(setting).to have_db_column(:auto_devops_enabled) }
@@ -318,33 +321,6 @@ describe ApplicationSetting do
end
end
- describe '.current' do
- context 'redis unavailable' do
- it 'returns an ApplicationSetting' do
- allow(Rails.cache).to receive(:fetch).and_call_original
- allow(described_class).to receive(:last).and_return(:last)
- expect(Rails.cache).to receive(:fetch).with(ApplicationSetting::CACHE_KEY).and_raise(ArgumentError)
-
- expect(described_class.current).to eq(:last)
- end
- end
-
- context 'when an ApplicationSetting is not yet present' do
- it 'does not cache nil object' do
- # when missing settings a nil object is returned, but not cached
- allow(described_class).to receive(:last).and_return(nil).twice
- expect(described_class.current).to be_nil
-
- # when the settings are set the method returns a valid object
- allow(described_class).to receive(:last).and_return(:last)
- expect(described_class.current).to eq(:last)
-
- # subsequent calls get everything from cache
- expect(described_class.current).to eq(:last)
- end
- end
- end
-
context 'restrict creating duplicates' do
before do
described_class.create_from_defaults
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index dc810489011..96173889ccd 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -629,6 +629,14 @@ describe Ci::Build do
it { is_expected.to eq('review/host') }
end
+
+ context 'when using persisted variables' do
+ let(:build) do
+ create(:ci_build, environment: 'review/x$CI_BUILD_ID')
+ end
+
+ it { is_expected.to eq('review/x') }
+ end
end
describe '#starts_environment?' do
@@ -1270,6 +1278,46 @@ describe Ci::Build do
end
end
+ describe '#playable?' do
+ context 'when build is a manual action' do
+ context 'when build has been skipped' do
+ subject { build_stubbed(:ci_build, :manual, status: :skipped) }
+
+ it { is_expected.not_to be_playable }
+ end
+
+ context 'when build has been canceled' do
+ subject { build_stubbed(:ci_build, :manual, status: :canceled) }
+
+ it { is_expected.to be_playable }
+ end
+
+ context 'when build is successful' do
+ subject { build_stubbed(:ci_build, :manual, status: :success) }
+
+ it { is_expected.to be_playable }
+ end
+
+ context 'when build has failed' do
+ subject { build_stubbed(:ci_build, :manual, status: :failed) }
+
+ it { is_expected.to be_playable }
+ end
+
+ context 'when build is a manual untriggered action' do
+ subject { build_stubbed(:ci_build, :manual, status: :manual) }
+
+ it { is_expected.to be_playable }
+ end
+ end
+
+ context 'when build is not a manual action' do
+ subject { build_stubbed(:ci_build, :success) }
+
+ it { is_expected.not_to be_playable }
+ end
+ end
+
describe 'project settings' do
describe '#allow_git_fetch' do
it 'return project allow_git_fetch configuration' do
@@ -1485,6 +1533,7 @@ describe Ci::Build do
let(:container_registry_enabled) { false }
let(:predefined_variables) do
[
+ { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true },
{ key: 'CI_JOB_ID', value: build.id.to_s, public: true },
{ key: 'CI_JOB_TOKEN', value: build.token, public: false },
{ key: 'CI_BUILD_ID', value: build.id.to_s, public: true },
@@ -1516,7 +1565,6 @@ describe Ci::Build do
{ key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path, public: true },
{ key: 'CI_PROJECT_URL', value: project.web_url, public: true },
{ key: 'CI_PROJECT_VISIBILITY', value: 'private', public: true },
- { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true },
{ key: 'CI_CONFIG_PATH', value: pipeline.ci_yaml_file_path, public: true },
{ key: 'CI_PIPELINE_SOURCE', value: pipeline.source, public: true },
{ key: 'CI_COMMIT_MESSAGE', value: pipeline.git_commit_message, public: true },
@@ -2044,7 +2092,7 @@ describe Ci::Build do
let(:deploy_token_variables) do
[
- { key: 'CI_DEPLOY_USER', value: deploy_token.name, public: true },
+ { key: 'CI_DEPLOY_USER', value: deploy_token.username, public: true },
{ key: 'CI_DEPLOY_PASSWORD', value: deploy_token.token, public: false }
]
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index ddd66a6be87..e4f4c62bd22 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -167,13 +167,39 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe '#persisted_variables' do
+ context 'when pipeline is not persisted yet' do
+ subject { build(:ci_pipeline).persisted_variables }
+
+ it 'does not contain some variables' do
+ keys = subject.map { |variable| variable[:key] }
+
+ expect(keys).not_to include 'CI_PIPELINE_ID'
+ end
+ end
+
+ context 'when pipeline is persisted' do
+ subject { build_stubbed(:ci_pipeline).persisted_variables }
+
+ it 'does contains persisted variables' do
+ keys = subject.map { |variable| variable[:key] }
+
+ expect(keys).to eq %w[CI_PIPELINE_ID]
+ end
+ end
+ end
+
describe '#predefined_variables' do
subject { pipeline.predefined_variables }
it 'includes all predefined variables in a valid order' do
keys = subject.map { |variable| variable[:key] }
- expect(keys).to eq %w[CI_PIPELINE_ID CI_CONFIG_PATH CI_PIPELINE_SOURCE CI_COMMIT_MESSAGE CI_COMMIT_TITLE CI_COMMIT_DESCRIPTION]
+ expect(keys).to eq %w[CI_CONFIG_PATH
+ CI_PIPELINE_SOURCE
+ CI_COMMIT_MESSAGE
+ CI_COMMIT_TITLE
+ CI_COMMIT_DESCRIPTION]
end
end
@@ -774,6 +800,33 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe '#number_of_warnings' do
+ it 'returns the number of warnings' do
+ create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline, name: 'rubocop')
+
+ expect(pipeline.number_of_warnings).to eq(1)
+ end
+
+ it 'supports eager loading of the number of warnings' do
+ pipeline2 = create(:ci_empty_pipeline, status: :created, project: project)
+
+ create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline, name: 'rubocop')
+ create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline2, name: 'rubocop')
+
+ pipelines = project.pipelines.to_a
+
+ pipelines.each(&:number_of_warnings)
+
+ # To run the queries we need to actually use the lazy objects, which we do
+ # by just sending "to_i" to them.
+ amount = ActiveRecord::QueryRecorder
+ .new { pipelines.each { |p| p.number_of_warnings.to_i } }
+ .count
+
+ expect(amount).to eq(1)
+ end
+ end
+
shared_context 'with some outdated pipelines' do
before do
create_pipeline(:canceled, 'ref', 'A', project)
diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb
index aeca6ee903a..d2302583ac8 100644
--- a/spec/models/clusters/applications/prometheus_spec.rb
+++ b/spec/models/clusters/applications/prometheus_spec.rb
@@ -79,12 +79,22 @@ describe Clusters::Applications::Prometheus do
end
it 'creates proper url' do
- expect(subject.prometheus_client.url).to eq('http://example.com/api/v1/proxy/namespaces/gitlab-managed-apps/service/prometheus-prometheus-server:80')
+ expect(subject.prometheus_client.url).to eq('http://example.com/api/v1/namespaces/gitlab-managed-apps/service/prometheus-prometheus-server:80/proxy')
end
it 'copies options and headers from kube client to proxy client' do
expect(subject.prometheus_client.options).to eq(kube_client.rest_client.options.merge(headers: kube_client.headers))
end
+
+ context 'when cluster is not reachable' do
+ before do
+ allow(kube_client).to receive(:proxy_url).and_raise(Kubeclient::HttpError.new(401, 'Unauthorized', nil))
+ end
+
+ it 'returns nil' do
+ expect(subject.prometheus_client).to be_nil
+ end
+ end
end
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 448b813c2e1..090f91168ad 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -52,22 +52,98 @@ describe Commit do
end
end
- describe '#author' do
+ describe '#author', :request_store do
it 'looks up the author in a case-insensitive way' do
user = create(:user, email: commit.author_email.upcase)
expect(commit.author).to eq(user)
end
- it 'caches the author', :request_store do
+ it 'caches the author' do
user = create(:user, email: commit.author_email)
- expect(User).to receive(:find_by_any_email).and_call_original
expect(commit.author).to eq(user)
+
key = "Commit:author:#{commit.author_email.downcase}"
- expect(RequestStore.store[key]).to eq(user)
+ expect(RequestStore.store[key]).to eq(user)
expect(commit.author).to eq(user)
end
+
+ context 'using eager loading' do
+ let!(:alice) { create(:user, email: 'alice@example.com') }
+ let!(:bob) { create(:user, email: 'hunter2@example.com') }
+
+ let(:alice_commit) do
+ described_class.new(RepoHelpers.sample_commit, project).tap do |c|
+ c.author_email = 'alice@example.com'
+ end
+ end
+
+ let(:bob_commit) do
+ # The commit for Bob uses one of his alternative Emails, instead of the
+ # primary one.
+ described_class.new(RepoHelpers.sample_commit, project).tap do |c|
+ c.author_email = 'bob@example.com'
+ end
+ end
+
+ let(:eve_commit) do
+ described_class.new(RepoHelpers.sample_commit, project).tap do |c|
+ c.author_email = 'eve@example.com'
+ end
+ end
+
+ let!(:commits) { [alice_commit, bob_commit, eve_commit] }
+
+ before do
+ create(:email, user: bob, email: 'bob@example.com')
+ end
+
+ it 'executes only two SQL queries' do
+ recorder = ActiveRecord::QueryRecorder.new do
+ # Running this first ensures we don't run one query for every
+ # commit.
+ commits.each(&:lazy_author)
+
+ # This forces the execution of the SQL queries necessary to load the
+ # data.
+ commits.each { |c| c.author.try(:id) }
+ end
+
+ expect(recorder.count).to eq(2)
+ end
+
+ it "preloads the authors for Commits matching a user's primary Email" do
+ commits.each(&:lazy_author)
+
+ expect(alice_commit.author).to eq(alice)
+ end
+
+ it "preloads the authors for Commits using a User's alternative Email" do
+ commits.each(&:lazy_author)
+
+ expect(bob_commit.author).to eq(bob)
+ end
+
+ it 'sets the author to Nil if an author could not be found for a Commit' do
+ commits.each(&:lazy_author)
+
+ expect(eve_commit.author).to be_nil
+ end
+
+ it 'does not execute SQL queries once the authors are preloaded' do
+ commits.each(&:lazy_author)
+ commits.each { |c| c.author.try(:id) }
+
+ recorder = ActiveRecord::QueryRecorder.new do
+ alice_commit.author
+ bob_commit.author
+ eve_commit.author
+ end
+
+ expect(recorder.count).to be_zero
+ end
+ end
end
describe '#to_reference' do
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 2ed29052dc1..f3f2bc28d2c 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -565,4 +565,10 @@ describe CommitStatus do
it_behaves_like 'commit status enqueued'
end
end
+
+ describe '#present' do
+ subject { commit_status.present }
+
+ it { is_expected.to be_a(CommitStatusPresenter) }
+ end
end
diff --git a/spec/models/concerns/cacheable_attributes_spec.rb b/spec/models/concerns/cacheable_attributes_spec.rb
new file mode 100644
index 00000000000..49e4b23ebc7
--- /dev/null
+++ b/spec/models/concerns/cacheable_attributes_spec.rb
@@ -0,0 +1,153 @@
+require 'spec_helper'
+
+describe CacheableAttributes do
+ let(:minimal_test_class) do
+ Class.new do
+ include ActiveModel::Model
+ extend ActiveModel::Callbacks
+ define_model_callbacks :commit
+ include CacheableAttributes
+
+ def self.name
+ 'TestClass'
+ end
+
+ def self.first
+ @_first ||= new('foo' => 'a')
+ end
+
+ def self.last
+ @_last ||= new('foo' => 'a', 'bar' => 'b')
+ end
+
+ attr_accessor :attributes
+
+ def initialize(attrs = {})
+ @attributes = attrs
+ end
+ end
+ end
+
+ shared_context 'with defaults' do
+ before do
+ minimal_test_class.define_singleton_method(:defaults) do
+ { foo: 'a', bar: 'b', baz: 'c' }
+ end
+ end
+ end
+
+ describe '.current_without_cache' do
+ it 'defaults to last' do
+ expect(minimal_test_class.current_without_cache).to eq(minimal_test_class.last)
+ end
+
+ it 'can be overriden' do
+ minimal_test_class.define_singleton_method(:current_without_cache) do
+ first
+ end
+
+ expect(minimal_test_class.current_without_cache).to eq(minimal_test_class.first)
+ end
+ end
+
+ describe '.cache_key' do
+ it 'excludes cache attributes' do
+ expect(minimal_test_class.cache_key).to eq("TestClass:#{Gitlab::VERSION}:#{Gitlab.migrations_hash}:json")
+ end
+ end
+
+ describe '.defaults' do
+ it 'defaults to {}' do
+ expect(minimal_test_class.defaults).to eq({})
+ end
+
+ context 'with defaults defined' do
+ include_context 'with defaults'
+
+ it 'can be overriden' do
+ expect(minimal_test_class.defaults).to eq({ foo: 'a', bar: 'b', baz: 'c' })
+ end
+ end
+ end
+
+ describe '.build_from_defaults' do
+ include_context 'with defaults'
+
+ context 'without any attributes given' do
+ it 'intializes a new object with the defaults' do
+ expect(minimal_test_class.build_from_defaults).not_to be_persisted
+ end
+ end
+
+ context 'without attributes given' do
+ it 'intializes a new object with the given attributes merged into the defaults' do
+ expect(minimal_test_class.build_from_defaults(foo: 'd').attributes[:foo]).to eq('d')
+ end
+ end
+ end
+
+ describe '.current', :use_clean_rails_memory_store_caching do
+ context 'redis unavailable' do
+ it 'returns an uncached record' do
+ allow(minimal_test_class).to receive(:last).and_return(:last)
+ expect(Rails.cache).to receive(:read).and_raise(Redis::BaseError)
+
+ expect(minimal_test_class.current).to eq(:last)
+ end
+ end
+
+ context 'when a record is not yet present' do
+ it 'does not cache nil object' do
+ # when missing settings a nil object is returned, but not cached
+ allow(minimal_test_class).to receive(:last).twice.and_return(nil)
+
+ expect(minimal_test_class.current).to be_nil
+ expect(Rails.cache.exist?(minimal_test_class.cache_key)).to be(false)
+ end
+
+ it 'cache non-nil object' do
+ # when the settings are set the method returns a valid object
+ allow(minimal_test_class).to receive(:last).and_call_original
+
+ expect(minimal_test_class.current).to eq(minimal_test_class.last)
+ expect(Rails.cache.exist?(minimal_test_class.cache_key)).to be(true)
+
+ # subsequent calls retrieve the record from the cache
+ last_record = minimal_test_class.last
+ expect(minimal_test_class).not_to receive(:last)
+ expect(minimal_test_class.current.attributes).to eq(last_record.attributes)
+ end
+ end
+ end
+
+ describe '.cached', :use_clean_rails_memory_store_caching do
+ context 'when cache is cold' do
+ it 'returns nil' do
+ expect(minimal_test_class.cached).to be_nil
+ end
+ end
+
+ context 'when cached settings do not include the latest defaults' do
+ before do
+ Rails.cache.write(minimal_test_class.cache_key, { bar: 'b', baz: 'c' }.to_json)
+ minimal_test_class.define_singleton_method(:defaults) do
+ { foo: 'a', bar: 'b', baz: 'c' }
+ end
+ end
+
+ it 'includes attributes from defaults' do
+ expect(minimal_test_class.cached.attributes[:foo]).to eq(minimal_test_class.defaults[:foo])
+ end
+ end
+ end
+
+ describe '#cache!', :use_clean_rails_memory_store_caching do
+ let(:appearance_record) { create(:appearance) }
+
+ it 'caches the attributes' do
+ appearance_record.cache!
+
+ expect(Rails.cache.read(Appearance.cache_key)).to eq(appearance_record.attributes.to_json)
+ end
+ end
+end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 3d3092b8ac9..bd6bf5b0712 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -266,6 +266,19 @@ describe Issuable do
end
end
+ describe '#time_estimate=' do
+ it 'coerces the value below Gitlab::Database::MAX_INT_VALUE' do
+ expect { issue.time_estimate = 100 }.to change { issue.time_estimate }.to(100)
+ expect { issue.time_estimate = Gitlab::Database::MAX_INT_VALUE + 100 }.to change { issue.time_estimate }.to(Gitlab::Database::MAX_INT_VALUE)
+ end
+
+ it 'skips coercion for not Integer values' do
+ expect { issue.time_estimate = nil }.to change { issue.time_estimate }.to(nil)
+ expect { issue.time_estimate = 'invalid time' }.not_to raise_error(StandardError)
+ expect { issue.time_estimate = 22.33 }.not_to raise_error(StandardError)
+ end
+ end
+
describe '#to_hook_data' do
let(:builder) { double }
diff --git a/spec/models/concerns/redis_cacheable_spec.rb b/spec/models/concerns/redis_cacheable_spec.rb
index 3d7963120b6..23c6c6233e9 100644
--- a/spec/models/concerns/redis_cacheable_spec.rb
+++ b/spec/models/concerns/redis_cacheable_spec.rb
@@ -1,21 +1,36 @@
require 'spec_helper'
describe RedisCacheable do
- let(:model) { double }
+ let(:model) do
+ Struct.new(:id, :attributes) do
+ def read_attribute(attribute)
+ attributes[attribute]
+ end
+
+ def cast_value_from_cache(attribute, cached_value)
+ cached_value
+ end
+
+ def has_attribute?(attribute)
+ attributes.has_key?(attribute)
+ end
+ end
+ end
+
+ let(:payload) { { name: 'value', time: Time.zone.now } }
+ let(:instance) { model.new(1, payload) }
+ let(:cache_key) { instance.__send__(:cache_attribute_key) }
before do
- model.extend(described_class)
- allow(model).to receive(:cache_attribute_key).and_return('key')
+ model.include(described_class)
end
describe '#cached_attribute' do
- let(:payload) { { attribute: 'value' } }
-
- subject { model.cached_attribute(payload.keys.first) }
+ subject { instance.cached_attribute(payload.keys.first) }
it 'gets the cache attribute' do
Gitlab::Redis::SharedState.with do |redis|
- expect(redis).to receive(:get).with('key')
+ expect(redis).to receive(:get).with(cache_key)
.and_return(payload.to_json)
end
@@ -24,16 +39,62 @@ describe RedisCacheable do
end
describe '#cache_attributes' do
- let(:values) { { name: 'new_name' } }
-
- subject { model.cache_attributes(values) }
+ subject { instance.cache_attributes(payload) }
it 'sets the cache attributes' do
Gitlab::Redis::SharedState.with do |redis|
- expect(redis).to receive(:set).with('key', values.to_json, anything)
+ expect(redis).to receive(:set).with(cache_key, payload.to_json, anything)
end
subject
end
end
+
+ describe '#cached_attr_reader', :clean_gitlab_redis_shared_state do
+ subject { instance.name }
+
+ before do
+ model.cached_attr_reader(:name)
+ end
+
+ context 'when there is no cached value' do
+ it 'reads the attribute' do
+ expect(instance).to receive(:read_attribute).and_call_original
+
+ expect(subject).to eq(payload[:name])
+ end
+ end
+
+ context 'when there is a cached value' do
+ it 'reads the cached value' do
+ expect(instance).not_to receive(:read_attribute)
+
+ instance.cache_attributes(payload)
+
+ expect(subject).to eq(payload[:name])
+ end
+ end
+
+ it 'always returns the latest values' do
+ expect(instance.name).to eq(payload[:name])
+
+ instance.cache_attributes(name: 'new_value')
+
+ expect(instance.name).to eq('new_value')
+ end
+ end
+
+ describe '#cast_value_from_cache' do
+ subject { instance.__send__(:cast_value_from_cache, attribute, value) }
+
+ context 'with runner contacted_at' do
+ let(:instance) { Ci::Runner.new }
+ let(:attribute) { :contacted_at }
+ let(:value) { '2018-05-07 13:53:08 UTC' }
+
+ it 'converts cache string to appropriate type' do
+ expect(subject).to be_an_instance_of(ActiveSupport::TimeWithZone)
+ end
+ end
+ end
end
diff --git a/spec/models/concerns/sortable_spec.rb b/spec/models/concerns/sortable_spec.rb
new file mode 100644
index 00000000000..b821a84d5e0
--- /dev/null
+++ b/spec/models/concerns/sortable_spec.rb
@@ -0,0 +1,108 @@
+require 'spec_helper'
+
+describe Sortable do
+ describe '.order_by' do
+ let(:relation) { Group.all }
+
+ describe 'ordering by id' do
+ it 'ascending' do
+ expect(relation).to receive(:reorder).with(id: :asc)
+
+ relation.order_by('id_asc')
+ end
+
+ it 'descending' do
+ expect(relation).to receive(:reorder).with(id: :desc)
+
+ relation.order_by('id_desc')
+ end
+ end
+
+ describe 'ordering by created day' do
+ it 'ascending' do
+ expect(relation).to receive(:reorder).with(created_at: :asc)
+
+ relation.order_by('created_asc')
+ end
+
+ it 'descending' do
+ expect(relation).to receive(:reorder).with(created_at: :desc)
+
+ relation.order_by('created_desc')
+ end
+
+ it 'order by "date"' do
+ expect(relation).to receive(:reorder).with(created_at: :desc)
+
+ relation.order_by('created_date')
+ end
+ end
+
+ describe 'ordering by name' do
+ it 'ascending' do
+ expect(relation).to receive(:reorder).with("lower(name) asc")
+
+ relation.order_by('name_asc')
+ end
+
+ it 'descending' do
+ expect(relation).to receive(:reorder).with("lower(name) desc")
+
+ relation.order_by('name_desc')
+ end
+ end
+
+ describe 'ordering by Updated Time' do
+ it 'ascending' do
+ expect(relation).to receive(:reorder).with(updated_at: :asc)
+
+ relation.order_by('updated_asc')
+ end
+
+ it 'descending' do
+ expect(relation).to receive(:reorder).with(updated_at: :desc)
+
+ relation.order_by('updated_desc')
+ end
+ end
+
+ it 'does not call reorder in case of unrecognized ordering' do
+ expect(relation).not_to receive(:reorder)
+
+ relation.order_by('random_ordering')
+ end
+ end
+
+ describe 'sorting groups' do
+ def ordered_group_names(order)
+ Group.all.order_by(order).map(&:name)
+ end
+
+ let!(:ref_time) { Time.parse('2018-05-01 00:00:00') }
+ let!(:group1) { create(:group, name: 'aa', id: 1, created_at: ref_time - 15.seconds, updated_at: ref_time) }
+ let!(:group2) { create(:group, name: 'AAA', id: 2, created_at: ref_time - 10.seconds, updated_at: ref_time - 5.seconds) }
+ let!(:group3) { create(:group, name: 'BB', id: 3, created_at: ref_time - 5.seconds, updated_at: ref_time - 10.seconds) }
+ let!(:group4) { create(:group, name: 'bbb', id: 4, created_at: ref_time, updated_at: ref_time - 15.seconds) }
+
+ it 'sorts groups by id' do
+ expect(ordered_group_names('id_asc')).to eq(%w(aa AAA BB bbb))
+ expect(ordered_group_names('id_desc')).to eq(%w(bbb BB AAA aa))
+ end
+
+ it 'sorts groups by name via case-insentitive comparision' do
+ expect(ordered_group_names('name_asc')).to eq(%w(aa AAA BB bbb))
+ expect(ordered_group_names('name_desc')).to eq(%w(bbb BB AAA aa))
+ end
+
+ it 'sorts groups by created_at' do
+ expect(ordered_group_names('created_asc')).to eq(%w(aa AAA BB bbb))
+ expect(ordered_group_names('created_desc')).to eq(%w(bbb BB AAA aa))
+ expect(ordered_group_names('created_date')).to eq(%w(bbb BB AAA aa))
+ end
+
+ it 'sorts groups by updated_at' do
+ expect(ordered_group_names('updated_asc')).to eq(%w(bbb BB AAA aa))
+ expect(ordered_group_names('updated_desc')).to eq(%w(aa AAA BB bbb))
+ end
+ end
+end
diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb
index 673049d1cc4..a3e68d2e646 100644
--- a/spec/models/generic_commit_status_spec.rb
+++ b/spec/models/generic_commit_status_spec.rb
@@ -78,4 +78,10 @@ describe GenericCommitStatus do
it { is_expected.not_to be_nil }
end
end
+
+ describe '#present' do
+ subject { generic_commit_status.present }
+
+ it { is_expected.to be_a(GenericCommitStatusPresenter) }
+ end
end
diff --git a/spec/models/project_services/gemnasium_service_spec.rb b/spec/models/project_services/gemnasium_service_spec.rb
index 4c61bc0af95..6e417323e40 100644
--- a/spec/models/project_services/gemnasium_service_spec.rb
+++ b/spec/models/project_services/gemnasium_service_spec.rb
@@ -26,24 +26,49 @@ describe GemnasiumService do
end
end
+ describe "deprecated?" do
+ let(:project) { create(:project, :repository) }
+ let(:gemnasium_service) { described_class.new }
+
+ before do
+ allow(gemnasium_service).to receive_messages(
+ project_id: project.id,
+ project: project,
+ service_hook: true,
+ token: 'verySecret',
+ api_key: 'GemnasiumUserApiKey'
+ )
+ end
+
+ it "is true" do
+ expect(gemnasium_service.deprecated?).to be true
+ end
+
+ it "can't create a new service" do
+ expect(gemnasium_service.save).to be false
+ expect(gemnasium_service.errors[:base].first)
+ .to eq('Gemnasium has been acquired by GitLab in January 2018. Since May 15, 2018, the service provided by Gemnasium is no longer available.')
+ end
+ end
+
describe "Execute" do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
+ let(:gemnasium_service) { described_class.new }
+ let(:sample_data) { Gitlab::DataBuilder::Push.build_sample(project, user) }
before do
- @gemnasium_service = described_class.new
- allow(@gemnasium_service).to receive_messages(
+ allow(gemnasium_service).to receive_messages(
project_id: project.id,
project: project,
service_hook: true,
token: 'verySecret',
api_key: 'GemnasiumUserApiKey'
)
- @sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
end
it "calls Gemnasium service" do
expect(Gemnasium::GitlabService).to receive(:execute).with(an_instance_of(Hash)).once
- @gemnasium_service.execute(@sample_data)
+ gemnasium_service.execute(sample_data)
end
end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 39625b559eb..af2240f4f89 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -469,6 +469,34 @@ describe Project do
end
end
+ describe "#readme_url" do
+ let(:project) { create(:project, :repository, path: "somewhere") }
+
+ context 'with a non-existing repository' do
+ it 'returns nil' do
+ allow(project.repository).to receive(:tree).with(:head).and_return(nil)
+
+ expect(project.readme_url).to be_nil
+ end
+ end
+
+ context 'with an existing repository' do
+ context 'when no README exists' do
+ it 'returns nil' do
+ allow_any_instance_of(Tree).to receive(:readme).and_return(nil)
+
+ expect(project.readme_url).to be_nil
+ end
+ end
+
+ context 'when a README exists' do
+ it 'returns the README' do
+ expect(project.readme_url).to eql("#{Gitlab.config.gitlab.url}/#{project.namespace.full_path}/somewhere/blob/master/README.md")
+ end
+ end
+ end
+ end
+
describe "#new_issuable_address" do
let(:project) { create(:project, path: "somewhere") }
let(:user) { create(:user) }
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index ac8d9a32d4e..0ccf55bd895 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -671,7 +671,7 @@ describe Repository do
end
end
- describe "search_files_by_content" do
+ shared_examples "search_files_by_content" do
let(:results) { repository.search_files_by_content('feature', 'master') }
subject { results }
@@ -718,7 +718,7 @@ describe Repository do
end
end
- describe "search_files_by_name" do
+ shared_examples "search_files_by_name" do
let(:results) { repository.search_files_by_name('files', 'master') }
it 'returns result' do
@@ -758,6 +758,16 @@ describe Repository do
end
end
+ describe 'with gitaly enabled' do
+ it_behaves_like 'search_files_by_content'
+ it_behaves_like 'search_files_by_name'
+ end
+
+ describe 'with gitaly disabled', :disable_gitaly do
+ it_behaves_like 'search_files_by_content'
+ it_behaves_like 'search_files_by_name'
+ end
+
describe '#async_remove_remote' do
before do
masterrev = repository.find_branch('master').dereferenced_target
@@ -2021,27 +2031,27 @@ describe Repository do
describe '#xcode_project?' do
before do
- allow(repository).to receive(:tree).with(:head).and_return(double(:tree, blobs: [blob]))
+ allow(repository).to receive(:tree).with(:head).and_return(double(:tree, trees: [tree]))
end
- context 'when the root contains a *.xcodeproj file' do
- let(:blob) { double(:blob, path: 'Foo.xcodeproj') }
+ context 'when the root contains a *.xcodeproj directory' do
+ let(:tree) { double(:tree, path: 'Foo.xcodeproj') }
it 'returns true' do
expect(repository.xcode_project?).to be_truthy
end
end
- context 'when the root contains a *.xcworkspace file' do
- let(:blob) { double(:blob, path: 'Foo.xcworkspace') }
+ context 'when the root contains a *.xcworkspace directory' do
+ let(:tree) { double(:tree, path: 'Foo.xcworkspace') }
it 'returns true' do
expect(repository.xcode_project?).to be_truthy
end
end
- context 'when the root contains no XCode config file' do
- let(:blob) { double(:blob, path: 'subdir/Foo.xcworkspace') }
+ context 'when the root contains no Xcode config directory' do
+ let(:tree) { double(:tree, path: 'Foo') }
it 'returns false' do
expect(repository.xcode_project?).to be_falsey
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 684fa030baf..6a2f4a39f09 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -393,24 +393,6 @@ describe User do
end
describe 'after commit hook' do
- describe '.update_invalid_gpg_signatures' do
- let(:user) do
- create(:user, email: 'tula.torphy@abshire.ca').tap do |user|
- user.skip_reconfirmation!
- end
- end
-
- it 'does nothing when the name is updated' do
- expect(user).not_to receive(:update_invalid_gpg_signatures)
- user.update_attributes!(name: 'Bette')
- end
-
- it 'synchronizes the gpg keys when the email is updated' do
- expect(user).to receive(:update_invalid_gpg_signatures).at_most(:twice)
- user.update_attributes!(email: 'shawnee.ritchie@denesik.com')
- end
- end
-
describe '#update_emails_with_primary_email' do
before do
@user = create(:user, email: 'primary@example.com').tap do |user|
@@ -450,6 +432,76 @@ describe User do
expect(@user.emails.first.confirmed_at).not_to eq nil
end
end
+
+ describe '#update_notification_email' do
+ # Regression: https://gitlab.com/gitlab-org/gitlab-ce/issues/22846
+ context 'when changing :email' do
+ let(:user) { create(:user) }
+ let(:new_email) { 'new-email@example.com' }
+
+ it 'sets :unconfirmed_email' do
+ expect do
+ user.tap { |u| u.update!(email: new_email) }.reload
+ end.to change(user, :unconfirmed_email).to(new_email)
+ end
+
+ it 'does not change :notification_email' do
+ expect do
+ user.tap { |u| u.update!(email: new_email) }.reload
+ end.not_to change(user, :notification_email)
+ end
+
+ it 'updates :notification_email to the new email once confirmed' do
+ user.update!(email: new_email)
+
+ expect do
+ user.tap(&:confirm).reload
+ end.to change(user, :notification_email).to eq(new_email)
+ end
+
+ context 'and :notification_email is set to a secondary email' do
+ let!(:email_attrs) { attributes_for(:email, :confirmed, user: user) }
+ let(:secondary) { create(:email, :confirmed, email: 'secondary@example.com', user: user) }
+
+ before do
+ user.emails.create(email_attrs)
+ user.tap { |u| u.update!(notification_email: email_attrs[:email]) }.reload
+ end
+
+ it 'does not change :notification_email to :email' do
+ expect do
+ user.tap { |u| u.update!(email: new_email) }.reload
+ end.not_to change(user, :notification_email)
+ end
+
+ it 'does not change :notification_email to :email once confirmed' do
+ user.update!(email: new_email)
+
+ expect do
+ user.tap(&:confirm).reload
+ end.not_to change(user, :notification_email)
+ end
+ end
+ end
+ end
+
+ describe '#update_invalid_gpg_signatures' do
+ let(:user) do
+ create(:user, email: 'tula.torphy@abshire.ca').tap do |user|
+ user.skip_reconfirmation!
+ end
+ end
+
+ it 'does nothing when the name is updated' do
+ expect(user).not_to receive(:update_invalid_gpg_signatures)
+ user.update_attributes!(name: 'Bette')
+ end
+
+ it 'synchronizes the gpg keys when the email is updated' do
+ expect(user).to receive(:update_invalid_gpg_signatures).at_most(:twice)
+ user.update_attributes!(email: 'shawnee.ritchie@denesik.com')
+ end
+ end
end
describe '#update_tracked_fields!', :clean_gitlab_redis_shared_state do
diff --git a/spec/presenters/ci/build_presenter_spec.rb b/spec/presenters/ci/build_presenter_spec.rb
index 4bc005df2fc..efd175247b5 100644
--- a/spec/presenters/ci/build_presenter_spec.rb
+++ b/spec/presenters/ci/build_presenter_spec.rb
@@ -10,7 +10,7 @@ describe Ci::BuildPresenter do
end
it 'inherits from Gitlab::View::Presenter::Delegated' do
- expect(described_class.superclass).to eq(Gitlab::View::Presenter::Delegated)
+ expect(described_class.ancestors).to include(Gitlab::View::Presenter::Delegated)
end
describe '#initialize' do
diff --git a/spec/presenters/commit_status_presenter_spec.rb b/spec/presenters/commit_status_presenter_spec.rb
new file mode 100644
index 00000000000..f81ee44e371
--- /dev/null
+++ b/spec/presenters/commit_status_presenter_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe CommitStatusPresenter do
+ let(:project) { create(:project) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+
+ subject(:presenter) do
+ described_class.new(build)
+ end
+
+ it 'inherits from Gitlab::View::Presenter::Delegated' do
+ expect(described_class.superclass).to eq(Gitlab::View::Presenter::Delegated)
+ end
+end
diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb
index 53d48a91007..fdddca5d0ef 100644
--- a/spec/requests/api/environments_spec.rb
+++ b/spec/requests/api/environments_spec.rb
@@ -15,7 +15,7 @@ describe API::Environments do
it 'returns project environments' do
project_data_keys = %w(
id description default_branch tag_list
- ssh_url_to_repo http_url_to_repo web_url
+ ssh_url_to_repo http_url_to_repo web_url readme_url
name name_with_namespace
path path_with_namespace
star_count forks_count
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index bb0034e3237..7d923932309 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -738,13 +738,16 @@ describe API::Groups do
describe "DELETE /groups/:id" do
context "when authenticated as user" do
it "removes group" do
- delete api("/groups/#{group1.id}", user1)
+ Sidekiq::Testing.fake! do
+ expect { delete api("/groups/#{group1.id}", user1) }.to change(GroupDestroyWorker.jobs, :size).by(1)
+ end
- expect(response).to have_gitlab_http_status(204)
+ expect(response).to have_gitlab_http_status(202)
end
it_behaves_like '412 response' do
let(:request) { api("/groups/#{group1.id}", user1) }
+ let(:success_status) { 202 }
end
it "does not remove a group if not an owner" do
@@ -773,7 +776,7 @@ describe API::Groups do
it "removes any existing group" do
delete api("/groups/#{group2.id}", admin)
- expect(response).to have_gitlab_http_status(204)
+ expect(response).to have_gitlab_http_status(202)
end
it "does not remove a non existing group" do
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 60e174ff92a..3106083293f 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -126,6 +126,15 @@ describe API::Issues do
it 'returns issues assigned to me' do
issue2 = create(:issue, assignees: [user2], project: project)
+ get api('/issues', user2), scope: 'assigned_to_me'
+
+ expect_paginated_array_response(size: 1)
+ expect(first_issue['id']).to eq(issue2.id)
+ end
+
+ it 'returns issues assigned to me (kebab-case)' do
+ issue2 = create(:issue, assignees: [user2], project: project)
+
get api('/issues', user2), scope: 'assigned-to-me'
expect_paginated_array_response(size: 1)
diff --git a/spec/requests/api/markdown_spec.rb b/spec/requests/api/markdown_spec.rb
new file mode 100644
index 00000000000..a55796cf343
--- /dev/null
+++ b/spec/requests/api/markdown_spec.rb
@@ -0,0 +1,112 @@
+require "spec_helper"
+
+describe API::Markdown do
+ RSpec::Matchers.define_negated_matcher :exclude, :include
+
+ describe "POST /markdown" do
+ let(:user) {} # No-op. It gets overwritten in the contexts below.
+
+ before do
+ post api("/markdown", user), params
+ end
+
+ shared_examples "rendered markdown text without GFM" do
+ it "renders markdown text" do
+ expect(response).to have_http_status(201)
+ expect(response.headers["Content-Type"]).to eq("application/json")
+ expect(json_response).to be_a(Hash)
+ expect(json_response["html"]).to eq("<p>#{text}</p>")
+ end
+ end
+
+ shared_examples "404 Project Not Found" do
+ it "responses with 404 Not Found" do
+ expect(response).to have_http_status(404)
+ expect(response.headers["Content-Type"]).to eq("application/json")
+ expect(json_response).to be_a(Hash)
+ expect(json_response["message"]).to eq("404 Project Not Found")
+ end
+ end
+
+ context "when arguments are invalid" do
+ context "when text is missing" do
+ let(:params) { {} }
+
+ it "responses with 400 Bad Request" do
+ expect(response).to have_http_status(400)
+ expect(response.headers["Content-Type"]).to eq("application/json")
+ expect(json_response).to be_a(Hash)
+ expect(json_response["error"]).to eq("text is missing")
+ end
+ end
+
+ context "when project is not found" do
+ let(:params) { { text: "Hello world!", gfm: true, project: "Dummy project" } }
+
+ it_behaves_like "404 Project Not Found"
+ end
+ end
+
+ context "when arguments are valid" do
+ set(:project) { create(:project) }
+ set(:issue) { create(:issue, project: project) }
+ let(:text) { ":tada: Hello world! :100: #{issue.to_reference}" }
+
+ context "when not using gfm" do
+ context "without project" do
+ let(:params) { { text: text } }
+
+ it_behaves_like "rendered markdown text without GFM"
+ end
+
+ context "with project" do
+ let(:params) { { text: text, project: project.full_path } }
+
+ context "when not authorized" do
+ it_behaves_like "404 Project Not Found"
+ end
+
+ context "when authorized" do
+ let(:user) { project.owner }
+
+ it_behaves_like "rendered markdown text without GFM"
+ end
+ end
+ end
+
+ context "when using gfm" do
+ context "without project" do
+ let(:params) { { text: text, gfm: true } }
+
+ it "renders markdown text" do
+ expect(response).to have_http_status(201)
+ expect(response.headers["Content-Type"]).to eq("application/json")
+ expect(json_response).to be_a(Hash)
+ expect(json_response["html"]).to include("Hello world!")
+ .and include('data-name="tada"')
+ .and include('data-name="100"')
+ .and include("#1")
+ .and exclude("<a href=\"#{IssuesHelper.url_for_issue(issue.iid, project)}\"")
+ .and exclude("#1</a>")
+ end
+ end
+
+ context "with project" do
+ let(:params) { { text: text, gfm: true, project: project.full_path } }
+ let(:user) { project.owner }
+
+ it "renders markdown text" do
+ expect(response).to have_http_status(201)
+ expect(response.headers["Content-Type"]).to eq("application/json")
+ expect(json_response).to be_a(Hash)
+ expect(json_response["html"]).to include("Hello world!")
+ .and include('data-name="tada"')
+ .and include('data-name="100"')
+ .and include("<a href=\"#{IssuesHelper.url_for_issue(issue.iid, project)}\"")
+ .and include("#1</a>")
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index f64623d7018..1eeeb4f1045 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -34,8 +34,7 @@ describe API::MergeRequests do
it 'returns an array of all merge requests' do
get api('/merge_requests', user), scope: 'all'
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
+ expect_paginated_array_response
end
it "returns authentication error without any scope" do
@@ -50,11 +49,23 @@ describe API::MergeRequests do
expect(response).to have_gitlab_http_status(401)
end
+ it "returns authentication error when scope is assigned_to_me" do
+ get api("/merge_requests"), scope: 'assigned_to_me'
+
+ expect(response).to have_gitlab_http_status(401)
+ end
+
it "returns authentication error when scope is created-by-me" do
get api("/merge_requests"), scope: 'created-by-me'
expect(response).to have_gitlab_http_status(401)
end
+
+ it "returns authentication error when scope is created_by_me" do
+ get api("/merge_requests"), scope: 'created_by_me'
+
+ expect(response).to have_gitlab_http_status(401)
+ end
end
context 'when authenticated' do
@@ -62,27 +73,14 @@ describe API::MergeRequests do
let!(:merge_request2) { create(:merge_request, :simple, author: user, assignee: user, source_project: project2, target_project: project2) }
let(:user2) { create(:user) }
- it 'returns an array of all merge requests' do
- get api('/merge_requests', user), scope: :all
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.map { |mr| mr['id'] })
- .to contain_exactly(merge_request.id, merge_request_closed.id, merge_request_merged.id, merge_request2.id)
- end
-
- it 'does not return unauthorized merge requests' do
+ it 'returns an array of all merge requests except unauthorized ones' do
private_project = create(:project, :private)
merge_request3 = create(:merge_request, :simple, source_project: private_project, target_project: private_project, source_branch: 'other-branch')
get api('/merge_requests', user), scope: :all
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.map { |mr| mr['id'] })
- .not_to include(merge_request3.id)
+ expect_response_contain_exactly(merge_request2, merge_request_merged, merge_request_closed, merge_request)
+ expect(json_response.map { |mr| mr['id'] }).not_to include(merge_request3.id)
end
it 'returns an array of merge requests created by current user if no scope is given' do
@@ -90,10 +88,7 @@ describe API::MergeRequests do
get api('/merge_requests', user2)
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
- expect(json_response.first['id']).to eq(merge_request3.id)
+ expect_response_ordered_exactly(merge_request3)
end
it 'returns an array of merge requests authored by the given user' do
@@ -101,10 +96,7 @@ describe API::MergeRequests do
get api('/merge_requests', user), author_id: user2.id, scope: :all
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
- expect(json_response.first['id']).to eq(merge_request3.id)
+ expect_response_ordered_exactly(merge_request3)
end
it 'returns an array of merge requests assigned to the given user' do
@@ -112,32 +104,39 @@ describe API::MergeRequests do
get api('/merge_requests', user), assignee_id: user2.id, scope: :all
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
- expect(json_response.first['id']).to eq(merge_request3.id)
+ expect_response_ordered_exactly(merge_request3)
end
it 'returns an array of merge requests assigned to me' do
merge_request3 = create(:merge_request, :simple, author: user, assignee: user2, source_project: project2, target_project: project2, source_branch: 'other-branch')
+ get api('/merge_requests', user2), scope: 'assigned_to_me'
+
+ expect_response_ordered_exactly(merge_request3)
+ end
+
+ it 'returns an array of merge requests assigned to me (kebab-case)' do
+ merge_request3 = create(:merge_request, :simple, author: user, assignee: user2, source_project: project2, target_project: project2, source_branch: 'other-branch')
+
get api('/merge_requests', user2), scope: 'assigned-to-me'
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
- expect(json_response.first['id']).to eq(merge_request3.id)
+ expect_response_ordered_exactly(merge_request3)
end
it 'returns an array of merge requests created by me' do
merge_request3 = create(:merge_request, :simple, author: user2, assignee: user, source_project: project2, target_project: project2, source_branch: 'other-branch')
+ get api('/merge_requests', user2), scope: 'created_by_me'
+
+ expect_response_ordered_exactly(merge_request3)
+ end
+
+ it 'returns an array of merge requests created by me (kebab-case)' do
+ merge_request3 = create(:merge_request, :simple, author: user2, assignee: user, source_project: project2, target_project: project2, source_branch: 'other-branch')
+
get api('/merge_requests', user2), scope: 'created-by-me'
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
- expect(json_response.first['id']).to eq(merge_request3.id)
+ expect_response_ordered_exactly(merge_request3)
end
it 'returns merge requests reacted by the authenticated user by the given emoji' do
@@ -146,19 +145,14 @@ describe API::MergeRequests do
get api('/merge_requests', user2), my_reaction_emoji: award_emoji.name, scope: 'all'
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
- expect(json_response.first['id']).to eq(merge_request3.id)
+ expect_response_ordered_exactly(merge_request3)
end
context 'source_branch param' do
it 'returns merge requests with the given source branch' do
get api('/merge_requests', user), source_branch: merge_request_closed.source_branch, state: 'all'
- expect(json_response.length).to eq(2)
- expect(json_response.map { |mr| mr['id'] })
- .to contain_exactly(merge_request_closed.id, merge_request_merged.id)
+ expect_response_contain_exactly(merge_request_closed, merge_request_merged)
end
end
@@ -166,9 +160,7 @@ describe API::MergeRequests do
it 'returns merge requests with the given target branch' do
get api('/merge_requests', user), target_branch: merge_request_closed.target_branch, state: 'all'
- expect(json_response.length).to eq(2)
- expect(json_response.map { |mr| mr['id'] })
- .to contain_exactly(merge_request_closed.id, merge_request_merged.id)
+ expect_response_contain_exactly(merge_request_closed, merge_request_merged)
end
end
@@ -177,8 +169,7 @@ describe API::MergeRequests do
get api('/merge_requests?created_before=2000-01-02T00:00:00.060Z', user)
- expect(json_response.size).to eq(1)
- expect(json_response.first['id']).to eq(merge_request2.id)
+ expect_response_ordered_exactly(merge_request2)
end
it 'returns merge requests created after a specific date' do
@@ -186,8 +177,7 @@ describe API::MergeRequests do
get api("/merge_requests?created_after=#{merge_request2.created_at}", user)
- expect(json_response.size).to eq(1)
- expect(json_response.first['id']).to eq(merge_request2.id)
+ expect_response_ordered_exactly(merge_request2)
end
it 'returns merge requests updated before a specific date' do
@@ -195,8 +185,7 @@ describe API::MergeRequests do
get api('/merge_requests?updated_before=2000-01-02T00:00:00.060Z', user)
- expect(json_response.size).to eq(1)
- expect(json_response.first['id']).to eq(merge_request2.id)
+ expect_response_ordered_exactly(merge_request2)
end
it 'returns merge requests updated after a specific date' do
@@ -204,8 +193,7 @@ describe API::MergeRequests do
get api("/merge_requests?updated_after=#{merge_request2.updated_at}", user)
- expect(json_response.size).to eq(1)
- expect(json_response.first['id']).to eq(merge_request2.id)
+ expect_response_ordered_exactly(merge_request2)
end
context 'search params' do
@@ -216,15 +204,13 @@ describe API::MergeRequests do
it 'returns merge requests matching given search string for title' do
get api("/merge_requests", user), search: merge_request.title
- expect(json_response.length).to eq(1)
- expect(json_response.first['id']).to eq(merge_request.id)
+ expect_response_ordered_exactly(merge_request)
end
it 'returns merge requests for project matching given search string for description' do
get api("/merge_requests", user), project_id: project.id, search: merge_request.description
- expect(json_response.length).to eq(1)
- expect(json_response.first['id']).to eq(merge_request.id)
+ expect_response_ordered_exactly(merge_request)
end
end
end
@@ -235,8 +221,7 @@ describe API::MergeRequests do
it 'returns merge requests for public projects' do
get api("/projects/#{project.id}/merge_requests")
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
+ expect_paginated_array_response
end
it "returns 404 for non public projects" do
@@ -265,10 +250,7 @@ describe API::MergeRequests do
it "returns an array of all merge_requests" do
get api("/projects/#{project.id}/merge_requests", user)
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(3)
+ expect_response_ordered_exactly(merge_request_merged, merge_request_closed, merge_request)
expect(json_response.last['title']).to eq(merge_request.title)
expect(json_response.last).to have_key('web_url')
expect(json_response.last['sha']).to eq(merge_request.diff_head_sha)
@@ -286,11 +268,8 @@ describe API::MergeRequests do
it "returns an array of all merge_requests using simple mode" do
get api("/projects/#{project.id}/merge_requests?view=simple", user)
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
+ expect_response_ordered_exactly(merge_request_merged, merge_request_closed, merge_request)
expect(json_response.last.keys).to match_array(%w(id iid title web_url created_at description project_id state updated_at))
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(3)
expect(json_response.last['iid']).to eq(merge_request.iid)
expect(json_response.last['title']).to eq(merge_request.title)
expect(json_response.last).to have_key('web_url')
@@ -302,51 +281,36 @@ describe API::MergeRequests do
it "returns an array of all merge_requests" do
get api("/projects/#{project.id}/merge_requests?state", user)
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(3)
+ expect_response_ordered_exactly(merge_request_merged, merge_request_closed, merge_request)
expect(json_response.last['title']).to eq(merge_request.title)
end
it "returns an array of open merge_requests" do
get api("/projects/#{project.id}/merge_requests?state=opened", user)
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
+ expect_response_ordered_exactly(merge_request)
expect(json_response.last['title']).to eq(merge_request.title)
end
it "returns an array of closed merge_requests" do
get api("/projects/#{project.id}/merge_requests?state=closed", user)
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
+ expect_response_ordered_exactly(merge_request_closed)
expect(json_response.first['title']).to eq(merge_request_closed.title)
end
it "returns an array of merged merge_requests" do
get api("/projects/#{project.id}/merge_requests?state=merged", user)
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
+ expect_response_ordered_exactly(merge_request_merged)
expect(json_response.first['title']).to eq(merge_request_merged.title)
end
it 'returns merge_request by "iids" array' do
get api("/projects/#{project.id}/merge_requests", user), iids: [merge_request.iid, merge_request_closed.iid]
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(2)
+ expect_response_ordered_exactly(merge_request_closed, merge_request)
expect(json_response.first['title']).to eq merge_request_closed.title
- expect(json_response.first['id']).to eq merge_request_closed.id
end
it 'matches V4 response schema' do
@@ -359,16 +323,14 @@ describe API::MergeRequests do
it 'returns an empty array if no issue matches milestone' do
get api("/projects/#{project.id}/merge_requests", user), milestone: '1.0.0'
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
+ expect_paginated_array_response
expect(json_response.length).to eq(0)
end
it 'returns an empty array if milestone does not exist' do
get api("/projects/#{project.id}/merge_requests", user), milestone: 'foo'
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
+ expect_paginated_array_response
expect(json_response.length).to eq(0)
end
@@ -382,17 +344,13 @@ describe API::MergeRequests do
it 'returns an array of merge requests matching state in milestone' do
get api("/projects/#{project.id}/merge_requests", user), milestone: '0.9', state: 'closed'
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
- expect(json_response.first['id']).to eq(merge_request_closed.id)
+ expect_response_ordered_exactly(merge_request_closed)
end
it 'returns an array of labeled merge requests' do
get api("/projects/#{project.id}/merge_requests?labels=#{label.title}", user)
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
+ expect_paginated_array_response
expect(json_response.length).to eq(1)
expect(json_response.first['labels']).to eq([label2.title, label.title])
end
@@ -400,16 +358,14 @@ describe API::MergeRequests do
it 'returns an array of labeled merge requests where all labels match' do
get api("/projects/#{project.id}/merge_requests?labels=#{label.title},foo,bar", user)
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
+ expect_paginated_array_response
expect(json_response.length).to eq(0)
end
it 'returns an empty array if no merge request matches labels' do
get api("/projects/#{project.id}/merge_requests?labels=foo,bar", user)
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
+ expect_paginated_array_response
expect(json_response.length).to eq(0)
end
@@ -427,13 +383,12 @@ describe API::MergeRequests do
get api("/projects/#{project.id}/merge_requests?labels=#{bug_label.title}&milestone=#{milestone1.title}&state=merged", user)
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
- expect(json_response.first['id']).to eq(mr2.id)
+ expect_response_ordered_exactly(mr2)
end
context "with ordering" do
+ let(:merge_requests) { [merge_request_merged, merge_request_closed, merge_request] }
+
before do
@mr_later = mr_with_later_created_and_updated_at_time
@mr_earlier = mr_with_earlier_created_and_updated_at_time
@@ -442,45 +397,25 @@ describe API::MergeRequests do
it "returns an array of merge_requests in ascending order" do
get api("/projects/#{project.id}/merge_requests?sort=asc", user)
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(3)
- response_dates = json_response.map { |merge_request| merge_request['created_at'] }
- expect(response_dates).to eq(response_dates.sort)
+ expect_response_ordered_exactly(*merge_requests.sort_by { |mr| mr['created_at'] })
end
it "returns an array of merge_requests in descending order" do
get api("/projects/#{project.id}/merge_requests?sort=desc", user)
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(3)
- response_dates = json_response.map { |merge_request| merge_request['created_at'] }
- expect(response_dates).to eq(response_dates.sort.reverse)
+ expect_response_ordered_exactly(*merge_requests.sort_by { |mr| mr['created_at'] }.reverse)
end
it "returns an array of merge_requests ordered by updated_at" do
get api("/projects/#{project.id}/merge_requests?order_by=updated_at", user)
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(3)
- response_dates = json_response.map { |merge_request| merge_request['updated_at'] }
- expect(response_dates).to eq(response_dates.sort.reverse)
+ expect_response_ordered_exactly(*merge_requests.sort_by { |mr| mr['updated_at'] }.reverse)
end
it "returns an array of merge_requests ordered by created_at" do
get api("/projects/#{project.id}/merge_requests?order_by=created_at&sort=asc", user)
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(3)
- response_dates = json_response.map { |merge_request| merge_request['created_at'] }
- expect(response_dates).to eq(response_dates.sort)
+ expect_response_ordered_exactly(*merge_requests.sort_by { |mr| mr['created_at'] })
end
end
@@ -488,9 +423,7 @@ describe API::MergeRequests do
it 'returns merge requests with the given source branch' do
get api('/merge_requests', user), source_branch: merge_request_closed.source_branch, state: 'all'
- expect(json_response.length).to eq(2)
- expect(json_response.map { |mr| mr['id'] })
- .to contain_exactly(merge_request_closed.id, merge_request_merged.id)
+ expect_response_contain_exactly(merge_request_closed, merge_request_merged)
end
end
@@ -498,9 +431,7 @@ describe API::MergeRequests do
it 'returns merge requests with the given target branch' do
get api('/merge_requests', user), target_branch: merge_request_closed.target_branch, state: 'all'
- expect(json_response.length).to eq(2)
- expect(json_response.map { |mr| mr['id'] })
- .to contain_exactly(merge_request_closed.id, merge_request_merged.id)
+ expect_response_contain_exactly(merge_request_closed, merge_request_merged)
end
end
end
@@ -1341,4 +1272,22 @@ describe API::MergeRequests do
merge_request_closed.save
merge_request_closed
end
+
+ def expect_response_contain_exactly(*items)
+ expect_paginated_array_response
+ expect(json_response.length).to eq(items.size)
+ expect(json_response.map { |element| element['id'] }).to contain_exactly(*items.map(&:id))
+ end
+
+ def expect_response_ordered_exactly(*items)
+ expect_paginated_array_response
+ expect(json_response.length).to eq(items.size)
+ expect(json_response.map { |element| element['id'] }).to eq(items.map(&:id))
+ end
+
+ def expect_paginated_array_response
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ end
end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 85a571b8f0e..9b7c3205c1f 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -18,7 +18,7 @@ describe API::Projects do
let(:user2) { create(:user) }
let(:user3) { create(:user) }
let(:admin) { create(:admin) }
- let(:project) { create(:project, namespace: user.namespace) }
+ let(:project) { create(:project, :repository, namespace: user.namespace) }
let(:project2) { create(:project, namespace: user.namespace) }
let(:snippet) { create(:project_snippet, :public, author: user, project: project, title: 'example') }
let(:project_member) { create(:project_member, :developer, user: user3, project: project) }
@@ -220,7 +220,7 @@ describe API::Projects do
it 'returns a simplified version of all the projects' do
expected_keys = %w(
id description default_branch tag_list
- ssh_url_to_repo http_url_to_repo web_url
+ ssh_url_to_repo http_url_to_repo web_url readme_url
name name_with_namespace
path path_with_namespace
star_count forks_count
@@ -854,6 +854,7 @@ describe API::Projects do
expect(json_response['only_allow_merge_if_pipeline_succeeds']).to eq(project.only_allow_merge_if_pipeline_succeeds)
expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved)
expect(json_response['merge_method']).to eq(project.merge_method.to_s)
+ expect(json_response['readme_url']).to eq(project.readme_url)
end
it 'returns a project by path name' do
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index da392c5ab81..efb9bddde44 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -918,6 +918,22 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
expect(job.reload.trace.raw).to eq 'BUILD TRACE appended appended'
end
+ context 'when job is cancelled' do
+ before do
+ job.cancel
+ end
+
+ context 'when trace is patched' do
+ before do
+ patch_the_trace
+ end
+
+ it 'returns Forbidden ' do
+ expect(response.status).to eq(403)
+ end
+ end
+ end
+
context 'when redis data are flushed' do
before do
redis_shared_state_cleanup!
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index e8196980a8c..05637eb0729 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -488,10 +488,6 @@ describe API::Users do
describe "PUT /users/:id" do
let!(:admin_user) { create(:admin) }
- before do
- admin
- end
-
it "updates user with new bio" do
put api("/users/#{user.id}", admin), { bio: 'new test bio' }
@@ -525,27 +521,28 @@ describe API::Users do
expect(json_response['avatar_url']).to include(user.avatar_path)
end
- it 'updates user with his own email' do
- put api("/users/#{user.id}", admin), email: user.email
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['email']).to eq(user.email)
- expect(user.reload.email).to eq(user.email)
- end
-
it 'updates user with a new email' do
+ old_email = user.email
+ old_notification_email = user.notification_email
put api("/users/#{user.id}", admin), email: 'new@email.com'
+ user.reload
+
expect(response).to have_gitlab_http_status(200)
- expect(user.reload.notification_email).to eq('new@email.com')
+ expect(user).to be_confirmed
+ expect(user.email).to eq(old_email)
+ expect(user.notification_email).to eq(old_notification_email)
+ expect(user.unconfirmed_email).to eq('new@email.com')
end
it 'skips reconfirmation when requested' do
- put api("/users/#{user.id}", admin), { skip_reconfirmation: true }
+ put api("/users/#{user.id}", admin), email: 'new@email.com', skip_reconfirmation: true
user.reload
- expect(user.confirmed_at).to be_present
+ expect(response).to have_gitlab_http_status(200)
+ expect(user).to be_confirmed
+ expect(user.email).to eq('new@email.com')
end
it 'updates user with his own username' do
diff --git a/spec/requests/api/v3/groups_spec.rb b/spec/requests/api/v3/groups_spec.rb
index a1cdf583de3..34d4b8e9565 100644
--- a/spec/requests/api/v3/groups_spec.rb
+++ b/spec/requests/api/v3/groups_spec.rb
@@ -458,9 +458,11 @@ describe API::V3::Groups do
describe "DELETE /groups/:id" do
context "when authenticated as user" do
it "removes group" do
- delete v3_api("/groups/#{group1.id}", user1)
+ Sidekiq::Testing.fake! do
+ expect { delete v3_api("/groups/#{group1.id}", user1) }.to change(GroupDestroyWorker.jobs, :size).by(1)
+ end
- expect(response).to have_gitlab_http_status(200)
+ expect(response).to have_gitlab_http_status(202)
end
it "does not remove a group if not an owner" do
@@ -489,7 +491,7 @@ describe API::V3::Groups do
it "removes any existing group" do
delete v3_api("/groups/#{group2.id}", admin)
- expect(response).to have_gitlab_http_status(200)
+ expect(response).to have_gitlab_http_status(202)
end
it "does not remove a non existing group" do
diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb
index 4c25bd935c6..158ddf171bc 100644
--- a/spec/requests/api/v3/projects_spec.rb
+++ b/spec/requests/api/v3/projects_spec.rb
@@ -82,7 +82,7 @@ describe API::V3::Projects do
it 'returns a simplified version of all the projects' do
expected_keys = %w(
id description default_branch tag_list
- ssh_url_to_repo http_url_to_repo web_url
+ ssh_url_to_repo http_url_to_repo web_url readme_url
name name_with_namespace
path path_with_namespace
star_count forks_count
diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb
index 2473c561f4b..e67d12b7a89 100644
--- a/spec/serializers/pipeline_entity_spec.rb
+++ b/spec/serializers/pipeline_entity_spec.rb
@@ -26,6 +26,13 @@ describe PipelineEntity do
expect(subject).to include :updated_at, :created_at
end
+ it 'excludes coverage data when disabled' do
+ entity = described_class
+ .represent(pipeline, request: request, disable_coverage: true)
+
+ expect(entity.as_json).not_to include(:coverage)
+ end
+
it 'contains details' do
expect(subject).to include :details
expect(subject[:details])
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 9a0b6efd8a9..2b88fcc9a96 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -395,7 +395,27 @@ describe Ci::CreatePipelineService do
result = execute_service
expect(result).to be_persisted
- expect(Environment.find_by(name: "review/master")).not_to be_nil
+ expect(Environment.find_by(name: "review/master")).to be_present
+ end
+ end
+
+ context 'with environment name including persisted variables' do
+ before do
+ config = YAML.dump(
+ deploy: {
+ environment: { name: "review/id1$CI_PIPELINE_ID/id2$CI_BUILD_ID" },
+ script: 'ls'
+ }
+ )
+
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ it 'skipps persisted variables in environment name' do
+ result = execute_service
+
+ expect(result).to be_persisted
+ expect(Environment.find_by(name: "review/id1/id2")).to be_present
end
end
diff --git a/spec/services/keys/destroy_service_spec.rb b/spec/services/keys/destroy_service_spec.rb
new file mode 100644
index 00000000000..28ac72ddd42
--- /dev/null
+++ b/spec/services/keys/destroy_service_spec.rb
@@ -0,0 +1,13 @@
+require 'spec_helper'
+
+describe Keys::DestroyService do
+ let(:user) { create(:user) }
+
+ subject { described_class.new(user) }
+
+ it 'destroys a key' do
+ key = create(:key)
+
+ expect { subject.execute(key) }.to change(Key, :count).by(-1)
+ end
+end
diff --git a/spec/services/projects/update_remote_mirror_service_spec.rb b/spec/services/projects/update_remote_mirror_service_spec.rb
index be09afd9f36..723cb374c37 100644
--- a/spec/services/projects/update_remote_mirror_service_spec.rb
+++ b/spec/services/projects/update_remote_mirror_service_spec.rb
@@ -343,7 +343,11 @@ describe Projects::UpdateRemoteMirrorService do
tag = repository.find_tag(name)
target = tag.try(:target)
target_commit = tag.try(:dereferenced_target)
- tags << Gitlab::Git::Tag.new(repository.raw_repository, name, target, target_commit)
+ tags << Gitlab::Git::Tag.new(repository.raw_repository, {
+ name: name,
+ target: target,
+ target_commit: target_commit
+ })
end
end
diff --git a/spec/support/helpers/rake_helpers.rb b/spec/support/helpers/rake_helpers.rb
index 86bfeed107c..acd9cce6a67 100644
--- a/spec/support/helpers/rake_helpers.rb
+++ b/spec/support/helpers/rake_helpers.rb
@@ -13,6 +13,10 @@ module RakeHelpers
allow(main_object).to receive(:print)
end
+ def silence_progress_bar
+ allow_any_instance_of(ProgressBar::Output).to receive(:stream).and_return(double().as_null_object)
+ end
+
def main_object
@main_object ||= TOPLEVEL_BINDING.eval('self')
end
diff --git a/spec/support/shared_examples/malicious_regexp_shared_examples.rb b/spec/support/shared_examples/malicious_regexp_shared_examples.rb
index ac5d22298bb..65026f1d7c0 100644
--- a/spec/support/shared_examples/malicious_regexp_shared_examples.rb
+++ b/spec/support/shared_examples/malicious_regexp_shared_examples.rb
@@ -1,3 +1,5 @@
+require 'timeout'
+
shared_examples 'malicious regexp' do
let(:malicious_text) { 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!' }
let(:malicious_regexp) { '(?i)^(([a-z])+.)+[A-Z]([a-z])+$' }
diff --git a/yarn.lock b/yarn.lock
index f34bc81067d..e0d46609f7d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -73,6 +73,20 @@
version "2.0.48"
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-2.0.48.tgz#3e90d8cde2d29015e5583017f7830cb3975b2eef"
+"@vue/component-compiler-utils@^1.2.1":
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-1.2.1.tgz#3d543baa75cfe5dab96e29415b78366450156ef6"
+ dependencies:
+ consolidate "^0.15.1"
+ hash-sum "^1.0.2"
+ lru-cache "^4.1.2"
+ merge-source-map "^1.1.0"
+ postcss "^6.0.20"
+ postcss-selector-parser "^3.1.1"
+ prettier "^1.11.1"
+ source-map "^0.5.6"
+ vue-template-es2015-compiler "^1.6.0"
+
abbrev@1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
@@ -1467,6 +1481,15 @@ cache-base@^1.0.1:
union-value "^1.0.0"
unset-value "^1.0.0"
+cache-loader@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/cache-loader/-/cache-loader-1.2.2.tgz#6d5c38ded959a09cc5d58190ab5af6f73bd353f5"
+ dependencies:
+ loader-utils "^1.1.0"
+ mkdirp "^0.5.1"
+ neo-async "^2.5.0"
+ schema-utils "^0.4.2"
+
cacheable-request@^2.1.1:
version "2.1.4"
resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-2.1.4.tgz#0d808801b6342ad33c91df9d0b44dc09b91e5c3d"
@@ -1947,9 +1970,9 @@ console-control-strings@^1.0.0, console-control-strings@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
-consolidate@^0.14.0:
- version "0.14.5"
- resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.14.5.tgz#5a25047bc76f73072667c8cb52c989888f494c63"
+consolidate@^0.15.1:
+ version "0.15.1"
+ resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.15.1.tgz#21ab043235c71a07d45d9aad98593b0dba56bab7"
dependencies:
bluebird "^3.1.1"
@@ -2021,18 +2044,6 @@ core-util-is@1.0.2, core-util-is@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
-cosmiconfig@^2.1.0, cosmiconfig@^2.1.1:
- version "2.2.2"
- resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-2.2.2.tgz#6173cebd56fac042c1f4390edf7af6c07c7cb892"
- dependencies:
- is-directory "^0.3.1"
- js-yaml "^3.4.3"
- minimist "^1.2.0"
- object-assign "^4.1.0"
- os-homedir "^1.0.1"
- parse-json "^2.2.0"
- require-from-string "^1.1.0"
-
create-ecdh@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d"
@@ -2660,7 +2671,7 @@ domutils@^1.5.1:
dom-serializer "0"
domelementtype "1"
-dot-prop@^4.1.0:
+dot-prop@^4.1.0, dot-prop@^4.1.1:
version "4.2.0"
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57"
dependencies:
@@ -4552,10 +4563,6 @@ is-descriptor@^1.0.0, is-descriptor@^1.0.2:
is-data-descriptor "^1.0.0"
kind-of "^6.0.2"
-is-directory@^0.3.1:
- version "0.3.1"
- resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1"
-
is-dotfile@^1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1"
@@ -4989,13 +4996,6 @@ js-yaml@3.x, js-yaml@^3.5.1, js-yaml@^3.7.0:
argparse "^1.0.7"
esprima "^4.0.0"
-js-yaml@^3.4.3:
- version "3.11.0"
- resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef"
- dependencies:
- argparse "^1.0.7"
- esprima "^4.0.0"
-
js-yaml@~3.7.0:
version "3.7.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.7.0.tgz#5c967ddd837a9bfdca5f2de84253abe8a1c03b80"
@@ -5555,6 +5555,13 @@ lru-cache@^4.0.1, lru-cache@^4.1.1:
pseudomap "^1.0.2"
yallist "^2.1.2"
+lru-cache@^4.1.2:
+ version "4.1.3"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c"
+ dependencies:
+ pseudomap "^1.0.2"
+ yallist "^2.1.2"
+
lru-cache@~2.6.5:
version "2.6.5"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.6.5.tgz#e56d6354148ede8d7707b58d143220fd08df0fd5"
@@ -5697,6 +5704,12 @@ merge-descriptors@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
+merge-source-map@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.1.0.tgz#2fdde7e6020939f70906a68f2d7ae685e4c8c646"
+ dependencies:
+ source-map "^0.6.1"
+
merge2@^1.2.1:
version "1.2.2"
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.2.2.tgz#03212e3da8d86c4d8523cebd6318193414f94e34"
@@ -6333,7 +6346,7 @@ os-browserify@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.2.1.tgz#63fc4ccee5d2d7763d26bbf8601078e6c2e0044f"
-os-homedir@^1.0.0, os-homedir@^1.0.1:
+os-homedir@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
@@ -6724,29 +6737,6 @@ postcss-filter-plugins@^2.0.0:
postcss "^5.0.4"
uniqid "^4.0.0"
-postcss-load-config@^1.1.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-1.2.0.tgz#539e9afc9ddc8620121ebf9d8c3673e0ce50d28a"
- dependencies:
- cosmiconfig "^2.1.0"
- object-assign "^4.1.0"
- postcss-load-options "^1.2.0"
- postcss-load-plugins "^2.3.0"
-
-postcss-load-options@^1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/postcss-load-options/-/postcss-load-options-1.2.0.tgz#b098b1559ddac2df04bc0bb375f99a5cfe2b6d8c"
- dependencies:
- cosmiconfig "^2.1.0"
- object-assign "^4.1.0"
-
-postcss-load-plugins@^2.3.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/postcss-load-plugins/-/postcss-load-plugins-2.3.0.tgz#745768116599aca2f009fad426b00175049d8d92"
- dependencies:
- cosmiconfig "^2.1.1"
- object-assign "^4.1.0"
-
postcss-merge-idents@^2.1.5:
version "2.1.7"
resolved "https://registry.yarnpkg.com/postcss-merge-idents/-/postcss-merge-idents-2.1.7.tgz#4c5530313c08e1d5b3bbf3d2bbc747e278eea270"
@@ -6886,6 +6876,14 @@ postcss-selector-parser@^2.0.0, postcss-selector-parser@^2.2.2:
indexes-of "^1.0.1"
uniq "^1.0.1"
+postcss-selector-parser@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz#4f875f4afb0c96573d5cf4d74011aee250a7e865"
+ dependencies:
+ dot-prop "^4.1.1"
+ indexes-of "^1.0.1"
+ uniq "^1.0.1"
+
postcss-svgo@^2.1.1:
version "2.1.6"
resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-2.1.6.tgz#b6df18aa613b666e133f08adb5219c2684ac108d"
@@ -6932,13 +6930,13 @@ postcss@^6.0.1, postcss@^6.0.14:
source-map "^0.6.1"
supports-color "^5.2.0"
-postcss@^6.0.8:
- version "6.0.21"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.21.tgz#8265662694eddf9e9a5960db6da33c39e4cd069d"
+postcss@^6.0.20:
+ version "6.0.22"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.22.tgz#e23b78314905c3b90cbd61702121e7a78848f2a3"
dependencies:
- chalk "^2.3.2"
+ chalk "^2.4.1"
source-map "^0.6.1"
- supports-color "^5.3.0"
+ supports-color "^5.4.0"
prelude-ls@~1.1.2:
version "1.1.2"
@@ -6960,14 +6958,14 @@ prettier@1.11.1:
version "1.11.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.11.1.tgz#61e43fc4cd44e68f2b0dfc2c38cd4bb0fccdcc75"
+prettier@^1.11.1:
+ version "1.12.1"
+ resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.12.1.tgz#c1ad20e803e7749faf905a409d2367e06bbe7325"
+
prettier@^1.5.3:
version "1.10.2"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.10.2.tgz#1af8356d1842276a99a5b5529c82dd9e9ad3cc93"
-prettier@^1.7.0:
- version "1.8.2"
- resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.8.2.tgz#bff83e7fd573933c607875e5ba3abbdffb96aeb8"
-
pretty-bytes@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-4.0.2.tgz#b2bf82e7350d65c6c33aa95aaa5a4f6327f61cd9"
@@ -7533,10 +7531,6 @@ require-directory@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
-require-from-string@^1.1.0:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-1.2.1.tgz#529c9ccef27380adfec9a2f965b649bbee636418"
-
require-main-filename@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
@@ -7587,12 +7581,6 @@ resolve@^1.1.6, resolve@^1.2.0:
dependencies:
path-parse "^1.0.5"
-resolve@^1.4.0:
- version "1.7.1"
- resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.7.1.tgz#aadd656374fd298aee895bc026b8297418677fd3"
- dependencies:
- path-parse "^1.0.5"
-
responselike@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7"
@@ -7710,7 +7698,7 @@ sax@~1.2.1:
version "1.2.2"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.2.tgz#fd8631a23bc7826bef5d871bdb87378c95647828"
-schema-utils@^0.4.0, schema-utils@^0.4.3, schema-utils@^0.4.4, schema-utils@^0.4.5:
+schema-utils@^0.4.0, schema-utils@^0.4.2, schema-utils@^0.4.3, schema-utils@^0.4.4, schema-utils@^0.4.5:
version "0.4.5"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.5.tgz#21836f0608aac17b78f9e3e24daff14a5ca13a3e"
dependencies:
@@ -8362,7 +8350,7 @@ supports-color@^5.2.0:
dependencies:
has-flag "^3.0.0"
-supports-color@^5.3.0:
+supports-color@^5.3.0, supports-color@^5.4.0:
version "5.4.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54"
dependencies:
@@ -8974,27 +8962,19 @@ vue-eslint-parser@^2.0.1:
esquery "^1.0.0"
lodash "^4.17.4"
-vue-hot-reload-api@^2.2.0:
+vue-hot-reload-api@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.0.tgz#97976142405d13d8efae154749e88c4e358cf926"
-vue-loader@^14.1.1:
- version "14.2.2"
- resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-14.2.2.tgz#c8cf3c2e29b6fb2ee595248a2aa6005038a125b3"
+vue-loader@^15.2.0:
+ version "15.2.0"
+ resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.2.0.tgz#5a8138e490a1040942d2f10ae68fa72b5a923364"
dependencies:
- consolidate "^0.14.0"
+ "@vue/component-compiler-utils" "^1.2.1"
hash-sum "^1.0.2"
loader-utils "^1.1.0"
- lru-cache "^4.1.1"
- postcss "^6.0.8"
- postcss-load-config "^1.1.0"
- postcss-selector-parser "^2.0.0"
- prettier "^1.7.0"
- resolve "^1.4.0"
- source-map "^0.6.1"
- vue-hot-reload-api "^2.2.0"
- vue-style-loader "^4.0.1"
- vue-template-es2015-compiler "^1.6.0"
+ vue-hot-reload-api "^2.3.0"
+ vue-style-loader "^4.1.0"
vue-resource@^1.5.0:
version "1.5.0"
@@ -9006,7 +8986,7 @@ vue-router@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.0.1.tgz#d9b05ad9c7420ba0f626d6500d693e60092cc1e9"
-vue-style-loader@^4.0.1:
+vue-style-loader@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-4.1.0.tgz#7588bd778e2c9f8d87bfc3c5a4a039638da7a863"
dependencies: