summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.flayignore1
-rw-r--r--.gitlab-ci.yml300
-rw-r--r--.gitlab/issue_templates/Security Developer Workflow.md4
-rw-r--r--.gitlab/merge_request_templates/Database Changes.md10
-rw-r--r--CONTRIBUTING.md37
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--Gemfile18
-rw-r--r--Gemfile.lock45
-rw-r--r--Gemfile.rails5.lock137
-rw-r--r--app/assets/javascripts/clusters/clusters_index.js26
-rw-r--r--app/assets/javascripts/clusters/components/gcp_signup_offer.js27
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js2
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_service.js30
-rw-r--r--app/assets/javascripts/deploy_keys/components/action_btn.vue71
-rw-r--r--app/assets/javascripts/deploy_keys/components/app.vue212
-rw-r--r--app/assets/javascripts/deploy_keys/components/key.vue322
-rw-r--r--app/assets/javascripts/deploy_keys/components/keys_panel.vue100
-rw-r--r--app/assets/javascripts/deploy_keys/index.js39
-rw-r--r--app/assets/javascripts/deploy_keys/service/index.js28
-rw-r--r--app/assets/javascripts/deploy_keys/store/index.js4
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js9
-rw-r--r--app/assets/javascripts/gpg_badges.js6
-rw-r--r--app/assets/javascripts/ide/components/activity_bar.vue106
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/actions.vue18
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue45
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/form.vue171
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list.vue103
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_item.vue3
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue30
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/success_message.vue8
-rw-r--r--app/assets/javascripts/ide/components/editor_mode_dropdown.vue80
-rw-r--r--app/assets/javascripts/ide/components/ide.vue225
-rw-r--r--app/assets/javascripts/ide/components/ide_context_bar.vue42
-rw-r--r--app/assets/javascripts/ide/components/ide_external_links.vue43
-rw-r--r--app/assets/javascripts/ide/components/ide_project_branches_tree.vue47
-rw-r--r--app/assets/javascripts/ide/components/ide_project_tree.vue65
-rw-r--r--app/assets/javascripts/ide/components/ide_repo_tree.vue41
-rw-r--r--app/assets/javascripts/ide/components/ide_review.vue62
-rw-r--r--app/assets/javascripts/ide/components/ide_side_bar.vue143
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue96
-rw-r--r--app/assets/javascripts/ide/components/ide_tree.vue42
-rw-r--r--app/assets/javascripts/ide/components/ide_tree_list.vue76
-rw-r--r--app/assets/javascripts/ide/components/mr_file_icon.vue4
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/index.vue3
-rw-r--r--app/assets/javascripts/ide/components/repo_commit_section.vue102
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue52
-rw-r--r--app/assets/javascripts/ide/components/repo_file.vue13
-rw-r--r--app/assets/javascripts/ide/components/repo_tab.vue36
-rw-r--r--app/assets/javascripts/ide/components/repo_tabs.vue17
-rw-r--r--app/assets/javascripts/ide/constants.js16
-rw-r--r--app/assets/javascripts/ide/ide_router.js7
-rw-r--r--app/assets/javascripts/ide/index.js33
-rw-r--r--app/assets/javascripts/ide/lib/common/model.js6
-rw-r--r--app/assets/javascripts/ide/lib/editor.js8
-rw-r--r--app/assets/javascripts/ide/stores/actions.js14
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js11
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js24
-rw-r--r--app/assets/javascripts/ide/stores/getters.js29
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js45
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js3
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js15
-rw-r--r--app/assets/javascripts/ide/stores/mutations/branch.js5
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js45
-rw-r--r--app/assets/javascripts/ide/stores/state.js5
-rw-r--r--app/assets/javascripts/ide/stores/utils.js1
-rw-r--r--app/assets/javascripts/main.js61
-rw-r--r--app/assets/javascripts/mini_pipeline_graph_dropdown.js4
-rw-r--r--app/assets/javascripts/monitoring/components/graph.vue8
-rw-r--r--app/assets/javascripts/monitoring/components/graph/flag.vue22
-rw-r--r--app/assets/javascripts/monitoring/components/graph/path.vue22
-rw-r--r--app/assets/javascripts/monitoring/components/graph/track_line.vue10
-rw-r--r--app/assets/javascripts/monitoring/mixins/monitoring_mixins.js20
-rw-r--r--app/assets/javascripts/monitoring/utils/date_time_formatters.js2
-rw-r--r--app/assets/javascripts/monitoring/utils/multiple_time_series.js1
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue6
-rw-r--r--app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js4
-rw-r--r--app/assets/javascripts/pages/ide/index.js9
-rw-r--r--app/assets/javascripts/pages/projects/clusters/gcp/login/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/clusters/new/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/compare/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/new/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue77
-rw-r--r--app/assets/javascripts/pages/projects/wikis/index.js28
-rw-r--r--app/assets/javascripts/pipelines/components/async_button.vue95
-rw-r--r--app/assets/javascripts/pipelines/components/graph/action_component.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue6
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_component.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table.vue53
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table_row.vue96
-rw-r--r--app/assets/javascripts/pipelines/components/stage.vue244
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines.js2
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js4
-rw-r--r--app/assets/javascripts/projects_dropdown/components/app.vue5
-rw-r--r--app/assets/javascripts/projects_dropdown/service/projects_service.js14
-rw-r--r--app/assets/javascripts/registry/components/table_registry.vue8
-rw-r--r--app/assets/javascripts/shortcuts.js1
-rw-r--r--app/assets/javascripts/user_callout.js9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue20
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue14
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue (renamed from app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js)6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/dependencies.js5
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue (renamed from app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js)166
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/icon.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/loading_button.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/navigation_tabs.vue88
-rw-r--r--app/assets/stylesheets/framework/buttons.scss8
-rw-r--r--app/assets/stylesheets/framework/gitlab_theme.scss30
-rw-r--r--app/assets/stylesheets/framework/mixins.scss10
-rw-r--r--app/assets/stylesheets/framework/terms.scss13
-rw-r--r--app/assets/stylesheets/framework/variables.scss19
-rw-r--r--app/assets/stylesheets/pages/clusters.scss48
-rw-r--r--app/assets/stylesheets/pages/commits.scss25
-rw-r--r--app/assets/stylesheets/pages/environments.scss37
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss4
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss194
-rw-r--r--app/assets/stylesheets/pages/projects.scss53
-rw-r--r--app/assets/stylesheets/pages/repo.scss418
-rw-r--r--app/assets/stylesheets/pages/wiki.scss5
-rw-r--r--app/assets/stylesheets/print.scss8
-rw-r--r--app/controllers/application_controller.rb9
-rw-r--r--app/controllers/concerns/issuable_actions.rb1
-rw-r--r--app/controllers/concerns/send_file_upload.rb4
-rw-r--r--app/controllers/groups/group_members_controller.rb9
-rw-r--r--app/controllers/groups/runners_controller.rb58
-rw-r--r--app/controllers/import/base_controller.rb6
-rw-r--r--app/controllers/projects/compare_controller.rb65
-rw-r--r--app/controllers/projects/mirrors_controller.rb67
-rw-r--r--app/controllers/projects/notes_controller.rb13
-rw-r--r--app/controllers/projects/pipelines_controller.rb23
-rw-r--r--app/controllers/projects/runner_projects_controller.rb4
-rw-r--r--app/controllers/projects/runners_controller.rb17
-rw-r--r--app/controllers/projects/settings/repository_controller.rb6
-rw-r--r--app/controllers/sent_notifications_controller.rb20
-rw-r--r--app/controllers/users/terms_controller.rb4
-rw-r--r--app/helpers/application_helper.rb13
-rw-r--r--app/helpers/application_settings_helper.rb3
-rw-r--r--app/helpers/clusters_helper.rb8
-rw-r--r--app/helpers/events_helper.rb2
-rw-r--r--app/helpers/gitlab_routing_helper.rb8
-rw-r--r--app/helpers/user_callouts_helper.rb5
-rw-r--r--app/helpers/users_helper.rb19
-rw-r--r--app/helpers/webpack_helper.rb45
-rw-r--r--app/mailers/emails/notes.rb5
-rw-r--r--app/mailers/notify.rb13
-rw-r--r--app/models/ability.rb8
-rw-r--r--app/models/application_setting.rb3
-rw-r--r--app/models/ci/build.rb1
-rw-r--r--app/models/ci/build_trace_chunk.rb180
-rw-r--r--app/models/ci/pipeline.rb45
-rw-r--r--app/models/ci/pipeline_variable.rb2
-rw-r--r--app/models/ci/runner.rb9
-rw-r--r--app/models/clusters/applications/runner.rb10
-rw-r--r--app/models/concerns/fast_destroy_all.rb91
-rw-r--r--app/models/concerns/participable.rb4
-rw-r--r--app/models/concerns/reactive_caching.rb17
-rw-r--r--app/models/concerns/routable.rb4
-rw-r--r--app/models/concerns/sha_attribute.rb30
-rw-r--r--app/models/concerns/time_trackable.rb6
-rw-r--r--app/models/group.rb31
-rw-r--r--app/models/member.rb11
-rw-r--r--app/models/merge_request.rb4
-rw-r--r--app/models/namespace.rb7
-rw-r--r--app/models/note.rb4
-rw-r--r--app/models/project.rb92
-rw-r--r--app/models/remote_mirror.rb219
-rw-r--r--app/models/repository.rb26
-rw-r--r--app/models/sent_notification.rb4
-rw-r--r--app/models/system_note_metadata.rb6
-rw-r--r--app/models/user.rb18
-rw-r--r--app/models/user_callout.rb3
-rw-r--r--app/policies/ci/build_policy.rb9
-rw-r--r--app/policies/ci/pipeline_policy.rb8
-rw-r--r--app/policies/global_policy.rb19
-rw-r--r--app/policies/project_policy.rb11
-rw-r--r--app/presenters/ci/pipeline_presenter.rb10
-rw-r--r--app/serializers/merge_request_widget_entity.rb7
-rw-r--r--app/serializers/project_mirror_entity.rb11
-rw-r--r--app/serializers/stage_entity.rb16
-rw-r--r--app/serializers/stage_serializer.rb7
-rw-r--r--app/services/ci/create_pipeline_service.rb1
-rw-r--r--app/services/concerns/exclusive_lease_guard.rb52
-rw-r--r--app/services/concerns/users/participable_service.rb41
-rw-r--r--app/services/git_push_service.rb8
-rw-r--r--app/services/notification_recipient_service.rb35
-rw-r--r--app/services/projects/destroy_service.rb2
-rw-r--r--app/services/projects/participants_service.rb32
-rw-r--r--app/services/projects/update_remote_mirror_service.rb30
-rw-r--r--app/services/users/migrate_to_ghost_user_service.rb4
-rw-r--r--app/views/admin/application_settings/_repository_check.html.haml2
-rw-r--r--app/views/admin/application_settings/_repository_mirrors_form.html.haml16
-rw-r--r--app/views/admin/application_settings/show.html.haml11
-rw-r--r--app/views/admin/runners/_runner.html.haml2
-rw-r--r--app/views/admin/runners/show.html.haml2
-rw-r--r--app/views/ci/status/_dropdown_graph_badge.html.haml2
-rw-r--r--app/views/groups/group_members/index.html.haml26
-rw-r--r--app/views/groups/issues.html.haml2
-rw-r--r--app/views/groups/runners/_group_runners.html.haml24
-rw-r--r--app/views/groups/runners/_index.html.haml9
-rw-r--r--app/views/groups/runners/_runner.html.haml27
-rw-r--r--app/views/groups/runners/edit.html.haml6
-rw-r--r--app/views/groups/settings/ci_cd/show.html.haml28
-rw-r--r--app/views/help/_shortcuts.html.haml2
-rw-r--r--app/views/help/ui.html.haml5
-rw-r--r--app/views/ide/index.html.haml3
-rw-r--r--app/views/layouts/_head.html.haml3
-rw-r--r--app/views/layouts/_init_auto_complete.html.haml15
-rw-r--r--app/views/layouts/terms.html.haml4
-rw-r--r--app/views/projects/_wiki.html.haml2
-rw-r--r--app/views/projects/clusters/_gcp_signup_offer_banner.html.haml12
-rw-r--r--app/views/projects/clusters/gcp/login.html.haml2
-rw-r--r--app/views/projects/clusters/index.html.haml2
-rw-r--r--app/views/projects/clusters/new.html.haml2
-rw-r--r--app/views/projects/commit/_commit_box.html.haml2
-rw-r--r--app/views/projects/commits/_commit.html.haml8
-rw-r--r--app/views/projects/commits/show.html.haml2
-rw-r--r--app/views/projects/compare/_form.html.haml2
-rw-r--r--app/views/projects/deploy_keys/_index.html.haml2
-rw-r--r--app/views/projects/merge_requests/creations/_new_compare.html.haml4
-rw-r--r--app/views/projects/mirrors/_instructions.html.haml10
-rw-r--r--app/views/projects/mirrors/_push.html.haml50
-rw-r--r--app/views/projects/mirrors/_show.html.haml3
-rw-r--r--app/views/projects/pipelines/_info.html.haml2
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml11
-rw-r--r--app/views/projects/pipelines/new.html.haml18
-rw-r--r--app/views/projects/runners/_group_runners.html.haml33
-rw-r--r--app/views/projects/runners/_index.html.haml20
-rw-r--r--app/views/projects/runners/_runner.html.haml14
-rw-r--r--app/views/projects/runners/_shared_runners.html.haml6
-rw-r--r--app/views/projects/runners/_specific_runners.html.haml3
-rw-r--r--app/views/projects/runners/edit.html.haml4
-rw-r--r--app/views/projects/settings/repository/show.html.haml2
-rw-r--r--app/views/projects/wikis/edit.html.haml11
-rw-r--r--app/views/projects/wikis/show.html.haml2
-rw-r--r--app/views/shared/_mini_pipeline_graph.html.haml3
-rw-r--r--app/views/shared/_remote_mirror_update_button.html.haml13
-rw-r--r--app/views/shared/boards/components/_board.html.haml3
-rw-r--r--app/views/shared/members/_filter_2fa_dropdown.html.haml11
-rw-r--r--app/views/shared/members/_member.html.haml4
-rw-r--r--app/views/shared/runners/_form.html.haml (renamed from app/views/projects/runners/_form.html.haml)13
-rw-r--r--app/views/shared/runners/_runner_description.html.haml16
-rw-r--r--app/views/shared/runners/show.html.haml (renamed from app/views/projects/runners/show.html.haml)22
-rw-r--r--app/views/users/show.html.haml8
-rw-r--r--app/workers/admin_email_worker.rb6
-rw-r--r--app/workers/all_queues.yml3
-rw-r--r--app/workers/ci/build_trace_chunk_flush_worker.rb12
-rw-r--r--app/workers/new_note_worker.rb2
-rw-r--r--app/workers/object_storage/migrate_uploads_worker.rb79
-rw-r--r--app/workers/repository_check/batch_worker.rb35
-rw-r--r--app/workers/repository_check/single_repository_worker.rb48
-rw-r--r--app/workers/repository_remove_remote_worker.rb35
-rw-r--r--app/workers/repository_update_remote_mirror_worker.rb49
-rwxr-xr-xbin/secpick5
-rwxr-xr-xbin/spinach13
-rw-r--r--changelogs/unreleased/23465-print-markdown.yml5
-rw-r--r--changelogs/unreleased/33697-pipelines-json-endpoint.yml5
-rw-r--r--changelogs/unreleased/36983-osw-heading-labels-color-fix.yml5
-rw-r--r--changelogs/unreleased/40725-move-mr-external-link-to-right.yml5
-rw-r--r--changelogs/unreleased/40855_remove_authentication_in_readonly_issue_api.yml5
-rw-r--r--changelogs/unreleased/41082-make-deploykeys-table-more-clearly-structured.yml5
-rw-r--r--changelogs/unreleased/42099-port-push-mirroring-to-ce-ce-port-v-2.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/44059-specify-variables-when-executing-a-manual-pipeline-from-the-ui.yml5
-rw-r--r--changelogs/unreleased/44319-remove-gray-buttons.yml5
-rw-r--r--changelogs/unreleased/44833-ide-clean-up-status-bar.yml5
-rw-r--r--changelogs/unreleased/44879.yml5
-rw-r--r--changelogs/unreleased/45442-updates-updated-at-to-issue-on-time-spent.yml5
-rw-r--r--changelogs/unreleased/45715-remove-modal-retry.yml5
-rw-r--r--changelogs/unreleased/46010-add-index-to-runner-type.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/46345-kubernetes-popover-illustration-skewed.yml5
-rw-r--r--changelogs/unreleased/46361-does-not-log-failed-sign-in-attempts-when-the-database-is-in-read-only-mode.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/add-git-commit-message-predefined-variable.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-remove-spinach.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-builds-artifacts-feature.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-deploy-keys-feature.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-ff-merge-requests-feature.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-forked-merge-requests-feature.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-issues-references-feature.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-merge-requests-references-feature.yml5
-rw-r--r--changelogs/unreleased/bvl-restrict-api-git-for-terms.yml6
-rw-r--r--changelogs/unreleased/docs-42067-document-runner-registration-api.yml5
-rw-r--r--changelogs/unreleased/dz-add-2fa-filter.yml5
-rw-r--r--changelogs/unreleased/feature-expose-runner-ip-to-api.yml5
-rw-r--r--changelogs/unreleased/fix-gb-add-pipeline-builds-foreign-key.yml5
-rw-r--r--changelogs/unreleased/fix-project-mirror-data-schema.yml6
-rw-r--r--changelogs/unreleased/fix-reactive-cache-retry-rate.yml5
-rw-r--r--changelogs/unreleased/fix-registry-created-at-tooltip.yml5
-rw-r--r--changelogs/unreleased/fix-shorcut-modal.yml5
-rw-r--r--changelogs/unreleased/fix-shortcut-close-screen-with-key.yml5
-rw-r--r--changelogs/unreleased/fix-wiki-find-page-invalid-encoding.yml5
-rw-r--r--changelogs/unreleased/ide-hide-merge-request-if-disabled.yml5
-rw-r--r--changelogs/unreleased/improve-commit-message-body-rendering.yml5
-rw-r--r--changelogs/unreleased/issue_43660.yml5
-rw-r--r--changelogs/unreleased/jprovazn-null-byte.yml5
-rw-r--r--changelogs/unreleased/jprovazn-pipeline-policy.yml6
-rw-r--r--changelogs/unreleased/jr-46209-web-ide-copy.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/refactor-move-squash-before-merge-vue-component.yml5
-rw-r--r--changelogs/unreleased/sh-enforce-unique-and-not-null-project-ids-project-features.yml5
-rw-r--r--changelogs/unreleased/sh-fix-blocked-user-account-ldap.yml5
-rw-r--r--changelogs/unreleased/sh-fix-cross-site-origin-uploads-js.yml5
-rw-r--r--changelogs/unreleased/support-active-setting-while-registering-a-runner.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/update-wiki-modal.yml5
-rw-r--r--changelogs/unreleased/zj-wiki-find-file-opt-out.yml5
-rw-r--r--config/initializers/6_validations.rb11
-rw-r--r--config/initializers/8_metrics.rb9
-rw-r--r--config/initializers/deprecations.rb8
-rw-r--r--config/initializers/gollum.rb14
-rw-r--r--config/initializers/static_files.rb2
-rw-r--r--config/initializers/warden.rb10
-rw-r--r--config/karma.config.js21
-rw-r--r--config/prometheus/additional_metrics.yml12
-rw-r--r--config/routes/group.rb7
-rw-r--r--config/routes/project.rb7
-rw-r--r--config/routes/repository.rb1
-rw-r--r--config/sidekiq_queues.yml3
-rw-r--r--config/webpack.config.js181
-rw-r--r--db/migrate/20121220064453_init_schema.rb307
-rw-r--r--db/migrate/20130102143055_rename_owner_to_creator_for_project.rb6
-rw-r--r--db/migrate/20130110172407_add_public_to_project.rb6
-rw-r--r--db/migrate/20130123114545_add_issues_tracker_to_project.rb6
-rw-r--r--db/migrate/20130125090214_add_user_permissions.rb12
-rw-r--r--db/migrate/20130131070232_remove_private_flag_from_project.rb10
-rw-r--r--db/migrate/20130206084024_add_description_to_namsespace.rb6
-rw-r--r--db/migrate/20130207104426_add_description_to_teams.rb6
-rw-r--r--db/migrate/20130211085435_add_issues_tracker_id_to_project.rb6
-rw-r--r--db/migrate/20130214154045_rename_state_to_merge_status_in_milestone.rb6
-rw-r--r--db/migrate/20130218140952_add_state_to_issue.rb6
-rw-r--r--db/migrate/20130218141038_add_state_to_merge_request.rb6
-rw-r--r--db/migrate/20130218141117_add_state_to_milestone.rb6
-rw-r--r--db/migrate/20130218141258_convert_closed_to_state_in_issue.rb19
-rw-r--r--db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb21
-rw-r--r--db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb19
-rw-r--r--db/migrate/20130218141444_remove_merged_from_merge_request.rb10
-rw-r--r--db/migrate/20130218141507_remove_closed_from_issue.rb10
-rw-r--r--db/migrate/20130218141536_remove_closed_from_merge_request.rb10
-rw-r--r--db/migrate/20130218141554_remove_closed_from_milestone.rb10
-rw-r--r--db/migrate/20130220124204_add_new_merge_status_to_merge_request.rb6
-rw-r--r--db/migrate/20130220125544_convert_merge_status_in_merge_request.rb20
-rw-r--r--db/migrate/20130220125545_remove_merge_status_from_merge_request.rb10
-rw-r--r--db/migrate/20130220133245_rename_new_merge_status_to_merge_status_in_milestone.rb6
-rw-r--r--db/migrate/20130304104623_add_state_to_user.rb6
-rw-r--r--db/migrate/20130304104740_convert_blocked_to_state.rb15
-rw-r--r--db/migrate/20130304105317_remove_blocked_from_user.rb10
-rw-r--r--db/migrate/20130315124931_user_color_scheme.rb15
-rw-r--r--db/migrate/20130318212250_add_snippets_to_features.rb6
-rw-r--r--db/migrate/20130319214458_create_forked_project_links.rb14
-rw-r--r--db/migrate/20130323174317_add_private_to_snippets.rb6
-rw-r--r--db/migrate/20130324151736_add_type_to_snippets.rb6
-rw-r--r--db/migrate/20130324172327_change_project_id_to_null_in_snipepts.rb10
-rw-r--r--db/migrate/20130324203535_add_type_value_for_snippets.rb9
-rw-r--r--db/migrate/20130325173941_add_notification_level_to_user.rb6
-rw-r--r--db/migrate/20130326142630_add_index_to_users_authentication_token.rb6
-rw-r--r--db/migrate/20130403003950_add_last_activity_column_into_project.rb24
-rw-r--r--db/migrate/20130404164628_add_notification_level_to_user_project.rb6
-rw-r--r--db/migrate/20130410175022_remove_wiki_table.rb10
-rw-r--r--db/migrate/20130419190306_allow_merges_for_forks.rb20
-rw-r--r--db/migrate/20130506085413_add_type_to_key.rb6
-rw-r--r--db/migrate/20130506090604_create_deploy_keys_projects.rb13
-rw-r--r--db/migrate/20130506095501_remove_project_id_from_key.rb23
-rw-r--r--db/migrate/20130522141856_add_more_fields_to_service.rb7
-rw-r--r--db/migrate/20130528184641_add_system_to_notes.rb17
-rw-r--r--db/migrate/20130611210815_increase_snippet_text_column_size.rb10
-rw-r--r--db/migrate/20130613165816_add_password_expires_at_to_users.rb6
-rw-r--r--db/migrate/20130613173246_add_created_by_id_to_user.rb6
-rw-r--r--db/migrate/20130614132337_add_improted_to_project.rb6
-rw-r--r--db/migrate/20130617095603_create_users_groups.rb14
-rw-r--r--db/migrate/20130621195223_add_notification_level_to_user_group.rb6
-rw-r--r--db/migrate/20130622115340_add_more_db_index.rb13
-rw-r--r--db/migrate/20130624162710_add_fingerprint_to_key.rb7
-rw-r--r--db/migrate/20130711063759_create_project_group_links.rb13
-rw-r--r--db/migrate/20130804151314_add_st_diff_to_note.rb6
-rw-r--r--db/migrate/20130809124851_add_permission_check_to_user.rb6
-rw-r--r--db/migrate/20130812143708_add_import_url_to_project.rb6
-rw-r--r--db/migrate/20130819182730_add_internal_ids_to_issues_and_mr.rb7
-rw-r--r--db/migrate/20130820102832_add_access_to_project_group_link.rb6
-rw-r--r--db/migrate/20130821090530_remove_deprecated_tables.rb12
-rw-r--r--db/migrate/20130821090531_add_internal_ids_to_milestones.rb6
-rw-r--r--db/migrate/20130909132950_add_description_to_merge_request.rb6
-rw-r--r--db/migrate/20130926081215_change_owner_id_for_group.rb10
-rw-r--r--db/migrate/20131005191208_add_avatar_to_users.rb6
-rw-r--r--db/migrate/20131009115346_add_confirmable_to_users.rb16
-rw-r--r--db/migrate/20131106151520_remove_default_branch.rb10
-rw-r--r--db/migrate/20131112114325_create_broadcast_messages.rb15
-rw-r--r--db/migrate/20131112220935_add_visibility_level_to_projects.rb16
-rw-r--r--db/migrate/20131129154016_add_archived_to_projects.rb6
-rw-r--r--db/migrate/20131130165425_add_color_and_font_to_broadcast_messages.rb7
-rw-r--r--db/migrate/20131202192556_add_event_fields_for_web_hook.rb8
-rw-r--r--db/migrate/20131214224427_add_hide_no_ssh_key_to_users.rb6
-rw-r--r--db/migrate/20131217102743_add_recipients_to_service.rb6
-rw-r--r--db/migrate/20140116231608_add_website_url_to_users.rb6
-rw-r--r--db/migrate/20140122112253_create_merge_request_diffs.rb24
-rw-r--r--db/migrate/20140122114406_migrate_mr_diffs.rb10
-rw-r--r--db/migrate/20140122122549_remove_m_rdiff_fields.rb22
-rw-r--r--db/migrate/20140125162722_add_avatar_to_projects.rb6
-rw-r--r--db/migrate/20140127170938_add_group_avatars.rb6
-rw-r--r--db/migrate/20140209025651_create_emails.rb16
-rw-r--r--db/migrate/20140214102325_add_api_key_to_services.rb6
-rw-r--r--db/migrate/20140304005354_add_index_merge_request_diffs_on_merge_request_id.rb6
-rw-r--r--db/migrate/20140305193308_add_tag_push_hooks_to_project_hook.rb6
-rw-r--r--db/migrate/20140312145357_add_import_status_to_project.rb6
-rw-r--r--db/migrate/20140313092127_init_schema.rb337
-rw-r--r--db/migrate/20140313092127_migrate_already_imported_projects.rb15
-rw-r--r--db/migrate/20180326202229_create_ci_build_trace_chunks.rb17
-rw-r--r--db/migrate/20180406204716_add_limits_ci_build_trace_chunks_raw_data_for_mysql.rb13
-rw-r--r--db/migrate/20180420010016_add_pipeline_build_foreign_key.rb27
-rw-r--r--db/migrate/20180420010616_cleanup_build_stage_migration.rb61
-rw-r--r--db/migrate/20180503131624_create_remote_mirrors.rb33
-rw-r--r--db/migrate/20180503141722_add_remote_mirror_available_overridden_to_projects.rb15
-rw-r--r--db/migrate/20180503193542_add_indexes_to_remote_mirror.rb15
-rw-r--r--db/migrate/20180503193953_add_mirror_available_to_application_settings.rb15
-rw-r--r--db/migrate/20180503200320_enable_prometheus_metrics_by_default.rb11
-rw-r--r--db/migrate/20180508055821_make_remote_mirrors_disabled_by_default.rb11
-rw-r--r--db/migrate/20180508100222_add_not_null_constraint_to_project_mirror_data_foreign_key.rb21
-rw-r--r--db/migrate/20180508102840_add_unique_constraint_to_project_mirror_data_project_id_index.rb31
-rw-r--r--db/migrate/20180508135515_set_runner_type_not_null.rb9
-rw-r--r--db/migrate/20180511090724_add_index_on_ci_runners_runner_type.rb15
-rw-r--r--db/migrate/limits_ci_build_trace_chunks_raw_data_for_mysql.rb9
-rw-r--r--db/post_migrate/20180511174224_add_unique_constraint_to_project_features_project_id.rb43
-rw-r--r--db/post_migrate/20180512061621_add_not_null_constraint_to_project_features_project_id.rb21
-rw-r--r--db/schema.rb50
-rw-r--r--doc/README.md3
-rw-r--r--doc/administration/external_database.md17
-rw-r--r--doc/administration/index.md4
-rw-r--r--doc/administration/job_traces.md95
-rw-r--r--doc/administration/pages/index.md8
-rw-r--r--doc/administration/raketasks/project_import_export.md2
-rw-r--r--doc/administration/repository_checks.md26
-rw-r--r--doc/api/group_milestones.md2
-rw-r--r--doc/api/jobs.md4
-rw-r--r--doc/api/runners.md83
-rw-r--r--doc/api/services.md6
-rw-r--r--doc/articles/openshift_and_gitlab/index.md5
-rw-r--r--doc/ci/README.md1
-rw-r--r--doc/ci/examples/container_scanning.md2
-rw-r--r--doc/ci/quick_start/README.md4
-rw-r--r--doc/ci/runners/README.md17
-rw-r--r--doc/ci/services/docker-services.md8
-rw-r--r--doc/ci/variables/README.md3
-rw-r--r--doc/ci/yaml/README.md18
-rw-r--r--doc/container_registry/README.md2
-rw-r--r--doc/customization/issue_closing.md7
-rw-r--r--doc/development/README.md2
-rw-r--r--doc/development/code_review.md5
-rw-r--r--doc/development/database_debugging.md2
-rw-r--r--doc/development/doc_styleguide.md118
-rw-r--r--doc/development/fe_guide/development_process.md6
-rw-r--r--doc/development/fe_guide/index.md6
-rw-r--r--doc/development/fe_guide/vue.md278
-rw-r--r--doc/development/fe_guide/vuex.md358
-rw-r--r--doc/development/rake_tasks.md5
-rw-r--r--doc/development/testing_guide/best_practices.md29
-rw-r--r--doc/development/testing_guide/ci.md3
-rw-r--r--doc/development/testing_guide/frontend_testing.md20
-rw-r--r--doc/development/testing_guide/index.md15
-rw-r--r--doc/development/testing_guide/testing_levels.md1
-rw-r--r--doc/development/writing_documentation.md17
-rw-r--r--doc/install/README.md1
-rw-r--r--doc/install/azure/index.md5
-rw-r--r--doc/install/docker.md2
-rw-r--r--doc/install/google_cloud_platform/index.md4
-rw-r--r--doc/install/installation.md4
-rw-r--r--doc/install/kubernetes/gitlab_omnibus.md2
-rw-r--r--doc/install/kubernetes/gitlab_runner_chart.md6
-rw-r--r--doc/install/kubernetes/index.md6
-rw-r--r--doc/install/openshift_and_gitlab/index.md2
-rw-r--r--doc/legal/README.md3
-rw-r--r--doc/legal/corporate_contributor_license_agreement.md5
-rw-r--r--doc/legal/individual_contributor_license_agreement.md5
-rw-r--r--doc/raketasks/features.md2
-rw-r--r--doc/raketasks/web_hooks.md2
-rw-r--r--doc/topics/git/how_to_install_git/index.md1
-rw-r--r--doc/university/training/gitlab_flow.md30
-rw-r--r--doc/university/training/topics/gitlab_flow.md56
-rw-r--r--doc/university/training/topics/merge_requests.md2
-rw-r--r--doc/update/10.7-to-10.8.md362
-rw-r--r--doc/user/admin_area/labels.md2
-rw-r--r--doc/user/group/index.md20
-rw-r--r--doc/user/index.md4
-rw-r--r--doc/user/permissions.md4
-rw-r--r--doc/user/project/integrations/prometheus_library/nginx.md10
-rw-r--r--doc/user/project/merge_requests/maintainer_access.md2
-rw-r--r--doc/user/project/pages/index.md4
-rw-r--r--doc/user/project/pages/introduction.md6
-rw-r--r--doc/user/project/settings/import_export.md1
-rw-r--r--doc/user/project/web_ide/img/commit_changes.pngbin672321 -> 244172 bytes
-rw-r--r--doc/user/project/web_ide/index.md23
-rw-r--r--doc/workflow/protected_branches.md2
-rw-r--r--doc/workflow/repository_mirroring.md111
-rw-r--r--doc/workflow/repository_mirroring/repository_mirroring_diverged_branch_push.pngbin0 -> 9512 bytes
-rw-r--r--doc/workflow/repository_mirroring/repository_mirroring_github_edit_personal_access_token.pngbin0 -> 20739 bytes
-rw-r--r--doc/workflow/repository_mirroring/repository_mirroring_gitlab_push_to_a_remote_repository.pngbin0 -> 16538 bytes
-rw-r--r--doc/workflow/repository_mirroring/repository_mirroring_gitlab_push_to_a_remote_repository_update_now.pngbin0 -> 16765 bytes
-rw-r--r--doc/workflow/repository_mirroring/repository_mirroring_push_settings.pngbin0 -> 18226 bytes
-rw-r--r--ee/app/controllers/ee/ldap/omniauth_callbacks_controller.rb22
-rw-r--r--ee/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb29
-rw-r--r--features/project/builds/artifacts.feature65
-rw-r--r--features/project/commits/diff_comments.feature96
-rw-r--r--features/project/deploy_keys.feature46
-rw-r--r--features/project/ff_merge_requests.feature41
-rw-r--r--features/project/forked_merge_requests.feature51
-rw-r--r--features/project/issues/references.feature33
-rw-r--r--features/project/merge_requests/references.feature31
-rw-r--r--features/steps/group/members.rb68
-rw-r--r--features/steps/profile/notifications.rb20
-rw-r--r--features/steps/project/builds/artifacts.rb98
-rw-r--r--features/steps/project/commits/branches.rb32
-rw-r--r--features/steps/project/commits/comments.rb6
-rw-r--r--features/steps/project/commits/diff_comments.rb6
-rw-r--r--features/steps/project/create.rb23
-rw-r--r--features/steps/project/deploy_keys.rb89
-rw-r--r--features/steps/project/ff_merge_requests.rb87
-rw-r--r--features/steps/project/forked_merge_requests.rb139
-rw-r--r--features/steps/project/issues/filter_labels.rb61
-rw-r--r--features/steps/project/issues/issues.rb175
-rw-r--r--features/steps/project/issues/milestones.rb20
-rw-r--r--features/steps/project/issues/references.rb7
-rw-r--r--features/steps/project/merge_requests/references.rb7
-rw-r--r--features/steps/project/source/browse_files.rb435
-rw-r--r--features/steps/shared/active_tab.rb32
-rw-r--r--features/steps/shared/admin.rb11
-rw-r--r--features/steps/shared/authentication.rb75
-rw-r--r--features/steps/shared/builds.rb53
-rw-r--r--features/steps/shared/diff_note.rb237
-rw-r--r--features/steps/shared/group.rb46
-rw-r--r--features/steps/shared/issuable.rb167
-rw-r--r--features/steps/shared/markdown.rb11
-rw-r--r--features/steps/shared/note.rb25
-rw-r--r--features/steps/shared/paths.rb430
-rw-r--r--features/steps/shared/project.rb132
-rw-r--r--features/steps/shared/project_tab.rb66
-rw-r--r--features/steps/shared/shortcuts.rb18
-rw-r--r--features/steps/shared/sidebar_active_tab.rb31
-rw-r--r--features/steps/shared/user.rb41
-rw-r--r--features/support/capybara.rb50
-rw-r--r--features/support/db_cleaner.rb11
-rw-r--r--features/support/env.rb60
-rw-r--r--features/support/gitaly.rb3
-rw-r--r--features/support/login_helpers.rb19
-rw-r--r--features/support/rerun.rb16
-rw-r--r--lib/api/api_guard.rb12
-rw-r--r--lib/api/entities.rb1
-rw-r--r--lib/api/issues.rb3
-rw-r--r--lib/api/runner.rb20
-rw-r--r--lib/gitlab/auth/blocked_user_tracker.rb4
-rw-r--r--lib/gitlab/auth/user_access_denied_reason.rb33
-rw-r--r--lib/gitlab/build_access.rb12
-rw-r--r--lib/gitlab/ci/pipeline/chain/build.rb3
-rw-r--r--lib/gitlab/ci/pipeline/chain/command.rb2
-rw-r--r--lib/gitlab/ci/trace.rb32
-rw-r--r--lib/gitlab/ci/trace/chunked_io.rb231
-rw-r--r--lib/gitlab/ci/trace/stream.rb11
-rw-r--r--lib/gitlab/email/handler/create_note_handler.rb3
-rw-r--r--lib/gitlab/email/handler/reply_processing.rb8
-rw-r--r--lib/gitlab/file_detector.rb1
-rw-r--r--lib/gitlab/git/blame.rb10
-rw-r--r--lib/gitlab/git/blob.rb6
-rw-r--r--lib/gitlab/git/raw_diff_change.rb13
-rw-r--r--lib/gitlab/git/repository.rb44
-rw-r--r--lib/gitlab/git/wiki.rb3
-rw-r--r--lib/gitlab/git_access.rb12
-rw-r--r--lib/gitlab/gitaly_client/repository_service.rb8
-rw-r--r--lib/gitlab/gitaly_client/storage_settings.rb11
-rw-r--r--lib/gitlab/import_export/import_export.yml1
-rw-r--r--lib/gitlab/import_export/relation_factory.rb2
-rw-r--r--lib/gitlab/metrics/prometheus.rb8
-rw-r--r--lib/gitlab/multi_collection_paginator.rb2
-rw-r--r--lib/gitlab/project_search_results.rb2
-rw-r--r--lib/gitlab/repo_path.rb19
-rw-r--r--lib/gitlab/untrusted_regexp.rb19
-rw-r--r--lib/gitlab/usage_data.rb1
-rw-r--r--lib/gitlab/webpack/dev_server_middleware.rb (renamed from lib/gitlab/middleware/webpack_proxy.rb)4
-rw-r--r--lib/gitlab/webpack/manifest.rb27
-rw-r--r--lib/tasks/gitlab/test.rake1
-rw-r--r--lib/tasks/migrate/add_limits_mysql.rake2
-rw-r--r--lib/tasks/spinach.rake60
-rw-r--r--locale/gitlab.pot130
-rw-r--r--package.json38
-rw-r--r--qa/qa/page/project/job/show.rb22
-rw-r--r--qa/qa/specs/features/project/deploy_key_clone_spec.rb12
-rwxr-xr-xscripts/gitaly-test-build37
-rwxr-xr-xscripts/gitaly-test-spawn26
-rw-r--r--scripts/gitaly_test.rb97
-rw-r--r--spec/controllers/concerns/send_file_upload_spec.rb15
-rw-r--r--spec/controllers/groups/runners_controller_spec.rb74
-rw-r--r--spec/controllers/projects/compare_controller_spec.rb310
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb27
-rw-r--r--spec/controllers/projects/mirrors_controller_spec.rb72
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb39
-rw-r--r--spec/db/production/settings_spec.rb6
-rw-r--r--spec/factories/ci/build_trace_chunks.rb7
-rw-r--r--spec/factories/ci/runners.rb2
-rw-r--r--spec/factories/clusters/clusters.rb4
-rw-r--r--spec/factories/merge_requests.rb5
-rw-r--r--spec/factories/project_wikis.rb2
-rw-r--r--spec/factories/projects.rb50
-rw-r--r--spec/factories/remote_mirrors.rb6
-rw-r--r--spec/fast_spec_helper.rb12
-rw-r--r--spec/features/groups/members/filter_members_spec.rb54
-rw-r--r--spec/features/issuables/markdown_references/internal_references_spec.rb140
-rw-r--r--spec/features/issuables/markdown_references/jira_spec.rb187
-rw-r--r--spec/features/issuables/markdown_references_spec.rb193
-rw-r--r--spec/features/projects/artifacts/browse_spec.rb67
-rw-r--r--spec/features/projects/artifacts/download_spec.rb61
-rw-r--r--spec/features/projects/artifacts/user_browses_artifacts_spec.rb110
-rw-r--r--spec/features/projects/artifacts/user_downloads_artifacts_spec.rb44
-rw-r--r--spec/features/projects/clusters/gcp_spec.rb41
-rw-r--r--spec/features/projects/commit/comments/user_adds_comment_spec.rb170
-rw-r--r--spec/features/projects/commit/comments/user_deletes_comments_spec.rb37
-rw-r--r--spec/features/projects/commit/comments/user_edits_comments_spec.rb42
-rw-r--r--spec/features/projects/commit/user_comments_on_commit_spec.rb110
-rw-r--r--spec/features/projects/deploy_keys_spec.rb6
-rw-r--r--spec/features/projects/jobs_spec.rb42
-rw-r--r--spec/features/projects/merge_requests/user_accepts_merge_request_spec.rb9
-rw-r--r--spec/features/projects/merge_requests/user_creates_merge_request_spec.rb80
-rw-r--r--spec/features/projects/merge_requests/user_merges_merge_request_spec.rb43
-rw-r--r--spec/features/projects/merge_requests/user_rebases_merge_request_spec.rb34
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb49
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb34
-rw-r--r--spec/features/projects/remote_mirror_spec.rb34
-rw-r--r--spec/features/projects/settings/repository_settings_spec.rb25
-rw-r--r--spec/features/projects/settings/user_interacts_with_deploy_keys_spec.rb125
-rw-r--r--spec/features/projects/tree/create_directory_spec.rb7
-rw-r--r--spec/features/projects/tree/create_file_spec.rb5
-rw-r--r--spec/features/projects/wiki/markdown_preview_spec.rb2
-rw-r--r--spec/features/projects/wiki/shortcuts_spec.rb2
-rw-r--r--spec/features/projects/wiki/user_creates_wiki_page_spec.rb10
-rw-r--r--spec/features/projects/wiki/user_deletes_wiki_page_spec.rb5
-rw-r--r--spec/features/projects/wiki/user_git_access_wiki_page_spec.rb2
-rw-r--r--spec/features/projects/wiki/user_updates_wiki_page_spec.rb8
-rw-r--r--spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb1
-rw-r--r--spec/features/projects/wiki/user_views_wiki_page_spec.rb2
-rw-r--r--spec/features/raven_js_spec.rb2
-rw-r--r--spec/features/runners_spec.rb134
-rw-r--r--spec/features/search/user_searches_for_wiki_pages_spec.rb2
-rw-r--r--spec/features/users/login_spec.rb102
-rw-r--r--spec/features/users/terms_spec.rb18
-rw-r--r--spec/fixtures/api/schemas/ci_detailed_status.json24
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_widget.json2
-rw-r--r--spec/fixtures/api/schemas/job.json24
-rw-r--r--spec/fixtures/api/schemas/pipeline_stage.json24
-rw-r--r--spec/helpers/application_helper_spec.rb12
-rw-r--r--spec/helpers/projects_helper_spec.rb2
-rw-r--r--spec/initializers/6_validations_spec.rb20
-rw-r--r--spec/javascripts/deploy_keys/components/action_btn_spec.js64
-rw-r--r--spec/javascripts/deploy_keys/components/app_spec.js137
-rw-r--r--spec/javascripts/deploy_keys/components/key_spec.js102
-rw-r--r--spec/javascripts/deploy_keys/components/keys_panel_spec.js42
-rw-r--r--spec/javascripts/fixtures/deploy_keys.rb4
-rw-r--r--spec/javascripts/fixtures/mini_dropdown_graph.html.haml1
-rw-r--r--spec/javascripts/gpg_badges_spec.js4
-rw-r--r--spec/javascripts/helpers/vue_mount_component_helper.js22
-rw-r--r--spec/javascripts/ide/components/activity_bar_spec.js92
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/actions_spec.js14
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js40
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/form_spec.js149
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/list_spec.js41
-rw-r--r--spec/javascripts/ide/components/ide_context_bar_spec.js37
-rw-r--r--spec/javascripts/ide/components/ide_external_links_spec.js43
-rw-r--r--spec/javascripts/ide/components/ide_project_tree_spec.js39
-rw-r--r--spec/javascripts/ide/components/ide_repo_tree_spec.js43
-rw-r--r--spec/javascripts/ide/components/ide_review_spec.js69
-rw-r--r--spec/javascripts/ide/components/ide_side_bar_spec.js33
-rw-r--r--spec/javascripts/ide/components/ide_spec.js9
-rw-r--r--spec/javascripts/ide/components/ide_status_bar_spec.js63
-rw-r--r--spec/javascripts/ide/components/ide_tree_list_spec.js54
-rw-r--r--spec/javascripts/ide/components/ide_tree_spec.js34
-rw-r--r--spec/javascripts/ide/components/repo_commit_section_spec.js162
-rw-r--r--spec/javascripts/ide/components/repo_editor_spec.js53
-rw-r--r--spec/javascripts/ide/components/repo_file_spec.js37
-rw-r--r--spec/javascripts/ide/components/repo_tabs_spec.js54
-rw-r--r--spec/javascripts/ide/lib/common/model_spec.js4
-rw-r--r--spec/javascripts/ide/lib/editor_spec.js4
-rw-r--r--spec/javascripts/ide/mock_data.js16
-rw-r--r--spec/javascripts/ide/stores/actions/file_spec.js40
-rw-r--r--spec/javascripts/ide/stores/actions/project_spec.js71
-rw-r--r--spec/javascripts/ide/stores/actions_spec.js43
-rw-r--r--spec/javascripts/ide/stores/getters_spec.js26
-rw-r--r--spec/javascripts/ide/stores/modules/commit/actions_spec.js30
-rw-r--r--spec/javascripts/ide/stores/mutations/branch_spec.js22
-rw-r--r--spec/javascripts/ide/stores/mutations/file_spec.js36
-rw-r--r--spec/javascripts/ide/stores/mutations_spec.js22
-rw-r--r--spec/javascripts/monitoring/graph/flag_spec.js19
-rw-r--r--spec/javascripts/monitoring/graph/track_line_spec.js10
-rw-r--r--spec/javascripts/monitoring/graph_path_spec.js2
-rw-r--r--spec/javascripts/monitoring/graph_spec.js7
-rw-r--r--spec/javascripts/pipelines/async_button_spec.js62
-rw-r--r--spec/javascripts/pipelines/graph/action_component_spec.js2
-rw-r--r--spec/javascripts/pipelines/mock_data.js101
-rw-r--r--spec/javascripts/pipelines/pipelines_table_row_spec.js33
-rw-r--r--spec/javascripts/pipelines/stage_spec.js11
-rw-r--r--spec/javascripts/projects_dropdown/components/app_spec.js41
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_maintainer_edit_spec.js40
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js22
-rw-r--r--spec/javascripts/vue_mr_widget/mock_data.js2
-rw-r--r--spec/javascripts/vue_mr_widget/mr_widget_options_spec.js2
-rw-r--r--spec/lib/backup/repository_spec.rb4
-rw-r--r--spec/lib/gitlab/auth/blocked_user_tracker_spec.rb44
-rw-r--r--spec/lib/gitlab/auth/user_access_denied_reason_spec.rb34
-rw-r--r--spec/lib/gitlab/build_access_spec.rb23
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/build_spec.rb9
-rw-r--r--spec/lib/gitlab/ci/trace/chunked_io_spec.rb383
-rw-r--r--spec/lib/gitlab/ci/trace/stream_spec.rb547
-rw-r--r--spec/lib/gitlab/ci/trace_spec.rb547
-rw-r--r--spec/lib/gitlab/data_builder/wiki_page_spec.rb2
-rw-r--r--spec/lib/gitlab/email/handler/create_issue_handler_spec.rb1
-rw-r--r--spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb1
-rw-r--r--spec/lib/gitlab/email/handler/create_note_handler_spec.rb1
-rw-r--r--spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb1
-rw-r--r--spec/lib/gitlab/email/receiver_spec.rb1
-rw-r--r--spec/lib/gitlab/git/blob_spec.rb20
-rw-r--r--spec/lib/gitlab/git/raw_diff_change_spec.rb2
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb77
-rw-r--r--spec/lib/gitlab/git_access_spec.rb94
-rw-r--r--spec/lib/gitlab/gitaly_client/repository_service_spec.rb11
-rw-r--r--spec/lib/gitlab/gitaly_client/storage_settings_spec.rb29
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml2
-rw-r--r--spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb2
-rw-r--r--spec/lib/gitlab/incoming_email_spec.rb4
-rw-r--r--spec/lib/gitlab/metrics/prometheus_spec.rb18
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb18
-rw-r--r--spec/lib/gitlab/repo_path_spec.rb19
-rw-r--r--spec/lib/gitlab/untrusted_regexp_spec.rb8
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb1
-rw-r--r--spec/mailers/notify_spec.rb32
-rw-r--r--spec/migrations/add_not_null_constraint_to_project_mirror_data_foreign_key_spec.rb18
-rw-r--r--spec/migrations/add_pipeline_build_foreign_key_spec.rb32
-rw-r--r--spec/migrations/add_unique_constraint_to_project_features_project_id_spec.rb59
-rw-r--r--spec/migrations/cleanup_build_stage_migration_spec.rb53
-rw-r--r--spec/models/blob_viewer/readme_spec.rb2
-rw-r--r--spec/models/ci/build_spec.rb5
-rw-r--r--spec/models/ci/build_trace_chunk_spec.rb396
-rw-r--r--spec/models/ci/pipeline_spec.rb2
-rw-r--r--spec/models/ci/runner_spec.rb29
-rw-r--r--spec/models/clusters/applications/runner_spec.rb9
-rw-r--r--spec/models/clusters/platforms/kubernetes_spec.rb9
-rw-r--r--spec/models/concerns/issuable_spec.rb18
-rw-r--r--spec/models/concerns/reactive_caching_spec.rb22
-rw-r--r--spec/models/concerns/sha_attribute_spec.rb69
-rw-r--r--spec/models/group_spec.rb89
-rw-r--r--spec/models/guest_spec.rb6
-rw-r--r--spec/models/merge_request_spec.rb16
-rw-r--r--spec/models/namespace_spec.rb15
-rw-r--r--spec/models/project_services/microsoft_teams_service_spec.rb2
-rw-r--r--spec/models/project_spec.rb125
-rw-r--r--spec/models/project_wiki_spec.rb17
-rw-r--r--spec/models/remote_mirror_spec.rb267
-rw-r--r--spec/models/repository_spec.rb70
-rw-r--r--spec/models/user_spec.rb41
-rw-r--r--spec/models/wiki_page_spec.rb2
-rw-r--r--spec/policies/ci/build_policy_spec.rb13
-rw-r--r--spec/policies/ci/pipeline_policy_spec.rb12
-rw-r--r--spec/policies/global_policy_spec.rb90
-rw-r--r--spec/policies/project_policy_spec.rb2
-rw-r--r--spec/requests/api/helpers_spec.rb18
-rw-r--r--spec/requests/api/issues_spec.rb300
-rw-r--r--spec/requests/api/runner_spec.rb74
-rw-r--r--spec/requests/api/runners_spec.rb5
-rw-r--r--spec/requests/api/search_spec.rb2
-rw-r--r--spec/requests/api/wikis_spec.rb34
-rw-r--r--spec/requests/git_http_spec.rb53
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb21
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb20
-rw-r--r--spec/services/ci/retry_build_service_spec.rb2
-rw-r--r--spec/services/ci/retry_pipeline_service_spec.rb29
-rw-r--r--spec/services/clusters/create_service_spec.rb68
-rw-r--r--spec/services/git_push_service_spec.rb66
-rw-r--r--spec/services/issuable/common_system_notes_service_spec.rb28
-rw-r--r--spec/services/notification_service_spec.rb31
-rw-r--r--spec/services/projects/destroy_service_spec.rb13
-rw-r--r--spec/services/projects/update_remote_mirror_service_spec.rb355
-rw-r--r--spec/services/test_hooks/project_service_spec.rb1
-rw-r--r--spec/services/wiki_pages/create_service_spec.rb2
-rw-r--r--spec/spec_helper.rb26
-rw-r--r--spec/support/api/time_tracking_shared_examples.rb28
-rw-r--r--spec/support/chunked_io/chunked_io_helpers.rb11
-rw-r--r--spec/support/forgery_protection.rb17
-rw-r--r--spec/support/helpers/kubernetes_helpers.rb69
-rw-r--r--spec/support/helpers/migrations_helpers.rb10
-rw-r--r--spec/support/helpers/notification_helpers.rb33
-rw-r--r--spec/support/helpers/test_env.rb6
-rw-r--r--spec/support/redis/redis_helpers.rb18
-rw-r--r--spec/support/services/clusters/create_service_shared.rb57
-rw-r--r--spec/support/services/migrate_to_ghost_user_service_shared_examples.rb2
-rw-r--r--spec/support/shared_contexts/email_shared_blocks.rb (renamed from spec/lib/gitlab/email/email_shared_blocks.rb)0
-rw-r--r--spec/support/shared_examples/ci_trace_shared_examples.rb741
-rw-r--r--spec/support/shared_examples/common_system_notes_examples.rb27
-rw-r--r--spec/support/shared_examples/fast_destroy_all.rb38
-rw-r--r--spec/support/shared_examples/notify_shared_examples.rb32
-rw-r--r--spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb2
-rw-r--r--spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb165
-rw-r--r--spec/workers/admin_email_worker_spec.rb41
-rw-r--r--spec/workers/repository_check/batch_worker_spec.rb4
-rw-r--r--spec/workers/repository_check/single_repository_worker_spec.rb79
-rw-r--r--spec/workers/repository_remove_remote_worker_spec.rb50
-rw-r--r--spec/workers/repository_update_remote_mirror_worker_spec.rb84
-rw-r--r--vendor/gitignore/Global/JetBrains.gitignore3
-rw-r--r--vendor/gitignore/Global/Vim.gitignore2
-rw-r--r--vendor/gitignore/Java.gitignore1
-rw-r--r--vendor/gitignore/Objective-C.gitignore2
-rw-r--r--vendor/gitignore/Swift.gitignore2
-rw-r--r--vendor/gitignore/TeX.gitignore6
-rw-r--r--vendor/gitignore/Unity.gitignore3
-rw-r--r--vendor/gitignore/VisualStudio.gitignore1
-rw-r--r--vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml191
-rw-r--r--vendor/gitlab-ci-yml/Chef.gitlab-ci.yml2
-rw-r--r--vendor/gitlab-ci-yml/Docker.gitlab-ci.yml2
-rw-r--r--vendor/gitlab-ci-yml/Maven.gitlab-ci.yml2
-rw-r--r--vendor/gitlab-ci-yml/Swift.gitlab-ci.yml16
-rw-r--r--vendor/ingress/values.yaml5
-rw-r--r--vendor/licenses.csv396
-rw-r--r--yarn.lock1592
821 files changed, 18046 insertions, 11466 deletions
diff --git a/.flayignore b/.flayignore
index 3d69bb2c985..0c4eee10ffa 100644
--- a/.flayignore
+++ b/.flayignore
@@ -9,3 +9,4 @@ lib/gitlab/gitaly_client/operation_service.rb
lib/gitlab/background_migration/*
app/models/project_services/kubernetes_service.rb
lib/gitlab/workhorse.rb
+lib/gitlab/ci/trace/chunked_io.rb
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 05487134cb1..b1445feee58 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -10,6 +10,7 @@ image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.7-golang-1.9-git
paths:
- vendor/ruby
- .yarn-cache/
+ - vendor/gitaly-ruby
.push-cache: &push-cache
cache:
@@ -30,7 +31,6 @@ variables:
GIT_SUBMODULE_STRATEGY: "none"
GET_SOURCES_ATTEMPTS: "3"
KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-master.json
- KNAPSACK_SPINACH_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/spinach_report-master.json
FLAKY_RSPEC_SUITE_REPORT_PATH: rspec_flaky/report-suite.json
before_script:
@@ -178,46 +178,6 @@ stages:
<<: *rspec-metadata-mysql
<<: *rails5
-.spinach-metadata: &spinach-metadata
- <<: *dedicated-runner
- <<: *except-docs-and-qa
- <<: *pull-cache
- <<: *rails5-variables
- stage: test
- script:
- - JOB_NAME=( $CI_JOB_NAME )
- - export CI_NODE_INDEX=${JOB_NAME[-2]}
- - export CI_NODE_TOTAL=${JOB_NAME[-1]}
- - export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- - export KNAPSACK_GENERATE_REPORT=true
- - export CACHE_CLASSES=true
- - cp ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
- - scripts/gitaly-test-spawn
- - knapsack spinach "-r rerun" -b || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -b -r rerun $(cat tmp/spinach-rerun.txt)'
- artifacts:
- expire_in: 31d
- when: always
- paths:
- - coverage/
- - knapsack/
- - tmp/capybara/
-
-.spinach-metadata-pg: &spinach-metadata-pg
- <<: *spinach-metadata
- <<: *use-pg
-
-.spinach-metadata-pg-rails5: &spinach-metadata-pg-rails5
- <<: *spinach-metadata-pg
- <<: *rails5
-
-.spinach-metadata-mysql: &spinach-metadata-mysql
- <<: *spinach-metadata
- <<: *use-mysql
-
-.spinach-metadata-mysql-rails5: &spinach-metadata-mysql-rails5
- <<: *spinach-metadata-mysql
- <<: *rails5
-
.only-canonical-masters: &only-canonical-masters
only:
- master@gitlab-org/gitlab-ce
@@ -349,9 +309,7 @@ retrieve-tests-metadata:
script:
- mkdir -p knapsack/${CI_PROJECT_NAME}/
- wget -O $KNAPSACK_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_RSPEC_SUITE_REPORT_PATH || rm $KNAPSACK_RSPEC_SUITE_REPORT_PATH
- - wget -O $KNAPSACK_SPINACH_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_SPINACH_SUITE_REPORT_PATH || rm $KNAPSACK_SPINACH_SUITE_REPORT_PATH
- '[[ -f $KNAPSACK_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_RSPEC_SUITE_REPORT_PATH}'
- - '[[ -f $KNAPSACK_SPINACH_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_SPINACH_SUITE_REPORT_PATH}'
- mkdir -p rspec_flaky/
- wget -O $FLAKY_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$FLAKY_RSPEC_SUITE_REPORT_PATH || rm $FLAKY_RSPEC_SUITE_REPORT_PATH
- '[[ -f $FLAKY_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_SUITE_REPORT_PATH}'
@@ -369,10 +327,9 @@ update-tests-metadata:
script:
- retry gem install fog-aws mime-types activesupport
- scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec-pg_node_*.json
- - scripts/merge-reports ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/spinach-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}
- - '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH'
+ - '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH'
- '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $FLAKY_RSPEC_SUITE_REPORT_PATH'
- rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json
- rm -f rspec_flaky/all_*.json rspec_flaky/new_*.json
@@ -438,134 +395,131 @@ setup-test-env:
paths:
- tmp/tests
- config/secrets.yml
-
-rspec-pg 0 28: *rspec-metadata-pg
-rspec-pg 1 28: *rspec-metadata-pg
-rspec-pg 2 28: *rspec-metadata-pg
-rspec-pg 3 28: *rspec-metadata-pg
-rspec-pg 4 28: *rspec-metadata-pg
-rspec-pg 5 28: *rspec-metadata-pg
-rspec-pg 6 28: *rspec-metadata-pg
-rspec-pg 7 28: *rspec-metadata-pg
-rspec-pg 8 28: *rspec-metadata-pg
-rspec-pg 9 28: *rspec-metadata-pg
-rspec-pg 10 28: *rspec-metadata-pg
-rspec-pg 11 28: *rspec-metadata-pg
-rspec-pg 12 28: *rspec-metadata-pg
-rspec-pg 13 28: *rspec-metadata-pg
-rspec-pg 14 28: *rspec-metadata-pg
-rspec-pg 15 28: *rspec-metadata-pg
-rspec-pg 16 28: *rspec-metadata-pg
-rspec-pg 17 28: *rspec-metadata-pg
-rspec-pg 18 28: *rspec-metadata-pg
-rspec-pg 19 28: *rspec-metadata-pg
-rspec-pg 20 28: *rspec-metadata-pg
-rspec-pg 21 28: *rspec-metadata-pg
-rspec-pg 22 28: *rspec-metadata-pg
-rspec-pg 23 28: *rspec-metadata-pg
-rspec-pg 24 28: *rspec-metadata-pg
-rspec-pg 25 28: *rspec-metadata-pg
-rspec-pg 26 28: *rspec-metadata-pg
-rspec-pg 27 28: *rspec-metadata-pg
-
-rspec-mysql 0 28: *rspec-metadata-mysql
-rspec-mysql 1 28: *rspec-metadata-mysql
-rspec-mysql 2 28: *rspec-metadata-mysql
-rspec-mysql 3 28: *rspec-metadata-mysql
-rspec-mysql 4 28: *rspec-metadata-mysql
-rspec-mysql 5 28: *rspec-metadata-mysql
-rspec-mysql 6 28: *rspec-metadata-mysql
-rspec-mysql 7 28: *rspec-metadata-mysql
-rspec-mysql 8 28: *rspec-metadata-mysql
-rspec-mysql 9 28: *rspec-metadata-mysql
-rspec-mysql 10 28: *rspec-metadata-mysql
-rspec-mysql 11 28: *rspec-metadata-mysql
-rspec-mysql 12 28: *rspec-metadata-mysql
-rspec-mysql 13 28: *rspec-metadata-mysql
-rspec-mysql 14 28: *rspec-metadata-mysql
-rspec-mysql 15 28: *rspec-metadata-mysql
-rspec-mysql 16 28: *rspec-metadata-mysql
-rspec-mysql 17 28: *rspec-metadata-mysql
-rspec-mysql 18 28: *rspec-metadata-mysql
-rspec-mysql 19 28: *rspec-metadata-mysql
-rspec-mysql 20 28: *rspec-metadata-mysql
-rspec-mysql 21 28: *rspec-metadata-mysql
-rspec-mysql 22 28: *rspec-metadata-mysql
-rspec-mysql 23 28: *rspec-metadata-mysql
-rspec-mysql 24 28: *rspec-metadata-mysql
-rspec-mysql 25 28: *rspec-metadata-mysql
-rspec-mysql 26 28: *rspec-metadata-mysql
-rspec-mysql 27 28: *rspec-metadata-mysql
-
-spinach-pg 0 2: *spinach-metadata-pg
-spinach-pg 1 2: *spinach-metadata-pg
-
-spinach-mysql 0 2: *spinach-metadata-mysql
-spinach-mysql 1 2: *spinach-metadata-mysql
-
-rspec-pg-rails5 0 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 1 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 2 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 3 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 4 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 5 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 6 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 7 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 8 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 9 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 10 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 11 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 12 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 13 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 14 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 15 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 16 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 17 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 18 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 19 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 20 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 21 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 22 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 23 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 24 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 25 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 26 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 27 28: *rspec-metadata-pg-rails5
-
-rspec-mysql-rails5 0 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 1 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 2 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 3 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 4 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 5 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 6 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 7 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 8 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 9 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 10 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 11 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 12 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 13 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 14 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 15 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 16 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 17 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 18 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 19 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 20 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 21 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 22 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 23 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 24 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 25 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 26 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 27 28: *rspec-metadata-mysql-rails5
-
-spinach-pg-rails5 0 2: *spinach-metadata-pg-rails5
-spinach-pg-rails5 1 2: *spinach-metadata-pg-rails5
-
-spinach-mysql-rails5 0 2: *spinach-metadata-mysql-rails5
-spinach-mysql-rails5 1 2: *spinach-metadata-mysql-rails5
+ - vendor/gitaly-ruby
+
+rspec-pg 0 30: *rspec-metadata-pg
+rspec-pg 1 30: *rspec-metadata-pg
+rspec-pg 2 30: *rspec-metadata-pg
+rspec-pg 3 30: *rspec-metadata-pg
+rspec-pg 4 30: *rspec-metadata-pg
+rspec-pg 5 30: *rspec-metadata-pg
+rspec-pg 6 30: *rspec-metadata-pg
+rspec-pg 7 30: *rspec-metadata-pg
+rspec-pg 8 30: *rspec-metadata-pg
+rspec-pg 9 30: *rspec-metadata-pg
+rspec-pg 10 30: *rspec-metadata-pg
+rspec-pg 11 30: *rspec-metadata-pg
+rspec-pg 12 30: *rspec-metadata-pg
+rspec-pg 13 30: *rspec-metadata-pg
+rspec-pg 14 30: *rspec-metadata-pg
+rspec-pg 15 30: *rspec-metadata-pg
+rspec-pg 16 30: *rspec-metadata-pg
+rspec-pg 17 30: *rspec-metadata-pg
+rspec-pg 18 30: *rspec-metadata-pg
+rspec-pg 19 30: *rspec-metadata-pg
+rspec-pg 20 30: *rspec-metadata-pg
+rspec-pg 21 30: *rspec-metadata-pg
+rspec-pg 22 30: *rspec-metadata-pg
+rspec-pg 23 30: *rspec-metadata-pg
+rspec-pg 24 30: *rspec-metadata-pg
+rspec-pg 25 30: *rspec-metadata-pg
+rspec-pg 26 30: *rspec-metadata-pg
+rspec-pg 27 30: *rspec-metadata-pg
+rspec-pg 28 30: *rspec-metadata-pg
+rspec-pg 29 30: *rspec-metadata-pg
+
+rspec-mysql 0 30: *rspec-metadata-mysql
+rspec-mysql 1 30: *rspec-metadata-mysql
+rspec-mysql 2 30: *rspec-metadata-mysql
+rspec-mysql 3 30: *rspec-metadata-mysql
+rspec-mysql 4 30: *rspec-metadata-mysql
+rspec-mysql 5 30: *rspec-metadata-mysql
+rspec-mysql 6 30: *rspec-metadata-mysql
+rspec-mysql 7 30: *rspec-metadata-mysql
+rspec-mysql 8 30: *rspec-metadata-mysql
+rspec-mysql 9 30: *rspec-metadata-mysql
+rspec-mysql 10 30: *rspec-metadata-mysql
+rspec-mysql 11 30: *rspec-metadata-mysql
+rspec-mysql 12 30: *rspec-metadata-mysql
+rspec-mysql 13 30: *rspec-metadata-mysql
+rspec-mysql 14 30: *rspec-metadata-mysql
+rspec-mysql 15 30: *rspec-metadata-mysql
+rspec-mysql 16 30: *rspec-metadata-mysql
+rspec-mysql 17 30: *rspec-metadata-mysql
+rspec-mysql 18 30: *rspec-metadata-mysql
+rspec-mysql 19 30: *rspec-metadata-mysql
+rspec-mysql 20 30: *rspec-metadata-mysql
+rspec-mysql 21 30: *rspec-metadata-mysql
+rspec-mysql 22 30: *rspec-metadata-mysql
+rspec-mysql 23 30: *rspec-metadata-mysql
+rspec-mysql 24 30: *rspec-metadata-mysql
+rspec-mysql 25 30: *rspec-metadata-mysql
+rspec-mysql 26 30: *rspec-metadata-mysql
+rspec-mysql 27 30: *rspec-metadata-mysql
+rspec-mysql 28 30: *rspec-metadata-mysql
+rspec-mysql 29 30: *rspec-metadata-mysql
+
+rspec-pg-rails5 0 30: *rspec-metadata-pg-rails5
+rspec-pg-rails5 1 30: *rspec-metadata-pg-rails5
+rspec-pg-rails5 2 30: *rspec-metadata-pg-rails5
+rspec-pg-rails5 3 30: *rspec-metadata-pg-rails5
+rspec-pg-rails5 4 30: *rspec-metadata-pg-rails5
+rspec-pg-rails5 5 30: *rspec-metadata-pg-rails5
+rspec-pg-rails5 6 30: *rspec-metadata-pg-rails5
+rspec-pg-rails5 7 30: *rspec-metadata-pg-rails5
+rspec-pg-rails5 8 30: *rspec-metadata-pg-rails5
+rspec-pg-rails5 9 30: *rspec-metadata-pg-rails5
+rspec-pg-rails5 10 30: *rspec-metadata-pg-rails5
+rspec-pg-rails5 11 30: *rspec-metadata-pg-rails5
+rspec-pg-rails5 12 30: *rspec-metadata-pg-rails5
+rspec-pg-rails5 13 30: *rspec-metadata-pg-rails5
+rspec-pg-rails5 14 30: *rspec-metadata-pg-rails5
+rspec-pg-rails5 15 30: *rspec-metadata-pg-rails5
+rspec-pg-rails5 16 30: *rspec-metadata-pg-rails5
+rspec-pg-rails5 17 30: *rspec-metadata-pg-rails5
+rspec-pg-rails5 18 30: *rspec-metadata-pg-rails5
+rspec-pg-rails5 19 30: *rspec-metadata-pg-rails5
+rspec-pg-rails5 20 30: *rspec-metadata-pg-rails5
+rspec-pg-rails5 21 30: *rspec-metadata-pg-rails5
+rspec-pg-rails5 22 30: *rspec-metadata-pg-rails5
+rspec-pg-rails5 23 30: *rspec-metadata-pg-rails5
+rspec-pg-rails5 24 30: *rspec-metadata-pg-rails5
+rspec-pg-rails5 25 30: *rspec-metadata-pg-rails5
+rspec-pg-rails5 26 30: *rspec-metadata-pg-rails5
+rspec-pg-rails5 27 30: *rspec-metadata-pg-rails5
+rspec-pg-rails5 28 30: *rspec-metadata-pg-rails5
+rspec-pg-rails5 29 30: *rspec-metadata-pg-rails5
+
+rspec-mysql-rails5 0 30: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 1 30: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 2 30: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 3 30: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 4 30: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 5 30: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 6 30: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 7 30: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 8 30: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 9 30: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 10 30: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 11 30: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 12 30: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 13 30: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 14 30: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 15 30: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 16 30: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 17 30: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 18 30: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 19 30: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 20 30: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 21 30: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 22 30: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 23 30: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 24 30: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 25 30: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 26 30: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 27 30: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 28 30: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 29 30: *rspec-metadata-mysql-rails5
static-analysis:
<<: *dedicated-no-docs-no-db-pull-cache-job
diff --git a/.gitlab/issue_templates/Security Developer Workflow.md b/.gitlab/issue_templates/Security Developer Workflow.md
index 8dd447ed121..0c878dbf64c 100644
--- a/.gitlab/issue_templates/Security Developer Workflow.md
+++ b/.gitlab/issue_templates/Security Developer Workflow.md
@@ -28,11 +28,11 @@ Set the title to: `[Security] Description of the original issue`
- [ ] Add the ~security label and prefix with the version `WIP: [X.Y]` the title of the MR
- [ ] Make sure all MRs have a link in the [links section](#links) and are assigned to a Release Manager.
-[seckpick documentation]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/process.md#secpick-script
+[seckpick documentation]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md#secpick-script
#### Documentation and final details
-- [ ] Check the topic on #security to see when the next release is going ot happen and add a link to the [links section](#links)
+- [ ] Check the topic on #security to see when the next release is going to happen and add a link to the [links section](#links)
- [ ] Find out the versions affected (the Git history of the files affected may help you with this) and add them to the [details section](#details)
- [ ] Fill in any upgrade notes that users may need to take into account in the [details section](#details)
- [ ] Add Yes/No and further details if needed to the migration and settings columns in the [details section](#details)
diff --git a/.gitlab/merge_request_templates/Database Changes.md b/.gitlab/merge_request_templates/Database Changes.md
index 2bb1f374e98..1c4f30d9320 100644
--- a/.gitlab/merge_request_templates/Database Changes.md
+++ b/.gitlab/merge_request_templates/Database Changes.md
@@ -37,12 +37,14 @@ When removing columns, tables, indexes or other structures:
- [ ] [Documentation created/updated](https://docs.gitlab.com/ee/development/doc_styleguide.html)
- [ ] API support added
- [ ] Tests added for this feature/bug
-- Review
- - [ ] Has been reviewed by Backend
- - [ ] Has been reviewed by Database
+- Conform by the [code review guidelines](https://docs.gitlab.com/ee/development/code_review.html)
+ - [ ] Has been reviewed by a Backend maintainer
+ - [ ] Has been reviewed by a Database specialist
- [ ] Conform by the [merge request performance guides](https://docs.gitlab.com/ee/development/merge_request_performance_guidelines.html)
- [ ] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/CONTRIBUTING.md#style-guides)
-- [ ] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)
+- [ ] If you have multiple commits, please combine them into a few logically organized commits by [squashing them](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)
- [ ] Internationalization required/considered
- [ ] If paid feature, have we considered GitLab.com plan and how it works for groups and is there a design for promoting it to users who aren't on the correct plan
- [ ] End-to-end tests pass (`package-and-qa` manual pipeline job)
+
+/label ~database
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 4f5d19ce2ce..5447ebbdd8c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -31,8 +31,8 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
- [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc)
- [Team labels (~"CI/CD", ~Discussion, ~Quality, ~Platform, etc.)](#team-labels-cicd-discussion-quality-platform-etc)
- [Milestone labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#milestone-labels-deliverable-stretch-next-patch-release)
- - [Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#bug-priority-labels-p1-p2-p3-etc)
- - [Severity labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#bug-severity-labels-s1-s2-s3-etc)
+ - [Priority labels (~P1, ~P2, ~P3 , ~P4)](#bug-priority-labels-p1-p2-p3-p4)
+ - [Severity labels (~S1, ~S2, ~S3 , ~S4)](#bug-severity-labels-s1-s2-s3-s4)
- [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests)
- [Implement design & UI elements](#implement-design--ui-elements)
- [Issue tracker](#issue-tracker)
@@ -211,9 +211,10 @@ Each issue scheduled for the current milestone should be labeled ~Deliverable
or ~"Stretch". Any open issue for a previous milestone should be labeled
~"Next Patch Release", or otherwise rescheduled to a different milestone.
-### Bug Priority labels (~P1, ~P2, ~P3 & etc.)
+### Bug Priority labels (~P1, ~P2, ~P3, ~P4)
-Bug Priority labels help us define the time a ~bug fix should be completed. Priority determines how quickly the defect turnaround time must be. If there are multiple defects, the priority decides which defect has to be fixed immediately versus later.
+Bug Priority labels help us define the time a ~bug fix should be completed. Priority determines how quickly the defect turnaround time must be.
+If there are multiple defects, the priority decides which defect has to be fixed immediately versus later.
This label documents the planned timeline & urgency which is used to measure against our actual SLA on delivering ~bug fixes.
| Label | Meaning | Estimate time to fix | Guidance |
@@ -223,16 +224,7 @@ This label documents the planned timeline & urgency which is used to measure aga
| ~P3 | Medium Priority | Within the next 3 releases (approx one quarter) | |
| ~P4 | Low Priority | Anything outside the next 3 releases (approx beyond one quarter) | The issue is prominent but does not impact user workflow and a workaround is documented |
-#### Specific Priority guidance
-
-| Label | Availability / Performance |
-|-------|--------------------------------------------------------------|
-| ~P1 | |
-| ~P2 | The issue is (almost) guaranteed to occur in the near future |
-| ~P3 | The issue is likely to occur in the near future |
-| ~P4 | The issue _may_ occur but it's not likely |
-
-### Bug Severity labels (~S1, ~S2, ~S3 & etc.)
+### Bug Severity labels (~S1, ~S2, ~S3, ~S4)
Severity labels help us clearly communicate the impact of a ~bug on users.
@@ -243,14 +235,14 @@ Severity labels help us clearly communicate the impact of a ~bug on users.
| ~S3 | Major Severity | Broken Feature, workaround acceptable | Can create merge requests only from the Merge Requests page, not through the Issue. |
| ~S4 | Low Severity | Functionality inconvenience or cosmetic issue | Label colors are incorrect / not being displayed. |
-#### Specific Severity guidance
+#### Severity impact guidance
-| Label | Security Impact |
-|-------|-------------------------------------------------------------------|
-| ~S1 | >50% customers impacted (possible company extinction level event) |
-| ~S2 | Multiple customers impacted (but not apocalyptic) |
-| ~S3 | A single customer impacted |
-| ~S4 | No customer impact, or expected impact within 30 days |
+| Label | Security Impact | Availability / Performance Impact |
+|-------|---------------------------------------------------------------------|--------------------------------------------------------------|
+| ~S1 | >50% users impacted (possible company extinction level event) | |
+| ~S2 | Many users or multiple paid customers impacted (but not apocalyptic)| The issue is (almost) guaranteed to occur in the near future |
+| ~S3 | A few users or a single paid customer impacted | The issue is likely to occur in the near future |
+| ~S4 | No paid users/customer impacted, or expected impact within 30 days | The issue _may_ occur but it's not likely |
### Label for community contributors (~"Accepting Merge Requests")
@@ -728,6 +720,3 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[polling-etag]: https://docs.gitlab.com/ce/development/polling.html
[testing]: doc/development/testing_guide/index.md
[us-english]: https://en.wikipedia.org/wiki/American_English
-
-[^1]: Please note that specs other than JavaScript specs are considered backend
- code.
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index cf22efd819d..897e21587ed 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.96.2
+0.100.0
diff --git a/Gemfile b/Gemfile
index a139e8a32c4..a6cd0ca980a 100644
--- a/Gemfile
+++ b/Gemfile
@@ -6,7 +6,7 @@ end
gem_versions = {}
gem_versions['activerecord_sane_schema_dumper'] = rails5? ? '1.0' : '0.2'
gem_versions['default_value_for'] = rails5? ? '~> 3.0.5' : '~> 3.0.0'
-gem_versions['rails'] = rails5? ? '5.0.6' : '4.2.10'
+gem_versions['rails'] = rails5? ? '5.0.7' : '4.2.10'
gem_versions['rails-i18n'] = rails5? ? '~> 5.1' : '~> 4.0.9'
# --- The end of special code for migrating to Rails 5.0 ---
@@ -160,7 +160,7 @@ gem 'state_machines-activerecord', '~> 0.5.1'
gem 'acts-as-taggable-on', '~> 5.0'
# Background jobs
-gem 'sidekiq', '~> 5.0'
+gem 'sidekiq', '~> 5.1'
gem 'sidekiq-cron', '~> 0.6.0'
gem 'redis-namespace', '~> 1.5.2'
gem 'sidekiq-limit_fetch', '~> 3.4', require: false
@@ -218,9 +218,6 @@ gem 'ruby-fogbugz', '~> 0.2.1'
# Kubernetes integration
gem 'kubeclient', '~> 3.0'
-# d3
-gem 'd3_rails', '~> 3.5.0'
-
# Sanitize user input
gem 'sanitize', '~> 2.0'
gem 'babosa', '~> 1.0.2'
@@ -260,7 +257,7 @@ gem 'addressable', '~> 2.5.2'
gem 'bootstrap-sass', '~> 3.3.0'
gem 'font-awesome-rails', '~> 4.7'
gem 'gemojione', '~> 3.3'
-gem 'gon', '~> 6.1.0'
+gem 'gon', '~> 6.2'
gem 'jquery-atwho-rails', '~> 1.3.2'
gem 'request_store', '~> 1.3'
gem 'select2-rails', '~> 3.5.9'
@@ -297,7 +294,7 @@ group :metrics do
gem 'influxdb', '~> 0.2', require: false
# Prometheus
- gem 'prometheus-client-mmap', '~> 0.9.1'
+ gem 'prometheus-client-mmap', '~> 0.9.2'
gem 'raindrops', '~> 0.18'
end
@@ -328,8 +325,6 @@ group :development, :test do
gem 'factory_bot_rails', '~> 4.8.2'
gem 'rspec-rails', '~> 3.6.0'
gem 'rspec-retry', '~> 0.4.5'
- gem 'spinach-rails', '~> 0.2.1'
- gem 'spinach-rerun-reporter', '~> 0.0.2'
gem 'rspec_profiling', '~> 0.0.5'
gem 'rspec-set', '~> 0.1.3'
gem 'rspec-parameterized', require: false
@@ -346,7 +341,6 @@ group :development, :test do
gem 'spring', '~> 2.0.0'
gem 'spring-commands-rspec', '~> 1.0.4'
- gem 'spring-commands-spinach', '~> 1.1.0'
gem 'gitlab-styles', '~> 2.3', require: false
# Pin these dependencies, otherwise a new rule could break the CI pipelines
@@ -416,7 +410,7 @@ group :ed25519 do
end
# Gitaly GRPC client
-gem 'gitaly-proto', '~> 0.97.0', require: 'gitaly'
+gem 'gitaly-proto', '~> 0.99.0', require: 'gitaly'
gem 'grpc', '~> 1.11.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed
@@ -434,4 +428,4 @@ gem 'lograge', '~> 0.5'
gem 'grape_logging', '~> 1.7'
# Asset synchronization
-gem 'asset_sync', '~> 2.2.0'
+gem 'asset_sync', '~> 2.4'
diff --git a/Gemfile.lock b/Gemfile.lock
index f7e2428a07f..18c25cc34b6 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -59,7 +59,7 @@ GEM
asciidoctor (1.5.6.2)
asciidoctor-plantuml (0.0.8)
asciidoctor (~> 1.5)
- asset_sync (2.2.0)
+ asset_sync (2.4.0)
activemodel (>= 4.1.0)
fog-core
mime-types (>= 2.99)
@@ -131,7 +131,6 @@ GEM
coderay (1.1.1)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
- colorize (0.7.7)
commonmarker (0.17.8)
ruby-enum (~> 0.5)
concord (0.1.5)
@@ -147,8 +146,6 @@ GEM
creole (0.5.0)
css_parser (1.5.0)
addressable
- d3_rails (3.5.11)
- railties (>= 3.1.0)
daemons (1.2.3)
database_cleaner (1.5.3)
debug_inspector (0.0.2)
@@ -197,7 +194,7 @@ GEM
et-orbi (1.0.3)
tzinfo
eventmachine (1.0.8)
- excon (0.60.0)
+ excon (0.62.0)
execjs (2.6.0)
expression_parser (0.9.0)
factory_bot (4.8.2)
@@ -290,8 +287,7 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
- gherkin-ruby (0.3.2)
- gitaly-proto (0.97.0)
+ gitaly-proto (0.99.0)
google-protobuf (~> 3.1)
grpc (~> 1.10)
github-linguist (5.3.3)
@@ -334,9 +330,8 @@ GEM
activesupport (>= 4.2.0)
gollum-grit_adapter (1.0.1)
gitlab-grit (~> 2.7, >= 2.7.1)
- gon (6.1.0)
+ gon (6.2.0)
actionpack (>= 3.0)
- json
multi_json
request_store (>= 1.0)
google-api-client (0.19.8)
@@ -634,7 +629,7 @@ GEM
parser
unparser
procto (0.0.3)
- prometheus-client-mmap (0.9.1)
+ prometheus-client-mmap (0.9.2)
pry (0.10.4)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
@@ -849,11 +844,11 @@ GEM
rack
shoulda-matchers (3.1.2)
activesupport (>= 4.0.0)
- sidekiq (5.0.5)
+ sidekiq (5.1.3)
concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0)
rack-protection (>= 1.5.0)
- redis (>= 3.3.4, < 5)
+ redis (>= 3.3.5, < 5)
sidekiq-cron (0.6.0)
rufus-scheduler (>= 3.3.0)
sidekiq (>= 4.2.1)
@@ -872,22 +867,10 @@ GEM
simplecov-html (0.10.0)
slack-notifier (1.5.1)
slop (3.6.0)
- spinach (0.8.10)
- colorize
- gherkin-ruby (>= 0.3.2)
- json
- spinach-rails (0.2.1)
- capybara (>= 2.0.0)
- railties (>= 3)
- spinach (>= 0.4)
- spinach-rerun-reporter (0.0.2)
- spinach (~> 0.8)
spring (2.0.1)
activesupport (>= 4.2)
spring-commands-rspec (1.0.4)
spring (>= 0.9.1)
- spring-commands-spinach (1.1.0)
- spring (>= 0.9.1)
sprockets (3.7.1)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
@@ -998,7 +981,7 @@ DEPENDENCIES
asana (~> 0.6.0)
asciidoctor (~> 1.5.6)
asciidoctor-plantuml (= 0.0.8)
- asset_sync (~> 2.2.0)
+ asset_sync (~> 2.4)
attr_encrypted (~> 3.1.0)
awesome_print (~> 1.2.0)
babosa (~> 1.0.2)
@@ -1024,7 +1007,6 @@ DEPENDENCIES
concurrent-ruby (~> 1.0.5)
connection_pool (~> 2.0)
creole (~> 0.5.0)
- d3_rails (~> 3.5.0)
database_cleaner (~> 1.5.0)
deckar01-task_list (= 2.0.0)
default_value_for (~> 3.0.0)
@@ -1060,7 +1042,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 0.97.0)
+ gitaly-proto (~> 0.99.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)
@@ -1068,7 +1050,7 @@ DEPENDENCIES
gitlab-markup (~> 1.6.2)
gitlab-styles (~> 2.3)
gitlab_omniauth-ldap (~> 2.0.4)
- gon (~> 6.1.0)
+ gon (~> 6.2)
google-api-client (~> 0.19.8)
google-protobuf (= 3.5.1)
gpgme
@@ -1133,7 +1115,7 @@ DEPENDENCIES
peek-sidekiq (~> 1.0.3)
pg (~> 0.18.2)
premailer-rails (~> 1.9.7)
- prometheus-client-mmap (~> 0.9.1)
+ prometheus-client-mmap (~> 0.9.2)
pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1)
@@ -1181,17 +1163,14 @@ DEPENDENCIES
settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6)
shoulda-matchers (~> 3.1.2)
- sidekiq (~> 5.0)
+ sidekiq (~> 5.1)
sidekiq-cron (~> 0.6.0)
sidekiq-limit_fetch (~> 3.4)
simple_po_parser (~> 1.1.2)
simplecov (~> 0.14.0)
slack-notifier (~> 1.5.1)
- spinach-rails (~> 0.2.1)
- spinach-rerun-reporter (~> 0.0.2)
spring (~> 2.0.0)
spring-commands-rspec (~> 1.0.4)
- spring-commands-spinach (~> 1.1.0)
sprockets (~> 3.7.0)
sshkey (~> 1.9.0)
stackprof (~> 0.2.10)
diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock
index 3056b97ccd5..af7305619eb 100644
--- a/Gemfile.rails5.lock
+++ b/Gemfile.rails5.lock
@@ -4,43 +4,43 @@ GEM
RedCloth (4.3.2)
abstract_type (0.0.7)
ace-rails-ap (4.1.4)
- actioncable (5.0.6)
- actionpack (= 5.0.6)
+ actioncable (5.0.7)
+ actionpack (= 5.0.7)
nio4r (>= 1.2, < 3.0)
websocket-driver (~> 0.6.1)
- actionmailer (5.0.6)
- actionpack (= 5.0.6)
- actionview (= 5.0.6)
- activejob (= 5.0.6)
+ actionmailer (5.0.7)
+ actionpack (= 5.0.7)
+ actionview (= 5.0.7)
+ activejob (= 5.0.7)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
- actionpack (5.0.6)
- actionview (= 5.0.6)
- activesupport (= 5.0.6)
+ actionpack (5.0.7)
+ actionview (= 5.0.7)
+ activesupport (= 5.0.7)
rack (~> 2.0)
rack-test (~> 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
- actionview (5.0.6)
- activesupport (= 5.0.6)
+ actionview (5.0.7)
+ activesupport (= 5.0.7)
builder (~> 3.1)
erubis (~> 2.7.0)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3)
- activejob (5.0.6)
- activesupport (= 5.0.6)
+ activejob (5.0.7)
+ activesupport (= 5.0.7)
globalid (>= 0.3.6)
- activemodel (5.0.6)
- activesupport (= 5.0.6)
- activerecord (5.0.6)
- activemodel (= 5.0.6)
- activesupport (= 5.0.6)
+ activemodel (5.0.7)
+ activesupport (= 5.0.7)
+ activerecord (5.0.7)
+ activemodel (= 5.0.7)
+ activesupport (= 5.0.7)
arel (~> 7.0)
activerecord_sane_schema_dumper (1.0)
rails (>= 5, < 6)
- activesupport (5.0.6)
+ activesupport (5.0.7)
concurrent-ruby (~> 1.0, >= 1.0.2)
- i18n (~> 0.7)
+ i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
acts-as-taggable-on (5.0.0)
@@ -62,13 +62,13 @@ GEM
asciidoctor (1.5.6.1)
asciidoctor-plantuml (0.0.8)
asciidoctor (~> 1.5)
- asset_sync (2.2.0)
+ asset_sync (2.4.0)
activemodel (>= 4.1.0)
fog-core
mime-types (>= 2.99)
unf
ast (2.4.0)
- atomic (1.1.100)
+ atomic (1.1.99)
attr_encrypted (3.1.0)
encryptor (~> 3.0.0)
attr_required (1.0.1)
@@ -132,7 +132,6 @@ GEM
coderay (1.1.2)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
- colorize (0.8.1)
commonmarker (0.17.9)
ruby-enum (~> 0.5)
concord (0.1.5)
@@ -144,12 +143,10 @@ GEM
connection_pool (2.2.1)
crack (0.4.3)
safe_yaml (~> 1.0.0)
- crass (1.0.3)
+ crass (1.0.4)
creole (0.5.0)
css_parser (1.6.0)
addressable
- d3_rails (3.5.17)
- railties (>= 3.1.0)
daemons (1.2.6)
database_cleaner (1.5.3)
debug_inspector (0.0.3)
@@ -291,8 +288,7 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
- gherkin-ruby (0.3.2)
- gitaly-proto (0.97.0)
+ gitaly-proto (0.99.0)
google-protobuf (~> 3.1)
grpc (~> 1.10)
github-linguist (5.3.3)
@@ -335,9 +331,8 @@ GEM
activesupport (>= 4.2.0)
gollum-grit_adapter (1.0.1)
gitlab-grit (~> 2.7, >= 2.7.1)
- gon (6.1.0)
+ gon (6.2.0)
actionpack (>= 3.0)
- json
multi_json
request_store (>= 1.0)
google-api-client (0.19.8)
@@ -367,8 +362,8 @@ GEM
rack (>= 1.3.0)
rack-accept
virtus (>= 1.0.0)
- grape-entity (0.6.1)
- activesupport (>= 5.0.0)
+ grape-entity (0.7.1)
+ activesupport (>= 4.0)
multi_json (>= 1.3.2)
grape-route-helpers (2.1.0)
activesupport
@@ -420,7 +415,7 @@ GEM
json (~> 1.8)
multi_xml (>= 0.5.2)
httpclient (2.8.3)
- i18n (0.9.5)
+ i18n (1.0.1)
concurrent-ruby (~> 1.0)
ice_nine (0.11.2)
influxdb (0.5.3)
@@ -515,7 +510,7 @@ GEM
net-ldap (0.16.1)
net-ssh (4.2.0)
netrc (0.11.0)
- nio4r (2.2.0)
+ nio4r (2.3.1)
nokogiri (1.8.2)
mini_portile2 (~> 2.3.0)
numerizer (0.1.1)
@@ -545,9 +540,9 @@ GEM
omniauth (~> 1.2)
omniauth-facebook (4.0.0)
omniauth-oauth2 (~> 1.2)
- omniauth-github (1.1.2)
- omniauth (~> 1.0)
- omniauth-oauth2 (~> 1.1)
+ omniauth-github (1.3.0)
+ omniauth (~> 1.5)
+ omniauth-oauth2 (>= 1.4.0, < 2.0)
omniauth-gitlab (1.0.3)
omniauth (~> 1.0)
omniauth-oauth2 (~> 1.0)
@@ -633,7 +628,7 @@ GEM
parser
unparser
procto (0.0.3)
- prometheus-client-mmap (0.9.1)
+ prometheus-client-mmap (0.9.2)
pry (0.11.3)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
@@ -644,7 +639,7 @@ GEM
pry (>= 0.10.4)
public_suffix (3.0.2)
pyu-ruby-sasl (0.0.3.3)
- rack (2.0.4)
+ rack (2.0.5)
rack-accept (0.4.5)
rack (>= 0.4)
rack-attack (4.4.1)
@@ -662,17 +657,17 @@ GEM
rack
rack-test (0.6.3)
rack (>= 1.0)
- rails (5.0.6)
- actioncable (= 5.0.6)
- actionmailer (= 5.0.6)
- actionpack (= 5.0.6)
- actionview (= 5.0.6)
- activejob (= 5.0.6)
- activemodel (= 5.0.6)
- activerecord (= 5.0.6)
- activesupport (= 5.0.6)
+ rails (5.0.7)
+ actioncable (= 5.0.7)
+ actionmailer (= 5.0.7)
+ actionpack (= 5.0.7)
+ actionview (= 5.0.7)
+ activejob (= 5.0.7)
+ activemodel (= 5.0.7)
+ activerecord (= 5.0.7)
+ activesupport (= 5.0.7)
bundler (>= 1.3.0)
- railties (= 5.0.6)
+ railties (= 5.0.7)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.2)
actionpack (~> 5.x, >= 5.0.1)
@@ -683,21 +678,21 @@ GEM
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
- rails-html-sanitizer (1.0.3)
- loofah (~> 2.0)
+ rails-html-sanitizer (1.0.4)
+ loofah (~> 2.2, >= 2.2.2)
rails-i18n (5.1.1)
i18n (>= 0.7, < 2)
railties (>= 5.0, < 6)
- railties (5.0.6)
- actionpack (= 5.0.6)
- activesupport (= 5.0.6)
+ railties (5.0.7)
+ actionpack (= 5.0.7)
+ activesupport (= 5.0.7)
method_source
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
rainbow (2.2.2)
rake
raindrops (0.19.0)
- rake (12.3.0)
+ rake (12.3.1)
rb-fsevent (0.10.3)
rb-inotify (0.9.10)
ffi (>= 0.5.0, < 2)
@@ -874,22 +869,10 @@ GEM
simplecov-html (~> 0.10.0)
simplecov-html (0.10.2)
slack-notifier (1.5.1)
- spinach (0.8.10)
- colorize
- gherkin-ruby (>= 0.3.2)
- json
- spinach-rails (0.2.1)
- capybara (>= 2.0.0)
- railties (>= 3)
- spinach (>= 0.4)
- spinach-rerun-reporter (0.0.2)
- spinach (~> 0.8)
spring (2.0.2)
activesupport (>= 4.2)
spring-commands-rspec (1.0.4)
spring (>= 0.9.1)
- spring-commands-spinach (1.1.0)
- spring (>= 0.9.1)
sprockets (3.7.1)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
@@ -1001,7 +984,7 @@ DEPENDENCIES
asana (~> 0.6.0)
asciidoctor (~> 1.5.6)
asciidoctor-plantuml (= 0.0.8)
- asset_sync (~> 2.2.0)
+ asset_sync (~> 2.4)
attr_encrypted (~> 3.1.0)
awesome_print (~> 1.2.0)
babosa (~> 1.0.2)
@@ -1027,12 +1010,11 @@ DEPENDENCIES
concurrent-ruby (~> 1.0.5)
connection_pool (~> 2.0)
creole (~> 0.5.0)
- d3_rails (~> 3.5.0)
database_cleaner (~> 1.5.0)
deckar01-task_list (= 2.0.0)
default_value_for (~> 3.0.5)
device_detector
- devise (~> 4.2)
+ devise (~> 4.4)
devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0)
doorkeeper (~> 4.3)
@@ -1063,7 +1045,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 0.97.0)
+ gitaly-proto (~> 0.99.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)
@@ -1071,12 +1053,12 @@ DEPENDENCIES
gitlab-markup (~> 1.6.2)
gitlab-styles (~> 2.3)
gitlab_omniauth-ldap (~> 2.0.4)
- gon (~> 6.1.0)
+ gon (~> 6.2)
google-api-client (~> 0.19.8)
google-protobuf (= 3.5.1)
gpgme
grape (~> 1.0)
- grape-entity (~> 0.6.0)
+ grape-entity (~> 0.7.1)
grape-route-helpers (~> 2.1.0)
grape_logging (~> 1.7)
grpc (~> 1.11.0)
@@ -1117,7 +1099,7 @@ DEPENDENCIES
omniauth-azure-oauth2 (~> 0.0.9)
omniauth-cas3 (~> 1.1.4)
omniauth-facebook (~> 4.0.0)
- omniauth-github (~> 1.1.1)
+ omniauth-github (~> 1.3)
omniauth-gitlab (~> 1.0.2)
omniauth-google-oauth2 (~> 0.5.3)
omniauth-kerberos (~> 0.3.0)
@@ -1136,14 +1118,14 @@ DEPENDENCIES
peek-sidekiq (~> 1.0.3)
pg (~> 0.18.2)
premailer-rails (~> 1.9.7)
- prometheus-client-mmap (~> 0.9.1)
+ prometheus-client-mmap (~> 0.9.2)
pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1)
rack-cors (~> 1.0.0)
rack-oauth2 (~> 1.2.1)
rack-proxy (~> 0.6.0)
- rails (= 5.0.6)
+ rails (= 5.0.7)
rails-controller-testing
rails-deprecated_sanitizer (~> 1.0.3)
rails-i18n (~> 5.1)
@@ -1191,11 +1173,8 @@ DEPENDENCIES
simple_po_parser (~> 1.1.2)
simplecov (~> 0.14.0)
slack-notifier (~> 1.5.1)
- spinach-rails (~> 0.2.1)
- spinach-rerun-reporter (~> 0.0.2)
spring (~> 2.0.0)
spring-commands-rspec (~> 1.0.4)
- spring-commands-spinach (~> 1.1.0)
sprockets (~> 3.7.0)
sshkey (~> 1.9.0)
stackprof (~> 0.2.10)
diff --git a/app/assets/javascripts/clusters/clusters_index.js b/app/assets/javascripts/clusters/clusters_index.js
index 2e3ad244375..1e5c733d151 100644
--- a/app/assets/javascripts/clusters/clusters_index.js
+++ b/app/assets/javascripts/clusters/clusters_index.js
@@ -1,20 +1,24 @@
-import Flash from '../flash';
-import { s__ } from '../locale';
-import setupToggleButtons from '../toggle_buttons';
+import createFlash from '~/flash';
+import { __ } from '~/locale';
+import setupToggleButtons from '~/toggle_buttons';
+import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
+
import ClustersService from './services/clusters_service';
export default () => {
const clusterList = document.querySelector('.js-clusters-list');
+
+ gcpSignupOffer();
+
// The empty state won't have a clusterList
if (clusterList) {
- setupToggleButtons(
- document.querySelector('.js-clusters-list'),
- (value, toggle) =>
- ClustersService.updateCluster(toggle.dataset.endpoint, { cluster: { enabled: value } })
- .catch((err) => {
- Flash(s__('ClusterIntegration|Something went wrong on our end.'));
- throw err;
- }),
+ setupToggleButtons(document.querySelector('.js-clusters-list'), (value, toggle) =>
+ ClustersService.updateCluster(toggle.dataset.endpoint, { cluster: { enabled: value } }).catch(
+ err => {
+ createFlash(__('Something went wrong on our end.'));
+ throw err;
+ },
+ ),
);
}
};
diff --git a/app/assets/javascripts/clusters/components/gcp_signup_offer.js b/app/assets/javascripts/clusters/components/gcp_signup_offer.js
new file mode 100644
index 00000000000..8bc20a1c09f
--- /dev/null
+++ b/app/assets/javascripts/clusters/components/gcp_signup_offer.js
@@ -0,0 +1,27 @@
+import $ from 'jquery';
+import axios from '~/lib/utils/axios_utils';
+import { __ } from '~/locale';
+import Flash from '~/flash';
+
+export default function gcpSignupOffer() {
+ const alertEl = document.querySelector('.gcp-signup-offer');
+ if (!alertEl) {
+ return;
+ }
+
+ const closeButtonEl = alertEl.getElementsByClassName('close')[0];
+ const { dismissEndpoint, featureId } = closeButtonEl.dataset;
+
+ closeButtonEl.addEventListener('click', () => {
+ axios
+ .post(dismissEndpoint, {
+ feature_name: featureId,
+ })
+ .then(() => {
+ $(alertEl).alert('close');
+ })
+ .catch(() => {
+ Flash(__('An error occurred while dismissing the alert. Refresh the page and try again.'));
+ });
+ });
+}
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
index 87f8854f940..1c43fc3cdc7 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
@@ -82,7 +82,6 @@ export default () => {
this.service
.fetchCycleAnalyticsData(fetchOptions)
- .then(resp => resp.json())
.then((response) => {
this.store.setCycleAnalyticsData(response);
this.selectDefaultStage();
@@ -116,7 +115,6 @@ export default () => {
stage,
startDate: this.startDate,
})
- .then(resp => resp.json())
.then((response) => {
this.isEmptyStage = !response.events.length;
this.store.setStageEvents(response.events, stage);
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js
index f496c38208d..4cf416c50e5 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js
@@ -1,16 +1,20 @@
-import Vue from 'vue';
-import VueResource from 'vue-resource';
-
-Vue.use(VueResource);
+import axios from '~/lib/utils/axios_utils';
export default class CycleAnalyticsService {
constructor(options) {
- this.requestPath = options.requestPath;
- this.cycleAnalytics = Vue.resource(this.requestPath);
+ this.axios = axios.create({
+ baseURL: options.requestPath,
+ });
}
fetchCycleAnalyticsData(options = { startDate: 30 }) {
- return this.cycleAnalytics.get({ cycle_analytics: { start_date: options.startDate } });
+ return this.axios
+ .get('', {
+ params: {
+ 'cycle_analytics[start_date]': options.startDate,
+ },
+ })
+ .then(x => x.data);
}
fetchStageData(options) {
@@ -19,12 +23,12 @@ export default class CycleAnalyticsService {
startDate,
} = options;
- return Vue.http.get(`${this.requestPath}/events/${stage.name}.json`, {
- params: {
- cycle_analytics: {
- start_date: startDate,
+ return this.axios
+ .get(`events/${stage.name}.json`, {
+ params: {
+ 'cycle_analytics[start_date]': startDate,
},
- },
- });
+ })
+ .then(x => x.data);
}
}
diff --git a/app/assets/javascripts/deploy_keys/components/action_btn.vue b/app/assets/javascripts/deploy_keys/components/action_btn.vue
index b839b9f286f..67dda0e29cb 100644
--- a/app/assets/javascripts/deploy_keys/components/action_btn.vue
+++ b/app/assets/javascripts/deploy_keys/components/action_btn.vue
@@ -1,55 +1,50 @@
<script>
- import eventHub from '../eventhub';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import loadingIcon from '~/vue_shared/components/loading_icon.vue';
+import eventHub from '../eventhub';
- export default {
- components: {
- loadingIcon,
+export default {
+ components: {
+ loadingIcon,
+ },
+ props: {
+ deployKey: {
+ type: Object,
+ required: true,
},
- props: {
- deployKey: {
- type: Object,
- required: true,
- },
- type: {
- type: String,
- required: true,
- },
- btnCssClass: {
- type: String,
- required: false,
- default: 'btn-default',
- },
+ type: {
+ type: String,
+ required: true,
},
- data() {
- return {
- isLoading: false,
- };
+ btnCssClass: {
+ type: String,
+ required: false,
+ default: 'btn-default',
},
- computed: {
- text() {
- return `${this.type.charAt(0).toUpperCase()}${this.type.slice(1)}`;
- },
- },
- methods: {
- doAction() {
- this.isLoading = true;
+ },
+ data() {
+ return {
+ isLoading: false,
+ };
+ },
+ methods: {
+ doAction() {
+ this.isLoading = true;
- eventHub.$emit(`${this.type}.key`, this.deployKey, () => {
- this.isLoading = false;
- });
- },
+ eventHub.$emit(`${this.type}.key`, this.deployKey, () => {
+ this.isLoading = false;
+ });
},
- };
+ },
+};
</script>
<template>
<button
- class="btn btn-sm prepend-left-10"
+ class="btn"
:class="[{ disabled: isLoading }, btnCssClass]"
:disabled="isLoading"
@click="doAction">
- {{ text }}
+ <slot></slot>
<loading-icon
v-if="isLoading"
:inline="true"
diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue
index 5a782237b7d..c41fe55db63 100644
--- a/app/assets/javascripts/deploy_keys/components/app.vue
+++ b/app/assets/javascripts/deploy_keys/components/app.vue
@@ -1,80 +1,115 @@
<script>
- import Flash from '../../flash';
- import eventHub from '../eventhub';
- import DeployKeysService from '../service';
- import DeployKeysStore from '../store';
- import keysPanel from './keys_panel.vue';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import { s__ } from '~/locale';
+import Flash from '~/flash';
+import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
+import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
+import eventHub from '../eventhub';
+import DeployKeysService from '../service';
+import DeployKeysStore from '../store';
+import KeysPanel from './keys_panel.vue';
- export default {
- components: {
- keysPanel,
- loadingIcon,
+export default {
+ components: {
+ KeysPanel,
+ LoadingIcon,
+ NavigationTabs,
+ },
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
},
- props: {
- endpoint: {
- type: String,
- required: true,
- },
+ projectId: {
+ type: String,
+ required: true,
},
- data() {
- return {
- isLoading: false,
- store: new DeployKeysStore(),
- };
+ },
+ data() {
+ return {
+ currentTab: 'enabled_keys',
+ isLoading: false,
+ store: new DeployKeysStore(),
+ };
+ },
+ scopes: {
+ enabled_keys: s__('DeployKeys|Enabled deploy keys'),
+ available_project_keys: s__('DeployKeys|Privately accessible deploy keys'),
+ public_keys: s__('DeployKeys|Publicly accessible deploy keys'),
+ },
+ computed: {
+ tabs() {
+ return Object.keys(this.$options.scopes).map(scope => {
+ const count = Array.isArray(this.keys[scope]) ? this.keys[scope].length : null;
+
+ return {
+ name: this.$options.scopes[scope],
+ scope,
+ isActive: scope === this.currentTab,
+ count,
+ };
+ });
+ },
+ hasKeys() {
+ return Object.keys(this.keys).length;
},
- computed: {
- hasKeys() {
- return Object.keys(this.keys).length;
- },
- keys() {
- return this.store.keys;
- },
+ keys() {
+ return this.store.keys;
},
- created() {
- this.service = new DeployKeysService(this.endpoint);
+ },
+ created() {
+ this.service = new DeployKeysService(this.endpoint);
- eventHub.$on('enable.key', this.enableKey);
- eventHub.$on('remove.key', this.disableKey);
- eventHub.$on('disable.key', this.disableKey);
+ eventHub.$on('enable.key', this.enableKey);
+ eventHub.$on('remove.key', this.disableKey);
+ eventHub.$on('disable.key', this.disableKey);
+ },
+ mounted() {
+ this.fetchKeys();
+ },
+ beforeDestroy() {
+ eventHub.$off('enable.key', this.enableKey);
+ eventHub.$off('remove.key', this.disableKey);
+ eventHub.$off('disable.key', this.disableKey);
+ },
+ methods: {
+ onChangeTab(tab) {
+ this.currentTab = tab;
},
- mounted() {
- this.fetchKeys();
+ fetchKeys() {
+ this.isLoading = true;
+
+ return this.service
+ .getKeys()
+ .then(data => {
+ this.isLoading = false;
+ this.store.keys = data;
+ })
+ .catch(() => {
+ this.isLoading = false;
+ this.store.keys = {};
+ return new Flash(s__('DeployKeys|Error getting deploy keys'));
+ });
},
- beforeDestroy() {
- eventHub.$off('enable.key', this.enableKey);
- eventHub.$off('remove.key', this.disableKey);
- eventHub.$off('disable.key', this.disableKey);
+ enableKey(deployKey) {
+ this.service
+ .enableKey(deployKey.id)
+ .then(this.fetchKeys)
+ .catch(() => new Flash(s__('DeployKeys|Error enabling deploy key')));
},
- methods: {
- fetchKeys() {
- this.isLoading = true;
-
- this.service.getKeys()
- .then((data) => {
- this.isLoading = false;
- this.store.keys = data;
- })
- .catch(() => new Flash('Error getting deploy keys'));
- },
- enableKey(deployKey) {
- this.service.enableKey(deployKey.id)
- .then(() => this.fetchKeys())
- .catch(() => new Flash('Error enabling deploy key'));
- },
- disableKey(deployKey, callback) {
- // eslint-disable-next-line no-alert
- if (confirm('You are going to remove this deploy key. Are you sure?')) {
- this.service.disableKey(deployKey.id)
- .then(() => this.fetchKeys())
- .then(callback)
- .catch(() => new Flash('Error removing deploy key'));
- } else {
- callback();
- }
- },
+ disableKey(deployKey, callback) {
+ // eslint-disable-next-line no-alert
+ if (confirm(s__('DeployKeys|You are going to remove this deploy key. Are you sure?'))) {
+ this.service
+ .disableKey(deployKey.id)
+ .then(this.fetchKeys)
+ .then(callback)
+ .catch(() => new Flash(s__('DeployKeys|Error removing deploy key')));
+ } else {
+ callback();
+ }
},
- };
+ },
+};
</script>
<template>
@@ -82,29 +117,38 @@
<loading-icon
v-if="isLoading && !hasKeys"
size="2"
- label="Loading deploy keys"
+ :label="s__('DeployKeys|Loading deploy keys')"
/>
- <div v-else-if="hasKeys">
+ <template v-else-if="hasKeys">
+ <div class="top-area scrolling-tabs-container inner-page-scroll-tabs">
+ <div class="fade-left">
+ <i
+ class="fa fa-angle-left"
+ aria-hidden="true"
+ >
+ </i>
+ </div>
+ <div class="fade-right">
+ <i
+ class="fa fa-angle-right"
+ aria-hidden="true"
+ >
+ </i>
+ </div>
+
+ <navigation-tabs
+ :tabs="tabs"
+ @onChangeTab="onChangeTab"
+ scope="deployKeys"
+ />
+ </div>
<keys-panel
- title="Enabled deploy keys for this project"
class="qa-project-deploy-keys"
- :keys="keys.enabled_keys"
- :store="store"
- :endpoint="endpoint"
- />
- <keys-panel
- title="Deploy keys from projects you have access to"
- :keys="keys.available_project_keys"
- :store="store"
- :endpoint="endpoint"
- />
- <keys-panel
- v-if="keys.public_keys.length"
- title="Public deploy keys available to any project"
- :keys="keys.public_keys"
+ :project-id="projectId"
+ :keys="keys[currentTab]"
:store="store"
:endpoint="endpoint"
/>
- </div>
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue
index c6091efd62f..6c2af7fa768 100644
--- a/app/assets/javascripts/deploy_keys/components/key.vue
+++ b/app/assets/javascripts/deploy_keys/components/key.vue
@@ -1,111 +1,235 @@
<script>
- import actionBtn from './action_btn.vue';
- import { getTimeago } from '../../lib/utils/datetime_utility';
- import tooltip from '../../vue_shared/directives/tooltip';
+import _ from 'underscore';
+import { s__, sprintf } from '~/locale';
+import icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
- export default {
- components: {
- actionBtn,
- },
- directives: {
- tooltip,
- },
- props: {
- deployKey: {
- type: Object,
- required: true,
- },
- store: {
- type: Object,
- required: true,
- },
- endpoint: {
- type: String,
- required: true,
- },
- },
- computed: {
- timeagoDate() {
- return getTimeago().format(this.deployKey.created_at);
- },
- editDeployKeyPath() {
- return `${this.endpoint}/${this.deployKey.id}/edit`;
- },
- },
- methods: {
- isEnabled(id) {
- return this.store.findEnabledKey(id) !== undefined;
- },
- tooltipTitle(project) {
- return project.can_push ? 'Write access allowed' : 'Read access only';
- },
- },
- };
+import actionBtn from './action_btn.vue';
+
+export default {
+ components: {
+ actionBtn,
+ icon,
+ },
+ directives: {
+ tooltip,
+ },
+ mixins: [timeagoMixin],
+ props: {
+ deployKey: {
+ type: Object,
+ required: true,
+ },
+ store: {
+ type: Object,
+ required: true,
+ },
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ projectId: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ projectsExpanded: false,
+ };
+ },
+ computed: {
+ editDeployKeyPath() {
+ return `${this.endpoint}/${this.deployKey.id}/edit`;
+ },
+ projects() {
+ const projects = [...this.deployKey.deploy_keys_projects];
+
+ if (this.projectId !== null) {
+ const indexOfCurrentProject = _.findIndex(
+ projects,
+ project =>
+ project &&
+ project.project &&
+ project.project.id &&
+ project.project.id.toString() === this.projectId,
+ );
+
+ if (indexOfCurrentProject > -1) {
+ const currentProject = projects.splice(indexOfCurrentProject, 1);
+ currentProject[0].project.full_name = s__('DeployKeys|Current project');
+ return currentProject.concat(projects);
+ }
+ }
+ return projects;
+ },
+ firstProject() {
+ return _.head(this.projects);
+ },
+ restProjects() {
+ return _.tail(this.projects);
+ },
+ restProjectsTooltip() {
+ return sprintf(s__('DeployKeys|Expand %{count} other projects'), {
+ count: this.restProjects.length,
+ });
+ },
+ restProjectsLabel() {
+ return sprintf(s__('DeployKeys|+%{count} others'), { count: this.restProjects.length });
+ },
+ isEnabled() {
+ return this.store.isEnabled(this.deployKey.id);
+ },
+ isRemovable() {
+ return (
+ this.store.isEnabled(this.deployKey.id) &&
+ this.deployKey.destroyed_when_orphaned &&
+ this.deployKey.almost_orphaned
+ );
+ },
+ isExpandable() {
+ return !this.projectsExpanded && this.restProjects.length > 1;
+ },
+ isExpanded() {
+ return this.projectsExpanded || this.restProjects.length === 1;
+ },
+ },
+ methods: {
+ projectTooltipTitle(project) {
+ return project.can_push
+ ? s__('DeployKeys|Write access allowed')
+ : s__('DeployKeys|Read access only');
+ },
+ toggleExpanded() {
+ this.projectsExpanded = !this.projectsExpanded;
+ },
+ },
+};
</script>
<template>
- <div>
- <div class="pull-left append-right-10 hidden-xs">
- <i
- aria-hidden="true"
- class="fa fa-key key-icon"
- >
- </i>
+ <div class="gl-responsive-table-row deploy-key">
+ <div class="table-section section-40">
+ <div
+ role="rowheader"
+ class="table-mobile-header">
+ {{ s__('DeployKeys|Deploy key') }}
+ </div>
+ <div class="table-mobile-content">
+ <strong class="title qa-key-title">
+ {{ deployKey.title }}
+ </strong>
+ <div class="fingerprint qa-key-fingerprint">
+ {{ deployKey.fingerprint }}
+ </div>
+ </div>
</div>
- <div class="deploy-key-content key-list-item-info">
- <strong class="title qa-key-title">
- {{ deployKey.title }}
- </strong>
- <div class="description qa-key-fingerprint">
- {{ deployKey.fingerprint }}
+ <div class="table-section section-30 section-wrap">
+ <div
+ role="rowheader"
+ class="table-mobile-header">
+ {{ s__('DeployKeys|Project usage') }}
+ </div>
+ <div class="table-mobile-content deploy-project-list">
+ <template v-if="projects.length > 0">
+ <a
+ class="label deploy-project-label"
+ :title="projectTooltipTitle(firstProject)"
+ v-tooltip
+ >
+ <span>
+ {{ firstProject.project.full_name }}
+ </span>
+ <icon :name="firstProject.can_push ? 'lock-open' : 'lock'"/>
+ </a>
+ <a
+ v-if="isExpandable"
+ class="label deploy-project-label"
+ @click="toggleExpanded"
+ :title="restProjectsTooltip"
+ v-tooltip
+ >
+ <span>{{ restProjectsLabel }}</span>
+ </a>
+ <a
+ v-else-if="isExpanded"
+ v-for="deployKeysProject in restProjects"
+ :key="deployKeysProject.project.full_path"
+ class="label deploy-project-label"
+ :href="deployKeysProject.project.full_path"
+ :title="projectTooltipTitle(deployKeysProject)"
+ v-tooltip
+ >
+ <span>
+ {{ deployKeysProject.project.full_name }}
+ </span>
+ <icon :name="deployKeysProject.can_push ? 'lock-open' : 'lock'"/>
+ </a>
+ </template>
+ <span
+ v-else
+ class="text-secondary">{{ __('None') }}</span>
</div>
</div>
- <div class="deploy-key-content prepend-left-default deploy-key-projects">
- <a
- v-for="(deployKeysProject, i) in deployKey.deploy_keys_projects"
- :key="i"
- class="label deploy-project-label"
- :href="deployKeysProject.project.full_path"
- :title="tooltipTitle(deployKeysProject)"
- v-tooltip
- >
- {{ deployKeysProject.project.full_name }}
- <i
- v-if="!deployKeysProject.can_push"
- aria-hidden="true"
- class="fa fa-lock"
- >
- </i>
- </a>
+ <div class="table-section section-15 text-right">
+ <div
+ role="rowheader"
+ class="table-mobile-header">
+ {{ __('Created') }}
+ </div>
+ <div class="table-mobile-content text-secondary key-created-at">
+ <span
+ :title="tooltipTitle(deployKey.created_at)"
+ v-tooltip>
+ <icon name="calendar"/>
+ <span>{{ timeFormated(deployKey.created_at) }}</span>
+ </span>
+ </div>
</div>
- <div class="deploy-key-content">
- <span class="key-created-at">
- created {{ timeagoDate }}
- </span>
- <a
- v-if="deployKey.can_edit"
- class="btn btn-sm"
- :href="editDeployKeyPath"
- >
- Edit
- </a>
- <action-btn
- v-if="!isEnabled(deployKey.id)"
- :deploy-key="deployKey"
- type="enable"
- />
- <action-btn
- v-else-if="deployKey.destroyed_when_orphaned && deployKey.almost_orphaned"
- :deploy-key="deployKey"
- btn-css-class="btn-warning"
- type="remove"
- />
- <action-btn
- v-else
- :deploy-key="deployKey"
- btn-css-class="btn-warning"
- type="disable"
- />
+ <div class="table-section section-15 table-button-footer deploy-key-actions">
+ <div class="btn-group table-action-buttons">
+ <action-btn
+ v-if="!isEnabled"
+ :deploy-key="deployKey"
+ type="enable"
+ >
+ {{ __('Enable') }}
+ </action-btn>
+ <a
+ v-if="deployKey.can_edit"
+ class="btn btn-default text-secondary"
+ :href="editDeployKeyPath"
+ :title="__('Edit')"
+ data-container="body"
+ v-tooltip
+ >
+ <icon name="pencil"/>
+ </a>
+ <action-btn
+ v-if="isRemovable"
+ :deploy-key="deployKey"
+ btn-css-class="btn-danger"
+ type="remove"
+ :title="__('Remove')"
+ data-container="body"
+ v-tooltip
+ >
+ <icon name="remove"/>
+ </action-btn>
+ <action-btn
+ v-else-if="isEnabled"
+ :deploy-key="deployKey"
+ btn-css-class="btn-warning"
+ type="disable"
+ :title="__('Disable')"
+ data-container="body"
+ v-tooltip
+ >
+ <icon name="cancel"/>
+ </action-btn>
+ </div>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/deploy_keys/components/keys_panel.vue b/app/assets/javascripts/deploy_keys/components/keys_panel.vue
index 822b0323156..3b146c7389a 100644
--- a/app/assets/javascripts/deploy_keys/components/keys_panel.vue
+++ b/app/assets/javascripts/deploy_keys/components/keys_panel.vue
@@ -1,62 +1,68 @@
<script>
- import key from './key.vue';
+import deployKey from './key.vue';
- export default {
- components: {
- key,
+export default {
+ components: {
+ deployKey,
+ },
+ props: {
+ keys: {
+ type: Array,
+ required: true,
},
- props: {
- title: {
- type: String,
- required: true,
- },
- keys: {
- type: Array,
- required: true,
- },
- showHelpBox: {
- type: Boolean,
- required: false,
- default: true,
- },
- store: {
- type: Object,
- required: true,
- },
- endpoint: {
- type: String,
- required: true,
- },
+ store: {
+ type: Object,
+ required: true,
},
- };
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ projectId: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+};
</script>
<template>
- <div class="deploy-keys-panel">
- <h5>
- {{ title }}
- ({{ keys.length }})
- </h5>
- <ul
- class="well-list"
- v-if="keys.length"
- >
- <li
+ <div class="deploy-keys-panel table-holder">
+ <template v-if="keys.length > 0">
+ <div
+ role="row"
+ class="gl-responsive-table-row table-row-header">
+ <div
+ role="rowheader"
+ class="table-section section-40">
+ {{ s__('DeployKeys|Deploy key') }}
+ </div>
+ <div
+ role="rowheader"
+ class="table-section section-30">
+ {{ s__('DeployKeys|Project usage') }}
+ </div>
+ <div
+ role="rowheader"
+ class="table-section section-15 text-right">
+ {{ __('Created') }}
+ </div>
+ </div>
+ <deploy-key
v-for="deployKey in keys"
:key="deployKey.id"
- >
- <key
- :deploy-key="deployKey"
- :store="store"
- :endpoint="endpoint"
- />
- </li>
- </ul>
+ :deploy-key="deployKey"
+ :store="store"
+ :endpoint="endpoint"
+ :project-id="projectId"
+ />
+ </template>
<div
class="settings-message text-center"
- v-else-if="showHelpBox"
+ v-else
>
- No deploy keys found. Create one with the form above.
+ {{ s__('DeployKeys|No deploy keys found. Create one with the form above.') }}
</div>
</div>
</template>
diff --git a/app/assets/javascripts/deploy_keys/index.js b/app/assets/javascripts/deploy_keys/index.js
index b727261648c..6e439be42ae 100644
--- a/app/assets/javascripts/deploy_keys/index.js
+++ b/app/assets/javascripts/deploy_keys/index.js
@@ -1,21 +1,24 @@
import Vue from 'vue';
import deployKeysApp from './components/app.vue';
-export default () => new Vue({
- el: document.getElementById('js-deploy-keys'),
- components: {
- deployKeysApp,
- },
- data() {
- return {
- endpoint: this.$options.el.dataset.endpoint,
- };
- },
- render(createElement) {
- return createElement('deploy-keys-app', {
- props: {
- endpoint: this.endpoint,
- },
- });
- },
-});
+export default () =>
+ new Vue({
+ el: document.getElementById('js-deploy-keys'),
+ components: {
+ deployKeysApp,
+ },
+ data() {
+ return {
+ endpoint: this.$options.el.dataset.endpoint,
+ projectId: this.$options.el.dataset.projectId,
+ };
+ },
+ render(createElement) {
+ return createElement('deploy-keys-app', {
+ props: {
+ endpoint: this.endpoint,
+ projectId: this.projectId,
+ },
+ });
+ },
+ });
diff --git a/app/assets/javascripts/deploy_keys/service/index.js b/app/assets/javascripts/deploy_keys/service/index.js
index fe6dbaa9498..9dc3b21f6f6 100644
--- a/app/assets/javascripts/deploy_keys/service/index.js
+++ b/app/assets/javascripts/deploy_keys/service/index.js
@@ -1,34 +1,24 @@
-import Vue from 'vue';
-import VueResource from 'vue-resource';
-
-Vue.use(VueResource);
+import axios from '~/lib/utils/axios_utils';
export default class DeployKeysService {
constructor(endpoint) {
- this.endpoint = endpoint;
-
- this.resource = Vue.resource(`${this.endpoint}{/id}`, {}, {
- enable: {
- method: 'PUT',
- url: `${this.endpoint}{/id}/enable`,
- },
- disable: {
- method: 'PUT',
- url: `${this.endpoint}{/id}/disable`,
- },
+ this.axios = axios.create({
+ baseURL: endpoint,
});
}
getKeys() {
- return this.resource.get()
- .then(response => response.json());
+ return this.axios.get()
+ .then(response => response.data);
}
enableKey(id) {
- return this.resource.enable({ id }, {});
+ return this.axios.put(`${id}/enable`)
+ .then(response => response.data);
}
disableKey(id) {
- return this.resource.disable({ id }, {});
+ return this.axios.put(`${id}/disable`)
+ .then(response => response.data);
}
}
diff --git a/app/assets/javascripts/deploy_keys/store/index.js b/app/assets/javascripts/deploy_keys/store/index.js
index 6210361af26..a350bc99a70 100644
--- a/app/assets/javascripts/deploy_keys/store/index.js
+++ b/app/assets/javascripts/deploy_keys/store/index.js
@@ -3,7 +3,7 @@ export default class DeployKeysStore {
this.keys = {};
}
- findEnabledKey(id) {
- return this.keys.enabled_keys.find(key => key.id === id);
+ isEnabled(id) {
+ return this.keys.enabled_keys.some(key => key.id === id);
}
}
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 7e9770a9ea2..9de57db48fd 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -408,7 +408,10 @@ class GfmAutoComplete {
fetchData($input, at) {
if (this.isLoadingData[at]) return;
+
this.isLoadingData[at] = true;
+ const dataSource = this.dataSources[GfmAutoComplete.atTypeMap[at]];
+
if (this.cachedData[at]) {
this.loadData($input, at, this.cachedData[at]);
} else if (GfmAutoComplete.atTypeMap[at] === 'emojis') {
@@ -418,12 +421,14 @@ class GfmAutoComplete {
GfmAutoComplete.glEmojiTag = glEmojiTag;
})
.catch(() => { this.isLoadingData[at] = false; });
- } else {
- AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true)
+ } else if (dataSource) {
+ AjaxCache.retrieve(dataSource, true)
.then((data) => {
this.loadData($input, at, data);
})
.catch(() => { this.isLoadingData[at] = false; });
+ } else {
+ this.isLoadingData[at] = false;
}
}
diff --git a/app/assets/javascripts/gpg_badges.js b/app/assets/javascripts/gpg_badges.js
index 502e3569321..029fd6a67d4 100644
--- a/app/assets/javascripts/gpg_badges.js
+++ b/app/assets/javascripts/gpg_badges.js
@@ -7,12 +7,12 @@ import { __ } from '~/locale';
export default class GpgBadges {
static fetch() {
const badges = $('.js-loading-gpg-badge');
- const form = $('.commits-search-form');
+ const tag = $('.js-signature-container');
badges.html('<i class="fa fa-spinner fa-spin"></i>');
- const params = parseQueryStringIntoObject(form.serialize());
- return axios.get(form.data('signaturesPath'), { params })
+ const params = parseQueryStringIntoObject(tag.serialize());
+ return axios.get(tag.data('signaturesPath'), { params })
.then(({ data }) => {
data.signatures.forEach((signature) => {
badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html);
diff --git a/app/assets/javascripts/ide/components/activity_bar.vue b/app/assets/javascripts/ide/components/activity_bar.vue
new file mode 100644
index 00000000000..05dbc1410de
--- /dev/null
+++ b/app/assets/javascripts/ide/components/activity_bar.vue
@@ -0,0 +1,106 @@
+<script>
+import { mapActions, mapGetters, mapState } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import { activityBarViews } from '../constants';
+
+export default {
+ components: {
+ Icon,
+ },
+ directives: {
+ tooltip,
+ },
+ computed: {
+ ...mapGetters(['currentProject', 'hasChanges']),
+ ...mapState(['currentActivityView']),
+ goBackUrl() {
+ return document.referrer || this.currentProject.web_url;
+ },
+ },
+ methods: {
+ ...mapActions(['updateActivityBarView']),
+ },
+ activityBarViews,
+};
+</script>
+
+<template>
+ <nav class="ide-activity-bar">
+ <ul class="list-unstyled">
+ <li v-once>
+ <a
+ v-tooltip
+ data-container="body"
+ data-placement="right"
+ :href="goBackUrl"
+ class="ide-sidebar-link"
+ :title="s__('IDE|Go back')"
+ :aria-label="s__('IDE|Go back')"
+ >
+ <icon
+ :size="16"
+ name="go-back"
+ />
+ </a>
+ </li>
+ <li>
+ <button
+ v-tooltip
+ data-container="body"
+ data-placement="right"
+ type="button"
+ class="ide-sidebar-link js-ide-edit-mode"
+ :class="{
+ active: currentActivityView === $options.activityBarViews.edit
+ }"
+ @click.prevent="updateActivityBarView($options.activityBarViews.edit)"
+ :title="s__('IDE|Edit')"
+ :aria-label="s__('IDE|Edit')"
+ >
+ <icon
+ name="code"
+ />
+ </button>
+ </li>
+ <li>
+ <button
+ v-tooltip
+ data-container="body"
+ data-placement="right"
+ type="button"
+ class="ide-sidebar-link js-ide-review-mode"
+ :class="{
+ active: currentActivityView === $options.activityBarViews.review
+ }"
+ @click.prevent="updateActivityBarView($options.activityBarViews.review)"
+ :title="s__('IDE|Review')"
+ :aria-label="s__('IDE|Review')"
+ >
+ <icon
+ name="file-modified"
+ />
+ </button>
+ </li>
+ <li v-show="hasChanges">
+ <button
+ v-tooltip
+ data-container="body"
+ data-placement="right"
+ type="button"
+ class="ide-sidebar-link js-ide-commit-mode"
+ :class="{
+ active: currentActivityView === $options.activityBarViews.commit
+ }"
+ @click.prevent="updateActivityBarView($options.activityBarViews.commit)"
+ :title="s__('IDE|Commit')"
+ :aria-label="s__('IDE|Commit')"
+ >
+ <icon
+ name="commit"
+ />
+ </button>
+ </li>
+ </ul>
+ </nav>
+</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
index 45321df191c..b4f3778d946 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
@@ -1,5 +1,5 @@
<script>
-import { mapState } from 'vuex';
+import { mapActions, mapState, mapGetters } from 'vuex';
import { sprintf, __ } from '~/locale';
import * as consts from '../../stores/modules/commit/constants';
import RadioGroup from './radio_group.vue';
@@ -9,7 +9,8 @@ export default {
RadioGroup,
},
computed: {
- ...mapState(['currentBranchId']),
+ ...mapState(['currentBranchId', 'changedFiles', 'stagedFiles']),
+ ...mapGetters(['currentProject']),
commitToCurrentBranchText() {
return sprintf(
__('Commit to %{branchName} branch'),
@@ -17,6 +18,17 @@ export default {
false,
);
},
+ disableMergeRequestRadio() {
+ return this.changedFiles.length > 0 && this.stagedFiles.length > 0;
+ },
+ },
+ mounted() {
+ if (this.disableMergeRequestRadio) {
+ this.updateCommitAction(consts.COMMIT_TO_CURRENT_BRANCH);
+ }
+ },
+ methods: {
+ ...mapActions('commit', ['updateCommitAction']),
},
commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
@@ -41,9 +53,11 @@ export default {
:show-input="true"
/>
<radio-group
+ v-if="currentProject.merge_requests_enabled"
:value="$options.commitToNewBranchMR"
:label="__('Create a new branch and merge request')"
:show-input="true"
+ :disabled="disableMergeRequestRadio"
/>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue b/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue
index 1f6bbca13b5..d0a60d647e5 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue
@@ -1,27 +1,9 @@
<script>
-import { mapActions, mapState, mapGetters } from 'vuex';
-import Icon from '~/vue_shared/components/icon.vue';
-import tooltip from '~/vue_shared/directives/tooltip';
+import { mapState } from 'vuex';
export default {
- components: {
- Icon,
- },
- directives: {
- tooltip,
- },
- props: {
- noChangesStateSvgPath: {
- type: String,
- required: true,
- },
- },
computed: {
- ...mapState(['lastCommitMsg', 'rightPanelCollapsed', 'changedFiles', 'stagedFiles']),
- ...mapGetters(['collapseButtonIcon', 'collapseButtonTooltip']),
- },
- methods: {
- ...mapActions(['toggleRightPanelCollapsed']),
+ ...mapState(['lastCommitMsg', 'noChangesStateSvgPath']),
},
};
</script>
@@ -31,31 +13,8 @@ export default {
v-if="!lastCommitMsg"
class="multi-file-commit-panel-section ide-commit-empty-state js-empty-state"
>
- <header
- class="multi-file-commit-panel-header"
- :class="{
- 'is-collapsed': rightPanelCollapsed,
- }"
- >
- <button
- v-tooltip
- :title="collapseButtonTooltip"
- data-container="body"
- data-placement="left"
- type="button"
- class="btn btn-transparent multi-file-commit-panel-collapse-btn"
- :aria-label="__('Toggle sidebar')"
- @click.stop="toggleRightPanelCollapsed"
- >
- <icon
- :name="collapseButtonIcon"
- :size="18"
- />
- </button>
- </header>
<div
class="ide-commit-empty-state-container"
- v-if="!rightPanelCollapsed"
>
<div class="svg-content svg-80">
<img :src="noChangesStateSvgPath" />
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
new file mode 100644
index 00000000000..4a645c827ad
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
@@ -0,0 +1,171 @@
+<script>
+import { mapState, mapActions, mapGetters } from 'vuex';
+import { sprintf, __ } from '~/locale';
+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';
+
+export default {
+ components: {
+ Actions,
+ LoadingButton,
+ CommitMessageField,
+ SuccessMessage,
+ },
+ data() {
+ return {
+ isCompact: true,
+ componentHeight: null,
+ };
+ },
+ computed: {
+ ...mapState(['changedFiles', 'stagedFiles', 'currentActivityView', 'lastCommitMsg']),
+ ...mapState('commit', ['commitMessage', 'submitCommitLoading']),
+ ...mapGetters(['hasChanges']),
+ ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']),
+ overviewText() {
+ return sprintf(
+ __(
+ '<strong>%{changedFilesLength} unstaged</strong> and <strong>%{stagedFilesLength} staged</strong> changes',
+ ),
+ {
+ stagedFilesLength: this.stagedFiles.length,
+ changedFilesLength: this.changedFiles.length,
+ },
+ );
+ },
+ },
+ watch: {
+ currentActivityView() {
+ if (this.lastCommitMsg) {
+ this.isCompact = false;
+ } else {
+ this.isCompact = !(
+ this.currentActivityView === activityBarViews.commit &&
+ window.innerHeight >= MAX_WINDOW_HEIGHT_COMPACT
+ );
+ }
+ },
+ lastCommitMsg() {
+ this.isCompact =
+ this.currentActivityView !== activityBarViews.commit && this.lastCommitMsg === '';
+ },
+ },
+ methods: {
+ ...mapActions(['updateActivityBarView']),
+ ...mapActions('commit', ['updateCommitMessage', 'discardDraft', 'commitChanges']),
+ toggleIsSmall() {
+ this.updateActivityBarView(activityBarViews.commit)
+ .then(() => {
+ this.isCompact = !this.isCompact;
+ })
+ .catch(e => {
+ throw e;
+ });
+ },
+ beforeEnterTransition() {
+ const elHeight = this.isCompact
+ ? this.$refs.formEl && this.$refs.formEl.offsetHeight
+ : this.$refs.compactEl && this.$refs.compactEl.offsetHeight;
+
+ this.componentHeight = elHeight + COMMIT_ITEM_PADDING;
+ },
+ enterTransition() {
+ this.$nextTick(() => {
+ const elHeight = this.isCompact
+ ? this.$refs.compactEl && this.$refs.compactEl.offsetHeight
+ : this.$refs.formEl && this.$refs.formEl.offsetHeight;
+
+ this.componentHeight = elHeight + COMMIT_ITEM_PADDING;
+ });
+ },
+ afterEndTransition() {
+ this.componentHeight = null;
+ },
+ },
+ activityBarViews,
+};
+</script>
+
+<template>
+ <div
+ class="multi-file-commit-form"
+ :class="{
+ 'is-compact': isCompact,
+ 'is-full': !isCompact
+ }"
+ :style="{
+ height: componentHeight ? `${componentHeight}px` : null,
+ }"
+ >
+ <transition
+ name="commit-form-slide-up"
+ @before-enter="beforeEnterTransition"
+ @enter="enterTransition"
+ @after-enter="afterEndTransition"
+ >
+ <div
+ v-if="isCompact"
+ class="commit-form-compact"
+ ref="compactEl"
+ >
+ <button
+ type="button"
+ :disabled="!hasChanges"
+ class="btn btn-primary btn-sm btn-block"
+ @click="toggleIsSmall"
+ >
+ {{ __('Commit') }}
+ </button>
+ <p
+ class="text-center"
+ v-html="overviewText"
+ ></p>
+ </div>
+ <form
+ v-if="!isCompact"
+ class="form-horizontal"
+ @submit.prevent.stop="commitChanges"
+ ref="formEl"
+ >
+ <transition name="fade">
+ <success-message
+ v-show="lastCommitMsg"
+ />
+ </transition>
+ <commit-message-field
+ :text="commitMessage"
+ @input="updateCommitMessage"
+ />
+ <div class="clearfix prepend-top-15">
+ <actions />
+ <loading-button
+ :loading="submitCommitLoading"
+ :disabled="commitButtonDisabled"
+ container-class="btn btn-success btn-sm pull-left"
+ :label="__('Commit')"
+ @click="commitChanges"
+ />
+ <button
+ v-if="!discardDraftButtonDisabled"
+ type="button"
+ class="btn btn-default btn-sm pull-right"
+ @click="discardDraft"
+ >
+ {{ __('Discard draft') }}
+ </button>
+ <button
+ v-else
+ type="button"
+ class="btn btn-default btn-sm pull-right"
+ @click="toggleIsSmall"
+ >
+ {{ __('Collapse') }}
+ </button>
+ </div>
+ </form>
+ </transition>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
index ff05ee8682a..c3ac18bfb83 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
@@ -1,16 +1,14 @@
<script>
-import { mapActions, mapState, mapGetters } from 'vuex';
+import { mapActions } from 'vuex';
import { __, sprintf } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import ListItem from './list_item.vue';
-import ListCollapsed from './list_collapsed.vue';
export default {
components: {
Icon,
ListItem,
- ListCollapsed,
},
directives: {
tooltip,
@@ -24,11 +22,6 @@ export default {
type: Array,
required: true,
},
- showToggle: {
- type: Boolean,
- required: false,
- default: true,
- },
iconName: {
type: String,
required: true,
@@ -51,9 +44,12 @@ export default {
default: false,
},
},
+ data() {
+ return {
+ showActionButton: false,
+ };
+ },
computed: {
- ...mapState(['rightPanelCollapsed']),
- ...mapGetters(['collapseButtonIcon', 'collapseButtonTooltip']),
titleText() {
return sprintf(__('%{title} changes'), {
title: this.title,
@@ -61,10 +57,13 @@ export default {
},
},
methods: {
- ...mapActions(['toggleRightPanelCollapsed', 'stageAllChanges', 'unstageAllChanges']),
+ ...mapActions(['stageAllChanges', 'unstageAllChanges']),
actionBtnClicked() {
this[this.action]();
},
+ setShowActionButton(show) {
+ this.showActionButton = show;
+ },
},
};
</script>
@@ -72,19 +71,14 @@ export default {
<template>
<div
class="ide-commit-list-container"
- :class="{
- 'is-collapsed': rightPanelCollapsed,
- }"
>
<header
class="multi-file-commit-panel-header"
+ @mouseenter="setShowActionButton(true)"
+ @mouseleave="setShowActionButton(false)"
>
<div
- v-if="!rightPanelCollapsed"
class="multi-file-commit-panel-header-title"
- :class="{
- 'append-right-10': showToggle,
- }"
>
<icon
v-once
@@ -92,7 +86,14 @@ export default {
:size="18"
/>
{{ titleText }}
+ <span
+ v-show="!showActionButton"
+ class="ide-commit-file-count"
+ >
+ {{ fileList.length }}
+ </span>
<button
+ v-show="showActionButton"
type="button"
class="btn btn-blank btn-link ide-staged-action-btn"
@click="actionBtnClicked"
@@ -100,52 +101,28 @@ export default {
{{ actionBtnText }}
</button>
</div>
- <button
- v-if="showToggle"
- v-tooltip
- :title="collapseButtonTooltip"
- data-container="body"
- data-placement="left"
- type="button"
- class="btn btn-transparent multi-file-commit-panel-collapse-btn"
- :aria-label="__('Toggle sidebar')"
- @click.stop="toggleRightPanelCollapsed"
- >
- <icon
- :name="collapseButtonIcon"
- :size="18"
- />
- </button>
</header>
- <list-collapsed
- v-if="rightPanelCollapsed"
- :files="fileList"
- :icon-name="iconName"
- :title="title"
- />
- <template v-else>
- <ul
- v-if="fileList.length"
- class="multi-file-commit-list list-unstyled append-bottom-0"
- >
- <li
- v-for="file in fileList"
- :key="file.key"
- >
- <list-item
- :file="file"
- :action-component="itemActionComponent"
- :key-prefix="title"
- :staged-list="stagedList"
- />
- </li>
- </ul>
- <p
- v-else
- class="multi-file-commit-list help-block"
+ <ul
+ v-if="fileList.length"
+ class="multi-file-commit-list list-unstyled append-bottom-0"
+ >
+ <li
+ v-for="file in fileList"
+ :key="file.key"
>
- {{ __('No changes') }}
- </p>
- </template>
+ <list-item
+ :file="file"
+ :action-component="itemActionComponent"
+ :key-prefix="title"
+ :staged-list="stagedList"
+ />
+ </li>
+ </ul>
+ <p
+ v-else
+ class="multi-file-commit-list help-block"
+ >
+ {{ __('No changes') }}
+ </p>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
index 872302840e2..03f3e4de83c 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
@@ -3,6 +3,7 @@ import { mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import StageButton from './stage_button.vue';
import UnstageButton from './unstage_button.vue';
+import { viewerTypes } from '../../constants';
export default {
components: {
@@ -53,7 +54,7 @@ export default {
keyPrefix: this.keyPrefix.toLowerCase(),
}).then(changeViewer => {
if (changeViewer) {
- this.updateViewer('diff');
+ this.updateViewer(viewerTypes.diff);
}
});
},
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
index b660a2961cb..00f2312ae51 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
@@ -1,5 +1,6 @@
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
+import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
@@ -26,10 +27,20 @@ export default {
required: false,
default: false,
},
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
...mapState('commit', ['commitAction']),
...mapGetters('commit', ['newBranchName']),
+ tooltipTitle() {
+ return this.disabled
+ ? __('This option is disabled while you still have unstaged changes')
+ : '';
+ },
},
methods: {
...mapActions('commit', ['updateCommitAction', 'updateBranchName']),
@@ -39,19 +50,28 @@ export default {
<template>
<fieldset>
- <label>
+ <label
+ v-tooltip
+ :title="tooltipTitle"
+ :class="{
+ 'is-disabled': disabled
+ }"
+ >
<input
type="radio"
name="commit-action"
:value="value"
@change="updateCommitAction($event.target.value)"
- :checked="checked"
- v-once
+ :checked="commitAction === value"
+ :disabled="disabled"
/>
<span class="prepend-left-10">
- <template v-if="label">
+ <span
+ v-if="label"
+ class="ide-radio-label"
+ >
{{ label }}
- </template>
+ </span>
<slot v-else></slot>
</span>
</label>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue b/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue
index 628a17eddca..a6df91b79c2 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue
@@ -2,14 +2,8 @@
import { mapState } from 'vuex';
export default {
- props: {
- committedStateSvgPath: {
- type: String,
- required: true,
- },
- },
computed: {
- ...mapState(['lastCommitMsg']),
+ ...mapState(['lastCommitMsg', 'committedStateSvgPath']),
},
};
</script>
diff --git a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
index 0c44a755f56..b9af4d27145 100644
--- a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
+++ b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
@@ -1,28 +1,15 @@
<script>
-import Icon from '~/vue_shared/components/icon.vue';
import { __, sprintf } from '~/locale';
+import { viewerTypes } from '../constants';
export default {
- components: {
- Icon,
- },
props: {
- hasChanges: {
- type: Boolean,
- required: false,
- default: false,
- },
- mergeRequestId: {
- type: String,
- required: false,
- default: '',
- },
viewer: {
type: String,
required: true,
},
- showShadow: {
- type: Boolean,
+ mergeRequestId: {
+ type: Number,
required: true,
},
},
@@ -38,84 +25,45 @@ export default {
this.$emit('click', mode);
},
},
+ viewerTypes,
};
</script>
<template>
<div
class="dropdown"
- :class="{
- shadow: showShadow,
- }"
>
<button
type="button"
- class="btn btn-primary btn-sm"
- :class="{
- 'btn-inverted': hasChanges,
- }"
+ class="btn btn-link"
data-toggle="dropdown"
>
- <template v-if="viewer === 'mrdiff' && mergeRequestId">
- {{ mergeReviewLine }}
- </template>
- <template v-else-if="viewer === 'editor'">
- {{ __('Editing') }}
- </template>
- <template v-else>
- {{ __('Reviewing') }}
- </template>
- <icon
- name="angle-down"
- :size="12"
- css-classes="caret-down"
- />
+ {{ __('Edit') }}
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-open-left">
<ul>
- <template v-if="mergeRequestId">
- <li>
- <a
- href="#"
- @click.prevent="changeMode('mrdiff')"
- :class="{
- 'is-active': viewer === 'mrdiff',
- }"
- >
- <strong class="dropdown-menu-inner-title">
- {{ mergeReviewLine }}
- </strong>
- <span class="dropdown-menu-inner-content">
- {{ __('Compare changes with the merge request target branch') }}
- </span>
- </a>
- </li>
- <li
- role="separator"
- class="divider"
- >
- </li>
- </template>
<li>
<a
href="#"
- @click.prevent="changeMode('editor')"
+ @click.prevent="changeMode($options.viewerTypes.mr)"
:class="{
- 'is-active': viewer === 'editor',
+ 'is-active': viewer === $options.viewerTypes.mr,
}"
>
- <strong class="dropdown-menu-inner-title">{{ __('Editing') }}</strong>
+ <strong class="dropdown-menu-inner-title">
+ {{ mergeReviewLine }}
+ </strong>
<span class="dropdown-menu-inner-content">
- {{ __('View and edit lines') }}
+ {{ __('Compare changes with the merge request target branch') }}
</span>
</a>
</li>
<li>
<a
href="#"
- @click.prevent="changeMode('diff')"
+ @click.prevent="changeMode($options.viewerTypes.diff)"
:class="{
- 'is-active': viewer === 'diff',
+ 'is-active': viewer === $options.viewerTypes.diff,
}"
>
<strong class="dropdown-menu-inner-title">{{ __('Reviewing') }}</strong>
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index 0274fc7d299..6c373a92776 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -1,144 +1,127 @@
<script>
- import { mapActions, mapState, mapGetters } from 'vuex';
- import Mousetrap from 'mousetrap';
- import ideSidebar from './ide_side_bar.vue';
- import ideContextbar from './ide_context_bar.vue';
- import repoTabs from './repo_tabs.vue';
- import ideStatusBar from './ide_status_bar.vue';
- import repoEditor from './repo_editor.vue';
- import FindFile from './file_finder/index.vue';
+import Mousetrap from 'mousetrap';
+import { mapActions, mapState, mapGetters } from 'vuex';
+import IdeSidebar from './ide_side_bar.vue';
+import RepoTabs from './repo_tabs.vue';
+import IdeStatusBar from './ide_status_bar.vue';
+import RepoEditor from './repo_editor.vue';
+import FindFile from './file_finder/index.vue';
- const originalStopCallback = Mousetrap.stopCallback;
+const originalStopCallback = Mousetrap.stopCallback;
- export default {
- components: {
- ideSidebar,
- ideContextbar,
- repoTabs,
- ideStatusBar,
- repoEditor,
- FindFile,
- },
- props: {
- emptyStateSvgPath: {
- type: String,
- required: true,
- },
- noChangesStateSvgPath: {
- type: String,
- required: true,
- },
- committedStateSvgPath: {
- type: String,
- required: true,
- },
- },
- computed: {
- ...mapState([
- 'changedFiles',
- 'openFiles',
- 'viewer',
- 'currentMergeRequestId',
- 'fileFindVisible',
- ]),
- ...mapGetters(['activeFile', 'hasChanges']),
- },
- mounted() {
- const returnValue = 'Are you sure you want to lose unsaved changes?';
- window.onbeforeunload = e => {
- if (!this.changedFiles.length) return undefined;
+export default {
+ components: {
+ IdeSidebar,
+ RepoTabs,
+ IdeStatusBar,
+ RepoEditor,
+ FindFile,
+ },
+ computed: {
+ ...mapState([
+ 'changedFiles',
+ 'openFiles',
+ 'viewer',
+ 'currentMergeRequestId',
+ 'fileFindVisible',
+ 'emptyStateSvgPath',
+ ]),
+ ...mapGetters(['activeFile', 'hasChanges']),
+ },
+ mounted() {
+ const returnValue = 'Are you sure you want to lose unsaved changes?';
+ window.onbeforeunload = e => {
+ if (!this.changedFiles.length) return undefined;
- Object.assign(e, {
- returnValue,
- });
- return returnValue;
- };
+ Object.assign(e, {
+ returnValue,
+ });
+ return returnValue;
+ };
- Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => {
- if (e.preventDefault) {
- e.preventDefault();
- }
+ Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => {
+ if (e.preventDefault) {
+ e.preventDefault();
+ }
- this.toggleFileFinder(!this.fileFindVisible);
- });
+ this.toggleFileFinder(!this.fileFindVisible);
+ });
- Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo);
- },
- methods: {
- ...mapActions(['toggleFileFinder']),
- mousetrapStopCallback(e, el, combo) {
- if (combo === 't' && el.classList.contains('dropdown-input-field')) {
- return true;
- } else if (combo === 'command+p' || combo === 'ctrl+p') {
- return false;
- }
+ Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo);
+ },
+ methods: {
+ ...mapActions(['toggleFileFinder']),
+ mousetrapStopCallback(e, el, combo) {
+ if (combo === 't' && el.classList.contains('dropdown-input-field')) {
+ return true;
+ } else if (combo === 'command+p' || combo === 'ctrl+p') {
+ return false;
+ }
- return originalStopCallback(e, el, combo);
- },
+ return originalStopCallback(e, el, combo);
},
- };
+ },
+};
</script>
<template>
- <div
- class="ide-view"
- >
- <find-file
- v-show="fileFindVisible"
- />
- <ide-sidebar />
+ <article class="ide">
<div
- class="multi-file-edit-pane"
+ class="ide-view"
>
- <template
- v-if="activeFile"
- >
- <repo-tabs
- :active-file="activeFile"
- :files="openFiles"
- :viewer="viewer"
- :has-changes="hasChanges"
- :merge-request-id="currentMergeRequestId"
- />
- <repo-editor
- class="multi-file-edit-pane-content"
- :file="activeFile"
- />
- <ide-status-bar
- :file="activeFile"
- />
- </template>
- <template
- v-else
+ <find-file
+ v-show="fileFindVisible"
+ />
+ <ide-sidebar />
+ <div
+ class="multi-file-edit-pane"
>
- <div
- v-once
- class="ide-empty-state"
+ <template
+ v-if="activeFile"
>
- <div class="row js-empty-state">
- <div class="col-xs-12">
- <div class="svg-content svg-250">
- <img :src="emptyStateSvgPath" />
+ <repo-tabs
+ :active-file="activeFile"
+ :files="openFiles"
+ :viewer="viewer"
+ :has-changes="hasChanges"
+ :merge-request-id="currentMergeRequestId"
+ />
+ <repo-editor
+ class="multi-file-edit-pane-content"
+ :file="activeFile"
+ />
+ </template>
+ <template
+ v-else
+ >
+ <div
+ v-once
+ class="ide-empty-state"
+ >
+ <div class="row js-empty-state">
+ <div class="col-xs-12">
+ <div class="svg-content svg-250">
+ <img :src="emptyStateSvgPath" />
+ </div>
</div>
- </div>
- <div class="col-xs-12">
- <div class="text-content text-center">
- <h4>
- Welcome to the GitLab IDE
- </h4>
- <p>
- You can select a file in the left sidebar to begin
- editing and use the right sidebar to commit your changes.
- </p>
+ <div class="col-xs-12">
+ <div class="text-content text-center">
+ <h4>
+ Welcome to the GitLab IDE
+ </h4>
+ <p>
+ Select a file from the left sidebar to begin editing.
+ Afterwards, you'll be able to commit your changes.
+ </p>
+ </div>
</div>
</div>
</div>
- </div>
- </template>
+ </template>
+ </div>
</div>
- <ide-contextbar
- :no-changes-state-svg-path="noChangesStateSvgPath"
- :committed-state-svg-path="committedStateSvgPath"
+ <ide-status-bar
+ :file="activeFile"
/>
- </div>
+ </article>
</template>
diff --git a/app/assets/javascripts/ide/components/ide_context_bar.vue b/app/assets/javascripts/ide/components/ide_context_bar.vue
deleted file mode 100644
index 627fbeb9adf..00000000000
--- a/app/assets/javascripts/ide/components/ide_context_bar.vue
+++ /dev/null
@@ -1,42 +0,0 @@
-<script>
-import icon from '~/vue_shared/components/icon.vue';
-import panelResizer from '~/vue_shared/components/panel_resizer.vue';
-import repoCommitSection from './repo_commit_section.vue';
-import ResizablePanel from './resizable_panel.vue';
-
-export default {
- components: {
- repoCommitSection,
- icon,
- panelResizer,
- ResizablePanel,
- },
- props: {
- noChangesStateSvgPath: {
- type: String,
- required: true,
- },
- committedStateSvgPath: {
- type: String,
- required: true,
- },
- },
-};
-</script>
-
-<template>
- <resizable-panel
- :collapsible="true"
- :initial-width="340"
- side="right"
- >
- <div
- class="multi-file-commit-panel-section"
- >
- <repo-commit-section
- :no-changes-state-svg-path="noChangesStateSvgPath"
- :committed-state-svg-path="committedStateSvgPath"
- />
- </div>
- </resizable-panel>
-</template>
diff --git a/app/assets/javascripts/ide/components/ide_external_links.vue b/app/assets/javascripts/ide/components/ide_external_links.vue
deleted file mode 100644
index c6f6e0d2348..00000000000
--- a/app/assets/javascripts/ide/components/ide_external_links.vue
+++ /dev/null
@@ -1,43 +0,0 @@
-<script>
-import icon from '~/vue_shared/components/icon.vue';
-
-export default {
- components: {
- icon,
- },
- props: {
- projectUrl: {
- type: String,
- required: true,
- },
- },
- computed: {
- goBackUrl() {
- return document.referrer || this.projectUrl;
- },
- },
-};
-</script>
-
-<template>
- <nav
- class="ide-external-links"
- v-once
- >
- <p>
- <a
- :href="goBackUrl"
- class="ide-sidebar-link"
- >
- <icon
- :size="16"
- class="append-right-8"
- name="go-back"
- />
- <span class="ide-external-links-text">
- {{ s__('Go back') }}
- </span>
- </a>
- </p>
- </nav>
-</template>
diff --git a/app/assets/javascripts/ide/components/ide_project_branches_tree.vue b/app/assets/javascripts/ide/components/ide_project_branches_tree.vue
deleted file mode 100644
index eb2749e6151..00000000000
--- a/app/assets/javascripts/ide/components/ide_project_branches_tree.vue
+++ /dev/null
@@ -1,47 +0,0 @@
-<script>
- import icon from '~/vue_shared/components/icon.vue';
- import repoTree from './ide_repo_tree.vue';
- import newDropdown from './new_dropdown/index.vue';
-
- export default {
- components: {
- repoTree,
- icon,
- newDropdown,
- },
- props: {
- projectId: {
- type: String,
- required: true,
- },
- branch: {
- type: Object,
- required: true,
- },
- },
- };
-</script>
-
-<template>
- <div class="branch-container">
- <div class="branch-header">
- <div class="branch-header-title str-truncated ref-name">
- <icon
- name="branch"
- :size="12"
- />
- {{ branch.name }}
- </div>
- <div class="branch-header-btns">
- <new-dropdown
- :project-id="projectId"
- :branch="branch.name"
- path=""
- />
- </div>
- </div>
- <repo-tree
- :tree="branch.tree"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/ide_project_tree.vue b/app/assets/javascripts/ide/components/ide_project_tree.vue
deleted file mode 100644
index a6f40286ac1..00000000000
--- a/app/assets/javascripts/ide/components/ide_project_tree.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<script>
-import ProjectAvatarImage from '~/vue_shared/components/project_avatar/image.vue';
-import Identicon from '../../vue_shared/components/identicon.vue';
-import BranchesTree from './ide_project_branches_tree.vue';
-import ExternalLinks from './ide_external_links.vue';
-
-export default {
- components: {
- BranchesTree,
- ExternalLinks,
- ProjectAvatarImage,
- Identicon,
- },
- props: {
- project: {
- type: Object,
- required: true,
- },
- },
-};
-</script>
-
-<template>
- <div class="projects-sidebar">
- <div class="context-header">
- <a
- :title="project.name"
- :href="project.web_url"
- >
- <div
- v-if="project.avatar_url"
- class="avatar-container s40 project-avatar"
- >
- <project-avatar-image
- class="avatar-container project-avatar"
- :link-href="project.path"
- :img-src="project.avatar_url"
- :img-alt="project.name"
- :img-size="40"
- />
- </div>
- <identicon
- v-else
- size-class="s40"
- :entity-id="project.id"
- :entity-name="project.name"
- />
- <div class="sidebar-context-title">
- {{ project.name }}
- </div>
- </a>
- </div>
- <external-links
- :project-url="project.web_url"
- />
- <div class="multi-file-commit-panel-inner-scroll">
- <branches-tree
- v-for="branch in project.branches"
- :key="branch.name"
- :project-id="project.path_with_namespace"
- :branch="branch"
- />
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/ide_repo_tree.vue b/app/assets/javascripts/ide/components/ide_repo_tree.vue
deleted file mode 100644
index e6af88e04bc..00000000000
--- a/app/assets/javascripts/ide/components/ide_repo_tree.vue
+++ /dev/null
@@ -1,41 +0,0 @@
-<script>
-import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
-import RepoFile from './repo_file.vue';
-
-export default {
- components: {
- RepoFile,
- SkeletonLoadingContainer,
- },
- props: {
- tree: {
- type: Object,
- required: true,
- },
- },
-};
-</script>
-
-<template>
- <div
- class="ide-file-list"
- >
- <template v-if="tree.loading">
- <div
- class="multi-file-loading-container"
- v-for="n in 3"
- :key="n"
- >
- <skeleton-loading-container />
- </div>
- </template>
- <template v-else>
- <repo-file
- v-for="file in tree.tree"
- :key="file.key"
- :file="file"
- :level="0"
- />
- </template>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/ide_review.vue b/app/assets/javascripts/ide/components/ide_review.vue
new file mode 100644
index 00000000000..0c9ec3b00f0
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_review.vue
@@ -0,0 +1,62 @@
+<script>
+import { mapGetters, mapState, mapActions } from 'vuex';
+import IdeTreeList from './ide_tree_list.vue';
+import EditorModeDropdown from './editor_mode_dropdown.vue';
+import { viewerTypes } from '../constants';
+
+export default {
+ components: {
+ IdeTreeList,
+ EditorModeDropdown,
+ },
+ computed: {
+ ...mapGetters(['currentMergeRequest']),
+ ...mapState(['viewer']),
+ showLatestChangesText() {
+ return !this.currentMergeRequest || this.viewer === viewerTypes.diff;
+ },
+ showMergeRequestText() {
+ return this.currentMergeRequest && this.viewer === viewerTypes.mr;
+ },
+ },
+ mounted() {
+ this.$nextTick(() => {
+ this.updateViewer(this.currentMergeRequest ? viewerTypes.mr : viewerTypes.diff);
+ });
+ },
+ methods: {
+ ...mapActions(['updateViewer']),
+ },
+};
+</script>
+
+<template>
+ <ide-tree-list
+ :viewer-type="viewer"
+ header-class="ide-review-header"
+ :disable-action-dropdown="true"
+ >
+ <template
+ slot="header"
+ >
+ <div class="ide-review-button-holder">
+ {{ __('Review') }}
+ <editor-mode-dropdown
+ v-if="currentMergeRequest"
+ :viewer="viewer"
+ :merge-request-id="currentMergeRequest.iid"
+ @click="updateViewer"
+ />
+ </div>
+ <div class="prepend-top-5 ide-review-sub-header">
+ <template v-if="showLatestChangesText">
+ {{ __('Latest changes') }}
+ </template>
+ <template v-else-if="showMergeRequestText">
+ {{ __('Merge request') }}
+ (<a :href="currentMergeRequest.web_url">!{{ currentMergeRequest.iid }}</a>)
+ </template>
+ </div>
+ </template>
+ </ide-tree-list>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue
index 8cf1ccb4fce..3f980203911 100644
--- a/app/assets/javascripts/ide/components/ide_side_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_side_bar.vue
@@ -1,36 +1,82 @@
<script>
- import { mapState, mapGetters } from 'vuex';
- import icon from '~/vue_shared/components/icon.vue';
- import panelResizer from '~/vue_shared/components/panel_resizer.vue';
- import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
- import projectTree from './ide_project_tree.vue';
- import ResizablePanel from './resizable_panel.vue';
+import { mapState, mapGetters } from 'vuex';
+import ProjectAvatarImage from '~/vue_shared/components/project_avatar/image.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
+import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
+import Identicon from '../../vue_shared/components/identicon.vue';
+import IdeTree from './ide_tree.vue';
+import ResizablePanel from './resizable_panel.vue';
+import ActivityBar from './activity_bar.vue';
+import CommitSection from './repo_commit_section.vue';
+import CommitForm from './commit_sidebar/form.vue';
+import IdeReview from './ide_review.vue';
+import SuccessMessage from './commit_sidebar/success_message.vue';
+import { activityBarViews } from '../constants';
- export default {
- components: {
- projectTree,
- icon,
- panelResizer,
- skeletonLoadingContainer,
- ResizablePanel,
+export default {
+ directives: {
+ tooltip,
+ },
+ components: {
+ Icon,
+ PanelResizer,
+ SkeletonLoadingContainer,
+ ResizablePanel,
+ ActivityBar,
+ ProjectAvatarImage,
+ Identicon,
+ CommitSection,
+ IdeTree,
+ CommitForm,
+ IdeReview,
+ SuccessMessage,
+ },
+ data() {
+ return {
+ showTooltip: false,
+ };
+ },
+ computed: {
+ ...mapState([
+ 'loading',
+ 'currentBranchId',
+ 'currentActivityView',
+ 'changedFiles',
+ 'stagedFiles',
+ 'lastCommitMsg',
+ ]),
+ ...mapGetters(['currentProject', 'someUncommitedChanges']),
+ showSuccessMessage() {
+ return (
+ this.currentActivityView === activityBarViews.edit &&
+ (this.lastCommitMsg && !this.someUncommitedChanges)
+ );
},
- computed: {
- ...mapState([
- 'loading',
- ]),
- ...mapGetters([
- 'projectsWithTrees',
- ]),
+ branchTooltipTitle() {
+ return this.showTooltip ? this.currentBranchId : undefined;
},
- };
+ },
+ watch: {
+ currentBranchId() {
+ this.$nextTick(() => {
+ this.showTooltip = this.$refs.branchId.scrollWidth > this.$refs.branchId.offsetWidth;
+ });
+ },
+ },
+};
</script>
<template>
<resizable-panel
:collapsible="false"
- :initial-width="290"
+ :initial-width="340"
side="left"
>
+ <activity-bar
+ v-if="!loading"
+ />
<div class="multi-file-commit-panel-inner">
<template v-if="loading">
<div
@@ -41,11 +87,54 @@
<skeleton-loading-container />
</div>
</template>
- <project-tree
- v-for="project in projectsWithTrees"
- :key="project.id"
- :project="project"
- />
+ <template v-else>
+ <div class="context-header ide-context-header">
+ <a
+ :href="currentProject.web_url"
+ >
+ <div
+ v-if="currentProject.avatar_url"
+ class="avatar-container s40 project-avatar"
+ >
+ <project-avatar-image
+ class="avatar-container project-avatar"
+ :link-href="currentProject.path"
+ :img-src="currentProject.avatar_url"
+ :img-alt="currentProject.name"
+ :img-size="40"
+ />
+ </div>
+ <identicon
+ v-else
+ size-class="s40"
+ :entity-id="currentProject.id"
+ :entity-name="currentProject.name"
+ />
+ <div class="ide-sidebar-project-title">
+ <div class="sidebar-context-title">
+ {{ currentProject.name }}
+ </div>
+ <div
+ class="sidebar-context-title ide-sidebar-branch-title"
+ ref="branchId"
+ v-tooltip
+ :title="branchTooltipTitle"
+ >
+ <icon
+ name="branch"
+ css-classes="append-right-5"
+ />{{ currentBranchId }}
+ </div>
+ </div>
+ </a>
+ </div>
+ <div class="multi-file-commit-panel-inner-scroll">
+ <component
+ :is="currentActivityView"
+ />
+ </div>
+ <commit-form />
+ </template>
</div>
</resizable-panel>
</template>
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
index c13eeeace3f..70c6d53c3ab 100644
--- a/app/assets/javascripts/ide/components/ide_status_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_status_bar.vue
@@ -1,11 +1,14 @@
<script>
+import { mapGetters } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import timeAgoMixin from '~/vue_shared/mixins/timeago';
+import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
export default {
components: {
icon,
+ userAvatarImage,
},
directives: {
tooltip,
@@ -14,40 +17,93 @@ export default {
props: {
file: {
type: Object,
- required: true,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ lastCommitFormatedAge: null,
+ };
+ },
+ computed: {
+ ...mapGetters(['currentProject', 'lastCommit']),
+ },
+ mounted() {
+ this.startTimer();
+ },
+ beforeDestroy() {
+ if (this.intervalId) {
+ clearInterval(this.intervalId);
+ }
+ },
+ methods: {
+ startTimer() {
+ this.intervalId = setInterval(() => {
+ this.commitAgeUpdate();
+ }, 1000);
+ },
+ commitAgeUpdate() {
+ if (this.lastCommit) {
+ this.lastCommitFormatedAge = this.timeFormated(this.lastCommit.committed_date);
+ }
+ },
+ getCommitPath(shortSha) {
+ return `${this.currentProject.web_url}/commit/${shortSha}`;
},
},
};
</script>
<template>
- <div class="ide-status-bar">
- <div>
- <div v-if="file.lastCommit && file.lastCommit.id">
- Last commit:
- <a
- v-tooltip
- :title="file.lastCommit.message"
- :href="file.lastCommit.url"
- >
- {{ timeFormated(file.lastCommit.updatedAt) }} by
- {{ file.lastCommit.author }}
- </a>
- </div>
+ <footer class="ide-status-bar">
+ <div
+ class="ide-status-branch"
+ v-if="lastCommit && lastCommitFormatedAge"
+ >
+ <icon
+ name="commit"
+ />
+ <a
+ v-tooltip
+ class="commit-sha"
+ :title="lastCommit.message"
+ :href="getCommitPath(lastCommit.short_id)"
+ >{{ lastCommit.short_id }}</a>
+ by
+ {{ lastCommit.author_name }}
+ <time
+ v-tooltip
+ data-placement="top"
+ data-container="body"
+ :datetime="lastCommit.committed_date"
+ :title="tooltipTitle(lastCommit.committed_date)"
+ >
+ {{ lastCommitFormatedAge }}
+ </time>
</div>
- <div class="text-right">
+ <div
+ v-if="file"
+ class="ide-status-file"
+ >
{{ file.name }}
</div>
- <div class="text-right">
+ <div
+ v-if="file"
+ class="ide-status-file"
+ >
{{ file.eol }}
</div>
<div
- class="text-right"
- v-if="!file.binary">
+ class="ide-status-file"
+ v-if="file && !file.binary">
{{ file.editorRow }}:{{ file.editorColumn }}
</div>
- <div class="text-right">
+ <div
+ v-if="file"
+ class="ide-status-file"
+ >
{{ file.fileLanguage }}
</div>
- </div>
+ </footer>
</template>
diff --git a/app/assets/javascripts/ide/components/ide_tree.vue b/app/assets/javascripts/ide/components/ide_tree.vue
new file mode 100644
index 00000000000..8fc4ebe6ca6
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_tree.vue
@@ -0,0 +1,42 @@
+<script>
+import { mapState, mapGetters, mapActions } from 'vuex';
+import NewDropdown from './new_dropdown/index.vue';
+import IdeTreeList from './ide_tree_list.vue';
+
+export default {
+ components: {
+ NewDropdown,
+ IdeTreeList,
+ },
+ computed: {
+ ...mapState(['currentBranchId']),
+ ...mapGetters(['currentProject', 'currentTree', 'activeFile']),
+ },
+ mounted() {
+ if (this.activeFile && this.activeFile.pending) {
+ this.$router.push(`/project${this.activeFile.url}`, () => {
+ this.updateViewer('editor');
+ });
+ }
+ },
+ methods: {
+ ...mapActions(['updateViewer']),
+ },
+};
+</script>
+
+<template>
+ <ide-tree-list
+ viewer-type="editor"
+ >
+ <template
+ slot="header"
+ >
+ {{ __('Edit') }}
+ <new-dropdown
+ :project-id="currentProject.name_with_namespace"
+ :branch="currentBranchId"
+ />
+ </template>
+ </ide-tree-list>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue
new file mode 100644
index 00000000000..e64a09fcc90
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_tree_list.vue
@@ -0,0 +1,76 @@
+<script>
+import { mapActions, mapGetters, mapState } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
+import RepoFile from './repo_file.vue';
+import NewDropdown from './new_dropdown/index.vue';
+
+export default {
+ components: {
+ Icon,
+ RepoFile,
+ SkeletonLoadingContainer,
+ NewDropdown,
+ },
+ props: {
+ viewerType: {
+ type: String,
+ required: true,
+ },
+ headerClass: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ disableActionDropdown: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ ...mapState(['currentBranchId']),
+ ...mapGetters(['currentProject', 'currentTree']),
+ showLoading() {
+ return !this.currentTree || this.currentTree.loading;
+ },
+ },
+ mounted() {
+ this.updateViewer(this.viewerType);
+ },
+ methods: {
+ ...mapActions(['updateViewer']),
+ },
+};
+</script>
+
+<template>
+ <div
+ class="ide-file-list"
+ >
+ <template v-if="showLoading">
+ <div
+ class="multi-file-loading-container"
+ v-for="n in 3"
+ :key="n"
+ >
+ <skeleton-loading-container />
+ </div>
+ </template>
+ <template v-else>
+ <header
+ class="ide-tree-header"
+ :class="headerClass"
+ >
+ <slot name="header"></slot>
+ </header>
+ <repo-file
+ v-for="file in currentTree.tree"
+ :key="file.key"
+ :file="file"
+ :level="0"
+ :disable-action-dropdown="disableActionDropdown"
+ />
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/mr_file_icon.vue b/app/assets/javascripts/ide/components/mr_file_icon.vue
index 8a440902dfc..179a589d1ac 100644
--- a/app/assets/javascripts/ide/components/mr_file_icon.vue
+++ b/app/assets/javascripts/ide/components/mr_file_icon.vue
@@ -16,8 +16,8 @@ export default {
<icon
name="git-merge"
v-tooltip
- title="__('Part of merge request changes')"
- css-classes="ide-file-changed-icon"
+ :title="__('Part of merge request changes')"
+ css-classes="append-right-8"
:size="12"
/>
</template>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue
index b1b5c0d4a28..a0ce1c9dac7 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/index.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue
@@ -17,7 +17,8 @@ export default {
},
path: {
type: String,
- required: true,
+ required: false,
+ default: '',
},
},
data() {
diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
index fa929381744..c5092d8e04d 100644
--- a/app/assets/javascripts/ide/components/repo_commit_section.vue
+++ b/app/assets/javascripts/ide/components/repo_commit_section.vue
@@ -3,13 +3,10 @@ import { mapState, mapActions, mapGetters } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
-import LoadingButton from '~/vue_shared/components/loading_button.vue';
import CommitFilesList from './commit_sidebar/list.vue';
import EmptyState from './commit_sidebar/empty_state.vue';
-import CommitMessageField from './commit_sidebar/message_field.vue';
-import SuccessMessage from './commit_sidebar/success_message.vue';
import * as consts from '../stores/modules/commit/constants';
-import Actions from './commit_sidebar/actions.vue';
+import { activityBarViews } from '../constants';
export default {
components: {
@@ -17,42 +14,50 @@ export default {
Icon,
CommitFilesList,
EmptyState,
- SuccessMessage,
- Actions,
- LoadingButton,
- CommitMessageField,
},
directives: {
tooltip,
},
- props: {
- noChangesStateSvgPath: {
- type: String,
- required: true,
- },
- committedStateSvgPath: {
- type: String,
- required: true,
- },
- },
computed: {
+ ...mapState([
+ 'changedFiles',
+ 'stagedFiles',
+ 'rightPanelCollapsed',
+ 'lastCommitMsg',
+ 'unusedSeal',
+ ]),
+ ...mapState('commit', ['commitMessage', 'submitCommitLoading']),
+ ...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges']),
+ ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']),
showStageUnstageArea() {
return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal);
},
- someUncommitedChanges() {
- return !!(this.changedFiles.length || this.stagedFiles.length);
+ },
+ watch: {
+ hasChanges() {
+ if (!this.hasChanges) {
+ this.updateActivityBarView(activityBarViews.edit);
+ }
},
- ...mapState(['changedFiles', 'stagedFiles', 'rightPanelCollapsed', 'lastCommitMsg', 'unusedSeal']),
- ...mapState('commit', ['commitMessage', 'submitCommitLoading']),
- ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']),
+ },
+ mounted() {
+ if (this.lastOpenedFile) {
+ this.openPendingTab({
+ file: this.lastOpenedFile,
+ })
+ .then(changeViewer => {
+ if (changeViewer) {
+ this.updateViewer('diff');
+ }
+ })
+ .catch(e => {
+ throw e;
+ });
+ }
},
methods: {
- ...mapActions('commit', [
- 'updateCommitMessage',
- 'discardDraft',
- 'commitChanges',
- 'updateCommitAction',
- ]),
+ ...mapActions(['openPendingTab', 'updateViewer', 'updateActivityBarView']),
+ ...mapActions('commit', ['commitChanges', 'updateCommitAction']),
forceCreateNewBranch() {
return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commitChanges());
},
@@ -80,6 +85,7 @@ export default {
v-if="showStageUnstageArea"
>
<commit-files-list
+ class="is-first"
icon-name="unstaged"
:title="__('Unstaged')"
:file-list="changedFiles"
@@ -94,49 +100,11 @@ export default {
action="unstageAllChanges"
:action-btn-text="__('Unstage all')"
item-action-component="unstage-button"
- :show-toggle="false"
:staged-list="true"
/>
</template>
<empty-state
v-if="unusedSeal"
- :no-changes-state-svg-path="noChangesStateSvgPath"
/>
- <div
- class="multi-file-commit-panel-bottom"
- >
- <form
- class="form-horizontal multi-file-commit-form"
- @submit.prevent.stop="commitChanges"
- v-if="!rightPanelCollapsed"
- >
- <success-message
- v-if="lastCommitMsg && !someUncommitedChanges"
- :committed-state-svg-path="committedStateSvgPath"
- />
- <commit-message-field
- :text="commitMessage"
- @input="updateCommitMessage"
- />
- <div class="clearfix prepend-top-15">
- <actions />
- <loading-button
- :loading="submitCommitLoading"
- :disabled="commitButtonDisabled"
- container-class="btn btn-success btn-sm pull-left"
- :label="__('Commit')"
- @click="commitChanges"
- />
- <button
- v-if="!discardDraftButtonDisabled"
- type="button"
- class="btn btn-default btn-sm pull-right"
- @click="discardDraft"
- >
- {{ __('Discard draft') }}
- </button>
- </div>
- </form>
- </div>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index 3a04cdd8e46..f8678b602ac 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -3,6 +3,7 @@
import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '~/flash';
import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
+import { activityBarViews, viewerTypes } from '../constants';
import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor';
import IdeFileButtons from './ide_file_buttons.vue';
@@ -19,8 +20,14 @@ export default {
},
},
computed: {
- ...mapState(['rightPanelCollapsed', 'viewer', 'delayViewerUpdated', 'panelResizing']),
- ...mapGetters(['currentMergeRequest', 'getStagedFile']),
+ ...mapState(['rightPanelCollapsed', 'viewer', 'panelResizing', 'currentActivityView']),
+ ...mapGetters([
+ 'currentMergeRequest',
+ 'getStagedFile',
+ 'isEditModeActive',
+ 'isCommitModeActive',
+ 'isReviewModeActive',
+ ]),
shouldHideEditor() {
return this.file && this.file.binary && !this.file.content;
},
@@ -40,6 +47,21 @@ export default {
// Compare key to allow for files opened in review mode to be cached differently
if (newVal.key !== this.file.key) {
this.initMonaco();
+
+ if (this.currentActivityView !== activityBarViews.edit) {
+ this.setFileViewMode({
+ file: this.file,
+ viewMode: 'edit',
+ });
+ }
+ }
+ },
+ currentActivityView() {
+ if (this.currentActivityView !== activityBarViews.edit) {
+ this.setFileViewMode({
+ file: this.file,
+ viewMode: 'edit',
+ });
}
},
rightPanelCollapsed() {
@@ -77,7 +99,6 @@ export default {
'setFileViewMode',
'setFileEOL',
'updateViewer',
- 'updateDelayViewerUpdated',
]),
initMonaco() {
if (this.shouldHideEditor) return;
@@ -89,14 +110,6 @@ export default {
baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '',
})
.then(() => {
- const viewerPromise = this.delayViewerUpdated
- ? this.updateViewer(this.file.pending ? 'diff' : 'editor')
- : Promise.resolve();
-
- return viewerPromise;
- })
- .then(() => {
- this.updateDelayViewerUpdated(false);
this.createEditorInstance();
})
.catch(err => {
@@ -108,10 +121,10 @@ export default {
this.editor.dispose();
this.$nextTick(() => {
- if (this.viewer === 'editor') {
+ if (this.viewer === viewerTypes.edit) {
this.editor.createInstance(this.$refs.editor);
} else {
- this.editor.createDiffInstance(this.$refs.editor);
+ this.editor.createDiffInstance(this.$refs.editor, !this.isReviewModeActive);
}
this.setupEditor();
@@ -127,7 +140,7 @@ export default {
this.file.staged && this.file.key.indexOf('unstaged-') === 0 ? head : null,
);
- if (this.viewer === 'mrdiff') {
+ if (this.viewer === viewerTypes.mr && this.file.mrChange) {
this.editor.attachMergeRequestModel(this.model);
} else {
this.editor.attachModel(this.model);
@@ -168,6 +181,7 @@ export default {
});
},
},
+ viewerTypes,
};
</script>
@@ -176,16 +190,17 @@ export default {
id="ide"
class="blob-viewer-container blob-editor-container"
>
- <div class="ide-mode-tabs clearfix">
+ <div class="ide-mode-tabs clearfix" >
<ul
class="nav-links pull-left"
- v-if="!shouldHideEditor">
+ v-if="!shouldHideEditor && isEditModeActive"
+ >
<li :class="editTabCSS">
<a
href="javascript:void(0);"
role="button"
@click.prevent="setFileViewMode({ file, viewMode: 'edit' })">
- <template v-if="viewer === 'editor'">
+ <template v-if="viewer === $options.viewerTypes.edit">
{{ __('Edit') }}
</template>
<template v-else>
@@ -212,6 +227,9 @@ export default {
v-show="!shouldHideEditor && file.viewMode === 'edit'"
ref="editor"
class="multi-file-editor-holder"
+ :class="{
+ 'is-readonly': isCommitModeActive,
+ }"
>
</div>
<content-viewer
diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue
index 89c5ce70dd3..14946f8c9fa 100644
--- a/app/assets/javascripts/ide/components/repo_file.vue
+++ b/app/assets/javascripts/ide/components/repo_file.vue
@@ -34,6 +34,11 @@ export default {
type: Number,
required: true,
},
+ disableActionDropdown: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
...mapGetters([
@@ -99,16 +104,14 @@ export default {
}
},
methods: {
- ...mapActions(['toggleTreeOpen', 'updateDelayViewerUpdated']),
+ ...mapActions(['toggleTreeOpen']),
clickFile() {
// Manual Action if a tree is selected/opened
if (this.isTree && this.$router.currentRoute.path === `/project${this.file.url}`) {
this.toggleTreeOpen(this.file.path);
}
- return this.updateDelayViewerUpdated(true).then(() => {
- router.push(`/project${this.file.url}`);
- });
+ router.push(`/project${this.file.url}`);
},
},
};
@@ -170,7 +173,7 @@ export default {
/>
</span>
<new-dropdown
- v-if="isTree"
+ v-if="isTree && !disableActionDropdown"
:project-id="file.projectId"
:branch="file.branchId"
:path="file.path"
diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue
index a3ee3184c19..fb26b973236 100644
--- a/app/assets/javascripts/ide/components/repo_tab.vue
+++ b/app/assets/javascripts/ide/components/repo_tab.vue
@@ -32,6 +32,8 @@ export default {
return `Close ${this.tab.name}`;
},
showChangedIcon() {
+ if (this.tab.pending) return true;
+
return this.fileHasChanged ? !this.tabMouseOver : false;
},
fileHasChanged() {
@@ -66,15 +68,32 @@ export default {
<template>
<li
+ :class="{
+ active: tab.active
+ }"
@click="clickFile(tab)"
@mouseover="mouseOverTab"
@mouseout="mouseOutTab"
>
+ <div
+ class="multi-file-tab"
+ :title="tab.url"
+ >
+ <file-icon
+ :file-name="tab.name"
+ :size="16"
+ />
+ {{ tab.name }}
+ <file-status-icon
+ :file="tab"
+ />
+ </div>
<button
type="button"
class="multi-file-tab-close"
@click.stop.prevent="closeFile(tab)"
:aria-label="closeLabel"
+ :disabled="tab.pending"
>
<icon
v-if="!showChangedIcon"
@@ -87,22 +106,5 @@ export default {
:force-modified-icon="true"
/>
</button>
-
- <div
- class="multi-file-tab"
- :class="{
- active: tab.active
- }"
- :title="tab.url"
- >
- <file-icon
- :file-name="tab.name"
- :size="16"
- />
- {{ tab.name }}
- <file-status-icon
- :file="tab"
- />
- </div>
</li>
</template>
diff --git a/app/assets/javascripts/ide/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue
index 7bd646ba9b0..99e51097e12 100644
--- a/app/assets/javascripts/ide/components/repo_tabs.vue
+++ b/app/assets/javascripts/ide/components/repo_tabs.vue
@@ -32,16 +32,6 @@ export default {
default: '',
},
},
- data() {
- return {
- showShadow: false,
- };
- },
- updated() {
- if (!this.$refs.tabsScroller) return;
-
- this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
- },
methods: {
...mapActions(['updateViewer', 'removePendingTab']),
openFileViewer(viewer) {
@@ -71,12 +61,5 @@ export default {
:tab="tab"
/>
</ul>
- <editor-mode
- :viewer="viewer"
- :show-shadow="showShadow"
- :has-changes="hasChanges"
- :merge-request-id="mergeRequestId"
- @click="openFileViewer"
- />
</div>
</template>
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
index b06da9f95d1..48d4cc43198 100644
--- a/app/assets/javascripts/ide/constants.js
+++ b/app/assets/javascripts/ide/constants.js
@@ -3,6 +3,22 @@ export const MAX_FILE_FINDER_RESULTS = 40;
export const FILE_FINDER_ROW_HEIGHT = 55;
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;
+
+export const activityBarViews = {
+ edit: 'ide-tree',
+ commit: 'commit-section',
+ review: 'ide-review',
+};
+
+export const viewerTypes = {
+ mr: 'mrdiff',
+ edit: 'editor',
+ diff: 'diff',
+};
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index 4a0a303d5a6..adca85dc65b 100644
--- a/app/assets/javascripts/ide/ide_router.js
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -2,6 +2,7 @@ import Vue from 'vue';
import VueRouter from 'vue-router';
import flash from '~/flash';
import store from './stores';
+import { activityBarViews } from './constants';
Vue.use(VueRouter);
@@ -63,6 +64,8 @@ router.beforeEach((to, from, next) => {
const fullProjectId = `${to.params.namespace}/${to.params.project}`;
if (to.params.branch) {
+ store.dispatch('setCurrentBranchId', to.params.branch);
+
store.dispatch('getBranchData', {
projectId: fullProjectId,
branchId: to.params.branch,
@@ -99,14 +102,14 @@ router.beforeEach((to, from, next) => {
throw e;
});
} else if (to.params.mrid) {
- store.dispatch('updateViewer', 'mrdiff');
-
store
.dispatch('getMergeRequestData', {
projectId: fullProjectId,
mergeRequestId: to.params.mrid,
})
.then(mr => {
+ store.dispatch('updateActivityBarView', activityBarViews.review);
+
store.dispatch('getBranchData', {
projectId: fullProjectId,
branchId: mr.source_branch,
diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js
index cbfb3dc54f2..c5835cd3b06 100644
--- a/app/assets/javascripts/ide/index.js
+++ b/app/assets/javascripts/ide/index.js
@@ -4,7 +4,9 @@ import ide from './components/ide.vue';
import store from './stores';
import router from './ide_router';
-function initIde(el) {
+Vue.use(Translate);
+
+export function initIde(el) {
if (!el) return null;
return new Vue({
@@ -14,20 +16,25 @@ function initIde(el) {
components: {
ide,
},
- render(createElement) {
- return createElement('ide', {
- props: {
- emptyStateSvgPath: el.dataset.emptyStateSvgPath,
- noChangesStateSvgPath: el.dataset.noChangesStateSvgPath,
- committedStateSvgPath: el.dataset.committedStateSvgPath,
- },
+ created() {
+ this.$store.dispatch('setEmptyStateSvgs', {
+ emptyStateSvgPath: el.dataset.emptyStateSvgPath,
+ noChangesStateSvgPath: el.dataset.noChangesStateSvgPath,
+ committedStateSvgPath: el.dataset.committedStateSvgPath,
});
},
+ render(createElement) {
+ return createElement('ide');
+ },
});
}
-const ideElement = document.getElementById('ide');
-
-Vue.use(Translate);
-
-initIde(ideElement);
+// tell webpack to load assets from origin so that web workers don't break
+export function resetServiceWorkersPublicPath() {
+ // __webpack_public_path__ is a global variable that can be used to adjust
+ // the webpack publicPath setting at runtime.
+ // see: https://webpack.js.org/guides/public-path/
+ const relativeRootPath = (gon && gon.relative_url_root) || '';
+ const webpackAssetPath = `${relativeRootPath}/assets/webpack/`;
+ __webpack_public_path__ = webpackAssetPath; // eslint-disable-line camelcase
+}
diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js
index 016dcda1fa1..b1e43a1e38c 100644
--- a/app/assets/javascripts/ide/lib/common/model.js
+++ b/app/assets/javascripts/ide/lib/common/model.js
@@ -14,12 +14,12 @@ export default class Model {
(this.originalModel = this.monaco.editor.createModel(
head ? head.content : this.file.raw,
undefined,
- new this.monaco.Uri(null, null, `original/${this.file.key}`),
+ new this.monaco.Uri(null, null, `original/${this.path}`),
)),
(this.model = this.monaco.editor.createModel(
this.content,
undefined,
- new this.monaco.Uri(null, null, this.file.key),
+ new this.monaco.Uri(null, null, this.path),
)),
);
if (this.file.mrChange) {
@@ -27,7 +27,7 @@ export default class Model {
(this.baseModel = this.monaco.editor.createModel(
this.file.baseRaw,
undefined,
- new this.monaco.Uri(null, null, `target/${this.file.path}`),
+ new this.monaco.Uri(null, null, `target/${this.path}`),
)),
);
}
diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
index b65d9c68a0b..9c3bb9cc17d 100644
--- a/app/assets/javascripts/ide/lib/editor.js
+++ b/app/assets/javascripts/ide/lib/editor.js
@@ -61,19 +61,19 @@ export default class Editor {
}
}
- createDiffInstance(domElement) {
+ createDiffInstance(domElement, readOnly = true) {
if (!this.instance) {
clearDomElement(domElement);
this.disposable.add(
(this.instance = this.monaco.editor.createDiffEditor(domElement, {
...defaultEditorOptions,
- readOnly: true,
quickSuggestions: false,
occurrencesHighlight: false,
- renderLineHighlight: 'none',
- hideCursorInOverviewRuler: true,
renderSideBySide: Editor.renderSideBySide(domElement),
+ readOnly,
+ renderLineHighlight: readOnly ? 'all' : 'none',
+ hideCursorInOverviewRuler: !readOnly,
})),
);
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index 7358dd9ef92..1a98b42761e 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -123,6 +123,8 @@ export const scrollToTab = () => {
};
export const stageAllChanges = ({ state, commit }) => {
+ commit(types.SET_LAST_COMMIT_MSG, '');
+
state.changedFiles.forEach(file => commit(types.STAGE_CHANGE, file.path));
};
@@ -138,6 +140,18 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
commit(types.UPDATE_DELAY_VIEWER_CHANGE, delay);
};
+export const updateActivityBarView = ({ commit }, view) => {
+ commit(types.UPDATE_ACTIVITY_BAR_VIEW, view);
+};
+
+export const setEmptyStateSvgs = ({ commit }, svgs) => {
+ commit(types.SET_EMPTY_STATE_SVGS, svgs);
+};
+
+export const setCurrentBranchId = ({ commit }, currentBranchId) => {
+ commit(types.SET_CURRENT_BRANCH, currentBranchId);
+};
+
export const updateTempFlagForEntry = ({ commit, dispatch, state }, { file, tempFile }) => {
commit(types.UPDATE_TEMP_FLAG, { path: file.path, tempFile });
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index 861830badee..b6baa693104 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -5,6 +5,7 @@ import service from '../../services';
import * as types from '../mutation_types';
import router from '../../ide_router';
import { setPageTitle } from '../utils';
+import { viewerTypes } from '../../constants';
export const closeFile = ({ commit, state, dispatch }, file) => {
const path = file.path;
@@ -23,13 +24,12 @@ export const closeFile = ({ commit, state, dispatch }, file) => {
const nextFileToOpen = state.openFiles[nextIndexToOpen];
if (nextFileToOpen.pending) {
- dispatch('updateViewer', 'diff');
+ dispatch('updateViewer', viewerTypes.diff);
dispatch('openPendingTab', {
file: nextFileToOpen,
keyPrefix: nextFileToOpen.staged ? 'staged' : 'unstaged',
});
} else {
- dispatch('updateDelayViewerUpdated', true);
router.push(`/project${nextFileToOpen.url}`);
}
} else if (!state.openFiles.length) {
@@ -184,6 +184,7 @@ export const stageChange = ({ commit, state }, path) => {
const stagedFile = state.stagedFiles.find(f => f.path === path);
commit(types.STAGE_CHANGE, path);
+ commit(types.SET_LAST_COMMIT_MSG, '');
if (stagedFile) {
eventHub.$emit(`editor.update.model.new.content.staged-${stagedFile.key}`, stagedFile.content);
@@ -195,9 +196,9 @@ export const unstageChange = ({ commit }, path) => {
};
export const openPendingTab = ({ commit, getters, dispatch, state }, { file, keyPrefix }) => {
- if (getters.activeFile && getters.activeFile === file && state.viewer === 'diff') {
- return false;
- }
+ if (getters.activeFile && getters.activeFile.key === `${keyPrefix}-${file.key}`) return false;
+
+ state.openFiles.forEach(f => eventHub.$emit(`editor.update.model.dispose.${f.key}`));
commit(types.ADD_PENDING_TAB, { file, keyPrefix });
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
index 4eb23b2ee0f..eff9bc03651 100644
--- a/app/assets/javascripts/ide/stores/actions/project.js
+++ b/app/assets/javascripts/ide/stores/actions/project.js
@@ -55,7 +55,6 @@ export const getBranchData = (
branch: data,
});
commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
- commit(types.SET_CURRENT_BRANCH, branchId);
resolve(data);
})
.catch(() => {
@@ -73,3 +72,26 @@ export const getBranchData = (
resolve(state.projects[`${projectId}`].branches[branchId]);
}
});
+
+export const refreshLastCommitData = (
+ { commit, state, dispatch },
+ { projectId, branchId } = {},
+) => service
+ .getBranchData(projectId, branchId)
+ .then(({ data }) => {
+ commit(types.SET_BRANCH_COMMIT, {
+ projectId,
+ branchId,
+ commit: data.commit,
+ });
+ })
+ .catch(() => {
+ flash(
+ 'Error loading last commit.',
+ 'alert',
+ document,
+ null,
+ false,
+ true,
+ );
+ });
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index a93d29fd865..b239a605371 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -1,5 +1,5 @@
-import { __ } from '~/locale';
import { getChangesCountForFiles, filePathMatches } from './utils';
+import { activityBarViews } from '../constants';
export const activeFile = state => state.openFiles.find(file => file.active) || null;
@@ -31,15 +31,12 @@ export const currentMergeRequest = state => {
return null;
};
-// eslint-disable-next-line no-confusing-arrow
-export const collapseButtonIcon = state =>
- state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
+export const currentProject = state => state.projects[state.currentProjectId];
-export const hasChanges = state => !!state.changedFiles.length || !!state.stagedFiles.length;
+export const currentTree = state =>
+ state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
-// eslint-disable-next-line no-confusing-arrow
-export const collapseButtonTooltip = state =>
- state.rightPanelCollapsed ? __('Expand sidebar') : __('Collapse sidebar');
+export const hasChanges = state => !!state.changedFiles.length || !!state.stagedFiles.length;
export const hasMergeRequest = state => !!state.currentMergeRequestId;
@@ -59,6 +56,16 @@ export const allBlobs = state =>
export const getChangedFile = state => path => state.changedFiles.find(f => f.path === path);
export const getStagedFile = state => path => state.stagedFiles.find(f => f.path === path);
+export const lastOpenedFile = state =>
+ [...state.changedFiles, ...state.stagedFiles].sort((a, b) => b.lastOpenedAt - a.lastOpenedAt)[0];
+
+export const isEditModeActive = state => state.currentActivityView === activityBarViews.edit;
+export const isCommitModeActive = state => state.currentActivityView === activityBarViews.commit;
+export const isReviewModeActive = state => state.currentActivityView === activityBarViews.review;
+
+export const someUncommitedChanges = state =>
+ !!(state.changedFiles.length || state.stagedFiles.length);
+
export const getChangesInFolder = state => path => {
const changedFilesCount = state.changedFiles.filter(f => filePathMatches(f, path)).length;
const stagedFilesCount = state.stagedFiles.filter(
@@ -74,5 +81,11 @@ export const getUnstagedFilesCountForPath = state => path =>
export const getStagedFilesCountForPath = state => path =>
getChangesCountForFiles(state.stagedFiles, path);
+export const lastCommit = (state, getters) => {
+ const branch = getters.currentProject && getters.currentProject.branches[state.currentBranchId];
+
+ return branch ? branch.commit : null;
+};
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index 4fbc97d053e..b85246b2502 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -8,6 +8,7 @@ import router from '../../../ide_router';
import service from '../../../services';
import * as types from './mutation_types';
import * as consts from './constants';
+import { activityBarViews } from '../../../constants';
import eventHub from '../../../eventhub';
export const updateCommitMessage = ({ commit }, message) => {
@@ -75,7 +76,7 @@ export const checkCommitStatus = ({ rootState }) =>
export const updateFilesAfterCommit = (
{ commit, dispatch, state, rootState, rootGetters },
- { data, branch },
+ { data },
) => {
const selectedProject = rootState.projects[rootState.currentProjectId];
const lastCommit = {
@@ -126,15 +127,9 @@ export const updateFilesAfterCommit = (
changed: !!changedFile,
});
});
-
- if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH && rootGetters.activeFile) {
- router.push(
- `/project/${rootState.currentProjectId}/blob/${branch}/${rootGetters.activeFile.path}`,
- );
- }
};
-export const commitChanges = ({ commit, state, getters, dispatch, rootState }) => {
+export const commitChanges = ({ commit, state, getters, dispatch, rootState, rootGetters }) => {
const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH;
const payload = createCommitPayload(getters.branchName, newBranch, state, rootState);
const getCommitStatus = newBranch ? Promise.resolve(false) : dispatch('checkCommitStatus');
@@ -187,7 +182,39 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState }) =
commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true });
}, 5000);
})
- .then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH));
+ .then(() => {
+ if (rootGetters.lastOpenedFile) {
+ dispatch(
+ 'openPendingTab',
+ {
+ file: rootGetters.lastOpenedFile,
+ },
+ { root: true },
+ )
+ .then(changeViewer => {
+ if (changeViewer) {
+ dispatch('updateViewer', 'diff', { root: true });
+ }
+ })
+ .catch(e => {
+ throw e;
+ });
+ } else {
+ dispatch('updateActivityBarView', activityBarViews.edit, { root: true });
+ dispatch('updateViewer', 'editor', { root: true });
+
+ router.push(
+ `/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 }));
})
.catch(err => {
let errMsg = __('Error committing changes. Please try again.');
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index 87b39379338..a3fb3232f1d 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -5,6 +5,7 @@ export const SET_LAST_COMMIT_MSG = 'SET_LAST_COMMIT_MSG';
export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED';
export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED';
export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS';
+export const SET_EMPTY_STATE_SVGS = 'SET_EMPTY_STATE_SVGS';
// Project Mutation Types
export const SET_PROJECT = 'SET_PROJECT';
@@ -19,6 +20,7 @@ export const SET_MERGE_REQUEST_VERSIONS = 'SET_MERGE_REQUEST_VERSIONS';
// Branch Mutation Types
export const SET_BRANCH = 'SET_BRANCH';
+export const SET_BRANCH_COMMIT = 'SET_BRANCH_COMMIT';
export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN';
@@ -59,6 +61,7 @@ export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT';
export const ADD_PENDING_TAB = 'ADD_PENDING_TAB';
export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB';
+export const UPDATE_ACTIVITY_BAR_VIEW = 'UPDATE_ACTIVITY_BAR_VIEW';
export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG';
export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';
export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL';
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index 539a07116b3..a257e2ef025 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -107,6 +107,21 @@ export default {
delayViewerUpdated,
});
},
+ [types.UPDATE_ACTIVITY_BAR_VIEW](state, currentActivityView) {
+ Object.assign(state, {
+ currentActivityView,
+ });
+ },
+ [types.SET_EMPTY_STATE_SVGS](
+ state,
+ { emptyStateSvgPath, noChangesStateSvgPath, committedStateSvgPath },
+ ) {
+ Object.assign(state, {
+ emptyStateSvgPath,
+ noChangesStateSvgPath,
+ committedStateSvgPath,
+ });
+ },
[types.TOGGLE_FILE_FINDER](state, fileFindVisible) {
Object.assign(state, {
fileFindVisible,
diff --git a/app/assets/javascripts/ide/stores/mutations/branch.js b/app/assets/javascripts/ide/stores/mutations/branch.js
index 2972ba5e38e..e09f88878f4 100644
--- a/app/assets/javascripts/ide/stores/mutations/branch.js
+++ b/app/assets/javascripts/ide/stores/mutations/branch.js
@@ -23,4 +23,9 @@ export default {
workingReference: reference,
});
},
+ [types.SET_BRANCH_COMMIT](state, { projectId, branchId, commit }) {
+ Object.assign(state.projects[projectId].branches[branchId], {
+ commit,
+ });
+ },
};
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index c3041c77199..13f123b6630 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -1,3 +1,4 @@
+/* eslint-disable no-param-reassign */
import * as types from '../mutation_types';
export default {
@@ -169,32 +170,24 @@ export default {
});
},
[types.ADD_PENDING_TAB](state, { file, keyPrefix = 'pending' }) {
- const key = `${keyPrefix}-${file.key}`;
- const pendingTab = state.openFiles.find(f => f.key === key && f.pending);
- let openFiles = state.openFiles.map(f => Object.assign(f, { active: false, opened: false }));
-
- if (!pendingTab) {
- const openFile = openFiles.find(f => f.path === file.path);
-
- openFiles = openFiles.concat(openFile ? null : file).reduce((acc, f) => {
- if (!f) return acc;
-
- if (f.path === file.path) {
- return acc.concat({
- ...f,
- content: file.content,
- active: true,
- pending: true,
- opened: true,
- key,
- });
- }
-
- return acc.concat(f);
- }, []);
- }
-
- Object.assign(state, { openFiles });
+ state.entries[file.path].opened = false;
+ state.entries[file.path].active = false;
+ state.entries[file.path].lastOpenedAt = new Date().getTime();
+ state.openFiles.forEach(f =>
+ Object.assign(f, {
+ opened: false,
+ active: false,
+ }),
+ );
+ state.openFiles = [
+ {
+ ...file,
+ key: `${keyPrefix}-${file.key}`,
+ pending: true,
+ opened: true,
+ active: true,
+ },
+ ];
},
[types.REMOVE_PENDING_TAB](state, file) {
Object.assign(state, {
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index 0976d278559..e7411f16a4f 100644
--- a/app/assets/javascripts/ide/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -1,3 +1,5 @@
+import { activityBarViews, viewerTypes } from '../constants';
+
export default () => ({
currentProjectId: '',
currentBranchId: '',
@@ -16,8 +18,9 @@ export default () => ({
rightPanelCollapsed: false,
panelResizing: false,
entries: {},
- viewer: 'editor',
+ viewer: viewerTypes.edit,
delayViewerUpdated: false,
+ currentActivityView: activityBarViews.edit,
unusedSeal: true,
fileFindVisible: false,
});
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index bc79ff4a542..e0b9766fbee 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -44,6 +44,7 @@ export const dataStructure = () => ({
size: 0,
parentPath: null,
lastOpenedAt: 0,
+ mrChange: null,
});
export const decorateData = entity => {
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 2c80baba10b..247aeb481c6 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -1,22 +1,19 @@
-/* eslint-disable import/first */
/* global $ */
import jQuery from 'jquery';
import Cookies from 'js-cookie';
import svg4everybody from 'svg4everybody';
-// expose common libraries as globals (TODO: remove these)
-window.jQuery = jQuery;
-window.$ = jQuery;
+// bootstrap webpack, common libs, polyfills, and behaviors
+import './webpack';
+import './commons';
+import './behaviors';
// lib/utils
import { handleLocationHash, addSelectOnFocusBehaviour } from './lib/utils/common_utils';
import { localTimeAgo } from './lib/utils/datetime_utility';
import { getLocationHash, visitUrl } from './lib/utils/url_utility';
-// behaviors
-import './behaviors/';
-
// everything else
import loadAwardsHandler from './awards_handler';
import bp from './breakpoints';
@@ -31,9 +28,12 @@ import initLogoAnimation from './logo';
import './milestone_select';
import './projects_dropdown';
import initBreadcrumbs from './breadcrumb';
-
import initDispatcher from './dispatcher';
+// expose jQuery as global (TODO: remove these)
+window.jQuery = jQuery;
+window.$ = jQuery;
+
// inject test utilities if necessary
if (process.env.NODE_ENV !== 'production' && gon && gon.test_env) {
$.fx.off = true;
@@ -52,10 +52,14 @@ document.addEventListener('beforeunload', () => {
});
window.addEventListener('hashchange', handleLocationHash);
-window.addEventListener('load', function onLoad() {
- window.removeEventListener('load', onLoad, false);
- handleLocationHash();
-}, false);
+window.addEventListener(
+ 'load',
+ function onLoad() {
+ window.removeEventListener('load', onLoad, false);
+ handleLocationHash();
+ },
+ false,
+);
gl.lazyLoader = new LazyLoader({
scrollContainer: window,
@@ -89,9 +93,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (bootstrapBreakpoint === 'xs') {
const $rightSidebar = $('aside.right-sidebar, .layout-page');
- $rightSidebar
- .removeClass('right-sidebar-expanded')
- .addClass('right-sidebar-collapsed');
+ $rightSidebar.removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
}
// prevent default action for disabled buttons
@@ -108,7 +110,8 @@ document.addEventListener('DOMContentLoaded', () => {
addSelectOnFocusBehaviour('.js-select-on-focus');
$('.remove-row').on('ajax:success', function removeRowAjaxSuccessCallback() {
- $(this).tooltip('destroy')
+ $(this)
+ .tooltip('destroy')
.closest('li')
.fadeOut();
});
@@ -118,7 +121,9 @@ document.addEventListener('DOMContentLoaded', () => {
});
$('.js-remove-tr').on('ajax:success', function removeTRAjaxSuccessCallback() {
- $(this).closest('tr').fadeOut();
+ $(this)
+ .closest('tr')
+ .fadeOut();
});
// Initialize select2 selects
@@ -155,7 +160,9 @@ document.addEventListener('DOMContentLoaded', () => {
// Form submitter
$('.trigger-submit').on('change', function triggerSubmitCallback() {
- $(this).parents('form').submit();
+ $(this)
+ .parents('form')
+ .submit();
});
localTimeAgo($('abbr.timeago, .js-timeago'), true);
@@ -204,9 +211,15 @@ document.addEventListener('DOMContentLoaded', () => {
$this.toggleClass('active');
if ($this.hasClass('active')) {
- notesHolders.show().find('.hide, .content').show();
+ notesHolders
+ .show()
+ .find('.hide, .content')
+ .show();
} else {
- notesHolders.hide().find('.content').hide();
+ notesHolders
+ .hide()
+ .find('.content')
+ .hide();
}
$(document).trigger('toggle.comments');
@@ -247,9 +260,11 @@ document.addEventListener('DOMContentLoaded', () => {
const flashContainer = document.querySelector('.flash-container');
if (flashContainer && flashContainer.children.length) {
- flashContainer.querySelectorAll('.flash-alert, .flash-notice, .flash-success').forEach((flashEl) => {
- removeFlashClickListener(flashEl);
- });
+ flashContainer
+ .querySelectorAll('.flash-alert, .flash-notice, .flash-success')
+ .forEach(flashEl => {
+ removeFlashClickListener(flashEl);
+ });
}
initDispatcher();
diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js b/app/assets/javascripts/mini_pipeline_graph_dropdown.js
index 01399de4c62..f8257b6abab 100644
--- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js
+++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js
@@ -1,5 +1,3 @@
-/* eslint-disable no-new */
-
import $ from 'jquery';
import flash from './flash';
import axios from './lib/utils/axios_utils';
@@ -62,7 +60,7 @@ export default class MiniPipelineGraph {
*/
renderBuildsList(stageContainer, data) {
const dropdownContainer = stageContainer.parentElement.querySelector(
- `${this.dropdownListSelector} .js-builds-dropdown-list`,
+ `${this.dropdownListSelector} .js-builds-dropdown-list ul`,
);
dropdownContainer.innerHTML = data;
diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue
index f93b1da4f58..de6755e0414 100644
--- a/app/assets/javascripts/monitoring/components/graph.vue
+++ b/app/assets/javascripts/monitoring/components/graph.vue
@@ -81,9 +81,8 @@ export default {
time: new Date(),
value: 0,
},
- currentDataIndex: 0,
currentXCoordinate: 0,
- currentFlagPosition: 0,
+ currentCoordinates: [],
showFlag: false,
showFlagContent: false,
timeSeries: [],
@@ -273,6 +272,9 @@ export default {
:line-style="path.lineStyle"
:line-color="path.lineColor"
:area-color="path.areaColor"
+ :current-coordinates="currentCoordinates[index]"
+ :current-time-series-index="index"
+ :show-dot="showFlagContent"
/>
<graph-deployment
:deployment-data="reducedDeploymentData"
@@ -298,9 +300,9 @@ export default {
:show-flag-content="showFlagContent"
:time-series="timeSeries"
:unit-of-display="unitOfDisplay"
- :current-data-index="currentDataIndex"
:legend-title="legendTitle"
:deployment-flag-data="deploymentFlagData"
+ :current-coordinates="currentCoordinates"
/>
</div>
<graph-legend
diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue
index b8202e25685..8a771107de8 100644
--- a/app/assets/javascripts/monitoring/components/graph/flag.vue
+++ b/app/assets/javascripts/monitoring/components/graph/flag.vue
@@ -47,14 +47,14 @@ export default {
type: String,
required: true,
},
- currentDataIndex: {
- type: Number,
- required: true,
- },
legendTitle: {
type: String,
required: true,
},
+ currentCoordinates: {
+ type: Array,
+ required: true,
+ },
},
computed: {
formatTime() {
@@ -90,10 +90,12 @@ export default {
},
},
methods: {
- seriesMetricValue(series) {
+ seriesMetricValue(seriesIndex, series) {
+ const indexFromCoordinates = this.currentCoordinates[seriesIndex]
+ ? this.currentCoordinates[seriesIndex].currentDataIndex : 0;
const index = this.deploymentFlagData
? this.deploymentFlagData.seriesIndex
- : this.currentDataIndex;
+ : indexFromCoordinates;
const value = series.values[index] && series.values[index].value;
if (isNaN(value)) {
return '-';
@@ -128,7 +130,7 @@ export default {
<h5 v-if="deploymentFlagData">
Deployed
</h5>
- {{ formatDate }} at
+ {{ formatDate }}
<strong>{{ formatTime }}</strong>
</div>
<div
@@ -163,9 +165,11 @@ export default {
:key="index"
>
<track-line :track="series"/>
- <td>{{ series.track }} {{ seriesMetricLabel(index, series) }}</td>
<td>
- <strong>{{ seriesMetricValue(series) }}</strong>
+ {{ series.track }} {{ seriesMetricLabel(index, series) }}
+ </td>
+ <td>
+ <strong>{{ seriesMetricValue(index, series) }}</strong>
</td>
</tr>
</table>
diff --git a/app/assets/javascripts/monitoring/components/graph/path.vue b/app/assets/javascripts/monitoring/components/graph/path.vue
index 881560124a5..52f8aa2ee3f 100644
--- a/app/assets/javascripts/monitoring/components/graph/path.vue
+++ b/app/assets/javascripts/monitoring/components/graph/path.vue
@@ -22,6 +22,15 @@ export default {
type: String,
required: true,
},
+ currentCoordinates: {
+ type: Object,
+ required: false,
+ default: () => ({ currentX: 0, currentY: 0 }),
+ },
+ showDot: {
+ type: Boolean,
+ required: true,
+ },
},
computed: {
strokeDashArray() {
@@ -33,12 +42,20 @@ export default {
};
</script>
<template>
- <g>
+ <g transform="translate(-5, 20)">
+ <circle
+ class="circle-path"
+ :cx="currentCoordinates.currentX"
+ :cy="currentCoordinates.currentY"
+ :fill="lineColor"
+ :stroke="lineColor"
+ r="3"
+ v-if="showDot"
+ />
<path
class="metric-area"
:d="generatedAreaPath"
:fill="areaColor"
- transform="translate(-5, 20)"
/>
<path
class="metric-line"
@@ -47,7 +64,6 @@ export default {
fill="none"
stroke-width="1"
:stroke-dasharray="strokeDashArray"
- transform="translate(-5, 20)"
/>
</g>
</template>
diff --git a/app/assets/javascripts/monitoring/components/graph/track_line.vue b/app/assets/javascripts/monitoring/components/graph/track_line.vue
index 79b322e2e42..18be65fd1ef 100644
--- a/app/assets/javascripts/monitoring/components/graph/track_line.vue
+++ b/app/assets/javascripts/monitoring/components/graph/track_line.vue
@@ -19,16 +19,16 @@ export default {
<template>
<td>
<svg
- width="15"
- height="6">
+ width="16"
+ height="8">
<line
:stroke-dasharray="stylizedLine"
:stroke="track.lineColor"
stroke-width="4"
:x1="0"
- :x2="15"
- :y1="2"
- :y2="2"
+ :x2="16"
+ :y1="4"
+ :y2="4"
/>
</svg>
</td>
diff --git a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
index 6cc67ba57ee..4f23814ff3e 100644
--- a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
+++ b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
@@ -52,14 +52,22 @@ const mixins = {
positionFlag() {
const timeSeries = this.timeSeries[0];
const hoveredDataIndex = bisectDate(timeSeries.values, this.hoverData.hoveredDate, 1);
+
this.currentData = timeSeries.values[hoveredDataIndex];
- this.currentDataIndex = hoveredDataIndex;
this.currentXCoordinate = Math.floor(timeSeries.timeSeriesScaleX(this.currentData.time));
- if (this.currentXCoordinate > (this.graphWidth - 200)) {
- this.currentFlagPosition = this.currentXCoordinate - 103;
- } else {
- this.currentFlagPosition = this.currentXCoordinate;
- }
+
+ this.currentCoordinates = this.timeSeries.map((series) => {
+ const currentDataIndex = bisectDate(series.values, this.hoverData.hoveredDate, 1);
+ const currentData = series.values[currentDataIndex];
+ const currentX = Math.floor(series.timeSeriesScaleX(currentData.time));
+ const currentY = Math.floor(series.timeSeriesScaleY(currentData.value));
+
+ return {
+ currentX,
+ currentY,
+ currentDataIndex,
+ };
+ });
if (this.hoverData.currentDeployXPos) {
this.showFlag = false;
diff --git a/app/assets/javascripts/monitoring/utils/date_time_formatters.js b/app/assets/javascripts/monitoring/utils/date_time_formatters.js
index f3c9acdd93e..d88c13609dc 100644
--- a/app/assets/javascripts/monitoring/utils/date_time_formatters.js
+++ b/app/assets/javascripts/monitoring/utils/date_time_formatters.js
@@ -14,7 +14,7 @@ const d3 = {
timeYear,
};
-export const dateFormat = d3.time('%a, %b %-d');
+export const dateFormat = d3.time('%d %b %Y, ');
export const timeFormat = d3.time('%-I:%M%p');
export const dateFormatWithName = d3.time('%a, %b %-d');
export const bisectDate = d3.bisector(d => d.time).left;
diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
index 8a93c7e6bae..4d3f1f1a7cc 100644
--- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js
+++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
@@ -123,6 +123,7 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
linePath: lineFunction(timeSeries.values),
areaPath: areaFunction(timeSeries.values),
timeSeriesScaleX,
+ timeSeriesScaleY,
values: timeSeries.values,
max: maximumValue,
average: accum / timeSeries.values.length,
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 396a675b4ac..48642c4a086 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -99,10 +99,6 @@ export default {
'js-note-target-reopen': !this.isOpen,
};
},
- supportQuickActions() {
- // Disable quick actions support for Epics
- return this.noteableType !== constants.EPIC_NOTEABLE_TYPE;
- },
markdownDocsPath() {
return this.getNotesData.markdownDocsPath;
},
@@ -359,7 +355,7 @@ Please check your network connection and try again.`;
name="note[note]"
class="note-textarea js-vue-comment-form
js-gfm-input js-autosize markdown-area js-vue-textarea"
- :data-supports-quick-actions="supportQuickActions"
+ data-supports-quick-actions="true"
aria-label="Description"
v-model="note"
ref="textarea"
diff --git a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
index 04a0d8117cc..d3b2656743d 100644
--- a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
@@ -1,6 +1,10 @@
+import initSettingsPanels from '~/settings_panels';
import AjaxVariableList from '~/ci_variable_list/ajax_variable_list';
document.addEventListener('DOMContentLoaded', () => {
+ // Initialize expandable settings panels
+ initSettingsPanels();
+
const variableListEl = document.querySelector('.js-ci-variable-list-section');
// eslint-disable-next-line no-new
new AjaxVariableList({
diff --git a/app/assets/javascripts/pages/ide/index.js b/app/assets/javascripts/pages/ide/index.js
new file mode 100644
index 00000000000..efadf6967aa
--- /dev/null
+++ b/app/assets/javascripts/pages/ide/index.js
@@ -0,0 +1,9 @@
+import { initIde, resetServiceWorkersPublicPath } from '~/ide/index';
+
+document.addEventListener('DOMContentLoaded', () => {
+ const ideElement = document.getElementById('ide');
+ if (ideElement) {
+ resetServiceWorkersPublicPath();
+ initIde(ideElement);
+ }
+});
diff --git a/app/assets/javascripts/pages/projects/clusters/gcp/login/index.js b/app/assets/javascripts/pages/projects/clusters/gcp/login/index.js
new file mode 100644
index 00000000000..0c2d7d7c96a
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/clusters/gcp/login/index.js
@@ -0,0 +1,3 @@
+import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
+
+gcpSignupOffer();
diff --git a/app/assets/javascripts/pages/projects/clusters/new/index.js b/app/assets/javascripts/pages/projects/clusters/new/index.js
new file mode 100644
index 00000000000..0c2d7d7c96a
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/clusters/new/index.js
@@ -0,0 +1,3 @@
+import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
+
+gcpSignupOffer();
diff --git a/app/assets/javascripts/pages/projects/compare/show/index.js b/app/assets/javascripts/pages/projects/compare/show/index.js
index 2b4fd3c47c0..a626ed2d30b 100644
--- a/app/assets/javascripts/pages/projects/compare/show/index.js
+++ b/app/assets/javascripts/pages/projects/compare/show/index.js
@@ -1,8 +1,10 @@
import Diff from '~/diff';
import initChangesDropdown from '~/init_changes_dropdown';
+import GpgBadges from '~/gpg_badges';
document.addEventListener('DOMContentLoaded', () => {
new Diff(); // eslint-disable-line no-new
const paddingTop = 16;
initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop);
+ GpgBadges.fetch();
});
diff --git a/app/assets/javascripts/pages/projects/pipelines/new/index.js b/app/assets/javascripts/pages/projects/pipelines/new/index.js
index 9aa8945e268..b0b077a5e4c 100644
--- a/app/assets/javascripts/pages/projects/pipelines/new/index.js
+++ b/app/assets/javascripts/pages/projects/pipelines/new/index.js
@@ -1,6 +1,12 @@
import $ from 'jquery';
import NewBranchForm from '~/new_branch_form';
+import setupNativeFormVariableList from '~/ci_variable_list/native_form_variable_list';
document.addEventListener('DOMContentLoaded', () => {
new NewBranchForm($('.js-new-pipeline-form')); // eslint-disable-line no-new
+
+ setupNativeFormVariableList({
+ container: $('.js-ci-variable-list-section'),
+ formField: 'variables_attributes',
+ });
});
diff --git a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue
new file mode 100644
index 00000000000..df21e2f8771
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue
@@ -0,0 +1,77 @@
+<script>
+import _ from 'underscore';
+import GlModal from '~/vue_shared/components/gl_modal.vue';
+import { s__, sprintf } from '~/locale';
+
+export default {
+ components: {
+ GlModal,
+ },
+ props: {
+ deleteWikiUrl: {
+ type: String,
+ required: true,
+ default: '',
+ },
+ pageTitle: {
+ type: String,
+ required: true,
+ default: '',
+ },
+ csrfToken: {
+ type: String,
+ required: true,
+ default: '',
+ },
+ },
+ computed: {
+ message() {
+ return s__('WikiPageConfirmDelete|Are you sure you want to delete this page?');
+ },
+ title() {
+ return sprintf(
+ s__('WikiPageConfirmDelete|Delete page %{pageTitle}?'),
+ {
+ pageTitle: _.escape(this.pageTitle),
+ },
+ false,
+ );
+ },
+ },
+ methods: {
+ onSubmit() {
+ this.$refs.form.submit();
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-modal
+ id="delete-wiki-modal"
+ :header-title-text="title"
+ footer-primary-button-variant="danger"
+ :footer-primary-button-text="s__('WikiPageConfirmDelete|Delete page')"
+ @submit="onSubmit"
+ >
+ {{ message }}
+ <form
+ ref="form"
+ :action="deleteWikiUrl"
+ method="post"
+ class="form-horizontal js-requires-input"
+ >
+ <input
+ ref="method"
+ type="hidden"
+ name="_method"
+ value="delete"
+ />
+ <input
+ type="hidden"
+ name="authenticity_token"
+ :value="csrfToken"
+ />
+ </form>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/pages/projects/wikis/index.js b/app/assets/javascripts/pages/projects/wikis/index.js
index ec01c66ffda..0295653cb29 100644
--- a/app/assets/javascripts/pages/projects/wikis/index.js
+++ b/app/assets/javascripts/pages/projects/wikis/index.js
@@ -1,12 +1,40 @@
import $ from 'jquery';
+import Vue from 'vue';
+import Translate from '~/vue_shared/translate';
+import csrf from '~/lib/utils/csrf';
import Wikis from './wikis';
import ShortcutsWiki from '../../../shortcuts_wiki';
import ZenMode from '../../../zen_mode';
import GLForm from '../../../gl_form';
+import deleteWikiModal from './components/delete_wiki_modal.vue';
document.addEventListener('DOMContentLoaded', () => {
new Wikis(); // eslint-disable-line no-new
new ShortcutsWiki(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new
new GLForm($('.wiki-form'), true); // eslint-disable-line no-new
+
+ const deleteWikiButton = document.getElementById('delete-wiki-button');
+
+ if (deleteWikiButton) {
+ Vue.use(Translate);
+
+ const { deleteWikiUrl, pageTitle } = deleteWikiButton.dataset;
+ const deleteWikiModalEl = document.getElementById('delete-wiki-modal');
+ const deleteModal = new Vue({ // eslint-disable-line
+ el: deleteWikiModalEl,
+ data: {
+ deleteWikiUrl: '',
+ },
+ render(createElement) {
+ return createElement(deleteWikiModal, {
+ props: {
+ pageTitle,
+ deleteWikiUrl,
+ csrfToken: csrf.token,
+ },
+ });
+ },
+ });
+ }
});
diff --git a/app/assets/javascripts/pipelines/components/async_button.vue b/app/assets/javascripts/pipelines/components/async_button.vue
deleted file mode 100644
index 0cdffbde05b..00000000000
--- a/app/assets/javascripts/pipelines/components/async_button.vue
+++ /dev/null
@@ -1,95 +0,0 @@
-<script>
- /* eslint-disable no-alert */
-
- import eventHub from '../event_hub';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
- import icon from '../../vue_shared/components/icon.vue';
- import tooltip from '../../vue_shared/directives/tooltip';
-
- export default {
- directives: {
- tooltip,
- },
- components: {
- loadingIcon,
- icon,
- },
- props: {
- endpoint: {
- type: String,
- required: true,
- },
- title: {
- type: String,
- required: true,
- },
- icon: {
- type: String,
- required: true,
- },
- cssClass: {
- type: String,
- required: true,
- },
- pipelineId: {
- type: Number,
- required: true,
- },
- type: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- isLoading: false,
- };
- },
- computed: {
- buttonClass() {
- return `btn ${this.cssClass}`;
- },
- },
- created() {
- // We're using eventHub to listen to the modal here instead of
- // using props because it would would make the parent components
- // much more complex to keep track of the loading state of each button
- eventHub.$on('postAction', this.setLoading);
- },
- beforeDestroy() {
- eventHub.$off('postAction', this.setLoading);
- },
- methods: {
- onClick() {
- eventHub.$emit('openConfirmationModal', {
- pipelineId: this.pipelineId,
- endpoint: this.endpoint,
- type: this.type,
- });
- },
- setLoading(endpoint) {
- if (endpoint === this.endpoint) {
- this.isLoading = true;
- }
- },
- },
- };
-</script>
-
-<template>
- <button
- v-tooltip
- type="button"
- @click="onClick"
- :class="buttonClass"
- :title="title"
- :aria-label="title"
- data-container="body"
- data-placement="top"
- :disabled="isLoading">
- <icon
- :name="icon"
- />
- <loading-icon v-if="isLoading" />
- </button>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue
index 29ee73a2a6f..fd3491c7fe0 100644
--- a/app/assets/javascripts/pipelines/components/graph/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue
@@ -61,7 +61,7 @@ export default {
methods: {
onClickAction() {
$(this.$el).tooltip('hide');
- eventHub.$emit('graphAction', this.link);
+ eventHub.$emit('postAction', this.link);
this.linkRequested = this.link;
this.isDisabled = true;
},
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 43121dd38f3..4027d26098f 100644
--- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
@@ -87,7 +87,8 @@ export default {
data-toggle="dropdown"
data-container="body"
class="dropdown-menu-toggle build-content"
- :title="tooltipText">
+ :title="tooltipText"
+ >
<job-name-component
:name="job.name"
@@ -104,7 +105,8 @@ export default {
<ul>
<li
v-for="(item, i) in job.jobs"
- :key="i">
+ :key="i"
+ >
<job-component
:job="item"
css-class-job-name="mini-pipeline-graph-dropdown-item"
diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue
index 4fcd4b79f4a..c1f0f051b63 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue
@@ -108,7 +108,7 @@ export default {
<div
v-else
v-tooltip
- class="js-job-component-tooltip"
+ class="js-job-component-tooltip non-details-job-component"
:title="tooltipText"
:class="cssClassJobName"
data-html="true"
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue
index 714aed1333e..41986b827cd 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_table.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue
@@ -1,7 +1,7 @@
<script>
- import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
+ import Modal from '~/vue_shared/components/gl_modal.vue';
import { s__, sprintf } from '~/locale';
- import pipelinesTableRowComponent from './pipelines_table_row.vue';
+ import PipelinesTableRowComponent from './pipelines_table_row.vue';
import eventHub from '../event_hub';
/**
@@ -11,8 +11,8 @@
*/
export default {
components: {
- pipelinesTableRowComponent,
- DeprecatedModal,
+ PipelinesTableRowComponent,
+ Modal,
},
props: {
pipelines: {
@@ -37,30 +37,18 @@
return {
pipelineId: '',
endpoint: '',
- type: '',
};
},
computed: {
modalTitle() {
- return this.type === 'stop' ?
- sprintf(s__('Pipeline|Stop pipeline #%{pipelineId}?'), {
- pipelineId: `'${this.pipelineId}'`,
- }, false) :
- sprintf(s__('Pipeline|Retry pipeline #%{pipelineId}?'), {
- pipelineId: `'${this.pipelineId}'`,
- }, false);
+ return sprintf(s__('Pipeline|Stop pipeline #%{pipelineId}?'), {
+ pipelineId: `${this.pipelineId}`,
+ }, false);
},
modalText() {
- return this.type === 'stop' ?
- sprintf(s__('Pipeline|You’re about to stop pipeline %{pipelineId}.'), {
- pipelineId: `<strong>#${this.pipelineId}</strong>`,
- }, false) :
- sprintf(s__('Pipeline|You’re about to retry pipeline %{pipelineId}.'), {
- pipelineId: `<strong>#${this.pipelineId}</strong>`,
- }, false);
- },
- primaryButtonLabel() {
- return this.type === 'stop' ? s__('Pipeline|Stop pipeline') : s__('Pipeline|Retry pipeline');
+ return sprintf(s__('Pipeline|You’re about to stop pipeline %{pipelineId}.'), {
+ pipelineId: `<strong>#${this.pipelineId}</strong>`,
+ }, false);
},
},
created() {
@@ -73,7 +61,6 @@
setModalData(data) {
this.pipelineId = data.pipelineId;
this.endpoint = data.endpoint;
- this.type = data.type;
},
onSubmit() {
eventHub.$emit('postAction', this.endpoint);
@@ -120,20 +107,16 @@
:auto-devops-help-path="autoDevopsHelpPath"
:view-type="viewType"
/>
- <deprecated-modal
+
+ <modal
id="confirmation-modal"
- :title="modalTitle"
- :text="modalText"
- kind="danger"
- :primary-button-label="primaryButtonLabel"
+ :header-title-text="modalTitle"
+ footer-primary-button-variant="danger"
+ :footer-primary-button-text="s__('Pipeline|Stop pipeline')"
@submit="onSubmit"
>
- <template
- slot="body"
- slot-scope="props"
- >
- <p v-html="props.text"></p>
- </template>
- </deprecated-modal>
+ <span v-html="modalText"></span>
+ </modal>
+
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
index 4cbd67e0372..498a97851fa 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
@@ -1,13 +1,14 @@
<script>
- /* eslint-disable no-param-reassign */
- import asyncButtonComponent from './async_button.vue';
- import pipelinesActionsComponent from './pipelines_actions.vue';
- import pipelinesArtifactsComponent from './pipelines_artifacts.vue';
- import ciBadge from '../../vue_shared/components/ci_badge_link.vue';
- import pipelineStage from './stage.vue';
- import pipelineUrl from './pipeline_url.vue';
- import pipelinesTimeago from './time_ago.vue';
- import commitComponent from '../../vue_shared/components/commit.vue';
+ import eventHub from '../event_hub';
+ import PipelinesActionsComponent from './pipelines_actions.vue';
+ import PipelinesArtifactsComponent from './pipelines_artifacts.vue';
+ import CiBadge from '../../vue_shared/components/ci_badge_link.vue';
+ import PipelineStage from './stage.vue';
+ import PipelineUrl from './pipeline_url.vue';
+ import PipelinesTimeago from './time_ago.vue';
+ 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';
/**
* Pipeline table row.
@@ -16,14 +17,15 @@
*/
export default {
components: {
- asyncButtonComponent,
- pipelinesActionsComponent,
- pipelinesArtifactsComponent,
- commitComponent,
- pipelineStage,
- pipelineUrl,
- ciBadge,
- pipelinesTimeago,
+ PipelinesActionsComponent,
+ PipelinesArtifactsComponent,
+ CommitComponent,
+ PipelineStage,
+ PipelineUrl,
+ CiBadge,
+ PipelinesTimeago,
+ LoadingButton,
+ Icon,
},
props: {
pipeline: {
@@ -44,6 +46,12 @@
required: true,
},
},
+ data() {
+ return {
+ isRetrying: false,
+ isCancelling: false,
+ };
+ },
computed: {
/**
* If provided, returns the commit tag.
@@ -119,8 +127,10 @@
if (this.pipeline.ref) {
return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
if (prop === 'path') {
+ // eslint-disable-next-line no-param-reassign
accumulator.ref_url = this.pipeline.ref[prop];
} else {
+ // eslint-disable-next-line no-param-reassign
accumulator[prop] = this.pipeline.ref[prop];
}
return accumulator;
@@ -216,6 +226,21 @@
return this.viewType === 'child';
},
},
+
+ methods: {
+ handleCancelClick() {
+ this.isCancelling = true;
+
+ eventHub.$emit('openConfirmationModal', {
+ pipelineId: this.pipeline.id,
+ endpoint: this.pipeline.cancel_path,
+ });
+ },
+ handleRetryClick() {
+ this.isRetrying = true;
+ eventHub.$emit('retryPipeline', this.pipeline.retry_path);
+ },
+ },
};
</script>
<template>
@@ -287,7 +312,8 @@
<div
v-if="displayPipelineActions"
- class="table-section section-20 table-button-footer pipeline-actions">
+ class="table-section section-20 table-button-footer pipeline-actions"
+ >
<div class="btn-group table-action-buttons">
<pipelines-actions-component
v-if="pipeline.details.manual_actions.length"
@@ -300,29 +326,27 @@
:artifacts="pipeline.details.artifacts"
/>
- <async-button-component
+ <loading-button
v-if="pipeline.flags.retryable"
- :endpoint="pipeline.retry_path"
- css-class="js-pipelines-retry-button btn-default btn-retry"
- title="Retry"
- icon="repeat"
- :pipeline-id="pipeline.id"
- data-toggle="modal"
- data-target="#confirmation-modal"
- type="retry"
- />
+ @click="handleRetryClick"
+ container-class="js-pipelines-retry-button btn btn-default btn-retry"
+ :loading="isRetrying"
+ :disabled="isRetrying"
+ >
+ <icon name="repeat" />
+ </loading-button>
- <async-button-component
+ <loading-button
v-if="pipeline.flags.cancelable"
- :endpoint="pipeline.cancel_path"
- css-class="js-pipelines-cancel-button btn-remove"
- title="Stop"
- icon="close"
- :pipeline-id="pipeline.id"
+ @click="handleCancelClick"
data-toggle="modal"
data-target="#confirmation-modal"
- type="stop"
- />
+ container-class="js-pipelines-cancel-button btn btn-remove"
+ :loading="isCancelling"
+ :disabled="isCancelling"
+ >
+ <icon name="close" />
+ </loading-button>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue
index 32cf3dba3c3..a65485c05eb 100644
--- a/app/assets/javascripts/pipelines/components/stage.vue
+++ b/app/assets/javascripts/pipelines/components/stage.vue
@@ -1,135 +1,140 @@
<script>
-
- /**
- * Renders each stage of the pipeline mini graph.
- *
- * Given the provided endpoint will make a request to
- * fetch the dropdown data when the stage is clicked.
- *
- * Request is made inside this component to make it reusable between:
- * 1. Pipelines main table
- * 2. Pipelines table in commit and Merge request views
- * 3. Merge request widget
- * 4. Commit widget
- */
-
- import $ from 'jquery';
- import Flash from '../../flash';
- import axios from '../../lib/utils/axios_utils';
- import eventHub from '../event_hub';
- import Icon from '../../vue_shared/components/icon.vue';
- import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
- import tooltip from '../../vue_shared/directives/tooltip';
-
- export default {
- components: {
- LoadingIcon,
- Icon,
+/**
+ * Renders each stage of the pipeline mini graph.
+ *
+ * Given the provided endpoint will make a request to
+ * fetch the dropdown data when the stage is clicked.
+ *
+ * Request is made inside this component to make it reusable between:
+ * 1. Pipelines main table
+ * 2. Pipelines table in commit and Merge request views
+ * 3. Merge request widget
+ * 4. Commit widget
+ */
+
+import $ from 'jquery';
+import { __ } from '../../locale';
+import Flash from '../../flash';
+import axios from '../../lib/utils/axios_utils';
+import eventHub from '../event_hub';
+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';
+
+export default {
+ components: {
+ LoadingIcon,
+ Icon,
+ JobComponent,
+ },
+
+ directives: {
+ tooltip,
+ },
+
+ props: {
+ stage: {
+ type: Object,
+ required: true,
},
- directives: {
- tooltip,
+ updateDropdown: {
+ type: Boolean,
+ required: false,
+ default: false,
},
-
- props: {
- stage: {
- type: Object,
- required: true,
- },
-
- updateDropdown: {
- type: Boolean,
- required: false,
- default: false,
- },
+ },
+
+ data() {
+ return {
+ isLoading: false,
+ dropdownContent: '',
+ };
+ },
+
+ computed: {
+ dropdownClass() {
+ return this.dropdownContent.length > 0
+ ? 'js-builds-dropdown-container'
+ : 'js-builds-dropdown-loading';
},
- data() {
- return {
- isLoading: false,
- dropdownContent: '',
- };
+ triggerButtonClass() {
+ return `ci-status-icon-${this.stage.status.group}`;
},
- computed: {
- dropdownClass() {
- return this.dropdownContent.length > 0 ? 'js-builds-dropdown-container' : 'js-builds-dropdown-loading';
- },
+ borderlessIcon() {
+ return `${this.stage.status.icon}_borderless`;
+ },
+ },
- triggerButtonClass() {
- return `ci-status-icon-${this.stage.status.group}`;
- },
+ watch: {
+ updateDropdown() {
+ if (this.updateDropdown && this.isDropdownOpen() && !this.isLoading) {
+ this.fetchJobs();
+ }
+ },
+ },
+
+ updated() {
+ if (this.dropdownContent.length > 0) {
+ this.stopDropdownClickPropagation();
+ }
+ },
+
+ methods: {
+ onClickStage() {
+ if (!this.isDropdownOpen()) {
+ eventHub.$emit('clickedDropdown');
+ this.isLoading = true;
+ this.fetchJobs();
+ }
+ },
- borderlessIcon() {
- return `${this.stage.status.icon}_borderless`;
- },
+ fetchJobs() {
+ axios
+ .get(this.stage.dropdown_path)
+ .then(({ data }) => {
+ this.dropdownContent = data.latest_statuses;
+ this.isLoading = false;
+ })
+ .catch(() => {
+ this.closeDropdown();
+ this.isLoading = false;
+
+ Flash(__('Something went wrong on our end.'));
+ });
},
- watch: {
- updateDropdown() {
- if (this.updateDropdown &&
- this.isDropdownOpen() &&
- !this.isLoading) {
- this.fetchJobs();
- }
- },
+ /**
+ * When the user right clicks or cmd/ctrl + click in the job name
+ * the dropdown should not be closed and the link should open in another tab,
+ * so we stop propagation of the click event inside the dropdown.
+ *
+ * Since this component is rendered multiple times per page we need to guarantee we only
+ * target the click event of this component.
+ */
+ stopDropdownClickPropagation() {
+ $(
+ '.js-builds-dropdown-list button, .js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item',
+ this.$el,
+ ).on('click', e => {
+ e.stopPropagation();
+ });
},
- updated() {
- if (this.dropdownContent.length > 0) {
- this.stopDropdownClickPropagation();
+ closeDropdown() {
+ if (this.isDropdownOpen()) {
+ $(this.$refs.dropdown).dropdown('toggle');
}
},
- methods: {
- onClickStage() {
- if (!this.isDropdownOpen()) {
- eventHub.$emit('clickedDropdown');
- this.isLoading = true;
- this.fetchJobs();
- }
- },
-
- fetchJobs() {
- axios.get(this.stage.dropdown_path)
- .then(({ data }) => {
- this.dropdownContent = data.html;
- this.isLoading = false;
- })
- .catch(() => {
- this.closeDropdown();
- this.isLoading = false;
-
- Flash('Something went wrong on our end.');
- });
- },
-
- /**
- * When the user right clicks or cmd/ctrl + click in the job name
- * the dropdown should not be closed and the link should open in another tab,
- * so we stop propagation of the click event inside the dropdown.
- *
- * Since this component is rendered multiple times per page we need to guarantee we only
- * target the click event of this component.
- */
- stopDropdownClickPropagation() {
- $(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item'))
- .on('click', (e) => {
- e.stopPropagation();
- });
- },
-
- closeDropdown() {
- if (this.isDropdownOpen()) {
- $(this.$refs.dropdown).dropdown('toggle');
- }
- },
-
- isDropdownOpen() {
- return this.$el.classList.contains('open');
- },
+ isDropdownOpen() {
+ return this.$el.classList.contains('open');
},
- };
+ },
+};
</script>
<template>
@@ -168,7 +173,6 @@
>
<li
- :class="dropdownClass"
class="js-builds-dropdown-list scrollable-menu"
>
@@ -176,8 +180,16 @@
<ul
v-else
- v-html="dropdownContent"
>
+ <li
+ v-for="job in dropdownContent"
+ :key="job.id"
+ >
+ <job-component
+ :job="job"
+ css-class-job-name="mini-pipeline-graph-dropdown-item"
+ />
+ </li>
</ul>
</li>
</ul>
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js
index 6d87f75ae8e..de0faf181e5 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines.js
@@ -53,10 +53,12 @@ export default {
});
eventHub.$on('postAction', this.postAction);
+ eventHub.$on('retryPipeline', this.postAction);
eventHub.$on('clickedDropdown', this.updateTable);
},
beforeDestroy() {
eventHub.$off('postAction', this.postAction);
+ eventHub.$off('retryPipeline', this.postAction);
eventHub.$off('clickedDropdown', this.updateTable);
},
destroyed() {
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index 6584f96130b..04fe7958fe6 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -29,10 +29,10 @@ export default () => {
};
},
created() {
- eventHub.$on('graphAction', this.postAction);
+ eventHub.$on('postAction', this.postAction);
},
beforeDestroy() {
- eventHub.$off('graphAction', this.postAction);
+ eventHub.$off('postAction', this.postAction);
},
methods: {
postAction(action) {
diff --git a/app/assets/javascripts/projects_dropdown/components/app.vue b/app/assets/javascripts/projects_dropdown/components/app.vue
index 34a60dd574b..0bbd8a41753 100644
--- a/app/assets/javascripts/projects_dropdown/components/app.vue
+++ b/app/assets/javascripts/projects_dropdown/components/app.vue
@@ -100,9 +100,10 @@ export default {
fetchSearchedProjects(searchQuery) {
this.searchQuery = searchQuery;
this.toggleLoader(true);
- this.service.getSearchedProjects(this.searchQuery)
+ this.service
+ .getSearchedProjects(this.searchQuery)
.then(res => res.json())
- .then((results) => {
+ .then(results => {
this.toggleSearchProjectsList(true);
this.store.setSearchedProjects(results);
})
diff --git a/app/assets/javascripts/projects_dropdown/service/projects_service.js b/app/assets/javascripts/projects_dropdown/service/projects_service.js
index 7231f520933..ed1c3deead2 100644
--- a/app/assets/javascripts/projects_dropdown/service/projects_service.js
+++ b/app/assets/javascripts/projects_dropdown/service/projects_service.js
@@ -50,7 +50,7 @@ export default class ProjectsService {
} else {
// Check if project is already present in frequents list
// When found, update metadata of it.
- storedFrequentProjects = JSON.parse(storedRawProjects).map((projectItem) => {
+ storedFrequentProjects = JSON.parse(storedRawProjects).map(projectItem => {
if (projectItem.id === project.id) {
matchFound = true;
const diff = Math.abs(project.lastAccessedOn - projectItem.lastAccessedOn) / HOUR_IN_MS;
@@ -104,13 +104,17 @@ export default class ProjectsService {
return [];
}
- if (bp.getBreakpointSize() === 'sm' ||
- bp.getBreakpointSize() === 'xs') {
+ if (bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'xs') {
frequentProjectsCount = FREQUENT_PROJECTS.LIST_COUNT_MOBILE;
}
- const frequentProjects = storedFrequentProjects
- .filter(project => project.frequency >= FREQUENT_PROJECTS.ELIGIBLE_FREQUENCY);
+ const frequentProjects = storedFrequentProjects.filter(
+ project => project.frequency >= FREQUENT_PROJECTS.ELIGIBLE_FREQUENCY,
+ );
+
+ if (!frequentProjects || frequentProjects.length === 0) {
+ return [];
+ }
// Sort all frequent projects in decending order of frequency
// and then by lastAccessedOn with recent most first
diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue
index ee4eb3581f3..a2227b2f554 100644
--- a/app/assets/javascripts/registry/components/table_registry.vue
+++ b/app/assets/javascripts/registry/components/table_registry.vue
@@ -111,7 +111,13 @@
</td>
<td>
- {{ timeFormated(item.createdAt) }}
+ <span
+ v-tooltip
+ :title="tooltipTitle(item.createdAt)"
+ data-placement="bottom"
+ >
+ {{ timeFormated(item.createdAt) }}
+ </span>
</td>
<td class="content">
diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js
index e31e067033f..99c71d6524a 100644
--- a/app/assets/javascripts/shortcuts.js
+++ b/app/assets/javascripts/shortcuts.js
@@ -85,6 +85,7 @@ export default class Shortcuts {
if ($modal.length) {
$modal.modal('toggle');
+ return null;
}
return axios.get(gon.shortcuts_path, {
diff --git a/app/assets/javascripts/user_callout.js b/app/assets/javascripts/user_callout.js
index 97d5cf96bcb..96dfff77859 100644
--- a/app/assets/javascripts/user_callout.js
+++ b/app/assets/javascripts/user_callout.js
@@ -15,7 +15,7 @@ export default class UserCallout {
init() {
if (!this.isCalloutDismissed || this.isCalloutDismissed === 'false') {
- $('.js-close-callout').on('click', e => this.dismissCallout(e));
+ this.userCalloutBody.find('.js-close-callout').on('click', e => this.dismissCallout(e));
}
}
@@ -23,12 +23,15 @@ export default class UserCallout {
const $currentTarget = $(e.currentTarget);
if (this.options.setCalloutPerProject) {
- Cookies.set(this.cookieName, 'true', { expires: 365, path: this.userCalloutBody.data('projectPath') });
+ Cookies.set(this.cookieName, 'true', {
+ expires: 365,
+ path: this.userCalloutBody.data('projectPath'),
+ });
} else {
Cookies.set(this.cookieName, 'true', { expires: 365 });
}
- if ($currentTarget.hasClass('close')) {
+ if ($currentTarget.hasClass('close') || $currentTarget.hasClass('js-close')) {
this.userCalloutBody.remove();
}
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
index 7bef2e97349..1fea231c816 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
@@ -109,12 +109,12 @@ export default {
rel="noopener noreferrer nofollow"
class="deploy-link js-deploy-url"
>
+ {{ deployment.external_url_formatted }}
<i
class="fa fa-external-link"
aria-hidden="true"
>
</i>
- {{ deployment.external_url_formatted }}
</a>
</template>
<span
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue
deleted file mode 100644
index f0298f732ea..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue
+++ /dev/null
@@ -1,20 +0,0 @@
-<script>
- export default {
- name: 'MRWidgetMaintainerEdit',
- props: {
- maintainerEditAllowed: {
- type: Boolean,
- default: false,
- required: false,
- },
- },
- };
-</script>
-
-<template>
- <section class="mr-info-list mr-links">
- <p v-if="maintainerEditAllowed">
- {{ s__("mrWidget|Allows edits from maintainers") }}
- </p>
- </section>
-</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
index c1618bc6ea0..3e36a3a10f9 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
@@ -3,6 +3,7 @@
import tooltip from '~/vue_shared/directives/tooltip';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import { s__, __ } from '~/locale';
+ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import mrWidgetAuthorTime from '../../components/mr_widget_author_time.vue';
import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
@@ -16,6 +17,7 @@
mrWidgetAuthorTime,
loadingIcon,
statusIcon,
+ ClipboardButton,
},
props: {
mr: {
@@ -162,6 +164,18 @@
<span class="label-branch">
<a :href="mr.targetBranchPath">{{ mr.targetBranch }}</a>
</span>
+ with
+ <a
+ :href="mr.mergeCommitPath"
+ class="commit-sha js-mr-merged-commit-sha"
+ >
+ {{ mr.shortMergeCommitSha }}
+ </a>
+ <clipboard-button
+ :title="__('Copy commit SHA to clipboard')"
+ :text="mr.shortMergeCommitSha"
+ css-class="btn-default btn-transparent btn-clipboard js-mr-merged-copy-sha"
+ />
</p>
<p v-if="mr.sourceBranchRemoved">
{{ s__("mrWidget|The source branch has been removed") }}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue
index bf8628d18a6..926a3172412 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue
@@ -10,6 +10,6 @@ In EE, the configuration extends this object to add a functioning squash-before-
button.
*/
-export default {
- template: '',
-};
+<script>
+ export default {};
+</script>
diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
index 7f5f28091da..15097fa2a3f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js
+++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
@@ -15,7 +15,6 @@ export { default as WidgetHeader } from './components/mr_widget_header.vue';
export { default as WidgetMergeHelp } from './components/mr_widget_merge_help.vue';
export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue';
export { default as Deployment } from './components/deployment.vue';
-export { default as WidgetMaintainerEdit } from './components/mr_widget_maintainer_edit.vue';
export { default as WidgetRelatedLinks } from './components/mr_widget_related_links.vue';
export { default as MergedState } from './components/states/mr_widget_merged.vue';
export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge.vue';
@@ -41,8 +40,8 @@ export { default as MRWidgetService } from './services/mr_widget_service';
export { default as eventHub } from './event_hub';
export { default as getStateKey } from './stores/get_state_key';
export { default as stateMaps } from './stores/state_maps';
-export { default as SquashBeforeMerge } from './components/states/mr_widget_squash_before_merge';
+export { default as SquashBeforeMerge } from './components/states/mr_widget_squash_before_merge.vue';
export { default as notify } from '../lib/utils/notify';
export { default as SourceBranchRemovalStatus } from './components/source_branch_removal_status.vue';
-export { default as mrWidgetOptions } from './mr_widget_options';
+export { default as mrWidgetOptions } from './mr_widget_options.vue';
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index 345f9ac1b4b..f69fe03fcb3 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -1,12 +1,13 @@
+<script>
+
import Project from '~/pages/projects/project';
import SmartInterval from '~/smart_interval';
-import Flash from '../flash';
+import createFlash from '../flash';
import {
WidgetHeader,
WidgetMergeHelp,
WidgetPipeline,
Deployment,
- WidgetMaintainerEdit,
WidgetRelatedLinks,
MergedState,
ClosedState,
@@ -40,10 +41,39 @@ import { setFavicon } from '../lib/utils/common_utils';
export default {
el: '#js-vue-mr-widget',
name: 'MRWidget',
+ components: {
+ 'mr-widget-header': WidgetHeader,
+ 'mr-widget-merge-help': WidgetMergeHelp,
+ 'mr-widget-pipeline': WidgetPipeline,
+ Deployment,
+ 'mr-widget-related-links': WidgetRelatedLinks,
+ 'mr-widget-merged': MergedState,
+ 'mr-widget-closed': ClosedState,
+ 'mr-widget-merging': MergingState,
+ 'mr-widget-failed-to-merge': FailedToMerge,
+ 'mr-widget-wip': WorkInProgressState,
+ 'mr-widget-archived': ArchivedState,
+ 'mr-widget-conflicts': ConflictsState,
+ 'mr-widget-nothing-to-merge': NothingToMergeState,
+ 'mr-widget-not-allowed': NotAllowedState,
+ 'mr-widget-missing-branch': MissingBranchState,
+ 'mr-widget-ready-to-merge': ReadyToMergeState,
+ 'sha-mismatch': ShaMismatchState,
+ 'mr-widget-squash-before-merge': SquashBeforeMerge,
+ 'mr-widget-checking': CheckingState,
+ 'mr-widget-unresolved-discussions': UnresolvedDiscussionsState,
+ 'mr-widget-pipeline-blocked': PipelineBlockedState,
+ 'mr-widget-pipeline-failed': PipelineFailedState,
+ 'mr-widget-merge-when-pipeline-succeeds': MergeWhenPipelineSucceedsState,
+ 'mr-widget-auto-merge-failed': AutoMergeFailed,
+ 'mr-widget-rebase': RebaseState,
+ SourceBranchRemovalStatus,
+ },
props: {
mrData: {
type: Object,
required: false,
+ default: null,
},
},
data() {
@@ -72,6 +102,13 @@ export default {
(!this.mr.isNothingToMergeState && !this.mr.isMergedState);
},
},
+ created() {
+ this.initPolling();
+ this.bindEventHubListeners();
+ },
+ mounted() {
+ this.handleMounted();
+ },
methods: {
createService(store) {
const endpoints = {
@@ -99,7 +136,7 @@ export default {
cb.call(null, data);
}
})
- .catch(() => new Flash('Something went wrong. Please try again.'));
+ .catch(() => createFlash('Something went wrong. Please try again.'));
},
initPolling() {
this.pollingInterval = new SmartInterval({
@@ -134,7 +171,7 @@ export default {
}
})
.catch(() => {
- new Flash('Something went wrong while fetching the environments for this merge request. Please try again.'); // eslint-disable-line
+ createFlash('Something went wrong while fetching the environments for this merge request. Please try again.'); // eslint-disable-line
});
},
fetchActionsContent() {
@@ -147,7 +184,7 @@ export default {
Project.initRefSwitcher();
}
})
- .catch(() => new Flash('Something went wrong. Please try again.'));
+ .catch(() => createFlash('Something went wrong. Please try again.'));
},
handleNotification(data) {
if (data.ci_status === this.mr.ciStatus) return;
@@ -202,76 +239,53 @@ export default {
this.initDeploymentsPolling();
},
},
- created() {
- this.initPolling();
- this.bindEventHubListeners();
- },
- mounted() {
- this.handleMounted();
- },
- components: {
- 'mr-widget-header': WidgetHeader,
- 'mr-widget-merge-help': WidgetMergeHelp,
- 'mr-widget-pipeline': WidgetPipeline,
- Deployment,
- 'mr-widget-maintainer-edit': WidgetMaintainerEdit,
- 'mr-widget-related-links': WidgetRelatedLinks,
- 'mr-widget-merged': MergedState,
- 'mr-widget-closed': ClosedState,
- 'mr-widget-merging': MergingState,
- 'mr-widget-failed-to-merge': FailedToMerge,
- 'mr-widget-wip': WorkInProgressState,
- 'mr-widget-archived': ArchivedState,
- 'mr-widget-conflicts': ConflictsState,
- 'mr-widget-nothing-to-merge': NothingToMergeState,
- 'mr-widget-not-allowed': NotAllowedState,
- 'mr-widget-missing-branch': MissingBranchState,
- 'mr-widget-ready-to-merge': ReadyToMergeState,
- 'mr-widget-sha-mismatch': ShaMismatchState,
- 'mr-widget-squash-before-merge': SquashBeforeMerge,
- 'mr-widget-checking': CheckingState,
- 'mr-widget-unresolved-discussions': UnresolvedDiscussionsState,
- 'mr-widget-pipeline-blocked': PipelineBlockedState,
- 'mr-widget-pipeline-failed': PipelineFailedState,
- 'mr-widget-merge-when-pipeline-succeeds': MergeWhenPipelineSucceedsState,
- 'mr-widget-auto-merge-failed': AutoMergeFailed,
- 'mr-widget-rebase': RebaseState,
- SourceBranchRemovalStatus,
- },
- template: `
- <div class="mr-state-widget prepend-top-default">
- <mr-widget-header :mr="mr" />
- <mr-widget-pipeline
- v-if="shouldRenderPipelines"
- :pipeline="mr.pipeline"
- :ci-status="mr.ciStatus"
- :has-ci="mr.hasCI"
- />
- <deployment
- v-for="deployment in mr.deployments"
- :key="deployment.id"
- :deployment="deployment"
+};
+</script>
+<template>
+ <div class="mr-state-widget prepend-top-default">
+ <mr-widget-header
+ :mr="mr"
+ />
+ <mr-widget-pipeline
+ v-if="shouldRenderPipelines"
+ :pipeline="mr.pipeline"
+ :ci-status="mr.ciStatus"
+ :has-ci="mr.hasCI"
+ />
+ <deployment
+ v-for="deployment in mr.deployments"
+ :key="deployment.id"
+ :deployment="deployment"
+ />
+ <div class="mr-widget-section">
+ <component
+ :is="componentName"
+ :mr="mr"
+ :service="service"
+ />
+
+ <section
+ v-if="mr.maintainerEditAllowed"
+ class="mr-info-list mr-links"
+ >
+ {{ s__("mrWidget|Allows edits from maintainers") }}
+ </section>
+
+ <mr-widget-related-links
+ v-if="shouldRenderRelatedLinks"
+ :state="mr.state"
+ :related-links="mr.relatedLinks"
+ />
+
+ <source-branch-removal-status
+ v-if="shouldRenderSourceBranchRemovalStatus"
/>
- <div class="mr-widget-section">
- <component
- :is="componentName"
- :mr="mr"
- :service="service" />
- <mr-widget-maintainer-edit
- :maintainerEditAllowed="mr.maintainerEditAllowed" />
- <mr-widget-related-links
- v-if="shouldRenderRelatedLinks"
- :state="mr.state"
- :related-links="mr.relatedLinks" />
- <source-branch-removal-status
- v-if="shouldRenderSourceBranchRemovalStatus"
- />
- </div>
- <div
- class="mr-widget-footer"
- v-if="shouldRenderMergeHelp">
- <mr-widget-merge-help />
- </div>
</div>
- `,
-};
+ <div
+ class="mr-widget-footer"
+ v-if="shouldRenderMergeHelp"
+ >
+ <mr-widget-merge-help />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index a47ca9fae86..83b7b054e6f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -20,6 +20,7 @@ export default class MergeRequestStore {
this.sourceBranch = data.source_branch;
this.mergeStatus = data.merge_status;
this.commitMessage = data.merge_commit_message;
+ this.shortMergeCommitSha = data.short_merge_commit_sha;
this.commitMessageWithDescription = data.merge_commit_message_with_description;
this.commitsCount = data.commits_count;
this.divergedCommitsCount = data.diverged_commits_count;
@@ -65,6 +66,7 @@ export default class MergeRequestStore {
this.createIssueToResolveDiscussionsPath = data.create_issue_to_resolve_discussions_path;
this.mergeCheckPath = data.merge_check_path;
this.mergeActionsContentPath = data.commit_change_content_path;
+ this.mergeCommitPath = data.merge_commit_path;
this.isRemovingSourceBranch = this.isRemovingSourceBranch || false;
this.isOpen = data.state === 'opened';
this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false;
diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue
index 1a0df49bc29..c42c4a1fbe7 100644
--- a/app/assets/javascripts/vue_shared/components/icon.vue
+++ b/app/assets/javascripts/vue_shared/components/icon.vue
@@ -65,6 +65,9 @@ export default {
spriteHref() {
return `${gon.sprite_icons}#${this.name}`;
},
+ iconTestClass() {
+ return `ic-${this.name}`;
+ },
iconSizeClass() {
return this.size ? `s${this.size}` : '';
},
@@ -74,7 +77,7 @@ export default {
<template>
<svg
- :class="[iconSizeClass, cssClasses]"
+ :class="[iconSizeClass, iconTestClass, cssClasses]"
:width="width"
:height="height"
:x="x"
diff --git a/app/assets/javascripts/vue_shared/components/loading_button.vue b/app/assets/javascripts/vue_shared/components/loading_button.vue
index e832d94d32f..88c13a1f340 100644
--- a/app/assets/javascripts/vue_shared/components/loading_button.vue
+++ b/app/assets/javascripts/vue_shared/components/loading_button.vue
@@ -70,12 +70,14 @@
/>
</transition>
<transition name="fade">
- <span
- v-if="label"
- class="js-loading-button-label"
- >
- {{ label }}
- </span>
+ <slot>
+ <span
+ v-if="label"
+ class="js-loading-button-label"
+ >
+ {{ label }}
+ </span>
+ </slot>
</transition>
</button>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
index b33a0101dbf..92d187e24bf 100644
--- a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
+++ b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
@@ -1,53 +1,53 @@
<script>
- import $ from 'jquery';
+import $ from 'jquery';
- /**
- * Given an array of tabs, renders non linked bootstrap tabs.
- * When a tab is clicked it will trigger an event and provide the clicked scope.
- *
- * This component is used in apps that handle the API call.
- * If you only need to change the URL this component should not be used.
- *
- * @example
- * <navigation-tabs
- * :tabs="[
- * {
- * name: String,
- * scope: String,
- * count: Number || Undefined,
- * isActive: Boolean,
- * },
- * ]"
- * @onChangeTab="onChangeTab"
- * />
- */
- export default {
- name: 'NavigationTabs',
- props: {
- tabs: {
- type: Array,
- required: true,
- },
- scope: {
- type: String,
- required: false,
- default: '',
- },
+/**
+ * Given an array of tabs, renders non linked bootstrap tabs.
+ * When a tab is clicked it will trigger an event and provide the clicked scope.
+ *
+ * This component is used in apps that handle the API call.
+ * If you only need to change the URL this component should not be used.
+ *
+ * @example
+ * <navigation-tabs
+ * :tabs="[
+ * {
+ * name: String,
+ * scope: String,
+ * count: Number || Undefined || Null,
+ * isActive: Boolean,
+ * },
+ * ]"
+ * @onChangeTab="onChangeTab"
+ * />
+ */
+export default {
+ name: 'NavigationTabs',
+ props: {
+ tabs: {
+ type: Array,
+ required: true,
},
- mounted() {
- $(document).trigger('init.scrolling-tabs');
+ scope: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ mounted() {
+ $(document).trigger('init.scrolling-tabs');
+ },
+ methods: {
+ shouldRenderBadge(count) {
+ // 0 is valid in a badge, but evaluates to false, we need to check for undefined or null
+ return !(count === undefined || count === null);
},
- methods: {
- shouldRenderBadge(count) {
- // 0 is valid in a badge, but evaluates to false, we need to check for undefined
- return count !== undefined;
- },
- onTabClick(tab) {
- this.$emit('onChangeTab', tab.scope);
- },
+ onTabClick(tab) {
+ this.$emit('onChangeTab', tab.scope);
},
- };
+ },
+};
</script>
<template>
<ul class="nav-links scrolling-tabs separator">
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index f4f5926e198..cd9d60b96d3 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -106,10 +106,6 @@
@include btn-color($red-500, $red-600, $red-600, $red-700, $red-700, $red-800, $white-light);
}
-@mixin btn-gray {
- @include btn-color($gray-light, $border-gray-normal, $gray-normal, $border-gray-normal, $gray-dark, $border-gray-dark, $gl-text-color);
-}
-
@mixin btn-white {
@include btn-color($white-light, $border-color, $white-normal, $border-white-normal, $white-dark, $border-gray-dark, $gl-text-color);
}
@@ -183,10 +179,6 @@
}
}
- &.btn-gray {
- @include btn-gray;
- }
-
&.btn-info,
&.btn-primary,
&.btn-register {
diff --git a/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss
index 05cb0196ced..0bbd6eb27c1 100644
--- a/app/assets/stylesheets/framework/gitlab_theme.scss
+++ b/app/assets/stylesheets/framework/gitlab_theme.scss
@@ -177,25 +177,6 @@
}
}
- // Web IDE
- .ide-sidebar-link {
- color: $color-200;
- background-color: $color-700;
-
- &:hover,
- &:focus {
- background-color: $color-500;
- }
-
- &:active {
- background: $color-800;
- }
- }
-
- .branch-container {
- border-left-color: $color-700;
- }
-
.branch-header-title {
color: $color-700;
}
@@ -203,6 +184,13 @@
.ide-file-list .file.file-active {
color: $color-700;
}
+
+ .ide-sidebar-link {
+ &.active {
+ color: $color-700;
+ box-shadow: inset 3px 0 $color-700;
+ }
+ }
}
body {
@@ -343,9 +331,5 @@ body {
.sidebar-top-level-items > li.active .badge {
color: $theme-gray-900;
}
-
- .ide-sidebar-link {
- color: $white-light;
- }
}
}
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index e12b5aab381..9ff24ebc127 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -17,6 +17,16 @@
*/
@mixin markdown-table {
width: auto;
+ display: inline-block;
+ overflow-x: auto;
+ border-left: 0;
+ border-right: 0;
+ border-bottom: 0;
+
+ @supports(width: fit-content) {
+ display: block;
+ width: fit-content;
+ }
}
/*
diff --git a/app/assets/stylesheets/framework/terms.scss b/app/assets/stylesheets/framework/terms.scss
index dadfaf1c3f9..744fd0ff796 100644
--- a/app/assets/stylesheets/framework/terms.scss
+++ b/app/assets/stylesheets/framework/terms.scss
@@ -1,4 +1,8 @@
.terms {
+ .with-performance-bar & {
+ margin-top: 0;
+ }
+
.alert-wrapper {
min-height: $header-height + $gl-padding;
}
@@ -13,6 +17,7 @@
display: flex;
align-items: center;
justify-content: space-between;
+ line-height: $line-height-base;
.title {
display: flex;
@@ -29,10 +34,14 @@
.navbar-collapse {
padding-right: 0;
+
+ .navbar-nav {
+ margin: 0;
+ }
}
- .nav li a {
- color: $theme-gray-700;
+ .nav li {
+ float: none;
}
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 3d28df455bb..b5505538541 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -230,6 +230,7 @@ $row-hover: $blue-50;
$row-hover-border: $blue-200;
$progress-color: #c0392b;
$header-height: 40px;
+$ide-statusbar-height: 27px;
$fixed-layout-width: 1280px;
$limited-layout-width: 990px;
$limited-layout-width-sm: 790px;
@@ -264,6 +265,7 @@ $performance-bar-height: 35px;
$flash-height: 52px;
$context-header-height: 60px;
$breadcrumb-min-height: 48px;
+$gcp-signup-offer-icon-max-width: 125px;
/*
* Common component specific colors
@@ -333,11 +335,10 @@ $diff-jagged-border-gradient-color: darken($white-normal, 8%);
/*
* Fonts
*/
-$monospace_font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas',
- 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace;
-$regular_font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
- Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif,
- 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
+$monospace_font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono',
+ 'Courier New', 'andale mono', 'lucida console', monospace;
+$regular_font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell,
+ 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
/*
* Dropdowns
@@ -465,11 +466,9 @@ $issue-boards-card-shadow: rgba(186, 186, 186, 0.5);
*/
$issue-boards-filter-height: 68px;
$issue-boards-breadcrumbs-height-xs: 63px;
-$issue-board-list-difference-xs: $header-height +
- $issue-boards-breadcrumbs-height-xs;
+$issue-board-list-difference-xs: $header-height + $issue-boards-breadcrumbs-height-xs;
$issue-board-list-difference-sm: $header-height + $breadcrumb-min-height;
-$issue-board-list-difference-md: $issue-board-list-difference-sm +
- $issue-boards-filter-height;
+$issue-board-list-difference-md: $issue-board-list-difference-sm + $issue-boards-filter-height;
/*
* Avatar
@@ -690,6 +689,8 @@ $stage-hover-bg: $gray-darker;
$ci-action-icon-size: 22px;
$pipeline-dropdown-line-height: 20px;
$pipeline-dropdown-status-icon-size: 18px;
+$ci-action-dropdown-button-size: 24px;
+$ci-action-dropdown-svg-size: 12px;
/*
CI variable lists
diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss
index 7b8ee026357..3fd13078131 100644
--- a/app/assets/stylesheets/pages/clusters.scss
+++ b/app/assets/stylesheets/pages/clusters.scss
@@ -26,3 +26,51 @@
margin-right: 0;
}
}
+
+.gcp-signup-offer {
+ background-color: $blue-50;
+ border: 1px solid $blue-300;
+ border-radius: $border-radius-default;
+
+ // TODO: To be superceded by cssLab
+ &.alert {
+ padding: 24px 16px;
+
+ &-dismissable {
+ padding-right: 32px;
+
+ .close {
+ top: -8px;
+ right: -16px;
+ color: $blue-500;
+ opacity: 1;
+ }
+ }
+ }
+
+ .gcp-logo {
+ margin-bottom: $gl-padding;
+ text-align: center;
+ }
+
+ img {
+ max-width: $gcp-signup-offer-icon-max-width;
+ }
+
+ a:not(.btn) {
+ color: $gl-link-color;
+ font-weight: normal;
+ text-decoration: none;
+ }
+
+ @media (min-width: $screen-sm-min) {
+ > div {
+ display: flex;
+ align-items: center;
+ }
+
+ .gcp-logo {
+ margin: 0;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 1aca3c5cf1a..944996159d7 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -2,7 +2,6 @@
background: none;
border: 0;
padding: 0;
- margin-top: 10px;
word-break: normal;
white-space: pre-wrap;
}
@@ -21,10 +20,6 @@
margin: 0;
color: $gl-text-color;
}
-
- .commit-description {
- margin-top: 15px;
- }
}
.commit-hash-full {
@@ -178,7 +173,7 @@
.commit-detail {
display: flex;
justify-content: space-between;
- align-items: center;
+ align-items: start;
flex-grow: 1;
}
@@ -268,20 +263,16 @@
.commit-row-description {
font-size: 14px;
- padding: 10px 15px;
- margin: 10px 0;
- background: $gray-light;
+ padding: 0 0 0 $gl-padding-8;
+ border: 0;
display: none;
white-space: pre-wrap;
word-break: normal;
-
- pre {
- border: 0;
- background: inherit;
- padding: 0;
- margin: 0;
- white-space: pre-wrap;
- }
+ color: $gl-text-color-secondary;
+ background: none;
+ font-family: inherit;
+ border-left: 2px solid $theme-gray-300;
+ border-radius: unset;
a {
color: $gl-text-color;
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 3a300086fa3..1f406cc1c2d 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -283,28 +283,59 @@
}
&.popover {
+ padding: 0;
+ border: 1px solid $border-color;
+
&.left {
left: auto;
right: 0;
margin-right: 10px;
+
+ > .arrow {
+ right: -16px;
+ border-left-color: $border-color;
+ }
+
+ > .arrow::after {
+ border-left-color: $theme-gray-50;
+ }
}
&.right {
left: 0;
right: auto;
margin-left: 10px;
+
+ > .arrow {
+ left: -16px;
+ border-right-color: $border-color;
+ }
+
+ > .arrow::after {
+ border-right-color: $theme-gray-50;
+ }
}
> .arrow {
- top: 40px;
+ top: 16px;
+ margin-top: -8px;
+ border-width: 8px;
}
> .popover-title,
> .popover-content {
- padding: 5px 8px;
+ padding: 8px;
font-size: 12px;
white-space: nowrap;
}
+
+ > .popover-title {
+ background-color: $theme-gray-50;
+ }
+ }
+
+ strong {
+ font-weight: 600;
}
}
@@ -317,7 +348,7 @@
vertical-align: middle;
+ td {
- padding-left: 5px;
+ padding-left: 8px;
vertical-align: top;
}
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 66db4917178..3581dd36a10 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -156,10 +156,6 @@
.dropdown-menu {
z-index: 300;
}
-
- .ci-action-icon-wrapper {
- line-height: 16px;
- }
}
.mini-pipeline-graph-dropdown-toggle {
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 3a8ec779c14..02803e7b040 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -22,7 +22,6 @@
}
.ci-table {
-
.label {
margin-bottom: 3px;
}
@@ -123,7 +122,6 @@
}
.branch-commit {
-
.ref-name {
font-weight: $gl-font-weight-bold;
max-width: 100px;
@@ -481,43 +479,6 @@
@extend .build-content:hover;
}
- .ci-action-icon-container {
- position: absolute;
- right: 5px;
- top: 5px;
-
- // Action Icons in big pipeline-graph nodes
- &.ci-action-icon-wrapper {
- height: 30px;
- width: 30px;
- background: $white-light;
- border: 1px solid $border-color;
- border-radius: 100%;
- display: block;
-
- &:hover {
- background-color: $stage-hover-bg;
- border: 1px solid $dropdown-toggle-active-border-color;
-
- svg {
- fill: $gl-text-color;
- }
- }
-
- svg {
- fill: $gl-text-color-secondary;
- position: relative;
- top: -1px;
- }
-
- &.play {
- svg {
- left: 2px;
- }
- }
- }
- }
-
.ci-status-icon svg {
height: 20px;
width: 20px;
@@ -548,7 +509,6 @@
border: 1px solid $dropdown-toggle-active-border-color;
}
-
// Connect first build in each stage with right horizontal line
&:first-child {
&::after {
@@ -602,6 +562,43 @@
}
}
}
+
+ .ci-action-icon-container {
+ position: absolute;
+ right: 5px;
+ top: 5px;
+
+ // Action Icons in big pipeline-graph nodes
+ &.ci-action-icon-wrapper {
+ height: 30px;
+ width: 30px;
+ background: $white-light;
+ border: 1px solid $border-color;
+ border-radius: 100%;
+ display: block;
+
+ &:hover {
+ background-color: $stage-hover-bg;
+ border: 1px solid $dropdown-toggle-active-border-color;
+
+ svg {
+ fill: $gl-text-color;
+ }
+ }
+
+ svg {
+ fill: $gl-text-color-secondary;
+ position: relative;
+ top: -1px;
+ }
+
+ &.play {
+ svg {
+ left: 2px;
+ }
+ }
+ }
+ }
}
// Triggers the dropdown in the big pipeline graph
@@ -710,93 +707,77 @@ button.mini-pipeline-graph-dropdown-toggle {
}
}
-// dropdown content for big and mini pipeline
+/**
+ Action icons inside dropdowns:
+ - mini graph in pipelines table
+ - dropdown in big graph
+ - mini graph in MR widget pipeline
+ - mini graph in Commit widget pipeline
+*/
.big-pipeline-graph-dropdown-menu,
.mini-pipeline-graph-dropdown-menu {
width: 240px;
max-width: 240px;
- .scrollable-menu {
+ // override dropdown.scss
+ &.dropdown-menu li button,
+ &.dropdown-menu li a.ci-action-icon-container {
padding: 0;
- max-height: 245px;
- overflow: auto;
+ text-align: center;
}
- li {
- position: relative;
+ .ci-action-icon-container {
+ position: absolute;
+ right: 8px;
+ top: 8px;
- // ensure .mini-pipeline-graph-dropdown-item has hover style when action-icon is hovered
- &:hover > .mini-pipeline-graph-dropdown-item,
- &:hover > .ci-job-component > .mini-pipeline-graph-dropdown-item {
- @extend .mini-pipeline-graph-dropdown-item:hover;
- }
+ &.ci-action-icon-wrapper {
+ height: $ci-action-dropdown-button-size;
+ width: $ci-action-dropdown-button-size;
- // Action icon on the right
- a.ci-action-icon-wrapper {
- border-radius: 50%;
+ background: $white-light;
border: 1px solid $border-color;
- width: $ci-action-icon-size;
- height: $ci-action-icon-size;
- padding: 2px 0 0 5px;
- font-size: 12px;
- background-color: $white-light;
- position: absolute;
- top: 50%;
- right: $gl-padding;
- margin-top: -#{$ci-action-icon-size / 2};
+ border-radius: 50%;
+ display: block;
- &:hover,
- &:focus {
+ &:hover {
background-color: $stage-hover-bg;
border: 1px solid $dropdown-toggle-active-border-color;
+
+ svg {
+ fill: $gl-text-color;
+ }
}
svg {
+ width: $ci-action-dropdown-svg-size;
+ height: $ci-action-dropdown-svg-size;
fill: $gl-text-color-secondary;
- width: #{$ci-action-icon-size - 6};
- height: #{$ci-action-icon-size - 6};
- left: -3px;
position: relative;
- top: -1px;
-
- &.icon-action-stop,
- &.icon-action-cancel {
- width: 12px;
- height: 12px;
- top: 1px;
- left: -1px;
- }
-
- &.icon-action-play {
- width: 11px;
- height: 11px;
- top: 1px;
- left: 1px;
- }
-
- &.icon-action-retry {
- width: 16px;
- height: 16px;
- top: 0;
- left: -3px;
- }
+ top: 0;
+ vertical-align: initial;
}
+ }
+ }
- &:hover svg,
- &:focus svg {
- fill: $gl-text-color;
- }
+ // SVGs in the commit widget and mr widget
+ a.ci-action-icon-container.ci-action-icon-wrapper svg {
+ top: 2px;
+ }
- &.icon-action-retry,
- &.icon-action-play {
- svg {
- width: #{$ci-action-icon-size - 6};
- height: #{$ci-action-icon-size - 6};
- left: 8px;
- }
- }
+ .scrollable-menu {
+ padding: 0;
+ max-height: 245px;
+ overflow: auto;
+ }
+ li {
+ position: relative;
+ // ensure .mini-pipeline-graph-dropdown-item has hover style when action-icon is hovered
+ &:hover > .mini-pipeline-graph-dropdown-item,
+ &:hover > .ci-job-component > .mini-pipeline-graph-dropdown-item {
+ @extend .mini-pipeline-graph-dropdown-item:hover;
}
// link to the build
@@ -808,6 +789,11 @@ button.mini-pipeline-graph-dropdown-toggle {
line-height: $line-height-base;
white-space: nowrap;
+ // Match dropdown.scss for all `a` tags
+ &.non-details-job-component {
+ padding: 8px 16px;
+ }
+
.ci-job-name-component {
align-items: center;
display: flex;
@@ -939,7 +925,7 @@ button.mini-pipeline-graph-dropdown-toggle {
&.dropdown-menu {
transform: translate(-80%, 0);
- @media(min-width: $screen-md-min) {
+ @media (min-width: $screen-md-min) {
transform: translate(-50%, 0);
right: auto;
left: 50%;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index d7d343b088a..dd0cb2c2613 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -205,7 +205,6 @@
.project-repo-buttons,
.group-buttons {
.btn {
- @include btn-gray;
padding: 3px 10px;
&:last-child {
@@ -294,7 +293,7 @@
}
.count {
- @include btn-gray;
+ @include btn-white;
display: inline-block;
background: $white-light;
border-radius: 2px;
@@ -354,30 +353,48 @@
min-width: 200px;
}
-.deploy-key-content {
- @media (min-width: $screen-sm-min) {
- float: left;
+.deploy-keys {
+ .scrolling-tabs-container {
+ position: relative;
+ }
+}
- &:last-child {
- float: right;
+.deploy-key {
+ // Ensure that the fingerprint does not overflow on small screens
+ .fingerprint {
+ word-break: break-all;
+ white-space: normal;
+ }
+
+ .deploy-project-label,
+ .key-created-at {
+ svg {
+ vertical-align: text-top;
}
}
-}
-.deploy-key-projects {
- @media (min-width: $screen-sm-min) {
- line-height: 42px;
+ .btn svg {
+ vertical-align: top;
+ }
+
+ .key-created-at {
+ line-height: unset;
}
}
-a.deploy-project-label {
- padding: 5px;
- margin-right: 5px;
- color: $gl-text-color;
- background-color: $row-hover;
+.deploy-project-list {
+ margin-bottom: -$gl-padding-4;
- &:hover {
- color: $gl-link-color;
+ a.deploy-project-label {
+ margin-right: $gl-padding-4;
+ margin-bottom: $gl-padding-4;
+ color: $gl-text-color-secondary;
+ background-color: $theme-gray-100;
+ line-height: $gl-btn-line-height;
+
+ &:hover {
+ color: $gl-link-color;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 888757c12d8..00457717f00 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -23,6 +23,7 @@
margin-top: 0;
border-top: 1px solid $white-dark;
border-bottom: 1px solid $white-dark;
+ padding-bottom: $ide-statusbar-height;
&.is-collapsed {
.ide-file-list {
@@ -121,14 +122,6 @@
.multi-file-loading-container {
margin-top: 10px;
padding: 10px;
-
- .animation-container {
- background: $gray-light;
-
- div {
- background: $gray-light;
- }
- }
}
.multi-file-table-col-commit-message {
@@ -155,69 +148,56 @@
}
li {
- position: relative;
- }
-
- .dropdown {
display: flex;
- margin-left: auto;
- margin-bottom: 1px;
- padding: 0 $grid-size;
- border-left: 1px solid $white-dark;
- background-color: $white-light;
-
- &.shadow {
- box-shadow: 0 0 10px $dropdown-shadow-color;
- }
+ align-items: center;
+ padding: $grid-size $gl-padding;
+ background-color: $gray-normal;
+ border-right: 1px solid $white-dark;
+ border-bottom: 1px solid $white-dark;
- .btn {
- margin-top: auto;
- margin-bottom: auto;
+ &.active {
+ background-color: $white-light;
+ border-bottom-color: $white-light;
}
}
}
.multi-file-tab {
- @include str-truncated(150px);
- padding: ($gl-padding / 2) ($gl-padding + 12) ($gl-padding / 2) $gl-padding;
- background-color: $gray-normal;
- border-right: 1px solid $white-dark;
- border-bottom: 1px solid $white-dark;
+ @include str-truncated(141px);
cursor: pointer;
svg {
vertical-align: middle;
}
-
- &.active {
- background-color: $white-light;
- border-bottom-color: $white-light;
- }
}
.multi-file-tab-close {
- position: absolute;
- right: 8px;
- top: 50%;
width: 16px;
height: 16px;
padding: 0;
+ margin-left: $grid-size;
background: none;
border: 0;
border-radius: $border-radius-default;
color: $theme-gray-900;
- transform: translateY(-50%);
svg {
position: relative;
top: -1px;
}
- &:hover {
+ .ide-file-changed-icon {
+ display: block;
+ position: relative;
+ top: 1px;
+ right: -2px;
+ }
+
+ &:not([disabled]):hover {
background-color: $theme-gray-200;
}
- &:focus {
+ &:not([disabled]):focus {
background-color: $blue-500;
color: $white-light;
outline: 0;
@@ -248,6 +228,17 @@
display: none;
}
+ .is-readonly,
+ .editor.original {
+ .view-lines {
+ cursor: default;
+ }
+
+ .cursors-layer {
+ display: none;
+ }
+ }
+
.monaco-diff-editor.vs {
.editor.modified {
box-shadow: none;
@@ -306,15 +297,12 @@
.margin-view-overlays .delete-sign {
opacity: 0.4;
}
-
- .cursors-layer {
- display: none;
- }
}
}
.multi-file-editor-holder {
height: 100%;
+ min-height: 0;
}
.preview-container {
@@ -380,6 +368,7 @@
.ide-btn-group {
padding: $gl-padding-4 $gl-vert-padding;
+ line-height: 24px;
}
.ide-status-bar {
@@ -387,7 +376,13 @@
padding: $gl-bar-padding $gl-padding;
background: $white-light;
display: flex;
- justify-content: flex-end;
+ justify-content: space-between;
+ height: $ide-statusbar-height;
+
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
> div + div {
padding-left: $gl-padding;
@@ -398,6 +393,14 @@
}
}
+.ide-status-file {
+ text-align: right;
+
+ .ide-status-branch + &,
+ &:first-child {
+ margin-left: auto;
+ }
+}
// Not great, but this is to deal with our current output
.multi-file-preview-holder {
height: 100%;
@@ -433,28 +436,35 @@
.multi-file-commit-panel {
display: flex;
position: relative;
- flex-direction: column;
width: 340px;
padding: 0;
background-color: $gray-light;
- padding-right: 3px;
+ padding-right: 1px;
+
+ .context-header {
+ width: auto;
+ margin-right: 0;
+
+ a:hover,
+ a:focus {
+ text-decoration: none;
+ }
+ }
.projects-sidebar {
min-height: 0;
display: flex;
flex-direction: column;
flex: 1;
-
- .context-header {
- width: auto;
- margin-right: 0;
- }
}
.multi-file-commit-panel-inner {
+ position: relative;
display: flex;
flex-direction: column;
height: 100%;
+ min-width: 0;
+ width: 100%;
}
.multi-file-commit-panel-inner-scroll {
@@ -462,68 +472,10 @@
flex: 1;
flex-direction: column;
overflow: auto;
- }
-
- &.is-collapsed {
- width: 60px;
-
- .multi-file-commit-list {
- padding-top: $gl-padding;
- overflow: hidden;
- }
-
- .multi-file-context-bar-icon {
- align-items: center;
-
- svg {
- float: none;
- margin: 0;
- }
- }
- }
-
- .branch-container {
- border-left: 4px solid;
- margin-bottom: $gl-bar-padding;
- }
-
- .branch-header {
- background: $white-dark;
- display: flex;
- }
-
- .branch-header-title {
- flex: 1;
- padding: $grid-size $gl-padding;
- font-weight: $gl-font-weight-bold;
-
- svg {
- vertical-align: middle;
- }
- }
-
- .branch-header-btns {
- padding: $gl-vert-padding $gl-padding;
- }
-
- .left-collapse-btn {
- display: none;
- background: $gray-light;
- text-align: left;
+ background-color: $white-light;
+ border-left: 1px solid $white-dark;
border-top: 1px solid $white-dark;
-
- svg {
- vertical-align: middle;
- }
- }
-}
-
-.multi-file-context-bar-icon {
- padding: 10px;
-
- svg {
- margin-right: 10px;
- float: left;
+ border-top-left-radius: $border-radius-small;
}
}
@@ -549,14 +501,13 @@
align-items: center;
margin-bottom: 0;
border-bottom: 1px solid $white-dark;
- padding: $gl-btn-padding 0;
- min-height: 56px;
+ padding: $gl-btn-padding $gl-padding;
}
.multi-file-commit-panel-header-title {
display: flex;
flex: 1;
- padding-left: $grid-size;
+ align-items: center;
svg {
margin-right: $gl-btn-padding;
@@ -572,7 +523,7 @@
.multi-file-commit-list {
flex: 1;
overflow: auto;
- padding: $gl-padding 0;
+ padding: $gl-padding;
min-height: 60px;
}
@@ -667,30 +618,24 @@
}
.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;
.btn {
font-size: $gl-font-size;
}
+
+ .multi-file-commit-panel-success-message {
+ top: 0;
+ }
}
.multi-file-commit-panel-bottom {
position: relative;
-
- .multi-file-commit-panel-success-message {
- position: absolute;
- top: 1px;
- left: 3px;
- bottom: 0;
- right: 0;
- z-index: 10;
- background: $gray-light;
- overflow: auto;
- display: flex;
- flex-direction: column;
- justify-content: center;
- }
}
.dirty-diff {
@@ -826,7 +771,7 @@
position: absolute;
top: 0;
bottom: 0;
- width: 3px;
+ width: 1px;
background-color: $white-dark;
&.dragright {
@@ -840,42 +785,40 @@
.ide-commit-list-container {
display: flex;
+ flex: 1;
flex-direction: column;
width: 100%;
- padding: 0 16px;
+ min-height: 140px;
- &:not(.is-collapsed) {
- flex: 1;
- min-height: 140px;
- }
-
- &.is-collapsed {
- .multi-file-commit-panel-header {
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
-
- svg {
- margin-left: auto;
- margin-right: auto;
- }
-
- .multi-file-commit-panel-collapse-btn {
- margin-right: auto;
- margin-left: auto;
- border-left: 0;
- }
- }
+ &.is-first {
+ border-bottom: 1px solid $white-dark;
}
}
.ide-staged-action-btn {
margin-left: auto;
- color: $gl-link-color;
+ line-height: 22px;
+}
+
+.ide-commit-file-count {
+ min-width: 22px;
+ margin-left: auto;
+ background-color: $gray-light;
+ border-radius: $border-radius-default;
+ border: 1px solid $white-dark;
+ line-height: 20px;
+ text-align: center;
}
.ide-commit-radios {
label {
font-weight: normal;
+
+ &.is-disabled {
+ .ide-radio-label {
+ text-decoration: line-through;
+ }
+ }
}
.help-block {
@@ -888,17 +831,58 @@
margin-left: 25px;
}
-.ide-external-links {
- p {
- margin: 0;
- }
-}
-
.ide-sidebar-link {
- padding: $gl-padding-8 $gl-padding;
display: flex;
align-items: center;
- font-weight: $gl-font-weight-bold;
+ position: relative;
+ height: 60px;
+ width: 100%;
+ padding: 0 $gl-padding;
+ color: $gl-text-color-secondary;
+ background-color: transparent;
+ border: 0;
+ border-top: 1px solid transparent;
+ border-bottom: 1px solid transparent;
+ outline: 0;
+
+ svg {
+ margin: 0 auto;
+ }
+
+ &:hover {
+ color: $gl-text-color;
+ background-color: $theme-gray-100;
+ }
+
+ &:focus {
+ color: $gl-text-color;
+ background-color: $theme-gray-200;
+ }
+
+ &.active {
+ // extend width over border of sidebar section
+ width: calc(100% + 1px);
+ padding-right: $gl-padding + 1px;
+ background-color: $white-light;
+ border-top-color: $white-dark;
+ border-bottom-color: $white-dark;
+
+ &::after {
+ content: '';
+ position: absolute;
+ right: -1px;
+ top: 0;
+ bottom: 0;
+ width: 1px;
+ background: $white-light;
+ }
+ }
+}
+
+.ide-activity-bar {
+ position: relative;
+ flex: 0 0 60px;
+ z-index: 1;
}
.ide-file-finder-overlay {
@@ -992,6 +976,80 @@
resize: none;
}
+.ide-tree-header {
+ display: flex;
+ align-items: center;
+ padding: 10px 0;
+ margin-left: 10px;
+ margin-right: 10px;
+ border-bottom: 1px solid $white-dark;
+
+ .ide-new-btn {
+ margin-left: auto;
+ }
+}
+
+.ide-sidebar-branch-title {
+ font-weight: $gl-font-weight-normal;
+
+ svg {
+ position: relative;
+ top: 3px;
+ margin-top: -1px;
+ }
+}
+
+.commit-form-compact {
+ .btn {
+ margin-bottom: 8px;
+ }
+
+ p {
+ margin-bottom: 0;
+ }
+}
+
+.commit-form-slide-up-enter-active,
+.commit-form-slide-up-leave-active {
+ position: absolute;
+ top: 16px;
+ left: 16px;
+ right: 16px;
+ transition: all 0.3s ease;
+}
+
+.is-full .commit-form-slide-up-enter,
+.is-compact .commit-form-slide-up-leave-to {
+ transform: translateY(100%);
+}
+
+.is-full .commit-form-slide-up-enter-to,
+.is-compact .commit-form-slide-up-leave {
+ transform: translateY(0);
+}
+
+.commit-form-slide-up-enter,
+.commit-form-slide-up-leave-to {
+ opacity: 0;
+}
+
+.ide-review-header {
+ flex-direction: column;
+ align-items: flex-start;
+
+ .dropdown {
+ margin-left: auto;
+ }
+
+ a {
+ color: $gl-link-color;
+ }
+}
+
+.ide-review-sub-header {
+ color: $gl-text-color-secondary;
+}
+
.ide-tree-changes {
display: flex;
align-items: center;
@@ -1001,3 +1059,37 @@
.ide-new-modal-label {
line-height: 34px;
}
+
+.multi-file-commit-panel-success-message {
+ position: absolute;
+ top: 61px;
+ left: 1px;
+ bottom: 0;
+ right: 0;
+ z-index: 10;
+ background: $white-light;
+ overflow: auto;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+}
+
+.ide-review-button-holder {
+ display: flex;
+ width: 100%;
+ align-items: center;
+}
+
+.ide-context-header {
+ .avatar {
+ flex: 0 0 40px;
+ }
+}
+
+.ide-sidebar-project-title {
+ min-width: 0;
+
+ .sidebar-context-title {
+ white-space: nowrap;
+ }
+}
diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss
index 9a0ec936979..e70a57c2a67 100644
--- a/app/assets/stylesheets/pages/wiki.scss
+++ b/app/assets/stylesheets/pages/wiki.scss
@@ -180,11 +180,6 @@ ul.wiki-pages-list.content-list {
}
}
-.wiki-holder {
- overflow-x: auto;
- overflow-y: hidden;
-}
-
.wiki {
table {
@include markdown-table;
diff --git a/app/assets/stylesheets/print.scss b/app/assets/stylesheets/print.scss
index b07a5ae22cd..90ccd4abd90 100644
--- a/app/assets/stylesheets/print.scss
+++ b/app/assets/stylesheets/print.scss
@@ -36,7 +36,9 @@ ul.notes-form,
.gutter-toggle,
.issuable-details .content-block-small,
.edit-link,
-.note-action-button {
+.note-action-button,
+.right-sidebar,
+.flash-container {
display: none !important;
}
@@ -53,3 +55,7 @@ pre {
.right-sidebar {
top: 0;
}
+
+a[href]::after {
+ content: none !important;
+}
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 2caffec66ac..2843d70c645 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -13,8 +13,7 @@ class ApplicationController < ActionController::Base
before_action :authenticate_sessionless_user!
before_action :authenticate_user!
- before_action :enforce_terms!, if: -> { Gitlab::CurrentSettings.current_application_settings.enforce_terms },
- unless: :peek_request?
+ before_action :enforce_terms!, if: :should_enforce_terms?
before_action :validate_user_service_ticket!
before_action :check_password_expiration
before_action :ldap_security_check
@@ -373,4 +372,10 @@ class ApplicationController < ActionController::Base
def peek_request?
request.path.start_with?('/-/peek')
end
+
+ def should_enforce_terms?
+ return false unless Gitlab::CurrentSettings.current_application_settings.enforce_terms
+
+ !(peek_request? || devise_controller?)
+ end
end
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 0379f76fc3d..c925b4aada5 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -18,7 +18,6 @@ module IssuableActions
def update
@issuable = update_service.execute(issuable) # rubocop:disable Gitlab/ModuleWithInstanceVariables
-
respond_to do |format|
format.html do
recaptcha_check_if_spammable { render :edit }
diff --git a/app/controllers/concerns/send_file_upload.rb b/app/controllers/concerns/send_file_upload.rb
index 55011c89886..237c93daee8 100644
--- a/app/controllers/concerns/send_file_upload.rb
+++ b/app/controllers/concerns/send_file_upload.rb
@@ -2,6 +2,10 @@ module SendFileUpload
def send_upload(file_upload, send_params: {}, redirect_params: {}, attachment: nil, disposition: 'attachment')
if attachment
redirect_params[:query] = { "response-content-disposition" => "#{disposition};filename=#{attachment.inspect}" }
+ # By default, Rails will send uploads with an extension of .js with a
+ # content-type of text/javascript, which will trigger Rails'
+ # cross-origin JavaScript protection.
+ send_params[:content_type] = 'text/plain' if File.extname(attachment) == '.js'
send_params.merge!(filename: attachment, disposition: disposition)
end
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 134b0dfc0db..ef3eba80154 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -11,13 +11,20 @@ class Groups::GroupMembersController < Groups::ApplicationController
:override
def index
+ can_manage_members = can?(current_user, :admin_group_member, @group)
+
@sort = params[:sort].presence || sort_value_name
@project = @group.projects.find(params[:project_id]) if params[:project_id]
@members = GroupMembersFinder.new(@group).execute
- @members = @members.non_invite unless can?(current_user, :admin_group, @group)
+ @members = @members.non_invite unless can_manage_members
@members = @members.search(params[:search]) if params[:search].present?
@members = @members.sort_by_attribute(@sort)
+
+ if can_manage_members && params[:two_factor].present?
+ @members = @members.filter_by_2fa(params[:two_factor])
+ end
+
@members = @members.page(params[:page]).per(50)
@members = present_members(@members.includes(:user))
diff --git a/app/controllers/groups/runners_controller.rb b/app/controllers/groups/runners_controller.rb
new file mode 100644
index 00000000000..78992ec7f46
--- /dev/null
+++ b/app/controllers/groups/runners_controller.rb
@@ -0,0 +1,58 @@
+class Groups::RunnersController < Groups::ApplicationController
+ # Proper policies should be implemented per
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/45894
+ before_action :authorize_admin_pipeline!
+
+ before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show]
+
+ def show
+ render 'shared/runners/show'
+ end
+
+ def edit
+ end
+
+ def update
+ if Ci::UpdateRunnerService.new(@runner).update(runner_params)
+ redirect_to group_runner_path(@group, @runner), notice: 'Runner was successfully updated.'
+ else
+ render 'edit'
+ end
+ end
+
+ def destroy
+ @runner.destroy
+
+ redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), status: 302
+ end
+
+ def resume
+ if Ci::UpdateRunnerService.new(@runner).update(active: true)
+ redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), notice: 'Runner was successfully updated.'
+ else
+ redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), alert: 'Runner was not updated.'
+ end
+ end
+
+ def pause
+ if Ci::UpdateRunnerService.new(@runner).update(active: false)
+ redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), notice: 'Runner was successfully updated.'
+ else
+ redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), alert: 'Runner was not updated.'
+ end
+ end
+
+ private
+
+ def runner
+ @runner ||= @group.runners.find(params[:id])
+ end
+
+ def authorize_admin_pipeline!
+ return render_404 unless can?(current_user, :admin_pipeline, group)
+ end
+
+ def runner_params
+ params.require(:runner).permit(Ci::Runner::FORM_EDITABLE)
+ end
+end
diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb
index bcb856ce3f4..663269a0f92 100644
--- a/app/controllers/import/base_controller.rb
+++ b/app/controllers/import/base_controller.rb
@@ -7,9 +7,9 @@ class Import::BaseController < ApplicationController
def find_jobs(import_type)
current_user.created_projects
- .includes(:import_state)
- .where(import_type: import_type)
- .to_json(only: [:id], methods: [:import_status])
+ .includes(:import_state)
+ .where(import_type: import_type)
+ .to_json(only: [:id], methods: [:import_status])
end
def find_or_create_namespace(names, owner)
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index 2b0c2ca97c0..f93e500a07a 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -8,8 +8,11 @@ class Projects::CompareController < Projects::ApplicationController
# Authorize
before_action :require_non_empty_project
before_action :authorize_download_code!
- before_action :define_ref_vars, only: [:index, :show, :diff_for_path]
- before_action :define_diff_vars, only: [:show, :diff_for_path]
+ # Defining ivars
+ before_action :define_diffs, only: [:show, :diff_for_path]
+ before_action :define_environment, only: [:show]
+ before_action :define_diff_notes_disabled, only: [:show, :diff_for_path]
+ before_action :define_commits, only: [:show, :diff_for_path, :signatures]
before_action :merge_request, only: [:index, :show]
def index
@@ -22,9 +25,9 @@ class Projects::CompareController < Projects::ApplicationController
end
def diff_for_path
- return render_404 unless @compare
+ return render_404 unless compare
- render_diff_for_path(@compare.diffs(diff_options))
+ render_diff_for_path(compare.diffs(diff_options))
end
def create
@@ -41,30 +44,60 @@ class Projects::CompareController < Projects::ApplicationController
end
end
+ def signatures
+ respond_to do |format|
+ format.json do
+ render json: {
+ signatures: @commits.select(&:has_signature?).map do |commit|
+ {
+ commit_sha: commit.sha,
+ html: view_to_html_string('projects/commit/_signature', signature: commit.signature)
+ }
+ end
+ }
+ end
+ end
+ end
+
private
- def define_ref_vars
- @start_ref = Addressable::URI.unescape(params[:from])
+ def compare
+ return @compare if defined?(@compare)
+
+ @compare = CompareService.new(@project, head_ref).execute(@project, start_ref)
+ end
+
+ def start_ref
+ @start_ref ||= Addressable::URI.unescape(params[:from])
+ end
+
+ def head_ref
+ return @ref if defined?(@ref)
+
@ref = @head_ref = Addressable::URI.unescape(params[:to])
end
- def define_diff_vars
- @compare = CompareService.new(@project, @head_ref)
- .execute(@project, @start_ref)
+ def define_commits
+ @commits = compare.present? ? prepare_commits_for_rendering(compare.commits) : []
+ end
- if @compare
- @commits = prepare_commits_for_rendering(@compare.commits)
- @diffs = @compare.diffs(diff_options)
+ def define_diffs
+ @diffs = compare.present? ? compare.diffs(diff_options) : []
+ end
- environment_params = @repository.branch_exists?(@head_ref) ? { ref: @head_ref } : { commit: @compare.commit }
+ def define_environment
+ if compare
+ environment_params = @repository.branch_exists?(head_ref) ? { ref: head_ref } : { commit: compare.commit }
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
-
- @diff_notes_disabled = true
end
end
+ def define_diff_notes_disabled
+ @diff_notes_disabled = compare.present?
+ end
+
def merge_request
@merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened
- .find_by(source_project: @project, source_branch: @head_ref, target_branch: @start_ref)
+ .find_by(source_project: @project, source_branch: head_ref, target_branch: start_ref)
end
end
diff --git a/app/controllers/projects/mirrors_controller.rb b/app/controllers/projects/mirrors_controller.rb
new file mode 100644
index 00000000000..5698ff4e706
--- /dev/null
+++ b/app/controllers/projects/mirrors_controller.rb
@@ -0,0 +1,67 @@
+class Projects::MirrorsController < Projects::ApplicationController
+ include RepositorySettingsRedirect
+
+ # Authorize
+ before_action :remote_mirror, only: [:update]
+ before_action :check_mirror_available!
+ before_action :authorize_admin_project!
+
+ layout "project_settings"
+
+ def show
+ redirect_to_repository_settings(project)
+ end
+
+ def update
+ if project.update_attributes(mirror_params)
+ flash[:notice] = 'Mirroring settings were successfully updated.'
+ else
+ flash[:alert] = project.errors.full_messages.join(', ').html_safe
+ end
+
+ respond_to do |format|
+ format.html { redirect_to_repository_settings(project) }
+ format.json do
+ if project.errors.present?
+ render json: project.errors, status: :unprocessable_entity
+ else
+ render json: ProjectMirrorSerializer.new.represent(project)
+ end
+ end
+ end
+ end
+
+ def update_now
+ if params[:sync_remote]
+ project.update_remote_mirrors
+ flash[:notice] = "The remote repository is being updated..."
+ end
+
+ redirect_to_repository_settings(project)
+ end
+
+ private
+
+ def remote_mirror
+ @remote_mirror = project.remote_mirrors.first_or_initialize
+ end
+
+ def check_mirror_available!
+ Gitlab::CurrentSettings.current_application_settings.mirror_available || current_user&.admin?
+ end
+
+ def mirror_params_attributes
+ [
+ remote_mirrors_attributes: %i[
+ url
+ id
+ enabled
+ only_protected_branches
+ ]
+ ]
+ end
+
+ def mirror_params
+ params.require(:project).permit(mirror_params_attributes)
+ end
+end
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index bc13b8ad7ba..4d4c2af2415 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -8,19 +8,6 @@ class Projects::NotesController < Projects::ApplicationController
before_action :authorize_create_note!, only: [:create]
before_action :authorize_resolve_note!, only: [:resolve, :unresolve]
- #
- # This is a fix to make spinach feature tests passing:
- # Controller actions are returned from AbstractController::Base and methods of parent classes are
- # excluded in order to return only specific controller related methods.
- # That is ok for the app (no :create method in ancestors)
- # but fails for tests because there is a :create method on FactoryBot (one of the ancestors)
- #
- # see https://github.com/rails/rails/blob/v4.2.7/actionpack/lib/abstract_controller/base.rb#L78
- #
- def create
- super
- end
-
def delete_attachment
note.remove_attachment!
note.update_attribute(:attachment, nil)
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 78d109cf33e..f7417a6a5aa 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -87,7 +87,7 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def failures
- if @pipeline.statuses.latest.failed.present?
+ if @pipeline.failed_builds.present?
render_show
else
redirect_to pipeline_path(@pipeline)
@@ -104,9 +104,18 @@ class Projects::PipelinesController < Projects::ApplicationController
@stage = pipeline.legacy_stage(params[:stage])
return not_found unless @stage
- respond_to do |format|
- format.json { render json: { html: view_to_html_string('projects/pipelines/_stage') } }
- end
+ render json: StageSerializer
+ .new(project: @project, current_user: @current_user)
+ .represent(@stage, details: true)
+ end
+
+ # TODO: This endpoint is used by mini-pipeline-graph
+ # TODO: This endpoint should be migrated to `stage.json`
+ def stage_ajax
+ @stage = pipeline.legacy_stage(params[:stage])
+ return not_found unless @stage
+
+ render json: { html: view_to_html_string('projects/pipelines/_stage') }
end
def retry
@@ -157,7 +166,7 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def create_params
- params.require(:pipeline).permit(:ref)
+ params.require(:pipeline).permit(:ref, variables_attributes: %i[key secret_value])
end
def pipeline
@@ -172,4 +181,8 @@ class Projects::PipelinesController < Projects::ApplicationController
# Also see https://gitlab.com/gitlab-org/gitlab-ce/issues/42343
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42339')
end
+
+ def authorize_update_pipeline!
+ return access_denied! unless can?(current_user, :update_pipeline, @pipeline)
+ end
end
diff --git a/app/controllers/projects/runner_projects_controller.rb b/app/controllers/projects/runner_projects_controller.rb
index 3cb01405b05..0ec2490655f 100644
--- a/app/controllers/projects/runner_projects_controller.rb
+++ b/app/controllers/projects/runner_projects_controller.rb
@@ -8,7 +8,7 @@ class Projects::RunnerProjectsController < Projects::ApplicationController
return head(403) unless can?(current_user, :assign_runner, @runner)
- path = runners_path(project)
+ path = project_runners_path(project)
runner_project = @runner.assign_to(project, current_user)
if runner_project.persisted?
@@ -22,6 +22,6 @@ class Projects::RunnerProjectsController < Projects::ApplicationController
runner_project = project.runner_projects.find(params[:id])
runner_project.destroy
- redirect_to runners_path(project), status: 302
+ redirect_to project_runners_path(project), status: 302
end
end
diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb
index b9bbe7115c4..bef94cea989 100644
--- a/app/controllers/projects/runners_controller.rb
+++ b/app/controllers/projects/runners_controller.rb
@@ -1,6 +1,6 @@
class Projects::RunnersController < Projects::ApplicationController
before_action :authorize_admin_build!
- before_action :set_runner, only: [:edit, :update, :destroy, :pause, :resume, :show]
+ before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show]
layout 'project_settings'
@@ -13,7 +13,7 @@ class Projects::RunnersController < Projects::ApplicationController
def update
if Ci::UpdateRunnerService.new(@runner).update(runner_params)
- redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
+ redirect_to project_runner_path(@project, @runner), notice: 'Runner was successfully updated.'
else
render 'edit'
end
@@ -24,26 +24,27 @@ class Projects::RunnersController < Projects::ApplicationController
@runner.destroy
end
- redirect_to runners_path(@project), status: 302
+ redirect_to project_runners_path(@project), status: 302
end
def resume
if Ci::UpdateRunnerService.new(@runner).update(active: true)
- redirect_to runners_path(@project), notice: 'Runner was successfully updated.'
+ redirect_to project_runners_path(@project), notice: 'Runner was successfully updated.'
else
- redirect_to runners_path(@project), alert: 'Runner was not updated.'
+ redirect_to project_runners_path(@project), alert: 'Runner was not updated.'
end
end
def pause
if Ci::UpdateRunnerService.new(@runner).update(active: false)
- redirect_to runners_path(@project), notice: 'Runner was successfully updated.'
+ redirect_to project_runners_path(@project), notice: 'Runner was successfully updated.'
else
- redirect_to runners_path(@project), alert: 'Runner was not updated.'
+ redirect_to project_runners_path(@project), alert: 'Runner was not updated.'
end
end
def show
+ render 'shared/runners/show'
end
def toggle_shared_runners
@@ -60,7 +61,7 @@ class Projects::RunnersController < Projects::ApplicationController
protected
- def set_runner
+ def runner
@runner ||= project.runners.find(params[:id])
end
diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb
index f17056f13e0..4697af4f26a 100644
--- a/app/controllers/projects/settings/repository_controller.rb
+++ b/app/controllers/projects/settings/repository_controller.rb
@@ -2,6 +2,7 @@ module Projects
module Settings
class RepositoryController < Projects::ApplicationController
before_action :authorize_admin_project!
+ before_action :remote_mirror, only: [:show]
def show
render_show
@@ -25,6 +26,7 @@ module Projects
define_deploy_token
define_protected_refs
+ remote_mirror
render 'show'
end
@@ -41,6 +43,10 @@ module Projects
load_gon_index
end
+ def remote_mirror
+ @remote_mirror = project.remote_mirrors.first_or_initialize
+ end
+
def access_levels_options
{
create_access_levels: levels_for_dropdown,
diff --git a/app/controllers/sent_notifications_controller.rb b/app/controllers/sent_notifications_controller.rb
index 04c36b3ebfe..93a71103a09 100644
--- a/app/controllers/sent_notifications_controller.rb
+++ b/app/controllers/sent_notifications_controller.rb
@@ -17,16 +17,20 @@ class SentNotificationsController < ApplicationController
flash[:notice] = "You have been unsubscribed from this thread."
if current_user
- case noteable
- when Issue
- redirect_to issue_path(noteable)
- when MergeRequest
- redirect_to merge_request_path(noteable)
- else
- redirect_to root_path
- end
+ redirect_to noteable_path(noteable)
else
redirect_to new_user_session_path
end
end
+
+ def noteable_path(noteable)
+ case noteable
+ when Issue
+ issue_path(noteable)
+ when MergeRequest
+ merge_request_path(noteable)
+ else
+ root_path
+ end
+ end
end
diff --git a/app/controllers/users/terms_controller.rb b/app/controllers/users/terms_controller.rb
index 95c5c3432d5..ab685b9106e 100644
--- a/app/controllers/users/terms_controller.rb
+++ b/app/controllers/users/terms_controller.rb
@@ -3,6 +3,10 @@ module Users
include InternalRedirect
skip_before_action :enforce_terms!
+ skip_before_action :check_password_expiration
+ skip_before_action :check_two_factor_requirement
+ skip_before_action :require_email
+
before_action :terms
layout 'terms'
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 6aa307b4db4..aa4569500b8 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -258,4 +258,17 @@ module ApplicationHelper
_('You are on a read-only GitLab instance.')
end
+
+ def autocomplete_data_sources(object, noteable_type)
+ return {} unless object && noteable_type
+
+ {
+ members: members_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
+ issues: issues_project_autocomplete_sources_path(object),
+ merge_requests: merge_requests_project_autocomplete_sources_path(object),
+ labels: labels_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
+ milestones: milestones_project_autocomplete_sources_path(object),
+ commands: commands_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id])
+ }
+ end
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 1bf98d550b0..b948e431882 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -250,7 +250,8 @@ module ApplicationSettingsHelper
:version_check_enabled,
:allow_local_requests_from_hooks_and_services,
:enforce_terms,
- :terms
+ :terms,
+ :mirror_available
]
end
end
diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb
index 7e4eb06b99d..c24d340d184 100644
--- a/app/helpers/clusters_helper.rb
+++ b/app/helpers/clusters_helper.rb
@@ -2,4 +2,12 @@ module ClustersHelper
def has_multiple_clusters?(project)
false
end
+
+ def render_gcp_signup_offer
+ return unless show_gcp_signup_offer?
+
+ content_tag :section, class: 'no-animate expanded' do
+ render 'projects/clusters/gcp_signup_offer_banner'
+ end
+ end
end
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index 079b3cd3aa0..cb6f709c604 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -41,7 +41,7 @@ module EventsHelper
key = key.to_s
active = 'active' if @event_filter.active?(key)
link_opts = {
- class: "event-filter-link has-tooltip",
+ class: "event-filter-link",
id: "#{key}_event_filter",
title: tooltip
}
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index 40073f714ee..61e12b0f31e 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -19,14 +19,6 @@ module GitlabRoutingHelper
project_commits_path(project, ref_name, *args)
end
- def runners_path(project, *args)
- project_runners_path(project, *args)
- end
-
- def runner_path(runner, *args)
- project_runner_path(@project, runner, *args)
- end
-
def environment_path(environment, *args)
project_environment_path(environment.project, environment, *args)
end
diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb
index 36abfaf19a5..da5fe25c07d 100644
--- a/app/helpers/user_callouts_helper.rb
+++ b/app/helpers/user_callouts_helper.rb
@@ -1,11 +1,16 @@
module UserCalloutsHelper
GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'.freeze
+ GCP_SIGNUP_OFFER = 'gcp_signup_offer'.freeze
def show_gke_cluster_integration_callout?(project)
can?(current_user, :create_cluster, project) &&
!user_dismissed?(GKE_CLUSTER_INTEGRATION)
end
+ def show_gcp_signup_offer?
+ !user_dismissed?(GCP_SIGNUP_OFFER)
+ end
+
private
def user_dismissed?(feature_name)
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index e803cd3a8d8..ce9373f5883 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -42,22 +42,11 @@ module UsersHelper
items << :sign_out if current_user
- # TODO: Remove these conditions when the permissions are prevented in
- # https://gitlab.com/gitlab-org/gitlab-ce/issues/45849
- terms_not_enforced = !Gitlab::CurrentSettings
- .current_application_settings
- .enforce_terms?
- required_terms_accepted = terms_not_enforced || current_user.terms_accepted?
+ return items if current_user&.required_terms_not_accepted?
- items << :help if required_terms_accepted
-
- if can?(current_user, :read_user, current_user) && required_terms_accepted
- items << :profile
- end
-
- if can?(current_user, :update_user, current_user) && required_terms_accepted
- items << :settings
- end
+ items << :help
+ items << :profile if can?(current_user, :read_user, current_user)
+ items << :settings if can?(current_user, :update_user, current_user)
items
end
diff --git a/app/helpers/webpack_helper.rb b/app/helpers/webpack_helper.rb
index 8bcced70d63..e12e4ba70e9 100644
--- a/app/helpers/webpack_helper.rb
+++ b/app/helpers/webpack_helper.rb
@@ -1,12 +1,12 @@
-require 'webpack/rails/manifest'
+require 'gitlab/webpack/manifest'
module WebpackHelper
- def webpack_bundle_tag(bundle, force_same_domain: false)
- javascript_include_tag(*gitlab_webpack_asset_paths(bundle, force_same_domain: force_same_domain))
+ def webpack_bundle_tag(bundle)
+ javascript_include_tag(*webpack_entrypoint_paths(bundle))
end
def webpack_controller_bundle_tags
- bundles = []
+ chunks = []
action = case controller.action_name
when 'create' then 'new'
@@ -16,37 +16,44 @@ module WebpackHelper
route = [*controller.controller_path.split('/'), action].compact
- until route.empty?
+ until chunks.any? || route.empty?
+ entrypoint = "pages.#{route.join('.')}"
begin
- asset_paths = gitlab_webpack_asset_paths("pages.#{route.join('.')}", extension: 'js')
- bundles.unshift(*asset_paths)
- rescue Webpack::Rails::Manifest::EntryPointMissingError
+ chunks = webpack_entrypoint_paths(entrypoint, extension: 'js')
+ rescue Gitlab::Webpack::Manifest::AssetMissingError
# no bundle exists for this path
end
-
route.pop
end
- javascript_include_tag(*bundles)
+ if chunks.empty?
+ chunks = webpack_entrypoint_paths("default", extension: 'js')
+ end
+
+ javascript_include_tag(*chunks)
end
- # override webpack-rails gem helper until changes can make it upstream
- def gitlab_webpack_asset_paths(source, extension: nil, force_same_domain: false)
+ def webpack_entrypoint_paths(source, extension: nil, exclude_duplicates: true)
return "" unless source.present?
- paths = Webpack::Rails::Manifest.asset_paths(source)
+ paths = Gitlab::Webpack::Manifest.entrypoint_paths(source)
if extension
paths.select! { |p| p.ends_with? ".#{extension}" }
end
- unless force_same_domain
- force_host = webpack_public_host
- if force_host
- paths.map! { |p| "#{force_host}#{p}" }
- end
+ force_host = webpack_public_host
+ if force_host
+ paths.map! { |p| "#{force_host}#{p}" }
end
- paths
+ if exclude_duplicates
+ @used_paths ||= []
+ new_paths = paths - @used_paths
+ @used_paths += new_paths
+ new_paths
+ else
+ paths
+ end
end
def webpack_public_host
diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb
index 50e17fe7717..d9a6fe2a41e 100644
--- a/app/mailers/emails/notes.rb
+++ b/app/mailers/emails/notes.rb
@@ -43,7 +43,7 @@ module Emails
private
def note_target_url_options
- [@project, @note.noteable, anchor: "note_#{@note.id}"]
+ [@project || @group, @note.noteable, anchor: "note_#{@note.id}"]
end
def note_thread_options(recipient_id)
@@ -58,8 +58,9 @@ module Emails
# `note_id` is a `Note` when originating in `NotifyPreview`
@note = note_id.is_a?(Note) ? note_id : Note.find(note_id)
@project = @note.project
+ @group = @note.noteable.try(:group)
- if @project && @note.persisted?
+ if (@project || @group) && @note.persisted?
@sent_notification = SentNotification.record_note(@note, recipient_id, reply_key)
end
end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 3646e08a15f..1db1482d6b7 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -94,6 +94,7 @@ class Notify < BaseMailer
def subject(*extra)
subject = ""
subject << "#{@project.name} | " if @project
+ subject << "#{@group.name} | " if @group
subject << extra.join(' | ') if extra.present?
subject << " | #{Gitlab.config.gitlab.email_subject_suffix}" if Gitlab.config.gitlab.email_subject_suffix.present?
subject
@@ -117,10 +118,9 @@ class Notify < BaseMailer
@reason = headers['X-GitLab-NotificationReason']
if Gitlab::IncomingEmail.enabled? && @sent_notification
- address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key))
- address.display_name = @project.full_name
-
- headers['Reply-To'] = address
+ headers['Reply-To'] = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key)).tap do |address|
+ address.display_name = reply_display_name(model)
+ end
fallback_reply_message_id = "<reply-#{reply_key}@#{Gitlab.config.gitlab.host}>".freeze
headers['References'] ||= []
@@ -132,6 +132,11 @@ class Notify < BaseMailer
mail(headers)
end
+ # `model` is used on EE code
+ def reply_display_name(_model)
+ @project.full_name
+ end
+
# Send an email that starts a new conversation thread,
# with headers suitable for grouping by thread in email clients.
#
diff --git a/app/models/ability.rb b/app/models/ability.rb
index 618d4af4272..bb600eaccba 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -10,6 +10,14 @@ class Ability
end
end
+ # Given a list of users and a group this method returns the users that can
+ # read the given group.
+ def users_that_can_read_group(users, group)
+ DeclarativePolicy.subject_scope do
+ users.select { |u| allowed?(u, :read_group, group) }
+ end
+ end
+
# Given a list of users and a snippet this method returns the users that can
# read the given snippet.
def users_that_can_read_personal_snippet(users, snippet)
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index a734cc7a26b..451e512aef7 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -334,7 +334,8 @@ class ApplicationSetting < ActiveRecord::Base
gitaly_timeout_fast: 10,
gitaly_timeout_medium: 30,
gitaly_timeout_default: 55,
- allow_local_requests_from_hooks_and_services: false
+ allow_local_requests_from_hooks_and_services: false,
+ mirror_available: true
}
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 9000ad860e9..61c10c427dd 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -19,6 +19,7 @@ module Ci
has_one :last_deployment, -> { order('deployments.id DESC') }, as: :deployable, class_name: 'Deployment'
has_many :trace_sections, class_name: 'Ci::BuildTraceSection'
+ has_many :trace_chunks, class_name: 'Ci::BuildTraceChunk', foreign_key: :build_id
has_many :job_artifacts, class_name: 'Ci::JobArtifact', foreign_key: :job_id, dependent: :destroy, inverse_of: :job # rubocop:disable Cop/ActiveRecordDependent
has_one :job_artifacts_archive, -> { where(file_type: Ci::JobArtifact.file_types[:archive]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb
new file mode 100644
index 00000000000..4856f10846c
--- /dev/null
+++ b/app/models/ci/build_trace_chunk.rb
@@ -0,0 +1,180 @@
+module Ci
+ class BuildTraceChunk < ActiveRecord::Base
+ include FastDestroyAll
+ extend Gitlab::Ci::Model
+
+ belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id
+
+ default_value_for :data_store, :redis
+
+ WriteError = Class.new(StandardError)
+
+ CHUNK_SIZE = 128.kilobytes
+ CHUNK_REDIS_TTL = 1.week
+ WRITE_LOCK_RETRY = 10
+ WRITE_LOCK_SLEEP = 0.01.seconds
+ WRITE_LOCK_TTL = 1.minute
+
+ enum data_store: {
+ redis: 1,
+ db: 2
+ }
+
+ class << self
+ def redis_data_key(build_id, chunk_index)
+ "gitlab:ci:trace:#{build_id}:chunks:#{chunk_index}"
+ end
+
+ def redis_data_keys
+ redis.pluck(:build_id, :chunk_index).map do |data|
+ redis_data_key(data.first, data.second)
+ end
+ end
+
+ def redis_delete_data(keys)
+ return if keys.empty?
+
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.del(keys)
+ end
+ end
+
+ ##
+ # FastDestroyAll concerns
+ def begin_fast_destroy
+ redis_data_keys
+ end
+
+ ##
+ # FastDestroyAll concerns
+ def finalize_fast_destroy(keys)
+ redis_delete_data(keys)
+ end
+ end
+
+ ##
+ # Data is memoized for optimizing #size and #end_offset
+ def data
+ @data ||= get_data.to_s
+ end
+
+ def truncate(offset = 0)
+ raise ArgumentError, 'Offset is out of range' if offset > size || offset < 0
+ return if offset == size # Skip the following process as it doesn't affect anything
+
+ self.append("", offset)
+ end
+
+ def append(new_data, offset)
+ raise ArgumentError, 'Offset is out of range' if offset > size || offset < 0
+ raise ArgumentError, 'Chunk size overflow' if CHUNK_SIZE < (offset + new_data.bytesize)
+
+ set_data(data.byteslice(0, offset) + new_data)
+ end
+
+ def size
+ data&.bytesize.to_i
+ end
+
+ def start_offset
+ chunk_index * CHUNK_SIZE
+ end
+
+ def end_offset
+ start_offset + size
+ end
+
+ def range
+ (start_offset...end_offset)
+ end
+
+ def use_database!
+ in_lock do
+ break if db?
+ break unless size > 0
+
+ self.update!(raw_data: data, data_store: :db)
+ self.class.redis_delete_data([redis_data_key])
+ end
+ end
+
+ private
+
+ def get_data
+ if redis?
+ redis_data
+ elsif db?
+ raw_data
+ else
+ raise 'Unsupported data store'
+ end&.force_encoding(Encoding::BINARY) # Redis/Database return UTF-8 string as default
+ end
+
+ def set_data(value)
+ raise ArgumentError, 'too much data' if value.bytesize > CHUNK_SIZE
+
+ in_lock do
+ if redis?
+ redis_set_data(value)
+ elsif db?
+ self.raw_data = value
+ else
+ raise 'Unsupported data store'
+ end
+
+ @data = value
+
+ save! if changed?
+ end
+
+ schedule_to_db if full?
+ end
+
+ def schedule_to_db
+ return if db?
+
+ Ci::BuildTraceChunkFlushWorker.perform_async(id)
+ end
+
+ def full?
+ size == CHUNK_SIZE
+ end
+
+ def redis_data
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.get(redis_data_key)
+ end
+ end
+
+ def redis_set_data(data)
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set(redis_data_key, data, ex: CHUNK_REDIS_TTL)
+ end
+ end
+
+ def redis_data_key
+ self.class.redis_data_key(build_id, chunk_index)
+ end
+
+ def in_lock
+ write_lock_key = "trace_write:#{build_id}:chunks:#{chunk_index}"
+
+ lease = Gitlab::ExclusiveLease.new(write_lock_key, timeout: WRITE_LOCK_TTL)
+ retry_count = 0
+
+ until uuid = lease.try_obtain
+ # Keep trying until we obtain the lease. To prevent hammering Redis too
+ # much we'll wait for a bit between retries.
+ sleep(WRITE_LOCK_SLEEP)
+ break if WRITE_LOCK_RETRY < (retry_count += 1)
+ end
+
+ raise WriteError, 'Failed to obtain write lock' unless uuid
+
+ self.reload if self.persisted?
+ return yield
+ ensure
+ Gitlab::ExclusiveLease.cancel(write_lock_key, uuid)
+ end
+ end
+end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index e1b9bc76475..1f49764e7cc 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -32,15 +32,21 @@ module Ci
has_many :auto_canceled_pipelines, class_name: 'Ci::Pipeline', foreign_key: 'auto_canceled_by_id'
has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id'
+ accepts_nested_attributes_for :variables, reject_if: :persisted?
+
delegate :id, to: :project, prefix: true
delegate :full_path, to: :project, prefix: true
- validates :source, exclusion: { in: %w(unknown), unless: :importing? }, on: :create
validates :sha, presence: { unless: :importing? }
validates :ref, presence: { unless: :importing? }
validates :status, presence: { unless: :importing? }
validate :valid_commit_sha, unless: :importing?
+ # Replace validator below with
+ # `validates :source, presence: { unless: :importing? }, on: :create`
+ # when removing Gitlab.rails5? code.
+ validate :valid_source, unless: :importing?, on: :create
+
after_create :keep_around_commits, unless: :importing?
enum source: {
@@ -269,19 +275,39 @@ module Ci
end
def git_author_name
- commit.try(:author_name)
+ strong_memoize(:git_author_name) do
+ commit.try(:author_name)
+ end
end
def git_author_email
- commit.try(:author_email)
+ strong_memoize(:git_author_email) do
+ commit.try(:author_email)
+ end
end
def git_commit_message
- commit.try(:message)
+ strong_memoize(:git_commit_message) do
+ commit.try(:message)
+ end
end
def git_commit_title
- commit.try(:title)
+ strong_memoize(:git_commit_title) do
+ commit.try(:title)
+ end
+ end
+
+ def git_commit_full_title
+ strong_memoize(:git_commit_full_title) do
+ commit.try(:full_title)
+ end
+ end
+
+ def git_commit_description
+ strong_memoize(:git_commit_description) do
+ commit.try(:description)
+ end
end
def short_sha
@@ -491,6 +517,9 @@ module Ci
.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)
+ .append(key: 'CI_COMMIT_TITLE', value: git_commit_full_title)
+ .append(key: 'CI_COMMIT_DESCRIPTION', value: git_commit_description)
end
def queued_duration
@@ -576,5 +605,11 @@ module Ci
project.repository.keep_around(self.sha)
project.repository.keep_around(self.before_sha)
end
+
+ def valid_source
+ if source.nil? || source == "unknown"
+ errors.add(:source, "invalid source")
+ end
+ end
end
end
diff --git a/app/models/ci/pipeline_variable.rb b/app/models/ci/pipeline_variable.rb
index de5aae17a15..38e14ffbc0c 100644
--- a/app/models/ci/pipeline_variable.rb
+++ b/app/models/ci/pipeline_variable.rb
@@ -5,6 +5,8 @@ module Ci
belongs_to :pipeline
+ alias_attribute :secret_value, :value
+
validates :key, uniqueness: { scope: :pipeline_id }
end
end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 23078f1c3ed..bda69f85a78 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -58,6 +58,7 @@ module Ci
validate :tag_constraints
validate :either_projects_or_group
validates :access_level, presence: true
+ validates :runner_type, presence: true
acts_as_taggable
@@ -107,7 +108,13 @@ module Ci
end
def assign_to(project, current_user = nil)
- self.is_shared = false if shared?
+ if shared?
+ self.is_shared = false if shared?
+ self.runner_type = :project_type
+ elsif group_type?
+ raise ArgumentError, 'Transitioning a group runner to a project runner is not supported'
+ end
+
self.save
project.runner_projects.create(runner_id: self.id)
end
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index 16efe90fa27..b881b4eaf36 100644
--- a/app/models/clusters/applications/runner.rb
+++ b/app/models/clusters/applications/runner.rb
@@ -43,12 +43,20 @@ module Clusters
def create_and_assign_runner
transaction do
- project.runners.create!(name: 'kubernetes-cluster', tag_list: %w(kubernetes cluster)).tap do |runner|
+ project.runners.create!(runner_create_params).tap do |runner|
update!(runner_id: runner.id)
end
end
end
+ def runner_create_params
+ {
+ name: 'kubernetes-cluster',
+ runner_type: :project_type,
+ tag_list: %w(kubernetes cluster)
+ }
+ end
+
def gitlab_url
Gitlab::Routing.url_helpers.root_url(only_path: false)
end
diff --git a/app/models/concerns/fast_destroy_all.rb b/app/models/concerns/fast_destroy_all.rb
new file mode 100644
index 00000000000..7ea042c6742
--- /dev/null
+++ b/app/models/concerns/fast_destroy_all.rb
@@ -0,0 +1,91 @@
+##
+# This module is for replacing `dependent: :destroy` and `before_destroy` hooks.
+#
+# In general, `destroy_all` is inefficient because it calls each callback with `DELETE` queries i.e. O(n), whereas,
+# `delete_all` is efficient as it deletes all rows with a single `DELETE` query.
+#
+# It's better to use `delete_all` as our best practice, however,
+# if external data (e.g. ObjectStorage, FileStorage or Redis) are assosiated with database records,
+# it is difficult to accomplish it.
+#
+# This module defines a format to use `delete_all` and delete associated external data.
+# Here is an exmaple
+#
+# Situation
+# - `Project` has many `Ci::BuildTraceChunk` through `Ci::Build`
+# - `Ci::BuildTraceChunk` stores associated data in Redis, so it relies on `dependent: :destroy` and `before_destroy` for the deletion
+#
+# How to use
+# - Define `use_fast_destroy :build_trace_chunks` in `Project` model.
+# - Define `begin_fast_destroy` and `finalize_fast_destroy(params)` in `Ci::BuildTraceChunk` model.
+# - Use `fast_destroy_all` instead of `destroy` and `destroy_all`
+# - Remove `dependent: :destroy` and `before_destroy` as it's no longer need
+#
+# Expectation
+# - When a project is `destroy`ed, the associated trace_chunks will be deleted by `delete_all`,
+# and the associated data will be removed, too.
+# - When `fast_destroy_all` is called, it also performns as same.
+module FastDestroyAll
+ extend ActiveSupport::Concern
+
+ ForbiddenActionError = Class.new(StandardError)
+
+ included do
+ before_destroy do
+ raise ForbiddenActionError, '`destroy` and `destroy_all` are forbbiden. Please use `fast_destroy_all`'
+ end
+ end
+
+ class_methods do
+ ##
+ # This method delete rows and associated external data efficiently
+ #
+ # This method can replace `destroy` and `destroy_all` without having `after_destroy` hook
+ def fast_destroy_all
+ params = begin_fast_destroy
+
+ delete_all
+
+ finalize_fast_destroy(params)
+ end
+
+ ##
+ # This method returns identifiers to delete associated external data (e.g. file paths, redis keys)
+ #
+ # This method must be defined in fast destroyable model
+ def begin_fast_destroy
+ raise NotImplementedError
+ end
+
+ ##
+ # This method deletes associated external data with the identifiers returned by `begin_fast_destroy`
+ #
+ # This method must be defined in fast destroyable model
+ def finalize_fast_destroy(params)
+ raise NotImplementedError
+ end
+ end
+
+ module Helpers
+ extend ActiveSupport::Concern
+
+ class_methods do
+ ##
+ # This method is to be defined on models which have fast destroyable models as children,
+ # and let us avoid to use `dependent: :destroy` hook
+ def use_fast_destroy(relation)
+ before_destroy(prepend: true) do
+ perform_fast_destroy(public_send(relation)) # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+ end
+
+ def perform_fast_destroy(subject)
+ params = subject.begin_fast_destroy
+
+ run_after_commit do
+ subject.finalize_fast_destroy(params)
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb
index e48bc0be410..01b1ef9f82c 100644
--- a/app/models/concerns/participable.rb
+++ b/app/models/concerns/participable.rb
@@ -98,6 +98,10 @@ module Participable
participants.merge(ext.users)
+ filter_by_ability(participants)
+ end
+
+ def filter_by_ability(participants)
case self
when PersonalSnippet
Ability.users_that_can_read_personal_snippet(participants.to_a, self)
diff --git a/app/models/concerns/reactive_caching.rb b/app/models/concerns/reactive_caching.rb
index 2589215ad19..eef9caf1c8e 100644
--- a/app/models/concerns/reactive_caching.rb
+++ b/app/models/concerns/reactive_caching.rb
@@ -60,13 +60,16 @@ module ReactiveCaching
end
def with_reactive_cache(*args, &blk)
- within_reactive_cache_lifetime(*args) do
+ bootstrap = !within_reactive_cache_lifetime?(*args)
+ Rails.cache.write(alive_reactive_cache_key(*args), true, expires_in: self.class.reactive_cache_lifetime)
+
+ if bootstrap
+ ReactiveCachingWorker.perform_async(self.class, id, *args)
+ nil
+ else
data = Rails.cache.read(full_reactive_cache_key(*args))
yield data if data.present?
end
- ensure
- Rails.cache.write(alive_reactive_cache_key(*args), true, expires_in: self.class.reactive_cache_lifetime)
- ReactiveCachingWorker.perform_async(self.class, id, *args)
end
def clear_reactive_cache!(*args)
@@ -75,7 +78,7 @@ module ReactiveCaching
def exclusively_update_reactive_cache!(*args)
locking_reactive_cache(*args) do
- within_reactive_cache_lifetime(*args) do
+ if within_reactive_cache_lifetime?(*args)
enqueuing_update(*args) do
value = calculate_reactive_cache(*args)
Rails.cache.write(full_reactive_cache_key(*args), value)
@@ -105,8 +108,8 @@ module ReactiveCaching
Gitlab::ExclusiveLease.cancel(full_reactive_cache_key(*args), uuid)
end
- def within_reactive_cache_lifetime(*args)
- yield if Rails.cache.read(alive_reactive_cache_key(*args))
+ def within_reactive_cache_lifetime?(*args)
+ !!Rails.cache.read(alive_reactive_cache_key(*args))
end
def enqueuing_update(*args)
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index 915ad6959be..0176a12a131 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -4,7 +4,9 @@ module Routable
extend ActiveSupport::Concern
included do
- has_one :route, as: :source, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ # Remove `inverse_of: source` when upgraded to rails 5.2
+ # See https://github.com/rails/rails/pull/28808
+ has_one :route, as: :source, autosave: true, dependent: :destroy, inverse_of: :source # rubocop:disable Cop/ActiveRecordDependent
has_many :redirect_routes, as: :source, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
validates :route, presence: true
diff --git a/app/models/concerns/sha_attribute.rb b/app/models/concerns/sha_attribute.rb
index 703a72c355c..3796737427a 100644
--- a/app/models/concerns/sha_attribute.rb
+++ b/app/models/concerns/sha_attribute.rb
@@ -4,18 +4,34 @@ module ShaAttribute
module ClassMethods
def sha_attribute(name)
return if ENV['STATIC_VERIFICATION']
- return unless table_exists?
+
+ validate_binary_column_exists!(name) unless Rails.env.production?
+
+ attribute(name, Gitlab::Database::ShaAttribute.new)
+ end
+
+ # This only gets executed in non-production environments as an additional check to ensure
+ # the column is the correct type. In production it should behave like any other attribute.
+ # See https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/5502 for more discussion
+ def validate_binary_column_exists!(name)
+ unless table_exists?
+ warn "WARNING: sha_attribute #{name.inspect} is invalid since the table doesn't exist - you may need to run database migrations"
+ return
+ end
column = columns.find { |c| c.name == name.to_s }
- # In case the table doesn't exist we won't be able to find the column,
- # thus we will only check the type if the column is present.
- if column && column.type != :binary
- raise ArgumentError,
- "sha_attribute #{name.inspect} is invalid since the column type is not :binary"
+ unless column
+ warn "WARNING: sha_attribute #{name.inspect} is invalid since the column doesn't exist - you may need to run database migrations"
+ return
end
- attribute(name, Gitlab::Database::ShaAttribute.new)
+ unless column.type == :binary
+ raise ArgumentError.new("sha_attribute #{name.inspect} is invalid since the column type is not :binary")
+ end
+ rescue => error
+ Gitlab::AppLogger.error "ShaAttribute initialization: #{error.message}"
+ raise
end
end
end
diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb
index 5911b56c34c..73fc5048dcf 100644
--- a/app/models/concerns/time_trackable.rb
+++ b/app/models/concerns/time_trackable.rb
@@ -30,6 +30,8 @@ module TimeTrackable
return if @time_spent == 0
+ touch if touchable?
+
if @time_spent == :reset
reset_spent_time
else
@@ -53,6 +55,10 @@ module TimeTrackable
private
+ def touchable?
+ valid? && persisted?
+ end
+
def reset_spent_time
timelogs.new(time_spent: total_time_spent * -1, user: @time_spent_user) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
diff --git a/app/models/group.rb b/app/models/group.rb
index f493836a92e..cefca316399 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -241,6 +241,13 @@ class Group < Namespace
.where(source_id: self_and_descendants.reorder(nil).select(:id))
end
+ # Returns all members that are part of the group, it's subgroups, and ancestor groups
+ def direct_and_indirect_members
+ GroupMember
+ .active_without_invites_and_requests
+ .where(source_id: self_and_hierarchy.reorder(nil).select(:id))
+ end
+
def users_with_parents
User
.where(id: members_with_parents.select(:user_id))
@@ -253,6 +260,30 @@ class Group < Namespace
.reorder(nil)
end
+ # Returns all users that are members of the group because:
+ # 1. They belong to the group
+ # 2. They belong to a project that belongs to the group
+ # 3. They belong to a sub-group or project in such sub-group
+ # 4. They belong to an ancestor group
+ def direct_and_indirect_users
+ union = Gitlab::SQL::Union.new([
+ User
+ .where(id: direct_and_indirect_members.select(:user_id))
+ .reorder(nil),
+ project_users_with_descendants
+ ])
+
+ User.from("(#{union.to_sql}) #{User.table_name}")
+ end
+
+ # Returns all users that are members of projects
+ # belonging to the current group or sub-groups
+ def project_users_with_descendants
+ User
+ .joins(projects: :group)
+ .where(namespaces: { id: self_and_descendants.select(:id) })
+ end
+
def max_member_access_for_user(user)
return GroupMember::OWNER if user.admin?
diff --git a/app/models/member.rb b/app/models/member.rb
index eac4a22a03f..68572f2e33a 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -96,6 +96,17 @@ class Member < ActiveRecord::Base
joins(:user).merge(User.search(query))
end
+ def filter_by_2fa(value)
+ case value
+ when 'enabled'
+ left_join_users.merge(User.with_two_factor_indistinct)
+ when 'disabled'
+ left_join_users.merge(User.without_two_factor)
+ else
+ all
+ end
+ end
+
def sort_by_attribute(method)
case method.to_s
when 'access_level_asc' then reorder(access_level: :asc)
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 63c6ada86e1..628c61d5d69 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -1007,6 +1007,10 @@ class MergeRequest < ActiveRecord::Base
@merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha
end
+ def short_merge_commit_sha
+ Commit.truncate_sha(merge_commit_sha) if merge_commit_sha
+ end
+
def can_be_reverted?(current_user)
return false unless merge_commit
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 5621eeba7c4..3dad4277713 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -166,6 +166,13 @@ class Namespace < ActiveRecord::Base
projects.with_shared_runners.any?
end
+ # Returns all ancestors, self, and descendants of the current namespace.
+ def self_and_hierarchy
+ Gitlab::GroupHierarchy
+ .new(self.class.where(id: id))
+ .all_groups
+ end
+
# Returns all the ancestors of the current namespaces.
def ancestors
return self.class.none unless parent_id
diff --git a/app/models/note.rb b/app/models/note.rb
index e426f84832b..109405d3f17 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -317,10 +317,6 @@ class Note < ActiveRecord::Base
!system? && !for_snippet?
end
- def can_create_notification?
- true
- end
-
def discussion_class(noteable = nil)
# When commit notes are rendered on an MR's Discussion page, they are
# displayed in one discussion instead of individually.
diff --git a/app/models/project.rb b/app/models/project.rb
index b12b694aabd..32d34f5e9b8 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -22,6 +22,7 @@ class Project < ActiveRecord::Base
include DeploymentPlatform
include ::Gitlab::Utils::StrongMemoize
include ChronicDurationAttribute
+ include FastDestroyAll::Helpers
extend Gitlab::ConfigHelper
@@ -64,6 +65,9 @@ class Project < ActiveRecord::Base
default_value_for :only_allow_merge_if_all_discussions_are_resolved, false
add_authentication_token_field :runners_token
+
+ before_validation :mark_remote_mirrors_for_removal, if: -> { ActiveRecord::Base.connection.table_exists?(:remote_mirrors) }
+
before_save :ensure_runners_token
after_save :update_project_statistics, if: :namespace_id_changed?
@@ -81,6 +85,9 @@ class Project < ActiveRecord::Base
after_update :update_forks_visibility_level
before_destroy :remove_private_deploy_keys
+
+ use_fast_destroy :build_trace_chunks
+
after_destroy -> { run_after_commit { remove_pages } }
after_destroy :remove_exports
@@ -225,6 +232,7 @@ class Project < ActiveRecord::Base
# still using `dependent: :destroy` here.
has_many :builds, class_name: 'Ci::Build', inverse_of: :project, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :build_trace_section_names, class_name: 'Ci::BuildTraceSectionName'
+ has_many :build_trace_chunks, class_name: 'Ci::BuildTraceChunk', through: :builds, source: :trace_chunks
has_many :runner_projects, class_name: 'Ci::RunnerProject'
has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
has_many :variables, class_name: 'Ci::Variable'
@@ -241,11 +249,17 @@ class Project < ActiveRecord::Base
has_many :project_badges, class_name: 'ProjectBadge'
has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting', inverse_of: :project, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :remote_mirrors, inverse_of: :project
+
accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature, update_only: true
accepts_nested_attributes_for :import_data
accepts_nested_attributes_for :auto_devops, update_only: true
+ accepts_nested_attributes_for :remote_mirrors,
+ allow_destroy: true,
+ reject_if: ->(attrs) { attrs[:id].blank? && attrs[:url].blank? }
+
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true
delegate :add_user, :add_users, to: :team
@@ -335,6 +349,7 @@ class Project < ActiveRecord::Base
scope :with_issues_enabled, -> { with_feature_enabled(:issues) }
scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) }
scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) }
+ scope :with_remote_mirrors, -> { joins(:remote_mirrors).where(remote_mirrors: { enabled: true }).distinct }
scope :with_group_runners_enabled, -> do
joins(:ci_cd_settings)
@@ -642,49 +657,55 @@ class Project < ActiveRecord::Base
}
end
- def ensure_import_state
- return if self[:import_status] == 'none' || self[:import_status].nil?
+ def ensure_import_state(force: false)
+ return if !force && (self[:import_status] == 'none' || self[:import_status].nil?)
return unless import_state.nil?
- create_import_state(import_state_args)
+ if persisted?
+ create_import_state(import_state_args)
+
+ update_column(:import_status, 'none')
+ else
+ build_import_state(import_state_args)
- update_column(:import_status, 'none')
+ self[:import_status] = 'none'
+ end
end
def import_schedule
- ensure_import_state
+ ensure_import_state(force: true)
- import_state&.schedule
+ import_state.schedule
end
def force_import_start
- ensure_import_state
+ ensure_import_state(force: true)
- import_state&.force_start
+ import_state.force_start
end
def import_start
- ensure_import_state
+ ensure_import_state(force: true)
- import_state&.start
+ import_state.start
end
def import_fail
- ensure_import_state
+ ensure_import_state(force: true)
- import_state&.fail_op
+ import_state.fail_op
end
def import_finish
- ensure_import_state
+ ensure_import_state(force: true)
- import_state&.finish
+ import_state.finish
end
def import_jid=(new_jid)
- ensure_import_state
+ ensure_import_state(force: true)
- import_state&.jid = new_jid
+ import_state.jid = new_jid
end
def import_jid
@@ -694,9 +715,9 @@ class Project < ActiveRecord::Base
end
def import_error=(new_error)
- ensure_import_state
+ ensure_import_state(force: true)
- import_state&.last_error = new_error
+ import_state.last_error = new_error
end
def import_error
@@ -706,9 +727,9 @@ class Project < ActiveRecord::Base
end
def import_status=(new_status)
- ensure_import_state
+ ensure_import_state(force: true)
- import_state&.status = new_status
+ import_state.status = new_status
end
def import_status
@@ -754,6 +775,37 @@ class Project < ActiveRecord::Base
import_type == 'gitea'
end
+ def has_remote_mirror?
+ remote_mirror_available? && remote_mirrors.enabled.exists?
+ end
+
+ def updating_remote_mirror?
+ remote_mirrors.enabled.started.exists?
+ end
+
+ def update_remote_mirrors
+ return unless remote_mirror_available?
+
+ remote_mirrors.enabled.each(&:sync)
+ end
+
+ def mark_stuck_remote_mirrors_as_failed!
+ remote_mirrors.stuck.update_all(
+ update_status: :failed,
+ last_error: 'The remote mirror took to long to complete.',
+ last_update_at: Time.now
+ )
+ end
+
+ def mark_remote_mirrors_for_removal
+ remote_mirrors.each(&:mark_for_delete_if_blank_url)
+ end
+
+ def remote_mirror_available?
+ remote_mirror_available_overridden ||
+ ::Gitlab::CurrentSettings.mirror_available
+ end
+
def check_limit
unless creator.can_create_project? || namespace.kind == 'group'
projects_limit = creator.projects_limit
diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb
new file mode 100644
index 00000000000..bbf8fd9c6a7
--- /dev/null
+++ b/app/models/remote_mirror.rb
@@ -0,0 +1,219 @@
+class RemoteMirror < ActiveRecord::Base
+ include AfterCommitQueue
+
+ PROTECTED_BACKOFF_DELAY = 1.minute
+ UNPROTECTED_BACKOFF_DELAY = 5.minutes
+
+ attr_encrypted :credentials,
+ key: Gitlab::Application.secrets.db_key_base,
+ marshal: true,
+ encode: true,
+ mode: :per_attribute_iv_and_salt,
+ insecure_mode: true,
+ algorithm: 'aes-256-cbc'
+
+ default_value_for :only_protected_branches, true
+
+ belongs_to :project, inverse_of: :remote_mirrors
+
+ validates :url, presence: true, url: { protocols: %w(ssh git http https), allow_blank: true }
+ validates :url, addressable_url: true, if: :url_changed?
+
+ before_save :set_new_remote_name, if: :mirror_url_changed?
+
+ after_save :set_override_remote_mirror_available, unless: -> { Gitlab::CurrentSettings.current_application_settings.mirror_available }
+ after_save :refresh_remote, if: :mirror_url_changed?
+ after_update :reset_fields, if: :mirror_url_changed?
+
+ after_commit :remove_remote, on: :destroy
+
+ scope :enabled, -> { where(enabled: true) }
+ scope :started, -> { with_update_status(:started) }
+ scope :stuck, -> { started.where('last_update_at < ? OR (last_update_at IS NULL AND updated_at < ?)', 1.day.ago, 1.day.ago) }
+
+ state_machine :update_status, initial: :none do
+ event :update_start do
+ transition [:none, :finished, :failed] => :started
+ end
+
+ event :update_finish do
+ transition started: :finished
+ end
+
+ event :update_fail do
+ transition started: :failed
+ end
+
+ state :started
+ state :finished
+ state :failed
+
+ after_transition any => :started do |remote_mirror, _|
+ Gitlab::Metrics.add_event(:remote_mirrors_running, path: remote_mirror.project.full_path)
+
+ remote_mirror.update(last_update_started_at: Time.now)
+ end
+
+ after_transition started: :finished do |remote_mirror, _|
+ Gitlab::Metrics.add_event(:remote_mirrors_finished, path: remote_mirror.project.full_path)
+
+ timestamp = Time.now
+ remote_mirror.update_attributes!(
+ last_update_at: timestamp, last_successful_update_at: timestamp, last_error: nil
+ )
+ end
+
+ after_transition started: :failed do |remote_mirror, _|
+ Gitlab::Metrics.add_event(:remote_mirrors_failed, path: remote_mirror.project.full_path)
+
+ remote_mirror.update(last_update_at: Time.now)
+ end
+ end
+
+ def remote_name
+ super || fallback_remote_name
+ end
+
+ def update_failed?
+ update_status == 'failed'
+ end
+
+ def update_in_progress?
+ update_status == 'started'
+ end
+
+ def update_repository(options)
+ raw.update(options)
+ end
+
+ def sync?
+ enabled?
+ end
+
+ def sync
+ return unless sync?
+
+ if recently_scheduled?
+ RepositoryUpdateRemoteMirrorWorker.perform_in(backoff_delay, self.id, Time.now)
+ else
+ RepositoryUpdateRemoteMirrorWorker.perform_async(self.id, Time.now)
+ end
+ end
+
+ def enabled
+ return false unless project && super
+ return false unless project.remote_mirror_available?
+ return false unless project.repository_exists?
+ return false if project.pending_delete?
+
+ true
+ end
+ alias_method :enabled?, :enabled
+
+ def updated_since?(timestamp)
+ last_update_started_at && last_update_started_at > timestamp && !update_failed?
+ end
+
+ def mark_for_delete_if_blank_url
+ mark_for_destruction if url.blank?
+ end
+
+ def mark_as_failed(error_message)
+ update_fail
+ update_column(:last_error, Gitlab::UrlSanitizer.sanitize(error_message))
+ end
+
+ def url=(value)
+ super(value) && return unless Gitlab::UrlSanitizer.valid?(value)
+
+ mirror_url = Gitlab::UrlSanitizer.new(value)
+ self.credentials = mirror_url.credentials
+
+ super(mirror_url.sanitized_url)
+ end
+
+ def url
+ if super
+ Gitlab::UrlSanitizer.new(super, credentials: credentials).full_url
+ end
+ rescue
+ super
+ end
+
+ def safe_url
+ return if url.nil?
+
+ result = URI.parse(url)
+ result.password = '*****' if result.password
+ result.user = '*****' if result.user && result.user != "git" # tokens or other data may be saved as user
+ result.to_s
+ end
+
+ private
+
+ def raw
+ @raw ||= Gitlab::Git::RemoteMirror.new(project.repository.raw, remote_name)
+ end
+
+ def fallback_remote_name
+ return unless id
+
+ "remote_mirror_#{id}"
+ end
+
+ def recently_scheduled?
+ return false unless self.last_update_started_at
+
+ self.last_update_started_at >= Time.now - backoff_delay
+ end
+
+ def backoff_delay
+ if self.only_protected_branches
+ PROTECTED_BACKOFF_DELAY
+ else
+ UNPROTECTED_BACKOFF_DELAY
+ end
+ end
+
+ def reset_fields
+ update_columns(
+ last_error: nil,
+ last_update_at: nil,
+ last_successful_update_at: nil,
+ update_status: 'finished'
+ )
+ end
+
+ def set_override_remote_mirror_available
+ enabled = read_attribute(:enabled)
+
+ project.update(remote_mirror_available_overridden: enabled)
+ end
+
+ def set_new_remote_name
+ self.remote_name = "remote_mirror_#{SecureRandom.hex}"
+ end
+
+ def refresh_remote
+ return unless project
+
+ # Before adding a new remote we have to delete the data from
+ # the previous remote name
+ prev_remote_name = remote_name_was || fallback_remote_name
+ run_after_commit do
+ project.repository.async_remove_remote(prev_remote_name)
+ end
+
+ project.repository.add_remote(remote_name, url)
+ end
+
+ def remove_remote
+ return unless project # could be pending to delete so don't need to touch the git repository
+
+ project.repository.async_remove_remote(remote_name)
+ end
+
+ def mirror_url_changed?
+ url_changed? || encrypted_credentials_changed?
+ end
+end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 6831305fb93..44c6bff6b66 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -37,7 +37,7 @@ class Repository
changelog license_blob license_key gitignore koding_yml
gitlab_ci_yml branch_names tag_names branch_count
tag_count avatar exists? root_ref has_visible_content?
- issue_template_names merge_request_template_names).freeze
+ issue_template_names merge_request_template_names xcode_project?).freeze
# Methods that use cache_method but only memoize the value
MEMOIZED_CACHED_METHODS = %i(license).freeze
@@ -55,7 +55,8 @@ class Repository
gitlab_ci: :gitlab_ci_yml,
avatar: :avatar,
issue_template: :issue_template_names,
- merge_request_template: :merge_request_template_names
+ merge_request_template: :merge_request_template_names,
+ xcode_config: :xcode_project?
}.freeze
def initialize(full_path, project, disk_path: nil, is_wiki: false)
@@ -594,6 +595,11 @@ class Repository
end
cache_method :gitlab_ci_yml
+ def xcode_project?
+ file_on_head(:xcode_config).present?
+ end
+ cache_method :xcode_project?
+
def head_commit
@head_commit ||= commit(self.root_ref)
end
@@ -854,13 +860,27 @@ class Repository
add_remote(remote_name, url, mirror_refmap: refmap)
fetch_remote(remote_name, forced: forced, prune: prune)
ensure
- remove_remote(remote_name) if tmp_remote_name
+ async_remove_remote(remote_name) if tmp_remote_name
end
def fetch_remote(remote, forced: false, ssh_auth: nil, no_tags: false, prune: true)
gitlab_shell.fetch_remote(raw_repository, remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags, prune: prune)
end
+ def async_remove_remote(remote_name)
+ return unless remote_name
+
+ job_id = RepositoryRemoveRemoteWorker.perform_async(project.id, remote_name)
+
+ if job_id
+ Rails.logger.info("Remove remote job scheduled for #{project.id} with remote name: #{remote_name} job ID #{job_id}.")
+ else
+ Rails.logger.info("Remove remote job failed to create for #{project.id} with remote name #{remote_name}.")
+ end
+
+ job_id
+ end
+
def fetch_source_branch!(source_repository, source_branch, local_ref)
raw_repository.fetch_source_branch!(source_repository.raw_repository, source_branch, local_ref)
end
diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb
index 6e311806be1..3da7c301d28 100644
--- a/app/models/sent_notification.rb
+++ b/app/models/sent_notification.rb
@@ -5,14 +5,14 @@ class SentNotification < ActiveRecord::Base
belongs_to :noteable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :recipient, class_name: "User"
- validates :project, :recipient, presence: true
+ validates :recipient, presence: true
validates :reply_key, presence: true, uniqueness: true
validates :noteable_id, presence: true, unless: :for_commit?
validates :commit_id, presence: true, if: :for_commit?
validates :in_reply_to_discussion_id, format: { with: /\A\h{40}\z/, allow_nil: true }
validate :note_valid
- after_save :keep_around_commit
+ after_save :keep_around_commit, if: :for_commit?
class << self
def reply_key
diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb
index 29035480371..1c2161accc4 100644
--- a/app/models/system_note_metadata.rb
+++ b/app/models/system_note_metadata.rb
@@ -17,7 +17,11 @@ class SystemNoteMetadata < ActiveRecord::Base
].freeze
validates :note, presence: true
- validates :action, inclusion: ICON_TYPES, allow_nil: true
+ validates :action, inclusion: { in: :icon_types }, allow_nil: true
belongs_to :note
+
+ def icon_types
+ ICON_TYPES
+ end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index a9cfd39f604..173ab38e20c 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -237,14 +237,18 @@ class User < ActiveRecord::Base
scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) }
scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) }
- def self.with_two_factor
+ def self.with_two_factor_indistinct
joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id")
- .where("u2f.id IS NOT NULL OR otp_required_for_login = ?", true).distinct(arel_table[:id])
+ .where("u2f.id IS NOT NULL OR users.otp_required_for_login = ?", true)
+ end
+
+ def self.with_two_factor
+ with_two_factor_indistinct.distinct(arel_table[:id])
end
def self.without_two_factor
joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id")
- .where("u2f.id IS NULL AND otp_required_for_login = ?", false)
+ .where("u2f.id IS NULL AND users.otp_required_for_login = ?", false)
end
#
@@ -1093,8 +1097,11 @@ class User < ActiveRecord::Base
# <https://github.com/plataformatec/devise/blob/v4.0.0/lib/devise/models/lockable.rb#L92>
#
def increment_failed_attempts!
+ return if ::Gitlab::Database.read_only?
+
self.failed_attempts ||= 0
self.failed_attempts += 1
+
if attempts_exceeded?
lock_access! unless access_locked?
else
@@ -1193,6 +1200,11 @@ class User < ActiveRecord::Base
accepted_term_id.present?
end
+ def required_terms_not_accepted?
+ Gitlab::CurrentSettings.current_application_settings.enforce_terms? &&
+ !terms_accepted?
+ end
+
protected
# override, from Devise::Validatable
diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb
index e4b69382626..9d461c6750a 100644
--- a/app/models/user_callout.rb
+++ b/app/models/user_callout.rb
@@ -2,7 +2,8 @@ class UserCallout < ActiveRecord::Base
belongs_to :user
enum feature_name: {
- gke_cluster_integration: 1
+ gke_cluster_integration: 1,
+ gcp_signup_offer: 2
}
validates :user, presence: true
diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb
index 808a81cbbf9..8b65758f3e8 100644
--- a/app/policies/ci/build_policy.rb
+++ b/app/policies/ci/build_policy.rb
@@ -14,11 +14,20 @@ module Ci
@subject.triggered_by?(@user)
end
+ condition(:branch_allows_maintainer_push) do
+ @subject.project.branch_allows_maintainer_push?(@user, @subject.ref)
+ end
+
rule { protected_ref }.policy do
prevent :update_build
prevent :erase_build
end
rule { can?(:admin_build) | (can?(:update_build) & owner_of_job) }.enable :erase_build
+
+ rule { can?(:public_access) & branch_allows_maintainer_push }.policy do
+ enable :update_build
+ enable :update_commit_status
+ end
end
end
diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb
index 6363c382ff8..540e4235299 100644
--- a/app/policies/ci/pipeline_policy.rb
+++ b/app/policies/ci/pipeline_policy.rb
@@ -4,8 +4,16 @@ module Ci
condition(:protected_ref) { ref_protected?(@user, @subject.project, @subject.tag?, @subject.ref) }
+ condition(:branch_allows_maintainer_push) do
+ @subject.project.branch_allows_maintainer_push?(@user, @subject.ref)
+ end
+
rule { protected_ref }.prevent :update_pipeline
+ rule { can?(:public_access) & branch_allows_maintainer_push }.policy do
+ enable :update_pipeline
+ end
+
def ref_protected?(user, project, tag, ref)
access = ::Gitlab::UserAccess.new(user, project: project)
diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb
index 64e550d19d0..1cf5515d9d7 100644
--- a/app/policies/global_policy.rb
+++ b/app/policies/global_policy.rb
@@ -1,22 +1,24 @@
class GlobalPolicy < BasePolicy
desc "User is blocked"
with_options scope: :user, score: 0
- condition(:blocked) { @user.blocked? }
+ condition(:blocked) { @user&.blocked? }
desc "User is an internal user"
with_options scope: :user, score: 0
- condition(:internal) { @user.internal? }
+ condition(:internal) { @user&.internal? }
desc "User's access has been locked"
with_options scope: :user, score: 0
- condition(:access_locked) { @user.access_locked? }
+ condition(:access_locked) { @user&.access_locked? }
- condition(:can_create_fork, scope: :user) { @user.manageable_namespaces.any? { |namespace| @user.can?(:create_projects, namespace) } }
+ condition(:can_create_fork, scope: :user) { @user && @user.manageable_namespaces.any? { |namespace| @user.can?(:create_projects, namespace) } }
+
+ condition(:required_terms_not_accepted, scope: :user, score: 0) do
+ @user&.required_terms_not_accepted?
+ end
rule { anonymous }.policy do
prevent :log_in
- prevent :access_api
- prevent :access_git
prevent :receive_notifications
prevent :use_quick_actions
prevent :create_group
@@ -38,6 +40,11 @@ class GlobalPolicy < BasePolicy
prevent :use_quick_actions
end
+ rule { required_terms_not_accepted }.policy do
+ prevent :access_api
+ prevent :access_git
+ end
+
rule { can_create_group }.policy do
enable :create_group
end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 3529d0aa60c..99a0d7118f2 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -76,10 +76,15 @@ class ProjectPolicy < BasePolicy
condition(:request_access_enabled, scope: :subject, score: 0) { project.request_access_enabled }
desc "Has merge requests allowing pushes to user"
- condition(:has_merge_requests_allowing_pushes, scope: :subject) do
+ condition(:has_merge_requests_allowing_pushes) do
project.merge_requests_allowing_push_to_user(user).any?
end
+ with_scope :global
+ condition(:mirror_available, score: 0) do
+ ::Gitlab::CurrentSettings.current_application_settings.mirror_available
+ end
+
# We aren't checking `:read_issue` or `:read_merge_request` in this case
# because it could be possible for a user to see an issuable-iid
# (`:read_issue_iid` or `:read_merge_request_iid`) but then wouldn't be
@@ -246,6 +251,8 @@ class ProjectPolicy < BasePolicy
enable :create_cluster
end
+ rule { (mirror_available & can?(:admin_project)) | admin }.enable :admin_remote_mirror
+
rule { archived }.policy do
prevent :push_code
prevent :push_to_delete_protected_branch
@@ -347,9 +354,7 @@ class ProjectPolicy < BasePolicy
# to run pipelines for the branches they have access to.
rule { can?(:public_access) & has_merge_requests_allowing_pushes }.policy do
enable :create_build
- enable :update_build
enable :create_pipeline
- enable :update_pipeline
end
rule do
diff --git a/app/presenters/ci/pipeline_presenter.rb b/app/presenters/ci/pipeline_presenter.rb
index 099b4720fb6..cc2bce9862d 100644
--- a/app/presenters/ci/pipeline_presenter.rb
+++ b/app/presenters/ci/pipeline_presenter.rb
@@ -1,11 +1,21 @@
module Ci
class PipelinePresenter < Gitlab::View::Presenter::Delegated
+ include Gitlab::Utils::StrongMemoize
+
FAILURE_REASONS = {
config_error: 'CI/CD YAML configuration error!'
}.freeze
presents :pipeline
+ def failed_builds
+ return [] unless can?(current_user, :read_build, pipeline)
+
+ strong_memoize(:failed_builds) do
+ pipeline.builds.latest.failed
+ end
+ end
+
def failure_reason
return unless pipeline.failure_reason?
diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb
index 4a812e39ee1..d0165c148eb 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -2,6 +2,7 @@ class MergeRequestWidgetEntity < IssuableEntity
expose :state
expose :in_progress_merge_commit_sha
expose :merge_commit_sha
+ expose :short_merge_commit_sha
expose :merge_error
expose :merge_params
expose :merge_status
@@ -207,6 +208,12 @@ class MergeRequestWidgetEntity < IssuableEntity
commit_change_content_project_merge_request_path(merge_request.project, merge_request)
end
+ expose :merge_commit_path do |merge_request|
+ if merge_request.merge_commit_sha
+ project_commit_path(merge_request.project, merge_request.merge_commit_sha)
+ end
+ end
+
private
delegate :current_user, to: :request
diff --git a/app/serializers/project_mirror_entity.rb b/app/serializers/project_mirror_entity.rb
new file mode 100644
index 00000000000..a9c08ac021a
--- /dev/null
+++ b/app/serializers/project_mirror_entity.rb
@@ -0,0 +1,11 @@
+class ProjectMirrorEntity < Grape::Entity
+ expose :id
+
+ expose :remote_mirrors_attributes do |project|
+ next [] unless project.remote_mirrors.present?
+
+ project.remote_mirrors.map do |remote|
+ remote.as_json(only: %i[id url enabled])
+ end
+ end
+end
diff --git a/app/serializers/stage_entity.rb b/app/serializers/stage_entity.rb
index 4523b15152e..2516df70ad9 100644
--- a/app/serializers/stage_entity.rb
+++ b/app/serializers/stage_entity.rb
@@ -11,6 +11,12 @@ class StageEntity < Grape::Entity
if: -> (_, opts) { opts[:grouped] },
with: JobGroupEntity
+ expose :latest_statuses,
+ if: -> (_, opts) { opts[:details] },
+ with: JobEntity do |stage|
+ latest_statuses
+ end
+
expose :detailed_status, as: :status, with: StatusEntity
expose :path do |stage|
@@ -35,4 +41,14 @@ class StageEntity < Grape::Entity
def detailed_status
stage.detailed_status(request.current_user)
end
+
+ def grouped_statuses
+ @grouped_statuses ||= stage.statuses.latest_ordered.group_by(&:status)
+ end
+
+ def latest_statuses
+ HasStatus::ORDERED_STATUSES.map do |ordered_status|
+ grouped_statuses.fetch(ordered_status, [])
+ end.flatten
+ end
end
diff --git a/app/serializers/stage_serializer.rb b/app/serializers/stage_serializer.rb
new file mode 100644
index 00000000000..091d8e91e43
--- /dev/null
+++ b/app/serializers/stage_serializer.rb
@@ -0,0 +1,7 @@
+class StageSerializer < BaseSerializer
+ include WithPagination
+
+ InvalidResourceError = Class.new(StandardError)
+
+ entity StageEntity
+end
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index 6ce86983287..17a53b6a8fd 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -24,6 +24,7 @@ module Ci
ignore_skip_ci: ignore_skip_ci,
save_incompleted: save_on_errors,
seeds_block: block,
+ variables_attributes: params[:variables_attributes],
project: project,
current_user: current_user)
diff --git a/app/services/concerns/exclusive_lease_guard.rb b/app/services/concerns/exclusive_lease_guard.rb
new file mode 100644
index 00000000000..30be6accc32
--- /dev/null
+++ b/app/services/concerns/exclusive_lease_guard.rb
@@ -0,0 +1,52 @@
+#
+# Concern that helps with getting an exclusive lease for running a block
+# of code.
+#
+# `#try_obtain_lease` takes a block which will be run if it was able to
+# obtain the lease. Implement `#lease_timeout` to configure the timeout
+# for the exclusive lease. Optionally override `#lease_key` to set the
+# lease key, it defaults to the class name with underscores.
+#
+module ExclusiveLeaseGuard
+ extend ActiveSupport::Concern
+
+ def try_obtain_lease
+ lease = exclusive_lease.try_obtain
+
+ unless lease
+ log_error('Cannot obtain an exclusive lease. There must be another instance already in execution.')
+ return
+ end
+
+ begin
+ yield lease
+ ensure
+ release_lease(lease)
+ end
+ end
+
+ def exclusive_lease
+ @lease ||= Gitlab::ExclusiveLease.new(lease_key, timeout: lease_timeout)
+ end
+
+ def lease_key
+ @lease_key ||= self.class.name.underscore
+ end
+
+ def lease_timeout
+ raise NotImplementedError,
+ "#{self.class.name} does not implement #{__method__}"
+ end
+
+ def release_lease(uuid)
+ Gitlab::ExclusiveLease.cancel(lease_key, uuid)
+ end
+
+ def renew_lease!
+ exclusive_lease.renew
+ end
+
+ def log_error(message, extra_args = {})
+ logger.error(message)
+ end
+end
diff --git a/app/services/concerns/users/participable_service.rb b/app/services/concerns/users/participable_service.rb
new file mode 100644
index 00000000000..bf60b96938d
--- /dev/null
+++ b/app/services/concerns/users/participable_service.rb
@@ -0,0 +1,41 @@
+module Users
+ module ParticipableService
+ extend ActiveSupport::Concern
+
+ included do
+ attr_reader :noteable
+ end
+
+ def noteable_owner
+ return [] unless noteable && noteable.author.present?
+
+ [as_hash(noteable.author)]
+ end
+
+ def participants_in_noteable
+ return [] unless noteable
+
+ users = noteable.participants(current_user)
+ sorted(users)
+ end
+
+ def sorted(users)
+ users.uniq.to_a.compact.sort_by(&:username).map do |user|
+ as_hash(user)
+ end
+ end
+
+ def groups
+ current_user.authorized_groups.sort_by(&:path).map do |group|
+ count = group.users.count
+ { username: group.full_path, name: group.full_name, count: count, avatar_url: group.avatar_url }
+ end
+ end
+
+ private
+
+ def as_hash(user)
+ { username: user.username, name: user.name, avatar_url: user.avatar_url }
+ end
+ end
+end
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index c037141fcde..f3bfc53dcd3 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -55,6 +55,7 @@ class GitPushService < BaseService
execute_related_hooks
perform_housekeeping
+ update_remote_mirrors
update_caches
update_signatures
@@ -119,6 +120,13 @@ class GitPushService < BaseService
protected
+ def update_remote_mirrors
+ return unless @project.has_remote_mirror?
+
+ @project.mark_stuck_remote_mirrors_as_failed!
+ @project.update_remote_mirrors
+ end
+
def execute_related_hooks
# Update merge requests that may be affected by this push. A new branch
# could cause the last commit of a merge request to change.
diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb
index 83e59a649b6..5658699664d 100644
--- a/app/services/notification_recipient_service.rb
+++ b/app/services/notification_recipient_service.rb
@@ -45,6 +45,10 @@ module NotificationRecipientService
target.project
end
+ def group
+ project&.group || target.try(:group)
+ end
+
def recipients
@recipients ||= []
end
@@ -67,6 +71,7 @@ module NotificationRecipientService
user, type,
reason: reason,
project: project,
+ group: group,
custom_action: custom_action,
target: target,
acting_user: acting_user
@@ -107,11 +112,11 @@ module NotificationRecipientService
# Users with a notification setting on group or project
user_ids += user_ids_notifiable_on(project, :custom)
- user_ids += user_ids_notifiable_on(project.group, :custom)
+ user_ids += user_ids_notifiable_on(group, :custom)
# Users with global level custom
user_ids_with_project_level_global = user_ids_notifiable_on(project, :global)
- user_ids_with_group_level_global = user_ids_notifiable_on(project.group, :global)
+ user_ids_with_group_level_global = user_ids_notifiable_on(group, :global)
global_users_ids = user_ids_with_project_level_global.concat(user_ids_with_group_level_global)
user_ids += user_ids_with_global_level_custom(global_users_ids, custom_action)
@@ -123,6 +128,10 @@ module NotificationRecipientService
add_recipients(project_watchers, :watch, nil)
end
+ def add_group_watchers
+ add_recipients(group_watchers, :watch, nil)
+ end
+
# Get project users with WATCH notification level
def project_watchers
project_members_ids = user_ids_notifiable_on(project)
@@ -138,6 +147,14 @@ module NotificationRecipientService
user_scope.where(id: user_ids_with_project_setting.concat(user_ids_with_group_setting).uniq)
end
+ def group_watchers
+ user_ids_with_group_global = user_ids_notifiable_on(group, :global)
+ user_ids = user_ids_with_global_level_watch(user_ids_with_group_global)
+ user_ids_with_group_setting = select_group_members_ids(group, [], user_ids_with_group_global, user_ids)
+
+ user_scope.where(id: user_ids_with_group_setting)
+ end
+
def add_subscribed_users
return unless target.respond_to? :subscribers
@@ -281,6 +298,14 @@ module NotificationRecipientService
note.project
end
+ def group
+ if note.for_project_noteable?
+ project.group
+ else
+ target.try(:group)
+ end
+ end
+
def build!
# Add all users participating in the thread (author, assignee, comment authors)
add_participants(note.author)
@@ -289,11 +314,11 @@ module NotificationRecipientService
if note.for_project_noteable?
# Merge project watchers
add_project_watchers
-
- # Merge project with custom notification
- add_custom_notifications
+ else
+ add_group_watchers
end
+ add_custom_notifications
add_subscribed_users
end
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index 71c93660b4b..adbc498d0bf 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -87,7 +87,7 @@ module Projects
new_path = removal_path(path)
if mv_repository(path, new_path)
- log_info("Repository \"#{path}\" moved to \"#{new_path}\"")
+ log_info(%Q{Repository "#{path}" moved to "#{new_path}" for project "#{project.full_path}"})
project.run_after_commit do
# self is now project
diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb
index e6193fcacee..eb0472c6024 100644
--- a/app/services/projects/participants_service.rb
+++ b/app/services/projects/participants_service.rb
@@ -1,6 +1,6 @@
module Projects
class ParticipantsService < BaseService
- attr_reader :noteable
+ include Users::ParticipableService
def execute(noteable)
@noteable = noteable
@@ -10,36 +10,6 @@ module Projects
participants.uniq
end
- def noteable_owner
- return [] unless noteable && noteable.author.present?
-
- [{
- name: noteable.author.name,
- username: noteable.author.username,
- avatar_url: noteable.author.avatar_url
- }]
- end
-
- def participants_in_noteable
- return [] unless noteable
-
- users = noteable.participants(current_user)
- sorted(users)
- end
-
- def sorted(users)
- users.uniq.to_a.compact.sort_by(&:username).map do |user|
- { username: user.username, name: user.name, avatar_url: user.avatar_url }
- end
- end
-
- def groups
- current_user.authorized_groups.sort_by(&:path).map do |group|
- count = group.users.count
- { username: group.full_path, name: group.full_name, count: count, avatar_url: group.avatar_url }
- end
- end
-
def all_members
count = project.team.members.flatten.count
[{ username: "all", name: "All Project and Group Members", count: count }]
diff --git a/app/services/projects/update_remote_mirror_service.rb b/app/services/projects/update_remote_mirror_service.rb
new file mode 100644
index 00000000000..8183a2f26d7
--- /dev/null
+++ b/app/services/projects/update_remote_mirror_service.rb
@@ -0,0 +1,30 @@
+module Projects
+ class UpdateRemoteMirrorService < BaseService
+ attr_reader :errors
+
+ def execute(remote_mirror)
+ @errors = []
+
+ return success unless remote_mirror.enabled?
+
+ begin
+ repository.fetch_remote(remote_mirror.remote_name, no_tags: true)
+
+ opts = {}
+ if remote_mirror.only_protected_branches?
+ opts[:only_branches_matching] = project.protected_branches.select(:name).map(&:name)
+ end
+
+ remote_mirror.update_repository(opts)
+ rescue => e
+ errors << e.message.strip
+ end
+
+ if errors.present?
+ error(errors.join("\n\n"))
+ else
+ success
+ end
+ end
+ end
+end
diff --git a/app/services/users/migrate_to_ghost_user_service.rb b/app/services/users/migrate_to_ghost_user_service.rb
index 976017dfa82..a2833b1e051 100644
--- a/app/services/users/migrate_to_ghost_user_service.rb
+++ b/app/services/users/migrate_to_ghost_user_service.rb
@@ -49,7 +49,7 @@ module Users
migrate_merge_requests
migrate_notes
migrate_abuse_reports
- migrate_award_emojis
+ migrate_award_emoji
end
def migrate_issues
@@ -70,7 +70,7 @@ module Users
user.reported_abuse_reports.update_all(reporter_id: ghost_user.id)
end
- def migrate_award_emojis
+ def migrate_award_emoji
user.award_emoji.update_all(user_id: ghost_user.id)
end
end
diff --git a/app/views/admin/application_settings/_repository_check.html.haml b/app/views/admin/application_settings/_repository_check.html.haml
index f33769b23c2..fe335f30a62 100644
--- a/app/views/admin/application_settings/_repository_check.html.haml
+++ b/app/views/admin/application_settings/_repository_check.html.haml
@@ -12,7 +12,7 @@
Enable Repository Checks
.help-block
GitLab will periodically run
- %a{ href: 'https://www.kernel.org/pub/software/scm/git/docs/git-fsck.html', target: 'blank' } 'git fsck'
+ %a{ href: 'https://git-scm.com/docs/git-fsck', target: 'blank' } 'git fsck'
in all project and wiki repositories to look for silent disk corruption issues.
.form-group
.col-sm-offset-2.col-sm-10
diff --git a/app/views/admin/application_settings/_repository_mirrors_form.html.haml b/app/views/admin/application_settings/_repository_mirrors_form.html.haml
new file mode 100644
index 00000000000..09183ec6260
--- /dev/null
+++ b/app/views/admin/application_settings/_repository_mirrors_form.html.haml
@@ -0,0 +1,16 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ = f.label :mirror_available, 'Enable mirror configuration', class: 'control-label col-sm-2'
+ .col-sm-10
+ .checkbox
+ = f.label :mirror_available do
+ = f.check_box :mirror_available
+ Allow mirrors to be setup for projects
+ %span.help-block
+ If disabled, only admins will be able to setup mirrors in projects.
+ = link_to icon('question-circle'), help_page_path('workflow/repository_mirroring')
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/show.html.haml b/app/views/admin/application_settings/show.html.haml
index 3c00e3c8fc4..3f440c76ee0 100644
--- a/app/views/admin/application_settings/show.html.haml
+++ b/app/views/admin/application_settings/show.html.haml
@@ -313,3 +313,14 @@
= _('Allow requests to the local network from hooks and services.')
.settings-content
= render 'outbound'
+
+%section.settings.as-mirror.no-animate#js-mirror-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Repository mirror settings')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Configure push mirrors.')
+ .settings-content
+ = render partial: 'repository_mirrors_form'
diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml
index 99fbbaec487..6e76e7c2768 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_with_tooltip runner.contacted_at
+ #{time_ago_in_words(runner.contacted_at)} ago
- else
Never
%td.admin-runner-btn-group-cell
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index d022016f70d..73fadc042b1 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -29,7 +29,7 @@
%hr
.append-bottom-20
- = render '/projects/runners/form', runner: @runner, runner_form_url: admin_runner_path(@runner)
+ = render 'shared/runners/form', runner: @runner, runner_form_url: admin_runner_path(@runner)
.row
.col-md-6
diff --git a/app/views/ci/status/_dropdown_graph_badge.html.haml b/app/views/ci/status/_dropdown_graph_badge.html.haml
index db2040110fa..d828f6f971d 100644
--- a/app/views/ci/status/_dropdown_graph_badge.html.haml
+++ b/app/views/ci/status/_dropdown_graph_badge.html.haml
@@ -16,5 +16,5 @@
%span.ci-build-text= subject.name
- if status.has_action?
- = link_to status.action_path, class: "ci-action-icon-wrapper js-ci-action-icon", method: status.action_method, data: { toggle: 'tooltip', title: status.action_title, container: 'body' } do
+ = link_to status.action_path, class: "ci-action-icon-container ci-action-icon-wrapper js-ci-action-icon", method: status.action_method, data: { toggle: 'tooltip', title: status.action_title, container: 'body' } do
= sprite_icon(status.action_icon, css_class: "icon-action-#{status.action_icon}")
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index ad9d5562ded..c8addc49117 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -1,10 +1,11 @@
- page_title "Members"
+- can_manage_members = can?(current_user, :admin_group_member, @group)
.project-members-page.prepend-top-default
%h4
Members
%hr
- - if can?(current_user, :admin_group_member, @group)
+ - if can_manage_members
.project-members-new.append-bottom-default
%p.clearfix
Add new member to
@@ -13,20 +14,23 @@
= render 'shared/members/requests', membership_source: @group, requesters: @requesters
- .append-bottom-default.clearfix
+ .clearfix
%h5.member.existing-title
Existing members
- = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do
- .form-group
- = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
- %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
- = icon("search")
- = render 'shared/members/sort_dropdown'
.panel.panel-default
- .panel-heading
- Members with access to
- %strong= @group.name
+ .panel-heading.flex-project-members-panel
+ %span.flex-project-title
+ Members with access to
+ %strong= @group.name
%span.badge= @members.total_count
+ = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form flex-project-members-form' do
+ .form-group
+ = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
+ %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
+ = icon("search")
+ - if can_manage_members
+ = render 'shared/members/filter_2fa_dropdown'
+ = render 'shared/members/sort_dropdown'
%ul.content-list.members-list
= render partial: 'shared/members/member', collection: @members, as: :member
= paginate @members, theme: 'gitlab'
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index bbfbea4ac7a..662db18cf86 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -8,7 +8,7 @@
.top-area
= render 'shared/issuable/nav', type: :issues
.nav-controls
- = link_to params.merge(rss_url_options), class: 'btn' do
+ = link_to safe_params.merge(rss_url_options), class: 'btn' do
= icon('rss')
%span.icon-label
Subscribe
diff --git a/app/views/groups/runners/_group_runners.html.haml b/app/views/groups/runners/_group_runners.html.haml
new file mode 100644
index 00000000000..e6c089c3494
--- /dev/null
+++ b/app/views/groups/runners/_group_runners.html.haml
@@ -0,0 +1,24 @@
+- link = link_to _('Runners API'), help_page_path('api/runners.md')
+
+%h3
+ = _('Group Runners')
+
+.bs-callout.bs-callout-warning
+ = _('GitLab Group Runners can execute code for all the projects in this group.')
+ = _('They can be managed using the %{link}.').html_safe % { link: link }
+
+-# Proper policies should be implemented per
+-# https://gitlab.com/gitlab-org/gitlab-ce/issues/45894
+- if can?(current_user, :admin_pipeline, @group)
+ = render partial: 'ci/runner/how_to_setup_runner',
+ locals: { registration_token: @group.runners_token, type: 'group' }
+
+- if @group.runners.empty?
+ %h4.underlined-title
+ = _('This group does not provide any group Runners yet.')
+
+- else
+ %h4.underlined-title
+ = _('Available group Runners : %{runners}.').html_safe % { runners: @group.runners.count }
+ %ul.bordered-list
+ = render partial: 'groups/runners/runner', collection: @group.runners, as: :runner
diff --git a/app/views/groups/runners/_index.html.haml b/app/views/groups/runners/_index.html.haml
new file mode 100644
index 00000000000..0cf9011b471
--- /dev/null
+++ b/app/views/groups/runners/_index.html.haml
@@ -0,0 +1,9 @@
+= render 'shared/runners/runner_description'
+
+%hr
+
+%p.lead
+ = _('To start serving your jobs you can add Runners to your group')
+.row
+ .col-sm-6
+ = render 'groups/runners/group_runners'
diff --git a/app/views/groups/runners/_runner.html.haml b/app/views/groups/runners/_runner.html.haml
new file mode 100644
index 00000000000..76650a961d6
--- /dev/null
+++ b/app/views/groups/runners/_runner.html.haml
@@ -0,0 +1,27 @@
+%li.runner{ id: dom_id(runner) }
+ %h4
+ = runner_status_icon(runner)
+
+ = link_to runner.short_sha, group_runner_path(@group, runner), class: 'commit-sha'
+
+ %small.edit-runner
+ = link_to edit_group_runner_path(@group, runner) do
+ = icon('edit')
+
+ .pull-right
+ - if runner.active?
+ = link_to _('Pause'), pause_group_runner_path(@group, runner), method: :post, class: 'btn btn-sm btn-danger', data: { confirm: _("Are you sure?") }
+ - else
+ = link_to _('Resume'), resume_group_runner_path(@group, runner), method: :post, class: 'btn btn-success btn-sm'
+ = link_to _('Remove Runner'), group_runner_path(@group, runner), data: { confirm: _("Are you sure?") }, method: :delete, class: 'btn btn-danger btn-sm'
+ .pull-right
+ %small.light
+ \##{runner.id}
+ - if runner.description.present?
+ %p.runner-description
+ = runner.description
+ - if runner.tag_list.present?
+ %p
+ - runner.tag_list.sort.each do |tag|
+ %span.label.label-primary
+ = tag
diff --git a/app/views/groups/runners/edit.html.haml b/app/views/groups/runners/edit.html.haml
new file mode 100644
index 00000000000..fcd096eeaa0
--- /dev/null
+++ b/app/views/groups/runners/edit.html.haml
@@ -0,0 +1,6 @@
+- page_title _('Edit'), "#{@runner.description} ##{@runner.id}", 'Runners'
+
+%h4 Runner ##{@runner.id}
+
+%hr
+ = render 'shared/runners/form', runner: @runner, runner_form_url: group_runner_path(@group, @runner)
diff --git a/app/views/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml
index dd82922ec55..082e1b7befa 100644
--- a/app/views/groups/settings/ci_cd/show.html.haml
+++ b/app/views/groups/settings/ci_cd/show.html.haml
@@ -1,11 +1,27 @@
- breadcrumb_title "CI / CD Settings"
- page_title "CI / CD"
-%h4
- = _('Secret variables')
- = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer'
+- expanded = Rails.env.test?
-%p
- = render "ci/variables/content"
+%section.settings#secret-variables.no-animate{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Secret variables')
+ = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer'
+ %button.btn.btn-default.js-settings-toggle{ type: "button" }
+ = expanded ? _('Collapse') : _('Expand')
+ %p.append-bottom-0
+ = render "ci/variables/content"
+ .settings-content
+ = render 'ci/variables/index', save_endpoint: group_variables_path
-= render 'ci/variables/index', save_endpoint: group_variables_path
+%section.settings#runners-settings.no-animate{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Runners settings')
+ %button.btn.btn-default.js-settings-toggle{ type: "button" }
+ = expanded ? _('Collapse') : _('Expand')
+ %p
+ = _('Register and see your runners for this group.')
+ .settings-content
+ = render 'groups/runners/index'
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index 29b23ae2e52..1c5b4aecabb 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -1,5 +1,5 @@
#modal-shortcuts.modal{ tabindex: -1 }
- .modal-dialog
+ .modal-dialog.modal-lg
.modal-content
.modal-header
%a.close{ href: "#", "data-dismiss" => "modal" } ×
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index ce09b44fbb2..7908a04c2eb 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -74,10 +74,10 @@
= lorem
.cover-controls
- = link_to '#', class: 'btn btn-gray' do
+ = link_to '#', class: 'btn btn-default' do
= icon('pencil')
&nbsp;
- = link_to '#', class: 'btn btn-gray' do
+ = link_to '#', class: 'btn btn-default' do
= icon('rss')
%h2#lists Lists
@@ -206,7 +206,6 @@
.example
%button.btn.btn-default{ :type => "button" } Default
- %button.btn.btn-gray{ :type => "button" } Gray
%button.btn.btn-primary{ :type => "button" } Primary
%button.btn.btn-success{ :type => "button" } Success
%button.btn.btn-info{ :type => "button" } Info
diff --git a/app/views/ide/index.html.haml b/app/views/ide/index.html.haml
index e0e8fe548d0..da9331b45dd 100644
--- a/app/views/ide/index.html.haml
+++ b/app/views/ide/index.html.haml
@@ -1,9 +1,6 @@
- @body_class = 'ide'
- page_title 'IDE'
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag 'ide', force_same_domain: true
-
#ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg'),
"no-changes-state-svg-path" => image_path('illustrations/multi-editor_no_changes_empty.svg'),
"committed-state-svg-path" => image_path('illustrations/multi-editor_all_changes_committed_empty.svg') } }
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index b981b5fdafa..02bdfe9aa3c 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -38,9 +38,6 @@
= yield :library_javascripts
= javascript_include_tag locale_path unless I18n.locale == :en
- = webpack_bundle_tag "webpack_runtime"
- = webpack_bundle_tag "common"
- = webpack_bundle_tag "main"
= webpack_bundle_tag "raven" if Gitlab::CurrentSettings.clientside_sentry_enabled
- if content_for?(:page_specific_javascripts)
diff --git a/app/views/layouts/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml
index 4276e6ee4bb..240e03a5d53 100644
--- a/app/views/layouts/_init_auto_complete.html.haml
+++ b/app/views/layouts/_init_auto_complete.html.haml
@@ -1,16 +1,11 @@
-- project = @target_project || @project
+- object = @target_project || @project || @group
- noteable_type = @noteable.class if @noteable.present?
-- if project
+- datasources = autocomplete_data_sources(object, noteable_type)
+
+- if object
-# haml-lint:disable InlineJavaScript
:javascript
gl = window.gl || {};
gl.GfmAutoComplete = gl.GfmAutoComplete || {};
- gl.GfmAutoComplete.dataSources = {
- members: "#{members_project_autocomplete_sources_path(project, type: noteable_type, type_id: params[:id])}",
- issues: "#{issues_project_autocomplete_sources_path(project)}",
- mergeRequests: "#{merge_requests_project_autocomplete_sources_path(project)}",
- labels: "#{labels_project_autocomplete_sources_path(project, type: noteable_type, type_id: params[:id])}",
- milestones: "#{milestones_project_autocomplete_sources_path(project)}",
- commands: "#{commands_project_autocomplete_sources_path(project, type: noteable_type, type_id: params[:id])}"
- };
+ gl.GfmAutoComplete.dataSources = #{datasources.to_json};
diff --git a/app/views/layouts/terms.html.haml b/app/views/layouts/terms.html.haml
index a30d6e2688c..87f4151f241 100644
--- a/app/views/layouts/terms.html.haml
+++ b/app/views/layouts/terms.html.haml
@@ -20,10 +20,10 @@
= brand_header_logo
- logo_text = brand_header_logo_type
- if logo_text.present?
- %span.logo-text.hidden-xs.prepend-left-8
+ %span.logo-text.prepend-left-8
= logo_text
- if header_link?(:user_dropdown)
- .navbar-collapse.collapse
+ .navbar-collapse
%ul.nav.navbar-nav
%li.header-user.dropdown
= link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
diff --git a/app/views/projects/_wiki.html.haml b/app/views/projects/_wiki.html.haml
index a56c3503c77..5646dc464f8 100644
--- a/app/views/projects/_wiki.html.haml
+++ b/app/views/projects/_wiki.html.haml
@@ -1,6 +1,6 @@
- if @wiki_home.present?
%div{ class: container_class }
- .wiki-holder.prepend-top-default.append-bottom-default
+ .prepend-top-default.append-bottom-default
.wiki
= render_wiki_content(@wiki_home)
- else
diff --git a/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml b/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml
new file mode 100644
index 00000000000..d0402197821
--- /dev/null
+++ b/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml
@@ -0,0 +1,12 @@
+- link = link_to(s_('ClusterIntegration|sign up'), 'https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer')
+.gcp-signup-offer.alert.alert-block.alert-dismissable.prepend-top-default.append-bottom-default{ role: 'alert' }
+ %button.close{ type: "button", data: { feature_id: UserCalloutsHelper::GCP_SIGNUP_OFFER, dismiss_endpoint: user_callouts_path } } &times;
+ %div
+ .col-sm-2.gcp-logo
+ = image_tag 'illustrations/logos/google-cloud-platform_logo.svg'
+ .col-sm-10
+ %h4= s_('ClusterIntegration|Redeem up to $500 in free credit for Google Cloud Platform')
+ %p= s_('ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for new GCP accounts to get started with GitLab\'s Google Kubernetes Engine Integration.').html_safe % { sign_up_link: link }
+ %a.btn.btn-info{ href: 'https://goo.gl/AaJzRW', target: '_blank', rel: 'noopener noreferrer' }
+ Apply for credit
+
diff --git a/app/views/projects/clusters/gcp/login.html.haml b/app/views/projects/clusters/gcp/login.html.haml
index dada51f39da..ff046c59a7a 100644
--- a/app/views/projects/clusters/gcp/login.html.haml
+++ b/app/views/projects/clusters/gcp/login.html.haml
@@ -1,6 +1,8 @@
- breadcrumb_title 'Kubernetes'
- page_title _("Login")
+= render_gcp_signup_offer
+
.row.prepend-top-default
.col-sm-4
= render 'projects/clusters/sidebar'
diff --git a/app/views/projects/clusters/index.html.haml b/app/views/projects/clusters/index.html.haml
index 17b244f4bf7..a55de84b5cd 100644
--- a/app/views/projects/clusters/index.html.haml
+++ b/app/views/projects/clusters/index.html.haml
@@ -1,6 +1,8 @@
- breadcrumb_title 'Kubernetes'
- page_title "Kubernetes Clusters"
+= render_gcp_signup_offer
+
.clusters-container
- if @clusters.empty?
= render "empty_state"
diff --git a/app/views/projects/clusters/new.html.haml b/app/views/projects/clusters/new.html.haml
index e004966bdcc..828e2a84753 100644
--- a/app/views/projects/clusters/new.html.haml
+++ b/app/views/projects/clusters/new.html.haml
@@ -1,6 +1,8 @@
- breadcrumb_title 'Kubernetes'
- page_title _("Kubernetes Cluster")
+= render_gcp_signup_offer
+
.row.prepend-top-default
.col-sm-4
= render 'sidebar'
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 213c4c90a0e..1bffb3e8bf0 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -54,7 +54,7 @@
%h3.commit-title
= markdown_field(@commit, :title)
- if @commit.description.present?
- %pre.commit-description
+ .commit-description<
= preserve(markdown_field(@commit, :description))
.info-well
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 3fd0fa348b3..c390c9c4469 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -36,16 +36,16 @@
- if commit.description?
%button.text-expander.hidden-xs.js-toggle-button{ type: "button" } ...
- - if commit.description?
- %pre.commit-row-description.js-toggle-content
- = preserve(markdown_field(commit, :description))
-
.commiter
- commit_author_link = commit_author_link(commit, avatar: false, size: 24)
- commit_timeago = time_ago_with_tooltip(commit.authored_date, placement: 'bottom')
- commit_text = _('%{commit_author_link} authored %{commit_timeago}') % { commit_author_link: commit_author_link, commit_timeago: commit_timeago }
#{ commit_text.html_safe }
+ - if commit.description?
+ %pre.commit-row-description.js-toggle-content.prepend-top-8.append-bottom-8
+ = preserve(markdown_field(commit, :description))
+
.commit-actions.flex-row.hidden-xs
- if request.xhr?
= render partial: 'projects/commit/signature', object: commit.signature
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index ab371521840..483cca11df9 100644
--- a/app/views/projects/commits/show.html.haml
+++ b/app/views/projects/commits/show.html.haml
@@ -24,7 +24,7 @@
= link_to _("Create merge request"), create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success'
.control
- = form_tag(project_commits_path(@project, @id), method: :get, class: 'commits-search-form', data: { 'signatures-path' => namespace_project_signatures_path }) do
+ = form_tag(project_commits_path(@project, @id), method: :get, class: 'commits-search-form js-signature-container', data: { 'signatures-path' => namespace_project_signatures_path }) do
= search_field_tag :search, params[:search], { placeholder: _('Filter by commit message'), id: 'commits-search', class: 'form-control search-text-input input-short', spellcheck: false }
.control
= link_to project_commits_path(@project, @ref, rss_url_options), title: _("Commits feed"), class: 'btn' do
diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml
index d0c8a699608..40cdf96e76d 100644
--- a/app/views/projects/compare/_form.html.haml
+++ b/app/views/projects/compare/_form.html.haml
@@ -1,4 +1,4 @@
-= form_tag project_compare_index_path(@project), method: :post, class: 'form-inline js-requires-input' do
+= form_tag project_compare_index_path(@project), method: :post, class: 'form-inline js-requires-input js-signature-container', data: { 'signatures-path' => signatures_namespace_project_compare_index_path } do
.clearfix
- if params[:to] && params[:from]
.compare-switch-container
diff --git a/app/views/projects/deploy_keys/_index.html.haml b/app/views/projects/deploy_keys/_index.html.haml
index 7dd8dc28e5b..6af57d3ab26 100644
--- a/app/views/projects/deploy_keys/_index.html.haml
+++ b/app/views/projects/deploy_keys/_index.html.haml
@@ -12,4 +12,4 @@
Create a new deploy key for this project
= render @deploy_keys.form_partial_path
%hr
- #js-deploy-keys{ data: { endpoint: project_deploy_keys_path(@project) } }
+ #js-deploy-keys{ data: { endpoint: project_deploy_keys_path(@project), project_id: @project.id } }
diff --git a/app/views/projects/merge_requests/creations/_new_compare.html.haml b/app/views/projects/merge_requests/creations/_new_compare.html.haml
index 4e10511411f..773b12b4536 100644
--- a/app/views/projects/merge_requests/creations/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_compare.html.haml
@@ -4,7 +4,7 @@
= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], url: project_new_merge_request_path(@project), method: :get, html: { class: "merge-request-form form-inline js-requires-input" } do |f|
.hide.alert.alert-danger.mr-compare-errors
.js-merge-request-new-compare.row{ 'data-source-branch-url': project_new_merge_request_branch_from_path(@source_project), 'data-target-branch-url': project_new_merge_request_branch_to_path(@source_project) }
- .col-md-6
+ .col-lg-6
.panel.panel-default.panel-new-merge-request
.panel-heading
Source branch
@@ -31,7 +31,7 @@
.text-center= icon('spinner spin', class: 'js-source-loading')
%ul.list-unstyled.mr_source_commit
- .col-md-6
+ .col-lg-6
.panel.panel-default.panel-new-merge-request
.panel-heading
Target branch
diff --git a/app/views/projects/mirrors/_instructions.html.haml b/app/views/projects/mirrors/_instructions.html.haml
new file mode 100644
index 00000000000..64f0fde30cf
--- /dev/null
+++ b/app/views/projects/mirrors/_instructions.html.haml
@@ -0,0 +1,10 @@
+.account-well.prepend-top-default.append-bottom-default
+ %ul
+ %li
+ The repository must be accessible over <code>http://</code>, <code>https://</code>, <code>ssh://</code> or <code>git://</code>.
+ %li
+ Include the username in the URL if required: <code>https://username@gitlab.company.com/group/project.git</code>.
+ %li
+ The update action will time out after 10 minutes. For big repositories, use a clone/push combination.
+ %li
+ The Git LFS objects will <strong>not</strong> be synced.
diff --git a/app/views/projects/mirrors/_push.html.haml b/app/views/projects/mirrors/_push.html.haml
new file mode 100644
index 00000000000..4a6aefce351
--- /dev/null
+++ b/app/views/projects/mirrors/_push.html.haml
@@ -0,0 +1,50 @@
+- expanded = Rails.env.test?
+%section.settings.no-animate{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ Push to a remote repository
+ %button.btn.js-settings-toggle
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ Set up the remote repository that you want to update with the content of the current repository
+ every time someone pushes to it.
+ = link_to 'Read more', help_page_path('workflow/repository_mirroring', anchor: 'pushing-to-a-remote-repository'), target: '_blank'
+ .settings-content
+ = form_for @project, url: project_mirror_path(@project) do |f|
+ %div
+ = form_errors(@project)
+ = render "shared/remote_mirror_update_button", remote_mirror: @remote_mirror
+ - if @remote_mirror.last_error.present?
+ .panel.panel-danger
+ .panel-heading
+ - if @remote_mirror.last_update_at
+ The remote repository failed to update #{time_ago_with_tooltip(@remote_mirror.last_update_at)}.
+ - else
+ The remote repository failed to update.
+
+ - if @remote_mirror.last_successful_update_at
+ Last successful update #{time_ago_with_tooltip(@remote_mirror.last_successful_update_at)}.
+ .panel-body
+ %pre
+ :preserve
+ #{h(@remote_mirror.last_error.strip)}
+ = f.fields_for :remote_mirrors, @remote_mirror do |rm_form|
+ .form-group
+ = rm_form.check_box :enabled, class: "pull-left"
+ .prepend-left-20
+ = rm_form.label :enabled, "Remote mirror repository", class: "label-light append-bottom-0"
+ %p.light.append-bottom-0
+ Automatically update the remote mirror's branches, tags, and commits from this repository every time someone pushes to it.
+ .form-group.has-feedback
+ = rm_form.label :url, "Git repository URL", class: "label-light"
+ = rm_form.text_field :url, class: "form-control", placeholder: 'https://username:password@gitlab.company.com/group/project.git'
+
+ = render "projects/mirrors/instructions"
+
+ .form-group
+ = rm_form.check_box :only_protected_branches, class: 'pull-left'
+ .prepend-left-20
+ = rm_form.label :only_protected_branches, class: 'label-light'
+ = link_to icon('question-circle'), help_page_path('user/project/protected_branches')
+
+ = f.submit 'Save changes', class: 'btn btn-create', name: 'update_remote_mirror'
diff --git a/app/views/projects/mirrors/_show.html.haml b/app/views/projects/mirrors/_show.html.haml
new file mode 100644
index 00000000000..de77701a373
--- /dev/null
+++ b/app/views/projects/mirrors/_show.html.haml
@@ -0,0 +1,3 @@
+- if can?(current_user, :admin_remote_mirror, @project)
+ = render 'projects/mirrors/push'
+
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index 85946aec1f2..9db30042bf4 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -5,7 +5,7 @@
%h3.commit-title
= markdown(@commit.title, pipeline: :single_line)
- if @commit.description.present?
- %pre.commit-description
+ .commit-description<
= preserve(markdown(@commit.description, pipeline: :single_line))
.info-well
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index 218e7338c83..4dbf95be357 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -1,5 +1,3 @@
-- failed_builds = @pipeline.statuses.latest.failed
-
.tabs-holder
%ul.pipelines-tabs.nav-links.no-top.no-bottom.mobile-separator
%li.js-pipeline-tab-link
@@ -9,11 +7,11 @@
= link_to builds_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do
= _("Jobs")
%span.badge.js-builds-counter= pipeline.total_size
- - if failed_builds.present?
+ - if @pipeline.failed_builds.present?
%li.js-failures-tab-link
= link_to failures_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-failures', action: 'failures', toggle: 'tab' }, class: 'failures-tab' do
= _("Failed Jobs")
- %span.badge.js-failures-counter= failed_builds.count
+ %span.badge.js-failures-counter= @pipeline.failed_builds.count
.tab-content
#js-tab-pipeline.tab-pane
@@ -43,9 +41,10 @@
%th Coverage
%th
= render partial: "projects/stage/stage", collection: pipeline.legacy_stages, as: :stage
- - if failed_builds.present?
+
+ - if @pipeline.failed_builds.present?
#js-tab-failures.build-failures.tab-pane
- - failed_builds.each_with_index do |build, index|
+ - @pipeline.failed_builds.each_with_index do |build, index|
.build-state
%span.ci-status-icon-failed= custom_icon('icon_status_failed')
%span.stage
diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml
index 8f2142af2ce..81984ee94b0 100644
--- a/app/views/projects/pipelines/new.html.haml
+++ b/app/views/projects/pipelines/new.html.haml
@@ -1,5 +1,6 @@
- breadcrumb_title "Pipelines"
- page_title = s_("Pipeline|Run Pipeline")
+- settings_link = link_to _('CI/CD settings'), project_settings_ci_cd_path(@project)
%h3.page-title
= s_("Pipeline|Run Pipeline")
@@ -8,17 +9,26 @@
= form_for @pipeline, as: :pipeline, url: project_pipelines_path(@project), html: { id: "new-pipeline-form", class: "form-horizontal js-new-pipeline-form js-requires-input" } do |f|
= form_errors(@pipeline)
.form-group
- = f.label :ref, s_('Pipeline|Run on'), class: 'control-label'
- .col-sm-10
+ .col-sm-12
+ = f.label :ref, s_('Pipeline|Create for')
= hidden_field_tag 'pipeline[ref]', params[:ref] || @project.default_branch
= dropdown_tag(params[:ref] || @project.default_branch,
options: { toggle_class: 'js-branch-select wide git-revision-dropdown-toggle',
filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown", placeholder: s_("Pipeline|Search branches"),
data: { selected: params[:ref] || @project.default_branch, field_name: 'pipeline[ref]' } })
.help-block
- = s_("Pipeline|Existing branch name, tag")
+ = s_("Pipeline|Existing branch name or tag")
+
+ .col-sm-12.prepend-top-10.js-ci-variable-list-section
+ %label
+ = s_('Pipeline|Variables')
+ %ul.ci-variable-list
+ = render 'ci/variables/variable_row', form_field: 'pipeline', only_key_value: true
+ .help-block
+ = (s_("Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default.") % {settings_link: settings_link}).html_safe
+
.form-actions
- = f.submit s_('Pipeline|Run pipeline'), class: 'btn btn-success', tabindex: 3
+ = f.submit s_('Pipeline|Create pipeline'), class: 'btn btn-success js-variables-save-button', tabindex: 3
= link_to 'Cancel', project_pipelines_path(@project), class: 'btn btn-default pull-right'
-# haml-lint:disable InlineJavaScript
diff --git a/app/views/projects/runners/_group_runners.html.haml b/app/views/projects/runners/_group_runners.html.haml
index a9dfd9cc786..dfed0553f84 100644
--- a/app/views/projects/runners/_group_runners.html.haml
+++ b/app/views/projects/runners/_group_runners.html.haml
@@ -1,32 +1,37 @@
-%h3 Group Runners
+- link = link_to _('Runners API'), help_page_path('api/runners.md')
+
+%h3
+ = _('Group Runners')
.bs-callout.bs-callout-warning
- GitLab Group Runners can execute code for all the projects in this group.
- They can be managed using the #{link_to 'Runners API', help_page_path('api/runners.md')}.
+ = _('GitLab Group Runners can execute code for all the projects in this group.')
+ = _('They can be managed using the %{link}.').html_safe % { link: link }
- if @project.group
%hr
- if @project.group_runners_enabled?
- = link_to toggle_group_runners_project_runners_path(@project), class: 'btn btn-warning', method: :post do
- Disable group Runners
+ = link_to toggle_group_runners_project_runners_path(@project), class: 'btn btn-close', method: :post do
+ = _('Disable group Runners')
- else
- = link_to toggle_group_runners_project_runners_path(@project), class: 'btn btn-success', method: :post do
- Enable group Runners
- &nbsp; for this project
+ = link_to toggle_group_runners_project_runners_path(@project), class: 'btn btn-success btn-inverted', method: :post do
+ = _('Enable group Runners')
+ &nbsp;
+ = _('for this project')
- if !@project.group
- This project does not belong to a group and can therefore not make use of group Runners.
+ = _('This project does not belong to a group and can therefore not make use of group Runners.')
- elsif @group_runners.empty?
- This group does not provide any group Runners yet.
+ = _('This group does not provide any group Runners yet.')
- if can?(current_user, :admin_pipeline, @project.group)
- = render partial: 'ci/runner/how_to_setup_runner',
- locals: { registration_token: @project.group.runners_token, type: 'group' }
+ - group_link = link_to _('Group CI/CD settings'), group_settings_ci_cd_path(@project.group)
+ = _('Group masters can register group runners in the %{link}').html_safe % { link: group_link }
- else
- Ask your group master to setup a group Runner.
+ = _('Ask your group master to setup a group Runner.')
- else
- %h4.underlined-title Available group Runners : #{@group_runners.count}
+ %h4.underlined-title
+ = _('Available group Runners : %{runners}').html_safe % { runners: @group_runners.count }
%ul.bordered-list
= render partial: 'projects/runners/runner', collection: @group_runners, as: :runner
diff --git a/app/views/projects/runners/_index.html.haml b/app/views/projects/runners/_index.html.haml
index 3f5119d408b..022687b831f 100644
--- a/app/views/projects/runners/_index.html.haml
+++ b/app/views/projects/runners/_index.html.haml
@@ -1,19 +1,4 @@
-.light.prepend-top-default
- %p
- A 'Runner' is a process which runs a job.
- You can setup as many Runners as you need.
- %br
- Runners can be placed on separate users, servers, and even on your local machine.
-
- %p Each Runner can be in one of the following states:
- %div
- %ul
- %li
- %span.label.label-success active
- \- Runner is active and can process any new jobs
- %li
- %span.label.label-danger paused
- \- Runner is paused and will not receive any new jobs
+= render 'shared/runners/runner_description'
%hr
@@ -23,7 +8,4 @@
= render 'projects/runners/specific_runners'
.col-sm-6
= render 'projects/runners/shared_runners'
-.row
- .col-sm-6
- .col-sm-6
= render 'projects/runners/group_runners'
diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml
index 0d2c0536eb5..69218f344f7 100644
--- a/app/views/projects/runners/_runner.html.haml
+++ b/app/views/projects/runners/_runner.html.haml
@@ -3,10 +3,10 @@
= runner_status_icon(runner)
- if @project_runners.include?(runner)
- = link_to runner.short_sha, runner_path(runner), class: 'commit-sha'
+ = link_to runner.short_sha, project_runner_path(@project, runner), class: 'commit-sha'
- if runner.locked?
- = icon('lock', class: 'has-tooltip', title: 'Locked to current projects')
+ = icon('lock', class: 'has-tooltip', title: _('Locked to current projects'))
%small.edit-runner
= link_to edit_project_runner_path(@project, runner) do
@@ -18,18 +18,18 @@
.pull-right
- if @project_runners.include?(runner)
- if runner.active?
- = link_to 'Pause', pause_project_runner_path(@project, runner), method: :post, class: 'btn btn-sm btn-danger', data: { confirm: "Are you sure?" }
+ = link_to _('Pause'), pause_project_runner_path(@project, runner), method: :post, class: 'btn btn-sm btn-danger', data: { confirm: _("Are you sure?") }
- else
- = link_to 'Resume', resume_project_runner_path(@project, runner), method: :post, class: 'btn btn-success btn-sm'
+ = link_to _('Resume'), resume_project_runner_path(@project, runner), method: :post, class: 'btn btn-success btn-sm'
- if runner.belongs_to_one_project?
- = link_to 'Remove Runner', runner_path(runner), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
+ = link_to _('Remove Runner'), project_runner_path(@project, runner), data: { confirm: _("Are you sure?") }, method: :delete, class: 'btn btn-danger btn-sm'
- else
- runner_project = @project.runner_projects.find_by(runner_id: runner)
- = link_to 'Disable for this project', project_runner_project_path(@project, runner_project), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
+ = link_to _('Disable for this project'), project_runner_project_path(@project, runner_project), data: { confirm: _("Are you sure?") }, method: :delete, class: 'btn btn-danger btn-sm'
- elsif !(runner.is_shared? || runner.group_type?) # We can simplify this to `runner.project_type?` when migrating #runner_type is complete
= form_for [@project.namespace.becomes(Namespace), @project, @project.runner_projects.new] do |f|
= f.hidden_field :runner_id, value: runner.id
- = f.submit 'Enable for this project', class: 'btn btn-sm'
+ = f.submit _('Enable for this project'), class: 'btn btn-sm'
.pull-right
%small.light
\##{runner.id}
diff --git a/app/views/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml
index 4fd4ca355a8..20a5ef039f8 100644
--- a/app/views/projects/runners/_shared_runners.html.haml
+++ b/app/views/projects/runners/_shared_runners.html.haml
@@ -1,4 +1,5 @@
-%h3 Shared Runners
+%h3
+ = _('Shared Runners')
.bs-callout.shared-runners-description
- if Gitlab::CurrentSettings.shared_runners_text.present?
@@ -17,8 +18,7 @@
&nbsp; for this project
- if @shared_runners_count.zero?
- This GitLab server does not provide any shared Runners yet.
- Please use the specific Runners or ask your administrator to create one.
+ = _('This GitLab instance does not provide any shared Runners yet. Instance administrators can register shared Runners in the admin area.')
- else
%h4.underlined-title Available shared Runners : #{@shared_runners_count}
%ul.bordered-list.available-shared-runners
diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml
index f0813e56b71..6c11ce3b394 100644
--- a/app/views/projects/runners/_specific_runners.html.haml
+++ b/app/views/projects/runners/_specific_runners.html.haml
@@ -1,4 +1,5 @@
-%h3 Specific Runners
+%h3
+ = _('Specific Runners')
= render partial: 'ci/runner/how_to_setup_specific_runner',
locals: { registration_token: @project.runners_token }
diff --git a/app/views/projects/runners/edit.html.haml b/app/views/projects/runners/edit.html.haml
index 78dc4817ed7..d59f9c19862 100644
--- a/app/views/projects/runners/edit.html.haml
+++ b/app/views/projects/runners/edit.html.haml
@@ -1,6 +1,6 @@
-- page_title "Edit", "#{@runner.description} ##{@runner.id}", "Runners"
+- page_title _('Edit'), "#{@runner.description} ##{@runner.id}", 'Runners'
%h4 Runner ##{@runner.id}
%hr
- = render 'form', runner: @runner, runner_form_url: runner_path(@runner)
+ = render 'shared/runners/form', runner: @runner, runner_form_url: project_runner_path(@project, @runner)
diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml
index f57590a908f..5dda2ec28b4 100644
--- a/app/views/projects/settings/repository/show.html.haml
+++ b/app/views/projects/settings/repository/show.html.haml
@@ -2,6 +2,8 @@
- page_title "Repository"
- @content_class = "limit-container-width" unless fluid_layout
+= render "projects/mirrors/show"
+
-# Protected branches & tags use a lot of nested partials.
-# The shared parts of the views can be found in the `shared` directory.
-# Those are used throughout the actual views. These `shared` views are then
diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml
index 9d3d4072027..35c7dc2984a 100644
--- a/app/views/projects/wikis/edit.html.haml
+++ b/app/views/projects/wikis/edit.html.haml
@@ -28,9 +28,16 @@
= link_to project_wiki_history_path(@project, @page), class: "btn" do
= s_("Wiki|Page history")
- if can?(current_user, :admin_wiki, @project)
- = link_to project_wiki_path(@project, @page), data: { confirm: s_("WikiPageConfirmDelete|Are you sure you want to delete this page?")}, method: :delete, class: "btn btn-danger" do
- = _("Delete")
+ %button.btn.btn-danger{ data: { toggle: 'modal',
+ target: '#delete-wiki-modal',
+ delete_wiki_url: project_wiki_path(@project, @page),
+ page_title: @page.title.capitalize },
+ id: 'delete-wiki-button',
+ type: 'button' }
+ = _('Delete')
= render 'form'
= render 'sidebar'
+
+#delete-wiki-modal.modal.fade
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index b3b83cee81a..ff72c8bb75d 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -24,7 +24,7 @@
- history_link = link_to s_("WikiHistoricalPage|history"), project_wiki_history_path(@project, @page)
= (s_("WikiHistoricalPage|You can view the %{most_recent_link} or browse the %{history_link}.") % { most_recent_link: most_recent_link, history_link: history_link }).html_safe
-.wiki-holder.prepend-top-default.append-bottom-default
+.prepend-top-default.append-bottom-default
.wiki
= render_wiki_content(@page)
diff --git a/app/views/shared/_mini_pipeline_graph.html.haml b/app/views/shared/_mini_pipeline_graph.html.haml
index 901a177323b..ac2164a4a71 100644
--- a/app/views/shared/_mini_pipeline_graph.html.haml
+++ b/app/views/shared/_mini_pipeline_graph.html.haml
@@ -6,12 +6,13 @@
- status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}"
.stage-container.dropdown{ class: klass }
- %button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_project_pipeline_path(pipeline.project, pipeline, stage: stage.name) } }
+ %button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_ajax_project_pipeline_path(pipeline.project, pipeline, stage: stage.name) } }
= sprite_icon(icon_status)
= icon('caret-down')
%ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container
%li.js-builds-dropdown-list.scrollable-menu
+ %ul
%li.js-builds-dropdown-loading.hidden
.text-center
diff --git a/app/views/shared/_remote_mirror_update_button.html.haml b/app/views/shared/_remote_mirror_update_button.html.haml
new file mode 100644
index 00000000000..34de1c0695f
--- /dev/null
+++ b/app/views/shared/_remote_mirror_update_button.html.haml
@@ -0,0 +1,13 @@
+- if @project.has_remote_mirror?
+ .append-bottom-default
+ - if remote_mirror.update_in_progress?
+ %span.btn.disabled
+ = icon("refresh spin")
+ Updating&hellip;
+ - else
+ = link_to update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: "btn" do
+ = icon("refresh")
+ Update Now
+ - if @remote_mirror.last_successful_update_at
+ %p.inline.prepend-left-10
+ Successfully updated #{time_ago_with_tooltip(@remote_mirror.last_successful_update_at)}.
diff --git a/app/views/shared/boards/components/_board.html.haml b/app/views/shared/boards/components/_board.html.haml
index 149bf8da4b9..4bff6468bb0 100644
--- a/app/views/shared/boards/components/_board.html.haml
+++ b/app/views/shared/boards/components/_board.html.haml
@@ -15,8 +15,9 @@
":title" => '(list.label ? list.label.description : "")',
data: { container: "body", placement: "bottom" },
class: "label color-label title board-title-text",
- ":style" => "{ backgroundColor: (list.label && list.label.color ? list.label.color : null), color: (list.label && list.label.color ? list.label.text_color : \"#2e2e2e\") }" }
+ ":style" => "{ backgroundColor: (list.label && list.label.color ? list.label.color : null), color: (list.label && list.label.text_color ? list.label.text_color : \"#2e2e2e\") }" }
{{ list.title }}
+
- if can?(current_user, :admin_list, current_board_parent)
%board-delete{ "inline-template" => true,
":list" => "list",
diff --git a/app/views/shared/members/_filter_2fa_dropdown.html.haml b/app/views/shared/members/_filter_2fa_dropdown.html.haml
new file mode 100644
index 00000000000..95c35c56b3c
--- /dev/null
+++ b/app/views/shared/members/_filter_2fa_dropdown.html.haml
@@ -0,0 +1,11 @@
+- filter = params[:two_factor] || 'everyone'
+- filter_options = { 'everyone' => 'Everyone', 'enabled' => 'Enabled', 'disabled' => 'Disabled' }
+.dropdown.inline.member-filter-2fa-dropdown
+ = dropdown_toggle('2FA: ' + filter_options[filter], { toggle: 'dropdown' })
+ %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
+ %li.dropdown-header
+ Filter by two-factor authentication
+ - filter_options.each do |value, title|
+ %li
+ = link_to filter_group_project_member_path(two_factor: value), class: ("is-active" if filter == value) do
+ = title
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 1c139827acf..1961ad6d616 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -20,6 +20,10 @@
%label.label.label-danger
%strong Blocked
+ - if user.two_factor_enabled?
+ %label.label.label-info
+ 2FA
+
- if source.instance_of?(Group) && source != @group
&middot;
= link_to source.full_name, source, class: "member-group-link"
diff --git a/app/views/projects/runners/_form.html.haml b/app/views/shared/runners/_form.html.haml
index 6a681736b6f..302a543cf12 100644
--- a/app/views/projects/runners/_form.html.haml
+++ b/app/views/shared/runners/_form.html.haml
@@ -18,12 +18,13 @@
.checkbox
= f.check_box :run_untagged
%span.light Indicates whether this runner can pick jobs without tags
- .form-group
- = label :locked, 'Lock to current projects', class: 'control-label'
- .col-sm-10
- .checkbox
- = f.check_box :locked
- %span.light When a runner is locked, it cannot be assigned to other projects
+ - unless runner.group_type?
+ .form-group
+ = label :locked, _('Lock to current projects'), class: 'control-label'
+ .col-sm-10
+ .checkbox
+ = f.check_box :locked
+ %span.light= _('When a runner is locked, it cannot be assigned to other projects')
.form-group
= label_tag :token, class: 'control-label' do
Token
diff --git a/app/views/shared/runners/_runner_description.html.haml b/app/views/shared/runners/_runner_description.html.haml
new file mode 100644
index 00000000000..1d59c2f7078
--- /dev/null
+++ b/app/views/shared/runners/_runner_description.html.haml
@@ -0,0 +1,16 @@
+.light.prepend-top-default
+ %p
+ = _("A 'Runner' is a process which runs a job. You can setup as many Runners as you need.")
+ %br
+ = _('Runners can be placed on separate users, servers, and even on your local machine.')
+
+ %p
+ = _('Each Runner can be in one of the following states:')
+ %div
+ %ul
+ %li
+ %span.label.label-success active
+ = _('- Runner is active and can process any new jobs')
+ %li
+ %span.label.label-danger paused
+ = _('- Runner is paused and will not receive any new jobs')
diff --git a/app/views/projects/runners/show.html.haml b/app/views/shared/runners/show.html.haml
index 322152cfaca..480a224b6d5 100644
--- a/app/views/projects/runners/show.html.haml
+++ b/app/views/shared/runners/show.html.haml
@@ -6,6 +6,9 @@
- if @runner.shared?
%span.runner-state.runner-state-shared
Shared
+ - elsif @runner.group_type?
+ %span.runner-state.runner-state-shared
+ Group
- else
%span.runner-state.runner-state-specific
Specific
@@ -18,16 +21,17 @@
%th Value
%tr
%td Active
- %td= @runner.active? ? 'Yes' : 'No'
+ %td= @runner.active? ? _('Yes') : _('No')
%tr
%td Protected
- %td= @runner.ref_protected? ? 'Yes' : 'No'
- %tr
- %td Can run untagged jobs
- %td= @runner.run_untagged? ? 'Yes' : 'No'
+ %td= @runner.ref_protected? ? _('Yes') : _('No')
%tr
- %td Locked to this project
- %td= @runner.locked? ? 'Yes' : 'No'
+ %td= _('Can run untagged jobs')
+ %td= @runner.run_untagged? ? _('Yes') : _('No')
+ - unless @runner.group_type?
+ %tr
+ %td= _('Locked to this project')
+ %td= @runner.locked? ? _('Yes') : _('No')
%tr
%td Tags
%td
@@ -56,12 +60,12 @@
%td Description
%td= @runner.description
%tr
- %td Maximum job timeout
+ %td= _('Maximum job timeout')
%td= @runner.maximum_timeout_human_readable
%tr
%td Last contact
%td
- if @runner.contacted_at
- = time_ago_with_tooltip @runner.contacted_at
+ #{time_ago_in_words(@runner.contacted_at)} ago
- else
Never
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index d35ddf3eb39..fb909237b9a 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -12,7 +12,7 @@
.cover-block.user-cover-block.top-area
.cover-controls
- if @user == current_user
- = link_to profile_path, class: 'btn btn-gray has-tooltip', title: 'Edit profile', 'aria-label': 'Edit profile' do
+ = link_to profile_path, class: 'btn btn-default has-tooltip', title: 'Edit profile', 'aria-label': 'Edit profile' do
= icon('pencil')
- elsif current_user
- if @user.abuse_report
@@ -20,13 +20,13 @@
data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } }
= icon('exclamation-circle')
- else
- = link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn btn-gray',
+ = link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn',
title: 'Report abuse', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
= icon('exclamation-circle')
- = link_to user_path(@user, rss_url_options), class: 'btn btn-gray has-tooltip', title: 'Subscribe', 'aria-label': 'Subscribe' do
+ = link_to user_path(@user, rss_url_options), class: 'btn btn-default has-tooltip', title: 'Subscribe', 'aria-label': 'Subscribe' do
= icon('rss')
- if current_user && current_user.admin?
- = link_to [:admin, @user], class: 'btn btn-gray', title: 'View user in admin area',
+ = link_to [:admin, @user], class: 'btn btn-default', title: 'View user in admin area',
data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('users')
diff --git a/app/workers/admin_email_worker.rb b/app/workers/admin_email_worker.rb
index bec0a003a1c..044e470141e 100644
--- a/app/workers/admin_email_worker.rb
+++ b/app/workers/admin_email_worker.rb
@@ -3,6 +3,12 @@ class AdminEmailWorker
include CronjobQueue
def perform
+ send_repository_check_mail if Gitlab::CurrentSettings.repository_checks_enabled
+ end
+
+ private
+
+ def send_repository_check_mail
repository_check_failed_count = Project.where(last_repository_check_failed: true).count
return if repository_check_failed_count.zero?
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index c469aea7052..b6433eb3eff 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -52,6 +52,7 @@
- pipeline_creation:create_pipeline
- pipeline_creation:run_pipeline_schedule
- pipeline_background:archive_trace
+- pipeline_background:ci_build_trace_chunk_flush
- pipeline_default:build_coverage
- pipeline_default:build_trace_sections
- pipeline_default:pipeline_metrics
@@ -106,9 +107,11 @@
- rebase
- repository_fork
- repository_import
+- repository_remove_remote
- storage_migrator
- system_hook_push
- update_merge_requests
- update_user_activity
- upload_checksum
- web_hook
+- repository_update_remote_mirror
diff --git a/app/workers/ci/build_trace_chunk_flush_worker.rb b/app/workers/ci/build_trace_chunk_flush_worker.rb
new file mode 100644
index 00000000000..218d6688bd9
--- /dev/null
+++ b/app/workers/ci/build_trace_chunk_flush_worker.rb
@@ -0,0 +1,12 @@
+module Ci
+ class BuildTraceChunkFlushWorker
+ include ApplicationWorker
+ include PipelineBackgroundQueue
+
+ def perform(build_trace_chunk_id)
+ ::Ci::BuildTraceChunk.find_by(id: build_trace_chunk_id).try do |build_trace_chunk|
+ build_trace_chunk.use_database!
+ end
+ end
+ end
+end
diff --git a/app/workers/new_note_worker.rb b/app/workers/new_note_worker.rb
index b925741934a..67c54fbf10e 100644
--- a/app/workers/new_note_worker.rb
+++ b/app/workers/new_note_worker.rb
@@ -5,7 +5,7 @@ class NewNoteWorker
# old `NewNoteWorker` jobs (can remove later)
def perform(note_id, _params = {})
if note = Note.find_by(id: note_id)
- NotificationService.new.new_note(note) if note.can_create_notification?
+ NotificationService.new.new_note(note)
Notes::PostProcessService.new(note).execute
else
Rails.logger.error("NewNoteWorker: couldn't find note with ID=#{note_id}, skipping job")
diff --git a/app/workers/object_storage/migrate_uploads_worker.rb b/app/workers/object_storage/migrate_uploads_worker.rb
index a6b2c251254..a3ecfa8e711 100644
--- a/app/workers/object_storage/migrate_uploads_worker.rb
+++ b/app/workers/object_storage/migrate_uploads_worker.rb
@@ -9,85 +9,6 @@ module ObjectStorage
SanityCheckError = Class.new(StandardError)
- class Upload < ActiveRecord::Base
- # Upper limit for foreground checksum processing
- CHECKSUM_THRESHOLD = 100.megabytes
-
- belongs_to :model, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
-
- validates :size, presence: true
- validates :path, presence: true
- validates :model, presence: true
- validates :uploader, presence: true
-
- before_save :calculate_checksum!, if: :foreground_checksummable?
- after_commit :schedule_checksum, if: :checksummable?
-
- scope :stored_locally, -> { where(store: [nil, ObjectStorage::Store::LOCAL]) }
- scope :stored_remotely, -> { where(store: ObjectStorage::Store::REMOTE) }
-
- def self.hexdigest(path)
- Digest::SHA256.file(path).hexdigest
- end
-
- def absolute_path
- raise ObjectStorage::RemoteStoreError, "Remote object has no absolute path." unless local?
- return path unless relative_path?
-
- uploader_class.absolute_path(self)
- end
-
- def calculate_checksum!
- self.checksum = nil
- return unless checksummable?
-
- self.checksum = self.class.hexdigest(absolute_path)
- end
-
- def build_uploader(mounted_as = nil)
- uploader_class.new(model, mounted_as).tap do |uploader|
- uploader.upload = self
- uploader.retrieve_from_store!(identifier)
- end
- end
-
- def exist?
- File.exist?(absolute_path)
- end
-
- def local?
- return true if store.nil?
-
- store == ObjectStorage::Store::LOCAL
- end
-
- private
-
- def checksummable?
- checksum.nil? && local? && exist?
- end
-
- def foreground_checksummable?
- checksummable? && size <= CHECKSUM_THRESHOLD
- end
-
- def schedule_checksum
- UploadChecksumWorker.perform_async(id)
- end
-
- def relative_path?
- !path.start_with?('/')
- end
-
- def identifier
- File.basename(path)
- end
-
- def uploader_class
- Object.const_get(uploader)
- end
- end
-
class MigrationResult
attr_reader :upload
attr_accessor :error
diff --git a/app/workers/repository_check/batch_worker.rb b/app/workers/repository_check/batch_worker.rb
index 76688cf51c1..72f0a9b0619 100644
--- a/app/workers/repository_check/batch_worker.rb
+++ b/app/workers/repository_check/batch_worker.rb
@@ -4,8 +4,11 @@ module RepositoryCheck
include CronjobQueue
RUN_TIME = 3600
+ BATCH_SIZE = 10_000
def perform
+ return unless Gitlab::CurrentSettings.repository_checks_enabled
+
start = Time.now
# This loop will break after a little more than one hour ('a little
@@ -15,7 +18,6 @@ module RepositoryCheck
# check, only one (or two) will be checked at a time.
project_ids.each do |project_id|
break if Time.now - start >= RUN_TIME
- break unless current_settings.repository_checks_enabled
next unless try_obtain_lease(project_id)
@@ -31,12 +33,20 @@ module RepositoryCheck
# getting ID's from Postgres is not terribly slow, and because no user
# has to sit and wait for this query to finish.
def project_ids
- limit = 10_000
- never_checked_projects = Project.where('last_repository_check_at IS NULL AND created_at < ?', 24.hours.ago)
- .limit(limit).pluck(:id)
- old_check_projects = Project.where('last_repository_check_at < ?', 1.month.ago)
- .reorder('last_repository_check_at ASC').limit(limit).pluck(:id)
- never_checked_projects + old_check_projects
+ never_checked_project_ids(BATCH_SIZE) + old_checked_project_ids(BATCH_SIZE)
+ end
+
+ def never_checked_project_ids(batch_size)
+ Project.where(last_repository_check_at: nil)
+ .where('created_at < ?', 24.hours.ago)
+ .limit(batch_size).pluck(:id)
+ end
+
+ def old_checked_project_ids(batch_size)
+ Project.where.not(last_repository_check_at: nil)
+ .where('last_repository_check_at < ?', 1.month.ago)
+ .reorder(last_repository_check_at: :asc)
+ .limit(batch_size).pluck(:id)
end
def try_obtain_lease(id)
@@ -47,16 +57,5 @@ module RepositoryCheck
timeout: 24.hours
).try_obtain
end
-
- def current_settings
- # No caching of the settings! If we cache them and an admin disables
- # this feature, an active RepositoryCheckWorker would keep going for up
- # to 1 hour after the feature was disabled.
- if Rails.env.test?
- Gitlab::CurrentSettings.fake_application_settings
- else
- ApplicationSetting.current
- end
- end
end
end
diff --git a/app/workers/repository_check/single_repository_worker.rb b/app/workers/repository_check/single_repository_worker.rb
index 116bc185b38..3cffb8b14e4 100644
--- a/app/workers/repository_check/single_repository_worker.rb
+++ b/app/workers/repository_check/single_repository_worker.rb
@@ -5,27 +5,34 @@ module RepositoryCheck
def perform(project_id)
project = Project.find(project_id)
+ healthy = project_healthy?(project)
+
+ update_repository_check_status(project, healthy)
+ end
+
+ private
+
+ def update_repository_check_status(project, healthy)
project.update_columns(
- last_repository_check_failed: !check(project),
+ last_repository_check_failed: !healthy,
last_repository_check_at: Time.now
)
end
- private
+ def project_healthy?(project)
+ repo_healthy?(project) && wiki_repo_healthy?(project)
+ end
- def check(project)
- if has_pushes?(project) && !git_fsck(project.repository)
- false
- elsif project.wiki_enabled?
- # Historically some projects never had their wiki repos initialized;
- # this happens on project creation now. Let's initialize an empty repo
- # if it is not already there.
- project.create_wiki
+ def repo_healthy?(project)
+ return true unless has_changes?(project)
- git_fsck(project.wiki.repository)
- else
- true
- end
+ git_fsck(project.repository)
+ end
+
+ def wiki_repo_healthy?(project)
+ return true unless has_wiki_changes?(project)
+
+ git_fsck(project.wiki.repository)
end
def git_fsck(repository)
@@ -39,8 +46,19 @@ module RepositoryCheck
false
end
- def has_pushes?(project)
+ def has_changes?(project)
Project.with_push.exists?(project.id)
end
+
+ def has_wiki_changes?(project)
+ return false unless project.wiki_enabled?
+
+ # Historically some projects never had their wiki repos initialized;
+ # this happens on project creation now. Let's initialize an empty repo
+ # if it is not already there.
+ return false unless project.create_wiki
+
+ has_changes?(project)
+ end
end
end
diff --git a/app/workers/repository_remove_remote_worker.rb b/app/workers/repository_remove_remote_worker.rb
new file mode 100644
index 00000000000..1c19b604b77
--- /dev/null
+++ b/app/workers/repository_remove_remote_worker.rb
@@ -0,0 +1,35 @@
+class RepositoryRemoveRemoteWorker
+ include ApplicationWorker
+ include ExclusiveLeaseGuard
+
+ LEASE_TIMEOUT = 1.hour
+
+ attr_reader :project, :remote_name
+
+ def perform(project_id, remote_name)
+ @remote_name = remote_name
+ @project = Project.find_by_id(project_id)
+
+ return unless @project
+
+ logger.info("Removing remote #{remote_name} from project #{project.id}")
+
+ try_obtain_lease do
+ remove_remote = @project.repository.remove_remote(remote_name)
+
+ if remove_remote
+ logger.info("Remote #{remote_name} was successfully removed from project #{project.id}")
+ else
+ logger.error("Could not remove remote #{remote_name} from project #{project.id}")
+ end
+ end
+ end
+
+ def lease_timeout
+ LEASE_TIMEOUT
+ end
+
+ def lease_key
+ "remove_remote_#{project.id}_#{remote_name}"
+ end
+end
diff --git a/app/workers/repository_update_remote_mirror_worker.rb b/app/workers/repository_update_remote_mirror_worker.rb
new file mode 100644
index 00000000000..bb963979e88
--- /dev/null
+++ b/app/workers/repository_update_remote_mirror_worker.rb
@@ -0,0 +1,49 @@
+class RepositoryUpdateRemoteMirrorWorker
+ UpdateAlreadyInProgressError = Class.new(StandardError)
+ UpdateError = Class.new(StandardError)
+
+ include ApplicationWorker
+ include Gitlab::ShellAdapter
+
+ sidekiq_options retry: 3, dead: false
+
+ sidekiq_retry_in { |count| 30 * count }
+
+ sidekiq_retries_exhausted do |msg, _|
+ Sidekiq.logger.warn "Failed #{msg['class']} with #{msg['args']}: #{msg['error_message']}"
+ end
+
+ def perform(remote_mirror_id, scheduled_time)
+ remote_mirror = RemoteMirror.find(remote_mirror_id)
+ return if remote_mirror.updated_since?(scheduled_time)
+
+ raise UpdateAlreadyInProgressError if remote_mirror.update_in_progress?
+
+ remote_mirror.update_start
+
+ project = remote_mirror.project
+ current_user = project.creator
+ result = Projects::UpdateRemoteMirrorService.new(project, current_user).execute(remote_mirror)
+ raise UpdateError, result[:message] if result[:status] == :error
+
+ remote_mirror.update_finish
+ rescue UpdateAlreadyInProgressError
+ raise
+ rescue UpdateError => ex
+ fail_remote_mirror(remote_mirror, ex.message)
+ raise
+ rescue => ex
+ return unless remote_mirror
+
+ fail_remote_mirror(remote_mirror, ex.message)
+ raise UpdateError, "#{ex.class}: #{ex.message}"
+ end
+
+ private
+
+ def fail_remote_mirror(remote_mirror, message)
+ remote_mirror.mark_as_failed(message)
+
+ Rails.logger.error(message)
+ end
+end
diff --git a/bin/secpick b/bin/secpick
index 76ae231e913..5029fe57cfe 100755
--- a/bin/secpick
+++ b/bin/secpick
@@ -5,7 +5,6 @@ require 'rainbow/refinement'
using Rainbow
BRANCH_PREFIX = 'security'.freeze
-STABLE_BRANCH_SUFFIX = 'stable'.freeze
REMOTE = 'dev'.freeze
options = { version: nil, branch: nil, sha: nil }
@@ -37,9 +36,9 @@ abort("Missing options. Use #{$0} --help to see the list of options available".r
abort("Wrong version format #{options[:version].bold}".red) unless options[:version] =~ /\A\d*\-\d*\Z/
branch = [BRANCH_PREFIX, options[:branch], options[:version]].join('-').freeze
-stable_branch = "#{options[:version]}-#{STABLE_BRANCH_SUFFIX}".freeze
+stable_branch = "#{BRANCH_PREFIX}-#{options[:version]}".freeze
-command = "git checkout #{stable_branch} && git pull #{REMOTE} #{stable_branch} && git checkout -B #{branch} && git cherry-pick #{options[:sha]} && git push #{REMOTE} #{branch}"
+command = "git fetch #{REMOTE} #{stable_branch} && git checkout #{stable_branch} && git pull #{REMOTE} #{stable_branch} && git checkout -B #{branch} && git cherry-pick #{options[:sha]} && git push #{REMOTE} #{branch}"
_stdin, stdout, stderr = Open3.popen3(command)
diff --git a/bin/spinach b/bin/spinach
deleted file mode 100755
index eda81c9ed8a..00000000000
--- a/bin/spinach
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/usr/bin/env ruby
-
-# Remove this block when removing rails5? code.
-gemfile = %w[1 true].include?(ENV["RAILS5"]) ? "Gemfile.rails5" : "Gemfile"
-ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../#{gemfile}", __dir__)
-
-begin
- load File.expand_path('../spring', __FILE__)
-rescue LoadError => e
- raise unless e.message.include?('spring')
-end
-require 'bundler/setup'
-load Gem.bin_path('spinach', 'spinach')
diff --git a/changelogs/unreleased/23465-print-markdown.yml b/changelogs/unreleased/23465-print-markdown.yml
new file mode 100644
index 00000000000..ba950667acc
--- /dev/null
+++ b/changelogs/unreleased/23465-print-markdown.yml
@@ -0,0 +1,5 @@
+---
+title: Fix print styles for markdown pages
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/33697-pipelines-json-endpoint.yml b/changelogs/unreleased/33697-pipelines-json-endpoint.yml
new file mode 100644
index 00000000000..d44e2729415
--- /dev/null
+++ b/changelogs/unreleased/33697-pipelines-json-endpoint.yml
@@ -0,0 +1,5 @@
+---
+title: Use VueJS for rendering pipeline stages
+merge_request:
+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
new file mode 100644
index 00000000000..082e0544dea
--- /dev/null
+++ b/changelogs/unreleased/36983-osw-heading-labels-color-fix.yml
@@ -0,0 +1,5 @@
+---
+title: Adjust issue boards list header label text color
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/40725-move-mr-external-link-to-right.yml b/changelogs/unreleased/40725-move-mr-external-link-to-right.yml
new file mode 100644
index 00000000000..e3ebeb5eb61
--- /dev/null
+++ b/changelogs/unreleased/40725-move-mr-external-link-to-right.yml
@@ -0,0 +1,5 @@
+---
+title: Moves MR widget external link icon to the right
+merge_request: 18828
+author: Jacopo Beschi @jacopo-beschi
+type: changed
diff --git a/changelogs/unreleased/40855_remove_authentication_in_readonly_issue_api.yml b/changelogs/unreleased/40855_remove_authentication_in_readonly_issue_api.yml
new file mode 100644
index 00000000000..58686639594
--- /dev/null
+++ b/changelogs/unreleased/40855_remove_authentication_in_readonly_issue_api.yml
@@ -0,0 +1,5 @@
+---
+title: made listing and showing public issue apis available without authentication
+merge_request: 18638
+author: haseebeqx
+type: changed
diff --git a/changelogs/unreleased/41082-make-deploykeys-table-more-clearly-structured.yml b/changelogs/unreleased/41082-make-deploykeys-table-more-clearly-structured.yml
new file mode 100644
index 00000000000..23704c2b37b
--- /dev/null
+++ b/changelogs/unreleased/41082-make-deploykeys-table-more-clearly-structured.yml
@@ -0,0 +1,5 @@
+---
+title: Make project deploy keys table more clearly structured
+merge_request: 18279
+author:
+type: changed
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
new file mode 100644
index 00000000000..f23521ea416
--- /dev/null
+++ b/changelogs/unreleased/42099-port-push-mirroring-to-ce-ce-port-v-2.yml
@@ -0,0 +1,5 @@
+---
+title: Adds push mirrors to GitLab Community Edition
+merge_request: 18715
+author:
+type: changed
diff --git a/changelogs/unreleased/43469-gcp-account-offer.yml b/changelogs/unreleased/43469-gcp-account-offer.yml
new file mode 100644
index 00000000000..323a4b81731
--- /dev/null
+++ b/changelogs/unreleased/43469-gcp-account-offer.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..a7128f7481e
--- /dev/null
+++ b/changelogs/unreleased/43557-osw-present-merge-sha-commit.yml
@@ -0,0 +1,5 @@
+---
+title: Display merge commit SHA in merge widget after merge
+merge_request: 18722
+author:
+type: added
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
new file mode 100644
index 00000000000..8854eeb5fba
--- /dev/null
+++ b/changelogs/unreleased/44059-specify-variables-when-executing-a-manual-pipeline-from-the-ui.yml
@@ -0,0 +1,5 @@
+---
+title: Enable specifying variables when executing a manual pipeline
+merge_request: 18440
+author:
+type: changed
diff --git a/changelogs/unreleased/44319-remove-gray-buttons.yml b/changelogs/unreleased/44319-remove-gray-buttons.yml
new file mode 100644
index 00000000000..9803dde8493
--- /dev/null
+++ b/changelogs/unreleased/44319-remove-gray-buttons.yml
@@ -0,0 +1,5 @@
+---
+title: Remove gray button styles
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/44833-ide-clean-up-status-bar.yml b/changelogs/unreleased/44833-ide-clean-up-status-bar.yml
new file mode 100644
index 00000000000..4c827e57195
--- /dev/null
+++ b/changelogs/unreleased/44833-ide-clean-up-status-bar.yml
@@ -0,0 +1,5 @@
+---
+title: Clean up WebIDE status bar and add useful info
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/44879.yml b/changelogs/unreleased/44879.yml
new file mode 100644
index 00000000000..b51e057bb7b
--- /dev/null
+++ b/changelogs/unreleased/44879.yml
@@ -0,0 +1,5 @@
+---
+title: Add the signature verfication badge to the compare view
+merge_request: 18245
+author: Marc Shaw
+type: added
diff --git a/changelogs/unreleased/45442-updates-updated-at-to-issue-on-time-spent.yml b/changelogs/unreleased/45442-updates-updated-at-to-issue-on-time-spent.yml
new file mode 100644
index 00000000000..0694206d4fb
--- /dev/null
+++ b/changelogs/unreleased/45442-updates-updated-at-to-issue-on-time-spent.yml
@@ -0,0 +1,5 @@
+---
+title: Updates updated_at on issuable when setting time spent
+merge_request: 18757
+author: Jacopo Beschi @jacopo-beschi
+type: added
diff --git a/changelogs/unreleased/45715-remove-modal-retry.yml b/changelogs/unreleased/45715-remove-modal-retry.yml
new file mode 100644
index 00000000000..04f2ff5142e
--- /dev/null
+++ b/changelogs/unreleased/45715-remove-modal-retry.yml
@@ -0,0 +1,5 @@
+---
+title: Remove modalbox confirmation when retrying a pipeline
+merge_request: 18879
+author:
+type: changed
diff --git a/changelogs/unreleased/46010-add-index-to-runner-type.yml b/changelogs/unreleased/46010-add-index-to-runner-type.yml
new file mode 100644
index 00000000000..fb8340e37b2
--- /dev/null
+++ b/changelogs/unreleased/46010-add-index-to-runner-type.yml
@@ -0,0 +1,5 @@
+---
+title: Add index on runner_type for ci_runners
+merge_request: 18897
+author:
+type: performance
diff --git a/changelogs/unreleased/46210-terms-acceptance-dropdown-menu.yml b/changelogs/unreleased/46210-terms-acceptance-dropdown-menu.yml
new file mode 100644
index 00000000000..8a7c549e356
--- /dev/null
+++ b/changelogs/unreleased/46210-terms-acceptance-dropdown-menu.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..e9cd8977394
--- /dev/null
+++ b/changelogs/unreleased/46286-fix-ingress-rbac-default-value.yml
@@ -0,0 +1,5 @@
+---
+title: Disables RBAC on nginx-ingress
+merge_request: 18947
+author:
+type: fixed
diff --git a/changelogs/unreleased/46345-kubernetes-popover-illustration-skewed.yml b/changelogs/unreleased/46345-kubernetes-popover-illustration-skewed.yml
new file mode 100644
index 00000000000..a0e6b39fef6
--- /dev/null
+++ b/changelogs/unreleased/46345-kubernetes-popover-illustration-skewed.yml
@@ -0,0 +1,5 @@
+---
+title: Correct skewed Kubernetes popover illustration
+merge_request: 18949
+author:
+type: fixed
diff --git a/changelogs/unreleased/46361-does-not-log-failed-sign-in-attempts-when-the-database-is-in-read-only-mode.yml b/changelogs/unreleased/46361-does-not-log-failed-sign-in-attempts-when-the-database-is-in-read-only-mode.yml
new file mode 100644
index 00000000000..e4255f11ecf
--- /dev/null
+++ b/changelogs/unreleased/46361-does-not-log-failed-sign-in-attempts-when-the-database-is-in-read-only-mode.yml
@@ -0,0 +1,5 @@
+---
+title: Does not log failed sign-in attempts when the database is in read-only mode
+merge_request: 18957
+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
new file mode 100644
index 00000000000..d778b44c110
--- /dev/null
+++ b/changelogs/unreleased/5750-backport-checksum-git-commanderror-exit-status-128.yml
@@ -0,0 +1,6 @@
+---
+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
new file mode 100644
index 00000000000..4db0ff4f3a0
--- /dev/null
+++ b/changelogs/unreleased/5794-we-should-failover-gracefully-when-we-can-t-connect-to-geo-tracking-database-ce.yml
@@ -0,0 +1,5 @@
+---
+title: ShaAttribute no longer stops startup if database is missing
+merge_request: 18726
+author:
+type: fixed
diff --git a/changelogs/unreleased/add-git-commit-message-predefined-variable.yml b/changelogs/unreleased/add-git-commit-message-predefined-variable.yml
new file mode 100644
index 00000000000..183fe69936e
--- /dev/null
+++ b/changelogs/unreleased/add-git-commit-message-predefined-variable.yml
@@ -0,0 +1,5 @@
+---
+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/blackst0ne-remove-spinach.yml b/changelogs/unreleased/blackst0ne-remove-spinach.yml
new file mode 100644
index 00000000000..104da257bad
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-remove-spinach.yml
@@ -0,0 +1,5 @@
+---
+title: Remove Spinach
+merge_request: 18869
+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
new file mode 100644
index 00000000000..98c56cf2b57
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-replace-spinach-project-builds-artifacts-feature.yml
@@ -0,0 +1,5 @@
+---
+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-deploy-keys-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-deploy-keys-feature.yml
new file mode 100644
index 00000000000..7014de4ece7
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-replace-spinach-project-deploy-keys-feature.yml
@@ -0,0 +1,5 @@
+---
+title: 'Replace the `project/deploy_keys.feature` spinach test with an rspec analog'
+merge_request: 18796
+author: '@blackst0ne'
+type: other
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-ff-merge-requests-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-ff-merge-requests-feature.yml
new file mode 100644
index 00000000000..7802391ec64
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-replace-spinach-project-ff-merge-requests-feature.yml
@@ -0,0 +1,5 @@
+---
+title: 'Replace the `project/ff_merge_requests.feature` spinach test with an rspec analog'
+merge_request: 18800
+author: '@blackst0ne'
+type: other
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-forked-merge-requests-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-forked-merge-requests-feature.yml
new file mode 100644
index 00000000000..2ac43490c26
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-replace-spinach-project-forked-merge-requests-feature.yml
@@ -0,0 +1,5 @@
+---
+title: 'Replace the `project/forked_merge_requests.feature` spinach test with an rspec analog'
+merge_request: 18867
+author: '@blackst0ne'
+type: other
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-references-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-references-feature.yml
new file mode 100644
index 00000000000..968a937ca5a
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-references-feature.yml
@@ -0,0 +1,5 @@
+---
+title: 'Replace the `project/issues/references.feature` spinach test with an rspec analog'
+merge_request: 18769
+author: '@blackst0ne'
+type: other
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-merge-requests-references-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-merge-requests-references-feature.yml
new file mode 100644
index 00000000000..c0ba984bfdc
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-replace-spinach-project-merge-requests-references-feature.yml
@@ -0,0 +1,5 @@
+---
+title: 'Replace the `project/merge_requests/references.feature` spinach test with an rspec analog'
+merge_request: 18794
+author: '@blackst0ne'
+type: other
diff --git a/changelogs/unreleased/bvl-restrict-api-git-for-terms.yml b/changelogs/unreleased/bvl-restrict-api-git-for-terms.yml
new file mode 100644
index 00000000000..49cd04b065b
--- /dev/null
+++ b/changelogs/unreleased/bvl-restrict-api-git-for-terms.yml
@@ -0,0 +1,6 @@
+---
+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/docs-42067-document-runner-registration-api.yml b/changelogs/unreleased/docs-42067-document-runner-registration-api.yml
new file mode 100644
index 00000000000..6b507174044
--- /dev/null
+++ b/changelogs/unreleased/docs-42067-document-runner-registration-api.yml
@@ -0,0 +1,5 @@
+---
+title: Expand documentation for Runners API
+merge_request: 16484
+author:
+type: other
diff --git a/changelogs/unreleased/dz-add-2fa-filter.yml b/changelogs/unreleased/dz-add-2fa-filter.yml
new file mode 100644
index 00000000000..82d501d6604
--- /dev/null
+++ b/changelogs/unreleased/dz-add-2fa-filter.yml
@@ -0,0 +1,5 @@
+---
+title: Add 2FA filter to the group members page
+merge_request: 18483
+author:
+type: changed
diff --git a/changelogs/unreleased/feature-expose-runner-ip-to-api.yml b/changelogs/unreleased/feature-expose-runner-ip-to-api.yml
new file mode 100644
index 00000000000..e755cf5f2d4
--- /dev/null
+++ b/changelogs/unreleased/feature-expose-runner-ip-to-api.yml
@@ -0,0 +1,5 @@
+---
+title: Expose runner ip address to runners API
+merge_request: 18799
+author: Lars Greiss
+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
new file mode 100644
index 00000000000..bded7bb7cc4
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-add-pipeline-builds-foreign-key.yml
@@ -0,0 +1,5 @@
+---
+title: Add database foreign key constraint between pipelines and build
+merge_request: 18822
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-project-mirror-data-schema.yml b/changelogs/unreleased/fix-project-mirror-data-schema.yml
new file mode 100644
index 00000000000..107f1fe3b9c
--- /dev/null
+++ b/changelogs/unreleased/fix-project-mirror-data-schema.yml
@@ -0,0 +1,6 @@
+---
+title: Fixes database inconsistencies between Community and Enterprise Edition on
+ import state
+merge_request: 18811
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-reactive-cache-retry-rate.yml b/changelogs/unreleased/fix-reactive-cache-retry-rate.yml
new file mode 100644
index 00000000000..044e7fe39c0
--- /dev/null
+++ b/changelogs/unreleased/fix-reactive-cache-retry-rate.yml
@@ -0,0 +1,5 @@
+---
+title: Update commit status from external CI services less aggressively
+merge_request: 18802
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-registry-created-at-tooltip.yml b/changelogs/unreleased/fix-registry-created-at-tooltip.yml
new file mode 100644
index 00000000000..911b3b10fd4
--- /dev/null
+++ b/changelogs/unreleased/fix-registry-created-at-tooltip.yml
@@ -0,0 +1,5 @@
+---
+title: 'Add missing tooltip to creation date on container registry overview'
+merge_request: 18767
+author: Lars Greiss
+type: fixed
diff --git a/changelogs/unreleased/fix-shorcut-modal.yml b/changelogs/unreleased/fix-shorcut-modal.yml
new file mode 100644
index 00000000000..796a1523a61
--- /dev/null
+++ b/changelogs/unreleased/fix-shorcut-modal.yml
@@ -0,0 +1,5 @@
+---
+title: Fix modal width of shorcuts help page
+merge_request: 18766
+author: Lars Greiss
+type: fixed
diff --git a/changelogs/unreleased/fix-shortcut-close-screen-with-key.yml b/changelogs/unreleased/fix-shortcut-close-screen-with-key.yml
new file mode 100644
index 00000000000..9cbc856a075
--- /dev/null
+++ b/changelogs/unreleased/fix-shortcut-close-screen-with-key.yml
@@ -0,0 +1,5 @@
+---
+title: Fix close keyboard shortcuts dialog using the keyboard shortcut
+merge_request: 18783
+author: Lars Greiss
+type: fixed
diff --git a/changelogs/unreleased/fix-wiki-find-page-invalid-encoding.yml b/changelogs/unreleased/fix-wiki-find-page-invalid-encoding.yml
new file mode 100644
index 00000000000..f003bef8671
--- /dev/null
+++ b/changelogs/unreleased/fix-wiki-find-page-invalid-encoding.yml
@@ -0,0 +1,5 @@
+---
+title: Fix finding wiki pages when they have invalidly-encoded content
+merge_request: 18856
+author:
+type: fixed
diff --git a/changelogs/unreleased/ide-hide-merge-request-if-disabled.yml b/changelogs/unreleased/ide-hide-merge-request-if-disabled.yml
new file mode 100644
index 00000000000..9efef2c6839
--- /dev/null
+++ b/changelogs/unreleased/ide-hide-merge-request-if-disabled.yml
@@ -0,0 +1,5 @@
+---
+title: Hide merge request option in IDE when disabled
+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
new file mode 100644
index 00000000000..3fb9b03725e
--- /dev/null
+++ b/changelogs/unreleased/improve-commit-message-body-rendering.yml
@@ -0,0 +1,5 @@
+---
+title: Improve commit message body rendering and fix responsive compare panels
+merge_request: 18725
+author: Constance Okoghenun
+type: changed
diff --git a/changelogs/unreleased/issue_43660.yml b/changelogs/unreleased/issue_43660.yml
new file mode 100644
index 00000000000..d83c0ebcbb5
--- /dev/null
+++ b/changelogs/unreleased/issue_43660.yml
@@ -0,0 +1,5 @@
+---
+title: Enable prometheus monitoring by default
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/jprovazn-null-byte.yml b/changelogs/unreleased/jprovazn-null-byte.yml
new file mode 100644
index 00000000000..4c4760ac412
--- /dev/null
+++ b/changelogs/unreleased/jprovazn-null-byte.yml
@@ -0,0 +1,5 @@
+---
+title: Fix filename matching when processing file or blob search results
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/jprovazn-pipeline-policy.yml b/changelogs/unreleased/jprovazn-pipeline-policy.yml
new file mode 100644
index 00000000000..2997c6c8667
--- /dev/null
+++ b/changelogs/unreleased/jprovazn-pipeline-policy.yml
@@ -0,0 +1,6 @@
+---
+title: Allow maintainers to retry pipelines on forked projects (if allowed in merge
+ request)
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/jr-46209-web-ide-copy.yml b/changelogs/unreleased/jr-46209-web-ide-copy.yml
new file mode 100644
index 00000000000..87ccae6ced0
--- /dev/null
+++ b/changelogs/unreleased/jr-46209-web-ide-copy.yml
@@ -0,0 +1,5 @@
+---
+title: Fix outdated Web IDE welcome copy
+merge_request: 18861
+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
new file mode 100644
index 00000000000..ab22739b73d
--- /dev/null
+++ b/changelogs/unreleased/live-trace-v2-efficient-destroy-all.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..875a66bc565
--- /dev/null
+++ b/changelogs/unreleased/live-trace-v2.yml
@@ -0,0 +1,5 @@
+---
+title: New CI Job live-trace architecture
+merge_request: 18169
+author:
+type: changed
diff --git a/changelogs/unreleased/refactor-move-squash-before-merge-vue-component.yml b/changelogs/unreleased/refactor-move-squash-before-merge-vue-component.yml
new file mode 100644
index 00000000000..b8b2762a21d
--- /dev/null
+++ b/changelogs/unreleased/refactor-move-squash-before-merge-vue-component.yml
@@ -0,0 +1,5 @@
+---
+title: Move SquashBeforeMerge vue component
+merge_request: 18813
+author: George Tsiolis
+type: performance
diff --git a/changelogs/unreleased/sh-enforce-unique-and-not-null-project-ids-project-features.yml b/changelogs/unreleased/sh-enforce-unique-and-not-null-project-ids-project-features.yml
new file mode 100644
index 00000000000..aae42b66c84
--- /dev/null
+++ b/changelogs/unreleased/sh-enforce-unique-and-not-null-project-ids-project-features.yml
@@ -0,0 +1,5 @@
+---
+title: Add a unique and not null constraint on the project_features.project_id column
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-blocked-user-account-ldap.yml b/changelogs/unreleased/sh-fix-blocked-user-account-ldap.yml
new file mode 100644
index 00000000000..f7abe763ea8
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-blocked-user-account-ldap.yml
@@ -0,0 +1,5 @@
+---
+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-cross-site-origin-uploads-js.yml b/changelogs/unreleased/sh-fix-cross-site-origin-uploads-js.yml
new file mode 100644
index 00000000000..3c51aaae896
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-cross-site-origin-uploads-js.yml
@@ -0,0 +1,5 @@
+---
+title: Fix cross-origin errors when attempting to download JavaScript attachments
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/support-active-setting-while-registering-a-runner.yml b/changelogs/unreleased/support-active-setting-while-registering-a-runner.yml
new file mode 100644
index 00000000000..6c378fd450a
--- /dev/null
+++ b/changelogs/unreleased/support-active-setting-while-registering-a-runner.yml
@@ -0,0 +1,5 @@
+---
+title: Add support for 'active' setting on Runner Registration API endpoint
+merge_request: 18848
+author:
+type: changed
diff --git a/changelogs/unreleased/tc-repo-verify-mails.yml b/changelogs/unreleased/tc-repo-verify-mails.yml
new file mode 100644
index 00000000000..b4d3c4b1596
--- /dev/null
+++ b/changelogs/unreleased/tc-repo-verify-mails.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..5dfd8154ecd
--- /dev/null
+++ b/changelogs/unreleased/tz-upgrade-underscore.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade underscore.js to 1.9.0
+merge_request: 18578
+author:
+type: other
diff --git a/changelogs/unreleased/update-wiki-modal.yml b/changelogs/unreleased/update-wiki-modal.yml
new file mode 100644
index 00000000000..00f2fc4f181
--- /dev/null
+++ b/changelogs/unreleased/update-wiki-modal.yml
@@ -0,0 +1,5 @@
+---
+title: New design for wiki page deletion confirmation
+merge_request: 18712
+author: Constance Okoghenun
+type: added
diff --git a/changelogs/unreleased/zj-wiki-find-file-opt-out.yml b/changelogs/unreleased/zj-wiki-find-file-opt-out.yml
new file mode 100644
index 00000000000..5af53c56017
--- /dev/null
+++ b/changelogs/unreleased/zj-wiki-find-file-opt-out.yml
@@ -0,0 +1,5 @@
+---
+title: Finding a wiki page is done by Gitaly by default
+merge_request:
+author:
+type: other
diff --git a/config/initializers/6_validations.rb b/config/initializers/6_validations.rb
index d92cdb97766..89aabe530fe 100644
--- a/config/initializers/6_validations.rb
+++ b/config/initializers/6_validations.rb
@@ -26,17 +26,6 @@ def validate_storages_config
Gitlab.config.repositories.storages.each do |name, repository_storage|
storage_validation_error("\"#{name}\" is not a valid storage name") unless storage_name_valid?(name)
- if repository_storage.is_a?(String)
- raise "#{name} is not a valid storage, because it has no `path` key. " \
- "It may be configured as:\n\n#{name}:\n path: #{repository_storage}\n\n" \
- "For source installations, update your config/gitlab.yml Refer to gitlab.yml.example for an updated example.\n\n" \
- "If you're using the Gitlab Development Kit, you can update your configuration running `gdk reconfigure`.\n"
- end
-
- if !repository_storage.is_a?(Gitlab::GitalyClient::StorageSettings) || repository_storage.legacy_disk_path.nil?
- storage_validation_error("#{name} is not a valid storage, because it has no `path` key. Refer to gitlab.yml.example for an updated example")
- end
-
%w(failure_count_threshold failure_reset_time storage_timeout).each do |setting|
# Falling back to the defaults is fine!
next if repository_storage[setting].nil?
diff --git a/config/initializers/8_metrics.rb b/config/initializers/8_metrics.rb
index 7cdf49159b4..8a851b89c56 100644
--- a/config/initializers/8_metrics.rb
+++ b/config/initializers/8_metrics.rb
@@ -119,7 +119,14 @@ def instrument_classes(instrumentation)
end
# rubocop:enable Metrics/AbcSize
-if Gitlab::Metrics.enabled?
+# With prometheus enabled by default this breaks all specs
+# that stubs methods using `any_instance_of` for the models reloaded here.
+#
+# We should deprecate the usage of `any_instance_of` in the future
+# check: https://github.com/rspec/rspec-mocks#settings-mocks-or-stubs-on-any-instance-of-a-class
+#
+# Related issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/33587
+if Gitlab::Metrics.enabled? && !Rails.env.test?
require 'pathname'
require 'influxdb'
require 'connection_pool'
diff --git a/config/initializers/deprecations.rb b/config/initializers/deprecations.rb
index 2476ea9e38a..c8d7f742bb1 100644
--- a/config/initializers/deprecations.rb
+++ b/config/initializers/deprecations.rb
@@ -1,5 +1,9 @@
-deprecator = ActiveSupport::Deprecation.new('11.0', 'GitLab')
-
if Gitlab.dev_env_or_com?
+ deprecator = ActiveSupport::Deprecation.new('11.0', 'GitLab')
+
+ deprecator.behavior = -> (message, callstack) {
+ Rails.logger.warn("#{message}: #{callstack[1..20].join}")
+ }
+
ActiveSupport::Deprecation.deprecate_methods(Gitlab::GitalyClient::StorageSettings, :legacy_disk_path, deprecator: deprecator)
end
diff --git a/config/initializers/gollum.rb b/config/initializers/gollum.rb
index 81e0577a7c9..ea9cc151a57 100644
--- a/config/initializers/gollum.rb
+++ b/config/initializers/gollum.rb
@@ -7,6 +7,20 @@ module Gollum
end
require "gollum-lib"
+module Gollum
+ class Page
+ def text_data(encoding = nil)
+ data = if raw_data.respond_to?(:encoding)
+ raw_data.force_encoding(encoding || Encoding::UTF_8)
+ else
+ raw_data
+ end
+
+ Gitlab::EncodingHelper.encode!(data)
+ end
+ end
+end
+
Rails.application.configure do
config.after_initialize do
Gollum::Page.per_page = Kaminari.config.default_per_page
diff --git a/config/initializers/static_files.rb b/config/initializers/static_files.rb
index d3a7a2b9f8b..6c28686e69a 100644
--- a/config/initializers/static_files.rb
+++ b/config/initializers/static_files.rb
@@ -34,7 +34,7 @@ if app.config.serve_static_files
)
app.config.middleware.insert_before(
Gitlab::Middleware::Static,
- Gitlab::Middleware::WebpackProxy,
+ Gitlab::Webpack::DevServerMiddleware,
proxy_path: app.config.webpack.public_path,
proxy_host: dev_server.host,
proxy_port: dev_server.port
diff --git a/config/initializers/warden.rb b/config/initializers/warden.rb
index bf079f8e1a7..8cc36820d3c 100644
--- a/config/initializers/warden.rb
+++ b/config/initializers/warden.rb
@@ -1,21 +1,21 @@
Rails.application.configure do |config|
- Warden::Manager.after_set_user do |user, auth, opts|
+ Warden::Manager.after_set_user(scope: :user) do |user, auth, opts|
Gitlab::Auth::UniqueIpsLimiter.limit_user!(user)
end
- Warden::Manager.before_failure do |env, opts|
+ Warden::Manager.before_failure(scope: :user) do |env, opts|
Gitlab::Auth::BlockedUserTracker.log_if_user_blocked(env)
end
- Warden::Manager.after_authentication do |user, auth, opts|
+ Warden::Manager.after_authentication(scope: :user) do |user, auth, opts|
ActiveSession.cleanup(user)
end
- Warden::Manager.after_set_user only: :fetch do |user, auth, opts|
+ Warden::Manager.after_set_user(scope: :user, only: :fetch) do |user, auth, opts|
ActiveSession.set(user, auth.request)
end
- Warden::Manager.before_logout do |user, auth, opts|
+ Warden::Manager.before_logout(scope: :user) do |user, auth, opts|
ActiveSession.destroy(user || auth.user, auth.request.session.id)
end
end
diff --git a/config/karma.config.js b/config/karma.config.js
index 3eb220eed99..28a688797d9 100644
--- a/config/karma.config.js
+++ b/config/karma.config.js
@@ -12,16 +12,14 @@ function fatalError(message) {
process.exit(1);
}
-// remove problematic plugins
-if (webpackConfig.plugins) {
- webpackConfig.plugins = webpackConfig.plugins.filter(function(plugin) {
- return !(
- plugin instanceof webpack.optimize.CommonsChunkPlugin ||
- plugin instanceof webpack.optimize.ModuleConcatenationPlugin ||
- plugin instanceof webpack.DefinePlugin
- );
- });
-}
+// disable problematic options
+webpackConfig.entry = undefined;
+webpackConfig.mode = 'development';
+webpackConfig.optimization.runtimeChunk = false;
+webpackConfig.optimization.splitChunks = false;
+
+// use quicker sourcemap option
+webpackConfig.devtool = 'cheap-inline-source-map';
const specFilters = argumentsParser
.option(
@@ -77,9 +75,6 @@ if (specFilters.length) {
);
}
-webpackConfig.entry = undefined;
-webpackConfig.devtool = 'cheap-inline-source-map';
-
// Karma configuration
module.exports = function(config) {
process.env.TZ = 'Etc/UTC';
diff --git a/config/prometheus/additional_metrics.yml b/config/prometheus/additional_metrics.yml
index 10ca612b246..13732384953 100644
--- a/config/prometheus/additional_metrics.yml
+++ b/config/prometheus/additional_metrics.yml
@@ -103,10 +103,10 @@
- title: "Throughput"
y_label: "Requests / Sec"
required_metrics:
- - nginx_responses_total
+ - nginx_server_requests
weight: 1
queries:
- - query_range: 'sum(rate(nginx_responses_total{server_zone!="*", server_zone!="_", %{environment_filter}}[2m])) by (status_code)'
+ - query_range: 'sum(rate(nginx_server_requests{server_zone!="*", server_zone!="_", %{environment_filter}}[2m])) by (code)'
unit: req / sec
label: Status Code
series:
@@ -121,19 +121,19 @@
- title: "Latency"
y_label: "Latency (ms)"
required_metrics:
- - nginx_upstream_response_msecs_avg
+ - nginx_server_requestMsec
weight: 1
queries:
- - query_range: 'avg(nginx_upstream_response_msecs_avg{%{environment_filter}})'
+ - query_range: 'avg(nginx_server_requestMsec{%{environment_filter}})'
label: Upstream
unit: ms
- title: "HTTP Error Rate"
y_label: "HTTP 500 Errors / Sec"
required_metrics:
- - nginx_responses_total
+ - nginx_server_requests
weight: 1
queries:
- - query_range: 'sum(rate(nginx_responses_total{status_code="5xx", %{environment_filter}}[2m]))'
+ - query_range: 'sum(rate(nginx_server_requests{code="5xx", %{environment_filter}}[2m]))'
label: HTTP Errors
unit: "errors / sec"
- group: System metrics (Kubernetes)
diff --git a/config/routes/group.rb b/config/routes/group.rb
index 170508e893d..7c4c3d370e0 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -58,6 +58,13 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
# On CE only index and show actions are needed
resources :boards, only: [:index, :show]
+
+ resources :runners, only: [:index, :edit, :update, :destroy, :show] do
+ member do
+ post :resume
+ post :pause
+ end
+ end
end
scope(path: '*id',
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 7fffd16f3cf..5a1be1a8b73 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -174,6 +174,12 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
+ resource :mirror, only: [:show, :update] do
+ member do
+ post :update_now
+ end
+ end
+
resources :pipelines, only: [:index, :new, :create, :show] do
collection do
resource :pipelines_settings, path: 'settings', only: [:show, :update]
@@ -182,6 +188,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
member do
get :stage
+ get :stage_ajax
post :cancel
post :retry
get :builds
diff --git a/config/routes/repository.rb b/config/routes/repository.rb
index 9e506a1a43a..e2bf8d6a7ff 100644
--- a/config/routes/repository.rb
+++ b/config/routes/repository.rb
@@ -18,6 +18,7 @@ scope format: false do
resources :compare, only: [:index, :create] do
collection do
get :diff_for_path
+ get :signatures
end
end
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 47fbbed44cf..e1e8f36b663 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -73,3 +73,6 @@
- [object_storage, 1]
- [plugin, 1]
- [pipeline_background, 1]
+ - [repository_update_remote_mirror, 1]
+ - [repository_remove_remote, 1]
+
diff --git a/config/webpack.config.js b/config/webpack.config.js
index b9d098ff9b9..5096f35e808 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -1,4 +1,3 @@
-const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const glob = require('glob');
@@ -6,9 +5,7 @@ const webpack = require('webpack');
const StatsWriterPlugin = require('webpack-stats-plugin').StatsWriterPlugin;
const CopyWebpackPlugin = require('copy-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
-const NameAllModulesPlugin = require('name-all-modules-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
-const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
const ROOT_PATH = path.resolve(__dirname, '..');
const IS_PRODUCTION = process.env.NODE_ENV === 'production';
@@ -21,10 +18,12 @@ const NO_COMPRESSION = process.env.NO_COMPRESSION;
let autoEntriesCount = 0;
let watchAutoEntries = [];
+const defaultEntries = ['./main'];
function generateEntries() {
// generate automatic entry points
const autoEntries = {};
+ const autoEntriesMap = {};
const pageEntries = glob.sync('pages/**/index.js', {
cwd: path.join(ROOT_PATH, 'app/assets/javascripts'),
});
@@ -33,25 +32,38 @@ function generateEntries() {
function generateAutoEntries(path, prefix = '.') {
const chunkPath = path.replace(/\/index\.js$/, '');
const chunkName = chunkPath.replace(/\//g, '.');
- autoEntries[chunkName] = `${prefix}/${path}`;
+ autoEntriesMap[chunkName] = `${prefix}/${path}`;
}
pageEntries.forEach(path => generateAutoEntries(path));
- autoEntriesCount = Object.keys(autoEntries).length;
+ const autoEntryKeys = Object.keys(autoEntriesMap);
+ autoEntriesCount = autoEntryKeys.length;
+
+ // import ancestor entrypoints within their children
+ autoEntryKeys.forEach(entry => {
+ const entryPaths = [autoEntriesMap[entry]];
+ const segments = entry.split('.');
+ while (segments.pop()) {
+ const ancestor = segments.join('.');
+ if (autoEntryKeys.includes(ancestor)) {
+ entryPaths.unshift(autoEntriesMap[ancestor]);
+ }
+ }
+ autoEntries[entry] = defaultEntries.concat(entryPaths);
+ });
const manualEntries = {
- common: './commons/index.js',
- main: './main.js',
+ default: defaultEntries,
raven: './raven/index.js',
- webpack_runtime: './webpack.js',
- ide: './ide/index.js',
};
return Object.assign(manualEntries, autoEntries);
}
const config = {
+ mode: IS_PRODUCTION ? 'production' : 'development',
+
context: path.join(ROOT_PATH, 'app/assets/javascripts'),
entry: generateEntries,
@@ -59,8 +71,36 @@ const config = {
output: {
path: path.join(ROOT_PATH, 'public/assets/webpack'),
publicPath: '/assets/webpack/',
- filename: IS_PRODUCTION ? '[name].[chunkhash].bundle.js' : '[name].bundle.js',
- chunkFilename: IS_PRODUCTION ? '[name].[chunkhash].chunk.js' : '[name].chunk.js',
+ filename: IS_PRODUCTION ? '[name].[chunkhash:8].bundle.js' : '[name].bundle.js',
+ chunkFilename: IS_PRODUCTION ? '[name].[chunkhash:8].chunk.js' : '[name].chunk.js',
+ 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,
+ },
+ },
+ },
},
module: {
@@ -92,10 +132,10 @@ const config = {
{
loader: 'worker-loader',
options: {
- inline: true,
+ name: '[name].[hash:8].worker.js',
},
},
- { loader: 'babel-loader' },
+ 'babel-loader',
],
},
{
@@ -103,7 +143,7 @@ const config = {
exclude: /node_modules/,
loader: 'file-loader',
options: {
- name: '[name].[hash].[ext]',
+ name: '[name].[hash:8].[ext]',
},
},
{
@@ -114,7 +154,7 @@ const config = {
{
loader: 'css-loader',
options: {
- name: '[name].[hash].[ext]',
+ name: '[name].[hash:8].[ext]',
},
},
],
@@ -124,7 +164,7 @@ const config = {
include: /node_modules\/katex\/dist\/fonts/,
loader: 'file-loader',
options: {
- name: '[name].[hash].[ext]',
+ name: '[name].[hash:8].[ext]',
},
},
{
@@ -166,54 +206,6 @@ const config = {
jQuery: 'jquery',
}),
- // assign deterministic module ids
- new webpack.NamedModulesPlugin(),
- new NameAllModulesPlugin(),
-
- // assign deterministic chunk ids
- new webpack.NamedChunksPlugin(chunk => {
- if (chunk.name) {
- return chunk.name;
- }
-
- const moduleNames = [];
-
- function collectModuleNames(m) {
- // handle ConcatenatedModule which does not have resource nor context set
- if (m.modules) {
- m.modules.forEach(collectModuleNames);
- return;
- }
-
- const pagesBase = path.join(ROOT_PATH, 'app/assets/javascripts/pages');
-
- if (m.resource.indexOf(pagesBase) === 0) {
- moduleNames.push(
- path
- .relative(pagesBase, m.resource)
- .replace(/\/index\.[a-z]+$/, '')
- .replace(/\//g, '__')
- );
- } else {
- moduleNames.push(path.relative(m.context, m.resource));
- }
- }
-
- chunk.forEachModule(collectModuleNames);
-
- const hash = crypto
- .createHash('sha256')
- .update(moduleNames.join('_'))
- .digest('hex');
-
- return `${moduleNames[0]}-${hash.substr(0, 6)}`;
- }),
-
- // create cacheable common library bundles
- new webpack.optimize.CommonsChunkPlugin({
- names: ['main', 'common', 'webpack_runtime'],
- }),
-
// copy pre-compiled vendor libraries verbatim
new CopyWebpackPlugin([
{
@@ -260,20 +252,6 @@ const config = {
if (IS_PRODUCTION) {
config.devtool = 'source-map';
- config.plugins.push(
- new webpack.NoEmitOnErrorsPlugin(),
- new webpack.LoaderOptionsPlugin({
- minimize: true,
- debug: false,
- }),
- new webpack.optimize.ModuleConcatenationPlugin(),
- new webpack.optimize.UglifyJsPlugin({
- sourceMap: true,
- }),
- new webpack.DefinePlugin({
- 'process.env': { NODE_ENV: JSON.stringify('production') },
- })
- );
// compression can require a lot of compute time and is disabled in CI
if (!NO_COMPRESSION) {
@@ -292,29 +270,30 @@ if (IS_DEV_SERVER) {
hot: DEV_SERVER_LIVERELOAD,
inline: DEV_SERVER_LIVERELOAD,
};
- config.plugins.push(
- // watch node_modules for changes if we encounter a missing module compile error
- new WatchMissingNodeModulesPlugin(path.join(ROOT_PATH, 'node_modules')),
-
- // watch for changes to our automatic entry point modules
- {
- apply(compiler) {
- compiler.plugin('emit', (compilation, callback) => {
- compilation.contextDependencies = [
- ...compilation.contextDependencies,
- ...watchAutoEntries,
- ];
-
- // report our auto-generated bundle count
- console.log(
- `${autoEntriesCount} entries from '/pages' automatically added to webpack output.`
- );
-
- callback();
- });
- },
- }
- );
+ 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());
}
diff --git a/db/migrate/20121220064453_init_schema.rb b/db/migrate/20121220064453_init_schema.rb
deleted file mode 100644
index f93dc92b70f..00000000000
--- a/db/migrate/20121220064453_init_schema.rb
+++ /dev/null
@@ -1,307 +0,0 @@
-# rubocop:disable all
-class InitSchema < ActiveRecord::Migration
- def up
-
- create_table "events", force: true do |t|
- t.string "target_type"
- t.integer "target_id"
- t.string "title"
- t.text "data"
- t.integer "project_id"
- t.datetime "created_at"
- t.datetime "updated_at"
- t.integer "action"
- t.integer "author_id"
- end
-
- add_index "events", ["action"], name: "index_events_on_action", using: :btree
- add_index "events", ["author_id"], name: "index_events_on_author_id", using: :btree
- add_index "events", ["created_at"], name: "index_events_on_created_at", using: :btree
- add_index "events", ["project_id"], name: "index_events_on_project_id", using: :btree
- add_index "events", ["target_id"], name: "index_events_on_target_id", using: :btree
- add_index "events", ["target_type"], name: "index_events_on_target_type", using: :btree
-
- create_table "issues", force: true do |t|
- t.string "title"
- t.integer "assignee_id"
- t.integer "author_id"
- t.integer "project_id"
- t.datetime "created_at"
- t.datetime "updated_at"
- t.boolean "closed", default: false, null: false
- t.integer "position", default: 0
- t.string "branch_name"
- t.text "description"
- t.integer "milestone_id"
- end
-
- add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
- add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree
- add_index "issues", ["closed"], name: "index_issues_on_closed", using: :btree
- add_index "issues", ["created_at"], name: "index_issues_on_created_at", using: :btree
- add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree
- add_index "issues", ["project_id"], name: "index_issues_on_project_id", using: :btree
- add_index "issues", ["title"], name: "index_issues_on_title", using: :btree
-
- create_table "keys", force: true do |t|
- t.integer "user_id"
- t.datetime "created_at"
- t.datetime "updated_at"
- t.text "key"
- t.string "title"
- t.string "identifier"
- t.integer "project_id"
- end
-
- add_index "keys", ["identifier"], name: "index_keys_on_identifier", using: :btree
- add_index "keys", ["project_id"], name: "index_keys_on_project_id", using: :btree
- add_index "keys", ["user_id"], name: "index_keys_on_user_id", using: :btree
-
- create_table "merge_requests", force: true do |t|
- t.string "target_branch", null: false
- t.string "source_branch", null: false
- t.integer "project_id", null: false
- t.integer "author_id"
- t.integer "assignee_id"
- t.string "title"
- t.boolean "closed", default: false, null: false
- t.datetime "created_at"
- t.datetime "updated_at"
- t.text "st_commits"
- t.text "st_diffs"
- t.boolean "merged", default: false, null: false
- t.integer "state", default: 1, null: false
- t.integer "milestone_id"
- end
-
- add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
- add_index "merge_requests", ["author_id"], name: "index_merge_requests_on_author_id", using: :btree
- add_index "merge_requests", ["closed"], name: "index_merge_requests_on_closed", using: :btree
- add_index "merge_requests", ["created_at"], name: "index_merge_requests_on_created_at", using: :btree
- add_index "merge_requests", ["milestone_id"], name: "index_merge_requests_on_milestone_id", using: :btree
- add_index "merge_requests", ["project_id"], name: "index_merge_requests_on_project_id", using: :btree
- add_index "merge_requests", ["source_branch"], name: "index_merge_requests_on_source_branch", using: :btree
- add_index "merge_requests", ["target_branch"], name: "index_merge_requests_on_target_branch", using: :btree
- add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree
-
- create_table "milestones", force: true do |t|
- t.string "title", null: false
- t.integer "project_id", null: false
- t.text "description"
- t.date "due_date"
- t.boolean "closed", default: false, null: false
- t.datetime "created_at"
- t.datetime "updated_at"
- end
-
- add_index "milestones", ["due_date"], name: "index_milestones_on_due_date", using: :btree
- add_index "milestones", ["project_id"], name: "index_milestones_on_project_id", using: :btree
-
- create_table "namespaces", force: true do |t|
- t.string "name", null: false
- t.string "path", null: false
- t.integer "owner_id", null: false
- t.datetime "created_at"
- t.datetime "updated_at"
- t.string "type"
- end
-
- add_index "namespaces", ["name"], name: "index_namespaces_on_name", using: :btree
- add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree
- add_index "namespaces", ["path"], name: "index_namespaces_on_path", using: :btree
- add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree
-
- create_table "notes", force: true do |t|
- t.text "note"
- t.string "noteable_type"
- t.integer "author_id"
- t.datetime "created_at"
- t.datetime "updated_at"
- t.integer "project_id"
- t.string "attachment"
- t.string "line_code"
- t.string "commit_id"
- t.integer "noteable_id"
- end
-
- add_index "notes", ["commit_id"], name: "index_notes_on_commit_id", using: :btree
- add_index "notes", ["created_at"], name: "index_notes_on_created_at", using: :btree
- add_index "notes", ["noteable_type"], name: "index_notes_on_noteable_type", using: :btree
- add_index "notes", ["project_id", "noteable_type"], name: "index_notes_on_project_id_and_noteable_type", using: :btree
- add_index "notes", ["project_id"], name: "index_notes_on_project_id", using: :btree
-
- create_table "projects", force: true do |t|
- t.string "name"
- t.string "path"
- t.text "description"
- t.datetime "created_at"
- t.datetime "updated_at"
- t.boolean "private_flag", default: true, null: false
- t.integer "owner_id"
- t.string "default_branch"
- t.boolean "issues_enabled", default: true, null: false
- t.boolean "wall_enabled", default: true, null: false
- t.boolean "merge_requests_enabled", default: true, null: false
- t.boolean "wiki_enabled", default: true, null: false
- t.integer "namespace_id"
- end
-
- add_index "projects", ["namespace_id"], name: "index_projects_on_namespace_id", using: :btree
- add_index "projects", ["owner_id"], name: "index_projects_on_owner_id", using: :btree
-
- create_table "protected_branches", force: true do |t|
- t.integer "project_id", null: false
- t.string "name", null: false
- t.datetime "created_at"
- t.datetime "updated_at"
- end
-
- create_table "services", force: true do |t|
- t.string "type"
- t.string "title"
- t.string "token"
- t.integer "project_id", null: false
- t.datetime "created_at"
- t.datetime "updated_at"
- t.boolean "active", default: false, null: false
- t.string "project_url"
- end
-
- add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree
-
- create_table "snippets", force: true do |t|
- t.string "title"
- t.text "content"
- t.integer "author_id", null: false
- t.integer "project_id", null: false
- t.datetime "created_at"
- t.datetime "updated_at"
- t.string "file_name"
- t.datetime "expires_at"
- end
-
- add_index "snippets", ["created_at"], name: "index_snippets_on_created_at", using: :btree
- add_index "snippets", ["expires_at"], name: "index_snippets_on_expires_at", using: :btree
- add_index "snippets", ["project_id"], name: "index_snippets_on_project_id", using: :btree
-
- create_table "taggings", force: true do |t|
- t.integer "tag_id"
- t.integer "taggable_id"
- t.string "taggable_type"
- t.integer "tagger_id"
- t.string "tagger_type"
- t.string "context"
- t.datetime "created_at"
- end
-
- add_index "taggings", ["tag_id"], name: "index_taggings_on_tag_id", using: :btree
- add_index "taggings", ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree
-
- create_table "tags", force: true do |t|
- t.string "name"
- end
-
- create_table "user_team_project_relationships", force: true do |t|
- t.integer "project_id"
- t.integer "user_team_id"
- t.integer "greatest_access"
- t.datetime "created_at"
- t.datetime "updated_at"
- end
-
- create_table "user_team_user_relationships", force: true do |t|
- t.integer "user_id"
- t.integer "user_team_id"
- t.boolean "group_admin"
- t.integer "permission"
- t.datetime "created_at"
- t.datetime "updated_at"
- end
-
- create_table "user_teams", force: true do |t|
- t.string "name"
- t.string "path"
- t.integer "owner_id"
- t.datetime "created_at"
- t.datetime "updated_at"
- end
-
- create_table "users", force: true do |t|
- t.string "email", default: "", null: false
- t.string "encrypted_password", default: "", null: false
- t.string "reset_password_token"
- t.datetime "reset_password_sent_at"
- t.datetime "remember_created_at"
- t.integer "sign_in_count", default: 0
- t.datetime "current_sign_in_at"
- t.datetime "last_sign_in_at"
- t.string "current_sign_in_ip"
- t.string "last_sign_in_ip"
- t.datetime "created_at"
- t.datetime "updated_at"
- t.string "name"
- t.boolean "admin", default: false, null: false
- t.integer "projects_limit", default: 10
- t.string "skype", default: "", null: false
- t.string "linkedin", default: "", null: false
- t.string "twitter", default: "", null: false
- t.string "authentication_token"
- t.boolean "dark_scheme", default: false, null: false
- t.integer "theme_id", default: 1, null: false
- t.string "bio"
- t.boolean "blocked", default: false, null: false
- t.integer "failed_attempts", default: 0
- t.datetime "locked_at"
- t.string "extern_uid"
- t.string "provider"
- t.string "username"
- end
-
- add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
- add_index "users", ["blocked"], name: "index_users_on_blocked", using: :btree
- add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
- add_index "users", ["extern_uid", "provider"], name: "index_users_on_extern_uid_and_provider", unique: true, using: :btree
- add_index "users", ["name"], name: "index_users_on_name", using: :btree
- add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
- add_index "users", ["username"], name: "index_users_on_username", using: :btree
-
- create_table "users_projects", force: true do |t|
- t.integer "user_id", null: false
- t.integer "project_id", null: false
- t.datetime "created_at"
- t.datetime "updated_at"
- t.integer "project_access", default: 0, null: false
- end
-
- add_index "users_projects", ["project_access"], name: "index_users_projects_on_project_access", using: :btree
- add_index "users_projects", ["project_id"], name: "index_users_projects_on_project_id", using: :btree
- add_index "users_projects", ["user_id"], name: "index_users_projects_on_user_id", using: :btree
-
- create_table "web_hooks", force: true do |t|
- t.string "url"
- t.integer "project_id"
- t.datetime "created_at"
- t.datetime "updated_at"
- t.string "type", default: "ProjectHook"
- t.integer "service_id"
- end
-
- create_table "wikis", force: true do |t|
- t.string "title"
- t.text "content"
- t.integer "project_id"
- t.datetime "created_at"
- t.datetime "updated_at"
- t.string "slug"
- t.integer "user_id"
- end
-
- add_index "wikis", ["project_id"], name: "index_wikis_on_project_id", using: :btree
- add_index "wikis", ["slug"], name: "index_wikis_on_slug", using: :btree
-
- end
-
- def down
- raise "Can not revert initial migration"
- end
-end
diff --git a/db/migrate/20130102143055_rename_owner_to_creator_for_project.rb b/db/migrate/20130102143055_rename_owner_to_creator_for_project.rb
deleted file mode 100644
index 84fd2060770..00000000000
--- a/db/migrate/20130102143055_rename_owner_to_creator_for_project.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class RenameOwnerToCreatorForProject < ActiveRecord::Migration
- def change
- rename_column :projects, :owner_id, :creator_id
- end
-end
diff --git a/db/migrate/20130110172407_add_public_to_project.rb b/db/migrate/20130110172407_add_public_to_project.rb
deleted file mode 100644
index 4362aadcc1d..00000000000
--- a/db/migrate/20130110172407_add_public_to_project.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddPublicToProject < ActiveRecord::Migration
- def change
- add_column :projects, :public, :boolean, default: false, null: false
- end
-end
diff --git a/db/migrate/20130123114545_add_issues_tracker_to_project.rb b/db/migrate/20130123114545_add_issues_tracker_to_project.rb
deleted file mode 100644
index ba8c50b53e2..00000000000
--- a/db/migrate/20130123114545_add_issues_tracker_to_project.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddIssuesTrackerToProject < ActiveRecord::Migration
- def change
- add_column :projects, :issues_tracker, :string, default: :gitlab, null: false
- end
-end
diff --git a/db/migrate/20130125090214_add_user_permissions.rb b/db/migrate/20130125090214_add_user_permissions.rb
deleted file mode 100644
index 1350eadb60e..00000000000
--- a/db/migrate/20130125090214_add_user_permissions.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# rubocop:disable all
-class AddUserPermissions < ActiveRecord::Migration
- def up
- add_column :users, :can_create_group, :boolean, default: true, null: false
- add_column :users, :can_create_team, :boolean, default: true, null: false
- end
-
- def down
- remove_column :users, :can_create_group
- remove_column :users, :can_create_team
- end
-end
diff --git a/db/migrate/20130131070232_remove_private_flag_from_project.rb b/db/migrate/20130131070232_remove_private_flag_from_project.rb
deleted file mode 100644
index f0273ba448e..00000000000
--- a/db/migrate/20130131070232_remove_private_flag_from_project.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# rubocop:disable all
-class RemovePrivateFlagFromProject < ActiveRecord::Migration
- def up
- remove_column :projects, :private_flag
- end
-
- def down
- add_column :projects, :private_flag, :boolean, default: true, null: false
- end
-end
diff --git a/db/migrate/20130206084024_add_description_to_namsespace.rb b/db/migrate/20130206084024_add_description_to_namsespace.rb
deleted file mode 100644
index 62676ce8914..00000000000
--- a/db/migrate/20130206084024_add_description_to_namsespace.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddDescriptionToNamsespace < ActiveRecord::Migration
- def change
- add_column :namespaces, :description, :string, default: '', null: false
- end
-end
diff --git a/db/migrate/20130207104426_add_description_to_teams.rb b/db/migrate/20130207104426_add_description_to_teams.rb
deleted file mode 100644
index bd9a4767b69..00000000000
--- a/db/migrate/20130207104426_add_description_to_teams.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddDescriptionToTeams < ActiveRecord::Migration
- def change
- add_column :user_teams, :description, :string, default: '', null: false
- end
-end
diff --git a/db/migrate/20130211085435_add_issues_tracker_id_to_project.rb b/db/migrate/20130211085435_add_issues_tracker_id_to_project.rb
deleted file mode 100644
index 56b01cbf892..00000000000
--- a/db/migrate/20130211085435_add_issues_tracker_id_to_project.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddIssuesTrackerIdToProject < ActiveRecord::Migration
- def change
- add_column :projects, :issues_tracker_id, :string
- end
-end
diff --git a/db/migrate/20130214154045_rename_state_to_merge_status_in_milestone.rb b/db/migrate/20130214154045_rename_state_to_merge_status_in_milestone.rb
deleted file mode 100644
index 4722cc13d4b..00000000000
--- a/db/migrate/20130214154045_rename_state_to_merge_status_in_milestone.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class RenameStateToMergeStatusInMilestone < ActiveRecord::Migration
- def change
- rename_column :merge_requests, :state, :merge_status
- end
-end
diff --git a/db/migrate/20130218140952_add_state_to_issue.rb b/db/migrate/20130218140952_add_state_to_issue.rb
deleted file mode 100644
index 3a5e978a182..00000000000
--- a/db/migrate/20130218140952_add_state_to_issue.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddStateToIssue < ActiveRecord::Migration
- def change
- add_column :issues, :state, :string
- end
-end
diff --git a/db/migrate/20130218141038_add_state_to_merge_request.rb b/db/migrate/20130218141038_add_state_to_merge_request.rb
deleted file mode 100644
index e0180c755e2..00000000000
--- a/db/migrate/20130218141038_add_state_to_merge_request.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddStateToMergeRequest < ActiveRecord::Migration
- def change
- add_column :merge_requests, :state, :string
- end
-end
diff --git a/db/migrate/20130218141117_add_state_to_milestone.rb b/db/migrate/20130218141117_add_state_to_milestone.rb
deleted file mode 100644
index 5f71608692c..00000000000
--- a/db/migrate/20130218141117_add_state_to_milestone.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddStateToMilestone < ActiveRecord::Migration
- def change
- add_column :milestones, :state, :string
- end
-end
diff --git a/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb b/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb
deleted file mode 100644
index 67a0d3b53eb..00000000000
--- a/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# rubocop:disable all
-class ConvertClosedToStateInIssue < ActiveRecord::Migration
- include Gitlab::Database::MigrationHelpers
-
- def up
- execute "UPDATE #{table_name} SET state = 'closed' WHERE closed = #{true_value}"
- execute "UPDATE #{table_name} SET state = 'opened' WHERE closed = #{false_value}"
- end
-
- def down
- execute "UPDATE #{table_name} SET closed = #{true_value} WHERE state = 'closed'"
- end
-
- private
-
- def table_name
- Issue.table_name
- end
-end
diff --git a/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb b/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb
deleted file mode 100644
index 307fc6a023d..00000000000
--- a/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# rubocop:disable all
-class ConvertClosedToStateInMergeRequest < ActiveRecord::Migration
- include Gitlab::Database::MigrationHelpers
-
- def up
- execute "UPDATE #{table_name} SET state = 'merged' WHERE closed = #{true_value} AND merged = #{true_value}"
- execute "UPDATE #{table_name} SET state = 'closed' WHERE closed = #{true_value} AND merged = #{false_value}"
- execute "UPDATE #{table_name} SET state = 'opened' WHERE closed = #{false_value}"
- end
-
- def down
- execute "UPDATE #{table_name} SET closed = #{true_value} WHERE state = 'closed'"
- execute "UPDATE #{table_name} SET closed = #{true_value}, merged = #{true_value} WHERE state = 'merged'"
- end
-
- private
-
- def table_name
- MergeRequest.table_name
- end
-end
diff --git a/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb b/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb
deleted file mode 100644
index d12703cf3b2..00000000000
--- a/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# rubocop:disable all
-class ConvertClosedToStateInMilestone < ActiveRecord::Migration
- include Gitlab::Database::MigrationHelpers
-
- def up
- execute "UPDATE #{table_name} SET state = 'closed' WHERE closed = #{true_value}"
- execute "UPDATE #{table_name} SET state = 'active' WHERE closed = #{false_value}"
- end
-
- def down
- execute "UPDATE #{table_name} SET closed = #{true_value} WHERE state = 'cloesd'"
- end
-
- private
-
- def table_name
- Milestone.table_name
- end
-end
diff --git a/db/migrate/20130218141444_remove_merged_from_merge_request.rb b/db/migrate/20130218141444_remove_merged_from_merge_request.rb
deleted file mode 100644
index afa5137061e..00000000000
--- a/db/migrate/20130218141444_remove_merged_from_merge_request.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# rubocop:disable all
-class RemoveMergedFromMergeRequest < ActiveRecord::Migration
- def up
- remove_column :merge_requests, :merged
- end
-
- def down
- add_column :merge_requests, :merged, :boolean, default: true, null: false
- end
-end
diff --git a/db/migrate/20130218141507_remove_closed_from_issue.rb b/db/migrate/20130218141507_remove_closed_from_issue.rb
deleted file mode 100644
index f250288bc3b..00000000000
--- a/db/migrate/20130218141507_remove_closed_from_issue.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# rubocop:disable all
-class RemoveClosedFromIssue < ActiveRecord::Migration
- def up
- remove_column :issues, :closed
- end
-
- def down
- add_column :issues, :closed, :boolean
- end
-end
diff --git a/db/migrate/20130218141536_remove_closed_from_merge_request.rb b/db/migrate/20130218141536_remove_closed_from_merge_request.rb
deleted file mode 100644
index efa12e32636..00000000000
--- a/db/migrate/20130218141536_remove_closed_from_merge_request.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# rubocop:disable all
-class RemoveClosedFromMergeRequest < ActiveRecord::Migration
- def up
- remove_column :merge_requests, :closed
- end
-
- def down
- add_column :merge_requests, :closed, :boolean
- end
-end
diff --git a/db/migrate/20130218141554_remove_closed_from_milestone.rb b/db/migrate/20130218141554_remove_closed_from_milestone.rb
deleted file mode 100644
index 75ac14e43be..00000000000
--- a/db/migrate/20130218141554_remove_closed_from_milestone.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# rubocop:disable all
-class RemoveClosedFromMilestone < ActiveRecord::Migration
- def up
- remove_column :milestones, :closed
- end
-
- def down
- add_column :milestones, :closed, :boolean
- end
-end
diff --git a/db/migrate/20130220124204_add_new_merge_status_to_merge_request.rb b/db/migrate/20130220124204_add_new_merge_status_to_merge_request.rb
deleted file mode 100644
index 97615e47c89..00000000000
--- a/db/migrate/20130220124204_add_new_merge_status_to_merge_request.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddNewMergeStatusToMergeRequest < ActiveRecord::Migration
- def change
- add_column :merge_requests, :new_merge_status, :string
- end
-end
diff --git a/db/migrate/20130220125544_convert_merge_status_in_merge_request.rb b/db/migrate/20130220125544_convert_merge_status_in_merge_request.rb
deleted file mode 100644
index 3b8c3686c55..00000000000
--- a/db/migrate/20130220125544_convert_merge_status_in_merge_request.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# rubocop:disable all
-class ConvertMergeStatusInMergeRequest < ActiveRecord::Migration
- def up
- execute "UPDATE #{table_name} SET new_merge_status = 'unchecked' WHERE merge_status = 1"
- execute "UPDATE #{table_name} SET new_merge_status = 'can_be_merged' WHERE merge_status = 2"
- execute "UPDATE #{table_name} SET new_merge_status = 'cannot_be_merged' WHERE merge_status = 3"
- end
-
- def down
- execute "UPDATE #{table_name} SET merge_status = 1 WHERE new_merge_status = 'unchecked'"
- execute "UPDATE #{table_name} SET merge_status = 2 WHERE new_merge_status = 'can_be_merged'"
- execute "UPDATE #{table_name} SET merge_status = 3 WHERE new_merge_status = 'cannot_be_merged'"
- end
-
- private
-
- def table_name
- MergeRequest.table_name
- end
-end
diff --git a/db/migrate/20130220125545_remove_merge_status_from_merge_request.rb b/db/migrate/20130220125545_remove_merge_status_from_merge_request.rb
deleted file mode 100644
index bd25ffbfc99..00000000000
--- a/db/migrate/20130220125545_remove_merge_status_from_merge_request.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# rubocop:disable all
-class RemoveMergeStatusFromMergeRequest < ActiveRecord::Migration
- def up
- remove_column :merge_requests, :merge_status
- end
-
- def down
- add_column :merge_requests, :merge_status, :integer
- end
-end
diff --git a/db/migrate/20130220133245_rename_new_merge_status_to_merge_status_in_milestone.rb b/db/migrate/20130220133245_rename_new_merge_status_to_merge_status_in_milestone.rb
deleted file mode 100644
index f0595720a39..00000000000
--- a/db/migrate/20130220133245_rename_new_merge_status_to_merge_status_in_milestone.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class RenameNewMergeStatusToMergeStatusInMilestone < ActiveRecord::Migration
- def change
- rename_column :merge_requests, :new_merge_status, :merge_status
- end
-end
diff --git a/db/migrate/20130304104623_add_state_to_user.rb b/db/migrate/20130304104623_add_state_to_user.rb
deleted file mode 100644
index 4456d022e3f..00000000000
--- a/db/migrate/20130304104623_add_state_to_user.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddStateToUser < ActiveRecord::Migration
- def change
- add_column :users, :state, :string
- end
-end
diff --git a/db/migrate/20130304104740_convert_blocked_to_state.rb b/db/migrate/20130304104740_convert_blocked_to_state.rb
deleted file mode 100644
index 9afd1093645..00000000000
--- a/db/migrate/20130304104740_convert_blocked_to_state.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# rubocop:disable all
-class ConvertBlockedToState < ActiveRecord::Migration
- def up
- User.transaction do
- User.where(blocked: true).update_all(state: :blocked)
- User.where(blocked: false).update_all(state: :active)
- end
- end
-
- def down
- User.transaction do
- User.where(state: :blocked).update_all(blocked: :true)
- end
- end
-end
diff --git a/db/migrate/20130304105317_remove_blocked_from_user.rb b/db/migrate/20130304105317_remove_blocked_from_user.rb
deleted file mode 100644
index 8f5b2c59b43..00000000000
--- a/db/migrate/20130304105317_remove_blocked_from_user.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# rubocop:disable all
-class RemoveBlockedFromUser < ActiveRecord::Migration
- def up
- remove_column :users, :blocked
- end
-
- def down
- add_column :users, :blocked, :boolean
- end
-end
diff --git a/db/migrate/20130315124931_user_color_scheme.rb b/db/migrate/20130315124931_user_color_scheme.rb
deleted file mode 100644
index 09af928fde7..00000000000
--- a/db/migrate/20130315124931_user_color_scheme.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# rubocop:disable all
-class UserColorScheme < ActiveRecord::Migration
- include Gitlab::Database::MigrationHelpers
-
- def up
- add_column :users, :color_scheme_id, :integer, null: false, default: 1
- execute("UPDATE users SET color_scheme_id = 2 WHERE dark_scheme = #{true_value}")
- remove_column :users, :dark_scheme
- end
-
- def down
- add_column :users, :dark_scheme, :boolean, null: false, default: false
- remove_column :users, :color_scheme_id
- end
-end
diff --git a/db/migrate/20130318212250_add_snippets_to_features.rb b/db/migrate/20130318212250_add_snippets_to_features.rb
deleted file mode 100644
index 9860b85f504..00000000000
--- a/db/migrate/20130318212250_add_snippets_to_features.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddSnippetsToFeatures < ActiveRecord::Migration
- def change
- add_column :projects, :snippets_enabled, :boolean, null: false, default: true
- end
-end
diff --git a/db/migrate/20130319214458_create_forked_project_links.rb b/db/migrate/20130319214458_create_forked_project_links.rb
deleted file mode 100644
index 065a5e08243..00000000000
--- a/db/migrate/20130319214458_create_forked_project_links.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# rubocop:disable all
-class CreateForkedProjectLinks < ActiveRecord::Migration
- DOWNTIME = false
-
- def change
- create_table :forked_project_links do |t|
- t.integer :forked_to_project_id, null: false
- t.integer :forked_from_project_id, null: false
-
- t.timestamps null: true
- end
- add_index :forked_project_links, :forked_to_project_id, unique: true
- end
-end
diff --git a/db/migrate/20130323174317_add_private_to_snippets.rb b/db/migrate/20130323174317_add_private_to_snippets.rb
deleted file mode 100644
index 376f4618d41..00000000000
--- a/db/migrate/20130323174317_add_private_to_snippets.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddPrivateToSnippets < ActiveRecord::Migration
- def change
- add_column :snippets, :private, :boolean, null: false, default: true
- end
-end
diff --git a/db/migrate/20130324151736_add_type_to_snippets.rb b/db/migrate/20130324151736_add_type_to_snippets.rb
deleted file mode 100644
index 097cb9bc7cb..00000000000
--- a/db/migrate/20130324151736_add_type_to_snippets.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddTypeToSnippets < ActiveRecord::Migration
- def change
- add_column :snippets, :type, :string
- end
-end
diff --git a/db/migrate/20130324172327_change_project_id_to_null_in_snipepts.rb b/db/migrate/20130324172327_change_project_id_to_null_in_snipepts.rb
deleted file mode 100644
index 9256e62086e..00000000000
--- a/db/migrate/20130324172327_change_project_id_to_null_in_snipepts.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# rubocop:disable all
-class ChangeProjectIdToNullInSnipepts < ActiveRecord::Migration
- def up
- change_column :snippets, :project_id, :integer, :null => true
- end
-
- def down
- change_column :snippets, :project_id, :integer, :null => false
- end
-end
diff --git a/db/migrate/20130324203535_add_type_value_for_snippets.rb b/db/migrate/20130324203535_add_type_value_for_snippets.rb
deleted file mode 100644
index 6e910fd74c7..00000000000
--- a/db/migrate/20130324203535_add_type_value_for_snippets.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# rubocop:disable all
-class AddTypeValueForSnippets < ActiveRecord::Migration
- def up
- Snippet.where("project_id IS NOT NULL").update_all(type: 'ProjectSnippet')
- end
-
- def down
- end
-end
diff --git a/db/migrate/20130325173941_add_notification_level_to_user.rb b/db/migrate/20130325173941_add_notification_level_to_user.rb
deleted file mode 100644
index 1dc58d4bcc8..00000000000
--- a/db/migrate/20130325173941_add_notification_level_to_user.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddNotificationLevelToUser < ActiveRecord::Migration
- def change
- add_column :users, :notification_level, :integer, null: false, default: 1
- end
-end
diff --git a/db/migrate/20130326142630_add_index_to_users_authentication_token.rb b/db/migrate/20130326142630_add_index_to_users_authentication_token.rb
deleted file mode 100644
index 0592181927e..00000000000
--- a/db/migrate/20130326142630_add_index_to_users_authentication_token.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddIndexToUsersAuthenticationToken < ActiveRecord::Migration
- def change
- add_index :users, :authentication_token, unique: true
- end
-end
diff --git a/db/migrate/20130403003950_add_last_activity_column_into_project.rb b/db/migrate/20130403003950_add_last_activity_column_into_project.rb
deleted file mode 100644
index 04a01612c6f..00000000000
--- a/db/migrate/20130403003950_add_last_activity_column_into_project.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# rubocop:disable all
-class AddLastActivityColumnIntoProject < ActiveRecord::Migration
- def up
- add_column :projects, :last_activity_at, :datetime
- add_index :projects, :last_activity_at
-
- select_all('SELECT id, updated_at FROM projects').each do |project|
- project_id = project['id']
- update_date = project['updated_at']
- event = select_one("SELECT created_at FROM events WHERE project_id = #{project_id} ORDER BY created_at DESC LIMIT 1")
-
- if event && event['created_at']
- update_date = event['created_at']
- end
-
- execute("UPDATE projects SET last_activity_at = '#{update_date}' WHERE id = #{project_id}")
- end
- end
-
- def down
- remove_index :projects, :last_activity_at
- remove_column :projects, :last_activity_at
- end
-end
diff --git a/db/migrate/20130404164628_add_notification_level_to_user_project.rb b/db/migrate/20130404164628_add_notification_level_to_user_project.rb
deleted file mode 100644
index 1e072d9c6e1..00000000000
--- a/db/migrate/20130404164628_add_notification_level_to_user_project.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddNotificationLevelToUserProject < ActiveRecord::Migration
- def change
- add_column :users_projects, :notification_level, :integer, null: false, default: 3
- end
-end
diff --git a/db/migrate/20130410175022_remove_wiki_table.rb b/db/migrate/20130410175022_remove_wiki_table.rb
deleted file mode 100644
index 5885b1cc375..00000000000
--- a/db/migrate/20130410175022_remove_wiki_table.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# rubocop:disable all
-class RemoveWikiTable < ActiveRecord::Migration
- def up
- drop_table :wikis
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20130419190306_allow_merges_for_forks.rb b/db/migrate/20130419190306_allow_merges_for_forks.rb
deleted file mode 100644
index ec953986c6a..00000000000
--- a/db/migrate/20130419190306_allow_merges_for_forks.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# rubocop:disable all
-class AllowMergesForForks < ActiveRecord::Migration
- def self.up
- add_column :merge_requests, :target_project_id, :integer, :null => true
- execute "UPDATE #{table_name} SET target_project_id = project_id"
- change_column :merge_requests, :target_project_id, :integer, :null => false
- rename_column :merge_requests, :project_id, :source_project_id
- end
-
- def self.down
- remove_column :merge_requests, :target_project_id
- rename_column :merge_requests, :source_project_id,:project_id
- end
-
- private
-
- def table_name
- MergeRequest.table_name
- end
-end
diff --git a/db/migrate/20130506085413_add_type_to_key.rb b/db/migrate/20130506085413_add_type_to_key.rb
deleted file mode 100644
index c9f1ee4e389..00000000000
--- a/db/migrate/20130506085413_add_type_to_key.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddTypeToKey < ActiveRecord::Migration
- def change
- add_column :keys, :type, :string
- end
-end
diff --git a/db/migrate/20130506090604_create_deploy_keys_projects.rb b/db/migrate/20130506090604_create_deploy_keys_projects.rb
deleted file mode 100644
index 8b9662a27c3..00000000000
--- a/db/migrate/20130506090604_create_deploy_keys_projects.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# rubocop:disable all
-class CreateDeployKeysProjects < ActiveRecord::Migration
- DOWNTIME = false
-
- def change
- create_table :deploy_keys_projects do |t|
- t.integer :deploy_key_id, null: false
- t.integer :project_id, null: false
-
- t.timestamps null: true
- end
- end
-end
diff --git a/db/migrate/20130506095501_remove_project_id_from_key.rb b/db/migrate/20130506095501_remove_project_id_from_key.rb
deleted file mode 100644
index 53abc4e7b52..00000000000
--- a/db/migrate/20130506095501_remove_project_id_from_key.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# rubocop:disable all
-class RemoveProjectIdFromKey < ActiveRecord::Migration
- def up
- puts 'Migrate deploy keys: '
- Key.where('project_id IS NOT NULL').update_all(type: 'DeployKey')
-
- DeployKey.all.each do |key|
- project = Project.find_by(id: key.project_id)
- if project
- project.deploy_keys << key
- print '.'
- end
- end
-
- puts 'Done'
-
- remove_column :keys, :project_id
- end
-
- def down
- add_column :keys, :project_id, :integer
- end
-end
diff --git a/db/migrate/20130522141856_add_more_fields_to_service.rb b/db/migrate/20130522141856_add_more_fields_to_service.rb
deleted file mode 100644
index 9f764a1d050..00000000000
--- a/db/migrate/20130522141856_add_more_fields_to_service.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# rubocop:disable all
-class AddMoreFieldsToService < ActiveRecord::Migration
- def change
- add_column :services, :subdomain, :string
- add_column :services, :room, :string
- end
-end
diff --git a/db/migrate/20130528184641_add_system_to_notes.rb b/db/migrate/20130528184641_add_system_to_notes.rb
deleted file mode 100644
index 27fbf8983ac..00000000000
--- a/db/migrate/20130528184641_add_system_to_notes.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# rubocop:disable all
-class AddSystemToNotes < ActiveRecord::Migration
- class Note < ActiveRecord::Base
- end
-
- def up
- add_column :notes, :system, :boolean, default: false, null: false
-
- Note.reset_column_information
- Note.update_all(system: false)
- Note.where("note like '_status changed to%'").update_all(system: true)
- end
-
- def down
- remove_column :notes, :system
- end
-end
diff --git a/db/migrate/20130611210815_increase_snippet_text_column_size.rb b/db/migrate/20130611210815_increase_snippet_text_column_size.rb
deleted file mode 100644
index f710c79a9a5..00000000000
--- a/db/migrate/20130611210815_increase_snippet_text_column_size.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# rubocop:disable all
-class IncreaseSnippetTextColumnSize < ActiveRecord::Migration
- def up
- # MYSQL LARGETEXT for snippet
- change_column :snippets, :content, :text, :limit => 4294967295
- end
-
- def down
- end
-end
diff --git a/db/migrate/20130613165816_add_password_expires_at_to_users.rb b/db/migrate/20130613165816_add_password_expires_at_to_users.rb
deleted file mode 100644
index 47306a370a8..00000000000
--- a/db/migrate/20130613165816_add_password_expires_at_to_users.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddPasswordExpiresAtToUsers < ActiveRecord::Migration
- def change
- add_column :users, :password_expires_at, :datetime
- end
-end
diff --git a/db/migrate/20130613173246_add_created_by_id_to_user.rb b/db/migrate/20130613173246_add_created_by_id_to_user.rb
deleted file mode 100644
index 3138c0f40a7..00000000000
--- a/db/migrate/20130613173246_add_created_by_id_to_user.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddCreatedByIdToUser < ActiveRecord::Migration
- def change
- add_column :users, :created_by_id, :integer
- end
-end
diff --git a/db/migrate/20130614132337_add_improted_to_project.rb b/db/migrate/20130614132337_add_improted_to_project.rb
deleted file mode 100644
index 26dc16e3b43..00000000000
--- a/db/migrate/20130614132337_add_improted_to_project.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddImprotedToProject < ActiveRecord::Migration
- def change
- add_column :projects, :imported, :boolean, default: false, null: false
- end
-end
diff --git a/db/migrate/20130617095603_create_users_groups.rb b/db/migrate/20130617095603_create_users_groups.rb
deleted file mode 100644
index 4ba7d0c9461..00000000000
--- a/db/migrate/20130617095603_create_users_groups.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# rubocop:disable all
-class CreateUsersGroups < ActiveRecord::Migration
- DOWNTIME = false
-
- def change
- create_table :users_groups do |t|
- t.integer :group_access, null: false
- t.integer :group_id, null: false
- t.integer :user_id, null: false
-
- t.timestamps null: true
- end
- end
-end
diff --git a/db/migrate/20130621195223_add_notification_level_to_user_group.rb b/db/migrate/20130621195223_add_notification_level_to_user_group.rb
deleted file mode 100644
index 6fd4941f615..00000000000
--- a/db/migrate/20130621195223_add_notification_level_to_user_group.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddNotificationLevelToUserGroup < ActiveRecord::Migration
- def change
- add_column :users_groups, :notification_level, :integer, null: false, default: 3
- end
-end
diff --git a/db/migrate/20130622115340_add_more_db_index.rb b/db/migrate/20130622115340_add_more_db_index.rb
deleted file mode 100644
index 4113217de59..00000000000
--- a/db/migrate/20130622115340_add_more_db_index.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# rubocop:disable all
-class AddMoreDbIndex < ActiveRecord::Migration
- def change
- add_index :deploy_keys_projects, :project_id
- add_index :web_hooks, :project_id
- add_index :protected_branches, :project_id
-
- add_index :users_groups, :user_id
- add_index :snippets, :author_id
- add_index :notes, :author_id
- add_index :notes, [:noteable_id, :noteable_type]
- end
-end
diff --git a/db/migrate/20130624162710_add_fingerprint_to_key.rb b/db/migrate/20130624162710_add_fingerprint_to_key.rb
deleted file mode 100644
index 3e574ea81b9..00000000000
--- a/db/migrate/20130624162710_add_fingerprint_to_key.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# rubocop:disable all
-class AddFingerprintToKey < ActiveRecord::Migration
- def change
- add_column :keys, :fingerprint, :string
- remove_column :keys, :identifier
- end
-end
diff --git a/db/migrate/20130711063759_create_project_group_links.rb b/db/migrate/20130711063759_create_project_group_links.rb
deleted file mode 100644
index efccb2aa938..00000000000
--- a/db/migrate/20130711063759_create_project_group_links.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# rubocop:disable all
-class CreateProjectGroupLinks < ActiveRecord::Migration
- DOWNTIME = false
-
- def change
- create_table :project_group_links do |t|
- t.integer :project_id, null: false
- t.integer :group_id, null: false
-
- t.timestamps null: true
- end
- end
-end
diff --git a/db/migrate/20130804151314_add_st_diff_to_note.rb b/db/migrate/20130804151314_add_st_diff_to_note.rb
deleted file mode 100644
index 9e2da73b695..00000000000
--- a/db/migrate/20130804151314_add_st_diff_to_note.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddStDiffToNote < ActiveRecord::Migration
- def change
- add_column :notes, :st_diff, :text, :null => true
- end
-end
diff --git a/db/migrate/20130809124851_add_permission_check_to_user.rb b/db/migrate/20130809124851_add_permission_check_to_user.rb
deleted file mode 100644
index 9f9dea36101..00000000000
--- a/db/migrate/20130809124851_add_permission_check_to_user.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddPermissionCheckToUser < ActiveRecord::Migration
- def change
- add_column :users, :last_credential_check_at, :datetime
- end
-end
diff --git a/db/migrate/20130812143708_add_import_url_to_project.rb b/db/migrate/20130812143708_add_import_url_to_project.rb
deleted file mode 100644
index d2bdfe1894e..00000000000
--- a/db/migrate/20130812143708_add_import_url_to_project.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddImportUrlToProject < ActiveRecord::Migration
- def change
- add_column :projects, :import_url, :string
- end
-end
diff --git a/db/migrate/20130819182730_add_internal_ids_to_issues_and_mr.rb b/db/migrate/20130819182730_add_internal_ids_to_issues_and_mr.rb
deleted file mode 100644
index 0e0e78b0f0d..00000000000
--- a/db/migrate/20130819182730_add_internal_ids_to_issues_and_mr.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# rubocop:disable all
-class AddInternalIdsToIssuesAndMr < ActiveRecord::Migration
- def change
- add_column :issues, :iid, :integer
- add_column :merge_requests, :iid, :integer
- end
-end
diff --git a/db/migrate/20130820102832_add_access_to_project_group_link.rb b/db/migrate/20130820102832_add_access_to_project_group_link.rb
deleted file mode 100644
index 98f3fa87523..00000000000
--- a/db/migrate/20130820102832_add_access_to_project_group_link.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddAccessToProjectGroupLink < ActiveRecord::Migration
- def change
- add_column :project_group_links, :group_access, :integer, null: false, default: ProjectGroupLink.default_access
- end
-end
diff --git a/db/migrate/20130821090530_remove_deprecated_tables.rb b/db/migrate/20130821090530_remove_deprecated_tables.rb
deleted file mode 100644
index d22e713a7a1..00000000000
--- a/db/migrate/20130821090530_remove_deprecated_tables.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# rubocop:disable all
-class RemoveDeprecatedTables < ActiveRecord::Migration
- def up
- drop_table :user_teams
- drop_table :user_team_project_relationships
- drop_table :user_team_user_relationships
- end
-
- def down
- raise 'No rollback for this migration'
- end
-end
diff --git a/db/migrate/20130821090531_add_internal_ids_to_milestones.rb b/db/migrate/20130821090531_add_internal_ids_to_milestones.rb
deleted file mode 100644
index e25b8f91662..00000000000
--- a/db/migrate/20130821090531_add_internal_ids_to_milestones.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddInternalIdsToMilestones < ActiveRecord::Migration
- def change
- add_column :milestones, :iid, :integer
- end
-end
diff --git a/db/migrate/20130909132950_add_description_to_merge_request.rb b/db/migrate/20130909132950_add_description_to_merge_request.rb
deleted file mode 100644
index fbac50c8216..00000000000
--- a/db/migrate/20130909132950_add_description_to_merge_request.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddDescriptionToMergeRequest < ActiveRecord::Migration
- def change
- add_column :merge_requests, :description, :text, null: true
- end
-end
diff --git a/db/migrate/20130926081215_change_owner_id_for_group.rb b/db/migrate/20130926081215_change_owner_id_for_group.rb
deleted file mode 100644
index 2bdd22d5a04..00000000000
--- a/db/migrate/20130926081215_change_owner_id_for_group.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# rubocop:disable all
-class ChangeOwnerIdForGroup < ActiveRecord::Migration
- def up
- change_column :namespaces, :owner_id, :integer, null: true
- end
-
- def down
- change_column :namespaces, :owner_id, :integer, null: false
- end
-end
diff --git a/db/migrate/20131005191208_add_avatar_to_users.rb b/db/migrate/20131005191208_add_avatar_to_users.rb
deleted file mode 100644
index df9057b81d6..00000000000
--- a/db/migrate/20131005191208_add_avatar_to_users.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddAvatarToUsers < ActiveRecord::Migration
- def change
- add_column :users, :avatar, :string
- end
-end
diff --git a/db/migrate/20131009115346_add_confirmable_to_users.rb b/db/migrate/20131009115346_add_confirmable_to_users.rb
deleted file mode 100644
index d714dd98e85..00000000000
--- a/db/migrate/20131009115346_add_confirmable_to_users.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# rubocop:disable all
-class AddConfirmableToUsers < ActiveRecord::Migration
- def self.up
- add_column :users, :confirmation_token, :string
- add_column :users, :confirmed_at, :datetime
- add_column :users, :confirmation_sent_at, :datetime
- add_column :users, :unconfirmed_email, :string
- add_index :users, :confirmation_token, unique: true
- User.update_all(confirmed_at: Time.now)
- end
-
- def self.down
- remove_column :users, :confirmation_token, :confirmed_at, :confirmation_sent_at
- remove_column :users, :unconfirmed_email
- end
-end
diff --git a/db/migrate/20131106151520_remove_default_branch.rb b/db/migrate/20131106151520_remove_default_branch.rb
deleted file mode 100644
index fd3d1ed7ab3..00000000000
--- a/db/migrate/20131106151520_remove_default_branch.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# rubocop:disable all
-class RemoveDefaultBranch < ActiveRecord::Migration
- def up
- remove_column :projects, :default_branch
- end
-
- def down
- add_column :projects, :default_branch, :string
- end
-end
diff --git a/db/migrate/20131112114325_create_broadcast_messages.rb b/db/migrate/20131112114325_create_broadcast_messages.rb
deleted file mode 100644
index ad2549e53af..00000000000
--- a/db/migrate/20131112114325_create_broadcast_messages.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# rubocop:disable all
-class CreateBroadcastMessages < ActiveRecord::Migration
- DOWNTIME = false
-
- def change
- create_table :broadcast_messages do |t|
- t.text :message, null: false
- t.datetime :starts_at
- t.datetime :ends_at
- t.integer :alert_type
-
- t.timestamps null: true
- end
- end
-end
diff --git a/db/migrate/20131112220935_add_visibility_level_to_projects.rb b/db/migrate/20131112220935_add_visibility_level_to_projects.rb
deleted file mode 100644
index 86d73753adc..00000000000
--- a/db/migrate/20131112220935_add_visibility_level_to_projects.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# rubocop:disable all
-class AddVisibilityLevelToProjects < ActiveRecord::Migration
- include Gitlab::Database::MigrationHelpers
-
- def self.up
- add_column :projects, :visibility_level, :integer, :default => 0, :null => false
- execute("UPDATE projects SET visibility_level = #{Gitlab::VisibilityLevel::PUBLIC} WHERE public = #{true_value}")
- remove_column :projects, :public
- end
-
- def self.down
- add_column :projects, :public, :boolean, :default => false, :null => false
- execute("UPDATE projects SET public = #{true_value} WHERE visibility_level = #{Gitlab::VisibilityLevel::PUBLIC}")
- remove_column :projects, :visibility_level
- end
-end
diff --git a/db/migrate/20131129154016_add_archived_to_projects.rb b/db/migrate/20131129154016_add_archived_to_projects.rb
deleted file mode 100644
index e8e6908d137..00000000000
--- a/db/migrate/20131129154016_add_archived_to_projects.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddArchivedToProjects < ActiveRecord::Migration
- def change
- add_column :projects, :archived, :boolean, default: false, null: false
- end
-end
diff --git a/db/migrate/20131130165425_add_color_and_font_to_broadcast_messages.rb b/db/migrate/20131130165425_add_color_and_font_to_broadcast_messages.rb
deleted file mode 100644
index 348a284a53e..00000000000
--- a/db/migrate/20131130165425_add_color_and_font_to_broadcast_messages.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# rubocop:disable all
-class AddColorAndFontToBroadcastMessages < ActiveRecord::Migration
- def change
- add_column :broadcast_messages, :color, :string
- add_column :broadcast_messages, :font, :string
- end
-end
diff --git a/db/migrate/20131202192556_add_event_fields_for_web_hook.rb b/db/migrate/20131202192556_add_event_fields_for_web_hook.rb
deleted file mode 100644
index 99d76611524..00000000000
--- a/db/migrate/20131202192556_add_event_fields_for_web_hook.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# rubocop:disable all
-class AddEventFieldsForWebHook < ActiveRecord::Migration
- def change
- add_column :web_hooks, :push_events, :boolean, default: true, null: false
- add_column :web_hooks, :issues_events, :boolean, default: false, null: false
- add_column :web_hooks, :merge_requests_events, :boolean, default: false, null: false
- end
-end
diff --git a/db/migrate/20131214224427_add_hide_no_ssh_key_to_users.rb b/db/migrate/20131214224427_add_hide_no_ssh_key_to_users.rb
deleted file mode 100644
index 4333dc59323..00000000000
--- a/db/migrate/20131214224427_add_hide_no_ssh_key_to_users.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddHideNoSshKeyToUsers < ActiveRecord::Migration
- def change
- add_column :users, :hide_no_ssh_key, :boolean, :default => false
- end
-end
diff --git a/db/migrate/20131217102743_add_recipients_to_service.rb b/db/migrate/20131217102743_add_recipients_to_service.rb
deleted file mode 100644
index 3c76be0f68d..00000000000
--- a/db/migrate/20131217102743_add_recipients_to_service.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddRecipientsToService < ActiveRecord::Migration
- def change
- add_column :services, :recipients, :text
- end
-end
diff --git a/db/migrate/20140116231608_add_website_url_to_users.rb b/db/migrate/20140116231608_add_website_url_to_users.rb
deleted file mode 100644
index 1c39423562e..00000000000
--- a/db/migrate/20140116231608_add_website_url_to_users.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddWebsiteUrlToUsers < ActiveRecord::Migration
- def change
- add_column :users, :website_url, :string, {:null => false, :default => ''}
- end
-end
diff --git a/db/migrate/20140122112253_create_merge_request_diffs.rb b/db/migrate/20140122112253_create_merge_request_diffs.rb
deleted file mode 100644
index 6c7a92b6950..00000000000
--- a/db/migrate/20140122112253_create_merge_request_diffs.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# rubocop:disable all
-class CreateMergeRequestDiffs < ActiveRecord::Migration
- DOWNTIME = false
-
- def up
- create_table :merge_request_diffs do |t|
- t.string :state, null: false, default: 'collected'
- t.text :st_commits, null: true
- t.text :st_diffs, null: true
- t.integer :merge_request_id, null: false
-
- t.timestamps null: true
- end
-
- if ActiveRecord::Base.configurations[Rails.env]['adapter'] =~ /^mysql/
- change_column :merge_request_diffs, :st_commits, :text, limit: 2147483647
- change_column :merge_request_diffs, :st_diffs, :text, limit: 2147483647
- end
- end
-
- def down
- drop_table :merge_request_diffs
- end
-end
diff --git a/db/migrate/20140122114406_migrate_mr_diffs.rb b/db/migrate/20140122114406_migrate_mr_diffs.rb
deleted file mode 100644
index 429aeb2293f..00000000000
--- a/db/migrate/20140122114406_migrate_mr_diffs.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# rubocop:disable all
-class MigrateMrDiffs < ActiveRecord::Migration
- def self.up
- execute "INSERT INTO merge_request_diffs ( merge_request_id, st_commits, st_diffs ) SELECT id, st_commits, st_diffs FROM merge_requests"
- end
-
- def self.down
- MergeRequestDiff.delete_all
- end
-end
diff --git a/db/migrate/20140122122549_remove_m_rdiff_fields.rb b/db/migrate/20140122122549_remove_m_rdiff_fields.rb
deleted file mode 100644
index bbf35811b61..00000000000
--- a/db/migrate/20140122122549_remove_m_rdiff_fields.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# rubocop:disable all
-class RemoveMRdiffFields < ActiveRecord::Migration
- def up
- remove_column :merge_requests, :st_commits
- remove_column :merge_requests, :st_diffs
- end
-
- def down
- add_column :merge_requests, :st_commits, :text, null: true, limit: 2147483647
- add_column :merge_requests, :st_diffs, :text, null: true, limit: 2147483647
-
- if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
- execute "UPDATE merge_requests mr
- SET (st_commits, st_diffs) = (md.st_commits, md.st_diffs)
- FROM merge_request_diffs md
- WHERE md.merge_request_id = mr.id"
- else
- execute "UPDATE merge_requests mr, merge_request_diffs md SET mr.st_commits = md.st_commits WHERE md.merge_request_id = mr.id"
- execute "UPDATE merge_requests mr, merge_request_diffs md SET mr.st_diffs = md.st_diffs WHERE md.merge_request_id = mr.id"
- end
- end
-end
diff --git a/db/migrate/20140125162722_add_avatar_to_projects.rb b/db/migrate/20140125162722_add_avatar_to_projects.rb
deleted file mode 100644
index 888341b7535..00000000000
--- a/db/migrate/20140125162722_add_avatar_to_projects.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddAvatarToProjects < ActiveRecord::Migration
- def change
- add_column :projects, :avatar, :string
- end
-end
diff --git a/db/migrate/20140127170938_add_group_avatars.rb b/db/migrate/20140127170938_add_group_avatars.rb
deleted file mode 100644
index 95d1c1c6b27..00000000000
--- a/db/migrate/20140127170938_add_group_avatars.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddGroupAvatars < ActiveRecord::Migration
- def change
- add_column :namespaces, :avatar, :string
- end
-end
diff --git a/db/migrate/20140209025651_create_emails.rb b/db/migrate/20140209025651_create_emails.rb
deleted file mode 100644
index 51886f8fc89..00000000000
--- a/db/migrate/20140209025651_create_emails.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# rubocop:disable all
-class CreateEmails < ActiveRecord::Migration
- DOWNTIME = false
-
- def change
- create_table :emails do |t|
- t.integer :user_id, null: false
- t.string :email, null: false
-
- t.timestamps null: true
- end
-
- add_index :emails, :user_id
- add_index :emails, :email, unique: true
- end
-end
diff --git a/db/migrate/20140214102325_add_api_key_to_services.rb b/db/migrate/20140214102325_add_api_key_to_services.rb
deleted file mode 100644
index b58c36c0a30..00000000000
--- a/db/migrate/20140214102325_add_api_key_to_services.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddApiKeyToServices < ActiveRecord::Migration
- def change
- add_column :services, :api_key, :string
- end
-end
diff --git a/db/migrate/20140304005354_add_index_merge_request_diffs_on_merge_request_id.rb b/db/migrate/20140304005354_add_index_merge_request_diffs_on_merge_request_id.rb
deleted file mode 100644
index aab8a41c2c3..00000000000
--- a/db/migrate/20140304005354_add_index_merge_request_diffs_on_merge_request_id.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddIndexMergeRequestDiffsOnMergeRequestId < ActiveRecord::Migration
- def change
- add_index :merge_request_diffs, :merge_request_id, unique: true
- end
-end
diff --git a/db/migrate/20140305193308_add_tag_push_hooks_to_project_hook.rb b/db/migrate/20140305193308_add_tag_push_hooks_to_project_hook.rb
deleted file mode 100644
index ec163bb843c..00000000000
--- a/db/migrate/20140305193308_add_tag_push_hooks_to_project_hook.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddTagPushHooksToProjectHook < ActiveRecord::Migration
- def change
- add_column :web_hooks, :tag_push_events, :boolean, default: false
- end
-end
diff --git a/db/migrate/20140312145357_add_import_status_to_project.rb b/db/migrate/20140312145357_add_import_status_to_project.rb
deleted file mode 100644
index 9947cd8c6f9..00000000000
--- a/db/migrate/20140312145357_add_import_status_to_project.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddImportStatusToProject < ActiveRecord::Migration
- def change
- add_column :projects, :import_status, :string
- end
-end
diff --git a/db/migrate/20140313092127_init_schema.rb b/db/migrate/20140313092127_init_schema.rb
new file mode 100644
index 00000000000..895298ed4ed
--- /dev/null
+++ b/db/migrate/20140313092127_init_schema.rb
@@ -0,0 +1,337 @@
+class InitSchema < ActiveRecord::Migration
+ DOWNTIME = true
+
+ def up
+ create_table "broadcast_messages", force: :cascade do |t|
+ t.text "message", null: false
+ t.datetime "starts_at"
+ t.datetime "ends_at"
+ t.integer "alert_type"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "color"
+ t.string "font"
+ end
+ create_table "deploy_keys_projects", force: :cascade do |t|
+ t.integer "deploy_key_id", null: false
+ t.integer "project_id", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+ add_index "deploy_keys_projects", ["project_id"], name: "index_deploy_keys_projects_on_project_id", using: :btree
+ create_table "emails", force: :cascade do |t|
+ t.integer "user_id", null: false
+ t.string "email", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+ add_index "emails", ["email"], name: "index_emails_on_email", unique: true, using: :btree
+ add_index "emails", ["user_id"], name: "index_emails_on_user_id", using: :btree
+ create_table "events", force: :cascade do |t|
+ t.string "target_type"
+ t.integer "target_id"
+ t.string "title"
+ t.text "data"
+ t.integer "project_id"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.integer "action"
+ t.integer "author_id"
+ end
+ add_index "events", ["action"], name: "index_events_on_action", using: :btree
+ add_index "events", ["author_id"], name: "index_events_on_author_id", using: :btree
+ add_index "events", ["created_at"], name: "index_events_on_created_at", using: :btree
+ add_index "events", ["project_id"], name: "index_events_on_project_id", using: :btree
+ add_index "events", ["target_id"], name: "index_events_on_target_id", using: :btree
+ add_index "events", ["target_type"], name: "index_events_on_target_type", using: :btree
+ create_table "forked_project_links", force: :cascade do |t|
+ t.integer "forked_to_project_id", null: false
+ t.integer "forked_from_project_id", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+ add_index "forked_project_links", ["forked_to_project_id"], name: "index_forked_project_links_on_forked_to_project_id", unique: true, using: :btree
+ create_table "issues", force: :cascade do |t|
+ t.string "title"
+ t.integer "assignee_id"
+ t.integer "author_id"
+ t.integer "project_id"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.integer "position", default: 0
+ t.string "branch_name"
+ t.text "description"
+ t.integer "milestone_id"
+ t.string "state"
+ t.integer "iid"
+ end
+ add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
+ add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree
+ add_index "issues", ["created_at"], name: "index_issues_on_created_at", using: :btree
+ add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree
+ add_index "issues", ["project_id"], name: "index_issues_on_project_id", using: :btree
+ add_index "issues", ["title"], name: "index_issues_on_title", using: :btree
+ create_table "keys", force: :cascade do |t|
+ t.integer "user_id"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.text "key"
+ t.string "title"
+ t.string "type"
+ t.string "fingerprint"
+ end
+ add_index "keys", ["user_id"], name: "index_keys_on_user_id", using: :btree
+ create_table "merge_request_diffs", force: :cascade do |t|
+ t.string "state", default: "collected", null: false
+ t.text "st_commits"
+ t.text "st_diffs"
+ t.integer "merge_request_id", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+ add_index "merge_request_diffs", ["merge_request_id"], name: "index_merge_request_diffs_on_merge_request_id", unique: true, using: :btree
+ create_table "merge_requests", force: :cascade do |t|
+ t.string "target_branch", null: false
+ t.string "source_branch", null: false
+ t.integer "source_project_id", null: false
+ t.integer "author_id"
+ t.integer "assignee_id"
+ t.string "title"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.integer "milestone_id"
+ t.string "state"
+ t.string "merge_status"
+ t.integer "target_project_id", null: false
+ t.integer "iid"
+ t.text "description"
+ end
+ add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
+ add_index "merge_requests", ["author_id"], name: "index_merge_requests_on_author_id", using: :btree
+ add_index "merge_requests", ["created_at"], name: "index_merge_requests_on_created_at", using: :btree
+ add_index "merge_requests", ["milestone_id"], name: "index_merge_requests_on_milestone_id", using: :btree
+ add_index "merge_requests", ["source_branch"], name: "index_merge_requests_on_source_branch", using: :btree
+ add_index "merge_requests", ["source_project_id"], name: "index_merge_requests_on_source_project_id", using: :btree
+ add_index "merge_requests", ["target_branch"], name: "index_merge_requests_on_target_branch", using: :btree
+ add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree
+ create_table "milestones", force: :cascade do |t|
+ t.string "title", null: false
+ t.integer "project_id", null: false
+ t.text "description"
+ t.date "due_date"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "state"
+ t.integer "iid"
+ end
+ add_index "milestones", ["due_date"], name: "index_milestones_on_due_date", using: :btree
+ add_index "milestones", ["project_id"], name: "index_milestones_on_project_id", using: :btree
+ create_table "namespaces", force: :cascade do |t|
+ t.string "name", null: false
+ t.string "path", null: false
+ t.integer "owner_id"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "type"
+ t.string "description", default: "", null: false
+ t.string "avatar"
+ end
+ add_index "namespaces", ["name"], name: "index_namespaces_on_name", using: :btree
+ add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree
+ add_index "namespaces", ["path"], name: "index_namespaces_on_path", using: :btree
+ add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree
+ create_table "notes", force: :cascade do |t|
+ t.text "note"
+ t.string "noteable_type"
+ t.integer "author_id"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.integer "project_id"
+ t.string "attachment"
+ t.string "line_code"
+ t.string "commit_id"
+ t.integer "noteable_id"
+ t.boolean "system", default: false, null: false
+ t.text "st_diff"
+ end
+ add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree
+ add_index "notes", ["commit_id"], name: "index_notes_on_commit_id", using: :btree
+ add_index "notes", ["created_at"], name: "index_notes_on_created_at", using: :btree
+ add_index "notes", ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree
+ add_index "notes", ["noteable_type"], name: "index_notes_on_noteable_type", using: :btree
+ add_index "notes", ["project_id", "noteable_type"], name: "index_notes_on_project_id_and_noteable_type", using: :btree
+ add_index "notes", ["project_id"], name: "index_notes_on_project_id", using: :btree
+ create_table "project_group_links", force: :cascade do |t|
+ t.integer "project_id", null: false
+ t.integer "group_id", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.integer "group_access", default: 30, null: false
+ end
+ create_table "projects", force: :cascade do |t|
+ t.string "name"
+ t.string "path"
+ t.text "description"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.integer "creator_id"
+ t.boolean "issues_enabled", default: true, null: false
+ t.boolean "wall_enabled", default: true, null: false
+ t.boolean "merge_requests_enabled", default: true, null: false
+ t.boolean "wiki_enabled", default: true, null: false
+ t.integer "namespace_id"
+ t.string "issues_tracker", default: "gitlab", null: false
+ t.string "issues_tracker_id"
+ t.boolean "snippets_enabled", default: true, null: false
+ t.datetime "last_activity_at"
+ t.string "import_url"
+ t.integer "visibility_level", default: 0, null: false
+ t.boolean "archived", default: false, null: false
+ t.string "avatar"
+ t.string "import_status"
+ end
+ add_index "projects", ["creator_id"], name: "index_projects_on_creator_id", using: :btree
+ add_index "projects", ["last_activity_at"], name: "index_projects_on_last_activity_at", using: :btree
+ add_index "projects", ["namespace_id"], name: "index_projects_on_namespace_id", using: :btree
+ create_table "protected_branches", force: :cascade do |t|
+ t.integer "project_id", null: false
+ t.string "name", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+ add_index "protected_branches", ["project_id"], name: "index_protected_branches_on_project_id", using: :btree
+ create_table "services", force: :cascade do |t|
+ t.string "type"
+ t.string "title"
+ t.string "token"
+ t.integer "project_id", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.boolean "active", default: false, null: false
+ t.string "project_url"
+ t.string "subdomain"
+ t.string "room"
+ t.text "recipients"
+ t.string "api_key"
+ end
+ add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree
+ create_table "snippets", force: :cascade do |t|
+ t.string "title"
+ t.text "content"
+ t.integer "author_id", null: false
+ t.integer "project_id"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "file_name"
+ t.datetime "expires_at"
+ t.boolean "private", default: true, null: false
+ t.string "type"
+ end
+ add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree
+ add_index "snippets", ["created_at"], name: "index_snippets_on_created_at", using: :btree
+ add_index "snippets", ["expires_at"], name: "index_snippets_on_expires_at", using: :btree
+ add_index "snippets", ["project_id"], name: "index_snippets_on_project_id", using: :btree
+ create_table "taggings", force: :cascade do |t|
+ t.integer "tag_id"
+ t.integer "taggable_id"
+ t.string "taggable_type"
+ t.integer "tagger_id"
+ t.string "tagger_type"
+ t.string "context"
+ t.datetime "created_at"
+ end
+ add_index "taggings", ["tag_id"], name: "index_taggings_on_tag_id", using: :btree
+ add_index "taggings", ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree
+ create_table "tags", force: :cascade do |t|
+ t.string "name"
+ end
+ create_table "users", force: :cascade do |t|
+ t.string "email", default: "", null: false
+ t.string "encrypted_password", default: "", null: false
+ t.string "reset_password_token"
+ t.datetime "reset_password_sent_at"
+ t.datetime "remember_created_at"
+ t.integer "sign_in_count", default: 0
+ t.datetime "current_sign_in_at"
+ t.datetime "last_sign_in_at"
+ t.string "current_sign_in_ip"
+ t.string "last_sign_in_ip"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "name"
+ t.boolean "admin", default: false, null: false
+ t.integer "projects_limit", default: 10
+ t.string "skype", default: "", null: false
+ t.string "linkedin", default: "", null: false
+ t.string "twitter", default: "", null: false
+ t.string "authentication_token"
+ t.integer "theme_id", default: 1, null: false
+ t.string "bio"
+ t.integer "failed_attempts", default: 0
+ t.datetime "locked_at"
+ t.string "extern_uid"
+ t.string "provider"
+ t.string "username"
+ t.boolean "can_create_group", default: true, null: false
+ t.boolean "can_create_team", default: true, null: false
+ t.string "state"
+ t.integer "color_scheme_id", default: 1, null: false
+ t.integer "notification_level", default: 1, null: false
+ t.datetime "password_expires_at"
+ t.integer "created_by_id"
+ t.datetime "last_credential_check_at"
+ t.string "avatar"
+ t.string "confirmation_token"
+ t.datetime "confirmed_at"
+ t.datetime "confirmation_sent_at"
+ t.string "unconfirmed_email"
+ t.boolean "hide_no_ssh_key", default: false
+ t.string "website_url", default: "", null: false
+ end
+ add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
+ add_index "users", ["authentication_token"], name: "index_users_on_authentication_token", unique: true, using: :btree
+ add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree
+ add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
+ add_index "users", ["extern_uid", "provider"], name: "index_users_on_extern_uid_and_provider", unique: true, using: :btree
+ add_index "users", ["name"], name: "index_users_on_name", using: :btree
+ add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
+ add_index "users", ["username"], name: "index_users_on_username", using: :btree
+ create_table "users_groups", force: :cascade do |t|
+ t.integer "group_access", null: false
+ t.integer "group_id", null: false
+ t.integer "user_id", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.integer "notification_level", default: 3, null: false
+ end
+ add_index "users_groups", ["user_id"], name: "index_users_groups_on_user_id", using: :btree
+ create_table "users_projects", force: :cascade do |t|
+ t.integer "user_id", null: false
+ t.integer "project_id", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.integer "project_access", default: 0, null: false
+ t.integer "notification_level", default: 3, null: false
+ end
+ add_index "users_projects", ["project_access"], name: "index_users_projects_on_project_access", using: :btree
+ add_index "users_projects", ["project_id"], name: "index_users_projects_on_project_id", using: :btree
+ add_index "users_projects", ["user_id"], name: "index_users_projects_on_user_id", using: :btree
+ create_table "web_hooks", force: :cascade do |t|
+ t.string "url"
+ t.integer "project_id"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "type", default: "ProjectHook"
+ t.integer "service_id"
+ t.boolean "push_events", default: true, null: false
+ t.boolean "issues_events", default: false, null: false
+ t.boolean "merge_requests_events", default: false, null: false
+ t.boolean "tag_push_events", default: false
+ end
+ add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
+ end
+
+ def down
+ raise ActiveRecord::IrreversibleMigration, "The initial migration is not revertable"
+ end
+end
diff --git a/db/migrate/20140313092127_migrate_already_imported_projects.rb b/db/migrate/20140313092127_migrate_already_imported_projects.rb
deleted file mode 100644
index 0afc26b8764..00000000000
--- a/db/migrate/20140313092127_migrate_already_imported_projects.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# rubocop:disable all
-class MigrateAlreadyImportedProjects < ActiveRecord::Migration
- include Gitlab::Database::MigrationHelpers
-
- def up
- execute("UPDATE projects SET import_status = 'finished' WHERE imported = #{true_value}")
- execute("UPDATE projects SET import_status = 'none' WHERE imported = #{false_value}")
- remove_column :projects, :imported
- end
-
- def down
- add_column :projects, :imported, :boolean, default: false
- execute("UPDATE projects SET imported = #{true_value} WHERE import_status = 'finished'")
- end
-end
diff --git a/db/migrate/20180326202229_create_ci_build_trace_chunks.rb b/db/migrate/20180326202229_create_ci_build_trace_chunks.rb
new file mode 100644
index 00000000000..fb3f5786e85
--- /dev/null
+++ b/db/migrate/20180326202229_create_ci_build_trace_chunks.rb
@@ -0,0 +1,17 @@
+class CreateCiBuildTraceChunks < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :ci_build_trace_chunks, id: :bigserial do |t|
+ t.integer :build_id, null: false
+ t.integer :chunk_index, null: false
+ t.integer :data_store, null: false
+ t.binary :raw_data
+
+ t.foreign_key :ci_builds, column: :build_id, on_delete: :cascade
+ t.index [:build_id, :chunk_index], unique: true
+ end
+ end
+end
diff --git a/db/migrate/20180406204716_add_limits_ci_build_trace_chunks_raw_data_for_mysql.rb b/db/migrate/20180406204716_add_limits_ci_build_trace_chunks_raw_data_for_mysql.rb
new file mode 100644
index 00000000000..0f2734853e6
--- /dev/null
+++ b/db/migrate/20180406204716_add_limits_ci_build_trace_chunks_raw_data_for_mysql.rb
@@ -0,0 +1,13 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+require Rails.root.join('db/migrate/limits_ci_build_trace_chunks_raw_data_for_mysql')
+
+class AddLimitsCiBuildTraceChunksRawDataForMysql < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ LimitsCiBuildTraceChunksRawDataForMysql.new.up
+ end
+end
diff --git a/db/migrate/20180420010016_add_pipeline_build_foreign_key.rb b/db/migrate/20180420010016_add_pipeline_build_foreign_key.rb
new file mode 100644
index 00000000000..6fabe07bc9c
--- /dev/null
+++ b/db/migrate/20180420010016_add_pipeline_build_foreign_key.rb
@@ -0,0 +1,27 @@
+class AddPipelineBuildForeignKey < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ execute <<~SQL
+ DELETE FROM ci_builds WHERE project_id IS NULL OR commit_id IS NULL
+ SQL
+
+ execute <<~SQL
+ DELETE FROM ci_builds WHERE NOT EXISTS
+ (SELECT true FROM ci_pipelines WHERE ci_pipelines.id = ci_builds.commit_id)
+ AND stage_id IS NULL
+ SQL
+
+ add_concurrent_foreign_key(:ci_builds, :ci_pipelines, column: :commit_id)
+ end
+
+ def down
+ return unless foreign_key_exists?(:ci_builds, :ci_pipelines, column: :commit_id)
+
+ remove_foreign_key(:ci_builds, column: :commit_id)
+ end
+end
diff --git a/db/migrate/20180420010616_cleanup_build_stage_migration.rb b/db/migrate/20180420010616_cleanup_build_stage_migration.rb
new file mode 100644
index 00000000000..24777294101
--- /dev/null
+++ b/db/migrate/20180420010616_cleanup_build_stage_migration.rb
@@ -0,0 +1,61 @@
+class CleanupBuildStageMigration < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ TMP_INDEX = 'tmp_id_stage_partial_null_index'.freeze
+
+ disable_ddl_transaction!
+
+ class Build < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'ci_builds'
+ self.inheritance_column = :_type_disabled
+ end
+
+ def up
+ disable_statement_timeout
+
+ ##
+ # We steal from the background migrations queue to catch up with the
+ # scheduled migrations set.
+ #
+ Gitlab::BackgroundMigration.steal('MigrateBuildStage')
+
+ ##
+ # We add temporary index, to make iteration over batches more performant.
+ # Conditional here is to avoid the need of doing that in a separate
+ # migration file to make this operation idempotent.
+ #
+ unless index_exists_by_name?(:ci_builds, TMP_INDEX)
+ add_concurrent_index(:ci_builds, :id, where: 'stage_id IS NULL', name: TMP_INDEX)
+ end
+
+ ##
+ # We check if there are remaining rows that should be migrated (for example
+ # if Sidekiq / Redis fails / is restarted, what could result in not all
+ # background migrations being executed correctly.
+ #
+ # We migrate remaining rows synchronously in a blocking way, to make sure
+ # that when this migration is done we are confident that all rows are
+ # already migrated.
+ #
+ Build.where('stage_id IS NULL').each_batch(of: 50) do |batch|
+ range = batch.pluck('MIN(id)', 'MAX(id)').first
+
+ Gitlab::BackgroundMigration::MigrateBuildStage.new.perform(*range)
+ end
+
+ ##
+ # We remove temporary index, because it is not required during standard
+ # operations and runtime.
+ #
+ remove_concurrent_index_by_name(:ci_builds, TMP_INDEX)
+ end
+
+ def down
+ if index_exists_by_name?(:ci_builds, TMP_INDEX)
+ remove_concurrent_index_by_name(:ci_builds, TMP_INDEX)
+ end
+ end
+end
diff --git a/db/migrate/20180503131624_create_remote_mirrors.rb b/db/migrate/20180503131624_create_remote_mirrors.rb
new file mode 100644
index 00000000000..7800186455f
--- /dev/null
+++ b/db/migrate/20180503131624_create_remote_mirrors.rb
@@ -0,0 +1,33 @@
+class CreateRemoteMirrors < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ return if table_exists?(:remote_mirrors)
+
+ create_table :remote_mirrors do |t|
+ t.references :project, index: true, foreign_key: { on_delete: :cascade }
+ t.string :url
+ t.boolean :enabled, default: true
+ t.string :update_status
+ t.datetime :last_update_at
+ t.datetime :last_successful_update_at
+ t.datetime :last_update_started_at
+ t.string :last_error
+ t.boolean :only_protected_branches, default: false, null: false
+ t.string :remote_name
+ t.text :encrypted_credentials
+ t.string :encrypted_credentials_iv
+ t.string :encrypted_credentials_salt
+
+ t.timestamps null: false
+ end
+ end
+
+ def down
+ drop_table(:remote_mirrors) if table_exists?(:remote_mirrors)
+ end
+end
diff --git a/db/migrate/20180503141722_add_remote_mirror_available_overridden_to_projects.rb b/db/migrate/20180503141722_add_remote_mirror_available_overridden_to_projects.rb
new file mode 100644
index 00000000000..841393971f4
--- /dev/null
+++ b/db/migrate/20180503141722_add_remote_mirror_available_overridden_to_projects.rb
@@ -0,0 +1,15 @@
+class AddRemoteMirrorAvailableOverriddenToProjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column(:projects, :remote_mirror_available_overridden, :boolean) unless column_exists?(:projects, :remote_mirror_available_overridden)
+ end
+
+ def down
+ remove_column(:projects, :remote_mirror_available_overridden) if column_exists?(:projects, :remote_mirror_available_overridden)
+ end
+end
diff --git a/db/migrate/20180503193542_add_indexes_to_remote_mirror.rb b/db/migrate/20180503193542_add_indexes_to_remote_mirror.rb
new file mode 100644
index 00000000000..9a9decffdab
--- /dev/null
+++ b/db/migrate/20180503193542_add_indexes_to_remote_mirror.rb
@@ -0,0 +1,15 @@
+class AddIndexesToRemoteMirror < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :remote_mirrors, :last_successful_update_at unless index_exists?(:remote_mirrors, :last_successful_update_at)
+ end
+
+ def down
+ remove_index :remote_mirrors, :last_successful_update_at if index_exists? :remote_mirrors, :last_successful_update_at
+ end
+end
diff --git a/db/migrate/20180503193953_add_mirror_available_to_application_settings.rb b/db/migrate/20180503193953_add_mirror_available_to_application_settings.rb
new file mode 100644
index 00000000000..25b9905b1a9
--- /dev/null
+++ b/db/migrate/20180503193953_add_mirror_available_to_application_settings.rb
@@ -0,0 +1,15 @@
+class AddMirrorAvailableToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:application_settings, :mirror_available, :boolean, default: true, allow_null: false) unless column_exists?(:application_settings, :mirror_available)
+ end
+
+ def down
+ remove_column(:application_settings, :mirror_available) if column_exists?(:application_settings, :mirror_available)
+ end
+end
diff --git a/db/migrate/20180503200320_enable_prometheus_metrics_by_default.rb b/db/migrate/20180503200320_enable_prometheus_metrics_by_default.rb
new file mode 100644
index 00000000000..2c8f86ff0f4
--- /dev/null
+++ b/db/migrate/20180503200320_enable_prometheus_metrics_by_default.rb
@@ -0,0 +1,11 @@
+class EnablePrometheusMetricsByDefault < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def up
+ change_column_default :application_settings, :prometheus_metrics_enabled, true
+ end
+
+ def down
+ change_column_default :application_settings, :prometheus_metrics_enabled, false
+ end
+end
diff --git a/db/migrate/20180508055821_make_remote_mirrors_disabled_by_default.rb b/db/migrate/20180508055821_make_remote_mirrors_disabled_by_default.rb
new file mode 100644
index 00000000000..0d8a8357a1e
--- /dev/null
+++ b/db/migrate/20180508055821_make_remote_mirrors_disabled_by_default.rb
@@ -0,0 +1,11 @@
+class MakeRemoteMirrorsDisabledByDefault < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def up
+ change_column_default :remote_mirrors, :enabled, false
+ end
+
+ def down
+ change_column_default :remote_mirrors, :enabled, true
+ end
+end
diff --git a/db/migrate/20180508100222_add_not_null_constraint_to_project_mirror_data_foreign_key.rb b/db/migrate/20180508100222_add_not_null_constraint_to_project_mirror_data_foreign_key.rb
new file mode 100644
index 00000000000..82087d15ccb
--- /dev/null
+++ b/db/migrate/20180508100222_add_not_null_constraint_to_project_mirror_data_foreign_key.rb
@@ -0,0 +1,21 @@
+class AddNotNullConstraintToProjectMirrorDataForeignKey < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ class ProjectImportState < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'project_mirror_data'
+ end
+
+ def up
+ ProjectImportState.where(project_id: nil).delete_all
+
+ change_column_null :project_mirror_data, :project_id, false
+ end
+
+ def down
+ change_column_null :project_mirror_data, :project_id, true
+ end
+end
diff --git a/db/migrate/20180508102840_add_unique_constraint_to_project_mirror_data_project_id_index.rb b/db/migrate/20180508102840_add_unique_constraint_to_project_mirror_data_project_id_index.rb
new file mode 100644
index 00000000000..acb976b52fa
--- /dev/null
+++ b/db/migrate/20180508102840_add_unique_constraint_to_project_mirror_data_project_id_index.rb
@@ -0,0 +1,31 @@
+class AddUniqueConstraintToProjectMirrorDataProjectIdIndex < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index(:project_mirror_data,
+ :project_id,
+ unique: true,
+ name: 'index_project_mirror_data_on_project_id_unique')
+
+ remove_concurrent_index_by_name(:project_mirror_data, 'index_project_mirror_data_on_project_id')
+
+ rename_index(:project_mirror_data,
+ 'index_project_mirror_data_on_project_id_unique',
+ 'index_project_mirror_data_on_project_id')
+ end
+
+ def down
+ rename_index(:project_mirror_data,
+ 'index_project_mirror_data_on_project_id',
+ 'index_project_mirror_data_on_project_id_old')
+
+ add_concurrent_index(:project_mirror_data, :project_id)
+
+ remove_concurrent_index_by_name(:project_mirror_data,
+ 'index_project_mirror_data_on_project_id_old')
+ end
+end
diff --git a/db/migrate/20180508135515_set_runner_type_not_null.rb b/db/migrate/20180508135515_set_runner_type_not_null.rb
new file mode 100644
index 00000000000..dd043ec7179
--- /dev/null
+++ b/db/migrate/20180508135515_set_runner_type_not_null.rb
@@ -0,0 +1,9 @@
+class SetRunnerTypeNotNull < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ change_column_null(:ci_runners, :runner_type, false)
+ end
+end
diff --git a/db/migrate/20180511090724_add_index_on_ci_runners_runner_type.rb b/db/migrate/20180511090724_add_index_on_ci_runners_runner_type.rb
new file mode 100644
index 00000000000..580f56007c7
--- /dev/null
+++ b/db/migrate/20180511090724_add_index_on_ci_runners_runner_type.rb
@@ -0,0 +1,15 @@
+class AddIndexOnCiRunnersRunnerType < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :ci_runners, :runner_type
+ end
+
+ def down
+ remove_index :ci_runners, :runner_type
+ end
+end
diff --git a/db/migrate/limits_ci_build_trace_chunks_raw_data_for_mysql.rb b/db/migrate/limits_ci_build_trace_chunks_raw_data_for_mysql.rb
new file mode 100644
index 00000000000..e1771912c3c
--- /dev/null
+++ b/db/migrate/limits_ci_build_trace_chunks_raw_data_for_mysql.rb
@@ -0,0 +1,9 @@
+class LimitsCiBuildTraceChunksRawDataForMysql < ActiveRecord::Migration
+ def up
+ return unless Gitlab::Database.mysql?
+
+ # Mysql needs MEDIUMTEXT type (up to 16MB) rather than TEXT (up to 64KB)
+ # Because 'raw_data' is always capped by Ci::BuildTraceChunk::CHUNK_SIZE, which is 128KB
+ change_column :ci_build_trace_chunks, :raw_data, :binary, limit: 16.megabytes - 1 #MEDIUMTEXT
+ end
+end
diff --git a/db/post_migrate/20180511174224_add_unique_constraint_to_project_features_project_id.rb b/db/post_migrate/20180511174224_add_unique_constraint_to_project_features_project_id.rb
new file mode 100644
index 00000000000..88a9f5f8256
--- /dev/null
+++ b/db/post_migrate/20180511174224_add_unique_constraint_to_project_features_project_id.rb
@@ -0,0 +1,43 @@
+class AddUniqueConstraintToProjectFeaturesProjectId < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class ProjectFeature < ActiveRecord::Base
+ self.table_name = 'project_features'
+
+ include EachBatch
+ end
+
+ def up
+ remove_duplicates
+
+ add_concurrent_index :project_features, :project_id, unique: true, name: 'index_project_features_on_project_id_unique'
+ remove_concurrent_index_by_name :project_features, 'index_project_features_on_project_id'
+ rename_index :project_features, 'index_project_features_on_project_id_unique', 'index_project_features_on_project_id'
+ end
+
+ def down
+ rename_index :project_features, 'index_project_features_on_project_id', 'index_project_features_on_project_id_old'
+ add_concurrent_index :project_features, :project_id
+ remove_concurrent_index_by_name :project_features, 'index_project_features_on_project_id_old'
+ end
+
+ private
+
+ def remove_duplicates
+ features = ProjectFeature
+ .select('MAX(id) AS max, COUNT(id), project_id')
+ .group(:project_id)
+ .having('COUNT(id) > 1')
+
+ features.each do |feature|
+ ProjectFeature
+ .where(project_id: feature['project_id'])
+ .where('id <> ?', feature['max'])
+ .each_batch { |batch| batch.delete_all }
+ end
+ end
+end
diff --git a/db/post_migrate/20180512061621_add_not_null_constraint_to_project_features_project_id.rb b/db/post_migrate/20180512061621_add_not_null_constraint_to_project_features_project_id.rb
new file mode 100644
index 00000000000..5a6d6ff4a10
--- /dev/null
+++ b/db/post_migrate/20180512061621_add_not_null_constraint_to_project_features_project_id.rb
@@ -0,0 +1,21 @@
+class AddNotNullConstraintToProjectFeaturesProjectId < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ class ProjectFeature < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'project_features'
+ end
+
+ def up
+ ProjectFeature.where(project_id: nil).delete_all
+
+ change_column_null :project_features, :project_id, false
+ end
+
+ def down
+ change_column_null :project_features, :project_id, true
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 27c70c03612..ed29d202f91 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: 20180503175054) do
+ActiveRecord::Schema.define(version: 20180512061621) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -134,7 +134,7 @@ ActiveRecord::Schema.define(version: 20180503175054) do
t.integer "cached_markdown_version"
t.boolean "clientside_sentry_enabled", default: false, null: false
t.string "clientside_sentry_dsn"
- t.boolean "prometheus_metrics_enabled", default: false, null: false
+ t.boolean "prometheus_metrics_enabled", default: true, null: false
t.boolean "help_page_hide_commercial_content", default: false
t.string "help_page_support_url"
t.integer "performance_bar_allowed_group_id"
@@ -165,6 +165,7 @@ ActiveRecord::Schema.define(version: 20180503175054) do
t.boolean "pages_domain_verification_enabled", default: true, null: false
t.boolean "allow_local_requests_from_hooks_and_services", default: false, null: false
t.boolean "enforce_terms", default: false
+ t.boolean "mirror_available", default: true, null: false
end
create_table "audit_events", force: :cascade do |t|
@@ -253,6 +254,15 @@ ActiveRecord::Schema.define(version: 20180503175054) do
add_index "chat_teams", ["namespace_id"], name: "index_chat_teams_on_namespace_id", unique: true, using: :btree
+ create_table "ci_build_trace_chunks", id: :bigserial, force: :cascade do |t|
+ t.integer "build_id", null: false
+ t.integer "chunk_index", null: false
+ t.integer "data_store", null: false
+ t.binary "raw_data"
+ end
+
+ add_index "ci_build_trace_chunks", ["build_id", "chunk_index"], name: "index_ci_build_trace_chunks_on_build_id_and_chunk_index", unique: true, using: :btree
+
create_table "ci_build_trace_section_names", force: :cascade do |t|
t.integer "project_id", null: false
t.string "name", null: false
@@ -487,12 +497,13 @@ ActiveRecord::Schema.define(version: 20180503175054) do
t.integer "access_level", default: 0, null: false
t.string "ip_address"
t.integer "maximum_timeout"
- t.integer "runner_type", limit: 2
+ t.integer "runner_type", limit: 2, null: false
end
add_index "ci_runners", ["contacted_at"], name: "index_ci_runners_on_contacted_at", using: :btree
add_index "ci_runners", ["is_shared"], name: "index_ci_runners_on_is_shared", using: :btree
add_index "ci_runners", ["locked"], name: "index_ci_runners_on_locked", using: :btree
+ add_index "ci_runners", ["runner_type"], name: "index_ci_runners_on_runner_type", using: :btree
add_index "ci_runners", ["token"], name: "index_ci_runners_on_token", using: :btree
create_table "ci_stages", force: :cascade do |t|
@@ -1483,7 +1494,7 @@ ActiveRecord::Schema.define(version: 20180503175054) do
add_index "project_deploy_tokens", ["project_id", "deploy_token_id"], name: "index_project_deploy_tokens_on_project_id_and_deploy_token_id", unique: true, using: :btree
create_table "project_features", force: :cascade do |t|
- t.integer "project_id"
+ t.integer "project_id", null: false
t.integer "merge_requests_access_level"
t.integer "issues_access_level"
t.integer "wiki_access_level"
@@ -1494,7 +1505,7 @@ ActiveRecord::Schema.define(version: 20180503175054) do
t.integer "repository_access_level", default: 20, null: false
end
- add_index "project_features", ["project_id"], name: "index_project_features_on_project_id", using: :btree
+ add_index "project_features", ["project_id"], name: "index_project_features_on_project_id", unique: true, using: :btree
create_table "project_group_links", force: :cascade do |t|
t.integer "project_id", null: false
@@ -1519,14 +1530,14 @@ ActiveRecord::Schema.define(version: 20180503175054) do
add_index "project_import_data", ["project_id"], name: "index_project_import_data_on_project_id", using: :btree
create_table "project_mirror_data", force: :cascade do |t|
- t.integer "project_id"
+ t.integer "project_id", null: false
t.string "status"
t.string "jid"
t.text "last_error"
end
add_index "project_mirror_data", ["jid"], name: "index_project_mirror_data_on_jid", using: :btree
- add_index "project_mirror_data", ["project_id"], name: "index_project_mirror_data_on_project_id", using: :btree
+ add_index "project_mirror_data", ["project_id"], name: "index_project_mirror_data_on_project_id", unique: true, using: :btree
add_index "project_mirror_data", ["status"], name: "index_project_mirror_data_on_status", using: :btree
create_table "project_statistics", force: :cascade do |t|
@@ -1593,6 +1604,7 @@ ActiveRecord::Schema.define(version: 20180503175054) do
t.boolean "merge_requests_rebase_enabled", default: false, null: false
t.integer "jobs_cache_index"
t.boolean "pages_https_only", default: true
+ t.boolean "remote_mirror_available_overridden"
end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
@@ -1698,6 +1710,27 @@ ActiveRecord::Schema.define(version: 20180503175054) do
add_index "releases", ["project_id", "tag"], name: "index_releases_on_project_id_and_tag", using: :btree
add_index "releases", ["project_id"], name: "index_releases_on_project_id", using: :btree
+ create_table "remote_mirrors", force: :cascade do |t|
+ t.integer "project_id"
+ t.string "url"
+ t.boolean "enabled", default: false
+ t.string "update_status"
+ t.datetime "last_update_at"
+ t.datetime "last_successful_update_at"
+ t.datetime "last_update_started_at"
+ t.string "last_error"
+ t.boolean "only_protected_branches", default: false, null: false
+ t.string "remote_name"
+ t.text "encrypted_credentials"
+ t.string "encrypted_credentials_iv"
+ t.string "encrypted_credentials_salt"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ add_index "remote_mirrors", ["last_successful_update_at"], name: "index_remote_mirrors_on_last_successful_update_at", using: :btree
+ add_index "remote_mirrors", ["project_id"], name: "index_remote_mirrors_on_project_id", using: :btree
+
create_table "routes", force: :cascade do |t|
t.integer "source_id", null: false
t.string "source_type", null: false
@@ -2110,11 +2143,13 @@ ActiveRecord::Schema.define(version: 20180503175054) do
add_foreign_key "boards", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "boards", "projects", name: "fk_f15266b5f9", on_delete: :cascade
add_foreign_key "chat_teams", "namespaces", on_delete: :cascade
+ add_foreign_key "ci_build_trace_chunks", "ci_builds", column: "build_id", on_delete: :cascade
add_foreign_key "ci_build_trace_section_names", "projects", on_delete: :cascade
add_foreign_key "ci_build_trace_sections", "ci_build_trace_section_names", column: "section_name_id", name: "fk_264e112c66", on_delete: :cascade
add_foreign_key "ci_build_trace_sections", "ci_builds", column: "build_id", name: "fk_4ebe41f502", on_delete: :cascade
add_foreign_key "ci_build_trace_sections", "projects", on_delete: :cascade
add_foreign_key "ci_builds", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_a2141b1522", on_delete: :nullify
+ add_foreign_key "ci_builds", "ci_pipelines", column: "commit_id", name: "fk_d3130c9a7f", on_delete: :cascade
add_foreign_key "ci_builds", "ci_stages", column: "stage_id", name: "fk_3a9eaa254d", on_delete: :cascade
add_foreign_key "ci_builds", "projects", name: "fk_befce0568a", on_delete: :cascade
add_foreign_key "ci_builds_metadata", "ci_builds", column: "build_id", on_delete: :cascade
@@ -2233,6 +2268,7 @@ ActiveRecord::Schema.define(version: 20180503175054) do
add_foreign_key "protected_tags", "projects", name: "fk_8e4af87648", on_delete: :cascade
add_foreign_key "push_event_payloads", "events", name: "fk_36c74129da", on_delete: :cascade
add_foreign_key "releases", "projects", name: "fk_47fe2a0596", on_delete: :cascade
+ add_foreign_key "remote_mirrors", "projects", on_delete: :cascade
add_foreign_key "services", "projects", name: "fk_71cce407f9", on_delete: :cascade
add_foreign_key "snippets", "projects", name: "fk_be41fd4bb7", on_delete: :cascade
add_foreign_key "subscriptions", "projects", on_delete: :cascade
diff --git a/doc/README.md b/doc/README.md
index a2e152ce383..ff8dd3fab8a 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -1,5 +1,6 @@
---
comments: false
+description: 'Learn how to use and administer GitLab, the most scalable Git-based fully integrated platform for software development.'
---
# GitLab Documentation
@@ -240,7 +241,7 @@ GitLab.com is hosted, managed, and administered by GitLab, Inc., with
and teams: Free, Bronze, Silver, and Gold.
GitLab.com subscriptions grants access
-to the same features available in GitLab self-hosted, **expect
+to the same features available in GitLab self-hosted, **except
[administration](administration/index.md) tools and settings**:
- GitLab.com Free includes the same features available in Core
diff --git a/doc/administration/external_database.md b/doc/administration/external_database.md
new file mode 100644
index 00000000000..31199f2cdc7
--- /dev/null
+++ b/doc/administration/external_database.md
@@ -0,0 +1,17 @@
+# Configure GitLab using an external PostgreSQL service
+
+If you're hosting GitLab on a cloud provider, you can optionally use a
+managed service for PostgreSQL. For example, AWS offers a managed Relational
+Database Service (RDS) that runs PostgreSQL.
+
+Alternatively, you may opt to manage your own PostgreSQL instance or cluster
+separate from the GitLab Omnibus package.
+
+If you use a cloud-managed service, or provide your own PostgreSQL instance:
+
+1. Setup PostgreSQL according to the
+ [database requirements document](../install/requirements.md#database).
+1. Set up a `gitlab` username with a password of your choice. The `gitlab` user
+ needs privileges to create the `gitlabhq_production` database.
+1. Configure the GitLab application servers with the appropriate details.
+ This step is covered in [Configuring GitLab for HA](high_availability/gitlab.md).
diff --git a/doc/administration/index.md b/doc/administration/index.md
index 5551a04959c..df935095e61 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -1,3 +1,7 @@
+---
+description: 'Learn how to install, configure, update, and maintain your GitLab instance.'
+---
+
# Administrator documentation **[CORE ONLY]**
Learn how to administer your GitLab instance (Community Edition and
diff --git a/doc/administration/job_traces.md b/doc/administration/job_traces.md
index 84a1ffeec98..f0b2054a7f3 100644
--- a/doc/administration/job_traces.md
+++ b/doc/administration/job_traces.md
@@ -40,3 +40,98 @@ To change the location where the job logs will be stored, follow the steps below
[reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure "How to reconfigure Omnibus GitLab"
[restart gitlab]: restart_gitlab.md#installations-from-source "How to restart GitLab"
+
+## New live trace architecture
+
+> [Introduced][ce-18169] in GitLab 10.4.
+
+> **Notes**:
+- This feature is still Beta, which could impact GitLab.com/on-premises instances, and in the worst case scenario, traces will be lost.
+- This feature is still being discussed in [an issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/46097) for the performance improvements.
+- This feature is off by default. Please check below how to enable/disable this featrue.
+
+**What is "live trace"?**
+
+Job trace that is sent by runner while jobs are running. You can see live trace in job pages UI.
+The live traces are archived once job finishes.
+
+**What is new architecture?**
+
+So far, when GitLab Runner sends a job trace to GitLab-Rails, traces have been saved to file storage as text files.
+This was a problem for [Cloud Native-compatible GitLab application](https://gitlab.com/gitlab-com/migration/issues/23) where GitLab had to rely on File Storage.
+
+This new live trace architecture stores chunks of traces in Redis and database instead of file storage.
+Redis is used as first-class storage, and it stores up-to 128kB. Once the full chunk is sent it will be flushed to database. Afterwhile, the data in Redis and database will be archived to ObjectStorage.
+
+Here is the detailed data flow.
+
+1. GitLab Runner picks a job from GitLab-Rails
+1. GitLab Runner sends a piece of trace to GitLab-Rails
+1. GitLab-Rails appends the data to Redis
+1. If the data in Redis is fulfilled 128kB, the data is flushed to Database.
+1. 2.~4. is continued until the job is finished
+1. Once the job is finished, GitLab-Rails schedules a sidekiq worker to archive the trace
+1. The sidekiq worker archives the trace to Object Storage, and cleanup the trace in Redis and Database
+
+**How to check if it's on or off?**
+
+```ruby
+Feature.enabled?('ci_enable_live_trace')
+```
+
+**How to enable?**
+
+```ruby
+Feature.enable('ci_enable_live_trace')
+```
+
+>**Note:**
+The transition period will be handled gracefully. Upcoming traces will be generated with the new architecture, and on-going live traces will stay with the legacy architecture (i.e. on-going live traces won't be re-generated forcibly with the new architecture).
+
+**How to disable?**
+
+```ruby
+Feature.disable('ci_enable_live_trace')
+```
+
+>**Note:**
+The transition period will be handled gracefully. Upcoming traces will be generated with the legacy architecture, and on-going live traces will stay with the new architecture (i.e. on-going live traces won't be re-generated forcibly with the legacy architecture).
+
+**Redis namespace:**
+
+`Gitlab::Redis::SharedState`
+
+**Potential impact:**
+
+- This feature could incur data loss:
+ - Case 1: When all data in Redis are accidentally flushed.
+ - On-going live traces could be recovered by re-sending traces (This is supported by all versions of GitLab Runner)
+ - Finished jobs which has not archived live traces will lose the last part (~128kB) of trace data.
+ - Case 2: When sidekiq workers failed to archive (e.g. There was a bug that prevents archiving process, Sidekiq inconsistancy, etc):
+ - Currently all trace data in Redis will be deleted after one week. If the sidekiq workers can't finish by the expiry date, the part of trace data will be lost.
+- This feature could consume all memory on Redis instance. If the number of jobs is 1000, 128MB (128kB * 1000) is consumed.
+- This feature could pressure Database replication lag. `INSERT` are generated to indicate that we have trace chunk. `UPDATE` with 128kB of data is issued once we receive multiple chunks.
+- and so on
+
+**How to test?**
+
+We're currently evaluating this feature on dev.gitalb.org or staging.gitlab.com to verify this features. Here is the list of tests/measurements.
+
+- Features:
+ - Live traces should be visible on job pages
+ - Archived traces should be visible on job pages
+ - Live traces should be archived to Object storage
+ - Live traces should be cleaned up after archived
+ - etc
+- Performance:
+ - Schedule 1000~10000 jobs and let GitLab-runners process concurrently. Measure memoery presssure, IO load, etc.
+ - etc
+- Failover:
+ - Simulate Redis outage
+ - etc
+
+**How to verify the correctnesss?**
+
+- TBD
+
+[ce-44935]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18169
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
index 9b3b1e48efd..c0221533f13 100644
--- a/doc/administration/pages/index.md
+++ b/doc/administration/pages/index.md
@@ -1,3 +1,7 @@
+---
+description: 'Learn how to administer GitLab Pages.'
+---
+
# GitLab Pages administration
> **Notes:**
@@ -8,8 +12,6 @@
GitLab from source, follow the [Pages source installation document](source.md).
- To learn how to use GitLab Pages, read the [user documentation][pages-userguide].
----
-
This document describes how to set up the _latest_ GitLab Pages feature. Make
sure to read the [changelog](#changelog) if you are upgrading to a new GitLab
version as it may include new features and changes needed to be made in your
@@ -24,8 +26,6 @@ SNI and exposes pages using HTTP2 by default.
You are encouraged to read its [README][pages-readme] to fully understand how
it works.
----
-
In the case of [custom domains](#custom-domains) (but not
[wildcard domains](#wildcard-domains)), the Pages daemon needs to listen on
ports `80` and/or `443`. For that reason, there is some flexibility in the way
diff --git a/doc/administration/raketasks/project_import_export.md b/doc/administration/raketasks/project_import_export.md
index 39b1883375e..ecc4ac6b29b 100644
--- a/doc/administration/raketasks/project_import_export.md
+++ b/doc/administration/raketasks/project_import_export.md
@@ -1,4 +1,4 @@
-# Project import/export
+# Project import/export administration **[CORE ONLY]**
>**Note:**
>
diff --git a/doc/administration/repository_checks.md b/doc/administration/repository_checks.md
index ee37ea49874..efeec9db517 100644
--- a/doc/administration/repository_checks.md
+++ b/doc/administration/repository_checks.md
@@ -13,12 +13,12 @@ checks failed you can see their output on the admin log page under
## Periodic checks
-When enabled, GitLab periodically runs a repository check on all project
-repositories and wiki repositories in order to detect data corruption problems.
+When enabled, GitLab periodically runs a repository check on all project
+repositories and wiki repositories in order to detect data corruption.
A project will be checked no more than once per month. If any projects
fail their repository checks all GitLab administrators will receive an email
-notification of the situation. This notification is sent out once a week on
-Sunday, by default.
+notification of the situation. This notification is sent out once a week,
+by default, midnight at the start of Sunday.
## Disabling periodic checks
@@ -28,16 +28,18 @@ panel.
## What to do if a check failed
If the repository check fails for some repository you should look up the error
-in repocheck.log (in the admin panel or on disk; see
-`/var/log/gitlab/gitlab-rails` for Omnibus installations or
-`/home/git/gitlab/log` for installations from source). Once you have
-resolved the issue use the admin panel to trigger a new repository check on
-the project. This will clear the 'check failed' state.
+in `repocheck.log`:
+
+- in the [admin panel](logs.md#repocheck.log)
+- or on disk, see:
+ - `/var/log/gitlab/gitlab-rails` for Omnibus installations
+ - `/home/git/gitlab/log` for installations from source
If for some reason the periodic repository check caused a lot of false
-alarms you can choose to clear ALL repository check states from the
-'Settings' page of the admin panel.
+alarms you can choose to clear *all* repository check states by
+clicking "Clear all repository checks" on the **Settings** page of the
+admin panel (`/admin/application_settings`).
---
[ce-3232]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3232 "Auto git fsck"
-[git-fsck]: https://www.kernel.org/pub/software/scm/git/docs/git-fsck.html "git fsck documentation"
+[git-fsck]: https://git-scm.com/docs/git-fsck "git fsck documentation"
diff --git a/doc/api/group_milestones.md b/doc/api/group_milestones.md
index 21d3ac73000..152929b7614 100644
--- a/doc/api/group_milestones.md
+++ b/doc/api/group_milestones.md
@@ -22,7 +22,7 @@ Parameters:
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `iids[]` | Array[integer] | optional | Return only the milestones having the given `iid` |
-| `state` | string | optional | Return only `active` or `closed` milestones` |
+| `state` | string | optional | Return only `active` or `closed` milestones |
| `search` | string | optional | Return only milestones with a title or description matching the provided string |
```bash
diff --git a/doc/api/jobs.md b/doc/api/jobs.md
index db4fe2f6880..e4e48edd9a7 100644
--- a/doc/api/jobs.md
+++ b/doc/api/jobs.md
@@ -82,7 +82,7 @@ Example of response
"artifacts_file": null,
"finished_at": "2015-12-24T17:54:24.921Z",
"id": 6,
- "name": "spinach:other",
+ "name": "rspec:other",
"pipeline": {
"id": 6,
"ref": "master",
@@ -196,7 +196,7 @@ Example of response
"artifacts_file": null,
"finished_at": "2015-12-24T17:54:24.921Z",
"id": 6,
- "name": "spinach:other",
+ "name": "rspec:other",
"pipeline": {
"id": 6,
"ref": "master",
diff --git a/doc/api/runners.md b/doc/api/runners.md
index f384ac57bfe..3ca07ce9795 100644
--- a/doc/api/runners.md
+++ b/doc/api/runners.md
@@ -411,3 +411,86 @@ DELETE /projects/:id/runners/:runner_id
```
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/9/runners/9"
```
+
+## Register a new Runner
+
+Register a new Runner for the instance.
+
+```
+POST /runners
+```
+
+| Attribute | Type | Required | Description |
+|-------------|---------|----------|---------------------|
+| `token` | string | yes | Registration token ([Read how to obtain a token](../ci/runners/README.md)) |
+| `description`| string | no | Runner's description|
+| `info` | hash | no | Runner's metadata |
+| `active` | boolean| no | Whether the Runner is active |
+| `locked` | boolean| no | Whether the Runner should be locked for current project |
+| `run_untagged` | boolean | no | Whether the Runner should handle untagged jobs |
+| `tag_list` | Array[String] | no | List of Runner's tags |
+| `maximum_timeout` | integer | no | Maximum timeout set when this Runner will handle the job |
+
+```
+curl --request POST "https://gitlab.example.com/api/v4/runners" --form "token=ipzXrMhuyyJPifUt6ANz" --form "description=test-1-20150125-test" --form "tag_list=ruby,mysql,tag1,tag2"
+```
+
+Response:
+
+| Status | Description |
+|-----------|---------------------------------|
+| 201 | Runner was created |
+
+Example response:
+
+```json
+{
+ "id": "12345",
+ "token": "6337ff461c94fd3fa32ba3b1ff4125"
+}
+```
+
+## Delete a registered Runner
+
+Deletes a registed Runner.
+
+```
+DELETE /runners
+```
+
+| Attribute | Type | Required | Description |
+|-------------|---------|----------|---------------------|
+| `token` | string | yes | Runner's authentication token |
+
+```
+curl --request DELETE "https://gitlab.example.com/api/v4/runners" --form "token=ebb6fc00521627750c8bb750f2490e"
+```
+
+Response:
+
+| Status | Description |
+|-----------|---------------------------------|
+| 204 | Runner was deleted |
+
+## Verify authentication for a registered Runner
+
+Validates authentication credentials for a registered Runner.
+
+```
+POST /runners/verify
+```
+
+| Attribute | Type | Required | Description |
+|-------------|---------|----------|---------------------|
+| `token` | string | yes | Runner's authentication token |
+
+```
+curl --request POST "https://gitlab.example.com/api/v4/runners/verify" --form "token=ebb6fc00521627750c8bb750f2490e"
+```
+
+Response:
+
+| Status | Description |
+|-----------|---------------------------------|
+| 200 | Credentials are valid |
+| 403 | Credentials are invalid |
diff --git a/doc/api/services.md b/doc/api/services.md
index 92f12acbc73..ec632125325 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -968,7 +968,7 @@ Group Chat Software
Set Microsoft Teams service for a project.
```
-PUT /projects/:id/services/microsoft_teams
+PUT /projects/:id/services/microsoft-teams
```
Parameters:
@@ -982,7 +982,7 @@ Parameters:
Delete Microsoft Teams service for a project.
```
-DELETE /projects/:id/services/microsoft_teams
+DELETE /projects/:id/services/microsoft-teams
```
### Get Microsoft Teams service settings
@@ -990,7 +990,7 @@ DELETE /projects/:id/services/microsoft_teams
Get Microsoft Teams service settings for a project.
```
-GET /projects/:id/services/microsoft_teams
+GET /projects/:id/services/microsoft-teams
```
## Mattermost notifications
diff --git a/doc/articles/openshift_and_gitlab/index.md b/doc/articles/openshift_and_gitlab/index.md
index b7594cfef7f..76fdb2eb00a 100644
--- a/doc/articles/openshift_and_gitlab/index.md
+++ b/doc/articles/openshift_and_gitlab/index.md
@@ -1 +1,4 @@
-This document was moved to [another location](../../install/openshift_and_gitlab/index.html).
+---
+redirect_to: '../../install/openshift_and_gitlab/index.html'
+---
+
diff --git a/doc/ci/README.md b/doc/ci/README.md
index 6aa0e5885db..8d1d72c2a2b 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -1,5 +1,6 @@
---
comments: false
+description: "Learn how to use GitLab CI/CD, the GitLab built-in Continuous Integration, Continuous Deployment, and Continuous Delivery toolset to build, test, and deploy your application."
---
# GitLab Continuous Integration (GitLab CI/CD)
diff --git a/doc/ci/examples/container_scanning.md b/doc/ci/examples/container_scanning.md
index eb76521cc02..a9501f6c577 100644
--- a/doc/ci/examples/container_scanning.md
+++ b/doc/ci/examples/container_scanning.md
@@ -23,7 +23,7 @@ sast:container:
- docker:stable-dind
script:
- docker run -d --name db arminc/clair-db:latest
- - docker run -p 6060:6060 --link db:postgres -d --name clair arminc/clair-local-scan:v2.0.1
+ - docker run -p 6060:6060 --link db:postgres -d --name clair --restart on-failure arminc/clair-local-scan:v2.0.1
- apk add -U wget ca-certificates
- docker pull ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG}
- wget https://github.com/arminc/clair-scanner/releases/download/v8/clair-scanner_linux_amd64
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index fec0ff87326..47e658f610e 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -151,7 +151,8 @@ The next step is to configure a Runner so that it picks the pending jobs.
In GitLab, Runners run the jobs that you define in `.gitlab-ci.yml`. A Runner
can be a virtual machine, a VPS, a bare-metal machine, a docker container or
even a cluster of containers. GitLab and the Runners communicate through an API,
-so the only requirement is that the Runner's machine has [Internet] access.
+so the only requirement is that the Runner's machine has network access to the
+GitLab server.
A Runner can be specific to a certain project or serve multiple projects in
GitLab. If it serves all projects it's called a _Shared Runner_.
@@ -226,4 +227,3 @@ CI with various languages.
[enabled]: ../enable_or_disable_ci.md
[stages]: ../yaml/README.md#stages
[pipeline]: ../pipelines.md
-[internet]: https://about.gitlab.com/images/theinternet.png
diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md
index 821413900fd..703a7f030ed 100644
--- a/doc/ci/runners/README.md
+++ b/doc/ci/runners/README.md
@@ -11,7 +11,7 @@ Ideally, the GitLab Runner should not be installed on the same machine as GitLab
Read the [requirements documentation](../../install/requirements.md#gitlab-runner)
for more information.
-## Shared vs specific Runners
+## Shared, specific and group Runners
After [installing the Runner][install], you can either register it as shared or
specific. You can only register a shared Runner if you have admin access to
@@ -32,6 +32,9 @@ are:
Runners. For example, if you want to deploy a certain project, you can setup
a specific Runner to have the right credentials for this. The [usage of tags](#using-tags)
may be useful in this case. Specific Runners process jobs using a [FIFO] queue.
+- **Group Runners** are useful when you have multiple projects under one group
+ and would like all projects to have access to a set of Runners. Group Runners
+ process jobs using a [FIFO] queue.
A Runner that is specific only runs for the specified project(s). A shared Runner
can run jobs for every project that has enabled the option **Allow shared Runners**
@@ -66,7 +69,7 @@ Runners to disabled.
## Registering a specific Runner
-Registering a specific can be done in two ways:
+Registering a specific Runner can be done in two ways:
1. Creating a Runner with the project registration token
1. Converting a shared Runner into a specific Runner (one-way, admin only)
@@ -79,6 +82,14 @@ visit the project you want to make the Runner work for in GitLab:
1. Go to **Settings > CI/CD** to obtain the token
1. [Register the Runner][register]
+## Registering a group Runner
+
+Creating a group Runner requires Master permissions for the group. To create a
+group Runner visit the group you want to make the Runner work for in GitLab:
+
+1. Go to **Settings > CI/CD** to obtain the token
+1. [Register the Runner][register]
+
### Making an existing shared Runner specific
If you are an admin on your GitLab instance, you can turn any shared Runner into
@@ -121,7 +132,7 @@ To enable/disable a Runner in your project:
> **Note**:
Consider that if you don't lock your specific Runner to a specific project, any
-user with Master role in you project can assign your runner to another arbitrary
+user with Master role in you project can assign your Runner to another arbitrary
project without requiring your authorization, so use it with caution.
An admin can enable/disable a specific Runner for projects:
diff --git a/doc/ci/services/docker-services.md b/doc/ci/services/docker-services.md
index 787c5e462e4..e5fc7a3c85f 100644
--- a/doc/ci/services/docker-services.md
+++ b/doc/ci/services/docker-services.md
@@ -1,9 +1,3 @@
---
-comments: false
+redirect_to: 'README.md'
---
-
-# GitLab CI Services
-
-- [Using MySQL](mysql.md)
-- [Using PostgreSQL](postgres.md)
-- [Using Redis](redis.md)
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 38a988f4507..42367bf13f7 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -41,6 +41,9 @@ future GitLab releases.**
| **CI_COMMIT_REF_SLUG** | 9.0 | all | `$CI_COMMIT_REF_NAME` lowercased, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. No leading / trailing `-`. Use in URLs, host names and domain names. |
| **CI_COMMIT_SHA** | 9.0 | all | The commit revision for which project is built |
| **CI_COMMIT_TAG** | 9.0 | 0.5 | The commit tag name. Present only when building tags. |
+| **CI_COMMIT_MESSAGE** | 10.8 | all | The full commit message. |
+| **CI_COMMIT_TITLE** | 10.8 | all | The title of the commit - the full first line of the message |
+| **CI_COMMIT_DESCRIPTION** | 10.8 | all | The description of the commit: the message without first line, if the title is shorter than 100 characters; full message in other case. |
| **CI_CONFIG_PATH** | 9.4 | 0.5 | The path to CI config file. Defaults to `.gitlab-ci.yml` |
| **CI_DEBUG_TRACE** | all | 1.7 | Whether [debug tracing](#debug-tracing) is enabled |
| **CI_DISPOSABLE_ENVIRONMENT** | all | 10.1 | Marks that the job is executed in a disposable environment (something that is created only for this job and disposed of/destroyed after the execution - all executors except `shell` and `ssh`). If the environment is disposable, it is set to true, otherwise it is not defined at all. |
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index fb6d9826d08..2a17a51d7f8 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -738,10 +738,15 @@ cache:
rspec:
script: test
cache:
+ key: rspec
paths:
- binaries/
```
+Note that since cache is shared between jobs, if you're using different
+paths for different jobs, you should also set a different **cache:key**
+otherwise cache content can be overwritten.
+
### `cache:key`
> Introduced in GitLab Runner v1.0.0.
@@ -756,10 +761,9 @@ or any other way that fits your workflow. This way, you can fine tune caching,
allowing you to cache data between different jobs or even different branches.
The `cache:key` variable can use any of the
-[predefined variables](../variables/README.md), and the default key, if not set,
-is `$CI_JOB_NAME-$CI_COMMIT_REF_NAME` which translates as "per-job and
-per-branch". It is the default across the project, therefore everything is
-shared between pipelines and jobs running on the same branch by default.
+[predefined variables](../variables/README.md), and the default key, if not
+set, is just literal `default` which means everything is shared between each
+pipelines and jobs by default, starting from GitLab 9.0.
NOTE: **Note:**
The `cache:key` variable cannot contain the `/` character, or the equivalent
@@ -779,7 +783,7 @@ If you use **Windows Batch** to run your shell scripts you need to replace
```yaml
cache:
- key: "%CI_JOB_STAGE%-%CI_COMMIT_REF_SLUG%"
+ key: "%CI_COMMIT_REF_SLUG%"
paths:
- binaries/
```
@@ -789,7 +793,7 @@ If you use **Windows PowerShell** to run your shell scripts you need to replace
```yaml
cache:
- key: "$env:CI_JOB_STAGE-$env:CI_COMMIT_REF_SLUG"
+ key: "$env:CI_COMMIT_REF_SLUG"
paths:
- binaries/
```
@@ -1572,7 +1576,7 @@ capitalization, the commit will be created but the pipeline will be skipped.
## Validate the .gitlab-ci.yml
Each instance of GitLab CI has an embedded debug tool called Lint, which validates the
-content of your `.gitlab-ci.yml` files. You can find the Lint under the page `ci/lint` of your
+content of your `.gitlab-ci.yml` files. You can find the Lint under the page `ci/lint` of your
project namespace (e.g, `http://gitlab-example.com/gitlab-org/project-123/-/ci/lint`)
## Using reserved keywords
diff --git a/doc/container_registry/README.md b/doc/container_registry/README.md
index fe3e4681ba7..5d2f5edcb18 100644
--- a/doc/container_registry/README.md
+++ b/doc/container_registry/README.md
@@ -1 +1 @@
-This document was moved in [user/project/container_registry](../user/project/container_registry.md).
+This document was moved to [another location](../user/project/container_registry.md).
diff --git a/doc/customization/issue_closing.md b/doc/customization/issue_closing.md
index d14ba6ad522..680c51e7524 100644
--- a/doc/customization/issue_closing.md
+++ b/doc/customization/issue_closing.md
@@ -1,8 +1,3 @@
---
-comments: false
+redirect_to: '../user/project/issues/automatic_issue_closing.md'
---
-
-This document was split into:
-
-- [administration/issue_closing_pattern.md](../administration/issue_closing_pattern.md).
-- [user/project/issues/automatic_issue_closing](../user/project/issues/automatic_issue_closing.md).
diff --git a/doc/development/README.md b/doc/development/README.md
index 3c77e99b8cf..898c60e96c0 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -1,5 +1,6 @@
---
comments: false
+description: 'Learn how to contribute to GitLab.'
---
# GitLab development guides
@@ -18,6 +19,7 @@ comments: false
- [Code review guidelines](code_review.md) for reviewing code and having code reviewed.
- [Automatic CE->EE merge](automatic_ce_ee_merge.md)
- [Guidelines for implementing Enterprise Edition features](ee_features.md)
+- [Security process for developers](https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md#security-releases-critical-non-critical-as-a-developer)
## UX and frontend guides
diff --git a/doc/development/code_review.md b/doc/development/code_review.md
index 7165b8062a7..d03b7fa23ca 100644
--- a/doc/development/code_review.md
+++ b/doc/development/code_review.md
@@ -29,6 +29,10 @@ There are a few rules to get your merge request accepted:
to ask one of the [Merge request coaches][team].
1. The reviewer will assign the merge request to a maintainer once the
reviewer is satisfied with the state of the merge request.
+1. Keep in mind that maintainers are also going to perform a final code review.
+ The ideal scenario is that the reviewer has already addressed any concerns
+ the maintainer would have found, and the maintainer only has to perform the
+ merge, but be prepared for further review comments.
For more guidance, see [CONTRIBUTING.md](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md).
@@ -207,3 +211,4 @@ Largely based on the [thoughtbot code review guide].
[projects]: https://about.gitlab.com/handbook/engineering/projects/
[team]: https://about.gitlab.com/team/
[build handbook]: https://about.gitlab.com/handbook/build/handbook/build#how-to-work-with-build
+[^1]: Please note that specs other than JavaScript specs are considered backend code.
diff --git a/doc/development/database_debugging.md b/doc/development/database_debugging.md
index 32f392f1303..9c31265e417 100644
--- a/doc/development/database_debugging.md
+++ b/doc/development/database_debugging.md
@@ -11,7 +11,7 @@ Available `RAILS_ENV`
- `production` (generally not for your main GDK db, but you may need this for e.g. omnibus)
- `development` (this is your main GDK db)
- - `test` (used for tests like rspec and spinach)
+ - `test` (used for tests like rspec)
## Nuke everything and start over
diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md
index 5da015ca557..5d595c33915 100644
--- a/doc/development/doc_styleguide.md
+++ b/doc/development/doc_styleguide.md
@@ -1,3 +1,7 @@
+---
+description: 'Writing styles, markup, formatting, and reusing regular expressions throughout the GitLab Documentation.'
+---
+
# Documentation style guidelines
The documentation style guide defines the markup structure used in
@@ -21,23 +25,39 @@ Check the GitLab handbook for the [writing styles guidelines](https://about.gitl
- Use [single spaces][] instead of double spaces
- Jump a line between different markups (e.g., after every paragraph, header, list, etc)
- Capitalize "G" and "L" in GitLab
-- Capitalize feature, products, and methods names. E.g.: GitLab Runner, Geo,
-Issue Boards, Git, Prometheus, Continuous Integration.
+- Use sentence case for titles, headings, labels, menu items, and buttons.
+- Use title case when referring to [features](https://about.gitlab.com/features/) or [products](https://about.gitlab.com/pricing/), and methods. Note that some features are also objects (e.g. "Merge Requests" and "merge requests"). E.g.: GitLab Runner, Geo, Issue Boards, Git, Prometheus, Continuous Integration.
## Formatting
+- Use double asterisks (`**`) to mark a word or text in bold (`**bold**`)
+- Use undescore (`_`) for text in italics (`_italic_`)
+- Jump a line between different markups, for example:
+
+ ```md
+ ## Header
+
+ Paragraph.
+
+ - List item
+ - List item
+ ```
+
+### Punctuation
+
+For punctuation rules, please refer to the [GitLab UX guide](https://design.gitlab.com/content/punctuation/).
+
+### Ordered and unordered lists
+
- Use dashes (`-`) for unordered lists instead of asterisks (`*`)
- Use the number one (`1`) for ordered lists
-- Use underscores (`_`) to mark a word or text in italics
-- Use double asterisks (`**`) to mark a word or text in bold
-- When using lists, prefer not to end each item with a period. You can use
- them if there are multiple sentences, just keep the last sentence without
- a period
+- For punctuation in bullet lists, please refer to the [GitLab UX guide](https://design.gitlab.com/content/punctuation/)
## Headings
-- Add only one H1 title in each document, by adding `#` at the beginning of
- it (when using markdown). For subheadings, use `##`, `###` and so on
+- Add **only one H1** in each document, by adding `#` at the beginning of
+ it (when using markdown). The `h1` will be the document `<title>`.
+- For subheadings, use `##`, `###` and so on
- Avoid putting numbers in headings. Numbers shift, hence documentation anchor
links shift too, which eventually leads to dead links. If you think it is
compelling to add numbers in headings, make sure to at least discuss it with
@@ -106,21 +126,75 @@ Inside the document:
- If a heading is placed right after an image, always add three dashes (`---`)
between the image and the heading
-## Notes
+## Alert boxes
-- Notes should be quoted with the word `Note:` being bold. Use this form:
+Whenever you want to call the attention to a particular sentence,
+use the following markup for highlighting.
- ```md
- >**Note:**
- This is something to note.
- ```
+_Note that the alert boxes only work for one paragraph only. Multiple paragraphs,
+lists, headers, etc will not render correctly._
+
+### Note
+
+```md
+NOTE: **Note:**
+This is something to note.
+```
+
+How it renders in docs.gitlab.com:
+
+NOTE: **Note:**
+This is something to note.
+
+### Tip
+
+```md
+TIP: **Tip:**
+This is a tip.
+```
+
+How it renders in docs.gitlab.com:
+
+TIP: **Tip:**
+This is a tip.
+
+### Caution
+
+```md
+CAUTION: **Caution:**
+This is something to be cautious about.
+```
+
+How it renders in docs.gitlab.com:
+
+CAUTION: **Caution:**
+This is something to be cautious about.
+
+### Danger
- which renders to:
+```md
+DANGER: **Danger:**
+This is a breaking change, a bug, or something very important to note.
+```
+
+How it renders in docs.gitlab.com:
- >**Note:**
- This is something to note.
+DANGER: **Danger:**
+This is a breaking change, a bug, or something very important to note.
- If the note spans across multiple lines it's OK to split the line.
+## Blockquotes
+
+For highlighting a text within a blue blockquote, use this format:
+
+```md
+> This is a blockquote.
+```
+
+which renders in docs.gitlab.com to:
+
+> This is a blockquote.
+
+If the text spans across multiple lines it's OK to split the line.
## Specific sections and terms
@@ -137,7 +211,7 @@ below.
> Introduced in GitLab 8.3.
```
-- If possible every feature should have a link to the MR, issue, or epic that introduced it.
+- Whenever possible, every feature should have a link to the MR, issue, or epic that introduced it.
The above note would be then transformed to:
```md
@@ -152,11 +226,9 @@ below.
the feature is available in:
```md
- > [Introduced][ee-1234] in [GitLab Starter](https://about.gitlab.com/products/) 8.3.
+ > [Introduced][ee-1234] in [GitLab Starter](https://about.gitlab.com/pricing/) 8.3.
```
- Otherwise, leave this mention out.
-
### Product badges
When a feature is available in EE-only tiers, add the corresponding tier according to the
diff --git a/doc/development/fe_guide/development_process.md b/doc/development/fe_guide/development_process.md
index 5504a6421d5..d240dbe8b02 100644
--- a/doc/development/fe_guide/development_process.md
+++ b/doc/development/fe_guide/development_process.md
@@ -22,9 +22,9 @@ Please use your best judgement when to use it and please contribute new points t
- [ ] Are all [departments](https://about.gitlab.com/handbook/engineering/#engineering-teams) that are needed from your perspective already involved in the issue? (For example is UX missing?)
- [ ] Is the specification complete? Are you missing decisions? How about error handling/defaults/edge cases? Take your time to understand the needed implementation and go through its flow.
- [ ] Are all necessary UX specifications available that you will need in order to implement? Are there new UX components/patterns in the designs? Then contact the UI component team early on. How should error messages or validation be handled?
-- [ ] **Library usage** Use Vuex as soon as you have even a medium state to manage, use Vue router if you need to have different views internally and want to link from the outside. Check what libraries we already have for which occassions.
+- [ ] **Library usage** Use Vuex as soon as you have even a medium state to manage, use Vue router if you need to have different views internally and want to link from the outside. Check what libraries we already have for which occasions.
- [ ] **Plan your implementation:**
- - [ ] **Architecture plan:** Create a plan aligned with GitLab's architecture, how you are going to do the implementation, for example Vue application setup and its components (through [onion skinning](https://gitlab.com/gitlab-org/gitlab-ce/issues/35873#note_39994091)), Store structure and data flow, which existing Vue components can you reuse. Its a good idea to go through your plan with another engineer to refine it.
+ - [ ] **Architecture plan:** Create a plan aligned with GitLab's architecture, how you are going to do the implementation, for example Vue application setup and its components (through [onion skinning](https://gitlab.com/gitlab-org/gitlab-ce/issues/35873#note_39994091)), Store structure and data flow, which existing Vue components can you reuse. It's a good idea to go through your plan with another engineer to refine it.
- [ ] **Backend:** The best way is to kickoff the implementation in a call and discuss with the assigned Backend engineer what you will need from the backend and also when. Can you reuse existing API's? How is the performance with the planned architecture? Maybe create together a JSON mock object to already start with development.
- [ ] **Communication:** It also makes sense to have for bigger features an own slack channel (normally called #f_{feature_name}) and even weekly demo calls with all people involved.
- [ ] **Dependency Plan:** Are there big dependencies in the plan between you and others, then maybe create an execution diagram to show what is blocking which part and the order of the different parts.
@@ -56,7 +56,7 @@ Please use your best judgement when to use it and please contribute new points t
- [ ] If you have multiple MR's then also smoke test against the final merge.
- [ ] Are there any big changes on how and especially how frequently we use the API then let production know about it
- [ ] Smoke test of the RC on dev., staging., canary deployments and .com
-- [ ] Follow up on issues that came out of the review. Create isssues for discovered edge cases that should be covered in future iterations.
+- [ ] Follow up on issues that came out of the review. Create issues for discovered edge cases that should be covered in future iterations.
---
diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md
index 3b4dfd50761..6d3796e7560 100644
--- a/doc/development/fe_guide/index.md
+++ b/doc/development/fe_guide/index.md
@@ -1,5 +1,8 @@
# Frontend Development Guidelines
+> **Notice:**
+We are currently in the process of re-writing our development guide to make it easier to find information. The new guide is still WIP but viewable in [development/new_fe_guide](../new_fe_guide/index.md)
+
This document describes various guidelines to ensure consistency and quality
across GitLab's frontend team.
@@ -45,6 +48,9 @@ Common JavaScript design patterns in GitLab's codebase.
## [Vue.js Best Practices](vue.md)
Vue specific design patterns and practices.
+## [Vuex](vuex.md)
+Vuex specific design patterns and practices.
+
## [Axios](axios.md)
Axios specific practices and gotchas.
diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md
index 9c4b0e86351..f971d8b7388 100644
--- a/doc/development/fe_guide/vue.md
+++ b/doc/development/fe_guide/vue.md
@@ -1,29 +1,7 @@
# Vue
-For more complex frontend features, we recommend using Vue.js. It shares
-some ideas with React.js as well as Angular.
-
To get started with Vue, read through [their documentation][vue-docs].
-## When to use Vue.js
-
-We recommend using Vue for more complex features. Here are some guidelines for when to use Vue.js:
-
-- If you are starting a new feature or refactoring an old one that highly interacts with the DOM;
-- For real time data updates;
-- If you are creating a component that will be reused elsewhere;
-
-## When not to use Vue.js
-
-We don't want to refactor all GitLab frontend code into Vue.js, here are some guidelines for
-when not to use Vue.js:
-
-- Adding or changing static information;
-- Features that highly depend on jQuery will be hard to work with Vue.js;
-- Features without reactive data;
-
-As always, the Frontend Architectural Experts are available to help with any Vue or JavaScript questions.
-
## Vue architecture
All new features built with Vue.js must follow a [Flux architecture][flux].
@@ -57,15 +35,15 @@ new_feature
│ └── ...
├── stores
│ └── new_feature_store.js
-├── services
+├── services # only when not using vuex
│ └── new_feature_service.js
-├── new_feature_bundle.js
+├── index.js
```
_For consistency purposes, we recommend you to follow the same structure._
Let's look into each of them:
-### A `*_bundle.js` file
+### A `index.js` file
This is the index file of your new feature. This is where the root Vue instance
of the new feature should be.
@@ -144,30 +122,30 @@ in one table would not be a good use of this pattern.
You can read more about components in Vue.js site, [Component System][component-system]
#### Components Gotchas
-1. Using SVGs in components: To use an SVG in a template we need to make it a property we can access through the component.
-A `prop` and a property returned by the `data` functions require `vue` to set a `getter` and a `setter` for each of them.
-The SVG should be a computed property in order to improve performance, note that computed properties are cached based on their dependencies.
-
-```javascript
-// bad
-import svg from 'svg.svg';
-data() {
- return {
- myIcon: svg,
- };
-};
-
-// good
-import svg from 'svg.svg';
-computed: {
- myIcon() {
- return svg;
- }
-}
-```
+1. Using SVGs icons in components: To use an SVG icon in a template use the `icon.vue`
+1. Using SVGs illustrations in components: To use an SVG illustrations in a template provide the path as a prop and display it through a standard img tag.
+ ```javascript
+ <script>
+ export default {
+ props: {
+ svgIllustrationPath: {
+ type: String,
+ required: true,
+ },
+ },
+ };
+ <script>
+ <template>
+ <img :src="svgIllustrationPath" />
+ </template>
+ ```
### A folder for the Store
+#### Vuex
+Check this [page](vuex.md) for more details.
+
+#### Flux like state management
The Store is a class that allows us to manage the state in a single
source of truth. It is not aware of the service or the components.
@@ -176,6 +154,8 @@ itself, please read this guide: [State Management][state-management]
### A folder for the Service
+**If you are using Vuex you won't need this step**
+
The Service is a class used only to communicate with the server.
It does not store or manipulate any data. It is not aware of the store or the components.
We use [axios][axios] to communicate with the server.
@@ -273,6 +253,9 @@ import Store from 'store';
import Service from 'service';
import TodoComponent from 'todoComponent';
export default {
+ components: {
+ todo: TodoComponent,
+ },
/**
* Although most data belongs in the store, each component it's own state.
* We want to show a loading spinner while we are fetching the todos, this state belong
@@ -291,10 +274,6 @@ export default {
};
},
- components: {
- todo: TodoComponent,
- },
-
created() {
this.service = new Service('todos');
@@ -476,201 +455,6 @@ need to test the rendered output. [Vue][vue-test] guide's to unit test show us e
Refer to [mock axios](axios.md#mock-axios-response-on-tests)
-## Vuex
-To manage the state of an application you may use [Vuex][vuex-docs].
-
-_Note:_ All of the below is explained in more detail in the official [Vuex documentation][vuex-docs].
-
-### Separation of concerns
-Vuex is composed of State, Getters, Mutations, Actions and Modules.
-
-When a user clicks on an action, we need to `dispatch` it. This action will `commit` a mutation that will change the state.
-_Note:_ The action itself will not update the state, only a mutation should update the state.
-
-#### File structure
-When using Vuex at GitLab, separate this concerns into different files to improve readability. If you can, separate the Mutation Types as well:
-
-```
-└── store
- ├── index.js # where we assemble modules and export the store
- ├── actions.js # actions
- ├── mutations.js # mutations
- ├── getters.js # getters
- └── mutation_types.js # mutation types
-```
-The following examples show an application that lists and adds users to the state.
-
-##### `index.js`
-This is the entry point for our store. You can use the following as a guide:
-
-```javascript
-import Vue from 'vue';
-import Vuex from 'vuex';
-import * as actions from './actions';
-import * as getters from './getters';
-import mutations from './mutations';
-
-Vue.use(Vuex);
-
-export default new Vuex.Store({
- actions,
- getters,
- mutations,
- state: {
- users: [],
- },
-});
-```
-_Note:_ If the state of the application is too complex, an individual file for the state may be better.
-
-##### `actions.js`
-An action commits a mutation. In this file, we will write the actions that will commit the respective mutation:
-
-```javascript
- import * as types from './mutation_types';
-
- export const addUser = ({ commit }, user) => {
- commit(types.ADD_USER, user);
- };
-```
-
-To dispatch an action from a component, use the `mapActions` helper:
-```javascript
-import { mapActions } from 'vuex';
-
-{
- methods: {
- ...mapActions([
- 'addUser',
- ]),
- onClickUser(user) {
- this.addUser(user);
- },
- },
-};
-```
-
-##### `getters.js`
-Sometimes we may need to get derived state based on store state, like filtering for a specific prop. This can be done through the `getters`:
-
-```javascript
-// get all the users with pets
-export getUsersWithPets = (state, getters) => {
- return state.users.filter(user => user.pet !== undefined);
-};
-```
-
-To access a getter from a component, use the `mapGetters` helper:
-```javascript
-import { mapGetters } from 'vuex';
-
-{
- computed: {
- ...mapGetters([
- 'getUsersWithPets',
- ]),
- },
-};
-```
-
-##### `mutations.js`
-The only way to actually change state in a Vuex store is by committing a mutation.
-
-```javascript
- import * as types from './mutation_types';
-
- export default {
- [types.ADD_USER](state, user) {
- state.users.push(user);
- },
- };
-```
-
-##### `mutations_types.js`
-From [vuex mutations docs][vuex-mutations]:
-> It is a commonly seen pattern to use constants for mutation types in various Flux implementations. This allows the code to take advantage of tooling like linters, and putting all constants in a single file allows your collaborators to get an at-a-glance view of what mutations are possible in the entire application.
-
-```javascript
-export const ADD_USER = 'ADD_USER';
-```
-
-### How to include the store in your application
-The store should be included in the main component of your application:
-```javascript
- // app.vue
- import store from 'store'; // it will include the index.js file
-
- export default {
- name: 'application',
- store,
- ...
- };
-```
-
-### Vuex Gotchas
-1. Avoid calling a mutation directly. Always use an action to commit a mutation. Doing so will keep consistency through out the application. From Vuex docs:
-
- > why don't we just call store.commit('action') directly? Well, remember that mutations must be synchronous? Actions aren't. We can perform asynchronous operations inside an action.
-
- ```javascript
- // component.vue
-
- // bad
- created() {
- this.$store.commit('mutation');
- }
-
- // good
- created() {
- this.$store.dispatch('action');
- }
- ```
-1. When possible, use mutation types instead of hardcoding strings. It will be less error prone.
-1. The State will be accessible in all components descending from the use where the store is instantiated.
-
-### Testing Vuex
-#### Testing Vuex concerns
-Refer to [vuex docs][vuex-testing] regarding testing Actions, Getters and Mutations.
-
-#### Testing components that need a store
-Smaller components might use `store` properties to access the data.
-In order to write unit tests for those components, we need to include the store and provide the correct state:
-
-```javascript
-//component_spec.js
-import Vue from 'vue';
-import store from './store';
-import component from './component.vue'
-
-describe('component', () => {
- let vm;
- let Component;
-
- beforeEach(() => {
- Component = Vue.extend(issueActions);
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('should show a user', () => {
- const user = {
- name: 'Foo',
- age: '30',
- };
-
- // populate the store
- store.dispatch('addUser', user);
-
- vm = new Component({
- store,
- propsData: props,
- }).$mount();
- });
-});
-```
-
[vue-docs]: http://vuejs.org/guide/index.html
[issue-boards]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/boards
[environments-table]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/environments
@@ -681,9 +465,5 @@ describe('component', () => {
[vue-test]: https://vuejs.org/v2/guide/unit-testing.html
[issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6
[flux]: https://facebook.github.io/flux
-[vuex-docs]: https://vuex.vuejs.org
-[vuex-structure]: https://vuex.vuejs.org/en/structure.html
-[vuex-mutations]: https://vuex.vuejs.org/en/mutations.html
-[vuex-testing]: https://vuex.vuejs.org/en/testing.html
[axios]: https://github.com/axios/axios
[axios-interceptors]: https://github.com/axios/axios#interceptors
diff --git a/doc/development/fe_guide/vuex.md b/doc/development/fe_guide/vuex.md
new file mode 100644
index 00000000000..6a89bfc7721
--- /dev/null
+++ b/doc/development/fe_guide/vuex.md
@@ -0,0 +1,358 @@
+# Vuex
+To manage the state of an application you should use [Vuex][vuex-docs].
+
+_Note:_ All of the below is explained in more detail in the official [Vuex documentation][vuex-docs].
+
+## Separation of concerns
+Vuex is composed of State, Getters, Mutations, Actions and Modules.
+
+When a user clicks on an action, we need to `dispatch` it. This action will `commit` a mutation that will change the state.
+_Note:_ The action itself will not update the state, only a mutation should update the state.
+
+## File structure
+When using Vuex at GitLab, separate this concerns into different files to improve readability:
+
+```
+└── store
+ ├── index.js # where we assemble modules and export the store
+ ├── actions.js # actions
+ ├── mutations.js # mutations
+ ├── getters.js # getters
+ ├── state.js # state
+ └── mutation_types.js # mutation types
+```
+The following example shows an application that lists and adds users to the state.
+(For a more complex example implementation take a look at the security applications store in [here](https://gitlab.com/gitlab-org/gitlab-ee/tree/master/ee/app/assets/javascripts/vue_shared/security_reports/store))
+
+### `index.js`
+This is the entry point for our store. You can use the following as a guide:
+
+```javascript
+import Vue from 'vue';
+import Vuex from 'vuex';
+import * as actions from './actions';
+import * as getters from './getters';
+import mutations from './mutations';
+import state from './state';
+
+Vue.use(Vuex);
+
+export default new Vuex.Store({
+ actions,
+ getters,
+ mutations,
+ state,
+});
+```
+
+### `state.js`
+The first thing you should do before writing any code is to design the state.
+
+Often we need to provide data from haml to our Vue application. Let's store it in the state for better access.
+
+```javascript
+ export default {
+ endpoint: null,
+
+ isLoading: false,
+ error: null,
+
+ isAddingUser: false,
+ errorAddingUser: false,
+
+ users: [],
+ };
+```
+
+#### Access `state` properties
+You can use `mapState` to access state properties in the components.
+
+### `actions.js`
+An action is a playload of information to send data from our application to our store.
+
+An action is usually composed by a `type` and a `payload` and they describe what happened.
+Enforcing that every change is described as an action lets us have a clear understanting of what is going on in the app.
+
+In this file, we will write the actions that will call the respective mutations:
+
+```javascript
+ import * as types from './mutation_types';
+ import axios from '~/lib/utils/axios-utils';
+ import createFlash from '~/flash';
+
+ export const requestUsers = ({ commit }) => commit(types.REQUEST_USERS);
+ export const receiveUsersSuccess = ({ commit }, data) => commit(types.RECEIVE_USERS_SUCCESS, data);
+ export const receiveUsersError = ({ commit }, error) => commit(types.REQUEST_USERS_ERROR, error);
+
+ export const fetchUsers = ({ state, dispatch }) => {
+ dispatch('requestUsers');
+
+ axios.get(state.endoint)
+ .then(({ data }) => dispatch('receiveUsersSuccess', data))
+ .catch((error) => {
+ dispatch('receiveUsersError', error)
+ createFlash('There was an error')
+ });
+ }
+
+ export const requestAddUser = ({ commit }) => commit(types.REQUEST_ADD_USER);
+ export const receiveAddUserSuccess = ({ commit }, data) => commit(types.RECEIVE_ADD_USER_SUCCESS, data);
+ export const receiveAddUserError = ({ commit }, error) => commit(types.REQUEST_ADD_USER_ERROR, error);
+
+ export const addUser = ({ state, dispatch }, user) => {
+ dispatch('requestAddUser');
+
+ axios.post(state.endoint, user)
+ .then(({ data }) => dispatch('receiveAddUserSuccess', data))
+ .catch((error) => dispatch('receiveAddUserError', error));
+ }
+```
+
+#### Actions Pattern: `request` and `receive` namespaces
+When a request is made we often want to show a loading state to the user.
+
+Instead of creating an action to toggle the loading state and dispatch it in the component,
+create:
+1. An action `requestSomething`, to toggle the loading state
+1. An action `receiveSomethingSuccess`, to handle the success callback
+1. An action `receiveSomethingError`, to handle the error callback
+1. An action `fetchSomething` to make the request.
+ 1. In case your application does more than a `GET` request you can use these as examples:
+ 1. `PUT`: `createSomething`
+ 2. `POST`: `updateSomething`
+ 3. `DELETE`: `deleteSomething`
+
+The component MUST only dispatch the `fetchNamespace` action. Actions namespaced with `request` or `receive` should not be called from the component
+The `fetch` action will be responsible to dispatch `requestNamespace`, `receiveNamespaceSuccess` and `receiveNamespaceError`
+
+By following this pattern we guarantee:
+1. All aplications follow the same pattern, making it easier for anyone to maintain the code
+1. All data in the application follows the same lifecycle pattern
+1. Actions are contained and human friendly
+1. Unit tests are easier
+1. Actions are simple and straightforward
+
+#### Dispatching actions
+To dispatch an action from a component, use the `mapActions` helper:
+```javascript
+import { mapActions } from 'vuex';
+
+{
+ methods: {
+ ...mapActions([
+ 'addUser',
+ ]),
+ onClickUser(user) {
+ this.addUser(user);
+ },
+ },
+};
+```
+
+#### `mutations.js`
+The mutations specify how the application state changes in response to actions sent to the store.
+The only way to change state in a Vuex store should be by committing a mutation.
+
+**It's a good idea to think of the state before writing any code.**
+
+Remember that actions only describe that something happened, they don't describe how the application state changes.
+
+**Never commit a mutation directly from a component**
+
+```javascript
+ import * as types from './mutation_types';
+
+ export default {
+ [types.REQUEST_USERS](state) {
+ state.isLoading = true;
+ },
+ [types.RECEIVE_USERS_SUCCESS](state, data) {
+ // Do any needed data transformation to the received payload here
+ state.users = data;
+ state.isLoading = false;
+ },
+ [types.REQUEST_USERS_ERROR](state, error) {
+ state.isLoading = false;
+ },
+ [types.REQUEST_ADD_USER](state, user) {
+ state.isAddingUser = true;
+ },
+ [types.RECEIVE_ADD_USER_SUCCESS](state, user) {
+ state.isAddingUser = false;
+ state.users.push(user);
+ },
+ [types.REQUEST_ADD_USER_ERROR](state, error) {
+ state.isAddingUser = true;
+ state.errorAddingUser = error∂;
+ },
+ };
+```
+
+#### `getters.js`
+Sometimes we may need to get derived state based on store state, like filtering for a specific prop.
+Using a getter will also cache the result based on dependencies due to [how computed props work](https://vuejs.org/v2/guide/computed.html#Computed-Caching-vs-Methods)
+This can be done through the `getters`:
+
+```javascript
+// get all the users with pets
+export const getUsersWithPets = (state, getters) => {
+ return state.users.filter(user => user.pet !== undefined);
+};
+```
+
+To access a getter from a component, use the `mapGetters` helper:
+```javascript
+import { mapGetters } from 'vuex';
+
+{
+ computed: {
+ ...mapGetters([
+ 'getUsersWithPets',
+ ]),
+ },
+};
+```
+
+#### `mutations_types.js`
+From [vuex mutations docs][vuex-mutations]:
+> It is a commonly seen pattern to use constants for mutation types in various Flux implementations. This allows the code to take advantage of tooling like linters, and putting all constants in a single file allows your collaborators to get an at-a-glance view of what mutations are possible in the entire application.
+
+```javascript
+export const ADD_USER = 'ADD_USER';
+```
+
+### How to include the store in your application
+The store should be included in the main component of your application:
+```javascript
+ // app.vue
+ import store from 'store'; // it will include the index.js file
+
+ export default {
+ name: 'application',
+ store,
+ ...
+ };
+```
+
+### Communicating with the Store
+```javascript
+<script>
+import { mapActions, mapState, mapGetters } from 'vuex';
+import store from './store';
+
+export default {
+ store,
+ computed: {
+ ...mapGetters([
+ 'getUsersWithPets'
+ ]),
+ ...mapState([
+ 'isLoading',
+ 'users',
+ 'error',
+ ]),
+ },
+ methods: {
+ ...mapActions([
+ 'fetchUsers',
+ 'addUser',
+ ]),
+
+ onClickAddUser(data) {
+ this.addUser(data);
+ }
+ },
+
+ created() {
+ this.fetchUsers()
+ }
+}
+</script>
+<template>
+ <ul>
+ <li v-if="isLoading">
+ Loading...
+ </li>
+ <li v-else-if="error">
+ {{ error }}
+ </li>
+ <template v-else>
+ <li
+ v-for="user in users"
+ :key="user.id"
+ >
+ {{ user }}
+ </li>
+ </template>
+ </ul>
+</template>
+```
+
+### Vuex Gotchas
+1. Do not call a mutation directly. Always use an action to commit a mutation. Doing so will keep consistency through out the application. From Vuex docs:
+
+ > why don't we just call store.commit('action') directly? Well, remember that mutations must be synchronous? Actions aren't. We can perform asynchronous operations inside an action.
+
+ ```javascript
+ // component.vue
+
+ // bad
+ created() {
+ this.$store.commit('mutation');
+ }
+
+ // good
+ created() {
+ this.$store.dispatch('action');
+ }
+ ```
+1. Use mutation types instead of hardcoding strings. It will be less error prone.
+1. The State will be accessible in all components descending from the use where the store is instantiated.
+
+### Testing Vuex
+#### Testing Vuex concerns
+Refer to [vuex docs][vuex-testing] regarding testing Actions, Getters and Mutations.
+
+#### Testing components that need a store
+Smaller components might use `store` properties to access the data.
+In order to write unit tests for those components, we need to include the store and provide the correct state:
+
+```javascript
+//component_spec.js
+import Vue from 'vue';
+import store from './store';
+import component from './component.vue'
+
+describe('component', () => {
+ let vm;
+ let Component;
+
+ beforeEach(() => {
+ Component = Vue.extend(issueActions);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should show a user', () => {
+ const user = {
+ name: 'Foo',
+ age: '30',
+ };
+
+ // populate the store
+ store.dipatch('addUser', user);
+
+ vm = new Component({
+ store,
+ propsData: props,
+ }).$mount();
+ });
+});
+```
+
+[vuex-docs]: https://vuex.vuejs.org
+[vuex-structure]: https://vuex.vuejs.org/en/structure.html
+[vuex-mutations]: https://vuex.vuejs.org/en/mutations.html
+[vuex-testing]: https://vuex.vuejs.org/en/testing.html
diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md
index fdfa1f10402..31addcaf675 100644
--- a/doc/development/rake_tasks.md
+++ b/doc/development/rake_tasks.md
@@ -65,12 +65,11 @@ To make sure that indices still fit. You could find great details in:
## Run tests
In order to run the test you can use the following commands:
-- `rake spinach` to run the spinach suite
- `rake spec` to run the rspec suite
- `rake karma` to run the karma test suite
- `rake gitlab:test` to run all the tests
-Note: Both `rake spinach` and `rake spec` takes significant time to pass.
+Note: `rake spec` takes significant time to pass.
Instead of running full test suite locally you can save a lot of time by running
a single test or directory related to your changes. After you submit merge request
CI will run full test suite for you. Green CI status in the merge request means
@@ -82,12 +81,10 @@ files it can find, also the ones in `/tmp`
To run a single test file you can use:
- `bin/rspec spec/controllers/commit_controller_spec.rb` for a rspec test
-- `bin/spinach features/project/issues/milestones.feature` for a spinach test
To run several tests inside one directory:
- `bin/rspec spec/requests/api/` for the rspec tests if you want to test API only
-- `bin/spinach features/profile/` for the spinach tests if you want to test only profile pages
### Speed-up tests, rake tasks, and migrations
diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md
index 7b32e0a47e1..a76a5096b69 100644
--- a/doc/development/testing_guide/best_practices.md
+++ b/doc/development/testing_guide/best_practices.md
@@ -12,8 +12,7 @@ Here are some things to keep in mind regarding test performance:
- `FactoryBot.build(...)` and `.build_stubbed` are faster than `.create`.
- Don't `create` an object when `build`, `build_stubbed`, `attributes_for`,
`spy`, or `double` will do. Database persistence is slow!
-- Don't mark a feature as requiring JavaScript (through `@javascript` in
- Spinach or `:js` in RSpec) unless it's _actually_ required for the test
+- Don't mark a feature as requiring JavaScript (through `:js` in RSpec) unless it's _actually_ required for the test
to be valid. Headless browser testing is slow!
[parallelization]: ci.md#test-suite-parallelization-on-the-ci
@@ -134,11 +133,24 @@ really fast since:
- gitlab-shell and Gitaly setup are skipped
- Test repositories setup are skipped
-Note that in some cases, you might have to add some `require_dependency 'foo'`
-in your file under test since Rails autoloading is not available in these cases.
+`fast_spec_helper` also support autoloading classes that are located inside the
+`lib/` directory. It means that as long as your class / module is using only
+code from the `lib/` directory you will not need to explicitly load any
+dependencies. `fast_spec_helper` also loads all ActiveSupport extensions,
+including core extensions that are commonly used in the Rails environment.
-This shouldn't be a problem since explicitely listing dependencies should be
-considered a good practice anyway.
+Note that in some cases, you might still have to load some dependencies using
+`require_dependency` when a code is using gems or a dependency is not located
+in `lib/`.
+
+For example, if you want to test your code that is calling the
+`Gitlab::UntrustedRegexp` class, which under the hood uses `re2` library, you
+should either add `require_dependency 're2'` to files in your library that
+need `re2` gem, to make this requirement explicit, or you can add it to the
+spec itself, but the former is preferred.
+
+It takes around one second to load tests that are using `fast_spec_helper`
+instead of 30+ seconds in case of a regular `spec_helper`.
### `let` variables
@@ -230,6 +242,11 @@ describe "#==" do
end
```
+### Prometheus tests
+
+Prometheus metrics may be preserved from one test run to another. To ensure that metrics are
+reset before each example, add the `:prometheus` tag to the Rspec test.
+
### Matchers
Custom matchers should be created to clarify the intent and/or hide the
diff --git a/doc/development/testing_guide/ci.md b/doc/development/testing_guide/ci.md
index e90de55068d..0d8e150e090 100644
--- a/doc/development/testing_guide/ci.md
+++ b/doc/development/testing_guide/ci.md
@@ -24,8 +24,7 @@ Our current CI parallelization setup is as follows:
uploaded to S3.
After that, the next pipeline will use the up-to-date
-`knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file. The same strategy
-is used for Spinach tests as well.
+`knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file.
### Monitoring
diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md
index 0d0d511582b..3b2b9c8c947 100644
--- a/doc/development/testing_guide/frontend_testing.md
+++ b/doc/development/testing_guide/frontend_testing.md
@@ -280,26 +280,6 @@ describe "Admin::AbuseReports", :js do
end
```
-### Spinach errors due to missing JavaScript
-
-NOTE: **Note:** Since we are discouraging the use of Spinach when writing new
-feature tests, you shouldn't ever need to use this. This information is kept
-available for legacy purposes only.
-
-In Spinach, the JavaScript driver is enabled differently. In the `*.feature`
-file for the failing spec, add the `@javascript` flag above the Scenario:
-
-```
-@javascript
-Scenario: Developer can approve merge request
- Given I am a "Shop" developer
- And I visit project "Shop" merge requests page
- And merge request 'Bug NS-04' must be approved
- And I click link "Bug NS-04"
- When I click link "Approve"
- Then I should see approved merge request "Bug NS-04"
-```
-
[jasmine-focus]: https://jasmine.github.io/2.5/focused_specs.html
[jasmine-jquery]: https://github.com/velesin/jasmine-jquery
[karma]: http://karma-runner.github.io/
diff --git a/doc/development/testing_guide/index.md b/doc/development/testing_guide/index.md
index 74d09eb91ff..0cd63a54b55 100644
--- a/doc/development/testing_guide/index.md
+++ b/doc/development/testing_guide/index.md
@@ -72,21 +72,6 @@ Everything you should know about how to run end-to-end tests using
---
-## Spinach (feature) tests
-
-GitLab [moved from Cucumber to Spinach](https://github.com/gitlabhq/gitlabhq/pull/1426)
-for its feature/integration tests in September 2012.
-
-As of March 2016, we are [trying to avoid adding new Spinach
-tests](https://gitlab.com/gitlab-org/gitlab-ce/issues/14121) going forward,
-opting for [RSpec feature](#features-integration) specs.
-
-Adding new Spinach scenarios is acceptable _only if_ the new scenario requires
-no more than one new `step` definition. If more than that is required, the
-test should be re-implemented using RSpec instead.
-
----
-
[Return to Development documentation](../README.md)
[^1]: /ci/yaml/README.html#dependencies
diff --git a/doc/development/testing_guide/testing_levels.md b/doc/development/testing_guide/testing_levels.md
index 51794f7f4df..07ced36f0c1 100644
--- a/doc/development/testing_guide/testing_levels.md
+++ b/doc/development/testing_guide/testing_levels.md
@@ -81,7 +81,6 @@ possible).
| Tests path | Testing engine | Notes |
| ---------- | -------------- | ----- |
| `spec/features/` | [Capybara] + [RSpec] | If your spec has the `:js` metadata, the browser driver will be [Poltergeist], otherwise it's using [RackTest]. |
-| `features/` | Spinach | Spinach tests are deprecated, [you shouldn't add new Spinach tests](#spinach-feature-tests). |
### Consider **not** writing a system test!
diff --git a/doc/development/writing_documentation.md b/doc/development/writing_documentation.md
index 9bca4637830..1c41fc7611f 100644
--- a/doc/development/writing_documentation.md
+++ b/doc/development/writing_documentation.md
@@ -1,3 +1,7 @@
+---
+description: Learn how to contribute to GitLab Documentation.
+---
+
# GitLab Documentation guidelines
- **General Documentation**: written by the [developers responsible by creating features](#contributing-to-docs). Should be submitted in the same merge request containing code. Feature proposals (by GitLab contributors) should also be accompanied by its respective documentation. They can be later improved by PMs and Technical Writers.
@@ -201,6 +205,19 @@ Things to note:
built-in help page, that's why we omit it in `git grep`.
- Use the checklist on the documentation MR description template.
+#### Alternative redirection method
+
+Alternatively to the method described above, you can simply replace the content
+of the old file with a frontmatter containing a redirect link:
+
+```yaml
+---
+redirect_to: '../path/to/file/README.md'
+---
+```
+
+It supports both full and relative URLs, e.g. `https://docs.gitlab.com/ee/path/to/file.html`, `../path/to/file.html`, `path/to/file.md`. Note that any `*.md` paths will be compiled to `*.html`.
+
### Redirections for pages with Disqus comments
If the documentation page being relocated already has any Disqus comments,
diff --git a/doc/install/README.md b/doc/install/README.md
index 5dadf57ea9a..27df03c6ac6 100644
--- a/doc/install/README.md
+++ b/doc/install/README.md
@@ -1,5 +1,6 @@
---
comments: false
+description: Read through the GitLab installation methods.
---
# Installation
diff --git a/doc/install/azure/index.md b/doc/install/azure/index.md
index b0c3ad960bb..21694b02d18 100644
--- a/doc/install/azure/index.md
+++ b/doc/install/azure/index.md
@@ -1,3 +1,8 @@
+---
+description: 'Learn how to spin up a
+pre-configured GitLab VM on Microsoft Azure and have your very own private GitLab instance up and running in around 30 minutes.'
+---
+
# Install GitLab on Microsoft Azure
> _This article was originally written by Dave Wentzel and [published on the GitLab Blog][Original-Blog-Post]._
diff --git a/doc/install/docker.md b/doc/install/docker.md
index 933756072ff..c7dc9db70c5 100644
--- a/doc/install/docker.md
+++ b/doc/install/docker.md
@@ -1,4 +1,4 @@
-# GitLab Docker images
+# Install GitLab with Docker
[Docker](https://www.docker.com) and container technology have been revolutionizing the software world for the past few years. They combine the performance and efficiency of native execution with the abstraction, security, and immutability of virtualization.
diff --git a/doc/install/google_cloud_platform/index.md b/doc/install/google_cloud_platform/index.md
index 2691495e0d4..ab5f7507f24 100644
--- a/doc/install/google_cloud_platform/index.md
+++ b/doc/install/google_cloud_platform/index.md
@@ -1,3 +1,7 @@
+---
+description: 'Learn how to install a GitLab instance on Google Cloud Platform.'
+---
+
# Installing GitLab on Google Cloud Platform
![GCP landing page](img/gcp_landing.png)
diff --git a/doc/install/installation.md b/doc/install/installation.md
index fa5bcfa6f07..a0ae9017f71 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -301,9 +301,9 @@ sudo usermod -aG redis git
### Clone the Source
# Clone GitLab repository
- sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 10-7-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 10-8-stable gitlab
-**Note:** You can change `10-6-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `10-8-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
diff --git a/doc/install/kubernetes/gitlab_omnibus.md b/doc/install/kubernetes/gitlab_omnibus.md
index 9c5258c2cdf..98af87455ec 100644
--- a/doc/install/kubernetes/gitlab_omnibus.md
+++ b/doc/install/kubernetes/gitlab_omnibus.md
@@ -129,8 +129,8 @@ You may see a temporary error message `SchedulerPredicates failed due to Persist
Add the GitLab Helm repository and initialize Helm:
```bash
-helm repo add gitlab https://charts.gitlab.io
helm init
+helm repo add gitlab https://charts.gitlab.io
```
Once you have reviewed the [configuration settings](#configuring-and-installing-gitlab) you can install the chart. We recommending saving your configuration options in a `values.yaml` file for easier upgrades in the future.
diff --git a/doc/install/kubernetes/gitlab_runner_chart.md b/doc/install/kubernetes/gitlab_runner_chart.md
index 0a093c9ec32..2aab225fcdb 100644
--- a/doc/install/kubernetes/gitlab_runner_chart.md
+++ b/doc/install/kubernetes/gitlab_runner_chart.md
@@ -1,6 +1,6 @@
# GitLab Runner Helm Chart
> **Note:**
-These charts have been tested on Google Kubernetes Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues).
+These charts have been tested on Google Kubernetes Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/gitlab-runner/issues).
The `gitlab-runner` Helm chart deploys a GitLab Runner instance into your
Kubernetes cluster.
@@ -25,7 +25,7 @@ For more information on available GitLab Helm Charts, please see our [overview](
Create a `values.yaml` file for your GitLab Runner configuration. See [Helm docs](https://github.com/kubernetes/helm/blob/master/docs/chart_template_guide/values_files.md)
for information on how your values file will override the defaults.
-The default configuration can always be found in the [values.yaml](https://gitlab.com/charts/charts.gitlab.io/blob/master/charts/gitlab-runner/values.yaml) in the chart repository.
+The default configuration can always be found in the [values.yaml](https://gitlab.com/charts/gitlab-runner/blob/master/values.yaml) in the chart repository.
### Required configuration
@@ -39,7 +39,7 @@ Unless you need to specify additional configuration, you are [ready to install](
### Other configuration
-The rest of the configuration is [documented in the `values.yaml`](https://gitlab.com/charts/charts.gitlab.io/blob/master/charts/gitlab-runner/values.yaml) in the chart repository.
+The rest of the configuration is [documented in the `values.yaml`](https://gitlab.com/charts/gitlab-runner/blob/master/values.yaml) in the chart repository.
Here is a snippet of the important settings:
diff --git a/doc/install/kubernetes/index.md b/doc/install/kubernetes/index.md
index 7d8b8fc1597..aeaa739edab 100644
--- a/doc/install/kubernetes/index.md
+++ b/doc/install/kubernetes/index.md
@@ -1,4 +1,9 @@
+---
+description: 'Read through the different methods to deploy GitLab on Kubernetes.'
+---
+
# Installing GitLab on Kubernetes
+
> **Note**: These charts have been tested on Google Kubernetes Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues).
The easiest method to deploy GitLab on [Kubernetes](https://kubernetes.io/) is
@@ -16,6 +21,7 @@ should be deployed, upgraded, and configured.
* [Community Contributed Charts](#community-contributed-charts): Community contributed charts, deprecated by the official GitLab chart.
## GitLab-Omnibus Chart (Recommended)
+
> **Note**: This chart is in beta while [additional features](https://gitlab.com/charts/charts.gitlab.io/issues/68) are being added.
This chart is the best available way to operate GitLab on Kubernetes. It deploys and configures nearly all features of GitLab, including: a [Runner](https://docs.gitlab.com/runner/), [Container Registry](../../user/project/container_registry.html#gitlab-container-registry), [Mattermost](https://docs.gitlab.com/omnibus/gitlab-mattermost/), [automatic SSL](https://github.com/kubernetes/charts/tree/master/stable/kube-lego), and a [load balancer](https://github.com/kubernetes/ingress/tree/master/controllers/nginx). It is based on our [GitLab Omnibus Docker Images](https://docs.gitlab.com/omnibus/docker/README.html).
diff --git a/doc/install/openshift_and_gitlab/index.md b/doc/install/openshift_and_gitlab/index.md
index e6ccfccd33f..1ced1fb513d 100644
--- a/doc/install/openshift_and_gitlab/index.md
+++ b/doc/install/openshift_and_gitlab/index.md
@@ -6,7 +6,7 @@ article_type: tutorial
date: 2016-06-28
---
-# Getting started with OpenShift Origin 3 and GitLab
+# How to install GitLab on OpenShift Origin 3
## Introduction
diff --git a/doc/legal/README.md b/doc/legal/README.md
index 6413f1d645f..d991429a652 100644
--- a/doc/legal/README.md
+++ b/doc/legal/README.md
@@ -4,5 +4,4 @@ comments: false
# Legal
-- [Corporate contributor license agreement](corporate_contributor_license_agreement.md)
-- [Individual contributor license agreement](individual_contributor_license_agreement.md)
+Please read through the [GitLab License Agreement](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md).
diff --git a/doc/legal/corporate_contributor_license_agreement.md b/doc/legal/corporate_contributor_license_agreement.md
index ebb24ba0a7f..e5fc7a3c85f 100644
--- a/doc/legal/corporate_contributor_license_agreement.md
+++ b/doc/legal/corporate_contributor_license_agreement.md
@@ -1,2 +1,3 @@
-This document has been replaced by a Developer Certificate of Origin and License,
-as described in [Contributing.md](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md). \ No newline at end of file
+---
+redirect_to: 'README.md'
+---
diff --git a/doc/legal/individual_contributor_license_agreement.md b/doc/legal/individual_contributor_license_agreement.md
index ebb24ba0a7f..e5fc7a3c85f 100644
--- a/doc/legal/individual_contributor_license_agreement.md
+++ b/doc/legal/individual_contributor_license_agreement.md
@@ -1,2 +1,3 @@
-This document has been replaced by a Developer Certificate of Origin and License,
-as described in [Contributing.md](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md). \ No newline at end of file
+---
+redirect_to: 'README.md'
+---
diff --git a/doc/raketasks/features.md b/doc/raketasks/features.md
index fee49cc27cc..57c16f110e4 100644
--- a/doc/raketasks/features.md
+++ b/doc/raketasks/features.md
@@ -1,4 +1,4 @@
-# Features
+# Namespaces
## Enable usernames and namespaces for user projects
diff --git a/doc/raketasks/web_hooks.md b/doc/raketasks/web_hooks.md
index 2ebf7c48f4e..5f3143f76cd 100644
--- a/doc/raketasks/web_hooks.md
+++ b/doc/raketasks/web_hooks.md
@@ -1,4 +1,4 @@
-# Webhooks
+# Webhooks administration **[CORE ONLY]**
## Add a webhook for **ALL** projects:
diff --git a/doc/topics/git/how_to_install_git/index.md b/doc/topics/git/how_to_install_git/index.md
index 6c909a1ba86..58d86f7d387 100644
--- a/doc/topics/git/how_to_install_git/index.md
+++ b/doc/topics/git/how_to_install_git/index.md
@@ -4,6 +4,7 @@ author_gitlab: SeanPackham
level: beginner
article_type: user guide
date: 2017-05-15
+description: 'This article describes how to install Git on macOS, Ubuntu Linux and Windows.'
---
# Installing Git
diff --git a/doc/university/training/gitlab_flow.md b/doc/university/training/gitlab_flow.md
index 02a6ad48a38..d7bc7bda43f 100644
--- a/doc/university/training/gitlab_flow.md
+++ b/doc/university/training/gitlab_flow.md
@@ -2,39 +2,31 @@
comments: false
---
-# GitLab Flow
+# What is the GitLab Flow
- A simplified branching strategy
- All features and fixes first go to master
- Allows for 'production' or 'stable' branches
- Bug fixes/hot fix patches are cherry-picked from master
----
-
-# Feature branches
+## Feature branches
- Create a feature/bugfix branch to do all work
- Use merge requests to merge to master
![inline](gitlab_flow/feature_branches.png)
----
-
-# Production branch
+## Production branch
- One, long-running production release branch
as opposed to individual stable branches
- Consider creating a tag for each version that gets deployed
----
-
-# Production branch
+## Production branch
![inline](gitlab_flow/production_branch.png)
----
-
-# Release branch
+## Release branch
- Useful if you release software to customers
- When preparing a new release, create stable branch
@@ -43,15 +35,11 @@ comments: false
- Cherry-pick critical bug fixes to stable branch for patch release
- Never commit bug fixes directly to stable branch
----
-
-# Release branch
+## Release branch
![inline](gitlab_flow/release_branches.png)
----
-
-# More details
+## More details
-Blog post on 'GitLab Flow' at
-[http://doc.gitlab.com/ee/workflow/gitlab_flow.html](http://doc.gitlab.com/ee/workflow/gitlab_flow.html)
+For more information read through the [GitLab Flow](../../workflow/gitlab_flow.md)
+documentation.
diff --git a/doc/university/training/topics/gitlab_flow.md b/doc/university/training/topics/gitlab_flow.md
index b8049b5c80e..f6006ce0a60 100644
--- a/doc/university/training/topics/gitlab_flow.md
+++ b/doc/university/training/topics/gitlab_flow.md
@@ -1,57 +1,3 @@
---
-comments: false
+redirect_to: '../gitlab_flow.md'
---
-
-# GitLab Flow
-
-----------
-
-- A simplified branching strategy
-- All features and fixes first go to master
-- Allows for 'production' or 'stable' branches
-- Bug fixes/hot fix patches are cherry-picked from master
-
-----------
-
-### Feature branches
-
-- Create a feature/bugfix branch to do all work
-- Use merge requests to merge to master
-
-![inline](http://gitlab.com/gitlab-org/University/raw/5baea0fe222a915d0500e40747d35eb18681cdc3/training/gitlab_flow/feature_branches.png)
-
-----------
-
-## Production branch
-
-- One, long-running production release branch
- as opposed to individual stable branches
-- Consider creating a tag for each version that gets deployed
-
-----------
-
-## Production branch
-
-![inline](http://gitlab.com/gitlab-org/University/raw/5baea0fe222a915d0500e40747d35eb18681cdc3/training/gitlab_flow/production_branch.png)
-
-----------
-
-## Release branch
-
-- Useful if you release software to customers
-- When preparing a new release, create stable branch
- from master
-- Consider creating a tag for each version
-- Cherry-pick critical bug fixes to stable branch for patch release
-- Never commit bug fixes directly to stable branch
-
-----------
-
-![inline](http://gitlab.com/gitlab-org/University/raw/5baea0fe222a915d0500e40747d35eb18681cdc3/training/gitlab_flow/release_branches.png)
-
-----------
-
-## More details
-
-Blog post on 'GitLab Flow' at
-[http://doc.gitlab.com/ee/workflow/gitlab_flow.html](http://doc.gitlab.com/ee/workflow/gitlab_flow.html)
diff --git a/doc/university/training/topics/merge_requests.md b/doc/university/training/topics/merge_requests.md
index 4e8c9de85a1..d7b771cd87b 100644
--- a/doc/university/training/topics/merge_requests.md
+++ b/doc/university/training/topics/merge_requests.md
@@ -2,7 +2,7 @@
comments: false
---
-# Merge requests
+# Code review and collaboration with Merge Requests
----------
diff --git a/doc/update/10.7-to-10.8.md b/doc/update/10.7-to-10.8.md
new file mode 100644
index 00000000000..13101a987f4
--- /dev/null
+++ b/doc/update/10.7-to-10.8.md
@@ -0,0 +1,362 @@
+---
+comments: false
+---
+
+# From 10.7 to 10.8
+
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag in the version dropdown at the
+top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
+### 1. Stop server
+
+```bash
+sudo service gitlab stop
+```
+
+### 2. Backup
+
+NOTE: If you installed GitLab from source, make sure `rsync` is installed.
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 3. Update Ruby
+
+NOTE: GitLab 9.0 and higher only support Ruby 2.3.x and dropped support for Ruby 2.1.x. Be
+sure to upgrade your interpreter if necessary.
+
+You can check which version you are running with `ruby -v`.
+
+Download Ruby and compile it:
+
+ ```bash
+ mkdir /tmp/ruby && cd /tmp/ruby
+ curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.7.tar.gz
+ echo '540996fec64984ab6099e34d2f5820b14904f15a ruby-2.3.7.tar.gz' | shasum -c - && tar xzf ruby-2.3.7.tar.gz
+ cd ruby-2.3.7
+
+ ./configure --disable-install-rdoc
+ make
+ sudo make install
+ ```
+
+Install Bundler:
+
+```bash
+sudo gem install bundler --no-ri --no-rdoc
+```
+
+### 4. Update Node
+
+GitLab utilizes [webpack](http://webpack.js.org) to compile frontend assets.
+This requires a minimum version of node v6.0.0.
+
+You can check which version you are running with `node -v`. If you are running
+a version older than `v6.0.0` you will need to update to a newer version. You
+can find instructions to install from community maintained packages or compile
+from source at the nodejs.org website.
+
+<https://nodejs.org/en/download/>
+
+GitLab also requires the use of yarn `>= v1.2.0` to manage JavaScript
+dependencies.
+
+```bash
+curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
+echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
+sudo apt-get update
+sudo apt-get install yarn
+```
+
+More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install).
+
+### 5. Update Go
+
+NOTE: GitLab 9.2 and higher only supports Go 1.8.3 and dropped support for Go
+1.5.x through 1.7.x. Be sure to upgrade your installation if necessary.
+
+You can check which version you are running with `go version`.
+
+Download and install Go:
+
+```bash
+# Remove former Go installation folder
+sudo rm -rf /usr/local/go
+
+curl --remote-name --progress https://storage.googleapis.com/golang/go1.8.3.linux-amd64.tar.gz
+echo '1862f4c3d3907e59b04a757cfda0ea7aa9ef39274af99a784f5be843c80c6772 go1.8.3.linux-amd64.tar.gz' | shasum -a256 -c - && \
+ sudo tar -C /usr/local -xzf go1.8.3.linux-amd64.tar.gz
+sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/
+rm go1.8.3.linux-amd64.tar.gz
+```
+
+### 6. Get latest code
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git fetch --all --prune
+sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
+sudo -u git -H git checkout -- locale
+```
+
+For GitLab Community Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 10-8-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 10-8-stable-ee
+```
+
+### 7. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+
+sudo -u git -H git fetch --all --tags --prune
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION)
+sudo -u git -H bin/compile
+```
+
+### 8. Update gitlab-workhorse
+
+Install and compile gitlab-workhorse. GitLab-Workhorse uses
+[GNU Make](https://www.gnu.org/software/make/).
+If you are not using Linux you may have to run `gmake` instead of
+`make` below.
+
+```bash
+cd /home/git/gitlab-workhorse
+
+sudo -u git -H git fetch --all --tags --prune
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION)
+sudo -u git -H make
+```
+
+### 9. Update Gitaly
+
+#### New Gitaly configuration options required
+
+In order to function Gitaly needs some additional configuration information. Below we assume you installed Gitaly in `/home/git/gitaly` and GitLab Shell in `/home/git/gitlab-shell`.
+
+```shell
+echo '
+[gitaly-ruby]
+dir = "/home/git/gitaly/ruby"
+
+[gitlab-shell]
+dir = "/home/git/gitlab-shell"
+' | sudo -u git tee -a /home/git/gitaly/config.toml
+```
+
+#### Check Gitaly configuration
+
+Due to a bug in the `rake gitlab:gitaly:install` script your Gitaly
+configuration file may contain syntax errors. The block name
+`[[storages]]`, which may occur more than once in your `config.toml`
+file, should be `[[storage]]` instead.
+
+```shell
+sudo -u git -H sed -i.pre-10.1 's/\[\[storages\]\]/[[storage]]/' /home/git/gitaly/config.toml
+```
+
+#### Compile Gitaly
+
+```shell
+cd /home/git/gitaly
+sudo -u git -H git fetch --all --tags --prune
+sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION)
+sudo -u git -H make
+```
+
+### 10. Update MySQL permissions
+
+If you are using MySQL you need to grant the GitLab user the necessary
+permissions on the database:
+
+```bash
+mysql -u root -p -e "GRANT TRIGGER ON \`gitlabhq_production\`.* TO 'git'@'localhost';"
+```
+
+If you use MySQL with replication, or just have MySQL configured with binary logging,
+you will need to also run the following on all of your MySQL servers:
+
+```bash
+mysql -u root -p -e "SET GLOBAL log_bin_trust_function_creators = 1;"
+```
+
+You can make this setting permanent by adding it to your `my.cnf`:
+
+```
+log_bin_trust_function_creators=1
+```
+
+### 11. Update configuration files
+
+#### New configuration options for `gitlab.yml`
+
+There might be configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/10-7-stable:config/gitlab.yml.example origin/10-8-stable:config/gitlab.yml.example
+```
+
+#### Nginx configuration
+
+Ensure you're still up-to-date with the latest NGINX configuration changes:
+
+```sh
+cd /home/git/gitlab
+
+# For HTTPS configurations
+git diff origin/10-7-stable:lib/support/nginx/gitlab-ssl origin/10-8-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/10-7-stable:lib/support/nginx/gitlab origin/10-8-stable:lib/support/nginx/gitlab
+```
+
+If you are using Strict-Transport-Security in your installation to continue using it you must enable it in your Nginx
+configuration as GitLab application no longer handles setting it.
+
+If you are using Apache instead of NGINX please see the updated [Apache templates].
+Also note that because Apache does not support upstreams behind Unix sockets you
+will need to let gitlab-workhorse listen on a TCP port. You can do this
+via [/etc/default/gitlab].
+
+[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
+[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-8-stable/lib/support/init.d/gitlab.default.example#L38
+
+#### SMTP configuration
+
+If you're installing from source and use SMTP to deliver mail, you will need to add the following line
+to config/initializers/smtp_settings.rb:
+
+```ruby
+ActionMailer::Base.delivery_method = :smtp
+```
+
+See [smtp_settings.rb.sample] as an example.
+
+[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-8-stable/config/initializers/smtp_settings.rb.sample#L13
+
+#### Init script
+
+There might be new configuration options available for [`gitlab.default.example`][gl-example]. View them with the command below and apply them manually to your current `/etc/default/gitlab`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/10-7-stable:lib/support/init.d/gitlab.default.example origin/10-8-stable:lib/support/init.d/gitlab.default.example
+```
+
+Ensure you're still up-to-date with the latest init script changes:
+
+```bash
+cd /home/git/gitlab
+
+sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+```
+
+For Ubuntu 16.04.1 LTS:
+
+```bash
+sudo systemctl daemon-reload
+```
+
+### 12. Install libs, migrations, etc.
+
+```bash
+cd /home/git/gitlab
+
+# MySQL installations (note: the line below states '--without postgres')
+sudo -u git -H bundle install --without postgres development test --deployment
+
+# PostgreSQL installations (note: the line below states '--without mysql')
+sudo -u git -H bundle install --without mysql development test --deployment
+
+# Optional: clean up old gems
+sudo -u git -H bundle clean
+
+# Run database migrations
+sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+
+# Compile GetText PO files
+
+sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production
+
+# Update node dependencies and recompile assets
+sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production
+
+# Clean up cache
+sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production
+```
+
+**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md).
+
+### 13. Start application
+
+```bash
+sudo service gitlab start
+sudo service nginx restart
+```
+
+### 14. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+```
+
+To make sure you didn't miss anything run a more thorough check:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+```
+
+If all items are green, then congratulations, the upgrade is complete!
+
+## Things went south? Revert to previous version (10.7)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 10.6 to 10.7](10.6-to-10.7.md), except for the
+database migration (the backup is already migrated to the previous version).
+
+### 2. Restore from the backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
+```
+
+If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
+
+[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-8-stable/config/gitlab.yml.example
+[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-8-stable/lib/support/init.d/gitlab.default.example
diff --git a/doc/user/admin_area/labels.md b/doc/user/admin_area/labels.md
index 9e2a89ebdf6..e383142c33e 100644
--- a/doc/user/admin_area/labels.md
+++ b/doc/user/admin_area/labels.md
@@ -1,4 +1,4 @@
-# Labels
+# Labels administration **[CORE ONLY]**
## Default Labels
diff --git a/doc/user/group/index.md b/doc/user/group/index.md
index 88f4bb2ee04..30761a66563 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -40,20 +40,20 @@ In GitLab, a namespace is a unique name to be used as a user name, a group name,
- `http://gitlab.example.com/groupname`
- `http://gitlab.example.com/groupname/subgroup_name`
-For example, consider a user called John:
+For example, consider a user named Alex:
-1. John creates his account on GitLab.com with the username `john`;
-his profile will be accessed under `https://gitlab.example.com/john`
-1. John creates a group for his team with the groupname `john-team`;
-his group and its projects will be accessed under `https://gitlab.example.com/john-team`
-1. John creates a subgroup of `john-team` with the subgroup name `marketing`;
-his subgroup and its projects will be accessed under `https://gitlab.example.com/john-team/marketing`
+1. Alex creates an account on GitLab.com with the username `alex`;
+their profile will be accessed under `https://gitlab.example.com/alex`
+1. Alex creates a group for their team with the groupname `alex-team`;
+the group and its projects will be accessed under `https://gitlab.example.com/alex-team`
+1. Alex creates a subgroup of `alex-team` with the subgroup name `marketing`;
+this subgroup and its projects will be accessed under `https://gitlab.example.com/alex-team/marketing`
By doing so:
-- Any team member mentions John with `@john`
-- John mentions everyone from his team with `@john-team`
-- John mentions only his marketing team with `@john-team/marketing`
+- Any team member mentions Alex with `@alex`
+- Alex mentions everyone from their team with `@alex-team`
+- Alex mentions only the marketing team with `@alex-team/marketing`
## Issues and merge requests within a group
diff --git a/doc/user/index.md b/doc/user/index.md
index 2494df46f1c..a50e5e8fbf8 100644
--- a/doc/user/index.md
+++ b/doc/user/index.md
@@ -1,3 +1,7 @@
+---
+description: 'Read through the GitLab User documentation to learn how to use, configure, and customize GitLab and GitLab.com to your own needs.'
+---
+
# User documentation
Welcome to GitLab! We're glad to have you here!
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index a9ba2a51242..0808e3949be 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -1,3 +1,7 @@
+---
+description: 'Understand and explore the user permission levels in GitLab, and what features each of them grants you access to.'
+---
+
# Permissions
Users have different abilities depending on the access level they have in a
diff --git a/doc/user/project/integrations/prometheus_library/nginx.md b/doc/user/project/integrations/prometheus_library/nginx.md
index fea3231006b..557487e1a75 100644
--- a/doc/user/project/integrations/prometheus_library/nginx.md
+++ b/doc/user/project/integrations/prometheus_library/nginx.md
@@ -10,17 +10,19 @@ The [Prometheus service](../prometheus.md) must be enabled.
## Metrics supported
+NGINX server metrics are detected, which tracks the pages and content directly served by NGINX.
+
| Name | Query |
| ---- | ----- |
-| Throughput (req/sec) | sum(rate(nginx_responses_total{server_zone!="*", server_zone!="_", %{environment_filter}}[2m])) by (status_code) |
-| Latency (ms) | avg(nginx_upstream_response_msecs_avg{%{environment_filter}}) |
-| HTTP Error Rate (HTTP Errors / sec) | rate(nginx_responses_total{status_code="5xx", %{environment_filter}}[2m])) |
+| Throughput (req/sec) | sum(rate(nginx_server_requests{server_zone!="*", server_zone!="_", %{environment_filter}}[2m])) by (code) |
+| Latency (ms) | avg(nginx_server_requestMsec{%{environment_filter}}) |
+| HTTP Error Rate (HTTP Errors / sec) | sum(rate(nginx_server_requests{code="5xx", %{environment_filter}}[2m])) |
## Configuring Prometheus to monitor for NGINX metrics
To get started with NGINX monitoring, you should first enable the [VTS statistics](https://github.com/vozlt/nginx-module-vts)) module for your NGINX server. This will capture and display statistics in an HTML readable form. Next, you should install and configure the [NGINX VTS exporter](https://github.com/hnlq715/nginx-vts-exporter) which parses these statistics and translates them into a Prometheus monitoring endpoint.
-If you are using NGINX as your Kubernetes ingress, there is [upcoming direct support](https://github.com/kubernetes/ingress/pull/423) for enabling Prometheus monitoring in the 0.9.0 release.
+If you are using NGINX as your Kubernetes ingress, GitLab will [automatically detect](nginx_ingress.md) the metrics once enabled in 0.9.0 and later releases.
## Specifying the Environment label
diff --git a/doc/user/project/merge_requests/maintainer_access.md b/doc/user/project/merge_requests/maintainer_access.md
index c9763a3fe02..89f71e16a50 100644
--- a/doc/user/project/merge_requests/maintainer_access.md
+++ b/doc/user/project/merge_requests/maintainer_access.md
@@ -16,3 +16,5 @@ source project, and only lasts while the merge request is open.
Enable this functionality while creating a merge request:
![Enable maintainer edits](./img/allow_maintainer_push.png)
+
+[ce-17395]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17395
diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md
index a97ce84b861..4b5c2539c4b 100644
--- a/doc/user/project/pages/index.md
+++ b/doc/user/project/pages/index.md
@@ -1,3 +1,7 @@
+---
+description: 'Learn how to use GitLab Pages to deploy a static website at no additional cost.'
+---
+
# GitLab Pages
With GitLab Pages it's easy to publish your project website. GitLab Pages is a hosting service for static websites, at no additional cost.
diff --git a/doc/user/project/pages/introduction.md b/doc/user/project/pages/introduction.md
index 0b5076b8c5d..fe4d15adfa1 100644
--- a/doc/user/project/pages/introduction.md
+++ b/doc/user/project/pages/introduction.md
@@ -1,4 +1,4 @@
-# GitLab Pages
+# Exploring GitLab Pages
> **Notes:**
> - This feature was [introduced][ee-80] in GitLab EE 8.3.
@@ -14,9 +14,7 @@ deploy static pages for your individual projects, your user or your group.
Read [GitLab Pages on GitLab.com](#gitlab-pages-on-gitlab-com) for specific
information, if you are using GitLab.com to host your website.
-Read through [All you Need to Know About GitLab Pages][pages-index-guide] for a list of all learning materials we have prepared for GitLab Pages (webpages, articles, guides, blog posts, video tutorials).
-
-## Getting started with GitLab Pages
+## Getting started with GitLab Pages domains
> **Note:**
> In the rest of this document we will assume that the general domain name that
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index 2c90f4b4413..b8bac01959e 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -5,6 +5,7 @@
> - [Introduced][ce-3050] in GitLab 8.9.
> - Importing will not be possible if the import instance version differs from
> that of the exporter.
+> - For GitLab admins, please read through [Project import/export administration](../../../administration/raketasks/project_import_export.md).
> - For existing installations, the project import option has to be enabled in
> application settings (`/admin/application_settings`) under 'Import sources'.
> Ask your administrator if you don't see the **GitLab export** button when
diff --git a/doc/user/project/web_ide/img/commit_changes.png b/doc/user/project/web_ide/img/commit_changes.png
index b6fcbf699aa..a5364c12760 100644
--- a/doc/user/project/web_ide/img/commit_changes.png
+++ b/doc/user/project/web_ide/img/commit_changes.png
Binary files differ
diff --git a/doc/user/project/web_ide/index.md b/doc/user/project/web_ide/index.md
index b7064b83c4e..105d8a6ab61 100644
--- a/doc/user/project/web_ide/index.md
+++ b/doc/user/project/web_ide/index.md
@@ -13,15 +13,27 @@ and from merge requests.
![Open Web IDE](img/open_web_ide.png)
-## Commit changes
+## File finder
-Changed files are shown on the right in the commit panel. All changes are
-automatically staged. To commit your changes, add a commit message and click
-the 'Commit Button'.
+> [Introduced in](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18323) [GitLab Core][ce] 10.8.
+
+The file finder allows you to quickly open files in the current branch by
+searching. The file finder is launched using the keyboard shortcut `Command-p`,
+`Control-p`, or `t` (when editor is not in focus). Type the filename or
+file path fragments to start seeing results.
+
+## Stage and commit changes
+
+After making your changes, click the Commit button in the bottom left to
+review the list of changed files. Click on each file to review the changes and
+click the tick icon to stage the file.
+
+Once you have staged some changes, you can add a commit message and commit the
+staged changes. Unstaged changes will not be commited.
![Commit changes](img/commit_changes.png)
-## Comparing changes
+## Reviewing changes
Before you commit your changes, you can compare them with the previous commit
by switching to the review mode or selecting the file from the staged files
@@ -30,4 +42,5 @@ list.
An additional review mode is available when you open a merge request, which
shows you a preview of the merge request diff if you commit your changes.
+[ce]: https://about.gitlab.com/pricing/
[ee]: https://about.gitlab.com/pricing/
diff --git a/doc/workflow/protected_branches.md b/doc/workflow/protected_branches.md
index aa48b8f750e..ced7d391ace 100644
--- a/doc/workflow/protected_branches.md
+++ b/doc/workflow/protected_branches.md
@@ -1 +1 @@
-This document is moved to [user/project/protected_branches.md](../user/project/protected_branches.md)
+This document was moved to [another location](../user/project/protected_branches.md).
diff --git a/doc/workflow/repository_mirroring.md b/doc/workflow/repository_mirroring.md
new file mode 100644
index 00000000000..dbe63144e38
--- /dev/null
+++ b/doc/workflow/repository_mirroring.md
@@ -0,0 +1,111 @@
+# Repository mirroring
+
+Repository Mirroring is a way to mirror repositories from external sources.
+It can be used to mirror all branches, tags, and commits that you have
+in your repository.
+
+Your mirror at GitLab will be updated automatically. You can
+also manually trigger an update at most once every 5 minutes.
+
+## Overview
+
+Repository mirroring is very useful when, for some reason, you must use a
+project from another source.
+
+There are two kinds of repository mirroring features supported by GitLab:
+**push** and **pull**, the latter being only available in GitLab Enterprise Edition.
+The **push** method mirrors the repository in GitLab to another location.
+
+Once the mirror repository is updated, all new branches,
+tags, and commits will be visible in the project's activity feed.
+Users with at least [developer access][perms] to the project can also force an
+immediate update with the click of a button. This button will not be available if
+the mirror is already being updated or 5 minutes still haven't passed since its last update.
+
+A few things/limitations to consider:
+
+- The repository must be accessible over `http://`, `https://`, `ssh://` or `git://`.
+- If your HTTP repository is not publicly accessible, add authentication
+ information to the URL, like: `https://username@gitlab.company.com/group/project.git`.
+ In some cases, you might need to use a personal access token instead of a
+ password, e.g., you want to mirror to GitHub and have 2FA enabled.
+- The import will time out after 15 minutes. For repositories that take longer
+ use a clone/push combination.
+- The Git LFS objects will not be synced. You'll need to push/pull them
+ manually.
+
+## Use-case
+
+- You have old projects in another source that you don't use actively anymore,
+ but don't want to remove for archiving purposes. In that case, you can create
+ a push mirror so that your active GitLab repository can push its changes to the
+ old location.
+
+## Pushing to a remote repository **[STARTER]**
+
+>[Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/249) in
+GitLab Enterprise Edition 8.7. [Moved to GitLab Community Edition][ce-18715] in 10.8.
+
+For an existing project, you can set up push mirror from your project's
+**Settings âž” Repository** and searching for the "Push to a remote repository"
+section. Check the "Remote mirror repository" box and fill in the Git URL of
+the repository to push to. Click **Save changes** for the changes to take
+effect.
+
+![Push settings](repository_mirroring/repository_mirroring_push_settings.png)
+
+When push mirroring is enabled, you are advised not to push commits directly
+to the mirrored repository to prevent the mirror diverging.
+All changes will end up in the mirrored repository whenever commits
+are pushed to GitLab, or when a [forced update](#forcing-an-update) is
+initiated.
+
+Pushes into GitLab are automatically pushed to the remote mirror at least once
+every 5 minutes after they are received or once every minute if **push only
+protected branches** is enabled.
+
+In case of a diverged branch, you will see an error indicated at the **Mirror
+repository** settings.
+
+![Diverged branch](
+repository_mirroring/repository_mirroring_diverged_branch_push.png)
+
+### Push only protected branches
+
+>[Introduced][ee-3350] in GitLab Enterprise Edition 10.3. [Moved to GitLab Community Edition][ce-18715] in 10.8.
+
+You can choose to only push your protected branches from GitLab to your remote repository.
+
+To use this option go to your project's repository settings page under push mirror.
+
+## Setting up a push mirror from GitLab to GitHub
+
+To set up a mirror from GitLab to GitHub, you need to follow these steps:
+
+1. Create a [GitHub personal access token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/) with the "public_repo" box checked:
+
+ ![edit personal access token GitHub](repository_mirroring/repository_mirroring_github_edit_personal_access_token.png)
+
+1. Fill in the "Git repository URL" with the personal access token replacing the password `https://GitHubUsername:GitHubPersonalAccessToken@github.com/group/project.git`:
+
+ ![push to remote repo](repository_mirroring/repository_mirroring_gitlab_push_to_a_remote_repository.png)
+
+1. Save
+1. And either wait or trigger the "Update Now" button:
+
+ ![update now](repository_mirroring/repository_mirroring_gitlab_push_to_a_remote_repository_update_now.png)
+
+## Forcing an update
+
+While mirrors are scheduled to update automatically, you can always force an update
+by using the **Update now** button which is exposed in various places:
+
+- in the commits page
+- in the branches page
+- in the tags page
+- in the **Mirror repository** settings page
+
+[ee-3350]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3350
+[ce-18715]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18715
+[perms]: ../user/permissions.md
+
diff --git a/doc/workflow/repository_mirroring/repository_mirroring_diverged_branch_push.png b/doc/workflow/repository_mirroring/repository_mirroring_diverged_branch_push.png
new file mode 100644
index 00000000000..038b05cb31d
--- /dev/null
+++ b/doc/workflow/repository_mirroring/repository_mirroring_diverged_branch_push.png
Binary files differ
diff --git a/doc/workflow/repository_mirroring/repository_mirroring_github_edit_personal_access_token.png b/doc/workflow/repository_mirroring/repository_mirroring_github_edit_personal_access_token.png
new file mode 100644
index 00000000000..139de42d8db
--- /dev/null
+++ b/doc/workflow/repository_mirroring/repository_mirroring_github_edit_personal_access_token.png
Binary files differ
diff --git a/doc/workflow/repository_mirroring/repository_mirroring_gitlab_push_to_a_remote_repository.png b/doc/workflow/repository_mirroring/repository_mirroring_gitlab_push_to_a_remote_repository.png
new file mode 100644
index 00000000000..ccbc1d92329
--- /dev/null
+++ b/doc/workflow/repository_mirroring/repository_mirroring_gitlab_push_to_a_remote_repository.png
Binary files differ
diff --git a/doc/workflow/repository_mirroring/repository_mirroring_gitlab_push_to_a_remote_repository_update_now.png b/doc/workflow/repository_mirroring/repository_mirroring_gitlab_push_to_a_remote_repository_update_now.png
new file mode 100644
index 00000000000..b16b3d2828e
--- /dev/null
+++ b/doc/workflow/repository_mirroring/repository_mirroring_gitlab_push_to_a_remote_repository_update_now.png
Binary files differ
diff --git a/doc/workflow/repository_mirroring/repository_mirroring_push_settings.png b/doc/workflow/repository_mirroring/repository_mirroring_push_settings.png
new file mode 100644
index 00000000000..f8199aa7c0f
--- /dev/null
+++ b/doc/workflow/repository_mirroring/repository_mirroring_push_settings.png
Binary files differ
diff --git a/ee/app/controllers/ee/ldap/omniauth_callbacks_controller.rb b/ee/app/controllers/ee/ldap/omniauth_callbacks_controller.rb
deleted file mode 100644
index f1e851a210b..00000000000
--- a/ee/app/controllers/ee/ldap/omniauth_callbacks_controller.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-module EE
- module Ldap
- module OmniauthCallbacksController
- extend ::Gitlab::Utils::Override
-
- override :sign_in_and_redirect
- def sign_in_and_redirect(user)
- # The counter gets incremented in `sign_in_and_redirect`
- show_ldap_sync_flash if user.sign_in_count == 0
-
- super
- end
-
- private
-
- def show_ldap_sync_flash
- flash[:notice] = 'LDAP sync in progress. This could take a few minutes. '\
- 'Refresh the page to see the changes.'
- end
- end
- end
-end
diff --git a/ee/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb b/ee/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb
deleted file mode 100644
index 0835ff35846..00000000000
--- a/ee/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-require 'spec_helper'
-
-describe Ldap::OmniauthCallbacksController do
- include_context 'Ldap::OmniauthCallbacksController'
-
- it "displays LDAP sync flash on first sign in" do
- post provider
-
- expect(flash[:notice]).to match(/LDAP sync in progress*/)
- end
-
- it "skips LDAP sync flash on subsequent sign ins" do
- user.update!(sign_in_count: 1)
-
- post provider
-
- expect(flash[:notice]).to eq nil
- end
-
- context 'access denied' do
- let(:valid_login?) { false }
-
- it 'logs a failure event' do
- stub_licensed_features(extended_audit_events: true)
-
- expect { post provider }.to change(SecurityEvent, :count).by(1)
- end
- end
-end
diff --git a/features/project/builds/artifacts.feature b/features/project/builds/artifacts.feature
deleted file mode 100644
index 5abc24949cf..00000000000
--- a/features/project/builds/artifacts.feature
+++ /dev/null
@@ -1,65 +0,0 @@
-Feature: Project Builds Artifacts
- Background:
- Given I sign in as a user
- And I own a project
- And project has CI enabled
- And project has a recent build
-
- Scenario: I download build artifacts
- Given recent build has artifacts available
- When I visit recent build details page
- And I click artifacts download button
- Then download of build artifacts archive starts
-
- Scenario: I browse build artifacts
- Given recent build has artifacts available
- And recent build has artifacts metadata available
- When I visit recent build details page
- And I click artifacts browse button
- Then I should see content of artifacts archive
- And I should see the build header
-
- Scenario: I browse subdirectory of build artifacts
- Given recent build has artifacts available
- And recent build has artifacts metadata available
- When I visit recent build details page
- And I click artifacts browse button
- And I click link to subdirectory within build artifacts
- Then I should see content of subdirectory within artifacts archive
- And I should see the directory name in the breadcrumb
-
- Scenario: I browse directory with UTF-8 characters in name
- Given recent build has artifacts available
- And recent build has artifacts metadata available
- And recent build artifacts contain directory with UTF-8 characters
- When I visit recent build details page
- And I click artifacts browse button
- And I navigate to directory with UTF-8 characters in name
- Then I should see content of directory with UTF-8 characters in name
-
- Scenario: I try to browse directory with invalid UTF-8 characters in name
- Given recent build has artifacts available
- And recent build has artifacts metadata available
- And recent build artifacts contain directory with invalid UTF-8 characters
- When I visit recent build details page
- And I click artifacts browse button
- And I navigate to parent directory of directory with invalid name
- Then I should not see directory with invalid name on the list
-
- @javascript
- Scenario: I download a single file from build artifacts
- Given recent build has artifacts available
- And recent build has artifacts metadata available
- When I visit recent build details page
- And I click artifacts browse button
- And I click a link to file within build artifacts
- Then I see a download link
-
- @javascript
- Scenario: I click on a row in an artifacts table
- Given recent build has artifacts available
- And recent build has artifacts metadata available
- When I visit recent build details page
- And I click artifacts browse button
- And I click a first row within build artifacts table
- Then page with a coresponding path is loading
diff --git a/features/project/commits/diff_comments.feature b/features/project/commits/diff_comments.feature
deleted file mode 100644
index 35687aac9ea..00000000000
--- a/features/project/commits/diff_comments.feature
+++ /dev/null
@@ -1,96 +0,0 @@
-@project_commits
-Feature: Project Commits Diff Comments
- Background:
- Given I sign in as a user
- And I own project "Shop"
- And I visit project commit page
-
- @javascript
- Scenario: I can comment on a commit diff
- Given I leave a diff comment like "Typo, please fix"
- Then I should see a diff comment saying "Typo, please fix"
-
- @javascript
- Scenario: I can add a diff comment with a single emoji
- Given I open a diff comment form
- And I write a diff comment like ":smile:"
- Then I should see a diff comment with an emoji image
-
- @javascript
- Scenario: I get a temporary form for the first comment on a diff line
- Given I open a diff comment form
- Then I should see a temporary diff comment form
-
- @javascript
- Scenario: I have a cancel button on the diff form
- Given I open a diff comment form
- Then I should see the cancel comment button
-
- @javascript
- Scenario: I can cancel a diff form
- Given I open a diff comment form
- And I cancel the diff comment
- Then I should not see the diff comment form
-
- @javascript
- Scenario: I can't open a second form for a diff line
- Given I open a diff comment form
- And I open a diff comment form
- Then I should only see one diff form
-
- @javascript
- Scenario: I can have multiple forms
- Given I open a diff comment form
- And I write a diff comment like ":-1: I don't like this"
- And I open another diff comment form
- Then I should see a diff comment form with ":-1: I don't like this"
- And I should see an empty diff comment form
-
- @javascript
- Scenario: I can preview multiple forms separately
- Given I preview a diff comment text like "Should fix it :smile:"
- And I preview another diff comment text like "DRY this up"
- Then I should see two separate previews
-
- @javascript
- Scenario: I have a reply button in discussions
- Given I leave a diff comment like "Typo, please fix"
- Then I should see a discussion reply button
-
- @javascript
- Scenario: I can preview with text
- Given I open a diff comment form
- And I write a diff comment like ":-1: I don't like this"
- Then The diff comment preview tab should display rendered Markdown
-
- @javascript
- Scenario: I preview a diff comment
- Given I preview a diff comment text like "Should fix it :smile:"
- Then I should see the diff comment preview
- And I should not see the diff comment text field
-
- @javascript
- Scenario: I can edit after preview
- Given I preview a diff comment text like "Should fix it :smile:"
- Then I should see the diff comment write tab
-
- @javascript
- Scenario: The form gets removed after posting
- Given I preview a diff comment text like "Should fix it :smile:"
- And I submit the diff comment
- Then I should not see the diff comment form
- And I should see a discussion reply button
-
- @javascript
- Scenario: I can add a comment on a side-by-side commit diff (left side)
- Given I open a diff comment form
- And I click side-by-side diff button
- When I leave a diff comment in a parallel view on the left side like "Old comment"
- Then I should see a diff comment on the left side saying "Old comment"
-
- @javascript
- Scenario: I can add a comment on a side-by-side commit diff (right side)
- Given I open a diff comment form
- And I click side-by-side diff button
- When I leave a diff comment in a parallel view on the right side like "New comment"
- Then I should see a diff comment on the right side saying "New comment"
diff --git a/features/project/deploy_keys.feature b/features/project/deploy_keys.feature
deleted file mode 100644
index 6f1ed9ff5b6..00000000000
--- a/features/project/deploy_keys.feature
+++ /dev/null
@@ -1,46 +0,0 @@
-Feature: Project Deploy Keys
- Background:
- Given I sign in as a user
- And I own project "Shop"
-
- @javascript
- Scenario: I should see deploy keys list
- Given project has deploy key
- When I visit project deploy keys page
- Then I should see project deploy key
-
- @javascript
- Scenario: I should see project deploy keys
- Given other projects have deploy keys
- When I visit project deploy keys page
- Then I should see other project deploy key
- And I should only see the same deploy key once
-
- @javascript
- Scenario: I should see public deploy keys
- Given public deploy key exists
- When I visit project deploy keys page
- Then I should see public deploy key
-
- @javascript
- Scenario: I add new deploy key
- Given I visit project deploy keys page
- And I submit new deploy key
- Then I should be on deploy keys page
- And I should see newly created deploy key
-
- @javascript
- Scenario: I attach other project deploy key to project
- Given other projects have deploy keys
- And I visit project deploy keys page
- When I click attach deploy key
- Then I should be on deploy keys page
- And I should see newly created deploy key
-
- @javascript
- Scenario: I attach public deploy key to project
- Given public deploy key exists
- And I visit project deploy keys page
- When I click attach deploy key
- Then I should be on deploy keys page
- And I should see newly created deploy key
diff --git a/features/project/ff_merge_requests.feature b/features/project/ff_merge_requests.feature
deleted file mode 100644
index 39035d551d1..00000000000
--- a/features/project/ff_merge_requests.feature
+++ /dev/null
@@ -1,41 +0,0 @@
-Feature: Project Ff Merge Requests
- Background:
- Given I sign in as a user
- And I own project "Shop"
- And project "Shop" have "Bug NS-05" open merge request with diffs inside
- And merge request "Bug NS-05" is mergeable
-
- @javascript
- Scenario: I do ff-only merge for rebased branch
- Given ff merge enabled
- And merge request "Bug NS-05" is rebased
- When I visit merge request page "Bug NS-05"
- Then I should see ff-only merge button
- When I accept this merge request
- Then I should see merged request
-
- @javascript
- Scenario: I do ff-only merge for merged branch
- Given ff merge enabled
- And merge request "Bug NS-05" merged target
- When I visit merge request page "Bug NS-05"
- Then I should see ff-only merge button
- When I accept this merge request
- Then I should see merged request
-
- @javascript
- Scenario: I do rebase before ff-only merge
- Given ff merge enabled
- And rebase before merge enabled
- When I visit merge request page "Bug NS-05"
- Then I should see rebase button
- When I press rebase button
- Then I should see rebase in progress message
-
- @javascript
- Scenario: I do rebase before regular merge
- Given rebase before merge enabled
- When I visit merge request page "Bug NS-05"
- Then I should see rebase button
- When I press rebase button
- Then I should see rebase in progress message
diff --git a/features/project/forked_merge_requests.feature b/features/project/forked_merge_requests.feature
deleted file mode 100644
index 9809b0ea0fe..00000000000
--- a/features/project/forked_merge_requests.feature
+++ /dev/null
@@ -1,51 +0,0 @@
-Feature: Project Forked Merge Requests
- Background:
- Given I sign in as a user
- And I am a member of project "Shop"
- And I have a project forked off of "Shop" called "Forked Shop"
-
- @javascript
- Scenario: I submit new unassigned merge request to a forked project
- Given I visit project "Forked Shop" merge requests page
- And I click link "New Merge Request"
- And I fill out a "Merge Request On Forked Project" merge request
- And I submit the merge request
- Then I should see merge request "Merge Request On Forked Project"
-
- # TODO: Improve it so it does not fail randomly
- #
- #@javascript
- #Scenario: I can edit a forked merge request
- #Given I visit project "Forked Shop" merge requests page
- #And I click link "New Merge Request"
- #And I fill out a "Merge Request On Forked Project" merge request
- #And I submit the merge request
- #And I should see merge request "Merge Request On Forked Project"
- #And I click link edit "Merge Request On Forked Project"
- #Then I see the edit page prefilled for "Merge Request On Forked Project"
- #And I update the merge request title
- #And I save the merge request
- #Then I should see the edited merge request
-
- Scenario: I cannot submit an invalid merge request
- Given I visit project "Forked Shop" merge requests page
- And I click link "New Merge Request"
- And I fill out an invalid "Merge Request On Forked Project" merge request
- Then I should see validation errors
-
- @javascript
- Scenario: Merge request should target fork repository by default
- Given I visit project "Forked Shop" merge requests page
- And I click link "New Merge Request"
- Then the target repository should be the original repository
-
- @javascript
- Scenario: I see the users in the target project for a new merge request
- Given I sign in as an admin
- And I have a project forked off of "Shop" called "Forked Shop"
- Then I visit project "Forked Shop" merge requests page
- And I click link "New Merge Request"
- And I fill out a "Merge Request On Forked Project" merge request
- When I click "Assign to" dropdown"
- Then I should see the target project ID in the input selector
- And I should see the users from the target project ID
diff --git a/features/project/issues/references.feature b/features/project/issues/references.feature
deleted file mode 100644
index 4ae2d653337..00000000000
--- a/features/project/issues/references.feature
+++ /dev/null
@@ -1,33 +0,0 @@
-@project_issues
-Feature: Project Issues References
- Background:
- Given I sign in as "John Doe"
- And public project "Community"
- And "John Doe" owns public project "Community"
- And project "Community" has "Community issue" open issue
- And I logout
- And I sign in as "Mary Jane"
- And private project "Enterprise"
- And "Mary Jane" owns private project "Enterprise"
- And project "Enterprise" has "Enterprise issue" open issue
- And project "Enterprise" has "Enterprise fix" open merge request
- And I visit issue page "Enterprise issue"
- And I leave a comment referencing issue "Community issue"
- And I visit merge request page "Enterprise fix"
- And I leave a comment referencing issue "Community issue"
- And I logout
-
- @javascript
- Scenario: Viewing the public issue as a "John Doe"
- Given I sign in as "John Doe"
- When I visit issue page "Community issue"
- Then I should not see any related merge requests
- And I should see no notes at all
-
- @javascript
- Scenario: Viewing the public issue as "Mary Jane"
- Given I sign in as "Mary Jane"
- When I visit issue page "Community issue"
- Then I should see the "Enterprise fix" related merge request
- And I should see a note linking to "Enterprise fix" merge request
- And I should see a note linking to "Enterprise issue" issue
diff --git a/features/project/merge_requests/references.feature b/features/project/merge_requests/references.feature
deleted file mode 100644
index 571612261a9..00000000000
--- a/features/project/merge_requests/references.feature
+++ /dev/null
@@ -1,31 +0,0 @@
-@project_merge_requests
-Feature: Project Merge Requests References
- Background:
- Given I sign in as "John Doe"
- And public project "Community"
- And "John Doe" owns public project "Community"
- And project "Community" has "Community fix" open merge request
- And I logout
- And I sign in as "Mary Jane"
- And private project "Enterprise"
- And "Mary Jane" owns private project "Enterprise"
- And project "Enterprise" has "Enterprise issue" open issue
- And project "Enterprise" has "Enterprise fix" open merge request
- And I visit issue page "Enterprise issue"
- And I leave a comment referencing issue "Community fix"
- And I visit merge request page "Enterprise fix"
- And I leave a comment referencing issue "Community fix"
- And I logout
-
- @javascript
- Scenario: Viewing the public issue as a "John Doe"
- Given I sign in as "John Doe"
- When I visit issue page "Community fix"
- Then I should see no notes at all
-
- @javascript
- Scenario: Viewing the public issue as "Mary Jane"
- Given I sign in as "Mary Jane"
- When I visit issue page "Community fix"
- And I should see a note linking to "Enterprise fix" merge request
- And I should see a note linking to "Enterprise issue" issue
diff --git a/features/steps/group/members.rb b/features/steps/group/members.rb
deleted file mode 100644
index 97bcca7730b..00000000000
--- a/features/steps/group/members.rb
+++ /dev/null
@@ -1,68 +0,0 @@
-class Spinach::Features::GroupMembers < Spinach::FeatureSteps
- include WaitForRequests
- include SharedAuthentication
- include SharedPaths
- include SharedGroup
- include SharedUser
-
- step 'I should see user "John Doe" in team list' do
- expect(group_members_list).to have_content("John Doe")
- end
-
- step 'I should not see user "Mary Jane" in team list' do
- expect(group_members_list).not_to have_content("Mary Jane")
- end
-
- step 'I click on the "Remove User From Group" button for "John Doe"' do
- find(:css, '.project-members-page li', text: "John Doe").find(:css, 'a.btn-remove').click
- # poltergeist always confirms popups.
- end
-
- step 'I click on the "Remove User From Group" button for "Mary Jane"' do
- find(:css, 'li', text: "Mary Jane").find(:css, 'a.btn-remove').click
- # poltergeist always confirms popups.
- end
-
- step 'I should not see the "Remove User From Group" button for "John Doe"' do
- expect(find(:css, '.project-members-page li', text: "John Doe")).not_to have_selector(:css, 'a.btn-remove')
- # poltergeist always confirms popups.
- end
-
- step 'I should not see the "Remove User From Group" button for "Mary Jane"' do
- expect(find(:css, 'li', text: "Mary Jane")).not_to have_selector(:css, 'a.btn-remove')
- # poltergeist always confirms popups.
- end
-
- step 'I change the "Mary Jane" role to "Developer"' do
- member = mary_jane_member
-
- page.within "#group_member_#{member.id}" do
- click_button member.human_access
-
- page.within '.dropdown-menu' do
- click_link 'Developer'
- end
-
- wait_for_requests
- end
- end
-
- step 'I should see "Mary Jane" as "Developer"' do
- member = mary_jane_member
-
- page.within "#group_member_#{member.id}" do
- expect(page).to have_content "Developer"
- end
- end
-
- private
-
- def mary_jane_member
- user = User.find_by(name: "Mary Jane")
- owned_group.members.find_by(user_id: user.id)
- end
-
- def group_members_list
- find(".panel .content-list")
- end
-end
diff --git a/features/steps/profile/notifications.rb b/features/steps/profile/notifications.rb
deleted file mode 100644
index f8eb0f01de8..00000000000
--- a/features/steps/profile/notifications.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-class Spinach::Features::ProfileNotifications < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
-
- step 'I visit profile notifications page' do
- visit profile_notifications_path
- end
-
- step 'I should see global notifications settings' do
- expect(page).to have_content "Notifications"
- end
-
- step 'I select Mention setting from dropdown' do
- first(:link, "On mention").click
- end
-
- step 'I should see Notification saved message' do
- expect(page).to have_content 'On mention'
- end
-end
diff --git a/features/steps/project/builds/artifacts.rb b/features/steps/project/builds/artifacts.rb
deleted file mode 100644
index 4b72355b125..00000000000
--- a/features/steps/project/builds/artifacts.rb
+++ /dev/null
@@ -1,98 +0,0 @@
-class Spinach::Features::ProjectBuildsArtifacts < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedBuilds
- include RepoHelpers
- include WaitForRequests
-
- step 'I click artifacts download button' do
- click_link 'Download'
- end
-
- step 'I click artifacts browse button' do
- click_link 'Browse'
- expect(page).not_to have_selector('.build-sidebar')
- end
-
- step 'I should see content of artifacts archive' do
- page.within('.tree-table') do
- expect(page).to have_no_content '..'
- expect(page).to have_content 'other_artifacts_0.1.2'
- expect(page).to have_content 'ci_artifacts.txt'
- expect(page).to have_content 'rails_sample.jpg'
- end
- end
-
- step 'I should see the build header' do
- page.within('.build-header') do
- expect(page).to have_content "Job ##{@build.id} in pipeline ##{@pipeline.id} for #{@pipeline.short_sha}"
- end
- end
-
- step 'I click link to subdirectory within build artifacts' do
- page.within('.tree-table') { click_link 'other_artifacts_0.1.2' }
- end
-
- step 'I should see content of subdirectory within artifacts archive' do
- page.within('.tree-table') do
- expect(page).to have_content '..'
- expect(page).to have_content 'another-subdirectory'
- expect(page).to have_content 'doc_sample.txt'
- end
- end
-
- step 'I should see the directory name in the breadcrumb' do
- page.within('.repo-breadcrumb') do
- expect(page).to have_content 'other_artifacts_0.1.2'
- end
- end
-
- step 'recent build artifacts contain directory with UTF-8 characters' do
- # metadata fixture contains relevant directory
- end
-
- step 'I navigate to directory with UTF-8 characters in name' do
- page.within('.tree-table') { click_link 'tests_encoding' }
- page.within('.tree-table') { click_link 'utf8 test dir ✓' }
- end
-
- step 'I should see content of directory with UTF-8 characters in name' do
- page.within('.tree-table') do
- expect(page).to have_content '..'
- expect(page).to have_content 'regular_file_2'
- end
- end
-
- step 'recent build artifacts contain directory with invalid UTF-8 characters' do
- # metadata fixture contains relevant directory
- end
-
- step 'I navigate to parent directory of directory with invalid name' do
- page.within('.tree-table') { click_link 'tests_encoding' }
- end
-
- step 'I should not see directory with invalid name on the list' do
- page.within('.tree-table') do
- expect(page).to have_no_content('non-utf8-dir')
- end
- end
-
- step 'I click a link to file within build artifacts' do
- page.within('.tree-table') { find_link('ci_artifacts.txt').click }
- wait_for_requests
- end
-
- step 'I see a download link' do
- expect(page).to have_link 'download it'
- end
-
- step 'I click a first row within build artifacts table' do
- row = first('tr[data-link]')
- @row_path = row['data-link']
- row.click
- end
-
- step 'page with a coresponding path is loading' do
- expect(current_path).to eq @row_path
- end
-end
diff --git a/features/steps/project/commits/branches.rb b/features/steps/project/commits/branches.rb
deleted file mode 100644
index 3ecd4c8b672..00000000000
--- a/features/steps/project/commits/branches.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedPaths
-
- step 'I click link "All"' do
- click_link "All"
- end
-
- step 'I click link "Protected"' do
- click_link "Protected"
- end
-
- step 'I click new branch link' do
- click_link "New branch"
- end
-
- step 'I submit new branch form with invalid name' do
- fill_in 'branch_name', with: '1.0 stable'
- page.find("body").click # defocus the branch_name input
- select_branch('master')
- click_button 'Create branch'
- end
-
- def select_branch(branch_name)
- find('.git-revision-dropdown-toggle').click
-
- page.within '#new-branch-form .dropdown-menu' do
- click_link branch_name
- end
- end
-end
diff --git a/features/steps/project/commits/comments.rb b/features/steps/project/commits/comments.rb
deleted file mode 100644
index 3d4d8ad6368..00000000000
--- a/features/steps/project/commits/comments.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-class Spinach::Features::ProjectCommitsComments < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedNote
- include SharedPaths
- include SharedProject
-end
diff --git a/features/steps/project/commits/diff_comments.rb b/features/steps/project/commits/diff_comments.rb
deleted file mode 100644
index b9d8cf2c5a5..00000000000
--- a/features/steps/project/commits/diff_comments.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-class Spinach::Features::ProjectCommitsDiffComments < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedDiffNote
- include SharedPaths
- include SharedProject
-end
diff --git a/features/steps/project/create.rb b/features/steps/project/create.rb
deleted file mode 100644
index 60fa232672e..00000000000
--- a/features/steps/project/create.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-class Spinach::Features::ProjectCreate < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedUser
-
- step 'fill project form with valid data' do
- fill_in 'project_path', with: 'Empty'
- page.within '#content-body' do
- click_button "Create project"
- end
- end
-
- step 'I should see project page' do
- expect(page).to have_content "Empty"
- expect(current_path).to eq project_path(Project.last)
- end
-
- step 'I should see empty project instructions' do
- expect(page).to have_content "git init"
- expect(page).to have_content "git remote"
- expect(page).to have_content Project.last.url_to_repo
- end
-end
diff --git a/features/steps/project/deploy_keys.rb b/features/steps/project/deploy_keys.rb
deleted file mode 100644
index 9db31522c5c..00000000000
--- a/features/steps/project/deploy_keys.rb
+++ /dev/null
@@ -1,89 +0,0 @@
-class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedPaths
-
- step 'project has deploy key' do
- create(:deploy_keys_project, project: @project)
- end
-
- step 'I should see project deploy key' do
- page.within(find('.deploy-keys')) do
- expect(page).to have_content deploy_key.title
- end
- end
-
- step 'I should see other project deploy key' do
- page.within(find('.deploy-keys')) do
- expect(page).to have_content other_deploy_key.title
- end
- end
-
- step 'I should see public deploy key' do
- page.within(find('.deploy-keys')) do
- expect(page).to have_content public_deploy_key.title
- end
- end
-
- step 'I click \'New Deploy Key\'' do
- click_link 'New deploy key'
- end
-
- step 'I submit new deploy key' do
- fill_in "deploy_key_title", with: "laptop"
- fill_in "deploy_key_key", with: "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzrEJUIR6Y03TCE9rIJ+GqTBvgb8t1jI9h5UBzCLuK4VawOmkLornPqLDrGbm6tcwM/wBrrLvVOqi2HwmkKEIecVO0a64A4rIYScVsXIniHRS6w5twyn1MD3sIbN+socBDcaldECQa2u1dI3tnNVcs8wi77fiRe7RSxePsJceGoheRQgC8AZ510UdIlO+9rjIHUdVN7LLyz512auAfYsgx1OfablkQ/XJcdEwDNgi9imI6nAXhmoKUm1IPLT2yKajTIC64AjLOnE0YyCh6+7RFMpiMyu1qiOCpdjYwTgBRiciNRZCH8xIedyCoAmiUgkUT40XYHwLuwiPJICpkAzp7Q== user@laptop"
- click_button "Add key"
- end
-
- step 'I should be on deploy keys page' do
- expect(current_path).to eq project_settings_repository_path(@project)
- end
-
- step 'I should see newly created deploy key' do
- @project.reload
- page.within(find('.deploy-keys')) do
- expect(page).to have_content(deploy_key.title)
- end
- end
-
- step 'other projects have deploy keys' do
- @second_project = create(:project, namespace: create(:group))
- @second_project.add_master(current_user)
- create(:deploy_keys_project, project: @second_project)
-
- @third_project = create(:project, namespace: create(:group))
- @third_project.add_master(current_user)
- create(:deploy_keys_project, project: @third_project, deploy_key: @second_project.deploy_keys.first)
- end
-
- step 'I should only see the same deploy key once' do
- page.within(find('.deploy-keys')) do
- expect(page).to have_selector('ul li', count: 1)
- end
- end
-
- step 'public deploy key exists' do
- create(:deploy_key, public: true)
- end
-
- step 'I click attach deploy key' do
- page.within(find('.deploy-keys')) do
- click_button 'Enable'
- expect(page).not_to have_selector('.fa-spinner')
- end
- end
-
- protected
-
- def deploy_key
- @project.deploy_keys.last
- end
-
- def other_deploy_key
- @second_project.deploy_keys.last
- end
-
- def public_deploy_key
- DeployKey.are_public.last
- end
-end
diff --git a/features/steps/project/ff_merge_requests.rb b/features/steps/project/ff_merge_requests.rb
deleted file mode 100644
index 27efcfd65b6..00000000000
--- a/features/steps/project/ff_merge_requests.rb
+++ /dev/null
@@ -1,87 +0,0 @@
-class Spinach::Features::ProjectFfMergeRequests < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedIssuable
- include SharedProject
- include SharedNote
- include SharedPaths
- include SharedMarkdown
- include SharedDiffNote
- include SharedUser
- include WaitForRequests
-
- step 'project "Shop" have "Bug NS-05" open merge request with diffs inside' do
- create(:merge_request_with_diffs,
- title: "Bug NS-05",
- source_project: project,
- target_project: project,
- author: project.users.first)
- end
-
- step 'merge request is mergeable' do
- expect(page).to have_button 'Merge'
- end
-
- step 'I should see ff-only merge button' do
- expect(page).to have_content "Fast-forward merge without a merge commit"
- expect(page).to have_button 'Merge'
- end
-
- step 'merge request "Bug NS-05" is mergeable' do
- merge_request.mark_as_mergeable
- end
-
- step 'I accept this merge request' do
- page.within '.mr-state-widget' do
- click_button "Merge"
- end
- end
-
- step 'I should see merged request' do
- page.within '.status-box' do
- expect(page).to have_content "Merged"
- wait_for_requests
- end
- end
-
- step 'ff merge enabled' do
- project = merge_request.target_project
- project.merge_requests_ff_only_enabled = true
- project.save!
- end
-
- step 'I should see rebase button' do
- expect(page).to have_button "Rebase"
- end
-
- step 'merge request "Bug NS-05" is rebased' do
- merge_request.source_branch = 'flatten-dir'
- merge_request.target_branch = 'improve/awesome'
- merge_request.reload_diff
- merge_request.save!
- end
-
- step 'merge request "Bug NS-05" merged target' do
- merge_request.source_branch = 'merged-target'
- merge_request.target_branch = 'improve/awesome'
- merge_request.reload_diff
- merge_request.save!
- end
-
- step 'rebase before merge enabled' do
- project = merge_request.target_project
- project.merge_requests_rebase_enabled = true
- project.save!
- end
-
- step 'I press rebase button' do
- click_button "Rebase"
- end
-
- step "I should see rebase in progress message" do
- expect(page).to have_content("Rebase in progress")
- end
-
- def merge_request
- @merge_request ||= MergeRequest.find_by!(title: "Bug NS-05")
- end
-end
diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb
deleted file mode 100644
index 82b931b2246..00000000000
--- a/features/steps/project/forked_merge_requests.rb
+++ /dev/null
@@ -1,139 +0,0 @@
-class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedNote
- include SharedPaths
- include Select2Helper
- include WaitForRequests
- include ProjectForksHelper
-
- step 'I am a member of project "Shop"' do
- @project = ::Project.find_by(name: "Shop")
- @project ||= create(:project, :repository, name: "Shop")
- @project.add_reporter(@user)
- end
-
- step 'I have a project forked off of "Shop" called "Forked Shop"' do
- @forked_project = fork_project(@project, @user,
- namespace: @user.namespace,
- repository: true)
- end
-
- step 'I click link "New Merge Request"' do
- page.within '#content-body' do
- page.has_link?('New Merge Request') ? click_link("New Merge Request") : click_link('New merge request')
- end
- end
-
- step 'I should see merge request "Merge Request On Forked Project"' do
- expect(@project.merge_requests.size).to be >= 1
- @merge_request = @project.merge_requests.last
- expect(current_path).to eq project_merge_request_path(@project, @merge_request)
- expect(@merge_request.title).to eq "Merge Request On Forked Project"
- expect(@merge_request.source_project).to eq @forked_project
- expect(@merge_request.source_branch).to eq "fix"
- expect(@merge_request.target_branch).to eq "master"
- expect(page).to have_content @forked_project.full_path
- expect(page).to have_content @project.full_path
- expect(page).to have_content @merge_request.source_branch
- expect(page).to have_content @merge_request.target_branch
-
- wait_for_requests
- end
-
- step 'I fill out a "Merge Request On Forked Project" merge request' do
- expect(page).to have_content('Source branch')
- expect(page).to have_content('Target branch')
-
- first('.js-source-project').click
- first('.dropdown-source-project a', text: @forked_project.full_path)
-
- first('.js-target-project').click
- first('.dropdown-target-project a', text: @project.full_path)
-
- first('.js-source-branch').click
- wait_for_requests
- first('.js-source-branch-dropdown .dropdown-content a', text: 'fix').click
-
- click_button "Compare branches and continue"
-
- expect(page).to have_css("h3.page-title", text: "New Merge Request")
-
- page.within 'form#new_merge_request' do
- fill_in "merge_request_title", with: "Merge Request On Forked Project"
- end
- end
-
- step 'I submit the merge request' do
- click_button "Submit merge request"
- end
-
- step 'I update the merge request title' do
- fill_in "merge_request_title", with: "An Edited Forked Merge Request"
- end
-
- step 'I save the merge request' do
- click_button "Save changes"
- end
-
- step 'I should see the edited merge request' do
- expect(page).to have_content "An Edited Forked Merge Request"
- expect(@project.merge_requests.size).to be >= 1
- @merge_request = @project.merge_requests.last
- expect(current_path).to eq project_merge_request_path(@project, @merge_request)
- expect(@merge_request.source_project).to eq @forked_project
- expect(@merge_request.source_branch).to eq "fix"
- expect(@merge_request.target_branch).to eq "master"
- expect(page).to have_content @forked_project.full_path
- expect(page).to have_content @project.full_path
- expect(page).to have_content @merge_request.source_branch
- expect(page).to have_content @merge_request.target_branch
- end
-
- step 'I should see last push widget' do
- expect(page).to have_content "You pushed to new_design"
- expect(page).to have_link "Create Merge Request"
- end
-
- step 'I click link edit "Merge Request On Forked Project"' do
- find("#edit_merge_request").click
- end
-
- step 'I see the edit page prefilled for "Merge Request On Forked Project"' do
- expect(current_path).to eq edit_project_merge_request_path(@project, @merge_request)
- expect(page).to have_content "Edit merge request #{@merge_request.to_reference}"
- expect(find("#merge_request_title").value).to eq "Merge Request On Forked Project"
- end
-
- step 'I fill out an invalid "Merge Request On Forked Project" merge request' do
- expect(find_by_id("merge_request_source_project_id", visible: false).value).to eq @forked_project.id.to_s
- expect(find_by_id("merge_request_target_project_id", visible: false).value).to eq @project.id.to_s
- expect(find_by_id("merge_request_source_branch", visible: false).value).to eq nil
- expect(find_by_id("merge_request_target_branch", visible: false).value).to eq "master"
- click_button "Compare branches"
- end
-
- step 'I should see validation errors' do
- expect(page).to have_content "You must select source and target branch"
- end
-
- step 'the target repository should be the original repository' do
- expect(find_by_id("merge_request_target_project_id").value).to eq "#{@project.id}"
- end
-
- step 'I click "Assign to" dropdown"' do
- click_button 'Assignee'
- end
-
- step 'I should see the target project ID in the input selector' do
- expect(find('.js-assignee-search')["data-project-id"]).to eq "#{@project.id}"
- end
-
- step 'I should see the users from the target project ID' do
- page.within '.dropdown-menu-user' do
- expect(page).to have_content 'Unassigned'
- expect(page).to have_content current_user.name
- expect(page).to have_content @project.users.first.name
- end
- end
-end
diff --git a/features/steps/project/issues/filter_labels.rb b/features/steps/project/issues/filter_labels.rb
deleted file mode 100644
index b467af53c98..00000000000
--- a/features/steps/project/issues/filter_labels.rb
+++ /dev/null
@@ -1,61 +0,0 @@
-class Spinach::Features::ProjectIssuesFilterLabels < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedPaths
- include Select2Helper
-
- step 'I should see "Bugfix1" in issues list' do
- page.within ".issues-list" do
- expect(page).to have_content "Bugfix1"
- end
- end
-
- step 'I should see "Bugfix2" in issues list' do
- page.within ".issues-list" do
- expect(page).to have_content "Bugfix2"
- end
- end
-
- step 'I should not see "Bugfix2" in issues list' do
- page.within ".issues-list" do
- expect(page).not_to have_content "Bugfix2"
- end
- end
-
- step 'I should not see "Feature1" in issues list' do
- page.within ".issues-list" do
- expect(page).not_to have_content "Feature1"
- end
- end
-
- step 'I click "dropdown close button"' do
- page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
- sleep 2
- end
-
- step 'I click link "feature"' do
- page.within ".labels-filter" do
- click_link "feature"
- end
- end
-
- step 'project "Shop" has issue "Bugfix1" with labels: "bug", "feature"' do
- project = Project.find_by(name: "Shop")
- issue = create(:issue, title: "Bugfix1", project: project)
- issue.labels << project.labels.find_by(title: 'bug')
- issue.labels << project.labels.find_by(title: 'feature')
- end
-
- step 'project "Shop" has issue "Bugfix2" with labels: "bug", "enhancement"' do
- project = Project.find_by(name: "Shop")
- issue = create(:issue, title: "Bugfix2", project: project)
- issue.labels << project.labels.find_by(title: 'bug')
- issue.labels << project.labels.find_by(title: 'enhancement')
- end
-
- step 'project "Shop" has issue "Feature1" with labels: "feature"' do
- project = Project.find_by(name: "Shop")
- issue = create(:issue, title: "Feature1", project: project)
- issue.labels << project.labels.find_by(title: 'feature')
- end
-end
diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb
deleted file mode 100644
index baa78c23203..00000000000
--- a/features/steps/project/issues/issues.rb
+++ /dev/null
@@ -1,175 +0,0 @@
-class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedIssuable
- include SharedProject
- include SharedNote
- include SharedPaths
- include SharedMarkdown
- include SharedUser
-
- step 'I should not see "Release 0.3" in issues' do
- expect(page).not_to have_content "Release 0.3"
- end
-
- step 'I click link "Closed"' do
- find('.issues-state-filters [data-state="closed"] span', text: 'Closed').click
- end
-
- step 'I should see "Release 0.3" in issues' do
- expect(page).to have_content "Release 0.3"
- end
-
- step 'I should not see "Release 0.4" in issues' do
- expect(page).not_to have_content "Release 0.4"
- end
-
- step 'I click link "All"' do
- find('.issues-state-filters [data-state="all"] span', text: 'All').click
- # Waits for load
- expect(find('.issues-state-filters > .active')).to have_content 'All'
- end
-
- step 'I should see issue "Tweet control"' do
- expect(page).to have_content "Tweet control"
- end
-
- step 'I click "author" dropdown' do
- page.find('.js-author-search').click
- sleep 1
- end
-
- step 'I see current user as the first user' do
- expect(page).to have_selector('.dropdown-content', visible: true)
- users = page.all('.dropdown-menu-author .dropdown-content li a')
- expect(users[0].text).to eq 'Any Author'
- expect(users[1].text).to eq "#{current_user.name} #{current_user.to_reference}"
- end
-
- step 'I click link "500 error on profile"' do
- click_link "500 error on profile"
- end
-
- step 'I should see label \'bug\' with issue' do
- page.within '.issuable-show-labels' do
- expect(page).to have_content 'bug'
- end
- end
-
- step 'I fill in issue search with "Re"' do
- filter_issue "Re"
- end
-
- step 'I fill in issue search with "Bu"' do
- filter_issue "Bu"
- end
-
- step 'I fill in issue search with ".3"' do
- filter_issue ".3"
- end
-
- step 'I fill in issue search with "Something"' do
- filter_issue "Something"
- end
-
- step 'I fill in issue search with ""' do
- filter_issue ""
- end
-
- step 'project "Shop" has milestone "v2.2"' do
- milestone = create(:milestone, title: "v2.2", project: project)
-
- 3.times { create(:issue, project: project, milestone: milestone) }
- end
-
- step 'project "Shop" has milestone "v3.0"' do
- milestone = create(:milestone, title: "v3.0", project: project)
-
- 3.times { create(:issue, project: project, milestone: milestone) }
- end
-
- When 'I select milestone "v3.0"' do
- select "v3.0", from: "milestone_id"
- end
-
- step 'I should see selected milestone with title "v3.0"' do
- issues_milestone_selector = "#issue_milestone_id_chzn > a"
- expect(find(issues_milestone_selector)).to have_content("v3.0")
- end
-
- When 'I select first assignee from "Shop" project' do
- first_assignee = project.users.first
- select first_assignee.name, from: "assignee_id"
- end
-
- step 'I should see first assignee from "Shop" as selected assignee' do
- issues_assignee_selector = "#issue_assignee_id_chzn > a"
-
- assignee_name = project.users.first.name
- expect(find(issues_assignee_selector)).to have_content(assignee_name)
- end
-
- step 'The list should be sorted by "Least popular"' do
- page.within '.issues-list' do
- page.within 'li.issue:nth-child(1)' do
- expect(page).to have_content 'Tweet control'
- expect(page).to have_content '1 2'
- end
-
- page.within 'li.issue:nth-child(2)' do
- expect(page).to have_content 'Release 0.4'
- expect(page).to have_content '2 1'
- end
-
- page.within 'li.issue:nth-child(3)' do
- expect(page).to have_content 'Bugfix'
- expect(page).not_to have_content '0 0'
- end
- end
- end
-
- When 'I visit empty project page' do
- project = Project.find_by(name: 'Empty Project')
- visit project_path(project)
- end
-
- When "I visit project \"Community\" issues page" do
- project = Project.find_by(name: 'Community')
- visit project_issues_path(project)
- end
-
- step 'project \'Shop\' has issue \'Bugfix1\' with description: \'Description for issue1\'' do
- create(:issue, title: 'Bugfix1', description: 'Description for issue1', project: project)
- end
-
- step 'project \'Shop\' has issue \'Feature1\' with description: \'Feature submitted for issue1\'' do
- create(:issue, title: 'Feature1', description: 'Feature submitted for issue1', project: project)
- end
-
- step 'I fill in issue search with \'Description for issue1\'' do
- filter_issue 'Description for issue'
- end
-
- step 'I fill in issue search with \'issue1\'' do
- filter_issue 'issue1'
- end
-
- step 'I fill in issue search with \'Rock and roll\'' do
- filter_issue 'Rock and roll'
- end
-
- step 'I should see \'Bugfix1\' in issues' do
- expect(page).to have_content 'Bugfix1'
- end
-
- step 'I should see \'Feature1\' in issues' do
- expect(page).to have_content 'Feature1'
- end
-
- step 'I should not see \'Bugfix1\' in issues' do
- expect(page).not_to have_content 'Bugfix1'
- end
-
- def filter_issue(text)
- fill_in 'issuable_search', with: text
- end
-end
diff --git a/features/steps/project/issues/milestones.rb b/features/steps/project/issues/milestones.rb
deleted file mode 100644
index 30927306a4f..00000000000
--- a/features/steps/project/issues/milestones.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedPaths
- include SharedMarkdown
-
- step 'project "Shop" has milestone "v2.2"' do
- project = Project.find_by(name: "Shop")
- milestone = create(:milestone,
- title: "v2.2",
- project: project,
- description: "# Description header"
- )
- 3.times { create(:issue, project: project, milestone: milestone) }
- end
-
- When 'I click link "All Issues"' do
- click_link 'All Issues'
- end
-end
diff --git a/features/steps/project/issues/references.rb b/features/steps/project/issues/references.rb
deleted file mode 100644
index 69e8b5cbde5..00000000000
--- a/features/steps/project/issues/references.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-class Spinach::Features::ProjectIssuesReferences < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedIssuable
- include SharedNote
- include SharedProject
- include SharedUser
-end
diff --git a/features/steps/project/merge_requests/references.rb b/features/steps/project/merge_requests/references.rb
deleted file mode 100644
index ab2ae6847a2..00000000000
--- a/features/steps/project/merge_requests/references.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-class Spinach::Features::ProjectMergeRequestsReferences < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedIssuable
- include SharedNote
- include SharedProject
- include SharedUser
-end
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
deleted file mode 100644
index afaad4b255e..00000000000
--- a/features/steps/project/source/browse_files.rb
+++ /dev/null
@@ -1,435 +0,0 @@
-# coding: utf-8
-class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedPaths
- include RepoHelpers
- include WaitForRequests
-
- step "I don't have write access" do
- @project = create(:project, :repository, name: "Other Project", path: "other-project")
- @project.add_reporter(@user)
- visit project_tree_path(@project, root_ref)
- end
-
- step 'I should see files from repository' do
- expect(page).to have_content "VERSION"
- expect(page).to have_content ".gitignore"
- expect(page).to have_content "LICENSE"
- end
-
- step 'I should see files from repository for "6d39438"' do
- expect(current_path).to eq project_tree_path(@project, "6d39438")
- expect(page).to have_content ".gitignore"
- expect(page).to have_content "LICENSE"
- end
-
- step 'I see the ".gitignore"' do
- expect(page).to have_content '.gitignore'
- end
-
- step 'I don\'t see the ".gitignore"' do
- expect(page).not_to have_content '.gitignore'
- end
-
- step 'I click on ".gitignore" file in repo' do
- click_link ".gitignore"
- end
-
- step 'I should see its content' do
- wait_for_requests
- expect(page).to have_content old_gitignore_content
- end
-
- step 'I should see its new content' do
- wait_for_requests
- expect(page).to have_content new_gitignore_content
- end
-
- step 'I click link "Raw"' do
- click_link 'Open raw'
- end
-
- step 'I should see raw file content' do
- expect(source).to eq '' # Body is filled in by gitlab-workhorse
- end
-
- step 'I click button "Edit"' do
- find('.js-edit-blob').click
- end
-
- step 'I cannot see the edit button' do
- expect(page).not_to have_link 'edit'
- end
-
- step 'I click button "Fork"' do
- click_link 'Fork'
- end
-
- step 'I edit code' do
- expect(page).to have_selector('.file-editor')
- set_new_content
- end
-
- step 'I fill the new file name' do
- fill_in :file_name, with: new_file_name
- end
-
- step 'I fill the new branch name' do
- fill_in :branch_name, with: 'new_branch_name', visible: true
- end
-
- step 'I fill the new file name with a new directory' do
- fill_in :file_name, with: new_file_name_with_directory
- end
-
- step 'I fill the commit message' do
- fill_in :commit_message, with: 'New commit message', visible: true
- end
-
- step 'I click link "Diff"' do
- click_link 'Preview changes'
- end
-
- step 'I click on "Commit changes"' do
- click_button 'Commit changes'
- end
-
- step 'I click on "Changes" tab' do
- click_link 'Changes'
- end
-
- step 'I click on "Create directory"' do
- click_button 'Create directory'
- end
-
- step 'I click on "Delete"' do
- click_on 'Delete'
- end
-
- step 'I click on "Delete file"' do
- click_button 'Delete file'
- end
-
- step 'I click on "Replace"' do
- click_on "Replace"
- end
-
- step 'I click on "Replace file"' do
- click_button 'Replace file'
- end
-
- step 'I see diff' do
- expect(page).to have_css '.line_holder.new'
- end
-
- step 'I click on "New file" link in repo' do
- find('.add-to-tree').click
- click_link 'New file'
- expect(page).to have_selector('.file-editor')
- end
-
- step 'I click on "Upload file" link in repo' do
- find('.add-to-tree').click
- click_link 'Upload file'
- end
-
- step 'I click on "New directory" link in repo' do
- find('.add-to-tree').click
- click_link 'New directory'
- end
-
- step 'I fill the new directory name' do
- fill_in :dir_name, with: new_dir_name
- end
-
- step 'I fill an existing directory name' do
- fill_in :dir_name, with: 'files'
- end
-
- step 'I can see new file page' do
- expect(page).to have_content "New File"
- expect(page).to have_content "Commit message"
- end
-
- step 'I click on "Upload file"' do
- click_button 'Upload file'
- end
-
- step 'I can see the new commit message' do
- expect(page).to have_content "New commit message"
- end
-
- step 'I upload a new text file' do
- drop_in_dropzone test_text_file
- end
-
- step 'I fill the upload file commit message' do
- page.within('#modal-upload-blob') do
- fill_in :commit_message, with: 'New commit message'
- end
- end
-
- step 'I replace it with a text file' do
- drop_in_dropzone test_text_file
- end
-
- step 'I fill the replace file commit message' do
- page.within('#modal-upload-blob') do
- fill_in :commit_message, with: 'Replacement file commit message'
- end
- end
-
- step 'I can see the replacement commit message' do
- expect(page).to have_content "Replacement file commit message"
- end
-
- step 'I can see the new text file' do
- expect(page).to have_content "Lorem ipsum dolor sit amet"
- expect(page).to have_content "Sed ut perspiciatis unde omnis"
- end
-
- step 'I click on files directory' do
- click_link 'files'
- end
-
- step 'I click on History link' do
- click_link 'History'
- end
-
- step 'I see Browse dir link' do
- expect(page).to have_link 'Browse Directory'
- expect(page).not_to have_link 'Browse Code'
- end
-
- step 'I click on readme file' do
- page.within '.tree-table' do
- click_link 'README.md'
- end
- end
-
- step 'I see Browse file link' do
- expect(page).to have_link 'Browse File'
- expect(page).not_to have_link 'Browse Files'
- end
-
- step 'I see Browse code link' do
- expect(page).to have_link 'Browse Files'
- expect(page).not_to have_link 'Browse Directory'
- end
-
- step 'I click on Permalink' do
- click_link 'Permalink'
- end
-
- step 'I am redirected to the files URL' do
- expect(current_path).to eq project_tree_path(@project, 'master')
- end
-
- step 'I am redirected to the ".gitignore"' do
- expect(current_path).to eq(project_blob_path(@project, 'master/.gitignore'))
- end
-
- step 'I am redirected to the permalink URL' do
- expect(current_path).to(
- eq(project_blob_path(@project,
- @project.repository.commit.sha +
- '/.gitignore'))
- )
- end
-
- step 'I am redirected to the new file' do
- expect(current_path).to eq(
- project_blob_path(@project, 'master/' + new_file_name))
- end
-
- step 'I am redirected to the new file with directory' do
- expect(current_path).to eq(
- project_blob_path(@project, 'master/' + new_file_name_with_directory))
- end
-
- step 'I am redirected to the new merge request page' do
- expect(current_path).to eq(project_new_merge_request_path(@project))
- end
-
- step "I am redirected to the fork's new merge request page" do
- fork = @user.fork_of(@project)
- expect(current_path).to eq(project_new_merge_request_path(fork))
- end
-
- step 'I am redirected to the root directory' do
- expect(current_path).to eq(
- project_tree_path(@project, 'master'))
- end
-
- step "I don't see the permalink link" do
- expect(page).not_to have_link('permalink')
- end
-
- step 'I see "Unable to create directory"' do
- expect(page).to have_content('A directory with this name already exists')
- end
-
- step 'I see "Path can contain only..."' do
- expect(page).to have_content('Path can contain only')
- end
-
- step 'I see a commit error message' do
- expect(page).to have_content('Your changes could not be committed')
- end
-
- step "I switch ref to 'test'" do
- first('.js-project-refs-dropdown').click
-
- page.within '.project-refs-form' do
- click_link "'test'"
- end
- end
-
- step "I switch ref to fix" do
- first('.js-project-refs-dropdown').click
-
- page.within '.project-refs-form' do
- click_link 'fix'
- end
- end
-
- step "I see the ref 'test' has been selected" do
- expect(page).to have_selector '.dropdown-toggle-text', text: "'test'"
- end
-
- step "I visit the 'test' tree" do
- visit project_tree_path(@project, "'test'")
- end
-
- step "I visit the fix tree" do
- visit project_tree_path(@project, "fix/.testdir")
- end
-
- step 'I see the commit data' do
- expect(page).to have_css('.tree-commit-link', visible: true)
- expect(page).not_to have_content('Loading commit data...')
- end
-
- step 'I see the commit data for a directory with a leading dot' do
- expect(page).to have_css('.tree-commit-link', visible: true)
- expect(page).not_to have_content('Loading commit data...')
- end
-
- step 'I click on "files/lfs/lfs_object.iso" file in repo' do
- allow_any_instance_of(Project).to receive(:lfs_enabled?).and_return(true)
- visit project_tree_path(@project, "lfs")
- click_link 'files'
- click_link "lfs"
- click_link "lfs_object.iso"
- end
-
- step 'I should see download link and object size' do
- expect(page).to have_content 'Download (1.5 MB)'
- end
-
- step 'I should not see lfs pointer details' do
- expect(page).not_to have_content 'version https://git-lfs.github.com/spec/v1'
- expect(page).not_to have_content 'oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897'
- expect(page).not_to have_content 'size 1575078'
- end
-
- step 'I should see buttons for allowed commands' do
- page.within '.content' do
- expect(page).to have_link 'Download'
- expect(page).to have_content 'History'
- expect(page).to have_content 'Permalink'
- expect(page).not_to have_content 'Edit'
- expect(page).not_to have_content 'Blame'
- expect(page).to have_content 'Delete'
- expect(page).to have_content 'Replace'
- end
- end
-
- step 'I should see a Fork/Cancel combo' do
- expect(page).to have_link 'Fork'
- expect(page).to have_button 'Cancel'
- end
-
- step 'I should see a notice about a new fork having been created' do
- expect(page).to have_content "You're not allowed to make changes to this project directly. A fork of this project has been created that you can make changes in, so you can submit a merge request."
- end
-
- # SVG files
- step 'I upload a new SVG file' do
- drop_in_dropzone test_svg_file
- end
-
- step 'I visit the SVG file' do
- visit project_blob_path(@project, 'new_branch_name/logo_sample.svg')
- end
-
- step 'I can see the new rendered SVG image' do
- expect(page).to have_css('.file-content img')
- end
-
- private
-
- def set_new_content
- find('#editor')
- execute_script("ace.edit('editor').setValue('#{new_gitignore_content}')")
- end
-
- # Content of the gitignore file on the seed repository.
- def old_gitignore_content
- '*.rbc'
- end
-
- # Constant value that differs from the content
- # of the gitignore of the seed repository.
- def new_gitignore_content
- old_gitignore_content + 'a'
- end
-
- # Constant value that is a valid filename and
- # not a filename present at root of the seed repository.
- def new_file_name
- 'not_a_file.md'
- end
-
- # Constant value that is a valid filename with directory and
- # not a filename present at root of the seed repository.
- def new_file_name_with_directory
- 'foo/bar/baz.txt'
- end
-
- # Constant value that is a valid directory and
- # not a directory present at root of the seed repository.
- def new_dir_name
- 'new_dir/subdir'
- end
-
- def drop_in_dropzone(file_path)
- # Generate a fake input selector
- page.execute_script <<-JS
- var fakeFileInput = window.$('<input/>').attr(
- {id: 'fakeFileInput', type: 'file'}
- ).appendTo('body');
- JS
- # Attach the file to the fake input selector with Capybara
- attach_file("fakeFileInput", file_path)
- # Add the file to a fileList array and trigger the fake drop event
- page.execute_script <<-JS
- var fileList = [$('#fakeFileInput')[0].files[0]];
- var e = jQuery.Event('drop', { dataTransfer : { files : fileList } });
- $('.dropzone')[0].dropzone.listeners[0].events.drop(e);
- JS
- end
-
- def test_text_file
- File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt')
- end
-
- def test_image_file
- File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif')
- end
-
- def test_svg_file
- File.join(Rails.root, 'spec', 'fixtures', 'logo_sample.svg')
- end
-end
diff --git a/features/steps/shared/active_tab.rb b/features/steps/shared/active_tab.rb
deleted file mode 100644
index 104d024fee2..00000000000
--- a/features/steps/shared/active_tab.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-module SharedActiveTab
- include Spinach::DSL
- include WaitForRequests
-
- after do
- wait_for_requests if javascript_test?
- end
-
- def ensure_active_main_tab(content)
- expect(find('.sidebar-top-level-items > li.active')).to have_content(content)
- end
-
- def ensure_active_sub_tab(content)
- expect(find('.sidebar-sub-level-items > li.active:not(.fly-out-top-item)')).to have_content(content)
- end
-
- def ensure_active_sub_nav(content)
- expect(find('.layout-nav .controls li.active')).to have_content(content)
- end
-
- step 'no other main tabs should be active' do
- expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1)
- end
-
- step 'no other sub tabs should be active' do
- expect(page).to have_selector('.sidebar-sub-level-items > li.active:not(.fly-out-top-item)', count: 1)
- end
-
- step 'no other sub navs should be active' do
- expect(page).to have_selector('.layout-nav .controls li.active', count: 1)
- end
-end
diff --git a/features/steps/shared/admin.rb b/features/steps/shared/admin.rb
deleted file mode 100644
index ac0a1764147..00000000000
--- a/features/steps/shared/admin.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-module SharedAdmin
- include Spinach::DSL
-
- step 'there are projects in system' do
- 2.times { create(:project, :repository) }
- end
-
- step 'system has users' do
- 2.times { create(:user) }
- end
-end
diff --git a/features/steps/shared/authentication.rb b/features/steps/shared/authentication.rb
deleted file mode 100644
index 97fac595d8e..00000000000
--- a/features/steps/shared/authentication.rb
+++ /dev/null
@@ -1,75 +0,0 @@
-require Rails.root.join('features', 'support', 'login_helpers')
-
-module SharedAuthentication
- include Spinach::DSL
- include LoginHelpers
-
- step 'I sign in as a user' do
- sign_out(@user) if @user
-
- @user = create(:user)
- sign_in(@user)
- end
-
- step 'I sign in via the UI' do
- gitlab_sign_in(create(:user))
- end
-
- step 'I sign in as an admin' do
- sign_out(@user) if @user
-
- @user = create(:admin)
- sign_in(@user)
- end
-
- step 'I sign in as "John Doe"' do
- gitlab_sign_in(user_exists("John Doe"))
- end
-
- step 'I sign in as "Mary Jane"' do
- gitlab_sign_in(user_exists("Mary Jane"))
- end
-
- step 'I should be redirected to sign in page' do
- expect(current_path).to eq new_user_session_path
- end
-
- step "I logout" do
- gitlab_sign_out
- end
-
- step "I logout directly" do
- gitlab_sign_out
- end
-
- def current_user
- @user || User.reorder(nil).first
- end
-
- private
-
- def gitlab_sign_in(user)
- visit new_user_session_path
-
- fill_in "user_login", with: user.email
- fill_in "user_password", with: "12345678"
- check 'user_remember_me'
- click_button "Sign in"
-
- @user = user
- end
-
- def gitlab_sign_out
- return unless @user
-
- if Capybara.current_driver == Capybara.javascript_driver
- find('.header-user-dropdown-toggle').click
- click_link 'Sign out'
- expect(page).to have_button('Sign in')
- else
- sign_out(@user)
- end
-
- @user = nil
- end
-end
diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb
deleted file mode 100644
index c2197584d8d..00000000000
--- a/features/steps/shared/builds.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-module SharedBuilds
- include Spinach::DSL
-
- step 'project has CI enabled' do
- @project.enable_ci
- end
-
- step 'project has coverage enabled' do
- @project.update_attribute(:build_coverage_regex, /Coverage (\d+)%/)
- end
-
- step 'project has a recent build' do
- @pipeline = create(:ci_empty_pipeline, project: @project, sha: @project.commit.sha, ref: 'master')
- @build = create(:ci_build, :running, :coverage, :trace_artifact, pipeline: @pipeline)
- end
-
- step 'recent build is successful' do
- @build.success
- end
-
- step 'recent build failed' do
- @build.drop
- end
-
- step 'project has another build that is running' do
- create(:ci_build, pipeline: @pipeline, name: 'second build', status_event: 'run')
- end
-
- step 'I visit recent build details page' do
- visit project_job_path(@project, @build)
- end
-
- step 'recent build has artifacts available' do
- artifacts = Rails.root + 'spec/fixtures/ci_build_artifacts.zip'
- archive = fixture_file_upload(artifacts, 'application/zip')
- @build.update_attributes(legacy_artifacts_file: archive)
- end
-
- step 'recent build has artifacts metadata available' do
- metadata = Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz'
- gzip = fixture_file_upload(metadata, 'application/x-gzip')
- @build.update_attributes(legacy_artifacts_metadata: gzip)
- end
-
- step 'recent build has a build trace' do
- @build.trace.set('job trace')
- end
-
- step 'download of build artifacts archive starts' do
- expect(page.response_headers['Content-Type']).to eq 'application/zip'
- expect(page.response_headers['Content-Transfer-Encoding']).to eq 'binary'
- end
-end
diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb
deleted file mode 100644
index aa32528a7ca..00000000000
--- a/features/steps/shared/diff_note.rb
+++ /dev/null
@@ -1,237 +0,0 @@
-module SharedDiffNote
- include Spinach::DSL
- include RepoHelpers
- include WaitForRequests
-
- after do
- wait_for_requests if javascript_test?
- end
-
- step 'I cancel the diff comment' do
- page.within(diff_file_selector) do
- find(".js-close-discussion-note-form").click
- end
- end
-
- step 'I delete a diff comment' do
- find('.note').hover
- find(".js-note-delete").click
- end
-
- step 'I haven\'t written any diff comment text' do
- page.within(diff_file_selector) do
- fill_in "note[note]", with: ""
- end
- end
-
- step 'I leave a diff comment like "Typo, please fix"' do
- page.within(diff_file_selector) do
- click_diff_line(sample_commit.line_code)
-
- page.within("form[data-line-code='#{sample_commit.line_code}']") do
- fill_in "note[note]", with: "Typo, please fix"
- find(".js-comment-button").click
- end
- end
- end
-
- step 'I leave a diff comment in a parallel view on the left side like "Old comment"' do
- click_parallel_diff_line(sample_commit.del_line_code, 'old')
- page.within("#{diff_file_selector} form[data-line-code='#{sample_commit.del_line_code}']") do
- fill_in "note[note]", with: "Old comment"
- find(".js-comment-button").click
- end
- end
-
- step 'I leave a diff comment in a parallel view on the right side like "New comment"' do
- click_parallel_diff_line(sample_commit.line_code, 'new')
- page.within("#{diff_file_selector} form[data-line-code='#{sample_commit.line_code}']") do
- fill_in "note[note]", with: "New comment"
- find(".js-comment-button").click
- end
- end
-
- step 'I preview a diff comment text like "Should fix it :smile:"' do
- page.within(diff_file_selector) do
- click_diff_line(sample_commit.line_code)
-
- page.within("form[data-line-code='#{sample_commit.line_code}']") do
- fill_in "note[note]", with: "Should fix it :smile:"
- find('.js-md-preview-button').click
- end
- end
- end
-
- step 'I preview another diff comment text like "DRY this up"' do
- page.within(diff_file_selector) do
- click_diff_line(sample_commit.del_line_code)
-
- page.within("form[data-line-code='#{sample_commit.del_line_code}']") do
- fill_in "note[note]", with: "DRY this up"
- find('.js-md-preview-button').click
- end
- end
- end
-
- step 'I open a diff comment form' do
- page.within(diff_file_selector) do
- click_diff_line(sample_commit.line_code)
- end
- end
-
- step 'I open another diff comment form' do
- page.within(diff_file_selector) do
- click_diff_line(sample_commit.del_line_code)
- end
- end
-
- step 'I write a diff comment like ":-1: I don\'t like this"' do
- page.within(diff_file_selector) do
- fill_in "note[note]", with: ":-1: I don\'t like this"
- end
- end
-
- step 'I write a diff comment like ":smile:"' do
- page.within(diff_file_selector) do
- click_diff_line(sample_commit.line_code)
-
- page.within("form[data-line-code='#{sample_commit.line_code}']") do
- fill_in 'note[note]', with: ':smile:'
- click_button('Comment')
- end
- end
- end
-
- step 'I submit the diff comment' do
- page.within(diff_file_selector) do
- click_button("Comment")
- end
- end
-
- step 'I should not see the diff comment form' do
- page.within(diff_file_selector) do
- expect(page).not_to have_css("form.new_note")
- end
- end
-
- step 'The diff comment preview tab should say there is nothing to do' do
- page.within(diff_file_selector) do
- find('.js-md-preview-button').click
- expect(find('.js-md-preview')).to have_content('Nothing to preview.')
- end
- end
-
- step 'I should not see the diff comment text field' do
- page.within(diff_file_selector) do
- expect(find('.js-note-text')).not_to be_visible
- end
- end
-
- step 'I should only see one diff form' do
- page.within(diff_file_selector) do
- expect(page).to have_css("form.new-note", count: 1)
- end
- end
-
- step 'I should see a diff comment form with ":-1: I don\'t like this"' do
- page.within(diff_file_selector) do
- expect(page).to have_field("note[note]", with: ":-1: I don\'t like this")
- end
- end
-
- step 'I should see a diff comment saying "Typo, please fix"' do
- page.within("#{diff_file_selector} .note") do
- expect(page).to have_content("Typo, please fix")
- end
- end
-
- step 'I should see a diff comment on the left side saying "Old comment"' do
- page.within("#{diff_file_selector} .notes_content.parallel.old") do
- expect(page).to have_content("Old comment")
- end
- end
-
- step 'I should see a diff comment on the right side saying "New comment"' do
- page.within("#{diff_file_selector} .notes_content.parallel.new") do
- expect(page).to have_content("New comment")
- end
- end
-
- step 'I should see a discussion reply button' do
- page.within(diff_file_selector) do
- expect(page).to have_button('Reply...')
- end
- end
-
- step 'I should see a temporary diff comment form' do
- page.within(diff_file_selector) do
- expect(page).to have_css(".js-temp-notes-holder form.new-note")
- end
- end
-
- step 'I should see an empty diff comment form' do
- page.within(diff_file_selector) do
- expect(page).to have_field("note[note]", with: "")
- end
- end
-
- step 'I should see the cancel comment button' do
- page.within("#{diff_file_selector} form") do
- expect(page).to have_css(".js-close-discussion-note-form", text: "Cancel")
- end
- end
-
- step 'I should see the diff comment preview' do
- page.within("#{diff_file_selector} form") do
- expect(page).to have_css('.js-md-preview', visible: true)
- end
- end
-
- step 'I should see the diff comment write tab' do
- page.within(diff_file_selector) do
- expect(page).to have_css('.js-md-write-button', visible: true)
- end
- end
-
- step 'The diff comment preview tab should display rendered Markdown' do
- page.within(diff_file_selector) do
- find('.js-md-preview-button').click
- expect(find('.js-md-preview')).to have_css('gl-emoji', visible: true)
- end
- end
-
- step 'I should see two separate previews' do
- page.within(diff_file_selector) do
- expect(page).to have_css('.js-md-preview', visible: true, count: 2)
- expect(page).to have_content('Should fix it')
- expect(page).to have_content('DRY this up')
- end
- end
-
- step 'I should see a diff comment with an emoji image' do
- page.within("#{diff_file_selector} .note") do
- expect(page).to have_xpath("//gl-emoji[@data-name='smile']")
- end
- end
-
- step 'I click side-by-side diff button' do
- find('#parallel-diff-btn').click
- end
-
- step 'I see side-by-side diff button' do
- expect(page).to have_content "Side-by-side"
- end
-
- def diff_file_selector
- '.diff-file:nth-of-type(1)'
- end
-
- def click_diff_line(code)
- find(".line_holder[id='#{code}'] button").click
- end
-
- def click_parallel_diff_line(code, line_type)
- find(".line_holder.parallel td[id='#{code}']").find(:xpath, 'preceding-sibling::*[1][self::td]').hover
- find(".line_holder.parallel button[data-line-code='#{code}']").click
- end
-end
diff --git a/features/steps/shared/group.rb b/features/steps/shared/group.rb
deleted file mode 100644
index 0126ce39c5a..00000000000
--- a/features/steps/shared/group.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-module SharedGroup
- include Spinach::DSL
-
- step 'current user is developer of group "Owned"' do
- is_member_of(current_user.name, "Owned", Gitlab::Access::DEVELOPER)
- end
-
- step '"John Doe" is guest of group "Guest"' do
- is_member_of("John Doe", "Guest", Gitlab::Access::GUEST)
- end
-
- step '"Mary Jane" is owner of group "Owned"' do
- is_member_of("Mary Jane", "Owned", Gitlab::Access::OWNER)
- end
-
- step '"Mary Jane" is guest of group "Owned"' do
- is_member_of("Mary Jane", "Owned", Gitlab::Access::GUEST)
- end
-
- step '"Mary Jane" is guest of group "Guest"' do
- is_member_of("Mary Jane", "Guest", Gitlab::Access::GUEST)
- end
-
- step 'I should see group "TestGroup"' do
- expect(page).to have_content "TestGroup"
- end
-
- step 'I should not see group "TestGroup"' do
- expect(page).not_to have_content "TestGroup"
- end
-
- protected
-
- def is_member_of(username, groupname, role)
- user = User.find_by(name: username) || create(:user, name: username)
- group = Group.find_by(name: groupname) || create(:group, name: groupname)
- group.add_user(user, role)
- project ||= create(:project, :repository, namespace: group)
- create(:closed_issue_event, project: project)
- project.add_master(user)
- end
-
- def owned_group
- @owned_group ||= Group.find_by(name: "Owned")
- end
-end
diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb
deleted file mode 100644
index a9174efd334..00000000000
--- a/features/steps/shared/issuable.rb
+++ /dev/null
@@ -1,167 +0,0 @@
-module SharedIssuable
- include Spinach::DSL
-
- def edit_issuable
- find('.js-issuable-edit', visible: true).click
- end
-
- step 'project "Community" has "Community issue" open issue' do
- create_issuable_for_project(
- project_name: 'Community',
- title: 'Community issue'
- )
- end
-
- step 'project "Community" has "Community fix" open merge request' do
- create_issuable_for_project(
- project_name: 'Community',
- type: :merge_request,
- title: 'Community fix'
- )
- end
-
- step 'project "Enterprise" has "Enterprise issue" open issue' do
- create_issuable_for_project(
- project_name: 'Enterprise',
- title: 'Enterprise issue'
- )
- end
-
- step 'project "Enterprise" has "Enterprise fix" open merge request' do
- create_issuable_for_project(
- project_name: 'Enterprise',
- type: :merge_request,
- title: 'Enterprise fix'
- )
- end
-
- step 'I leave a comment referencing issue "Community issue"' do
- leave_reference_comment(
- issuable: Issue.find_by(title: 'Community issue'),
- from_project_name: 'Enterprise'
- )
- end
-
- step 'I leave a comment referencing issue "Community fix"' do
- leave_reference_comment(
- issuable: MergeRequest.find_by(title: 'Community fix'),
- from_project_name: 'Enterprise'
- )
- end
-
- step 'I visit issue page "Enterprise issue"' do
- issue = Issue.find_by(title: 'Enterprise issue')
- visit project_issue_path(issue.project, issue)
- end
-
- step 'I visit merge request page "Enterprise fix"' do
- mr = MergeRequest.find_by(title: 'Enterprise fix')
- visit project_merge_request_path(mr.target_project, mr)
- end
-
- step 'I visit issue page "Community issue"' do
- issue = Issue.find_by(title: 'Community issue')
- visit project_issue_path(issue.project, issue)
- end
-
- step 'I visit issue page "Community fix"' do
- mr = MergeRequest.find_by(title: 'Community fix')
- visit project_merge_request_path(mr.target_project, mr)
- end
-
- step 'I should not see any related merge requests' do
- page.within '.issue-details' do
- expect(page).not_to have_content('#merge-requests .merge-requests-title')
- end
- end
-
- step 'I should see the "Enterprise fix" related merge request' do
- page.within '#merge-requests .merge-requests-title' do
- expect(page).to have_content('1 Related Merge Request')
- end
-
- page.within '#merge-requests ul' do
- expect(page).to have_content('Enterprise fix')
- end
- end
-
- step 'I should see a note linking to "Enterprise fix" merge request' do
- visible_note(
- issuable: MergeRequest.find_by(title: 'Enterprise fix'),
- from_project_name: 'Community',
- user_name: 'Mary Jane'
- )
- end
-
- step 'I should see a note linking to "Enterprise issue" issue' do
- visible_note(
- issuable: Issue.find_by(title: 'Enterprise issue'),
- from_project_name: 'Community',
- user_name: 'Mary Jane'
- )
- end
-
- step 'I click link "Edit" for the merge request' do
- edit_issuable
- end
-
- step 'I sort the list by "Least popular"' do
- find('button.dropdown-toggle').click
-
- page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do
- click_link 'Least popular'
- end
- end
-
- step 'I click link "Next" in the sidebar' do
- page.within '.issuable-sidebar' do
- click_link 'Next'
- end
- end
-
- def create_issuable_for_project(project_name:, title:, type: :issue)
- project = Project.find_by(name: project_name)
-
- attrs = {
- title: title,
- author: project.users.first,
- description: '# Description header'
- }
-
- case type
- when :issue
- attrs[:project] = project
- when :merge_request
- attrs.merge!(
- source_project: project,
- target_project: project,
- source_branch: 'fix',
- target_branch: 'master'
- )
- end
-
- create(type, attrs)
- end
-
- def leave_reference_comment(issuable:, from_project_name:)
- project = Project.find_by(name: from_project_name)
-
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: "##{issuable.to_reference(project)}"
- click_button 'Comment'
- end
- end
-
- def visible_note(issuable:, from_project_name:, user_name:)
- project = Project.find_by(name: from_project_name)
-
- expect(page).to have_content(user_name)
- expect(page).to have_content("mentioned in #{issuable.class.to_s.titleize.downcase} #{issuable.to_reference(project)}")
- end
-
- def expect_sidebar_content(content)
- page.within '.issuable-sidebar' do
- expect(page).to have_content content
- end
- end
-end
diff --git a/features/steps/shared/markdown.rb b/features/steps/shared/markdown.rb
deleted file mode 100644
index 65118f07ca2..00000000000
--- a/features/steps/shared/markdown.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-module SharedMarkdown
- include Spinach::DSL
-
- step 'I should not see the Markdown preview' do
- expect(find('.gfm-form .js-md-preview')).not_to be_visible
- end
-
- step 'I haven\'t written any description text' do
- find('.gfm-form').fill_in 'Description', with: ''
- end
-end
diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb
deleted file mode 100644
index bf1b88c60d7..00000000000
--- a/features/steps/shared/note.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-module SharedNote
- include Spinach::DSL
- include WaitForRequests
-
- after do
- wait_for_requests if javascript_test?
- end
-
- step 'I haven\'t written any comment text' do
- page.within(".js-main-target-form") do
- fill_in "note[note]", with: ""
- end
- end
-
- step 'The comment preview tab should say there is nothing to do' do
- page.within(".js-main-target-form") do
- find('.js-md-preview-button').click
- expect(find('.js-md-preview')).to have_content('Nothing to preview.')
- end
- end
-
- step 'I should see no notes at all' do
- expect(page).not_to have_css('.note')
- end
-end
diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb
deleted file mode 100644
index b6c648a707d..00000000000
--- a/features/steps/shared/paths.rb
+++ /dev/null
@@ -1,430 +0,0 @@
-module SharedPaths
- include Spinach::DSL
- include RepoHelpers
- include DashboardHelper
- include WaitForRequests
-
- step 'I visit new project page' do
- visit new_project_path
- end
-
- step 'I visit login page' do
- visit new_user_session_path
- end
-
- # ----------------------------------------
- # User
- # ----------------------------------------
-
- step 'I visit user "John Doe" page' do
- visit user_path("john_doe")
- end
-
- # ----------------------------------------
- # Group
- # ----------------------------------------
-
- step 'I visit group "Owned" page' do
- visit group_path(Group.find_by(name: "Owned"))
- end
-
- step 'I visit group "Owned" activity page' do
- visit activity_group_path(Group.find_by(name: "Owned"))
- end
-
- step 'I visit group "Owned" issues page' do
- visit issues_group_path(Group.find_by(name: "Owned"))
- end
-
- step 'I visit group "Owned" merge requests page' do
- visit merge_requests_group_path(Group.find_by(name: "Owned"))
- end
-
- step 'I visit group "Owned" milestones page' do
- visit group_milestones_path(Group.find_by(name: "Owned"))
- end
-
- step 'I visit group "Owned" members page' do
- visit group_group_members_path(Group.find_by(name: "Owned"))
- end
-
- step 'I visit group "Owned" projects page' do
- visit projects_group_path(Group.find_by(name: "Owned"))
- end
-
- step 'I visit group "Guest" page' do
- visit group_path(Group.find_by(name: "Guest"))
- end
-
- step 'I visit group "Guest" issues page' do
- visit issues_group_path(Group.find_by(name: "Guest"))
- end
-
- step 'I visit group "Guest" merge requests page' do
- visit merge_requests_group_path(Group.find_by(name: "Guest"))
- end
-
- step 'I visit group "Guest" members page' do
- visit group_group_members_path(Group.find_by(name: "Guest"))
- end
-
- step 'I visit group "Guest" settings page' do
- visit edit_group_path(Group.find_by(name: "Guest"))
- end
-
- # ----------------------------------------
- # Dashboard
- # ----------------------------------------
-
- step 'I visit dashboard page' do
- visit dashboard_projects_path
- end
-
- step 'I visit dashboard activity page' do
- visit activity_dashboard_path
- end
-
- step 'I visit dashboard projects page' do
- visit projects_dashboard_path
- end
-
- step 'I visit dashboard issues page' do
- visit assigned_issues_dashboard_path
- end
-
- step 'I visit dashboard search page' do
- visit search_path
- end
-
- step 'I visit dashboard help page' do
- visit help_path
- end
-
- step 'I visit dashboard groups page' do
- visit dashboard_groups_path
- end
-
- step 'I should be redirected to the dashboard groups page' do
- expect(current_path).to eq dashboard_groups_path
- end
-
- step 'I visit dashboard starred projects page' do
- visit starred_dashboard_projects_path
- end
-
- # ----------------------------------------
- # Profile
- # ----------------------------------------
-
- step 'I visit profile page' do
- visit profile_path
- end
-
- step 'I visit profile applications page' do
- visit applications_profile_path
- end
-
- step 'I visit profile password page' do
- visit edit_profile_password_path
- end
-
- step 'I visit profile account page' do
- visit profile_account_path
- end
-
- step 'I visit profile SSH keys page' do
- visit profile_keys_path
- end
-
- step 'I visit profile preferences page' do
- visit profile_preferences_path
- end
-
- step 'I visit Authentication log page' do
- visit audit_log_profile_path
- end
-
- # ----------------------------------------
- # Admin
- # ----------------------------------------
-
- step 'I visit admin page' do
- visit admin_root_path
- end
-
- step 'I visit abuse reports page' do
- visit admin_abuse_reports_path
- end
-
- step 'I visit admin projects page' do
- visit admin_projects_path
- end
-
- step 'I visit admin users page' do
- visit admin_users_path
- end
-
- step 'I visit admin logs page' do
- visit admin_logs_path
- end
-
- step 'I visit admin messages page' do
- visit admin_broadcast_messages_path
- end
-
- step 'I visit admin hooks page' do
- visit admin_hooks_path
- end
-
- step 'I visit admin Resque page' do
- visit admin_background_jobs_path
- end
-
- step 'I visit admin teams page' do
- visit admin_teams_path
- end
-
- step 'I visit spam logs page' do
- visit admin_spam_logs_path
- end
-
- # ----------------------------------------
- # Generic Project
- # ----------------------------------------
-
- step "I visit my project's settings page" do
- visit edit_project_path(@project)
- end
-
- step 'I visit a binary file in the repo' do
- visit project_blob_path(@project,
- File.join(root_ref, 'files/images/logo-black.png'))
- end
-
- step "I visit my project's commits page" do
- visit project_commits_path(@project, root_ref, { limit: 5 })
- end
-
- step "I visit my project's commits page for a specific path" do
- visit project_commits_path(@project, root_ref + "/files/ruby/regex.rb", { limit: 5 })
- end
-
- step 'I visit my project\'s commits stats page' do
- visit stats_project_repository_path(@project)
- end
-
- step "I visit my project's graph page" do
- # Stub Graph max_size to speed up test (10 commits vs. 650)
- Network::Graph.stub(max_count: 10)
-
- visit project_network_path(@project, root_ref)
- end
-
- step "I visit my project's issues page" do
- visit project_issues_path(@project)
- end
-
- step "I visit my project's merge requests page" do
- visit project_merge_requests_path(@project)
- end
-
- step "I visit my project's members page" do
- visit project_project_members_path(@project)
- end
-
- step "I visit my project's wiki page" do
- visit project_wiki_path(@project, :home)
- end
-
- step 'I visit project hooks page' do
- visit project_settings_integrations_path(@project)
- end
-
- step 'I visit project deploy keys page' do
- visit project_deploy_keys_path(@project)
- end
-
- step 'I visit project find file page' do
- visit project_find_file_path(@project, root_ref)
- end
-
- # ----------------------------------------
- # "Shop" Project
- # ----------------------------------------
-
- step 'I visit project "Shop" page' do
- visit project_path(project)
- end
-
- step 'I visit project "Forked Shop" merge requests page' do
- visit project_merge_requests_path(@forked_project)
- end
-
- step 'I visit edit project "Shop" page' do
- visit edit_project_path(project)
- end
-
- step 'I visit compare refs page' do
- visit project_compare_index_path(@project)
- end
-
- step 'I visit project commits page' do
- visit project_commits_path(@project, root_ref, { limit: 5 })
- end
-
- step 'I visit project commits page for stable branch' do
- visit project_commits_path(@project, 'stable', { limit: 5 })
- end
-
- step 'I visit blob file from repo' do
- visit project_blob_path(@project, File.join(sample_commit.id, sample_blob.path))
- end
-
- step 'I visit ".gitignore" file in repo' do
- visit project_blob_path(@project, File.join(root_ref, '.gitignore'))
- end
-
- step 'I am on the new file page' do
- expect(current_path).to eq(project_create_blob_path(@project, root_ref))
- end
-
- step 'I am on the ".gitignore" edit file page' do
- expect(current_path).to eq(
- project_edit_blob_path(@project, File.join(root_ref, '.gitignore')))
- end
-
- step 'I visit project source page for "6d39438"' do
- visit project_tree_path(@project, "6d39438")
- end
-
- step 'I visit project source page for' \
- ' "6d394385cf567f80a8fd85055db1ab4c5295806f"' do
- visit project_tree_path(@project,
- '6d394385cf567f80a8fd85055db1ab4c5295806f')
- end
-
- step 'I visit project tags page' do
- visit project_tags_path(@project)
- end
-
- step 'I visit project commit page' do
- visit project_commit_path(@project, sample_commit.id)
- end
-
- step 'I visit issue page "Release 0.4"' do
- issue = Issue.find_by(title: "Release 0.4")
- visit project_issue_path(issue.project, issue)
- end
-
- step 'I visit project "Forum" labels page' do
- project = Project.find_by(name: 'Forum')
- visit project_labels_path(project)
- end
-
- step 'I visit project "Shop" new label page' do
- project = Project.find_by(name: 'Shop')
- visit new_project_label_path(project)
- end
-
- step 'I visit project "Forum" new label page' do
- project = Project.find_by(name: 'Forum')
- visit new_project_label_path(project)
- end
-
- step 'I visit merge request page "Bug NS-04"' do
- visit merge_request_path("Bug NS-04")
- wait_for_requests
- end
-
- step 'I visit merge request page "Bug NS-05"' do
- visit merge_request_path("Bug NS-05")
- wait_for_requests
- end
-
- step 'I visit merge request page "Bug NS-07"' do
- visit merge_request_path("Bug NS-07")
- wait_for_requests
- end
-
- step 'I visit merge request page "Bug NS-08"' do
- visit merge_request_path("Bug NS-08")
- wait_for_requests
- end
-
- step 'I visit merge request page "Bug CO-01"' do
- mr = MergeRequest.find_by(title: "Bug CO-01")
- visit project_merge_request_path(mr.target_project, mr)
- wait_for_requests
- end
-
- step 'I visit forked project "Shop" merge requests page' do
- visit project_merge_requests_path(project)
- end
-
- step 'I visit project "Shop" team page' do
- visit project_project_members_path(project)
- end
-
- step 'I visit project wiki page' do
- visit project_wiki_path(@project, :home)
- end
-
- # ----------------------------------------
- # Visibility Projects
- # ----------------------------------------
-
- step 'I visit project "Community" source page' do
- project = Project.find_by(name: 'Community')
- visit project_tree_path(project, root_ref)
- end
-
- step 'I visit project "Internal" page' do
- project = Project.find_by(name: "Internal")
- visit project_path(project)
- end
-
- step 'I visit project "Enterprise" page' do
- project = Project.find_by(name: "Enterprise")
- visit project_path(project)
- end
-
- # ----------------------------------------
- # Empty Projects
- # ----------------------------------------
-
- step "I should not see command line instructions" do
- expect(page).not_to have_css('.empty_wrapper')
- end
-
- # ----------------------------------------
- # Public Projects
- # ----------------------------------------
- step 'I visit the public groups area' do
- visit explore_groups_path
- end
-
- # ----------------------------------------
- # Snippets
- # ----------------------------------------
-
- step 'I visit project "Shop" snippets page' do
- visit project_snippets_path(project)
- end
-
- step 'I visit snippets page' do
- visit explore_snippets_path
- end
-
- def root_ref
- @project.repository.root_ref
- end
-
- def project
- Project.find_by!(name: 'Shop')
- end
-
- def merge_request_path(title)
- mr = MergeRequest.find_by(title: title)
- project_merge_request_path(mr.target_project, mr)
- end
-end
diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb
deleted file mode 100644
index a1945cf5f3d..00000000000
--- a/features/steps/shared/project.rb
+++ /dev/null
@@ -1,132 +0,0 @@
-module SharedProject
- include Spinach::DSL
-
- # Create a project without caring about what it's called
- step "I own a project" do
- @project = create(:project, :repository, namespace: @user.namespace)
- @project.add_master(@user)
- end
-
- step "I own a project in some group namespace" do
- @group = create(:group, name: 'some group')
- @project = create(:project, namespace: @group)
- @project.add_master(@user)
- end
-
- # Create a specific project called "Shop"
- step 'I own project "Shop"' do
- @project = Project.find_by(name: "Shop")
- @project ||= create(:project, :repository, name: "Shop", namespace: @user.namespace)
- @project.add_master(@user)
- end
-
- def current_project
- @project ||= Project.first
- end
-
- # ----------------------------------------
- # Visibility of archived project
- # ----------------------------------------
-
- step 'I should not see project "Archive"' do
- project = Project.find_by(name: "Archive")
- expect(page).not_to have_content project.full_name
- end
-
- step 'I should see project "Archive"' do
- project = Project.find_by(name: "Archive")
- expect(page).to have_content project.full_name
- end
-
- # ----------------------------------------
- # Visibility level
- # ----------------------------------------
-
- step 'private project "Enterprise"' do
- create(:project, :private, :repository, name: 'Enterprise')
- end
-
- step 'I should see project "Enterprise"' do
- expect(page).to have_content "Enterprise"
- end
-
- step 'I should not see project "Enterprise"' do
- expect(page).not_to have_content "Enterprise"
- end
-
- step 'internal project "Internal"' do
- create(:project, :internal, :repository, name: 'Internal')
- end
-
- step 'I should see project "Internal"' do
- page.within '.js-projects-list-holder' do
- expect(page).to have_content "Internal"
- end
- end
-
- step 'I should not see project "Internal"' do
- page.within '.js-projects-list-holder' do
- expect(page).not_to have_content "Internal"
- end
- end
-
- step 'public project "Community"' do
- create(:project, :public, :repository, name: 'Community')
- end
-
- step 'I should see project "Community"' do
- expect(page).to have_content "Community"
- end
-
- step 'I should not see project "Community"' do
- expect(page).not_to have_content "Community"
- end
-
- step '"John Doe" owns private project "Enterprise"' do
- user_owns_project(
- user_name: 'John Doe',
- project_name: 'Enterprise'
- )
- end
-
- step '"Mary Jane" owns private project "Enterprise"' do
- user_owns_project(
- user_name: 'Mary Jane',
- project_name: 'Enterprise'
- )
- end
-
- step '"John Doe" owns internal project "Internal"' do
- user_owns_project(
- user_name: 'John Doe',
- project_name: 'Internal',
- visibility: :internal
- )
- end
-
- step '"John Doe" owns public project "Community"' do
- user_owns_project(
- user_name: 'John Doe',
- project_name: 'Community',
- visibility: :public
- )
- end
-
- step 'public empty project "Empty Public Project"' do
- create :project_empty_repo, :public, name: "Empty Public Project"
- end
-
- step 'project "Shop" has labels: "bug", "feature", "enhancement"' do
- project = Project.find_by(name: "Shop")
- create(:label, project: project, title: 'bug')
- create(:label, project: project, title: 'feature')
- create(:label, project: project, title: 'enhancement')
- end
-
- def user_owns_project(user_name:, project_name:, visibility: :private)
- user = user_exists(user_name, username: user_name.gsub(/\s/, '').underscore)
- project = Project.find_by(name: project_name)
- project ||= create(:project, visibility, name: project_name, namespace: user.namespace)
- project.add_master(user)
- end
-end
diff --git a/features/steps/shared/project_tab.rb b/features/steps/shared/project_tab.rb
deleted file mode 100644
index 5a516ee33bc..00000000000
--- a/features/steps/shared/project_tab.rb
+++ /dev/null
@@ -1,66 +0,0 @@
-require_relative 'active_tab'
-
-module SharedProjectTab
- include Spinach::DSL
- include SharedActiveTab
-
- step 'the active main tab should be Project' do
- ensure_active_main_tab('Overview')
- end
-
- step 'the active main tab should be Repository' do
- ensure_active_main_tab('Repository')
- end
-
- step 'the active main tab should be Issues' do
- ensure_active_main_tab('Issues')
- end
-
- step 'the active sub tab should be Members' do
- ensure_active_sub_tab('Members')
- end
-
- step 'the active main tab should be Merge Requests' do
- ensure_active_main_tab('Merge Requests')
- end
-
- step 'the active main tab should be Snippets' do
- ensure_active_main_tab('Snippets')
- end
-
- step 'the active main tab should be Wiki' do
- ensure_active_main_tab('Wiki')
- end
-
- step 'the active main tab should be Members' do
- ensure_active_main_tab('Members')
- end
-
- step 'the active main tab should be Settings' do
- ensure_active_main_tab('Settings')
- end
-
- step 'the active sub tab should be Graph' do
- ensure_active_sub_tab('Graph')
- end
-
- step 'the active sub tab should be Files' do
- ensure_active_sub_tab('Files')
- end
-
- step 'the active sub tab should be Commits' do
- ensure_active_sub_tab('Commits')
- end
-
- step 'the active sub tab should be Home' do
- ensure_active_sub_tab('Details')
- end
-
- step 'the active sub tab should be Activity' do
- ensure_active_sub_tab('Activity')
- end
-
- step 'the active sub tab should be Charts' do
- ensure_active_sub_tab('Charts')
- end
-end
diff --git a/features/steps/shared/shortcuts.rb b/features/steps/shared/shortcuts.rb
deleted file mode 100644
index a75a8474d26..00000000000
--- a/features/steps/shared/shortcuts.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-module SharedShortcuts
- include Spinach::DSL
-
- step 'I press "g" and "p"' do
- find('body').native.send_key('g')
- find('body').native.send_key('p')
- end
-
- step 'I press "g" and "i"' do
- find('body').native.send_key('g')
- find('body').native.send_key('i')
- end
-
- step 'I press "g" and "m"' do
- find('body').native.send_key('g')
- find('body').native.send_key('m')
- end
-end
diff --git a/features/steps/shared/sidebar_active_tab.rb b/features/steps/shared/sidebar_active_tab.rb
deleted file mode 100644
index 07fff16e867..00000000000
--- a/features/steps/shared/sidebar_active_tab.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-module SharedSidebarActiveTab
- include Spinach::DSL
-
- step 'no other main tabs should be active' do
- expect(page).to have_selector('.nav-sidebar li.active', count: 1)
- end
-
- def ensure_active_main_tab(content)
- expect(find('.nav-sidebar li.active')).to have_content(content)
- end
-
- step 'the active main tab should be Home' do
- ensure_active_main_tab('Projects')
- end
-
- step 'the active main tab should be Groups' do
- ensure_active_main_tab('Groups')
- end
-
- step 'the active main tab should be Projects' do
- ensure_active_main_tab('Projects')
- end
-
- step 'the active main tab should be Issues' do
- ensure_active_main_tab('Issues')
- end
-
- step 'the active main tab should be Merge Requests' do
- ensure_active_main_tab('Merge Requests')
- end
-end
diff --git a/features/steps/shared/user.rb b/features/steps/shared/user.rb
deleted file mode 100644
index 9cadc91769d..00000000000
--- a/features/steps/shared/user.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-module SharedUser
- include Spinach::DSL
-
- step 'User "John Doe" exists' do
- user_exists("John Doe", { username: "john_doe" })
- end
-
- step 'User "Mary Jane" exists' do
- user_exists("Mary Jane", { username: "mary_jane" })
- end
-
- step 'gitlab user "Mike"' do
- create(:user, name: "Mike")
- end
-
- protected
-
- def user_exists(name, options = {})
- User.find_by(name: name) || create(:user, { name: name, admin: false }.merge(options))
- end
-
- step 'I have no ssh keys' do
- @user.keys.delete_all
- end
-
- step 'I click on "Personal projects" tab' do
- page.within '.nav-links' do
- click_link 'Personal projects'
- end
-
- expect(page).to have_css('.tab-content #projects.active')
- end
-
- step 'I click on "Contributed projects" tab' do
- page.within '.nav-links' do
- click_link 'Contributed projects'
- end
-
- expect(page).to have_css('.tab-content #contributed.active')
- end
-end
diff --git a/features/support/capybara.rb b/features/support/capybara.rb
deleted file mode 100644
index 8879c9ab650..00000000000
--- a/features/support/capybara.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-require 'capybara-screenshot/spinach'
-
-# Give CI some extra time
-timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 60 : 30
-
-Capybara.register_driver :chrome do |app|
- capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
- # This enables access to logs with `page.driver.manage.get_log(:browser)`
- loggingPrefs: {
- browser: "ALL",
- client: "ALL",
- driver: "ALL",
- server: "ALL"
- }
- )
-
- options = Selenium::WebDriver::Chrome::Options.new
- options.add_argument("window-size=1240,1400")
-
- # Chrome won't work properly in a Docker container in sandbox mode
- options.add_argument("no-sandbox")
-
- # Run headless by default unless CHROME_HEADLESS specified
- options.add_argument("headless") unless ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i
-
- # Disable /dev/shm use in CI. See https://gitlab.com/gitlab-org/gitlab-ee/issues/4252
- options.add_argument("disable-dev-shm-usage") if ENV['CI'] || ENV['CI_SERVER']
-
- Capybara::Selenium::Driver.new(
- app,
- browser: :chrome,
- desired_capabilities: capabilities,
- options: options
- )
-end
-
-Capybara.javascript_driver = :chrome
-Capybara.default_max_wait_time = timeout
-Capybara.ignore_hidden_elements = false
-
-# Keep only the screenshots generated from the last failing test suite
-Capybara::Screenshot.prune_strategy = :keep_last_run
-# From https://github.com/mattheworiordan/capybara-screenshot/issues/84#issuecomment-41219326
-Capybara::Screenshot.register_driver(:chrome) do |driver, path|
- driver.browser.save_screenshot(path)
-end
-
-Spinach.hooks.before_run do
- TestEnv.eager_load_driver_server
-end
diff --git a/features/support/db_cleaner.rb b/features/support/db_cleaner.rb
deleted file mode 100644
index 31c922d23c3..00000000000
--- a/features/support/db_cleaner.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-require 'database_cleaner'
-
-DatabaseCleaner[:active_record].strategy = :deletion
-
-Spinach.hooks.before_scenario do
- DatabaseCleaner.start
-end
-
-Spinach.hooks.after_scenario do
- DatabaseCleaner.clean
-end
diff --git a/features/support/env.rb b/features/support/env.rb
deleted file mode 100644
index 8fa2fcb6e3e..00000000000
--- a/features/support/env.rb
+++ /dev/null
@@ -1,60 +0,0 @@
-require './spec/simplecov_env'
-SimpleCovEnv.start!
-
-ENV['RAILS_ENV'] = 'test'
-require './config/environment'
-require 'rspec/expectations'
-
-if ENV['CI']
- require 'knapsack'
- Knapsack::Adapters::SpinachAdapter.bind
-end
-
-WebMock.enable!
-
-%w(select2_helper test_env repo_helpers wait_for_requests project_forks_helper).each do |f|
- require Rails.root.join('spec', 'support', 'helpers', f)
-end
-
-%w(sidekiq webmock).each do |f|
- require Rails.root.join('spec', 'support', f)
-end
-
-Dir["#{Rails.root}/features/steps/shared/*.rb"].each { |file| require file }
-
-Spinach.hooks.before_run do
- include RSpec::Mocks::ExampleMethods
- include ActiveJob::TestHelper
- include FactoryBot::Syntax::Methods
- include GitlabRoutingHelper
-
- RSpec::Mocks.setup
- TestEnv.init(mailer: false)
-
- # skip pre-receive hook check so we can use
- # web editor and merge
- TestEnv.disable_pre_receive
-end
-
-Spinach.hooks.after_scenario do |scenario_data, step_definitions|
- if scenario_data.tags.include?('javascript')
- include WaitForRequests
- block_and_wait_for_requests_complete
- end
-end
-
-module StdoutReporterWithScenarioLocation
- # Override the standard reporter to show filename and line number next to each
- # scenario for easy, focused re-runs
- def before_scenario_run(scenario, step_definitions = nil)
- @max_step_name_length = scenario.steps.map(&:name).map(&:length).max if scenario.steps.any? # rubocop:disable Gitlab/ModuleWithInstanceVariables
- name = scenario.name
-
- # This number has no significance, it's just to line things up
- max_length = @max_step_name_length + 19 # rubocop:disable Gitlab/ModuleWithInstanceVariables
- out.puts "\n #{'Scenario:'.green} #{name.light_green.ljust(max_length)}" \
- " # #{scenario.feature.filename}:#{scenario.line}"
- end
-end
-
-Spinach::Reporter::Stdout.prepend(StdoutReporterWithScenarioLocation)
diff --git a/features/support/gitaly.rb b/features/support/gitaly.rb
deleted file mode 100644
index 3cd5f4ce497..00000000000
--- a/features/support/gitaly.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-Spinach.hooks.before_scenario do
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(true)
-end
diff --git a/features/support/login_helpers.rb b/features/support/login_helpers.rb
deleted file mode 100644
index 540ff25a4f2..00000000000
--- a/features/support/login_helpers.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-module LoginHelpers
- # After inclusion, IntegrationHelpers calls these two methods that aren't
- # supported by Spinach, so we perform the end results ourselves
- class << self
- def setup(*args)
- Spinach.hooks.before_scenario do
- Warden.test_mode!
- end
- end
-
- def teardown(*args)
- Spinach.hooks.after_scenario do
- Warden.test_reset!
- end
- end
- end
-
- include Devise::Test::IntegrationHelpers
-end
diff --git a/features/support/rerun.rb b/features/support/rerun.rb
deleted file mode 100644
index 60b78f9d050..00000000000
--- a/features/support/rerun.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# The spinach-rerun-reporter doesn't define the on_undefined_step
-# See it here: https://github.com/javierav/spinach-rerun-reporter/blob/master/lib/spinach/reporter/rerun.rb
-require 'spinach-rerun-reporter'
-
-module Spinach
- class Reporter
- class Rerun
- def on_undefined_step(step_data, failure, step_definitions = nil)
- super step_data, failure, step_definitions
-
- # save feature file and scenario line
- @rerun << "#{current_feature.filename}:#{current_scenario.line}"
- end
- end
- end
-end
diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb
index c2113551207..c17089759de 100644
--- a/lib/api/api_guard.rb
+++ b/lib/api/api_guard.rb
@@ -45,7 +45,9 @@ module API
user = find_user_from_sources
return unless user
- forbidden!('User is blocked') unless Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api)
+ unless api_access_allowed?(user)
+ forbidden!(api_access_denied_message(user))
+ end
user
end
@@ -72,6 +74,14 @@ module API
end
end
end
+
+ def api_access_allowed?(user)
+ Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api)
+ end
+
+ def api_access_denied_message(user)
+ Gitlab::Auth::UserAccessDeniedReason.new(user).rejection_message
+ end
end
module ClassMethods
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index a9bab5c56cf..25d78fc761d 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -967,6 +967,7 @@ module API
class Runner < Grape::Entity
expose :id
expose :description
+ expose :ip_address
expose :active
expose :is_shared
expose :name
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 12ff2a1398b..2f50f94c897 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -2,7 +2,7 @@ module API
class Issues < Grape::API
include PaginationParams
- before { authenticate! }
+ before { authenticate_non_get! }
helpers ::Gitlab::IssuableMetadata
@@ -70,6 +70,7 @@ module API
desc: 'Return issues for the given scope: `created-by-me`, `assigned-to-me` or `all`'
end
get do
+ authenticate! unless params[:scope] == 'all'
issues = paginate(find_issues)
options = {
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
index 67896ae1fc5..649feba1036 100644
--- a/lib/api/runner.rb
+++ b/lib/api/runner.rb
@@ -11,13 +11,14 @@ module API
requires :token, type: String, desc: 'Registration token'
optional :description, type: String, desc: %q(Runner's description)
optional :info, type: Hash, desc: %q(Runner's metadata)
+ optional :active, type: Boolean, desc: 'Should Runner be active'
optional :locked, type: Boolean, desc: 'Should Runner be locked for current project'
optional :run_untagged, type: Boolean, desc: 'Should Runner handle untagged jobs'
optional :tag_list, type: Array[String], desc: %q(List of Runner's tags)
optional :maximum_timeout, type: Integer, desc: 'Maximum timeout set when this Runner will handle the job'
end
post '/' do
- attributes = attributes_for_keys([:description, :locked, :run_untagged, :tag_list, :maximum_timeout])
+ attributes = attributes_for_keys([:description, :active, :locked, :run_untagged, :tag_list, :maximum_timeout])
.merge(get_runner_details_from_request)
runner =
@@ -153,9 +154,20 @@ module API
content_range = request.headers['Content-Range']
content_range = content_range.split('-')
- stream_size = job.trace.append(request.body.read, content_range[0].to_i)
- if stream_size < 0
- break error!('416 Range Not Satisfiable', 416, { 'Range' => "0-#{-stream_size}" })
+ # TODO:
+ # it seems that `Content-Range` as formatted by runner is wrong,
+ # the `byte_end` should point to final byte, but it points byte+1
+ # that means that we have to calculate end of body,
+ # as we cannot use `content_length[1]`
+ # Issue: https://gitlab.com/gitlab-org/gitlab-runner/issues/3275
+
+ body_data = request.body.read
+ body_start = content_range[0].to_i
+ body_end = body_start + body_data.bytesize
+
+ stream_size = job.trace.append(body_data, body_start)
+ unless stream_size == body_end
+ break error!('416 Range Not Satisfiable', 416, { 'Range' => "0-#{stream_size}" })
end
status 202
diff --git a/lib/gitlab/auth/blocked_user_tracker.rb b/lib/gitlab/auth/blocked_user_tracker.rb
index dae03a179e4..7609a7b04f6 100644
--- a/lib/gitlab/auth/blocked_user_tracker.rb
+++ b/lib/gitlab/auth/blocked_user_tracker.rb
@@ -17,7 +17,9 @@ module Gitlab
# message passed along by Warden.
return unless message == User::BLOCKED_MESSAGE
- login = env.dig(ACTIVE_RECORD_REQUEST_PARAMS, 'user', 'login')
+ # Check for either LDAP or regular GitLab account logins
+ login = env.dig(ACTIVE_RECORD_REQUEST_PARAMS, 'username') ||
+ env.dig(ACTIVE_RECORD_REQUEST_PARAMS, 'user', 'login')
return unless login.present?
diff --git a/lib/gitlab/auth/user_access_denied_reason.rb b/lib/gitlab/auth/user_access_denied_reason.rb
new file mode 100644
index 00000000000..af310aa12fc
--- /dev/null
+++ b/lib/gitlab/auth/user_access_denied_reason.rb
@@ -0,0 +1,33 @@
+module Gitlab
+ module Auth
+ class UserAccessDeniedReason
+ def initialize(user)
+ @user = user
+ end
+
+ def rejection_message
+ case rejection_type
+ when :internal
+ 'This action cannot be performed by internal users'
+ when :terms_not_accepted
+ 'You must accept the Terms of Service in order to perform this action. '\
+ 'Please access GitLab from a web browser to accept these terms.'
+ else
+ 'Your account has been blocked.'
+ end
+ end
+
+ private
+
+ def rejection_type
+ if @user.internal?
+ :internal
+ elsif @user.required_terms_not_accepted?
+ :terms_not_accepted
+ else
+ :blocked
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/build_access.rb b/lib/gitlab/build_access.rb
new file mode 100644
index 00000000000..08a8f846ca5
--- /dev/null
+++ b/lib/gitlab/build_access.rb
@@ -0,0 +1,12 @@
+module Gitlab
+ class BuildAccess < UserAccess
+ attr_accessor :user, :project
+
+ # This bypasses the `can?(:access_git)`-check we normally do in `UserAccess`
+ # for CI. That way if a user was able to trigger a pipeline, then the
+ # build is allowed to clone the project.
+ def can_access_git?
+ true
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/chain/build.rb b/lib/gitlab/ci/pipeline/chain/build.rb
index 70732d26bbd..b5eb0cfa2f0 100644
--- a/lib/gitlab/ci/pipeline/chain/build.rb
+++ b/lib/gitlab/ci/pipeline/chain/build.rb
@@ -14,7 +14,8 @@ module Gitlab
trigger_requests: Array(@command.trigger_request),
user: @command.current_user,
pipeline_schedule: @command.schedule,
- protected: @command.protected_ref?
+ protected: @command.protected_ref?,
+ variables_attributes: Array(@command.variables_attributes)
)
@pipeline.set_config_source
diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb
index a1849b01c5d..a53c80d34f7 100644
--- a/lib/gitlab/ci/pipeline/chain/command.rb
+++ b/lib/gitlab/ci/pipeline/chain/command.rb
@@ -7,7 +7,7 @@ module Gitlab # rubocop:disable Naming/FileName
:origin_ref, :checkout_sha, :after_sha, :before_sha,
:trigger_request, :schedule,
:ignore_skip_ci, :save_incompleted,
- :seeds_block
+ :seeds_block, :variables_attributes
) do
include Gitlab::Utils::StrongMemoize
diff --git a/lib/gitlab/ci/trace.rb b/lib/gitlab/ci/trace.rb
index 47b67930c6d..fe15fabc2e8 100644
--- a/lib/gitlab/ci/trace.rb
+++ b/lib/gitlab/ci/trace.rb
@@ -36,16 +36,16 @@ module Gitlab
end
def set(data)
- write do |stream|
+ write('w+b') do |stream|
data = job.hide_secrets(data)
stream.set(data)
end
end
def append(data, offset)
- write do |stream|
+ write('a+b') do |stream|
current_length = stream.size
- break -current_length unless current_length == offset
+ break current_length unless current_length == offset
data = job.hide_secrets(data)
stream.append(data, offset)
@@ -54,13 +54,15 @@ module Gitlab
end
def exist?
- trace_artifact&.exists? || current_path.present? || old_trace.present?
+ trace_artifact&.exists? || job.trace_chunks.any? || current_path.present? || old_trace.present?
end
def read
stream = Gitlab::Ci::Trace::Stream.new do
if trace_artifact
trace_artifact.open
+ elsif job.trace_chunks.any?
+ Gitlab::Ci::Trace::ChunkedIO.new(job)
elsif current_path
File.open(current_path, "rb")
elsif old_trace
@@ -73,9 +75,15 @@ module Gitlab
stream&.close
end
- def write
+ def write(mode)
stream = Gitlab::Ci::Trace::Stream.new do
- File.open(ensure_path, "a+b")
+ if current_path
+ File.open(current_path, mode)
+ elsif Feature.enabled?('ci_enable_live_trace')
+ Gitlab::Ci::Trace::ChunkedIO.new(job)
+ else
+ File.open(ensure_path, mode)
+ end
end
yield(stream).tap do
@@ -92,6 +100,7 @@ module Gitlab
FileUtils.rm(trace_path, force: true)
end
+ job.trace_chunks.fast_destroy_all
job.erase_old_trace!
end
@@ -99,7 +108,12 @@ module Gitlab
raise ArchiveError, 'Already archived' if trace_artifact
raise ArchiveError, 'Job is not finished yet' unless job.complete?
- if current_path
+ if job.trace_chunks.any?
+ Gitlab::Ci::Trace::ChunkedIO.new(job) do |stream|
+ archive_stream!(stream)
+ stream.destroy!
+ end
+ elsif current_path
File.open(current_path) do |stream|
archive_stream!(stream)
FileUtils.rm(current_path)
@@ -116,7 +130,7 @@ module Gitlab
def archive_stream!(stream)
clone_file!(stream, JobArtifactUploader.workhorse_upload_path) do |clone_path|
- create_job_trace!(job, clone_path)
+ create_build_trace!(job, clone_path)
end
end
@@ -132,7 +146,7 @@ module Gitlab
end
end
- def create_job_trace!(job, path)
+ def create_build_trace!(job, path)
File.open(path) do |stream|
job.create_job_artifacts_trace!(
project: job.project,
diff --git a/lib/gitlab/ci/trace/chunked_io.rb b/lib/gitlab/ci/trace/chunked_io.rb
new file mode 100644
index 00000000000..bfe0c2a2c26
--- /dev/null
+++ b/lib/gitlab/ci/trace/chunked_io.rb
@@ -0,0 +1,231 @@
+##
+# This class is compatible with IO class (https://ruby-doc.org/core-2.3.1/IO.html)
+# source: https://gitlab.com/snippets/1685610
+module Gitlab
+ module Ci
+ class Trace
+ class ChunkedIO
+ CHUNK_SIZE = ::Ci::BuildTraceChunk::CHUNK_SIZE
+
+ FailedToGetChunkError = Class.new(StandardError)
+
+ attr_reader :build
+ attr_reader :tell, :size
+ attr_reader :chunk, :chunk_range
+
+ alias_method :pos, :tell
+
+ def initialize(build, &block)
+ @build = build
+ @chunks_cache = []
+ @tell = 0
+ @size = calculate_size
+ yield self if block_given?
+ end
+
+ def close
+ # no-op
+ end
+
+ def binmode
+ # no-op
+ end
+
+ def binmode?
+ true
+ end
+
+ def seek(pos, where = IO::SEEK_SET)
+ new_pos =
+ case where
+ when IO::SEEK_END
+ size + pos
+ when IO::SEEK_SET
+ pos
+ when IO::SEEK_CUR
+ tell + pos
+ else
+ -1
+ end
+
+ raise ArgumentError, 'new position is outside of file' if new_pos < 0 || new_pos > size
+
+ @tell = new_pos
+ end
+
+ def eof?
+ tell == size
+ end
+
+ def each_line
+ until eof?
+ line = readline
+ break if line.nil?
+
+ yield(line)
+ end
+ end
+
+ def read(length = nil, outbuf = "")
+ out = ""
+
+ length ||= size - tell
+
+ until length <= 0 || eof?
+ data = chunk_slice_from_offset
+ break if data.empty?
+
+ chunk_bytes = [CHUNK_SIZE - chunk_offset, length].min
+ chunk_data = data.byteslice(0, chunk_bytes)
+
+ out << chunk_data
+ @tell += chunk_data.bytesize
+ length -= chunk_data.bytesize
+ end
+
+ # If outbuf is passed, we put the output into the buffer. This supports IO.copy_stream functionality
+ if outbuf
+ outbuf.slice!(0, outbuf.bytesize)
+ outbuf << out
+ end
+
+ out
+ end
+
+ def readline
+ out = ""
+
+ until eof?
+ data = chunk_slice_from_offset
+ new_line = data.index("\n")
+
+ if !new_line.nil?
+ out << data[0..new_line]
+ @tell += new_line + 1
+ break
+ else
+ out << data
+ @tell += data.bytesize
+ end
+ end
+
+ out
+ end
+
+ def write(data)
+ start_pos = tell
+
+ while tell < start_pos + data.bytesize
+ # get slice from current offset till the end where it falls into chunk
+ chunk_bytes = CHUNK_SIZE - chunk_offset
+ chunk_data = data.byteslice(tell - start_pos, chunk_bytes)
+
+ # append data to chunk, overwriting from that point
+ ensure_chunk.append(chunk_data, chunk_offset)
+
+ # move offsets within buffer
+ @tell += chunk_data.bytesize
+ @size = [size, tell].max
+ end
+
+ tell - start_pos
+ ensure
+ invalidate_chunk_cache
+ end
+
+ def truncate(offset)
+ raise ArgumentError, 'Outside of file' if offset > size || offset < 0
+ return if offset == size # Skip the following process as it doesn't affect anything
+
+ @tell = offset
+ @size = offset
+
+ # remove all next chunks
+ trace_chunks.where('chunk_index > ?', chunk_index).fast_destroy_all
+
+ # truncate current chunk
+ current_chunk.truncate(chunk_offset)
+ ensure
+ invalidate_chunk_cache
+ end
+
+ def flush
+ # no-op
+ end
+
+ def present?
+ true
+ end
+
+ def destroy!
+ trace_chunks.fast_destroy_all
+ @tell = @size = 0
+ ensure
+ invalidate_chunk_cache
+ end
+
+ private
+
+ ##
+ # The below methods are not implemented in IO class
+ #
+ def in_range?
+ @chunk_range&.include?(tell)
+ end
+
+ def chunk_slice_from_offset
+ unless in_range?
+ current_chunk.tap do |chunk|
+ raise FailedToGetChunkError unless chunk
+
+ @chunk = chunk.data
+ @chunk_range = chunk.range
+ end
+ end
+
+ @chunk[chunk_offset..CHUNK_SIZE]
+ end
+
+ def chunk_offset
+ tell % CHUNK_SIZE
+ end
+
+ def chunk_index
+ tell / CHUNK_SIZE
+ end
+
+ def chunk_start
+ chunk_index * CHUNK_SIZE
+ end
+
+ def chunk_end
+ [chunk_start + CHUNK_SIZE, size].min
+ end
+
+ def invalidate_chunk_cache
+ @chunks_cache = []
+ end
+
+ def current_chunk
+ @chunks_cache[chunk_index] ||= trace_chunks.find_by(chunk_index: chunk_index)
+ end
+
+ def build_chunk
+ @chunks_cache[chunk_index] = ::Ci::BuildTraceChunk.new(build: build, chunk_index: chunk_index)
+ end
+
+ def ensure_chunk
+ current_chunk || build_chunk
+ end
+
+ def trace_chunks
+ ::Ci::BuildTraceChunk.where(build: build)
+ end
+
+ def calculate_size
+ trace_chunks.order(chunk_index: :desc).first.try(&:end_offset).to_i
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/trace/stream.rb b/lib/gitlab/ci/trace/stream.rb
index 187ad8b833a..a71040e5e56 100644
--- a/lib/gitlab/ci/trace/stream.rb
+++ b/lib/gitlab/ci/trace/stream.rb
@@ -39,6 +39,8 @@ module Gitlab
end
def append(data, offset)
+ data = data.force_encoding(Encoding::BINARY)
+
stream.truncate(offset)
stream.seek(0, IO::SEEK_END)
stream.write(data)
@@ -46,8 +48,11 @@ module Gitlab
end
def set(data)
- truncate(0)
+ data = data.force_encoding(Encoding::BINARY)
+
+ stream.seek(0, IO::SEEK_SET)
stream.write(data)
+ stream.truncate(data.bytesize)
stream.flush()
end
@@ -127,11 +132,11 @@ module Gitlab
buf += debris
debris, *lines = buf.each_line.to_a
lines.reverse_each do |line|
- yield(line.force_encoding('UTF-8'))
+ yield(line.force_encoding(Encoding.default_external))
end
end
- yield(debris.force_encoding('UTF-8')) unless debris.empty?
+ yield(debris.force_encoding(Encoding.default_external)) unless debris.empty?
end
def read_backward(length)
diff --git a/lib/gitlab/email/handler/create_note_handler.rb b/lib/gitlab/email/handler/create_note_handler.rb
index 8eea33b9ab5..5791dbd0484 100644
--- a/lib/gitlab/email/handler/create_note_handler.rb
+++ b/lib/gitlab/email/handler/create_note_handler.rb
@@ -8,6 +8,7 @@ module Gitlab
include ReplyProcessing
delegate :project, to: :sent_notification, allow_nil: true
+ delegate :noteable, to: :sent_notification
def can_handle?
mail_key =~ /\A\w+\z/
@@ -18,7 +19,7 @@ module Gitlab
validate_permission!(:create_note)
- raise NoteableNotFoundError unless sent_notification.noteable
+ raise NoteableNotFoundError unless noteable
raise EmptyEmailError if message.blank?
verify_record!(
diff --git a/lib/gitlab/email/handler/reply_processing.rb b/lib/gitlab/email/handler/reply_processing.rb
index 32c5caf93e8..da5ff350549 100644
--- a/lib/gitlab/email/handler/reply_processing.rb
+++ b/lib/gitlab/email/handler/reply_processing.rb
@@ -32,8 +32,12 @@ module Gitlab
def validate_permission!(permission)
raise UserNotFoundError unless author
raise UserBlockedError if author.blocked?
- raise ProjectNotFound unless author.can?(:read_project, project)
- raise UserNotAuthorizedError unless author.can?(permission, project)
+
+ if project
+ raise ProjectNotFound unless author.can?(:read_project, project)
+ end
+
+ raise UserNotAuthorizedError unless author.can?(permission, project || noteable)
end
def verify_record!(record:, invalid_exception:, record_name:)
diff --git a/lib/gitlab/file_detector.rb b/lib/gitlab/file_detector.rb
index cc2638172ec..77af7a868d0 100644
--- a/lib/gitlab/file_detector.rb
+++ b/lib/gitlab/file_detector.rb
@@ -14,6 +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},
# Configuration files
gitignore: '.gitignore',
diff --git a/lib/gitlab/git/blame.rb b/lib/gitlab/git/blame.rb
index 6d6ed065f79..4158d50cd9e 100644
--- a/lib/gitlab/git/blame.rb
+++ b/lib/gitlab/git/blame.rb
@@ -15,10 +15,7 @@ module Gitlab
def each
@blames.each do |blame|
- yield(
- Gitlab::Git::Commit.new(@repo, blame.commit),
- blame.line
- )
+ yield(blame.commit, blame.line)
end
end
@@ -60,9 +57,8 @@ module Gitlab
end
end
- # load all commits in single call
- commits.keys.each do |key|
- commits[key] = @repo.lookup(key)
+ Gitlab::Git::Commit.batch_by_oid(@repo, commits.keys).each do |commit|
+ commits[commit.sha] = commit
end
# get it together
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index eabcf46cf58..d78d29b7ac6 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -62,6 +62,12 @@ module Gitlab
end
end
+ # Returns an array of Blob instances just with the metadata, that means
+ # the data attribute has no content.
+ def batch_metadata(repository, blob_references)
+ batch(repository, blob_references, blob_size_limit: 0)
+ end
+
# Find LFS blobs given an array of sha ids
# Returns array of Gitlab::Git::Blob
# Does not guarantee blob data will be set
diff --git a/lib/gitlab/git/raw_diff_change.rb b/lib/gitlab/git/raw_diff_change.rb
index 92f6c45ce25..98de9328071 100644
--- a/lib/gitlab/git/raw_diff_change.rb
+++ b/lib/gitlab/git/raw_diff_change.rb
@@ -6,7 +6,15 @@ module Gitlab
attr_reader :blob_id, :blob_size, :old_path, :new_path, :operation
def initialize(raw_change)
- parse(raw_change)
+ if raw_change.is_a?(Gitaly::GetRawChangesResponse::RawChange)
+ @blob_id = raw_change.blob_id
+ @blob_size = raw_change.size
+ @old_path = raw_change.old_path.presence
+ @new_path = raw_change.new_path.presence
+ @operation = raw_change.operation&.downcase || :unknown
+ else
+ parse(raw_change)
+ end
end
private
@@ -20,13 +28,14 @@ module Gitlab
# 85bc2f9753afd5f4fc5d7c75f74f8d526f26b4f3 107 R060\tfiles/js/commit.js.coffee\tfiles/js/commit.coffee
def parse(raw_change)
@blob_id, @blob_size, @raw_operation, raw_paths = raw_change.split(' ', 4)
+ @blob_size = @blob_size.to_i
@operation = extract_operation
@old_path, @new_path = extract_paths(raw_paths)
end
def extract_paths(file_path)
case operation
- when :renamed
+ when :copied, :renamed
file_path.split(/\t/)
when :deleted
[file_path, nil]
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 60ce8cfc195..29a3a35812c 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -30,6 +30,7 @@ module Gitlab
EMPTY_REPOSITORY_CHECKSUM = '0000000000000000000000000000000000000000'.freeze
NoRepository = Class.new(StandardError)
+ InvalidRepository = Class.new(StandardError)
InvalidBlobName = Class.new(StandardError)
InvalidRef = Class.new(StandardError)
GitError = Class.new(StandardError)
@@ -581,19 +582,36 @@ module Gitlab
# old_rev and new_rev are commit ID's
# the result of this method is an array of Gitlab::Git::RawDiffChange
def raw_changes_between(old_rev, new_rev)
- result = []
+ @raw_changes_between ||= {}
- circuit_breaker.perform do
- Open3.pipeline_r(git_diff_cmd(old_rev, new_rev), format_git_cat_file_script, git_cat_file_cmd) do |last_stdout, wait_threads|
- last_stdout.each_line { |line| result << ::Gitlab::Git::RawDiffChange.new(line.chomp!) }
+ @raw_changes_between[[old_rev, new_rev]] ||= begin
+ return [] if new_rev.blank? || new_rev == Gitlab::Git::BLANK_SHA
+
+ gitaly_migrate(:raw_changes_between) do |is_enabled|
+ if is_enabled
+ gitaly_repository_client.raw_changes_between(old_rev, new_rev)
+ .each_with_object([]) do |msg, arr|
+ msg.raw_changes.each { |change| arr << ::Gitlab::Git::RawDiffChange.new(change) }
+ end
+ else
+ result = []
+
+ circuit_breaker.perform do
+ Open3.pipeline_r(git_diff_cmd(old_rev, new_rev), format_git_cat_file_script, git_cat_file_cmd) do |last_stdout, wait_threads|
+ last_stdout.each_line { |line| result << ::Gitlab::Git::RawDiffChange.new(line.chomp!) }
- if wait_threads.any? { |waiter| !waiter.value&.success? }
- raise ::Gitlab::Git::Repository::GitError, "Unabled to obtain changes between #{old_rev} and #{new_rev}"
+ if wait_threads.any? { |waiter| !waiter.value&.success? }
+ raise ::Gitlab::Git::Repository::GitError, "Unabled to obtain changes between #{old_rev} and #{new_rev}"
+ end
+ end
+ end
+
+ result
end
end
end
-
- result
+ rescue ArgumentError => e
+ raise Gitlab::Git::Repository::GitError.new(e)
end
# Returns the SHA of the most recent common ancestor of +from+ and +to+
@@ -1573,7 +1591,7 @@ module Gitlab
def checksum
gitaly_migrate(:calculate_checksum,
- status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
+ status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
gitaly_repository_client.calculate_checksum
else
@@ -2522,10 +2540,12 @@ module Gitlab
output, status = run_git(args)
if status.nil? || !status.zero?
- # Empty repositories return with a non-zero status and an empty output.
- return EMPTY_REPOSITORY_CHECKSUM if output&.empty?
+ # 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?
- raise ChecksumError, output
+ return EMPTY_REPOSITORY_CHECKSUM
end
refs = output.split("\n")
diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb
index 84a26fe4a6f..d75a5f15c29 100644
--- a/lib/gitlab/git/wiki.rb
+++ b/lib/gitlab/git/wiki.rb
@@ -67,7 +67,8 @@ module Gitlab
end
def page(title:, version: nil, dir: nil)
- @repository.gitaly_migrate(:wiki_find_page) do |is_enabled|
+ @repository.gitaly_migrate(:wiki_find_page,
+ status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
gitaly_find_page(title: title, version: version, dir: dir)
else
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 0d1ee73ca1a..db7c29be94b 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -2,8 +2,6 @@
# class return an instance of `GitlabAccessStatus`
module Gitlab
class GitAccess
- include Gitlab::Utils::StrongMemoize
-
UnauthorizedError = Class.new(StandardError)
NotFoundError = Class.new(StandardError)
ProjectCreationError = Class.new(StandardError)
@@ -17,7 +15,6 @@ module Gitlab
deploy_key_upload: 'This deploy key does not have write access to this project.',
no_repo: 'A repository for this project does not exist yet.',
project_not_found: 'The project you were looking for could not be found.',
- account_blocked: 'Your account has been blocked.',
command_not_allowed: "The command you're trying to execute is not allowed.",
upload_pack_disabled_over_http: 'Pulling over HTTP is not allowed.',
receive_pack_disabled_over_http: 'Pushing over HTTP is not allowed.',
@@ -108,8 +105,11 @@ module Gitlab
end
def check_active_user!
- if user && !user_access.allowed?
- raise UnauthorizedError, ERROR_MESSAGES[:account_blocked]
+ return unless user
+
+ unless user_access.allowed?
+ message = Gitlab::Auth::UserAccessDeniedReason.new(user).rejection_message
+ raise UnauthorizedError, message
end
end
@@ -340,6 +340,8 @@ module Gitlab
def user_access
@user_access ||= if ci?
CiAccess.new
+ elsif user && request_from_ci_build?
+ BuildAccess.new(user, project: project)
else
UserAccess.new(user, project: project)
end
diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb
index 498187997e1..132a5947f17 100644
--- a/lib/gitlab/gitaly_client/repository_service.rb
+++ b/lib/gitlab/gitaly_client/repository_service.rb
@@ -292,6 +292,14 @@ module Gitlab
request = Gitaly::CalculateChecksumRequest.new(repository: @gitaly_repo)
response = GitalyClient.call(@storage, :repository_service, :calculate_checksum, request)
response.checksum.presence
+ rescue GRPC::DataLoss => e
+ raise Gitlab::Git::Repository::InvalidRepository.new(e)
+ end
+
+ def raw_changes_between(from, to)
+ request = Gitaly::GetRawChangesRequest.new(repository: @gitaly_repo, from_revision: from, to_revision: to)
+
+ GitalyClient.call(@storage, :repository_service, :get_raw_changes, request)
end
end
end
diff --git a/lib/gitlab/gitaly_client/storage_settings.rb b/lib/gitlab/gitaly_client/storage_settings.rb
index 8668caf0c55..9a576e463e3 100644
--- a/lib/gitlab/gitaly_client/storage_settings.rb
+++ b/lib/gitlab/gitaly_client/storage_settings.rb
@@ -5,6 +5,14 @@ module Gitlab
# directly.
class StorageSettings
DirectPathAccessError = Class.new(StandardError)
+ InvalidConfigurationError = Class.new(StandardError)
+
+ INVALID_STORAGE_MESSAGE = <<~MSG.freeze
+ Storage is invalid because it has no `path` key.
+
+ For source installations, update your config/gitlab.yml Refer to gitlab.yml.example for an updated example.
+ If you're using the Gitlab Development Kit, you can update your configuration running `gdk reconfigure`.
+ MSG
# This class will give easily recognizable NoMethodErrors
Deprecated = Class.new
@@ -12,7 +20,8 @@ module Gitlab
attr_reader :legacy_disk_path
def initialize(storage)
- raise "expected a Hash, got a #{storage.class.name}" unless storage.is_a?(Hash)
+ raise InvalidConfigurationError, "expected a Hash, got a #{storage.class.name}" unless storage.is_a?(Hash)
+ raise InvalidConfigurationError, INVALID_STORAGE_MESSAGE unless storage.has_key?('path')
# Support a nil 'path' field because some of the circuit breaker tests use it.
@legacy_disk_path = File.expand_path(storage['path'], Rails.root) if storage['path']
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index 0d1c4f73c6e..21ac7f7e0b6 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -106,6 +106,7 @@ excluded_attributes:
- :last_repository_updated_at
- :last_repository_check_at
- :storage_version
+ - :remote_mirror_available_overridden
- :description_html
snippets:
- :expired_at
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index e3e9f156fb4..4a41a69840b 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -28,7 +28,7 @@ module Gitlab
IMPORTED_OBJECT_MAX_RETRIES = 5.freeze
- EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels group_label group_labels].freeze
+ EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels group_label group_labels project_feature].freeze
TOKEN_RESET_MODELS = %w[Ci::Trigger Ci::Build ProjectHook].freeze
diff --git a/lib/gitlab/metrics/prometheus.rb b/lib/gitlab/metrics/prometheus.rb
index d12ba0ec176..d41a855bff1 100644
--- a/lib/gitlab/metrics/prometheus.rb
+++ b/lib/gitlab/metrics/prometheus.rb
@@ -25,6 +25,14 @@ module Gitlab
end
end
+ def reset_registry!
+ clear_memoization(:registry)
+
+ REGISTRY_MUTEX.synchronize do
+ ::Prometheus::Client.reset!
+ end
+ end
+
def registry
strong_memoize(:registry) do
REGISTRY_MUTEX.synchronize do
diff --git a/lib/gitlab/multi_collection_paginator.rb b/lib/gitlab/multi_collection_paginator.rb
index 43921a8c1c0..fd5de73c526 100644
--- a/lib/gitlab/multi_collection_paginator.rb
+++ b/lib/gitlab/multi_collection_paginator.rb
@@ -5,7 +5,7 @@ module Gitlab
def initialize(*collections, per_page: nil)
raise ArgumentError.new('Only 2 collections are supported') if collections.size != 2
- @per_page = per_page || Kaminari.config.default_per_page
+ @per_page = (per_page || Kaminari.config.default_per_page).to_i
@first_collection, @second_collection = collections
end
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 390efda326a..2e9b6e302f5 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -59,7 +59,7 @@ module Gitlab
startline = 0
result.each_line.each_with_index do |line, index|
- prefix ||= line.match(/^(?<ref>[^:]*):(?<filename>.*)\x00(?<startline>\d+)\x00/)&.tap do |matches|
+ prefix ||= line.match(/^(?<ref>[^:]*):(?<filename>[^\x00]*)\x00(?<startline>\d+)\x00/)&.tap do |matches|
ref = matches[:ref]
filename = matches[:filename]
startline = matches[:startline]
diff --git a/lib/gitlab/repo_path.rb b/lib/gitlab/repo_path.rb
index 1fa2a19b0af..4888184403c 100644
--- a/lib/gitlab/repo_path.rb
+++ b/lib/gitlab/repo_path.rb
@@ -4,7 +4,8 @@ module Gitlab
def self.parse(repo_path)
wiki = false
- project_path = strip_storage_path(repo_path.sub(/\.git\z/, ''), fail_on_not_found: false)
+ project_path = repo_path.sub(/\.git\z/, '').sub(%r{\A/}, '')
+
project, was_redirected = find_project(project_path)
if project_path.end_with?('.wiki') && project.nil?
@@ -17,22 +18,6 @@ module Gitlab
[project, wiki, redirected_path]
end
- def self.strip_storage_path(repo_path, fail_on_not_found: true)
- result = repo_path
-
- storage = Gitlab.config.repositories.storages.values.find do |params|
- repo_path.start_with?(params.legacy_disk_path)
- end
-
- if storage
- result = result.sub(storage.legacy_disk_path, '')
- elsif fail_on_not_found
- raise NotFoundError.new("No known storage path matches #{repo_path.inspect}")
- end
-
- result.sub(%r{\A/*}, '')
- end
-
def self.find_project(project_path)
project = Project.find_by_full_path(project_path, follow_redirects: true)
was_redirected = project && project.full_path.casecmp(project_path) != 0
diff --git a/lib/gitlab/untrusted_regexp.rb b/lib/gitlab/untrusted_regexp.rb
index 7ce2e9d636e..75ba0799058 100644
--- a/lib/gitlab/untrusted_regexp.rb
+++ b/lib/gitlab/untrusted_regexp.rb
@@ -11,7 +11,11 @@ module Gitlab
class UntrustedRegexp
delegate :===, to: :regexp
- def initialize(pattern)
+ def initialize(pattern, multiline: false)
+ if multiline
+ pattern = "(?m)#{pattern}"
+ end
+
@regexp = RE2::Regexp.new(pattern, log_errors: false)
raise RegexpError.new(regexp.error) unless regexp.ok?
@@ -31,6 +35,19 @@ module Gitlab
RE2.Replace(text, regexp, rewrite)
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.
+ #
+ # One difference between these is `(?m)` multi-line mode. Ruby regex enables
+ # this by default, but also handles `^` and `$` differently.
+ # See: https://www.regular-expressions.info/modifiers.html
+ def self.with_fallback(pattern, multiline: false)
+ UntrustedRegexp.new(pattern, multiline: multiline)
+ rescue RegexpError
+ Regexp.new(pattern)
+ end
+
private
attr_reader :regexp
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 8c0a4d55ea2..e294f3c4ebc 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -71,6 +71,7 @@ module Gitlab
projects_imported_from_github: Project.where(import_type: 'github').count,
protected_branches: ProtectedBranch.count,
releases: Release.count,
+ remote_mirrors: RemoteMirror.count,
snippets: Snippet.count,
todos: Todo.count,
uploads: Upload.count,
diff --git a/lib/gitlab/middleware/webpack_proxy.rb b/lib/gitlab/webpack/dev_server_middleware.rb
index 6aecf63231f..b9a75eaac63 100644
--- a/lib/gitlab/middleware/webpack_proxy.rb
+++ b/lib/gitlab/webpack/dev_server_middleware.rb
@@ -3,8 +3,8 @@
# :nocov:
module Gitlab
- module Middleware
- class WebpackProxy < Rack::Proxy
+ module Webpack
+ class DevServerMiddleware < Rack::Proxy
def initialize(app = nil, opts = {})
@proxy_host = opts.fetch(:proxy_host, 'localhost')
@proxy_port = opts.fetch(:proxy_port, 3808)
diff --git a/lib/gitlab/webpack/manifest.rb b/lib/gitlab/webpack/manifest.rb
new file mode 100644
index 00000000000..0c343e5bc1d
--- /dev/null
+++ b/lib/gitlab/webpack/manifest.rb
@@ -0,0 +1,27 @@
+require 'webpack/rails/manifest'
+
+module Gitlab
+ module Webpack
+ class Manifest < ::Webpack::Rails::Manifest
+ # Raised if a supplied asset does not exist in the webpack manifest
+ AssetMissingError = Class.new(StandardError)
+
+ class << self
+ def entrypoint_paths(source)
+ raise ::Webpack::Rails::Manifest::WebpackError, manifest["errors"] unless manifest_bundled?
+
+ entrypoint = manifest["entrypoints"][source]
+ if entrypoint && entrypoint["assets"]
+ # Can be either a string or an array of strings.
+ # Do not include source maps as they are not javascript
+ [entrypoint["assets"]].flatten.reject { |p| p =~ /.*\.map$/ }.map do |p|
+ "/#{::Rails.configuration.webpack.public_path}/#{p}"
+ end
+ else
+ raise AssetMissingError, "Can't find entry point '#{source}' in webpack manifest"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/test.rake b/lib/tasks/gitlab/test.rake
index 523b0fa055b..2222807fe13 100644
--- a/lib/tasks/gitlab/test.rake
+++ b/lib/tasks/gitlab/test.rake
@@ -4,7 +4,6 @@ namespace :gitlab do
cmds = [
%w(rake brakeman),
%w(rake rubocop),
- %w(rake spinach),
%w(rake spec),
%w(rake karma)
]
diff --git a/lib/tasks/migrate/add_limits_mysql.rake b/lib/tasks/migrate/add_limits_mysql.rake
index 151f42a2222..c6204f89de4 100644
--- a/lib/tasks/migrate/add_limits_mysql.rake
+++ b/lib/tasks/migrate/add_limits_mysql.rake
@@ -1,6 +1,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')
desc "GitLab | Add limits to strings in mysql database"
task add_limits_mysql: :environment do
@@ -8,4 +9,5 @@ task add_limits_mysql: :environment do
LimitsToMysql.new.up
MarkdownCacheLimitsToMysql.new.up
MergeRequestDiffFileLimitsToMysql.new.up
+ LimitsCiBuildTraceChunksRawDataForMysql.new.up
end
diff --git a/lib/tasks/spinach.rake b/lib/tasks/spinach.rake
deleted file mode 100644
index 19ff13f06c0..00000000000
--- a/lib/tasks/spinach.rake
+++ /dev/null
@@ -1,60 +0,0 @@
-Rake::Task["spinach"].clear if Rake::Task.task_defined?('spinach')
-
-namespace :spinach do
- namespace :project do
- desc "GitLab | Spinach | Run project commits, issues and merge requests spinach features"
- task :half do
- run_spinach_tests('@project_commits,@project_issues,@project_merge_requests')
- end
-
- desc "GitLab | Spinach | Run remaining project spinach features"
- task :rest do
- run_spinach_tests('~@admin,~@dashboard,~@profile,~@public,~@snippets,~@project_commits,~@project_issues,~@project_merge_requests')
- end
- end
-
- desc "GitLab | Spinach | Run project spinach features"
- task :project do
- run_spinach_tests('~@admin,~@dashboard,~@profile,~@public,~@snippets')
- end
-
- desc "GitLab | Spinach | Run other spinach features"
- task :other do
- run_spinach_tests('@admin,@dashboard,@profile,@public,@snippets')
- end
-
- desc "GitLab | Spinach | Run other spinach features"
- task :builds do
- run_spinach_tests('@builds')
- end
-end
-
-desc "GitLab | Run spinach"
-task :spinach do
- run_spinach_tests(nil)
-end
-
-def run_system_command(cmd)
- system({ 'RAILS_ENV' => 'test', 'force' => 'yes' }, *cmd)
-end
-
-def run_spinach_command(args)
- run_system_command(%w(spinach -r rerun) + args)
-end
-
-def run_spinach_tests(tags)
- success = run_spinach_command(%W(--tags #{tags}))
- 3.times do |_|
- break if success
- break unless File.exist?('tmp/spinach-rerun.txt')
-
- tests = File.foreach('tmp/spinach-rerun.txt').map(&:chomp)
- puts ''
- puts "Spinach tests for #{tags}: Retrying tests... #{tests}".color(:red)
- puts ''
- sleep(3)
- success = run_spinach_command(tests)
- end
-
- raise("spinach tests for #{tags} failed!") unless success
-end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 728c3605131..20377dc81b2 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-02 22:28+0200\n"
-"PO-Revision-Date: 2018-05-02 22:28+0200\n"
+"POT-Creation-Date: 2018-05-09 09:24+0200\n"
+"PO-Revision-Date: 2018-05-09 09:24+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
@@ -104,6 +104,12 @@ msgstr ""
msgid "+ %{moreCount} more"
msgstr ""
+msgid "- Runner is active and can process any new jobs"
+msgstr ""
+
+msgid "- Runner is paused and will not receive any new jobs"
+msgstr ""
+
msgid "- show less"
msgstr ""
@@ -156,6 +162,9 @@ msgstr ""
msgid "<strong>Removes</strong> source branch"
msgstr ""
+msgid "A 'Runner' is a process which runs a job. You can setup as many Runners as you need."
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr ""
@@ -387,6 +396,9 @@ msgstr ""
msgid "Artifacts"
msgstr ""
+msgid "Ask your group master to setup a group Runner."
+msgstr ""
+
msgid "Assign custom color like #FF0000"
msgstr ""
@@ -474,6 +486,12 @@ msgstr ""
msgid "Available"
msgstr ""
+msgid "Available group Runners : %{runners}"
+msgstr ""
+
+msgid "Available group Runners : %{runners}."
+msgstr ""
+
msgid "Avatar will be removed. Are you sure?"
msgstr ""
@@ -716,6 +734,9 @@ msgstr ""
msgid "CI/CD configuration"
msgstr ""
+msgid "CI/CD settings"
+msgstr ""
+
msgid "CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery."
msgstr ""
@@ -749,6 +770,9 @@ msgstr ""
msgid "CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages."
msgstr ""
+msgid "Can run untagged jobs"
+msgstr ""
+
msgid "Cancel"
msgstr ""
@@ -1652,6 +1676,15 @@ msgstr ""
msgid "Directory name"
msgstr ""
+msgid "Disable"
+msgstr ""
+
+msgid "Disable for this project"
+msgstr ""
+
+msgid "Disable group Runners"
+msgstr ""
+
msgid "Discard changes"
msgstr ""
@@ -1703,6 +1736,9 @@ msgstr ""
msgid "Due date"
msgstr ""
+msgid "Each Runner can be in one of the following states:"
+msgstr ""
+
msgid "Edit"
msgstr ""
@@ -1724,6 +1760,9 @@ msgstr ""
msgid "Embed"
msgstr ""
+msgid "Enable"
+msgstr ""
+
msgid "Enable Auto DevOps"
msgstr ""
@@ -1736,6 +1775,12 @@ msgstr ""
msgid "Enable and configure Prometheus metrics."
msgstr ""
+msgid "Enable for this project"
+msgstr ""
+
+msgid "Enable group Runners"
+msgstr ""
+
msgid "Enable or disable version check and usage ping."
msgstr ""
@@ -1978,6 +2023,9 @@ msgstr ""
msgid "GitLab CI Linter has been moved"
msgstr ""
+msgid "GitLab Group Runners can execute code for all the projects in this group."
+msgstr ""
+
msgid "GitLab Runner section"
msgstr ""
@@ -2002,9 +2050,18 @@ msgstr ""
msgid "Got it!"
msgstr ""
+msgid "Group CI/CD settings"
+msgstr ""
+
msgid "Group ID"
msgstr ""
+msgid "Group Runners"
+msgstr ""
+
+msgid "Group masters can register group runners in the %{link}"
+msgstr ""
+
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
msgstr ""
@@ -2315,9 +2372,18 @@ msgstr ""
msgid "Lock %{issuableDisplayName}"
msgstr ""
+msgid "Lock to current projects"
+msgstr ""
+
msgid "Locked"
msgstr ""
+msgid "Locked to current projects"
+msgstr ""
+
+msgid "Locked to this project"
+msgstr ""
+
msgid "Login"
msgstr ""
@@ -2348,6 +2414,9 @@ msgstr ""
msgid "Maximum git storage failures"
msgstr ""
+msgid "Maximum job timeout"
+msgstr ""
+
msgid "May"
msgstr ""
@@ -2491,6 +2560,9 @@ msgstr ""
msgid "New tag"
msgstr ""
+msgid "No"
+msgstr ""
+
msgid "No assignee"
msgstr ""
@@ -2680,6 +2752,9 @@ msgstr ""
msgid "Password"
msgstr ""
+msgid "Pause"
+msgstr ""
+
msgid "Pending"
msgstr ""
@@ -3145,6 +3220,9 @@ msgstr ""
msgid "Register / Sign In"
msgstr ""
+msgid "Register and see your runners for this group."
+msgstr ""
+
msgid "Related Commits"
msgstr ""
@@ -3169,6 +3247,12 @@ msgstr ""
msgid "Remind later"
msgstr ""
+msgid "Remove"
+msgstr ""
+
+msgid "Remove Runner"
+msgstr ""
+
msgid "Remove avatar"
msgstr ""
@@ -3202,6 +3286,9 @@ msgstr ""
msgid "Resolve discussion"
msgstr ""
+msgid "Resume"
+msgstr ""
+
msgid "Retry"
msgstr ""
@@ -3234,6 +3321,15 @@ msgstr ""
msgid "Runners"
msgstr ""
+msgid "Runners API"
+msgstr ""
+
+msgid "Runners can be placed on separate users, servers, and even on your local machine."
+msgstr ""
+
+msgid "Runners settings"
+msgstr ""
+
msgid "Running"
msgstr ""
@@ -3360,6 +3456,9 @@ msgstr ""
msgid "Share"
msgstr ""
+msgid "Shared Runners"
+msgstr ""
+
msgid "Show command"
msgstr ""
@@ -3515,6 +3614,9 @@ msgstr ""
msgid "Spam and Anti-bot Protection"
msgstr ""
+msgid "Specific Runners"
+msgstr ""
+
msgid "Specify the following URL during the Runner setup:"
msgstr ""
@@ -3772,9 +3874,18 @@ msgstr ""
msgid "There was an error when unsubscribing from this label."
msgstr ""
+msgid "They can be managed using the %{link}."
+msgstr ""
+
+msgid "This GitLab instance does not provide any shared Runners yet. Instance administrators can register shared Runners in the admin area."
+msgstr ""
+
msgid "This directory"
msgstr ""
+msgid "This group does not provide any group Runners yet."
+msgstr ""
+
msgid "This is a confidential issue."
msgstr ""
@@ -3832,6 +3943,9 @@ msgstr ""
msgid "This project"
msgstr ""
+msgid "This project does not belong to a group and can therefore not make use of group Runners."
+msgstr ""
+
msgid "This repository"
msgstr ""
@@ -4016,6 +4130,9 @@ msgstr ""
msgid "To import an SVN repository, check out %{svn_link}."
msgstr ""
+msgid "To start serving your jobs you can add Runners to your group"
+msgstr ""
+
msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
msgstr ""
@@ -4187,6 +4304,9 @@ msgstr ""
msgid "Web terminal"
msgstr ""
+msgid "When a runner is locked, it cannot be assigned to other projects"
+msgstr ""
+
msgid "When enabled, users cannot use GitLab until the terms have been accepted."
msgstr ""
@@ -4304,6 +4424,9 @@ msgstr ""
msgid "Write a commit message..."
msgstr ""
+msgid "Yes"
+msgstr ""
+
msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
msgstr ""
@@ -4447,6 +4570,9 @@ msgstr ""
msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
msgstr ""
+msgid "for this project"
+msgstr ""
+
msgid "importing"
msgstr ""
diff --git a/package.json b/package.json
index e95add1d7e9..59b4988b264 100644
--- a/package.json
+++ b/package.json
@@ -16,11 +16,11 @@
"webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js"
},
"dependencies": {
- "@gitlab-org/gitlab-svgs": "^1.18.0",
+ "@gitlab-org/gitlab-svgs": "^1.22.0",
"autosize": "^4.0.0",
"axios": "^0.17.1",
- "babel-core": "^6.26.0",
- "babel-loader": "^7.1.2",
+ "babel-core": "^6.26.3",
+ "babel-loader": "^7.1.4",
"babel-plugin-transform-define": "^1.3.0",
"babel-preset-latest": "^6.24.1",
"babel-preset-stage-2": "^6.24.1",
@@ -30,11 +30,11 @@
"chart.js": "1.0.2",
"classlist-polyfill": "^1.2.0",
"clipboard": "^1.7.1",
- "compression-webpack-plugin": "^1.1.7",
- "copy-webpack-plugin": "^4.4.1",
+ "compression-webpack-plugin": "^1.1.11",
+ "copy-webpack-plugin": "^4.5.1",
"core-js": "^2.4.1",
"cropper": "^2.3.0",
- "css-loader": "^0.28.9",
+ "css-loader": "^0.28.11",
"d3-array": "^1.2.1",
"d3-axis": "^1.0.8",
"d3-brush": "^1.0.4",
@@ -49,7 +49,7 @@
"dropzone": "^4.2.0",
"emoji-unicode-version": "^0.2.1",
"exports-loader": "^0.7.0",
- "file-loader": "^1.1.8",
+ "file-loader": "^1.1.11",
"fuzzaldrin-plus": "^0.5.0",
"glob": "^7.1.2",
"imports-loader": "^0.8.0",
@@ -64,24 +64,23 @@
"marked": "^0.3.12",
"monaco-editor": "0.10.0",
"mousetrap": "^1.4.6",
- "name-all-modules-plugin": "^1.0.1",
"pikaday": "^1.6.1",
"prismjs": "^1.6.0",
"raphael": "^2.2.7",
"raven-js": "^3.22.1",
"raw-loader": "^0.5.1",
- "react-dev-utils": "^5.0.0",
"sanitize-html": "^1.16.1",
"select2": "3.5.2-browserify",
+ "sha1": "^1.1.1",
"sql.js": "^0.4.0",
- "style-loader": "^0.20.2",
+ "style-loader": "^0.21.0",
"svg4everybody": "2.1.9",
"three": "^0.84.0",
"three-orbit-controls": "^82.1.0",
"three-stl-loader": "^1.0.4",
"timeago.js": "^3.0.2",
- "underscore": "^1.8.3",
- "url-loader": "^0.6.2",
+ "underscore": "^1.9.0",
+ "url-loader": "^1.0.1",
"visibilityjs": "^1.2.4",
"vue": "^2.5.16",
"vue-loader": "^14.1.1",
@@ -90,13 +89,14 @@
"vue-template-compiler": "^2.5.16",
"vue-virtual-scroll-list": "^1.2.5",
"vuex": "^3.0.1",
- "webpack": "^3.11.0",
- "webpack-bundle-analyzer": "^2.10.0",
- "webpack-stats-plugin": "^0.1.5",
- "worker-loader": "^1.1.0"
+ "webpack": "^4.7.0",
+ "webpack-bundle-analyzer": "^2.11.1",
+ "webpack-cli": "^2.1.2",
+ "webpack-stats-plugin": "^0.2.1",
+ "worker-loader": "^1.1.1"
},
"devDependencies": {
- "axios-mock-adapter": "^1.10.0",
+ "axios-mock-adapter": "^1.15.0",
"babel-eslint": "^8.0.2",
"babel-plugin-istanbul": "^4.1.6",
"babel-plugin-rewire": "^1.1.0",
@@ -124,8 +124,8 @@
"karma-mocha-reporter": "^2.2.5",
"karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "3.0.0",
- "nodemon": "^1.15.1",
+ "nodemon": "^1.17.3",
"prettier": "1.11.1",
- "webpack-dev-server": "^2.11.2"
+ "webpack-dev-server": "^3.1.4"
}
}
diff --git a/qa/qa/page/project/job/show.rb b/qa/qa/page/project/job/show.rb
index 21bda74efb2..83bb224b5c3 100644
--- a/qa/qa/page/project/job/show.rb
+++ b/qa/qa/page/project/job/show.rb
@@ -1,18 +1,28 @@
module QA::Page
module Project::Job
class Show < QA::Page::Base
+ COMPLETED_STATUSES = %w[passed failed canceled blocked skipped manual].freeze # excludes created, pending, running
+ PASSED_STATUS = 'passed'.freeze
+
view 'app/views/projects/jobs/show.html.haml' do
element :build_output, '.js-build-output'
end
- def output
- css = '.js-build-output'
+ view 'app/assets/javascripts/vue_shared/components/ci_badge_link.vue' do
+ element :status_badge, 'ci-status'
+ end
- wait(reload: false) do
- has_css?(css)
- end
+ def completed?
+ COMPLETED_STATUSES.include? find('.ci-status').text
+ end
- find(css).text
+ def passed?
+ find('.ci-status').text == PASSED_STATUS
+ end
+
+ # Reminder: You may wish to wait for a particular job status before checking output
+ def output
+ find('.js-build-output').text
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 98ea86bf75e..bf8fa230244 100644
--- a/qa/qa/specs/features/project/deploy_key_clone_spec.rb
+++ b/qa/qa/specs/features/project/deploy_key_clone_spec.rb
@@ -87,16 +87,12 @@ module QA
Page::Project::Show.act { wait_for_push }
Page::Menu::Side.act { click_ci_cd_pipelines }
Page::Project::Pipeline::Index.act { go_to_latest_pipeline }
-
- Page::Project::Pipeline::Show.act do
- go_to_first_job
-
- wait do
- !has_content?('running')
- end
- end
+ Page::Project::Pipeline::Show.act { go_to_first_job }
Page::Project::Job::Show.perform do |job|
+ job.wait(reload: false) { job.completed? }
+
+ expect(job.passed?).to be_truthy, "Job status did not become \"passed\"."
expect(job.output).to include(sha1sum)
end
end
diff --git a/scripts/gitaly-test-build b/scripts/gitaly-test-build
index b42ae2a2595..374401caf89 100755
--- a/scripts/gitaly-test-build
+++ b/scripts/gitaly-test-build
@@ -2,28 +2,29 @@
require 'fileutils'
+require_relative 'gitaly_test'
+
# This script assumes tmp/tests/gitaly already contains the correct
# Gitaly version. We just have to compile it and run its 'bundle
-# install'. We have this separate script for that because weird things
-# were happening in CI when we have a 'bundle exec' process that later
-# called 'bundle install' using a different Gemfile, as happens with
-# gitlab-ce and gitaly.
+# install'. We have this separate script for that to avoid bundle
+# poisoning in CI. This script should only be run in CI.
+class GitalyTestBuild
+ include GitalyTest
-tmp_tests_gitaly_dir = File.expand_path('../tmp/tests/gitaly', __dir__)
+ def run
+ abort 'gitaly build failed' unless system(env, 'make', chdir: tmp_tests_gitaly_dir)
-# Use the top-level bundle vendor folder so that we don't reinstall gems twice
-bundle_vendor_path = File.expand_path('../vendor', __dir__)
+ check_gitaly_config!
-env = {
- # This ensure the `clean` config set in `scripts/prepare_build.sh` isn't taken into account
- 'BUNDLE_IGNORE_CONFIG' => 'true',
- 'BUNDLE_GEMFILE' => File.join(tmp_tests_gitaly_dir, 'ruby', 'Gemfile'),
- 'BUNDLE_FLAGS' => "--jobs=4 --path=#{bundle_vendor_path} --retry=3"
-}
+ # Starting gitaly further validates its configuration
+ pid = start_gitaly
+ Process.kill('TERM', pid)
-abort 'gitaly build failed' unless system(env, 'make', chdir: tmp_tests_gitaly_dir)
+ # Make the 'gitaly' executable look newer than 'GITALY_SERVER_VERSION'.
+ # Without this a gitaly executable created in the setup-test-env job
+ # will look stale compared to GITALY_SERVER_VERSION.
+ FileUtils.touch(File.join(tmp_tests_gitaly_dir, 'gitaly'), mtime: Time.now + (1 << 24))
+ end
+end
-# Make the 'gitaly' executable look newer than 'GITALY_SERVER_VERSION'.
-# Without this a gitaly executable created in the setup-test-env job
-# will look stale compared to GITALY_SERVER_VERSION.
-FileUtils.touch(File.join(tmp_tests_gitaly_dir, 'gitaly'), mtime: Time.now + (1 << 24))
+GitalyTestBuild.new.run
diff --git a/scripts/gitaly-test-spawn b/scripts/gitaly-test-spawn
index ecb68c6acc6..e9f91f75650 100755
--- a/scripts/gitaly-test-spawn
+++ b/scripts/gitaly-test-spawn
@@ -1,9 +1,23 @@
#!/usr/bin/env ruby
-gitaly_dir = 'tmp/tests/gitaly'
-env = { 'HOME' => File.expand_path('tmp/tests'),
- 'GEM_PATH' => Gem.path.join(':') }
-args = %W[#{gitaly_dir}/gitaly #{gitaly_dir}/config.toml]
+# This script is used both in CI and in local development 'rspec' runs.
-# Print the PID of the spawned process
-puts spawn(env, *args, [:out, :err] => 'log/gitaly-test.log')
+require_relative 'gitaly_test'
+
+class GitalyTestSpawn
+ include GitalyTest
+
+ def run
+ check_gitaly_config!
+
+ # # Uncomment line below to see all gitaly logs merged into CI trace
+ # spawn('sleep 1; tail -f log/gitaly-test.log')
+
+ pid = start_gitaly
+
+ # In local development this pid file is used by rspec.
+ IO.write(File.expand_path('../tmp/tests/gitaly.pid', __dir__), pid)
+ end
+end
+
+GitalyTestSpawn.new.run
diff --git a/scripts/gitaly_test.rb b/scripts/gitaly_test.rb
new file mode 100644
index 00000000000..dee4c2eba7e
--- /dev/null
+++ b/scripts/gitaly_test.rb
@@ -0,0 +1,97 @@
+# This file contains environment settings for gitaly when it's running
+# as part of the gitlab-ce/ee test suite.
+#
+# Please be careful when modifying this file. Your changes must work
+# both for local development rspec runs, and in CI.
+
+require 'socket'
+
+module GitalyTest
+ def tmp_tests_gitaly_dir
+ File.expand_path('../tmp/tests/gitaly', __dir__)
+ end
+
+ def gemfile
+ File.join(tmp_tests_gitaly_dir, 'ruby', 'Gemfile')
+ end
+
+ def env
+ env_hash = {
+ 'HOME' => File.expand_path('tmp/tests'),
+ 'GEM_PATH' => Gem.path.join(':'),
+ 'BUNDLE_APP_CONFIG' => File.join(File.dirname(gemfile), '.bundle/config'),
+ 'BUNDLE_FLAGS' => "--jobs=4 --retry=3",
+ 'BUNDLE_INSTALL_FLAGS' => nil,
+ 'BUNDLE_GEMFILE' => gemfile,
+ 'RUBYOPT' => nil
+ }
+
+ if ENV['CI']
+ bundle_path = File.expand_path('../vendor/gitaly-ruby', __dir__)
+ env_hash['BUNDLE_FLAGS'] << " --path=#{bundle_path}"
+ end
+
+ env_hash
+ end
+
+ def config_path
+ File.join(tmp_tests_gitaly_dir, 'config.toml')
+ end
+
+ def start_gitaly
+ args = %W[#{tmp_tests_gitaly_dir}/gitaly #{config_path}]
+ pid = spawn(env, *args, [:out, :err] => 'log/gitaly-test.log')
+
+ begin
+ try_connect!
+ rescue
+ Process.kill('TERM', pid)
+ raise
+ end
+
+ pid
+ end
+
+ def check_gitaly_config!
+ puts 'Checking gitaly-ruby bundle...'
+ abort 'bundle check failed' unless system(env, 'bundle', 'check', chdir: File.dirname(gemfile))
+ end
+
+ def read_socket_path
+ # This code needs to work in an environment where we cannot use bundler,
+ # so we cannot easily use the toml-rb gem. This ad-hoc parser should be
+ # good enough.
+ config_text = IO.read(config_path)
+
+ config_text.lines.each do |line|
+ match_data = line.match(/^\s*socket_path\s*=\s*"([^"]*)"$/)
+
+ return match_data[1] if match_data
+ end
+
+ raise "failed to find socket_path in #{config_path}"
+ end
+
+ def try_connect!
+ print "Trying to connect to gitaly: "
+ timeout = 20
+ delay = 0.1
+ socket = read_socket_path
+
+ Integer(timeout / delay).times do
+ begin
+ UNIXSocket.new(socket)
+ puts ' OK'
+
+ return
+ rescue Errno::ENOENT, Errno::ECONNREFUSED
+ print '.'
+ sleep delay
+ end
+ end
+
+ puts ' FAILED'
+
+ raise "could not connect to #{socket}"
+ end
+end
diff --git a/spec/controllers/concerns/send_file_upload_spec.rb b/spec/controllers/concerns/send_file_upload_spec.rb
index f4c99ea4064..58bb91a0c80 100644
--- a/spec/controllers/concerns/send_file_upload_spec.rb
+++ b/spec/controllers/concerns/send_file_upload_spec.rb
@@ -51,6 +51,21 @@ describe SendFileUpload do
end
end
+ context 'with attachment' do
+ subject { controller.send_upload(uploader, attachment: 'test.js') }
+
+ it 'sends a file with content-type of text/plain' do
+ expected_params = {
+ content_type: 'text/plain',
+ filename: 'test.js',
+ disposition: 'attachment'
+ }
+ expect(controller).to receive(:send_file).with(uploader.path, expected_params)
+
+ subject
+ end
+ end
+
context 'when remote file is used' do
before do
stub_uploads_object_storage(uploader: uploader_class)
diff --git a/spec/controllers/groups/runners_controller_spec.rb b/spec/controllers/groups/runners_controller_spec.rb
new file mode 100644
index 00000000000..6d31b0ce959
--- /dev/null
+++ b/spec/controllers/groups/runners_controller_spec.rb
@@ -0,0 +1,74 @@
+require 'spec_helper'
+
+describe Groups::RunnersController do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:runner) { create(:ci_runner) }
+
+ let(:params) do
+ {
+ group_id: group,
+ id: runner
+ }
+ end
+
+ before do
+ sign_in(user)
+ group.add_master(user)
+ group.runners << runner
+ end
+
+ describe '#update' do
+ it 'updates the runner and ticks the queue' do
+ new_desc = runner.description.swapcase
+
+ expect do
+ post :update, params.merge(runner: { description: new_desc } )
+ end.to change { runner.ensure_runner_queue_value }
+
+ runner.reload
+
+ expect(response).to have_gitlab_http_status(302)
+ expect(runner.description).to eq(new_desc)
+ end
+ end
+
+ describe '#destroy' do
+ it 'destroys the runner' do
+ delete :destroy, params
+
+ expect(response).to have_gitlab_http_status(302)
+ expect(Ci::Runner.find_by(id: runner.id)).to be_nil
+ end
+ end
+
+ describe '#resume' do
+ it 'marks the runner as active and ticks the queue' do
+ runner.update(active: false)
+
+ expect do
+ post :resume, params
+ end.to change { runner.ensure_runner_queue_value }
+
+ runner.reload
+
+ expect(response).to have_gitlab_http_status(302)
+ expect(runner.active).to eq(true)
+ end
+ end
+
+ describe '#pause' do
+ it 'marks the runner as inactive and ticks the queue' do
+ runner.update(active: true)
+
+ expect do
+ post :pause, params
+ end.to change { runner.ensure_runner_queue_value }
+
+ runner.reload
+
+ expect(response).to have_gitlab_http_status(302)
+ expect(runner.active).to eq(false)
+ end
+ end
+end
diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb
index 046ce027965..b15cde4314e 100644
--- a/spec/controllers/projects/compare_controller_spec.rb
+++ b/spec/controllers/projects/compare_controller_spec.rb
@@ -3,96 +3,99 @@ require 'spec_helper'
describe Projects::CompareController do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
- let(:ref_from) { "improve%2Fawesome" }
- let(:ref_to) { "feature" }
before do
sign_in(user)
project.add_master(user)
end
- it 'compare shows some diffs' do
- get(:show,
- namespace_id: project.namespace,
- project_id: project,
- from: ref_from,
- to: ref_to)
+ describe 'GET index' do
+ render_views
+
+ before do
+ get :index, namespace_id: project.namespace, project_id: project
+ end
- expect(response).to be_success
- expect(assigns(:diffs).diff_files.first).not_to be_nil
- expect(assigns(:commits).length).to be >= 1
+ it 'returns successfully' do
+ expect(response).to be_success
+ end
end
- it 'compare shows some diffs with ignore whitespace change option' do
- get(:show,
+ describe 'GET show' do
+ render_views
+
+ subject(:show_request) { get :show, request_params }
+
+ let(:request_params) do
+ {
namespace_id: project.namespace,
project_id: project,
- from: '08f22f25',
- to: '66eceea0',
- w: 1)
-
- expect(response).to be_success
- diff_file = assigns(:diffs).diff_files.first
- expect(diff_file).not_to be_nil
- expect(assigns(:commits).length).to be >= 1
- # without whitespace option, there are more than 2 diff_splits
- diff_splits = diff_file.diff.diff.split("\n")
- expect(diff_splits.length).to be <= 2
- end
+ from: source_ref,
+ to: target_ref,
+ w: whitespace
+ }
+ end
- describe 'non-existent refs' do
- it 'uses invalid source ref' do
- get(:show,
- namespace_id: project.namespace,
- project_id: project,
- from: 'non-existent',
- to: ref_to)
+ let(:whitespace) { nil }
- expect(response).to be_success
- expect(assigns(:diffs).diff_files.to_a).to eq([])
- expect(assigns(:commits)).to eq([])
- end
+ context 'when the refs exist' do
+ context 'when we set the white space param' do
+ let(:source_ref) { "08f22f25" }
+ let(:target_ref) { "66eceea0" }
+ let(:whitespace) { 1 }
- it 'uses invalid target ref' do
- get(:show,
- namespace_id: project.namespace,
- project_id: project,
- from: ref_from,
- to: 'non-existent')
+ it 'shows some diffs with ignore whitespace change option' do
+ show_request
- expect(response).to be_success
- expect(assigns(:diffs)).to eq(nil)
- expect(assigns(:commits)).to eq(nil)
- end
+ expect(response).to be_success
+ diff_file = assigns(:diffs).diff_files.first
+ expect(diff_file).not_to be_nil
+ expect(assigns(:commits).length).to be >= 1
+ # without whitespace option, there are more than 2 diff_splits
+ diff_splits = diff_file.diff.diff.split("\n")
+ expect(diff_splits.length).to be <= 2
+ end
+ end
+
+ context 'when we do not set the white space param' do
+ let(:source_ref) { "improve%2Fawesome" }
+ let(:target_ref) { "feature" }
+ let(:whitespace) { nil }
- it 'redirects back to index when params[:from] is empty and preserves params[:to]' do
- post(:create,
- namespace_id: project.namespace,
- project_id: project,
- from: '',
- to: 'master')
+ it 'sets the diffs and commits ivars' do
+ show_request
- expect(response).to redirect_to(project_compare_index_path(project, to: 'master'))
+ expect(response).to be_success
+ expect(assigns(:diffs).diff_files.first).not_to be_nil
+ expect(assigns(:commits).length).to be >= 1
+ end
+ end
end
- it 'redirects back to index when params[:to] is empty and preserves params[:from]' do
- post(:create,
- namespace_id: project.namespace,
- project_id: project,
- from: 'master',
- to: '')
+ context 'when the source ref does not exist' do
+ let(:source_ref) { 'non-existent-source-ref' }
+ let(:target_ref) { "feature" }
+
+ it 'sets empty diff and commit ivars' do
+ show_request
- expect(response).to redirect_to(project_compare_index_path(project, from: 'master'))
+ expect(response).to be_success
+ expect(assigns(:diffs).diff_files.to_a).to eq([])
+ expect(assigns(:commits)).to eq([])
+ end
end
- it 'redirects back to index when params[:from] and params[:to] are empty' do
- post(:create,
- namespace_id: project.namespace,
- project_id: project,
- from: '',
- to: '')
+ context 'when the target ref does not exist' do
+ let(:target_ref) { 'non-existent-target-ref' }
+ let(:source_ref) { "improve%2Fawesome" }
- expect(response).to redirect_to(namespace_project_compare_index_path)
+ it 'sets empty diff and commit ivars' do
+ show_request
+
+ expect(response).to be_success
+ expect(assigns(:diffs)).to eq([])
+ expect(assigns(:commits)).to eq([])
+ end
end
end
@@ -107,12 +110,14 @@ describe Projects::CompareController do
end
let(:existing_path) { 'files/ruby/feature.rb' }
+ let(:source_ref) { "improve%2Fawesome" }
+ let(:target_ref) { "feature" }
- context 'when the from and to refs exist' do
- context 'when the user has access to the project' do
+ context 'when the source and target refs exist' do
+ context 'when the user has access target the project' do
context 'when the path exists in the diff' do
it 'disables diff notes' do
- diff_for_path(from: ref_from, to: ref_to, old_path: existing_path, new_path: existing_path)
+ diff_for_path(from: source_ref, to: target_ref, old_path: existing_path, new_path: existing_path)
expect(assigns(:diff_notes_disabled)).to be_truthy
end
@@ -123,13 +128,13 @@ describe Projects::CompareController do
meth.call(diffs)
end
- diff_for_path(from: ref_from, to: ref_to, old_path: existing_path, new_path: existing_path)
+ diff_for_path(from: source_ref, to: target_ref, old_path: existing_path, new_path: existing_path)
end
end
context 'when the path does not exist in the diff' do
before do
- diff_for_path(from: ref_from, to: ref_to, old_path: existing_path.succ, new_path: existing_path.succ)
+ diff_for_path(from: source_ref, to: target_ref, old_path: existing_path.succ, new_path: existing_path.succ)
end
it 'returns a 404' do
@@ -138,10 +143,10 @@ describe Projects::CompareController do
end
end
- context 'when the user does not have access to the project' do
+ context 'when the user does not have access target the project' do
before do
project.team.truncate
- diff_for_path(from: ref_from, to: ref_to, old_path: existing_path, new_path: existing_path)
+ diff_for_path(from: source_ref, to: target_ref, old_path: existing_path, new_path: existing_path)
end
it 'returns a 404' do
@@ -150,9 +155,9 @@ describe Projects::CompareController do
end
end
- context 'when the from ref does not exist' do
+ context 'when the source ref does not exist' do
before do
- diff_for_path(from: ref_from.succ, to: ref_to, old_path: existing_path, new_path: existing_path)
+ diff_for_path(from: source_ref.succ, to: target_ref, old_path: existing_path, new_path: existing_path)
end
it 'returns a 404' do
@@ -160,9 +165,9 @@ describe Projects::CompareController do
end
end
- context 'when the to ref does not exist' do
+ context 'when the target ref does not exist' do
before do
- diff_for_path(from: ref_from, to: ref_to.succ, old_path: existing_path, new_path: existing_path)
+ diff_for_path(from: source_ref, to: target_ref.succ, old_path: existing_path, new_path: existing_path)
end
it 'returns a 404' do
@@ -170,4 +175,153 @@ describe Projects::CompareController do
end
end
end
+
+ describe 'POST create' do
+ subject(:create_request) { post :create, request_params }
+
+ let(:request_params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ from: source_ref,
+ to: target_ref
+ }
+ end
+
+ context 'when sending valid params' do
+ let(:source_ref) { "improve%2Fawesome" }
+ let(:target_ref) { "feature" }
+
+ it 'redirects back to show' do
+ create_request
+
+ expect(response).to redirect_to(project_compare_path(project, to: target_ref, from: source_ref))
+ end
+ end
+
+ context 'when sending invalid params' do
+ context 'when the source ref is empty and target ref is set' do
+ let(:source_ref) { '' }
+ let(:target_ref) { 'master' }
+
+ it 'redirects back to index and preserves the target ref' do
+ create_request
+
+ expect(response).to redirect_to(project_compare_index_path(project, to: target_ref))
+ end
+ end
+
+ context 'when the target ref is empty and source ref is set' do
+ let(:source_ref) { 'master' }
+ let(:target_ref) { '' }
+
+ it 'redirects back to index and preserves source ref' do
+ create_request
+
+ expect(response).to redirect_to(project_compare_index_path(project, from: source_ref))
+ end
+ end
+
+ context 'when the target and source ref are empty' do
+ let(:source_ref) { '' }
+ let(:target_ref) { '' }
+
+ it 'redirects back to index' do
+ create_request
+
+ expect(response).to redirect_to(namespace_project_compare_index_path)
+ end
+ end
+ end
+ end
+
+ describe 'GET signatures' do
+ subject(:signatures_request) { get :signatures, request_params }
+
+ let(:request_params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ from: source_ref,
+ to: target_ref,
+ format: :json
+ }
+ end
+
+ context 'when the source and target refs exist' do
+ let(:source_ref) { "improve%2Fawesome" }
+ let(:target_ref) { "feature" }
+
+ context 'when the user has access to the project' do
+ render_views
+
+ let(:signature_commit) { build(:commit, project: project, safe_message: "message", sha: 'signature_commit') }
+ let(:non_signature_commit) { build(:commit, project: project, safe_message: "message", sha: 'non_signature_commit') }
+
+ before do
+ escaped_source_ref = Addressable::URI.unescape(source_ref)
+ escaped_target_ref = Addressable::URI.unescape(target_ref)
+
+ compare_service = CompareService.new(project, escaped_target_ref)
+ compare = compare_service.execute(project, escaped_source_ref)
+
+ expect(CompareService).to receive(:new).with(project, escaped_target_ref).and_return(compare_service)
+ expect(compare_service).to receive(:execute).with(project, escaped_source_ref).and_return(compare)
+
+ expect(compare).to receive(:commits).and_return([signature_commit, non_signature_commit])
+ expect(non_signature_commit).to receive(:has_signature?).and_return(false)
+ end
+
+ it 'returns only the commit with a signature' do
+ signatures_request
+
+ expect(response).to have_gitlab_http_status(200)
+ parsed_body = JSON.parse(response.body)
+ signatures = parsed_body['signatures']
+
+ expect(signatures.size).to eq(1)
+ expect(signatures.first['commit_sha']).to eq(signature_commit.sha)
+ expect(signatures.first['html']).to be_present
+ end
+ end
+
+ context 'when the user does not have access to the project' do
+ before do
+ project.team.truncate
+ end
+
+ it 'returns a 404' do
+ signatures_request
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ context 'when the source ref does not exist' do
+ let(:source_ref) { 'non-existent-ref-source' }
+ let(:target_ref) { "feature" }
+
+ it 'returns no signatures' do
+ signatures_request
+
+ expect(response).to have_gitlab_http_status(200)
+ parsed_body = JSON.parse(response.body)
+ expect(parsed_body['signatures']).to be_empty
+ end
+ end
+
+ context 'when the target ref does not exist' do
+ let(:target_ref) { 'non-existent-ref-target' }
+ let(:source_ref) { "improve%2Fawesome" }
+
+ it 'returns no signatures' do
+ signatures_request
+
+ expect(response).to have_gitlab_http_status(200)
+ parsed_body = JSON.parse(response.body)
+ expect(parsed_body['signatures']).to be_empty
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index b9a979044fe..a08fcea27a5 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -1,7 +1,7 @@
# coding: utf-8
require 'spec_helper'
-describe Projects::JobsController do
+describe Projects::JobsController, :clean_gitlab_redis_shared_state do
include ApiHelpers
include HttpIOHelpers
@@ -10,6 +10,7 @@ describe Projects::JobsController do
let(:user) { create(:user) }
before do
+ stub_feature_flags(ci_enable_live_trace: true)
stub_not_protect_default_branch
end
@@ -489,43 +490,43 @@ describe Projects::JobsController do
id: job.id
end
- context 'when job has a trace artifact' do
+ context "when job has a trace artifact" do
let(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) }
it 'returns a trace' do
response = subject
expect(response).to have_gitlab_http_status(:ok)
- expect(response.content_type).to eq 'text/plain; charset=utf-8'
- expect(response.body).to eq job.job_artifacts_trace.open.read
+ expect(response.headers["Content-Type"]).to eq("text/plain; charset=utf-8")
+ expect(response.body).to eq(job.job_artifacts_trace.open.read)
end
end
- context 'when job has a trace file' do
+ context "when job has a trace file" do
let(:job) { create(:ci_build, :trace_live, pipeline: pipeline) }
- it 'send a trace file' do
+ it "send a trace file" do
response = subject
expect(response).to have_gitlab_http_status(:ok)
- expect(response.content_type).to eq 'text/plain; charset=utf-8'
- expect(response.body).to eq 'BUILD TRACE'
+ expect(response.headers["Content-Type"]).to eq("text/plain; charset=utf-8")
+ expect(response.body).to eq("BUILD TRACE")
end
end
- context 'when job has a trace in database' do
+ context "when job has a trace in database" do
let(:job) { create(:ci_build, pipeline: pipeline) }
before do
- job.update_column(:trace, 'Sample trace')
+ job.update_column(:trace, "Sample trace")
end
- it 'send a trace file' do
+ it "send a trace file" do
response = subject
expect(response).to have_gitlab_http_status(:ok)
- expect(response.content_type).to eq 'text/plain; charset=utf-8'
- expect(response.body).to eq 'Sample trace'
+ expect(response.headers["Content-Type"]).to eq("text/plain; charset=utf-8")
+ expect(response.body).to eq("Sample trace")
end
end
diff --git a/spec/controllers/projects/mirrors_controller_spec.rb b/spec/controllers/projects/mirrors_controller_spec.rb
new file mode 100644
index 00000000000..45c1218a39c
--- /dev/null
+++ b/spec/controllers/projects/mirrors_controller_spec.rb
@@ -0,0 +1,72 @@
+require 'spec_helper'
+
+describe Projects::MirrorsController do
+ include ReactiveCachingHelpers
+
+ describe 'setting up a remote mirror' do
+ set(:project) { create(:project, :repository) }
+
+ context 'when the current project is not a mirror' do
+ it 'allows to create a remote mirror' do
+ sign_in(project.owner)
+
+ expect do
+ do_put(project, remote_mirrors_attributes: { '0' => { 'enabled' => 1, 'url' => 'http://foo.com' } })
+ end.to change { RemoteMirror.count }.to(1)
+ end
+ end
+ end
+
+ describe '#update' do
+ let(:project) { create(:project, :repository, :remote_mirror) }
+
+ before do
+ sign_in(project.owner)
+ end
+
+ around do |example|
+ Sidekiq::Testing.fake! { example.run }
+ end
+
+ context 'With valid URL for a push' do
+ let(:remote_mirror_attributes) do
+ { "0" => { "enabled" => "0", url: 'https://updated.example.com' } }
+ end
+
+ it 'processes a successful update' do
+ do_put(project, remote_mirrors_attributes: remote_mirror_attributes)
+
+ expect(response).to redirect_to(project_settings_repository_path(project))
+ expect(flash[:notice]).to match(/successfully updated/)
+ end
+
+ it 'should create a RemoteMirror object' do
+ expect { do_put(project, remote_mirrors_attributes: remote_mirror_attributes) }.to change(RemoteMirror, :count).by(1)
+ end
+ end
+
+ context 'With invalid URL for a push' do
+ let(:remote_mirror_attributes) do
+ { "0" => { "enabled" => "0", url: 'ftp://invalid.invalid' } }
+ end
+
+ it 'processes an unsuccessful update' do
+ do_put(project, remote_mirrors_attributes: remote_mirror_attributes)
+
+ expect(response).to redirect_to(project_settings_repository_path(project))
+ expect(flash[:alert]).to match(/must be a valid URL/)
+ end
+
+ it 'should not create a RemoteMirror object' do
+ expect { do_put(project, remote_mirrors_attributes: remote_mirror_attributes) }.not_to change(RemoteMirror, :count)
+ end
+ end
+ end
+
+ def do_put(project, options, extra_attrs = {})
+ attrs = extra_attrs.merge(namespace_id: project.namespace.to_param, project_id: project.to_param)
+ attrs[:project] = options
+
+ put :update, attrs
+ end
+end
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index 35ac999cc65..a451bbb97b6 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -109,8 +109,7 @@ describe Projects::PipelinesController do
it 'returns html source for stage dropdown' do
expect(response).to have_gitlab_http_status(:ok)
- expect(response).to render_template('projects/pipelines/_stage')
- expect(json_response).to include('html')
+ expect(response).to match_response_schema('pipeline_stage')
end
end
@@ -133,6 +132,42 @@ describe Projects::PipelinesController do
end
end
+ describe 'GET stages_ajax.json' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ context 'when accessing existing stage' do
+ before do
+ create(:ci_build, pipeline: pipeline, stage: 'build')
+
+ get_stage_ajax('build')
+ end
+
+ it 'returns html source for stage dropdown' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template('projects/pipelines/_stage')
+ expect(json_response).to include('html')
+ end
+ end
+
+ context 'when accessing unknown stage' do
+ before do
+ get_stage_ajax('test')
+ end
+
+ it 'responds with not found' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ def get_stage_ajax(name)
+ get :stage_ajax, namespace_id: project.namespace,
+ project_id: project,
+ id: pipeline.id,
+ stage: name,
+ format: :json
+ end
+ end
+
describe 'GET status.json' do
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:status) { pipeline.detailed_status(double('user')) }
diff --git a/spec/db/production/settings_spec.rb b/spec/db/production/settings_spec.rb
index c8d016070f5..db19e98b851 100644
--- a/spec/db/production/settings_spec.rb
+++ b/spec/db/production/settings_spec.rb
@@ -48,15 +48,15 @@ describe 'seed production settings' do
end
end
- context 'GITLAB_PROMETHEUS_METRICS_ENABLED is false' do
+ context 'GITLAB_PROMETHEUS_METRICS_ENABLED is default' do
before do
stub_env('GITLAB_PROMETHEUS_METRICS_ENABLED', '')
end
- it 'prometheus_metrics_enabled is set to false' do
+ it 'prometheus_metrics_enabled is set to true' do
load(settings_file)
- expect(settings.prometheus_metrics_enabled).to eq(false)
+ expect(settings.prometheus_metrics_enabled).to eq(true)
end
end
end
diff --git a/spec/factories/ci/build_trace_chunks.rb b/spec/factories/ci/build_trace_chunks.rb
new file mode 100644
index 00000000000..c0b9a25bfe8
--- /dev/null
+++ b/spec/factories/ci/build_trace_chunks.rb
@@ -0,0 +1,7 @@
+FactoryBot.define do
+ factory :ci_build_trace_chunk, class: Ci::BuildTraceChunk do
+ build factory: :ci_build
+ chunk_index 0
+ data_store :redis
+ end
+end
diff --git a/spec/factories/ci/runners.rb b/spec/factories/ci/runners.rb
index 34b8b246d0f..cdc170b9ccb 100644
--- a/spec/factories/ci/runners.rb
+++ b/spec/factories/ci/runners.rb
@@ -6,6 +6,7 @@ FactoryBot.define do
is_shared false
active true
access_level :not_protected
+ runner_type :project_type
trait :online do
contacted_at Time.now
@@ -13,6 +14,7 @@ FactoryBot.define do
trait :shared do
is_shared true
+ runner_type :instance_type
end
trait :specific do
diff --git a/spec/factories/clusters/clusters.rb b/spec/factories/clusters/clusters.rb
index 98566f907f9..0430762c1ff 100644
--- a/spec/factories/clusters/clusters.rb
+++ b/spec/factories/clusters/clusters.rb
@@ -4,8 +4,8 @@ FactoryBot.define do
name 'test-cluster'
trait :project do
- after(:create) do |cluster, evaluator|
- cluster.projects << create(:project)
+ before(:create) do |cluster, evaluator|
+ cluster.projects << create(:project, :repository)
end
end
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index d26cb0c3417..fab0ec22450 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -41,6 +41,11 @@ FactoryBot.define do
state :merged
end
+ trait :merged_target do
+ source_branch "merged-target"
+ target_branch "improve/awesome"
+ end
+
trait :closed do
state :closed
end
diff --git a/spec/factories/project_wikis.rb b/spec/factories/project_wikis.rb
index db2eb4fc863..4d21ed47f39 100644
--- a/spec/factories/project_wikis.rb
+++ b/spec/factories/project_wikis.rb
@@ -2,7 +2,7 @@ FactoryBot.define do
factory :project_wiki do
skip_create
- project
+ association :project, :wiki_repo
user { project.creator }
initialize_with { new(project, user) }
end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index a6128903546..16e025618a6 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -69,43 +69,19 @@ FactoryBot.define do
end
trait :import_scheduled do
- transient do
- status :scheduled
- end
-
- before(:create) do |project, evaluator|
- project.create_import_state(status: evaluator.status)
- end
+ import_status :scheduled
end
trait :import_started do
- transient do
- status :started
- end
-
- before(:create) do |project, evaluator|
- project.create_import_state(status: evaluator.status)
- end
+ import_status :started
end
trait :import_finished do
- transient do
- status :finished
- end
-
- before(:create) do |project, evaluator|
- project.create_import_state(status: evaluator.status)
- end
+ import_status :finished
end
trait :import_failed do
- transient do
- status :failed
- end
-
- before(:create) do |project, evaluator|
- project.create_import_state(status: evaluator.status)
- end
+ import_status :failed
end
trait :archived do
@@ -183,6 +159,17 @@ FactoryBot.define do
end
end
+ trait :remote_mirror do
+ transient do
+ remote_name "remote_mirror_#{SecureRandom.hex}"
+ url "http://foo.com"
+ enabled true
+ end
+ after(:create) do |project, evaluator|
+ project.remote_mirrors.create!(url: evaluator.url, enabled: evaluator.enabled)
+ end
+ end
+
trait :stubbed_repository do
after(:build) do |project|
allow(project).to receive(:empty_repo?).and_return(false)
@@ -193,6 +180,13 @@ FactoryBot.define do
trait :wiki_repo do
after(:create) do |project|
raise 'Failed to create wiki repository!' unless project.create_wiki
+
+ # We delete hooks so that gitlab-shell will not try to authenticate with
+ # an API that isn't running
+ project.gitlab_shell.rm_directory(
+ project.repository_storage,
+ File.join("#{project.wiki.repository.disk_path}.git", "hooks")
+ )
end
end
diff --git a/spec/factories/remote_mirrors.rb b/spec/factories/remote_mirrors.rb
new file mode 100644
index 00000000000..adc7da27522
--- /dev/null
+++ b/spec/factories/remote_mirrors.rb
@@ -0,0 +1,6 @@
+FactoryBot.define do
+ factory :remote_mirror, class: 'RemoteMirror' do
+ association :project, :repository
+ url "http://foo:bar@test.com"
+ end
+end
diff --git a/spec/fast_spec_helper.rb b/spec/fast_spec_helper.rb
index 978113a08a4..134eb25e4b1 100644
--- a/spec/fast_spec_helper.rb
+++ b/spec/fast_spec_helper.rb
@@ -3,14 +3,8 @@ require 'bundler/setup'
ENV['GITLAB_ENV'] = 'test'
ENV['IN_MEMORY_APPLICATION_SETTINGS'] = 'true'
-unless Object.respond_to?(:require_dependency)
- class Object
- alias_method :require_dependency, :require
- end
-end
-
-# Defines Settings and Gitlab.config which are at the center of the app
require_relative '../config/settings'
-require_relative '../lib/gitlab' unless defined?(Gitlab.config)
-
require_relative 'support/rspec'
+require 'active_support/all'
+
+ActiveSupport::Dependencies.autoload_paths << 'lib'
diff --git a/spec/features/groups/members/filter_members_spec.rb b/spec/features/groups/members/filter_members_spec.rb
new file mode 100644
index 00000000000..5ddb5894624
--- /dev/null
+++ b/spec/features/groups/members/filter_members_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+feature 'Groups > Members > Filter members' do
+ let(:user) { create(:user) }
+ let(:user_with_2fa) { create(:user, :two_factor_via_otp) }
+ let(:group) { create(:group) }
+
+ background do
+ group.add_owner(user)
+ group.add_master(user_with_2fa)
+
+ sign_in(user)
+ end
+
+ scenario 'shows all members' do
+ visit_members_list
+
+ expect(first_member).to include(user.name)
+ expect(second_member).to include(user_with_2fa.name)
+ expect(page).to have_css('.member-filter-2fa-dropdown .dropdown-toggle-text', text: '2FA: Everyone')
+ end
+
+ scenario 'shows only 2FA members' do
+ visit_members_list(two_factor: 'enabled')
+
+ expect(first_member).to include(user_with_2fa.name)
+ expect(members_list.size).to eq(1)
+ expect(page).to have_css('.member-filter-2fa-dropdown .dropdown-toggle-text', text: '2FA: Enabled')
+ end
+
+ scenario 'shows only non 2FA members' do
+ visit_members_list(two_factor: 'disabled')
+
+ expect(first_member).to include(user.name)
+ expect(members_list.size).to eq(1)
+ expect(page).to have_css('.member-filter-2fa-dropdown .dropdown-toggle-text', text: '2FA: Disabled')
+ end
+
+ def visit_members_list(options = {})
+ visit group_group_members_path(group.to_param, options)
+ end
+
+ def members_list
+ page.all('ul.content-list > li')
+ end
+
+ def first_member
+ members_list.first.text
+ end
+
+ def second_member
+ members_list.last.text
+ end
+end
diff --git a/spec/features/issuables/markdown_references/internal_references_spec.rb b/spec/features/issuables/markdown_references/internal_references_spec.rb
new file mode 100644
index 00000000000..9613e22bf24
--- /dev/null
+++ b/spec/features/issuables/markdown_references/internal_references_spec.rb
@@ -0,0 +1,140 @@
+require "rails_helper"
+
+describe "Internal references", :js do
+ include Spec::Support::Helpers::Features::NotesHelpers
+
+ let(:private_project_user) { private_project.owner }
+ let(:private_project) { create(:project, :private, :repository) }
+ let(:private_project_issue) { create(:issue, project: private_project) }
+ let(:private_project_merge_request) { create(:merge_request, source_project: private_project) }
+ let(:public_project_user) { public_project.owner }
+ let(:public_project) { create(:project, :public, :repository) }
+ let(:public_project_issue) { create(:issue, project: public_project) }
+ let(:public_project_merge_request) { create(:merge_request, source_project: public_project) }
+
+ context "when referencing to open issue" do
+ context "from private project" do
+ context "from issue" do
+ before do
+ sign_in(private_project_user)
+
+ visit(project_issue_path(private_project, private_project_issue))
+
+ add_note("##{public_project_issue.to_reference(private_project)}")
+ end
+
+ context "when user doesn't have access to private project" do
+ before do
+ sign_in(public_project_user)
+
+ visit(project_issue_path(public_project, public_project_issue))
+ end
+
+ it { expect(page).not_to have_css(".note") }
+ end
+ end
+
+ context "from merge request" do
+ before do
+ sign_in(private_project_user)
+
+ visit(project_merge_request_path(private_project, private_project_merge_request))
+
+ add_note("##{public_project_issue.to_reference(private_project)}")
+ end
+
+ context "when user doesn't have access to private project" do
+ before do
+ sign_in(public_project_user)
+
+ visit(project_issue_path(public_project, public_project_issue))
+ end
+
+ it "doesn't show any references" do
+ page.within(".issue-details") do
+ expect(page).not_to have_content("#merge-requests .merge-requests-title")
+ end
+ end
+ end
+
+ context "when user has access to private project" do
+ before do
+ visit(project_issue_path(public_project, public_project_issue))
+ end
+
+ it "shows references" do
+ page.within("#merge-requests .merge-requests-title") do
+ expect(page).to have_content("1 Related Merge Request")
+ end
+
+ page.within("#merge-requests ul") do
+ expect(page).to have_content(private_project_merge_request.title)
+ end
+
+ expect(page).to have_content("mentioned in merge request #{private_project_merge_request.to_reference(public_project)}")
+ .and have_content(private_project_user.name)
+ end
+ end
+ end
+ end
+ end
+
+ context "when referencing to open merge request" do
+ context "from private project" do
+ context "from issue" do
+ before do
+ sign_in(private_project_user)
+
+ visit(project_issue_path(private_project, private_project_issue))
+
+ add_note("##{public_project_merge_request.to_reference(private_project)}")
+ end
+
+ context "when user doesn't have access to private project" do
+ before do
+ sign_in(public_project_user)
+
+ visit(project_merge_request_path(public_project, public_project_merge_request))
+ end
+
+ it { expect(page).not_to have_css(".note") }
+ end
+ end
+
+ context "from merge request" do
+ before do
+ sign_in(private_project_user)
+
+ visit(project_merge_request_path(private_project, private_project_merge_request))
+
+ add_note("##{public_project_merge_request.to_reference(private_project)}")
+ end
+
+ context "when user doesn't have access to private project" do
+ before do
+ sign_in(public_project_user)
+
+ visit(project_merge_request_path(public_project, public_project_merge_request))
+ end
+
+ it "doesn't show any references" do
+ page.within(".merge-request-details") do
+ expect(page).not_to have_content("#merge-requests .merge-requests-title")
+ end
+ end
+ end
+
+ context "when user has access to private project" do
+ before do
+ visit(project_merge_request_path(public_project, public_project_merge_request))
+ end
+
+ it "shows references" do
+ expect(page).to have_content("mentioned in merge request #{private_project_merge_request.to_reference(public_project)}")
+ .and have_content(private_project_user.name)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/issuables/markdown_references/jira_spec.rb b/spec/features/issuables/markdown_references/jira_spec.rb
new file mode 100644
index 00000000000..fa0ab88624e
--- /dev/null
+++ b/spec/features/issuables/markdown_references/jira_spec.rb
@@ -0,0 +1,187 @@
+require "rails_helper"
+
+describe "Jira", :js do
+ let(:user) { create(:user) }
+ let(:actual_project) { create(:project, :public, :repository) }
+ let(:merge_request) { create(:merge_request, target_project: actual_project, source_project: actual_project) }
+ let(:issue_actual_project) { create(:issue, project: actual_project) }
+ let!(:other_project) { create(:project, :public) }
+ let!(:issue_other_project) { create(:issue, project: other_project) }
+ let(:issues) { [issue_actual_project, issue_other_project] }
+
+ shared_examples "correct references" do
+ before do
+ remotelink = double(:remotelink, all: [], build: double(save!: true))
+
+ stub_request(:get, "https://jira.example.com/rest/api/2/issue/JIRA-5")
+ stub_request(:post, "https://jira.example.com/rest/api/2/issue/JIRA-5/comment")
+ allow_any_instance_of(JIRA::Resource::Issue).to receive(:remotelink).and_return(remotelink)
+
+ sign_in(user)
+
+ visit(merge_request_path(merge_request))
+
+ build_note
+ end
+
+ it "creates a link to the referenced issue on the preview" do
+ find(".js-md-preview-button").click
+
+ wait_for_requests
+
+ page.within(".md-preview-holder") do
+ links_expectations
+ end
+ end
+
+ it "creates a link to the referenced issue after submit" do
+ click_button("Comment")
+
+ wait_for_requests
+
+ page.within("#diff-notes-app") do
+ links_expectations
+ end
+ end
+
+ it "creates a note on the referenced issues" do
+ click_button("Comment")
+
+ wait_for_requests
+
+ if referenced_issues.include?(issue_actual_project)
+ visit(issue_path(issue_actual_project))
+
+ page.within("#notes") do
+ expect(page).to have_content("#{user.to_reference} mentioned in merge request #{merge_request.to_reference}")
+ end
+ end
+
+ if referenced_issues.include?(issue_other_project)
+ visit(issue_path(issue_other_project))
+
+ page.within("#notes") do
+ expect(page).to have_content("#{user.to_reference} mentioned in merge request #{merge_request.to_reference(other_project)}")
+ end
+ end
+ end
+ end
+
+ context "when internal issues tracker is enabled for the other project" do
+ context "when only internal issues tracker is enabled for the actual project" do
+ include_examples "correct references" do
+ let(:referenced_issues) { [issue_actual_project, issue_other_project] }
+ let(:jira_referenced) { false }
+ end
+ end
+
+ context "when both external and internal issues trackers are enabled for the actual project" do
+ before do
+ create(:jira_service, project: actual_project)
+ end
+
+ include_examples "correct references" do
+ let(:referenced_issues) { [issue_actual_project, issue_other_project] }
+ let(:jira_referenced) { true }
+ end
+ end
+
+ context "when only external issues tracker is enabled for the actual project" do
+ let(:actual_project) { create(:project, :public, :repository, :issues_disabled) }
+
+ before do
+ create(:jira_service, project: actual_project)
+ end
+
+ include_examples "correct references" do
+ let(:referenced_issues) { [issue_other_project] }
+ let(:jira_referenced) { true }
+ end
+ end
+
+ context "when no tracker is enabled for the actual project" do
+ let(:actual_project) { create(:project, :public, :repository, :issues_disabled) }
+
+ include_examples 'correct references' do
+ let(:referenced_issues) { [issue_other_project] }
+ let(:jira_referenced) { false }
+ end
+ end
+ end
+
+ context "when internal issues tracker is disabled for the other project" do
+ let(:other_project) { create(:project, :public, :repository, :issues_disabled) }
+
+ context "when only internal issues tracker is enabled for the actual project" do
+ include_examples "correct references" do
+ let(:referenced_issues) { [issue_actual_project] }
+ let(:jira_referenced) { false }
+ end
+ end
+
+ context "when both external and internal issues trackers are enabled for the actual project" do
+ before do
+ create(:jira_service, project: actual_project)
+ end
+
+ include_examples "correct references" do
+ let(:referenced_issues) { [issue_actual_project] }
+ let(:jira_referenced) { true }
+ end
+ end
+
+ context "when only external issues tracker is enabled for the actual project" do
+ let(:actual_project) { create(:project, :public, :repository, :issues_disabled) }
+
+ before do
+ create(:jira_service, project: actual_project)
+ end
+
+ include_examples "correct references" do
+ let(:referenced_issues) { [] }
+ let(:jira_referenced) { true }
+ end
+ end
+
+ context "when no issues tracker is enabled for the actual project" do
+ let(:actual_project) { create(:project, :public, :repository, :issues_disabled) }
+
+ include_examples "correct references" do
+ let(:referenced_issues) { [] }
+ let(:jira_referenced) { false }
+ end
+ end
+ end
+
+ private
+
+ def build_note
+ markdown = <<~HEREDOC
+ Referencing internal issue #{issue_actual_project.to_reference},
+ cross-project #{issue_other_project.to_reference(actual_project)} external JIRA-5
+ and non existing #999
+ HEREDOC
+
+ page.within("#diff-notes-app") do
+ fill_in("note_note", with: markdown)
+ end
+ end
+
+ def links_expectations
+ issues.each do |issue|
+ if referenced_issues.include?(issue)
+ expect(page).to have_link(issue.to_reference, href: issue_path(issue))
+ else
+ expect(page).not_to have_link(issue.to_reference, href: issue_path(issue))
+ end
+ end
+
+ if jira_referenced
+ expect(page).to have_link("JIRA-5", href: "https://jira.example.com/browse/JIRA-5")
+ else
+ expect(page).not_to have_link("JIRA-5", href: "https://jira.example.com/browse/JIRA-5")
+ end
+
+ expect(page).not_to have_link("#999")
+ end
+end
diff --git a/spec/features/issuables/markdown_references_spec.rb b/spec/features/issuables/markdown_references_spec.rb
deleted file mode 100644
index dd4e10a9886..00000000000
--- a/spec/features/issuables/markdown_references_spec.rb
+++ /dev/null
@@ -1,193 +0,0 @@
-require 'rails_helper'
-
-describe 'Markdown References', :js do
- let(:user) { create(:user) }
- let(:actual_project) { create(:project, :public, :repository) }
- let(:merge_request) { create(:merge_request, target_project: actual_project, source_project: actual_project)}
- let(:issue_actual_project) { create(:issue, project: actual_project) }
- let!(:other_project) { create(:project, :public) }
- let!(:issue_other_project) { create(:issue, project: other_project) }
- let(:issues) { [issue_actual_project, issue_other_project] }
-
- def build_note
- markdown = "Referencing internal issue #{issue_actual_project.to_reference}, " +
- "cross-project #{issue_other_project.to_reference(actual_project)} external JIRA-5 " +
- "and non existing #999"
-
- page.within('#diff-notes-app') do
- fill_in 'note_note', with: markdown
- end
- end
-
- shared_examples 'correct references' do
- before do
- remotelink = double(:remotelink, all: [], build: double(save!: true))
-
- stub_request(:get, "https://jira.example.com/rest/api/2/issue/JIRA-5")
- stub_request(:post, "https://jira.example.com/rest/api/2/issue/JIRA-5/comment")
- allow_any_instance_of(JIRA::Resource::Issue).to receive(:remotelink).and_return(remotelink)
-
- sign_in(user)
- visit merge_request_path(merge_request)
- build_note
- end
-
- def links_expectations
- issues.each do |issue|
- if referenced_issues.include?(issue)
- expect(page).to have_link(issue.to_reference, href: issue_path(issue))
- else
- expect(page).not_to have_link(issue.to_reference, href: issue_path(issue))
- end
- end
-
- if jira_referenced
- expect(page).to have_link('JIRA-5', href: 'https://jira.example.com/browse/JIRA-5')
- else
- expect(page).not_to have_link('JIRA-5', href: 'https://jira.example.com/browse/JIRA-5')
- end
-
- expect(page).not_to have_link('#999')
- end
-
- it 'creates a link to the referenced issue on the preview' do
- find('.js-md-preview-button').click
- wait_for_requests
-
- page.within('.md-preview-holder') do
- links_expectations
- end
- end
-
- it 'creates a link to the referenced issue after submit' do
- click_button 'Comment'
- wait_for_requests
-
- page.within('#diff-notes-app') do
- links_expectations
- end
- end
-
- it 'creates a note on the referenced issues' do
- click_button 'Comment'
- wait_for_requests
-
- if referenced_issues.include?(issue_actual_project)
- visit issue_path(issue_actual_project)
-
- page.within('#notes') do
- expect(page).to have_content(
- "#{user.to_reference} mentioned in merge request #{merge_request.to_reference}"
- )
- end
- end
-
- if referenced_issues.include?(issue_other_project)
- visit issue_path(issue_other_project)
-
- page.within('#notes') do
- expect(page).to have_content(
- "#{user.to_reference} mentioned in merge request #{merge_request.to_reference(other_project)}"
- )
- end
- end
- end
- end
-
- context 'when internal issues tracker is enabled for the other project' do
- context 'when only internal issues tracker is enabled for the actual project' do
- include_examples 'correct references' do
- let(:referenced_issues) { [issue_actual_project, issue_other_project] }
- let(:jira_referenced) { false }
- end
- end
-
- context 'when both external and internal issues trackers are enabled for the actual project' do
- before do
- create(:jira_service, project: actual_project)
- end
-
- include_examples 'correct references' do
- let(:referenced_issues) { [issue_actual_project, issue_other_project] }
- let(:jira_referenced) { true }
- end
- end
-
- context 'when only external issues tracker is enabled for the actual project' do
- before do
- create(:jira_service, project: actual_project)
-
- actual_project.issues_enabled = false
- actual_project.save!
- end
-
- include_examples 'correct references' do
- let(:referenced_issues) { [issue_other_project] }
- let(:jira_referenced) { true }
- end
- end
-
- context 'when no tracker is enabled for the actual project' do
- before do
- actual_project.issues_enabled = false
- actual_project.save!
- end
-
- include_examples 'correct references' do
- let(:referenced_issues) { [issue_other_project] }
- let(:jira_referenced) { false }
- end
- end
- end
-
- context 'when internal issues tracker is disabled for the other project' do
- before do
- other_project.issues_enabled = false
- other_project.save!
- end
-
- context 'when only internal issues tracker is enabled for the actual project' do
- include_examples 'correct references' do
- let(:referenced_issues) { [issue_actual_project] }
- let(:jira_referenced) { false }
- end
- end
-
- context 'when both external and internal issues trackers are enabled for the actual project' do
- before do
- create(:jira_service, project: actual_project)
- end
-
- include_examples 'correct references' do
- let(:referenced_issues) { [issue_actual_project] }
- let(:jira_referenced) { true }
- end
- end
-
- context 'when only external issues tracker is enabled for the actual project' do
- before do
- create(:jira_service, project: actual_project)
-
- actual_project.issues_enabled = false
- actual_project.save!
- end
-
- include_examples 'correct references' do
- let(:referenced_issues) { [] }
- let(:jira_referenced) { true }
- end
- end
-
- context 'when no issues tracker is enabled for the actual project' do
- before do
- actual_project.issues_enabled = false
- actual_project.save!
- end
-
- include_examples 'correct references' do
- let(:referenced_issues) { [] }
- let(:jira_referenced) { false }
- end
- end
- end
-end
diff --git a/spec/features/projects/artifacts/browse_spec.rb b/spec/features/projects/artifacts/browse_spec.rb
deleted file mode 100644
index cb69aff8d5f..00000000000
--- a/spec/features/projects/artifacts/browse_spec.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-require 'spec_helper'
-
-feature 'Browse artifact', :js do
- let(:project) { create(:project, :public) }
- let(:pipeline) { create(:ci_empty_pipeline, project: project) }
- let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) }
- let(:browse_url) do
- browse_path('other_artifacts_0.1.2')
- end
-
- def browse_path(path)
- browse_project_job_artifacts_path(project, job, path)
- end
-
- context 'when visiting old URL' do
- before do
- visit browse_url.sub('/-/jobs', '/builds')
- end
-
- it "redirects to new URL" do
- expect(page.current_path).to eq(browse_url)
- end
- end
-
- context 'when browsing a directory with an text file' do
- let(:txt_entry) { job.artifacts_metadata_entry('other_artifacts_0.1.2/doc_sample.txt') }
-
- before do
- allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
- allow(Gitlab.config.pages).to receive(:artifacts_server).and_return(true)
- end
-
- context 'when the project is public' do
- it "shows external link icon and styles" do
- visit browse_url
-
- link = first('.tree-item-file-external-link')
-
- expect(page).to have_link('doc_sample.txt', href: file_project_job_artifacts_path(project, job, path: txt_entry.blob.path))
- expect(link[:target]).to eq('_blank')
- expect(link[:rel]).to include('noopener')
- expect(link[:rel]).to include('noreferrer')
- expect(page).to have_selector('.js-artifact-tree-external-icon')
- end
- end
-
- context 'when the project is private' do
- let!(:private_project) { create(:project, :private) }
- let(:pipeline) { create(:ci_empty_pipeline, project: private_project) }
- let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) }
- let(:user) { create(:user) }
-
- before do
- private_project.add_developer(user)
-
- sign_in(user)
- end
-
- it 'shows internal link styles' do
- visit browse_project_job_artifacts_path(private_project, job, 'other_artifacts_0.1.2')
-
- expect(page).to have_link('doc_sample.txt')
- expect(page).not_to have_selector('.js-artifact-tree-external-icon')
- end
- end
- end
-end
diff --git a/spec/features/projects/artifacts/download_spec.rb b/spec/features/projects/artifacts/download_spec.rb
deleted file mode 100644
index 6f76c14910b..00000000000
--- a/spec/features/projects/artifacts/download_spec.rb
+++ /dev/null
@@ -1,61 +0,0 @@
-require 'spec_helper'
-
-feature 'Download artifact' do
- let(:project) { create(:project, :public) }
- let(:pipeline) { create(:ci_empty_pipeline, status: :success, project: project) }
- let(:job) { create(:ci_build, :artifacts, :success, pipeline: pipeline) }
-
- shared_examples 'downloading' do
- it 'downloads the zip' do
- expect(page.response_headers['Content-Disposition'])
- .to eq(%Q{attachment; filename="#{job.artifacts_file.filename}"})
-
- # Check the content does match, but don't print this as error message
- expect(page.source.b == job.artifacts_file.file.read.b)
- end
- end
-
- context 'when downloading' do
- before do
- visit download_url
- end
-
- context 'via job id' do
- let(:download_url) do
- download_project_job_artifacts_path(project, job)
- end
-
- it_behaves_like 'downloading'
- end
-
- context 'via branch name and job name' do
- let(:download_url) do
- latest_succeeded_project_artifacts_path(project, "#{pipeline.ref}/download", job: job.name)
- end
-
- it_behaves_like 'downloading'
- end
- end
-
- context 'when visiting old URL' do
- before do
- visit download_url.sub('/-/jobs', '/builds')
- end
-
- context 'via job id' do
- let(:download_url) do
- download_project_job_artifacts_path(project, job)
- end
-
- it_behaves_like 'downloading'
- end
-
- context 'via branch name and job name' do
- let(:download_url) do
- latest_succeeded_project_artifacts_path(project, "#{pipeline.ref}/download", job: job.name)
- end
-
- it_behaves_like 'downloading'
- end
- end
-end
diff --git a/spec/features/projects/artifacts/user_browses_artifacts_spec.rb b/spec/features/projects/artifacts/user_browses_artifacts_spec.rb
new file mode 100644
index 00000000000..9ebbbaea911
--- /dev/null
+++ b/spec/features/projects/artifacts/user_browses_artifacts_spec.rb
@@ -0,0 +1,110 @@
+require "spec_helper"
+
+describe "User browses artifacts" do
+ let(:project) { create(:project, :public) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+ let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) }
+ let(:browse_url) { browse_project_job_artifacts_path(project, job, "other_artifacts_0.1.2") }
+
+ context "when visiting old URL" do
+ it "redirects to new URL" do
+ visit(browse_url.sub("/-/jobs", "/builds"))
+
+ expect(page.current_path).to eq(browse_url)
+ end
+ end
+
+ context "when browsing artifacts root directory" do
+ before do
+ visit(browse_project_job_artifacts_path(project, job))
+ end
+
+ it "shows artifacts" do
+ expect(page).not_to have_selector(".build-sidebar")
+
+ page.within(".tree-table") do
+ expect(page).to have_no_content("..")
+ .and have_content("other_artifacts_0.1.2")
+ .and have_content("ci_artifacts.txt")
+ .and have_content("rails_sample.jpg")
+ end
+
+ page.within(".build-header") do
+ expect(page).to have_content("Job ##{job.id} in pipeline ##{pipeline.id} for #{pipeline.short_sha}")
+ end
+ end
+
+ it "shows an artifact" do
+ click_link("ci_artifacts.txt")
+
+ expect(page).to have_link("download it")
+ end
+ end
+
+ context "when browsing a directory with UTF-8 characters in its name" do
+ before do
+ visit(browse_project_job_artifacts_path(project, job))
+ end
+
+ it "shows correct content", :js do
+ page.within(".tree-table") do
+ click_link("tests_encoding")
+
+ expect(page).to have_no_content("non-utf8-dir")
+
+ click_link("utf8 test dir ✓")
+
+ expect(page).to have_content("..").and have_content("regular_file_2")
+ end
+ end
+ end
+
+ context "when browsing a directory with a text file" do
+ let(:txt_entry) { job.artifacts_metadata_entry("other_artifacts_0.1.2/doc_sample.txt") }
+
+ before do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+ allow(Gitlab.config.pages).to receive(:artifacts_server).and_return(true)
+ end
+
+ context "when the project is public" do
+ before do
+ visit(browse_url)
+ end
+
+ it "shows correct content" do
+ link = first(".tree-item-file-external-link")
+
+ expect(link[:target]).to eq("_blank")
+ expect(link[:rel]).to include("noopener").and include("noreferrer")
+ expect(page).to have_link("doc_sample.txt", href: file_project_job_artifacts_path(project, job, path: txt_entry.blob.path))
+ .and have_selector(".js-artifact-tree-external-icon")
+
+ page.within(".tree-table") do
+ expect(page).to have_content("..").and have_content("another-subdirectory")
+ end
+
+ page.within(".repo-breadcrumb") do
+ expect(page).to have_content("other_artifacts_0.1.2")
+ end
+ end
+ end
+
+ context "when the project is private" do
+ let!(:private_project) { create(:project, :private) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: private_project) }
+ let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) }
+ let(:user) { create(:user) }
+
+ before do
+ private_project.add_developer(user)
+
+ sign_in(user)
+
+ visit(browse_project_job_artifacts_path(private_project, job, "other_artifacts_0.1.2"))
+ end
+
+ it { expect(page).to have_link("doc_sample.txt").and have_no_selector(".js-artifact-tree-external-icon") }
+ end
+ end
+end
diff --git a/spec/features/projects/artifacts/user_downloads_artifacts_spec.rb b/spec/features/projects/artifacts/user_downloads_artifacts_spec.rb
new file mode 100644
index 00000000000..67ed2f18d76
--- /dev/null
+++ b/spec/features/projects/artifacts/user_downloads_artifacts_spec.rb
@@ -0,0 +1,44 @@
+require "spec_helper"
+
+describe "User downloads artifacts" do
+ set(:project) { create(:project, :public) }
+ set(:pipeline) { create(:ci_empty_pipeline, status: :success, project: project) }
+ set(:job) { create(:ci_build, :artifacts, :success, pipeline: pipeline) }
+
+ shared_examples "downloading" do
+ it "downloads the zip" do
+ expect(page.response_headers["Content-Disposition"]).to eq(%Q{attachment; filename="#{job.artifacts_file.filename}"})
+ expect(page.response_headers['Content-Transfer-Encoding']).to eq("binary")
+ expect(page.response_headers['Content-Type']).to eq("application/zip")
+ expect(page.source.b).to eq(job.artifacts_file.file.read.b)
+ end
+ end
+
+ context "when downloading" do
+ before do
+ visit(url)
+ end
+
+ context "via job id" do
+ set(:url) { download_project_job_artifacts_path(project, job) }
+
+ it_behaves_like "downloading"
+ end
+
+ context "via branch name and job name" do
+ set(:url) { latest_succeeded_project_artifacts_path(project, "#{pipeline.ref}/download", job: job.name) }
+
+ it_behaves_like "downloading"
+ end
+
+ context "via clicking the `Download` button" do
+ set(:url) { project_job_path(project, job) }
+
+ before do
+ click_link("Download")
+ end
+
+ it_behaves_like "downloading"
+ end
+ end
+end
diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb
index dfe8e02dce0..a8a627d8806 100644
--- a/spec/features/projects/clusters/gcp_spec.rb
+++ b/spec/features/projects/clusters/gcp_spec.rb
@@ -182,6 +182,47 @@ feature 'Gcp Cluster', :js do
it 'user sees a login page' do
expect(page).to have_css('.signin-with-google')
+ expect(page).to have_link('Google account')
+ end
+ end
+
+ context 'when user has not dismissed GCP signup offer' do
+ before do
+ visit project_clusters_path(project)
+ end
+
+ it 'user sees offer on cluster index page' do
+ expect(page).to have_css('.gcp-signup-offer')
+ end
+
+ it 'user sees offer on cluster create page' do
+ click_link 'Add Kubernetes cluster'
+
+ expect(page).to have_css('.gcp-signup-offer')
+ end
+
+ it 'user sees offer on cluster GCP login page' do
+ click_link 'Add Kubernetes cluster'
+ click_link 'Create on Google Kubernetes Engine'
+
+ expect(page).to have_css('.gcp-signup-offer')
+ end
+ end
+
+ context 'when user has dismissed GCP signup offer' do
+ before do
+ visit project_clusters_path(project)
+ end
+
+ it 'user does not see offer after dismissing' do
+ expect(page).to have_css('.gcp-signup-offer')
+
+ find('.gcp-signup-offer .close').click
+ wait_for_requests
+
+ click_link 'Add Kubernetes cluster'
+
+ expect(page).not_to have_css('.gcp-signup-offer')
end
end
end
diff --git a/spec/features/projects/commit/comments/user_adds_comment_spec.rb b/spec/features/projects/commit/comments/user_adds_comment_spec.rb
new file mode 100644
index 00000000000..6397df086a7
--- /dev/null
+++ b/spec/features/projects/commit/comments/user_adds_comment_spec.rb
@@ -0,0 +1,170 @@
+require "spec_helper"
+
+describe "User adds a comment on a commit", :js do
+ include Spec::Support::Helpers::Features::NotesHelpers
+ include RepoHelpers
+
+ let(:comment_text) { "XML attached" }
+ let(:another_comment_text) { "SVG attached" }
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ project.add_developer(user)
+ end
+
+ context "inline view" do
+ before do
+ visit(project_commit_path(project, sample_commit.id))
+ end
+
+ it "adds a comment" do
+ page.within(".js-main-target-form") do
+ expect(page).not_to have_link("Cancel")
+
+ emoji = ":+1:"
+
+ fill_in("note[note]", with: "#{comment_text} #{emoji}")
+
+ # Check on `Preview` tab
+ click_link("Preview")
+
+ expect(find(".js-md-preview")).to have_content(comment_text).and have_css("gl-emoji")
+ expect(page).not_to have_css(".js-note-text")
+
+ # Check on the `Write` tab
+ click_link("Write")
+
+ expect(page).to have_field("note[note]", with: "#{comment_text} #{emoji}")
+
+ # Submit comment from the `Preview` tab to get rid of a separate `it` block
+ # which would specially tests if everything gets cleared from the note form.
+ click_link("Preview")
+ click_button("Comment")
+ end
+
+ wait_for_requests
+
+ page.within(".note") do
+ expect(page).to have_content(comment_text).and have_css("gl-emoji")
+ end
+
+ page.within(".js-main-target-form") do
+ expect(page).to have_field("note[note]", with: "").and have_no_css(".js-md-preview")
+ end
+ end
+
+ context "when commenting on diff" do
+ it "adds a comment" do
+ page.within(".diff-file:nth-of-type(1)") do
+ # Open a form for a comment and check UI elements are visible and acting as expecting.
+ click_diff_line(sample_commit.line_code)
+
+ expect(page).to have_css(".js-temp-notes-holder form.new-note")
+ .and have_css(".js-close-discussion-note-form", text: "Cancel")
+
+ # The `Cancel` button closes the current form. The page should not have any open forms after that.
+ find(".js-close-discussion-note-form").click
+
+ expect(page).not_to have_css("form.new_note")
+
+ # Try to open the same form twice. There should be only one form opened.
+ click_diff_line(sample_commit.line_code)
+ click_diff_line(sample_commit.line_code)
+
+ expect(page).to have_css("form.new-note", count: 1)
+
+ # Fill in a form.
+ page.within("form[data-line-code='#{sample_commit.line_code}']") do
+ fill_in("note[note]", with: "#{comment_text} :smile:")
+ end
+
+ # Open another form and check we have two forms now (because the first one is filled in).
+ click_diff_line(sample_commit.del_line_code)
+
+ expect(page).to have_field("note[note]", with: "#{comment_text} :smile:")
+ .and have_field("note[note]", with: "")
+
+ # Test Preview feature for both forms.
+ page.within("form[data-line-code='#{sample_commit.line_code}']") do
+ click_link("Preview")
+ end
+
+ page.within("form[data-line-code='#{sample_commit.del_line_code}']") do
+ fill_in("note[note]", with: another_comment_text)
+
+ click_link("Preview")
+ end
+
+ expect(page).to have_css(".js-md-preview", visible: true, count: 2)
+ .and have_content(comment_text)
+ .and have_content(another_comment_text)
+ .and have_xpath("//gl-emoji[@data-name='smile']")
+
+ # Test UI elements, then submit.
+ page.within("form[data-line-code='#{sample_commit.line_code}']") do
+ expect(find(".js-note-text", visible: false).text).to eq("")
+ expect(page).to have_css('.js-md-write-button')
+
+ click_button("Comment")
+ end
+
+ expect(page).to have_button("Reply...").and have_no_css("form.new_note")
+ end
+
+ # A comment should be added and visible.
+ page.within(".diff-file:nth-of-type(1) .note") do
+ expect(page).to have_content(comment_text).and have_xpath("//gl-emoji[@data-name='smile']")
+ end
+ end
+ end
+ end
+
+ context "side-by-side view" do
+ before do
+ visit(project_commit_path(project, sample_commit.id, view: "parallel"))
+ end
+
+ it "adds a comment" do
+ new_comment = "New comment"
+ old_comment = "Old comment"
+
+ # Left side.
+ click_parallel_diff_line(sample_commit.del_line_code)
+
+ page.within(".diff-file:nth-of-type(1) form[data-line-code='#{sample_commit.del_line_code}']") do
+ fill_in("note[note]", with: old_comment)
+ click_button("Comment")
+ end
+
+ page.within(".diff-file:nth-of-type(1) .notes_content.parallel.old") do
+ expect(page).to have_content(old_comment)
+ end
+
+ # Right side.
+ click_parallel_diff_line(sample_commit.line_code)
+
+ page.within(".diff-file:nth-of-type(1) form[data-line-code='#{sample_commit.line_code}']") do
+ fill_in("note[note]", with: new_comment)
+ click_button("Comment")
+ end
+
+ wait_for_requests
+
+ expect(all(".diff-file:nth-of-type(1) .notes_content.parallel.new")[1].text).to have_content(new_comment)
+ end
+ end
+
+ private
+
+ def click_diff_line(line)
+ find(".line_holder[id='#{line}'] td:nth-of-type(1)").hover
+ find(".line_holder[id='#{line}'] button").click
+ end
+
+ def click_parallel_diff_line(line)
+ find(".line_holder.parallel td[id='#{line}']").find(:xpath, 'preceding-sibling::*[1][self::td]').hover
+ find(".line_holder.parallel button[data-line-code='#{line}']").click
+ end
+end
diff --git a/spec/features/projects/commit/comments/user_deletes_comments_spec.rb b/spec/features/projects/commit/comments/user_deletes_comments_spec.rb
new file mode 100644
index 00000000000..a727cab4ac7
--- /dev/null
+++ b/spec/features/projects/commit/comments/user_deletes_comments_spec.rb
@@ -0,0 +1,37 @@
+require "spec_helper"
+
+describe "User deletes comments on a commit", :js do
+ include Spec::Support::Helpers::Features::NotesHelpers
+ include RepoHelpers
+
+ let(:comment_text) { "XML attached" }
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ project.add_developer(user)
+
+ visit(project_commit_path(project, sample_commit.id))
+
+ add_note(comment_text)
+ end
+
+ it "deletes comment" do
+ page.within(".note") do
+ expect(page).to have_content(comment_text)
+ end
+
+ page.within(".main-notes-list") do
+ note = find(".note")
+ note.hover
+
+ find(".more-actions").click
+ find(".more-actions .dropdown-menu li", match: :first)
+
+ accept_confirm { find(".js-note-delete").click }
+ end
+
+ expect(page).not_to have_css(".note")
+ end
+end
diff --git a/spec/features/projects/commit/comments/user_edits_comments_spec.rb b/spec/features/projects/commit/comments/user_edits_comments_spec.rb
new file mode 100644
index 00000000000..75bccd99f59
--- /dev/null
+++ b/spec/features/projects/commit/comments/user_edits_comments_spec.rb
@@ -0,0 +1,42 @@
+require "spec_helper"
+
+describe "User edits a comment on a commit", :js do
+ include Spec::Support::Helpers::Features::NotesHelpers
+ include RepoHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ project.add_developer(user)
+
+ visit(project_commit_path(project, sample_commit.id))
+
+ add_note("XML attached")
+ end
+
+ it "edits comment" do
+ NEW_COMMENT_TEXT = "+1 Awesome!".freeze
+
+ page.within(".main-notes-list") do
+ note = find(".note")
+ note.hover
+
+ note.find(".js-note-edit").click
+ end
+
+ page.find(".current-note-edit-form textarea")
+
+ page.within(".current-note-edit-form") do
+ fill_in("note[note]", with: NEW_COMMENT_TEXT)
+ click_button("Save comment")
+ end
+
+ wait_for_requests
+
+ page.within(".note") do
+ expect(page).to have_content(NEW_COMMENT_TEXT)
+ end
+ end
+end
diff --git a/spec/features/projects/commit/user_comments_on_commit_spec.rb b/spec/features/projects/commit/user_comments_on_commit_spec.rb
deleted file mode 100644
index 5174f793367..00000000000
--- a/spec/features/projects/commit/user_comments_on_commit_spec.rb
+++ /dev/null
@@ -1,110 +0,0 @@
-require "spec_helper"
-
-describe "User comments on commit", :js do
- include Spec::Support::Helpers::Features::NotesHelpers
- include RepoHelpers
-
- let(:project) { create(:project, :repository) }
- let(:user) { create(:user) }
-
- COMMENT_TEXT = "XML attached".freeze
-
- before do
- sign_in(user)
- project.add_developer(user)
-
- visit(project_commit_path(project, sample_commit.id))
- end
-
- context "when adding new comment" do
- it "adds comment" do
- EMOJI = ":+1:".freeze
-
- page.within(".js-main-target-form") do
- expect(page).not_to have_link("Cancel")
-
- fill_in("note[note]", with: "#{COMMENT_TEXT} #{EMOJI}")
-
- # Check on `Preview` tab
- click_link("Preview")
-
- expect(find(".js-md-preview")).to have_content(COMMENT_TEXT).and have_css("gl-emoji")
- expect(page).not_to have_css(".js-note-text")
-
- # Check on `Write` tab
- click_link("Write")
-
- expect(page).to have_field("note[note]", with: "#{COMMENT_TEXT} #{EMOJI}")
-
- # Submit comment from the `Preview` tab to get rid of a separate `it` block
- # which would specially tests if everything gets cleared from the note form.
- click_link("Preview")
- click_button("Comment")
- end
-
- wait_for_requests
-
- page.within(".note") do
- expect(page).to have_content(COMMENT_TEXT).and have_css("gl-emoji")
- end
-
- page.within(".js-main-target-form") do
- expect(page).to have_field("note[note]", with: "").and have_no_css(".js-md-preview")
- end
- end
- end
-
- context "when editing comment" do
- before do
- add_note(COMMENT_TEXT)
- end
-
- it "edits comment" do
- NEW_COMMENT_TEXT = "+1 Awesome!".freeze
-
- page.within(".main-notes-list") do
- note = find(".note")
- note.hover
-
- note.find(".js-note-edit").click
- end
-
- page.find(".current-note-edit-form textarea")
-
- page.within(".current-note-edit-form") do
- fill_in("note[note]", with: NEW_COMMENT_TEXT)
- click_button("Save comment")
- end
-
- wait_for_requests
-
- page.within(".note") do
- expect(page).to have_content(NEW_COMMENT_TEXT)
- end
- end
- end
-
- context "when deleting comment" do
- before do
- add_note(COMMENT_TEXT)
- end
-
- it "deletes comment" do
- page.within(".note") do
- expect(page).to have_content(COMMENT_TEXT)
- end
-
- page.within(".main-notes-list") do
- note = find(".note")
- note.hover
-
- find(".more-actions").click
- find(".more-actions .dropdown-menu li", match: :first)
-
- accept_confirm { find(".js-note-delete").click }
- end
-
- expect(page).not_to have_css(".note")
- end
- end
-end
diff --git a/spec/features/projects/deploy_keys_spec.rb b/spec/features/projects/deploy_keys_spec.rb
index 886c56e7163..43a23c42f83 100644
--- a/spec/features/projects/deploy_keys_spec.rb
+++ b/spec/features/projects/deploy_keys_spec.rb
@@ -18,12 +18,12 @@ describe 'Project deploy keys', :js do
visit project_settings_repository_path(project)
page.within(find('.deploy-keys')) do
- expect(page).to have_selector('.deploy-keys li', count: 1)
+ expect(page).to have_selector('.deploy-key', count: 1)
- accept_confirm { find(:button, text: 'Remove').send_keys(:return) }
+ accept_confirm { find('.ic-remove').click() }
expect(page).not_to have_selector('.fa-spinner', count: 0)
- expect(page).to have_selector('.deploy-keys li', count: 0)
+ expect(page).to have_selector('.deploy-key', count: 0)
end
end
end
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index a00db6dd161..9d1c4cbad8b 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
require 'tempfile'
-feature 'Jobs' do
+feature 'Jobs', :clean_gitlab_redis_shared_state do
let(:user) { create(:user) }
let(:user_access_level) { :developer }
let(:project) { create(:project, :repository) }
@@ -282,7 +282,7 @@ feature 'Jobs' do
it 'loads job trace' do
expect(page).to have_content 'BUILD TRACE'
- job.trace.write do |stream|
+ job.trace.write('a+b') do |stream|
stream.append(' and more trace', 11)
end
@@ -593,44 +593,6 @@ feature 'Jobs' do
end
end
- context 'storage form' do
- let(:existing_file) { Tempfile.new('existing-trace-file').path }
-
- before do
- job.run!
- end
-
- context 'when job has trace in file', :js do
- before do
- allow_any_instance_of(Gitlab::Ci::Trace)
- .to receive(:paths)
- .and_return([existing_file])
- end
-
- it 'sends the right headers' do
- requests = inspect_requests(inject_headers: { 'X-Sendfile-Type' => 'X-Sendfile' }) do
- visit raw_project_job_path(project, job)
- end
- expect(requests.first.response_headers['Content-Type']).to eq('text/plain; charset=utf-8')
- expect(requests.first.response_headers['X-Sendfile']).to eq(existing_file)
- end
- end
-
- context 'when job has trace in the database', :js do
- before do
- allow_any_instance_of(Gitlab::Ci::Trace)
- .to receive(:paths)
- .and_return([])
-
- visit project_job_path(project, job)
- end
-
- it 'sends the right headers' do
- expect(page).not_to have_selector('.js-raw-link-controller')
- end
- end
- end
-
context "when visiting old URL" do
let(:raw_job_url) do
raw_project_job_path(project, job)
diff --git a/spec/features/projects/merge_requests/user_accepts_merge_request_spec.rb b/spec/features/projects/merge_requests/user_accepts_merge_request_spec.rb
index c35ba2d7016..01aeed93947 100644
--- a/spec/features/projects/merge_requests/user_accepts_merge_request_spec.rb
+++ b/spec/features/projects/merge_requests/user_accepts_merge_request_spec.rb
@@ -10,6 +10,15 @@ describe 'User accepts a merge request', :js do
sign_in(user)
end
+ it 'presents merged merge request content' do
+ visit(merge_request_path(merge_request))
+
+ click_button('Merge')
+
+ expect(page).to have_content("The changes were merged into #{merge_request.target_branch} with \
+ #{merge_request.short_merge_commit_sha}")
+ end
+
context 'with removing the source branch' do
before do
visit(merge_request_path(merge_request))
diff --git a/spec/features/projects/merge_requests/user_creates_merge_request_spec.rb b/spec/features/projects/merge_requests/user_creates_merge_request_spec.rb
index f285c6c8783..1f21ef7b382 100644
--- a/spec/features/projects/merge_requests/user_creates_merge_request_spec.rb
+++ b/spec/features/projects/merge_requests/user_creates_merge_request_spec.rb
@@ -1,32 +1,84 @@
-require 'spec_helper'
+require "spec_helper"
-describe 'User creates a merge request', :js do
+describe "User creates a merge request", :js do
+ include ProjectForksHelper
+
+ let(:title) { "Some feature" }
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
before do
project.add_master(user)
sign_in(user)
+ end
+ it "creates a merge request" do
visit(project_new_merge_request_path(project))
- end
- it 'creates a merge request' do
- find('.js-source-branch').click
- click_link('fix')
+ find(".js-source-branch").click
+ click_link("fix")
- find('.js-target-branch').click
- click_link('feature')
+ find(".js-target-branch").click
+ click_link("feature")
- click_button('Compare branches')
+ click_button("Compare branches")
- fill_in('merge_request_title', with: 'Wiki Feature')
- click_button('Submit merge request')
+ fill_in("Title", with: title)
+ click_button("Submit merge request")
- page.within('.merge-request') do
- expect(page).to have_content('Wiki Feature')
+ page.within(".merge-request") do
+ expect(page).to have_content(title)
end
+ end
+
+ context "to a forked project" do
+ let(:forked_project) { fork_project(project, user, namespace: user.namespace, repository: true) }
+
+ it "creates a merge request" do
+ visit(project_new_merge_request_path(forked_project))
+
+ expect(page).to have_content("Source branch").and have_content("Target branch")
+ expect(find("#merge_request_target_project_id", visible: false).value).to eq(project.id.to_s)
+
+ click_button("Compare branches and continue")
+
+ expect(page).to have_content("You must select source and target branch")
+
+ first(".js-source-project").click
+ first(".dropdown-source-project a", text: forked_project.full_path)
+
+ first(".js-target-project").click
+ first(".dropdown-target-project a", text: project.full_path)
+
+ first(".js-source-branch").click
- wait_for_requests
+ wait_for_requests
+
+ source_branch = "fix"
+
+ first(".js-source-branch-dropdown .dropdown-content a", text: source_branch).click
+
+ click_button("Compare branches and continue")
+
+ expect(page).to have_css("h3.page-title", text: "New Merge Request")
+
+ page.within("form#new_merge_request") do
+ fill_in("Title", with: title)
+ end
+
+ click_button("Assignee")
+
+ expect(find(".js-assignee-search")["data-project-id"]).to eq(project.id.to_s)
+
+ page.within(".dropdown-menu-user") do
+ expect(page).to have_content("Unassigned")
+ .and have_content(user.name)
+ .and have_content(project.users.first.name)
+ end
+
+ click_button("Submit merge request")
+
+ expect(page).to have_content(title).and have_content("Request to merge #{user.namespace.name}:#{source_branch} into master")
+ end
end
end
diff --git a/spec/features/projects/merge_requests/user_merges_merge_request_spec.rb b/spec/features/projects/merge_requests/user_merges_merge_request_spec.rb
new file mode 100644
index 00000000000..6539e6e9208
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_merges_merge_request_spec.rb
@@ -0,0 +1,43 @@
+require "spec_helper"
+
+describe "User merges a merge request", :js do
+ let(:user) { project.owner }
+
+ before do
+ sign_in(user)
+ end
+
+ shared_examples "fast forward merge a merge request" do
+ it "merges a merge request" do
+ expect(page).to have_content("Fast-forward merge without a merge commit").and have_button("Merge")
+
+ page.within(".mr-state-widget") do
+ click_button("Merge")
+ end
+
+ page.within(".status-box") do
+ expect(page).to have_content("Merged")
+ end
+ end
+ end
+
+ context "ff-only merge" do
+ let(:project) { create(:project, :public, :repository, merge_requests_ff_only_enabled: true) }
+
+ before do
+ visit(merge_request_path(merge_request))
+ end
+
+ context "when branch is rebased" do
+ let!(:merge_request) { create(:merge_request, :rebased, source_project: project) }
+
+ it_behaves_like "fast forward merge a merge request"
+ end
+
+ context "when branch is merged" do
+ let!(:merge_request) { create(:merge_request, :merged_target, source_project: project) }
+
+ it_behaves_like "fast forward merge a merge request"
+ end
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_rebases_merge_request_spec.rb b/spec/features/projects/merge_requests/user_rebases_merge_request_spec.rb
new file mode 100644
index 00000000000..92e1c9942b1
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_rebases_merge_request_spec.rb
@@ -0,0 +1,34 @@
+require "spec_helper"
+
+describe "User rebases a merge request", :js do
+ let(:merge_request) { create(:merge_request, :simple, source_project: project) }
+ let(:user) { project.owner }
+
+ before do
+ sign_in(user)
+ end
+
+ shared_examples "rebases" do
+ it "rebases" do
+ visit(merge_request_path(merge_request))
+
+ expect(page).to have_button("Rebase")
+
+ click_button("Rebase")
+
+ expect(page).to have_content("Rebase in progress")
+ end
+ end
+
+ context "when merge is regular" do
+ let(:project) { create(:project, :public, :repository, merge_requests_rebase_enabled: true) }
+
+ it_behaves_like "rebases"
+ end
+
+ context "when merge is ff-only" do
+ let(:project) { create(:project, :public, :repository, merge_requests_ff_only_enabled: true) }
+
+ it_behaves_like "rebases"
+ end
+end
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index 990e5c4d9df..a29c21f6fef 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -3,10 +3,11 @@ require 'spec_helper'
describe 'Pipeline', :js do
let(:project) { create(:project) }
let(:user) { create(:user) }
+ let(:role) { :developer }
before do
sign_in(user)
- project.add_developer(user)
+ project.add_role(user, role)
end
shared_context 'pipeline builds' do
@@ -153,9 +154,10 @@ describe 'Pipeline', :js do
end
context 'page tabs' do
- it 'shows Pipeline and Jobs tabs with link' do
+ it 'shows Pipeline, Jobs and Failed Jobs tabs with link' do
expect(page).to have_link('Pipeline')
expect(page).to have_link('Jobs')
+ expect(page).to have_link('Failed Jobs')
end
it 'shows counter in Jobs tab' do
@@ -165,6 +167,16 @@ describe 'Pipeline', :js do
it 'shows Pipeline tab as active' do
expect(page).to have_css('.js-pipeline-tab-link.active')
end
+
+ context 'without permission to access builds' do
+ let(:project) { create(:project, :public, :repository, public_builds: false) }
+ let(:role) { :guest }
+
+ it 'does not show failed jobs tab pane' do
+ expect(page).to have_link('Pipeline')
+ expect(page).not_to have_content('Failed Jobs')
+ end
+ end
end
context 'retrying jobs' do
@@ -308,8 +320,7 @@ describe 'Pipeline', :js do
end
describe 'GET /:project/pipelines/:id/failures' do
- let(:project) { create(:project, :repository) }
- let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
+ let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: '1234') }
let(:pipeline_failures_page) { failures_project_pipeline_path(project, pipeline) }
let!(:failed_build) { create(:ci_build, :failed, pipeline: pipeline) }
@@ -340,11 +351,39 @@ describe 'Pipeline', :js do
visit pipeline_failures_page
end
- it 'includes failed jobs' do
+ it 'shows jobs tab pane as active' do
+ expect(page).to have_content('Failed Jobs')
+ expect(page).to have_css('#js-tab-failures.active')
+ end
+
+ it 'lists failed builds' do
+ expect(page).to have_content(failed_build.name)
+ expect(page).to have_content(failed_build.stage)
+ end
+
+ it 'does not show trace' do
expect(page).to have_content('No job trace')
end
end
+ context 'without permission to access builds' do
+ let(:role) { :guest }
+
+ before do
+ project.update(public_builds: false)
+ end
+
+ context 'when accessing failed jobs page' do
+ before do
+ visit pipeline_failures_page
+ end
+
+ it 'fails to access the page' do
+ expect(page).to have_content('Access Denied')
+ end
+ end
+ end
+
context 'without failures' do
before do
failed_build.update!(status: :success)
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index 705ba78a0b7..9c165b17704 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -125,7 +125,7 @@ describe 'Pipelines', :js do
context 'when canceling' do
before do
find('.js-pipelines-cancel-button').click
- find('.js-primary-button').click
+ find('.js-modal-primary-action').click
wait_for_requests
end
@@ -156,7 +156,6 @@ describe 'Pipelines', :js do
context 'when retrying' do
before do
find('.js-pipelines-retry-button').click
- find('.js-primary-button').click
wait_for_requests
end
@@ -256,7 +255,7 @@ describe 'Pipelines', :js do
context 'when canceling' do
before do
find('.js-pipelines-cancel-button').click
- find('.js-primary-button').click
+ find('.js-modal-primary-action').click
end
it 'indicates that pipeline was canceled' do
@@ -388,9 +387,9 @@ describe 'Pipelines', :js do
it 'should be possible to cancel pending build' do
find('.js-builds-dropdown-button').click
- find('a.js-ci-action-icon').click
+ find('.js-ci-action').click
+ wait_for_requests
- expect(page).to have_content('canceled')
expect(build.reload).to be_canceled
end
end
@@ -407,7 +406,7 @@ describe 'Pipelines', :js do
within('.js-builds-dropdown-list') do
build_element = page.find('.mini-pipeline-graph-dropdown-item')
- expect(build_element['data-title']).to eq('build - failed <br> (unknown failure)')
+ expect(build_element['data-original-title']).to eq('build - failed <br> (unknown failure)')
end
end
end
@@ -517,16 +516,31 @@ describe 'Pipelines', :js do
end
it 'creates a new pipeline' do
- expect { click_on 'Run pipeline' }
+ expect { click_on 'Create pipeline' }
.to change { Ci::Pipeline.count }.by(1)
expect(Ci::Pipeline.last).to be_web
end
+
+ context 'when variables are specified' do
+ it 'creates a new pipeline with variables' do
+ page.within '.ci-variable-row-body' do
+ fill_in "Input variable key", with: "key_name"
+ fill_in "Input variable value", with: "value"
+ end
+
+ expect { click_on 'Create pipeline' }
+ .to change { Ci::Pipeline.count }.by(1)
+
+ expect(Ci::Pipeline.last.variables.map { |var| var.slice(:key, :secret_value) })
+ .to eq [{ key: "key_name", secret_value: "value" }.with_indifferent_access]
+ end
+ end
end
context 'without gitlab-ci.yml' do
before do
- click_on 'Run pipeline'
+ click_on 'Create pipeline'
end
it { expect(page).to have_content('Missing .gitlab-ci.yml file') }
@@ -539,7 +553,7 @@ describe 'Pipelines', :js do
click_link 'master'
end
- expect { click_on 'Run pipeline' }
+ expect { click_on 'Create pipeline' }
.to change { Ci::Pipeline.count }.by(1)
end
end
@@ -557,7 +571,7 @@ describe 'Pipelines', :js do
it 'has field to add a new pipeline' do
expect(page).to have_selector('.js-branch-select')
expect(find('.js-branch-select')).to have_content project.default_branch
- expect(page).to have_content('Run on')
+ expect(page).to have_content('Create for')
end
end
diff --git a/spec/features/projects/remote_mirror_spec.rb b/spec/features/projects/remote_mirror_spec.rb
new file mode 100644
index 00000000000..81a6b613cc8
--- /dev/null
+++ b/spec/features/projects/remote_mirror_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+feature 'Project remote mirror', :feature do
+ let(:project) { create(:project, :repository, :remote_mirror) }
+ let(:remote_mirror) { project.remote_mirrors.first }
+ let(:user) { create(:user) }
+
+ describe 'On a project', :js do
+ before do
+ project.add_master(user)
+ sign_in user
+ end
+
+ context 'when last_error is present but last_update_at is not' do
+ it 'renders error message without timstamp' do
+ remote_mirror.update_attributes(last_error: 'Some new error', last_update_at: nil)
+
+ visit project_mirror_path(project)
+
+ expect(page).to have_content('The remote repository failed to update.')
+ end
+ end
+
+ context 'when last_error and last_update_at are present' do
+ it 'renders error message with timestamp' do
+ remote_mirror.update_attributes(last_error: 'Some new error', last_update_at: Time.now - 5.minutes)
+
+ visit project_mirror_path(project)
+
+ expect(page).to have_content('The remote repository failed to update 5 minutes ago.')
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb
index e1dfe617691..08b40653764 100644
--- a/spec/features/projects/settings/repository_settings_spec.rb
+++ b/spec/features/projects/settings/repository_settings_spec.rb
@@ -54,7 +54,7 @@ describe 'Projects > Settings > Repository settings' do
project.deploy_keys << private_deploy_key
visit project_settings_repository_path(project)
- find('li', text: private_deploy_key.title).click_link('Edit')
+ find('.deploy-key', text: private_deploy_key.title).find('.ic-pencil').click()
fill_in 'deploy_key_title', with: 'updated_deploy_key'
check 'deploy_key_deploy_keys_projects_attributes_0_can_push'
@@ -71,11 +71,15 @@ describe 'Projects > Settings > Repository settings' do
visit project_settings_repository_path(project)
- find('li', text: private_deploy_key.title).click_link('Edit')
+ find('.js-deployKeys-tab-available_project_keys').click()
+
+ find('.deploy-key', text: private_deploy_key.title).find('.ic-pencil').click()
fill_in 'deploy_key_title', with: 'updated_deploy_key'
click_button 'Save changes'
+ find('.js-deployKeys-tab-available_project_keys').click()
+
expect(page).to have_content('updated_deploy_key')
end
@@ -83,7 +87,7 @@ describe 'Projects > Settings > Repository settings' do
project.deploy_keys << private_deploy_key
visit project_settings_repository_path(project)
- accept_confirm { find('li', text: private_deploy_key.title).click_button('Remove') }
+ accept_confirm { find('.deploy-key', text: private_deploy_key.title).find('.ic-remove').click() }
expect(page).not_to have_content(private_deploy_key.title)
end
@@ -115,5 +119,20 @@ describe 'Projects > Settings > Repository settings' do
expect(page).to have_content('Your new project deploy token has been created')
end
end
+
+ context 'remote mirror settings' do
+ let(:user2) { create(:user) }
+
+ before do
+ project.add_master(user2)
+
+ visit project_settings_repository_path(project)
+ end
+
+ it 'shows push mirror settings' do
+ expect(page).to have_selector('#project_remote_mirrors_attributes_0_enabled')
+ expect(page).to have_selector('#project_remote_mirrors_attributes_0_url')
+ end
+ end
end
end
diff --git a/spec/features/projects/settings/user_interacts_with_deploy_keys_spec.rb b/spec/features/projects/settings/user_interacts_with_deploy_keys_spec.rb
new file mode 100644
index 00000000000..71a077039b7
--- /dev/null
+++ b/spec/features/projects/settings/user_interacts_with_deploy_keys_spec.rb
@@ -0,0 +1,125 @@
+require "spec_helper"
+
+describe "User interacts with deploy keys", :js do
+ let(:project) { create(:project, :repository) }
+ let(:user) { project.owner }
+
+ before do
+ sign_in(user)
+ end
+
+ shared_examples "attaches a key" do
+ it "attaches key" do
+ visit(project_deploy_keys_path(project))
+
+ page.within(".deploy-keys") do
+ find(".badge", text: "1").click
+
+ click_button("Enable")
+
+ expect(page).not_to have_selector(".fa-spinner")
+ expect(current_path).to eq(project_settings_repository_path(project))
+
+ find(".js-deployKeys-tab-enabled_keys").click
+
+ expect(page).to have_content(deploy_key.title)
+ end
+ end
+ end
+
+ context "viewing deploy keys" do
+ let(:deploy_key) { create(:deploy_key) }
+
+ context "when project has keys" do
+ before do
+ create(:deploy_keys_project, project: project, deploy_key: deploy_key)
+ end
+
+ it "shows deploy keys" do
+ visit(project_deploy_keys_path(project))
+
+ page.within(".deploy-keys") do
+ expect(page).to have_content(deploy_key.title)
+ end
+ end
+ end
+
+ context "when another project has keys" do
+ let(:another_project) { create(:project) }
+
+ before do
+ create(:deploy_keys_project, project: another_project, deploy_key: deploy_key)
+
+ another_project.add_master(user)
+ end
+
+ it "shows deploy keys" do
+ visit(project_deploy_keys_path(project))
+
+ page.within(".deploy-keys") do
+ find('.js-deployKeys-tab-available_project_keys').click
+
+ expect(page).to have_content(deploy_key.title)
+ expect(find(".js-deployKeys-tab-available_project_keys .badge")).to have_content("1")
+ end
+ end
+ end
+
+ context "when there are public deploy keys" do
+ let!(:deploy_key) { create(:deploy_key, public: true) }
+
+ it "shows public deploy keys" do
+ visit(project_deploy_keys_path(project))
+
+ page.within(".deploy-keys") do
+ find(".js-deployKeys-tab-public_keys").click
+
+ expect(page).to have_content(deploy_key.title)
+ end
+ end
+ end
+ end
+
+ context "adding deploy keys" do
+ before do
+ visit(project_deploy_keys_path(project))
+ end
+
+ it "adds new key" do
+ DEPLOY_KEY_TITLE = attributes_for(:key)[:title]
+ DEPLOY_KEY_BODY = attributes_for(:key)[:key]
+
+ fill_in("deploy_key_title", with: DEPLOY_KEY_TITLE)
+ fill_in("deploy_key_key", with: DEPLOY_KEY_BODY)
+
+ click_button("Add key")
+
+ expect(current_path).to eq(project_settings_repository_path(project))
+
+ page.within(".deploy-keys") do
+ expect(page).to have_content(DEPLOY_KEY_TITLE)
+ end
+ end
+ end
+
+ context "attaching existing keys" do
+ context "from another project" do
+ let(:another_project) { create(:project) }
+ let(:deploy_key) { create(:deploy_key) }
+
+ before do
+ create(:deploy_keys_project, project: another_project, deploy_key: deploy_key)
+
+ another_project.add_master(user)
+ end
+
+ it_behaves_like "attaches a key"
+ end
+
+ context "when keys are public" do
+ let!(:deploy_key) { create(:deploy_key, public: true) }
+
+ it_behaves_like "attaches a key"
+ end
+ end
+end
diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb
index b242e41df1c..3017048e506 100644
--- a/spec/features/projects/tree/create_directory_spec.rb
+++ b/spec/features/projects/tree/create_directory_spec.rb
@@ -44,12 +44,17 @@ feature 'Multi-file editor new directory', :js do
wait_for_requests
- click_button 'Stage all'
+ find('.js-ide-commit-mode').click
+
+ find('.multi-file-commit-list-item').hover
+ first('.multi-file-discard-btn .btn').click
fill_in('commit-message', with: 'commit message ide')
click_button('Commit')
+ find('.js-ide-edit-mode').click
+
expect(page).to have_content('folder name')
end
end
diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb
index 7d65456e049..56471c8e7aa 100644
--- a/spec/features/projects/tree/create_file_spec.rb
+++ b/spec/features/projects/tree/create_file_spec.rb
@@ -34,7 +34,10 @@ feature 'Multi-file editor new file', :js do
wait_for_requests
- click_button 'Stage all'
+ find('.js-ide-commit-mode').click
+
+ find('.multi-file-commit-list-item').hover
+ first('.multi-file-discard-btn .btn').click
fill_in('commit-message', with: 'commit message ide')
diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb
index 006c15d60c5..6586ccaa400 100644
--- a/spec/features/projects/wiki/markdown_preview_spec.rb
+++ b/spec/features/projects/wiki/markdown_preview_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
feature 'Projects > Wiki > User previews markdown changes', :js do
let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
+ let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
let(:wiki_content) do
<<-HEREDOC
[regular link](regular)
diff --git a/spec/features/projects/wiki/shortcuts_spec.rb b/spec/features/projects/wiki/shortcuts_spec.rb
index f70d1e710dd..6178361082e 100644
--- a/spec/features/projects/wiki/shortcuts_spec.rb
+++ b/spec/features/projects/wiki/shortcuts_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
feature 'Wiki shortcuts', :js do
let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
+ let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
let(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'home', content: 'Home page' }) }
before do
diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
index fe6fa55fa75..9989e1ffda7 100644
--- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
@@ -12,7 +12,7 @@ describe "User creates wiki page" do
context "when wiki is empty" do
context "in a user namespace" do
- let(:project) { create(:project, namespace: user.namespace) }
+ let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
it "shows validation error message" do
page.within(".wiki-form") do
@@ -142,7 +142,7 @@ describe "User creates wiki page" do
end
context "in a group namespace", :js do
- let(:project) { create(:project, namespace: create(:group, :public)) }
+ let(:project) { create(:project, :wiki_repo, namespace: create(:group, :public)) }
it "has `Create home` as a commit message" do
expect(page).to have_field("wiki[message]", with: "Create home")
@@ -164,11 +164,11 @@ describe "User creates wiki page" do
context "when wiki is not empty", :js do
before do
- create(:wiki_page, wiki: create(:project, namespace: user.namespace).wiki, attrs: { title: "home", content: "Home page" })
+ create(:wiki_page, wiki: create(:project, :wiki_repo, namespace: user.namespace).wiki, attrs: { title: "home", content: "Home page" })
end
context "in a user namespace" do
- let(:project) { create(:project, namespace: user.namespace) }
+ let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
context "via the `new wiki page` page" do
it "creates a page with a single word" do
@@ -261,7 +261,7 @@ describe "User creates wiki page" do
end
context "in a group namespace" do
- let(:project) { create(:project, namespace: create(:group, :public)) }
+ let(:project) { create(:project, :wiki_repo, namespace: create(:group, :public)) }
context "via the `new wiki page` page" do
it "creates a page" do
diff --git a/spec/features/projects/wiki/user_deletes_wiki_page_spec.rb b/spec/features/projects/wiki/user_deletes_wiki_page_spec.rb
index 605e332196b..2c67cec6b67 100644
--- a/spec/features/projects/wiki/user_deletes_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_deletes_wiki_page_spec.rb
@@ -1,8 +1,8 @@
require 'spec_helper'
-feature 'User deletes wiki page' do
+feature 'User deletes wiki page', :js do
let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
+ let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
let(:wiki_page) { create(:wiki_page, wiki: project.wiki) }
before do
@@ -13,6 +13,7 @@ feature 'User deletes wiki page' do
it 'deletes a page' do
click_on('Edit')
click_on('Delete')
+ find('.js-modal-primary-action').click
expect(page).to have_content('Page was successfully deleted')
end
diff --git a/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb b/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb
index 37a118c34ab..823399ac3c3 100644
--- a/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe 'Projects > Wiki > User views Git access wiki page' do
let(:user) { create(:user) }
- let(:project) { create(:project, :public) }
+ let(:project) { create(:project, :wiki_repo, :public) }
let(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'home', content: '[some link](other-page)' }) }
before do
diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
index ef1bb712846..e019e3ce5a5 100644
--- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
@@ -14,7 +14,7 @@ describe 'User updates wiki page' do
end
context 'in a user namespace' do
- let(:project) { create(:project, namespace: user.namespace) }
+ let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
it 'redirects back to the home edit page' do
page.within(:css, '.wiki-form .form-actions') do
@@ -66,7 +66,7 @@ describe 'User updates wiki page' do
end
context 'in a user namespace' do
- let(:project) { create(:project, namespace: user.namespace) }
+ let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
it 'updates a page' do
click_link('Edit')
@@ -134,7 +134,7 @@ describe 'User updates wiki page' do
end
context 'in a group namespace' do
- let(:project) { create(:project, namespace: create(:group, :public)) }
+ let(:project) { create(:project, :wiki_repo, namespace: create(:group, :public)) }
it 'updates a page' do
click_link('Edit')
@@ -154,7 +154,7 @@ describe 'User updates wiki page' do
end
context 'when the page is in a subdir' do
- let!(:project) { create(:project, namespace: user.namespace) }
+ let!(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
let(:project_wiki) { create(:project_wiki, project: project, user: project.creator) }
let(:page_name) { 'page_name' }
let(:page_dir) { "foo/bar/#{page_name}" }
diff --git a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
index 2682b62fa04..92b50169476 100644
--- a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
+++ b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
@@ -11,6 +11,7 @@ describe 'Projects > Wiki > User views wiki in project page' do
context 'when repository is disabled for project' do
let(:project) do
create(:project,
+ :wiki_repo,
:repository_disabled,
:merge_requests_disabled,
:builds_disabled)
diff --git a/spec/features/projects/wiki/user_views_wiki_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_page_spec.rb
index 306e382119a..6661714222a 100644
--- a/spec/features/projects/wiki/user_views_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_views_wiki_page_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe 'User views a wiki page' do
shared_examples 'wiki page user view' do
let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
+ let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
let(:wiki_page) do
create(:wiki_page,
wiki: project.wiki,
diff --git a/spec/features/raven_js_spec.rb b/spec/features/raven_js_spec.rb
index 74890c86047..a9e815eaf4f 100644
--- a/spec/features/raven_js_spec.rb
+++ b/spec/features/raven_js_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
feature 'RavenJS' do
- let(:raven_path) { '/raven.bundle.js' }
+ let(:raven_path) { '/raven.chunk.js' }
it 'should not load raven if sentry is disabled' do
visit new_user_session_path
diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb
index b396e103345..e0cd963fe39 100644
--- a/spec/features/runners_spec.rb
+++ b/spec/features/runners_spec.rb
@@ -15,7 +15,7 @@ feature 'Runners' do
end
scenario 'user can see a button to install runners on kubernetes clusters' do
- visit runners_path(project)
+ visit project_runners_path(project)
expect(page).to have_link('Install Runner on Kubernetes', href: project_clusters_path(project))
end
@@ -36,7 +36,7 @@ feature 'Runners' do
end
scenario 'user sees the specific runner' do
- visit runners_path(project)
+ visit project_runners_path(project)
within '.activated-specific-runners' do
expect(page).to have_content(specific_runner.display_name)
@@ -48,7 +48,7 @@ feature 'Runners' do
end
scenario 'user can pause and resume the specific runner' do
- visit runners_path(project)
+ visit project_runners_path(project)
within '.activated-specific-runners' do
expect(page).to have_content('Pause')
@@ -68,7 +68,7 @@ feature 'Runners' do
end
scenario 'user removes an activated specific runner if this is last project for that runners' do
- visit runners_path(project)
+ visit project_runners_path(project)
within '.activated-specific-runners' do
click_on 'Remove Runner'
@@ -78,7 +78,7 @@ feature 'Runners' do
end
scenario 'user edits the runner to be protected' do
- visit runners_path(project)
+ visit project_runners_path(project)
within '.activated-specific-runners' do
first('.edit-runner > a').click
@@ -98,7 +98,7 @@ feature 'Runners' do
end
scenario 'user edits runner not to run untagged jobs' do
- visit runners_path(project)
+ visit project_runners_path(project)
within '.activated-specific-runners' do
first('.edit-runner > a').click
@@ -117,7 +117,7 @@ feature 'Runners' do
given!(:shared_runner) { create(:ci_runner, :shared) }
scenario 'user sees CI/CD setting page' do
- visit runners_path(project)
+ visit project_runners_path(project)
expect(page.find('.available-shared-runners')).to have_content(shared_runner.display_name)
end
@@ -134,7 +134,7 @@ feature 'Runners' do
end
scenario 'user enables and disables a specific runner' do
- visit runners_path(project)
+ visit project_runners_path(project)
within '.available-specific-runners' do
click_on 'Enable for this project'
@@ -159,7 +159,7 @@ feature 'Runners' do
end
scenario 'user sees shared runners description' do
- visit runners_path(project)
+ visit project_runners_path(project)
expect(page.find('.shared-runners-description')).to have_content(shared_runners_html)
end
@@ -174,7 +174,7 @@ feature 'Runners' do
end
scenario 'user enables shared runners' do
- visit runners_path(project)
+ visit project_runners_path(project)
click_on 'Enable shared Runners'
@@ -182,7 +182,7 @@ feature 'Runners' do
end
end
- context 'group runners' do
+ context 'group runners in project settings' do
background do
project.add_master(user)
end
@@ -198,12 +198,12 @@ feature 'Runners' do
given(:project) { create :project, group: group }
scenario 'group runners are not available' do
- visit runners_path(project)
+ visit project_runners_path(project)
- expect(page).to have_content 'This group does not provide any group Runners yet.'
+ expect(page).to have_content 'This group does not provide any group Runners yet'
- expect(page).to have_content 'Setup a group Runner manually'
- expect(page).not_to have_content 'Ask your group master to setup a group Runner.'
+ expect(page).to have_content 'Group masters can register group runners in the Group CI/CD settings'
+ expect(page).not_to have_content 'Ask your group master to setup a group Runner'
end
end
end
@@ -213,7 +213,7 @@ feature 'Runners' do
given(:project) { create :project }
scenario 'group runners are not available' do
- visit runners_path(project)
+ visit project_runners_path(project)
expect(page).to have_content 'This project does not belong to a group and can therefore not make use of group Runners.'
end
@@ -224,11 +224,11 @@ feature 'Runners' do
given(:project) { create :project, group: group }
scenario 'group runners are not available' do
- visit runners_path(project)
+ visit project_runners_path(project)
expect(page).to have_content 'This group does not provide any group Runners yet.'
- expect(page).not_to have_content 'Setup a group Runner manually'
+ expect(page).not_to have_content 'Group masters can register group runners in the Group CI/CD settings'
expect(page).to have_content 'Ask your group master to setup a group Runner.'
end
end
@@ -239,14 +239,14 @@ feature 'Runners' do
given!(:ci_runner) { create :ci_runner, groups: [group], description: 'group-runner' }
scenario 'group runners are available' do
- visit runners_path(project)
+ visit project_runners_path(project)
expect(page).to have_content 'Available group Runners : 1'
expect(page).to have_content 'group-runner'
end
scenario 'group runners may be disabled for a project' do
- visit runners_path(project)
+ visit project_runners_path(project)
click_on 'Disable group Runners'
@@ -261,4 +261,98 @@ feature 'Runners' do
end
end
end
+
+ context 'group runners in group settings' do
+ given(:group) { create :group }
+ background do
+ group.add_master(user)
+ end
+
+ context 'group with no runners' do
+ scenario 'there are no runners displayed' do
+ visit group_settings_ci_cd_path(group)
+
+ expect(page).to have_content 'This group does not provide any group Runners yet'
+ end
+ end
+
+ context 'group with a runner' do
+ let!(:runner) { create :ci_runner, groups: [group], description: 'group-runner' }
+
+ scenario 'the runner is visible' do
+ visit group_settings_ci_cd_path(group)
+
+ expect(page).not_to have_content 'This group does not provide any group Runners yet'
+ expect(page).to have_content 'Available group Runners : 1'
+ expect(page).to have_content 'group-runner'
+ end
+
+ scenario 'user can pause and resume the group runner' do
+ visit group_settings_ci_cd_path(group)
+
+ expect(page).to have_content('Pause')
+ expect(page).not_to have_content('Resume')
+
+ click_on 'Pause'
+
+ expect(page).not_to have_content('Pause')
+ expect(page).to have_content('Resume')
+
+ click_on 'Resume'
+
+ expect(page).to have_content('Pause')
+ expect(page).not_to have_content('Resume')
+ end
+
+ scenario 'user can view runner details' do
+ visit group_settings_ci_cd_path(group)
+
+ expect(page).to have_content(runner.display_name)
+
+ click_on runner.short_sha
+
+ expect(page).to have_content(runner.platform)
+ end
+
+ scenario 'user can remove a group runner' do
+ visit group_settings_ci_cd_path(group)
+
+ click_on 'Remove Runner'
+
+ expect(page).not_to have_content(runner.display_name)
+ end
+
+ scenario 'user edits the runner to be protected' do
+ visit group_settings_ci_cd_path(group)
+
+ first('.edit-runner > a').click
+
+ expect(page.find_field('runner[access_level]')).not_to be_checked
+
+ check 'runner_access_level'
+ click_button 'Save changes'
+
+ expect(page).to have_content 'Protected Yes'
+ end
+
+ context 'when a runner has a tag' do
+ background do
+ runner.update(tag_list: ['tag'])
+ end
+
+ scenario 'user edits runner not to run untagged jobs' do
+ visit group_settings_ci_cd_path(group)
+
+ first('.edit-runner > a').click
+
+ expect(page.find_field('runner[run_untagged]')).to be_checked
+
+ uncheck 'runner_run_untagged'
+ click_button 'Save changes'
+
+ expect(page).to have_content 'Can run untagged jobs No'
+ end
+ end
+ end
+ end
end
diff --git a/spec/features/search/user_searches_for_wiki_pages_spec.rb b/spec/features/search/user_searches_for_wiki_pages_spec.rb
index 7934779058f..5098fb49ee1 100644
--- a/spec/features/search/user_searches_for_wiki_pages_spec.rb
+++ b/spec/features/search/user_searches_for_wiki_pages_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe 'User searches for wiki pages', :js do
let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
+ let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
let!(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'test_wiki', content: 'Some Wiki content' }) }
before do
diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb
index 94a2b289e64..6f968a2c590 100644
--- a/spec/features/users/login_spec.rb
+++ b/spec/features/users/login_spec.rb
@@ -437,5 +437,107 @@ feature 'Login' do
expect(current_path).to eq(root_path)
end
+
+ context 'when 2FA is required for the user' do
+ before do
+ group = create(:group, require_two_factor_authentication: true)
+ group.add_developer(user)
+ end
+
+ context 'when the user did not enable 2FA' do
+ it 'asks to set 2FA before asking to accept the terms' do
+ visit new_user_session_path
+
+ fill_in 'user_login', with: user.email
+ fill_in 'user_password', with: '12345678'
+
+ click_button 'Sign in'
+
+ expect_to_be_on_terms_page
+ click_button 'Accept terms'
+
+ expect(current_path).to eq(profile_two_factor_auth_path)
+
+ fill_in 'pin_code', with: user.reload.current_otp
+
+ click_button 'Register with two-factor app'
+ click_link 'Proceed'
+
+ expect(current_path).to eq(profile_account_path)
+ end
+ end
+
+ context 'when the user already enabled 2FA' do
+ before do
+ user.update!(otp_required_for_login: true,
+ otp_secret: User.generate_otp_secret(32))
+ end
+
+ it 'asks the user to accept the terms' do
+ visit new_user_session_path
+
+ fill_in 'user_login', with: user.email
+ fill_in 'user_password', with: '12345678'
+ click_button 'Sign in'
+
+ fill_in 'user_otp_attempt', with: user.reload.current_otp
+ click_button 'Verify code'
+
+ expect_to_be_on_terms_page
+ click_button 'Accept terms'
+
+ expect(current_path).to eq(root_path)
+ end
+ end
+ end
+
+ context 'when the users password is expired' do
+ before do
+ user.update!(password_expires_at: Time.parse('2018-05-08 11:29:46 UTC'))
+ end
+
+ it 'asks the user to accept the terms before setting a new password' do
+ visit new_user_session_path
+
+ fill_in 'user_login', with: user.email
+ fill_in 'user_password', with: '12345678'
+ click_button 'Sign in'
+
+ expect_to_be_on_terms_page
+ click_button 'Accept terms'
+
+ expect(current_path).to eq(new_profile_password_path)
+
+ fill_in 'user_current_password', with: '12345678'
+ fill_in 'user_password', with: 'new password'
+ fill_in 'user_password_confirmation', with: 'new password'
+ click_button 'Set new password'
+
+ expect(page).to have_content('Password successfully changed')
+ end
+ end
+
+ context 'when the user does not have an email configured' do
+ let(:user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'saml', email: 'temp-email-for-oauth-user@gitlab.localhost') }
+
+ before do
+ stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], providers: [mock_saml_config])
+ end
+
+ it 'asks the user to accept the terms before setting an email' do
+ gitlab_sign_in_via('saml', user, 'my-uid')
+
+ expect_to_be_on_terms_page
+ click_button 'Accept terms'
+
+ expect(current_path).to eq(profile_path)
+
+ fill_in 'Email', with: 'hello@world.com'
+
+ click_button 'Update profile settings'
+
+ expect(page).to have_content('Profile was successfully updated')
+ end
+ end
end
end
diff --git a/spec/features/users/terms_spec.rb b/spec/features/users/terms_spec.rb
index bf6b5fa3d6a..f9469adbfe3 100644
--- a/spec/features/users/terms_spec.rb
+++ b/spec/features/users/terms_spec.rb
@@ -81,4 +81,22 @@ describe 'Users > Terms' do
expect(find_field('issue_description').value).to eq("We don't want to lose what the user typed")
end
end
+
+ context 'when the terms are enforced' do
+ before do
+ enforce_terms
+ end
+
+ context 'signing out', :js do
+ it 'allows the user to sign out without a response' do
+ visit terms_path
+
+ find('.header-user-dropdown-toggle').click
+ click_link('Sign out')
+
+ expect(page).to have_content('Sign in')
+ expect(page).to have_content('Register')
+ end
+ end
+ end
end
diff --git a/spec/fixtures/api/schemas/ci_detailed_status.json b/spec/fixtures/api/schemas/ci_detailed_status.json
new file mode 100644
index 00000000000..01e34249bf1
--- /dev/null
+++ b/spec/fixtures/api/schemas/ci_detailed_status.json
@@ -0,0 +1,24 @@
+{
+ "type": "object",
+ "required" : [
+ "icon",
+ "text",
+ "label",
+ "group",
+ "tooltip",
+ "has_details",
+ "details_path",
+ "favicon"
+ ],
+ "properties": {
+ "icon": { "type": "string" },
+ "text": { "type": "string" },
+ "label": { "type": "string" },
+ "group": { "type": "string" },
+ "tooltip": { "type": "string" },
+ "has_details": { "type": "boolean" },
+ "details_path": { "type": "string" },
+ "favicon": { "type": "string" }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/entities/merge_request_widget.json b/spec/fixtures/api/schemas/entities/merge_request_widget.json
index a622bf88b13..233102c4314 100644
--- a/spec/fixtures/api/schemas/entities/merge_request_widget.json
+++ b/spec/fixtures/api/schemas/entities/merge_request_widget.json
@@ -22,6 +22,7 @@
"in_progress_merge_commit_sha": { "type": ["string", "null"] },
"merge_error": { "type": ["string", "null"] },
"merge_commit_sha": { "type": ["string", "null"] },
+ "short_merge_commit_sha": { "type": ["string", "null"] },
"merge_params": { "type": ["object", "null"] },
"merge_status": { "type": "string" },
"merge_user_id": { "type": ["integer", "null"] },
@@ -100,6 +101,7 @@
"merge_commit_message_with_description": { "type": "string" },
"diverged_commits_count": { "type": "integer" },
"commit_change_content_path": { "type": "string" },
+ "merge_commit_path": { "type": ["string", "null"] },
"remove_wip_path": { "type": ["string", "null"] },
"commits_count": { "type": "integer" },
"remove_source_branch": { "type": ["boolean", "null"] },
diff --git a/spec/fixtures/api/schemas/job.json b/spec/fixtures/api/schemas/job.json
new file mode 100644
index 00000000000..7b92ab25bc1
--- /dev/null
+++ b/spec/fixtures/api/schemas/job.json
@@ -0,0 +1,24 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "name",
+ "started",
+ "build_path",
+ "playable",
+ "created_at",
+ "updated_at",
+ "status"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "name": { "type": "string" },
+ "started": { "type": "boolean" } ,
+ "build_path": { "type": "string" },
+ "playable": { "type": "boolean" },
+ "created_at": { "type": "string" },
+ "updated_at": { "type": "string" },
+ "status": { "$ref": "ci_detailed_status.json" }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/pipeline_stage.json b/spec/fixtures/api/schemas/pipeline_stage.json
new file mode 100644
index 00000000000..55454200bb3
--- /dev/null
+++ b/spec/fixtures/api/schemas/pipeline_stage.json
@@ -0,0 +1,24 @@
+{
+ "type": "object",
+ "required" : [
+ "name",
+ "title",
+ "status",
+ "path",
+ "dropdown_path"
+ ],
+ "properties" : {
+ "name": { "type": "string" },
+ "title": { "type": "string" },
+ "groups": { "optional": true },
+ "latest_statuses": {
+ "type": "array",
+ "items": { "$ref": "job.json" },
+ "optional": true
+ },
+ "status": { "$ref": "ci_detailed_status.json" },
+ "path": { "type": "string" },
+ "dropdown_path": { "type": "string" }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 5e454f8b310..593b2ca1825 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -151,4 +151,16 @@ describe ApplicationHelper do
end
end
end
+
+ describe '#autocomplete_data_sources' do
+ let(:project) { create(:project) }
+ let(:noteable_type) { Issue }
+ it 'returns paths for autocomplete_sources_controller' do
+ sources = helper.autocomplete_data_sources(project, noteable_type)
+ expect(sources.keys).to match_array([:members, :issues, :merge_requests, :labels, :milestones, :commands])
+ sources.keys.each do |key|
+ expect(sources[key]).not_to be_nil
+ end
+ end
+ end
end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 8fcb175416f..f8877b6d1aa 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -283,7 +283,7 @@ describe ProjectsHelper do
end
it 'removes the repo path' do
- repo = "#{storage_path}/namespace/test.git"
+ repo = File.join(storage_path, 'namespace/test.git')
import_error = "Could not clone #{repo}\n"
expect(sanitize_repo_path(project, import_error)).to eq('Could not clone [REPOS PATH]/namespace/test.git')
diff --git a/spec/initializers/6_validations_spec.rb b/spec/initializers/6_validations_spec.rb
index 1dc307ea922..8d9dc092547 100644
--- a/spec/initializers/6_validations_spec.rb
+++ b/spec/initializers/6_validations_spec.rb
@@ -42,26 +42,6 @@ describe '6_validations' do
expect { validate_storages_config }.to raise_error('"name with spaces" is not a valid storage name. Please fix this in your gitlab.yml before starting GitLab.')
end
end
-
- context 'with incomplete settings' do
- before do
- mock_storages('foo' => {})
- end
-
- it 'throws an error suggesting the user to update its settings' do
- expect { validate_storages_config }.to raise_error('foo is not a valid storage, because it has no `path` key. Refer to gitlab.yml.example for an updated example. Please fix this in your gitlab.yml before starting GitLab.')
- end
- end
-
- context 'with deprecated settings structure' do
- before do
- mock_storages('foo' => 'tmp/tests/paths/a/b/c')
- end
-
- it 'throws an error suggesting the user to update its settings' do
- expect { validate_storages_config }.to raise_error("foo is not a valid storage, because it has no `path` key. It may be configured as:\n\nfoo:\n path: tmp/tests/paths/a/b/c\n\nFor source installations, update your config/gitlab.yml Refer to gitlab.yml.example for an updated example.\n\nIf you're using the Gitlab Development Kit, you can update your configuration running `gdk reconfigure`.\n")
- end
- end
end
describe 'validate_storages_paths' do
diff --git a/spec/javascripts/deploy_keys/components/action_btn_spec.js b/spec/javascripts/deploy_keys/components/action_btn_spec.js
index 7025c3d836c..5bf72cc0018 100644
--- a/spec/javascripts/deploy_keys/components/action_btn_spec.js
+++ b/spec/javascripts/deploy_keys/components/action_btn_spec.js
@@ -7,62 +7,64 @@ describe('Deploy keys action btn', () => {
const deployKey = data.enabled_keys[0];
let vm;
- beforeEach((done) => {
- const ActionBtnComponent = Vue.extend(actionBtn);
-
- vm = new ActionBtnComponent({
- propsData: {
- deployKey,
- type: 'enable',
+ beforeEach(done => {
+ const ActionBtnComponent = Vue.extend({
+ components: {
+ actionBtn,
+ },
+ data() {
+ return {
+ deployKey,
+ };
},
- }).$mount();
+ template: `
+ <action-btn
+ :deploy-key="deployKey"
+ type="enable">
+ Enable
+ </action-btn>`,
+ });
+
+ vm = new ActionBtnComponent().$mount();
- setTimeout(done);
+ Vue.nextTick()
+ .then(done)
+ .catch(done.fail);
});
- it('renders the type as uppercase', () => {
- expect(
- vm.$el.textContent.trim(),
- ).toBe('Enable');
+ it('renders the default slot', () => {
+ expect(vm.$el.textContent.trim()).toBe('Enable');
});
- it('sends eventHub event with btn type', (done) => {
+ it('sends eventHub event with btn type', done => {
spyOn(eventHub, '$emit');
vm.$el.click();
- setTimeout(() => {
- expect(
- eventHub.$emit,
- ).toHaveBeenCalledWith('enable.key', deployKey, jasmine.anything());
+ Vue.nextTick(() => {
+ expect(eventHub.$emit).toHaveBeenCalledWith('enable.key', deployKey, jasmine.anything());
done();
});
});
- it('shows loading spinner after click', (done) => {
+ it('shows loading spinner after click', done => {
vm.$el.click();
- setTimeout(() => {
- expect(
- vm.$el.querySelector('.fa'),
- ).toBeDefined();
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.fa')).toBeDefined();
done();
});
});
- it('disables button after click', (done) => {
+ it('disables button after click', done => {
vm.$el.click();
- setTimeout(() => {
- expect(
- vm.$el.classList.contains('disabled'),
- ).toBeTruthy();
+ Vue.nextTick(() => {
+ expect(vm.$el.classList.contains('disabled')).toBeTruthy();
- expect(
- vm.$el.getAttribute('disabled'),
- ).toBe('disabled');
+ expect(vm.$el.getAttribute('disabled')).toBe('disabled');
done();
});
diff --git a/spec/javascripts/deploy_keys/components/app_spec.js b/spec/javascripts/deploy_keys/components/app_spec.js
index b870f87eab9..183d7cf2d41 100644
--- a/spec/javascripts/deploy_keys/components/app_spec.js
+++ b/spec/javascripts/deploy_keys/components/app_spec.js
@@ -1,26 +1,26 @@
-import _ from 'underscore';
import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import eventHub from '~/deploy_keys/eventhub';
import deployKeysApp from '~/deploy_keys/components/app.vue';
+import { TEST_HOST } from 'spec/test_constants';
describe('Deploy keys app component', () => {
const data = getJSONFixture('deploy_keys/keys.json');
let vm;
-
- const deployKeysResponse = (request, next) => {
- next(request.respondWith(JSON.stringify(data), {
- status: 200,
- }));
- };
+ let mock;
beforeEach((done) => {
- const Component = Vue.extend(deployKeysApp);
+ // setup axios mock before component
+ mock = new MockAdapter(axios);
+ mock.onGet(`${TEST_HOST}/dummy/`).replyOnce(200, data);
- Vue.http.interceptors.push(deployKeysResponse);
+ const Component = Vue.extend(deployKeysApp);
vm = new Component({
propsData: {
- endpoint: '/test',
+ endpoint: `${TEST_HOST}/dummy`,
+ projectId: '8',
},
}).$mount();
@@ -28,120 +28,115 @@ describe('Deploy keys app component', () => {
});
afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, deployKeysResponse);
+ mock.restore();
});
- it('renders loading icon', (done) => {
+ it('renders loading icon', done => {
vm.store.keys = {};
vm.isLoading = false;
Vue.nextTick(() => {
- expect(
- vm.$el.querySelectorAll('.deploy-keys-panel').length,
- ).toBe(0);
+ expect(vm.$el.querySelectorAll('.deploy-keys .nav-links li').length).toBe(0);
- expect(
- vm.$el.querySelector('.fa-spinner'),
- ).toBeDefined();
+ expect(vm.$el.querySelector('.fa-spinner')).toBeDefined();
done();
});
});
it('renders keys panels', () => {
- expect(
- vm.$el.querySelectorAll('.deploy-keys-panel').length,
- ).toBe(3);
+ expect(vm.$el.querySelectorAll('.deploy-keys .nav-links li').length).toBe(3);
});
- it('does not render key panels when keys object is empty', (done) => {
- vm.store.keys = {};
-
- Vue.nextTick(() => {
- expect(
- vm.$el.querySelectorAll('.deploy-keys-panel').length,
- ).toBe(0);
-
- done();
- });
+ it('renders the titles with keys count', () => {
+ const textContent = selector => {
+ const element = vm.$el.querySelector(`${selector}`);
+
+ expect(element).not.toBeNull();
+ return element.textContent.trim();
+ };
+
+ expect(textContent('.js-deployKeys-tab-enabled_keys')).toContain('Enabled deploy keys');
+ expect(textContent('.js-deployKeys-tab-available_project_keys')).toContain(
+ 'Privately accessible deploy keys',
+ );
+ expect(textContent('.js-deployKeys-tab-public_keys')).toContain(
+ 'Publicly accessible deploy keys',
+ );
+
+ expect(textContent('.js-deployKeys-tab-enabled_keys .badge')).toBe(
+ `${vm.store.keys.enabled_keys.length}`,
+ );
+ expect(textContent('.js-deployKeys-tab-available_project_keys .badge')).toBe(
+ `${vm.store.keys.available_project_keys.length}`,
+ );
+ expect(textContent('.js-deployKeys-tab-public_keys .badge')).toBe(
+ `${vm.store.keys.public_keys.length}`,
+ );
});
- it('does not render public panel when empty', (done) => {
- vm.store.keys.public_keys = [];
+ it('does not render key panels when keys object is empty', done => {
+ vm.store.keys = {};
Vue.nextTick(() => {
- expect(
- vm.$el.querySelectorAll('.deploy-keys-panel').length,
- ).toBe(2);
+ expect(vm.$el.querySelectorAll('.deploy-keys .nav-links li').length).toBe(0);
done();
});
});
- it('re-fetches deploy keys when enabling a key', (done) => {
+ it('re-fetches deploy keys when enabling a key', done => {
const key = data.public_keys[0];
spyOn(vm.service, 'getKeys');
- spyOn(vm.service, 'enableKey').and.callFake(() => new Promise((resolve) => {
- resolve();
-
- setTimeout(() => {
- expect(vm.service.getKeys).toHaveBeenCalled();
-
- done();
- });
- }));
+ spyOn(vm.service, 'enableKey').and.callFake(() => Promise.resolve());
eventHub.$emit('enable.key', key);
- expect(vm.service.enableKey).toHaveBeenCalledWith(key.id);
+ Vue.nextTick(() => {
+ expect(vm.service.enableKey).toHaveBeenCalledWith(key.id);
+ expect(vm.service.getKeys).toHaveBeenCalled();
+ done();
+ });
});
- it('re-fetches deploy keys when disabling a key', (done) => {
+ it('re-fetches deploy keys when disabling a key', done => {
const key = data.public_keys[0];
spyOn(window, 'confirm').and.returnValue(true);
spyOn(vm.service, 'getKeys');
- spyOn(vm.service, 'disableKey').and.callFake(() => new Promise((resolve) => {
- resolve();
-
- setTimeout(() => {
- expect(vm.service.getKeys).toHaveBeenCalled();
-
- done();
- });
- }));
+ spyOn(vm.service, 'disableKey').and.callFake(() => Promise.resolve());
eventHub.$emit('disable.key', key);
- expect(vm.service.disableKey).toHaveBeenCalledWith(key.id);
+ Vue.nextTick(() => {
+ expect(vm.service.disableKey).toHaveBeenCalledWith(key.id);
+ expect(vm.service.getKeys).toHaveBeenCalled();
+ done();
+ });
});
- it('calls disableKey when removing a key', (done) => {
+ it('calls disableKey when removing a key', done => {
const key = data.public_keys[0];
spyOn(window, 'confirm').and.returnValue(true);
spyOn(vm.service, 'getKeys');
- spyOn(vm.service, 'disableKey').and.callFake(() => new Promise((resolve) => {
- resolve();
-
- setTimeout(() => {
- expect(vm.service.getKeys).toHaveBeenCalled();
-
- done();
- });
- }));
+ spyOn(vm.service, 'disableKey').and.callFake(() => Promise.resolve());
eventHub.$emit('remove.key', key);
- expect(vm.service.disableKey).toHaveBeenCalledWith(key.id);
+ Vue.nextTick(() => {
+ expect(vm.service.disableKey).toHaveBeenCalledWith(key.id);
+ expect(vm.service.getKeys).toHaveBeenCalled();
+ done();
+ });
});
it('hasKeys returns true when there are keys', () => {
expect(vm.hasKeys).toEqual(3);
});
- it('resets remove button loading state', (done) => {
+ it('resets disable button loading state', done => {
spyOn(window, 'confirm').and.returnValue(false);
const btn = vm.$el.querySelector('.btn-warning');
@@ -149,7 +144,7 @@ describe('Deploy keys app component', () => {
btn.click();
Vue.nextTick(() => {
- expect(btn.querySelector('.fa')).toBeNull();
+ expect(btn.querySelector('.btn-warning')).not.toExist();
done();
});
diff --git a/spec/javascripts/deploy_keys/components/key_spec.js b/spec/javascripts/deploy_keys/components/key_spec.js
index b7aadf604a4..4279add21d1 100644
--- a/spec/javascripts/deploy_keys/components/key_spec.js
+++ b/spec/javascripts/deploy_keys/components/key_spec.js
@@ -7,7 +7,7 @@ describe('Deploy keys key', () => {
let vm;
const KeyComponent = Vue.extend(key);
const data = getJSONFixture('deploy_keys/keys.json');
- const createComponent = (deployKey) => {
+ const createComponent = deployKey => {
const store = new DeployKeysStore();
store.keys = data;
@@ -23,37 +23,42 @@ describe('Deploy keys key', () => {
describe('enabled key', () => {
const deployKey = data.enabled_keys[0];
- beforeEach((done) => {
+ beforeEach(done => {
createComponent(deployKey);
setTimeout(done);
});
it('renders the keys title', () => {
- expect(
- vm.$el.querySelector('.title').textContent.trim(),
- ).toContain('My title');
+ expect(vm.$el.querySelector('.title').textContent.trim()).toContain('My title');
});
it('renders human friendly formatted created date', () => {
- expect(
- vm.$el.querySelector('.key-created-at').textContent.trim(),
- ).toBe(`created ${getTimeago().format(deployKey.created_at)}`);
+ expect(vm.$el.querySelector('.key-created-at').textContent.trim()).toBe(
+ `${getTimeago().format(deployKey.created_at)}`,
+ );
});
- it('shows edit button', () => {
- expect(
- vm.$el.querySelectorAll('.btn')[0].textContent.trim(),
- ).toBe('Edit');
+ it('shows pencil button for editing', () => {
+ expect(vm.$el.querySelector('.btn .ic-pencil')).toExist();
});
- it('shows remove button', () => {
- expect(
- vm.$el.querySelectorAll('.btn')[1].textContent.trim(),
- ).toBe('Remove');
+ it('shows disable button when the project is not deletable', () => {
+ expect(vm.$el.querySelector('.btn .ic-cancel')).toExist();
});
- it('shows write access title when key has write access', (done) => {
+ it('shows remove button when the project is deletable', done => {
+ vm.deployKey.destroyed_when_orphaned = true;
+ vm.deployKey.almost_orphaned = true;
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.btn .ic-remove')).toExist();
+ done();
+ });
+ });
+ });
+
+ describe('deploy key labels', () => {
+ it('shows write access title when key has write access', done => {
vm.deployKey.deploy_keys_projects[0].can_push = true;
Vue.nextTick(() => {
@@ -64,7 +69,7 @@ describe('Deploy keys key', () => {
});
});
- it('does not show write access title when key has write access', (done) => {
+ it('does not show write access title when key has write access', done => {
vm.deployKey.deploy_keys_projects[0].can_push = false;
Vue.nextTick(() => {
@@ -74,36 +79,73 @@ describe('Deploy keys key', () => {
done();
});
});
+
+ it('shows expandable button if more than two projects', () => {
+ const labels = vm.$el.querySelectorAll('.deploy-project-label');
+ expect(labels.length).toBe(2);
+ expect(labels[1].textContent).toContain('others');
+ expect(labels[1].getAttribute('data-original-title')).toContain('Expand');
+ });
+
+ it('expands all project labels after click', done => {
+ const length = vm.deployKey.deploy_keys_projects.length;
+ vm.$el.querySelectorAll('.deploy-project-label')[1].click();
+
+ Vue.nextTick(() => {
+ const labels = vm.$el.querySelectorAll('.deploy-project-label');
+ expect(labels.length).toBe(length);
+ expect(labels[1].textContent).not.toContain(`+${length} others`);
+ expect(labels[1].getAttribute('data-original-title')).not.toContain('Expand');
+ done();
+ });
+ });
+
+ it('shows two projects', done => {
+ vm.deployKey.deploy_keys_projects = [...vm.deployKey.deploy_keys_projects].slice(0, 2);
+
+ Vue.nextTick(() => {
+ const labels = vm.$el.querySelectorAll('.deploy-project-label');
+ expect(labels.length).toBe(2);
+ expect(labels[1].textContent).toContain(
+ vm.deployKey.deploy_keys_projects[1].project.full_name,
+ );
+ done();
+ });
+ });
});
describe('public keys', () => {
const deployKey = data.public_keys[0];
- beforeEach((done) => {
+ beforeEach(done => {
createComponent(deployKey);
setTimeout(done);
});
- it('shows edit button', () => {
- expect(
- vm.$el.querySelectorAll('.btn')[0].textContent.trim(),
- ).toBe('Edit');
+ it('renders deploy keys without any enabled projects', done => {
+ vm.deployKey.deploy_keys_projects = [];
+
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.deploy-project-list').textContent.trim()).toBe('None');
+
+ done();
+ });
});
it('shows enable button', () => {
- expect(
- vm.$el.querySelectorAll('.btn')[1].textContent.trim(),
- ).toBe('Enable');
+ expect(vm.$el.querySelectorAll('.btn')[0].textContent.trim()).toBe('Enable');
});
- it('shows disable button when key is enabled', (done) => {
+ it('shows pencil button for editing', () => {
+ expect(vm.$el.querySelector('.btn .ic-pencil')).toExist();
+ });
+
+ it('shows disable button when key is enabled', done => {
vm.store.keys.enabled_keys.push(deployKey);
Vue.nextTick(() => {
- expect(
- vm.$el.querySelectorAll('.btn')[1].textContent.trim(),
- ).toBe('Disable');
+ expect(vm.$el.querySelector('.btn .ic-cancel')).toExist();
done();
});
diff --git a/spec/javascripts/deploy_keys/components/keys_panel_spec.js b/spec/javascripts/deploy_keys/components/keys_panel_spec.js
index 08357d2b547..f71f5ccf082 100644
--- a/spec/javascripts/deploy_keys/components/keys_panel_spec.js
+++ b/spec/javascripts/deploy_keys/components/keys_panel_spec.js
@@ -6,7 +6,7 @@ describe('Deploy keys panel', () => {
const data = getJSONFixture('deploy_keys/keys.json');
let vm;
- beforeEach((done) => {
+ beforeEach(done => {
const DeployKeysPanelComponent = Vue.extend(deployKeysPanel);
const store = new DeployKeysStore();
store.keys = data;
@@ -24,46 +24,38 @@ describe('Deploy keys panel', () => {
setTimeout(done);
});
- it('renders the title with keys count', () => {
- expect(
- vm.$el.querySelector('h5').textContent.trim(),
- ).toContain('test');
-
- expect(
- vm.$el.querySelector('h5').textContent.trim(),
- ).toContain(`(${vm.keys.length})`);
+ it('renders list of keys', () => {
+ expect(vm.$el.querySelectorAll('.deploy-key').length).toBe(vm.keys.length);
});
- it('renders list of keys', () => {
- expect(
- vm.$el.querySelectorAll('li').length,
- ).toBe(vm.keys.length);
+ it('renders table header', () => {
+ const tableHeader = vm.$el.querySelector('.table-row-header');
+
+ expect(tableHeader).toExist();
+ expect(tableHeader.textContent).toContain('Deploy key');
+ expect(tableHeader.textContent).toContain('Project usage');
+ expect(tableHeader.textContent).toContain('Created');
});
- it('renders help box if keys are empty', (done) => {
+ it('renders help box if keys are empty', done => {
vm.keys = [];
Vue.nextTick(() => {
- expect(
- vm.$el.querySelector('.settings-message'),
- ).toBeDefined();
+ expect(vm.$el.querySelector('.settings-message')).toBeDefined();
- expect(
- vm.$el.querySelector('.settings-message').textContent.trim(),
- ).toBe('No deploy keys found. Create one with the form above.');
+ expect(vm.$el.querySelector('.settings-message').textContent.trim()).toBe(
+ 'No deploy keys found. Create one with the form above.',
+ );
done();
});
});
- it('does not render help box if keys are empty & showHelpBox is false', (done) => {
+ it('renders no table header if keys are empty', done => {
vm.keys = [];
- vm.showHelpBox = false;
Vue.nextTick(() => {
- expect(
- vm.$el.querySelector('.settings-message'),
- ).toBeNull();
+ expect(vm.$el.querySelector('.table-row-header')).not.toExist();
done();
});
diff --git a/spec/javascripts/fixtures/deploy_keys.rb b/spec/javascripts/fixtures/deploy_keys.rb
index 580894ceaf9..24699c3043a 100644
--- a/spec/javascripts/fixtures/deploy_keys.rb
+++ b/spec/javascripts/fixtures/deploy_keys.rb
@@ -7,6 +7,8 @@ describe Projects::DeployKeysController, '(JavaScript fixtures)', type: :control
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project_empty_repo, namespace: namespace, path: 'todos-project') }
let(:project2) { create(:project, :internal)}
+ let(:project3) { create(:project, :internal)}
+ let(:project4) { create(:project, :internal)}
before(:all) do
clean_frontend_fixtures('deploy_keys/')
@@ -28,6 +30,8 @@ describe Projects::DeployKeysController, '(JavaScript fixtures)', type: :control
internal_key = create(:deploy_key, key: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDNd/UJWhPrpb+b/G5oL109y57yKuCxE+WUGJGYaj7WQKsYRJmLYh1mgjrl+KVyfsWpq4ylOxIfFSnN9xBBFN8mlb0Fma5DC7YsSsibJr3MZ19ZNBprwNcdogET7aW9I0In7Wu5f2KqI6e5W/spJHCy4JVxzVMUvk6Myab0LnJ2iQ== dummy@gitlab.com')
create(:deploy_keys_project, project: project, deploy_key: project_key)
create(:deploy_keys_project, project: project2, deploy_key: internal_key)
+ create(:deploy_keys_project, project: project3, deploy_key: project_key)
+ create(:deploy_keys_project, project: project4, deploy_key: project_key)
get :index,
namespace_id: project.namespace.to_param,
diff --git a/spec/javascripts/fixtures/mini_dropdown_graph.html.haml b/spec/javascripts/fixtures/mini_dropdown_graph.html.haml
index b532b48a95b..74584993739 100644
--- a/spec/javascripts/fixtures/mini_dropdown_graph.html.haml
+++ b/spec/javascripts/fixtures/mini_dropdown_graph.html.haml
@@ -4,6 +4,7 @@
%ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container
%li.js-builds-dropdown-list.scrollable-menu
+ %ul
%li.js-builds-dropdown-loading.hidden
%span.fa.fa-spinner
diff --git a/spec/javascripts/gpg_badges_spec.js b/spec/javascripts/gpg_badges_spec.js
index 5decb5e6bbd..97c771dcfd3 100644
--- a/spec/javascripts/gpg_badges_spec.js
+++ b/spec/javascripts/gpg_badges_spec.js
@@ -16,8 +16,8 @@ describe('GpgBadges', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
setFixtures(`
- <form
- class="commits-search-form" data-signatures-path="/hello" action="/hello"
+ <form
+ class="commits-search-form js-signature-container" data-signatures-path="/hello" action="/hello"
method="get">
<input name="utf8" type="hidden" value="✓">
<input type="search" name="search" id="commits-search"class="form-control search-text-input input-short">
diff --git a/spec/javascripts/helpers/vue_mount_component_helper.js b/spec/javascripts/helpers/vue_mount_component_helper.js
index effacbcff4e..a34a1add4e0 100644
--- a/spec/javascripts/helpers/vue_mount_component_helper.js
+++ b/spec/javascripts/helpers/vue_mount_component_helper.js
@@ -1,14 +1,30 @@
+import Vue from 'vue';
+
+const mountComponent = (Component, props = {}, el = null) => new Component({
+ propsData: props,
+}).$mount(el);
+
export const createComponentWithStore = (Component, store, propsData = {}) => new Component({
store,
propsData,
});
+export const createComponentWithMixin = (mixins = [], state = {}, props = {}, template = '<div></div>') => {
+ const Component = Vue.extend({
+ template,
+ mixins,
+ data() {
+ return props;
+ },
+ });
+
+ return mountComponent(Component, props);
+};
+
export const mountComponentWithStore = (Component, { el, props, store }) =>
new Component({
store,
propsData: props || { },
}).$mount(el);
-export default (Component, props = {}, el = null) => new Component({
- propsData: props,
-}).$mount(el);
+export default mountComponent;
diff --git a/spec/javascripts/ide/components/activity_bar_spec.js b/spec/javascripts/ide/components/activity_bar_spec.js
new file mode 100644
index 00000000000..946c7e8e9c8
--- /dev/null
+++ b/spec/javascripts/ide/components/activity_bar_spec.js
@@ -0,0 +1,92 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import { activityBarViews } from '~/ide/constants';
+import ActivityBar from '~/ide/components/activity_bar.vue';
+import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
+import { resetStore } from '../helpers';
+
+describe('IDE activity bar', () => {
+ const Component = Vue.extend(ActivityBar);
+ let vm;
+
+ beforeEach(() => {
+ Vue.set(store.state.projects, 'abcproject', {
+ web_url: 'testing',
+ });
+ Vue.set(store.state, 'currentProjectId', 'abcproject');
+
+ vm = createComponentWithStore(Component, store);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ describe('goBackUrl', () => {
+ it('renders the Go Back link with the referrer when present', () => {
+ const fakeReferrer = '/example/README.md';
+ spyOnProperty(document, 'referrer').and.returnValue(fakeReferrer);
+
+ vm.$mount();
+
+ expect(vm.goBackUrl).toEqual(fakeReferrer);
+ });
+
+ it('renders the Go Back link with the project url when referrer is not present', () => {
+ const fakeReferrer = '';
+ spyOnProperty(document, 'referrer').and.returnValue(fakeReferrer);
+
+ vm.$mount();
+
+ expect(vm.goBackUrl).toEqual('testing');
+ });
+ });
+
+ describe('updateActivityBarView', () => {
+ beforeEach(() => {
+ spyOn(vm, 'updateActivityBarView');
+
+ vm.$mount();
+ });
+
+ it('calls updateActivityBarView with edit value on click', () => {
+ vm.$el.querySelector('.js-ide-edit-mode').click();
+
+ expect(vm.updateActivityBarView).toHaveBeenCalledWith(activityBarViews.edit);
+ });
+
+ it('calls updateActivityBarView with commit value on click', () => {
+ vm.$el.querySelector('.js-ide-commit-mode').click();
+
+ expect(vm.updateActivityBarView).toHaveBeenCalledWith(activityBarViews.commit);
+ });
+
+ it('calls updateActivityBarView with review value on click', () => {
+ vm.$el.querySelector('.js-ide-review-mode').click();
+
+ expect(vm.updateActivityBarView).toHaveBeenCalledWith(activityBarViews.review);
+ });
+ });
+
+ describe('active item', () => {
+ beforeEach(() => {
+ vm.$mount();
+ });
+
+ it('sets edit item active', () => {
+ expect(vm.$el.querySelector('.js-ide-edit-mode').classList).toContain('active');
+ });
+
+ it('sets commit item active', done => {
+ vm.$store.state.currentActivityView = activityBarViews.commit;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.js-ide-commit-mode').classList).toContain('active');
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/commit_sidebar/actions_spec.js b/spec/javascripts/ide/components/commit_sidebar/actions_spec.js
index 144e78d14b5..27f10caccb1 100644
--- a/spec/javascripts/ide/components/commit_sidebar/actions_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/actions_spec.js
@@ -3,6 +3,7 @@ import store from '~/ide/stores';
import commitActions from '~/ide/components/commit_sidebar/actions.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore } from 'spec/ide/helpers';
+import { projectData } from 'spec/ide/mock_data';
describe('IDE commit sidebar actions', () => {
let vm;
@@ -13,6 +14,8 @@ describe('IDE commit sidebar actions', () => {
vm = createComponentWithStore(Component, store);
vm.$store.state.currentBranchId = 'master';
+ vm.$store.state.currentProjectId = 'abcproject';
+ Vue.set(vm.$store.state.projects, 'abcproject', { ...projectData });
vm.$mount();
@@ -32,4 +35,15 @@ describe('IDE commit sidebar actions', () => {
it('renders current branch text', () => {
expect(vm.$el.textContent).toContain('Commit to master branch');
});
+
+ it('hides merge request option when project merge requests are disabled', done => {
+ vm.$store.state.projects.abcproject.merge_requests_enabled = false;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelectorAll('input[type="radio"]').length).toBe(2);
+ expect(vm.$el.textContent).not.toContain('Create a new branch and merge request');
+
+ done();
+ });
+ });
});
diff --git a/spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js b/spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js
index 53275b78da5..16d0b354a30 100644
--- a/spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js
@@ -10,10 +10,9 @@ describe('IDE commit panel empty state', () => {
beforeEach(() => {
const Component = Vue.extend(emptyState);
- vm = createComponentWithStore(Component, store, {
- noChangesStateSvgPath: 'no-changes',
- committedStateSvgPath: 'committed-state',
- });
+ Vue.set(store.state, 'noChangesStateSvgPath', 'no-changes');
+
+ vm = createComponentWithStore(Component, store);
vm.$mount();
});
@@ -27,37 +26,4 @@ describe('IDE commit panel empty state', () => {
it('renders no changes text when last commit message is empty', () => {
expect(vm.$el.textContent).toContain('No changes');
});
-
- describe('toggle button', () => {
- it('calls store action', () => {
- spyOn(vm, 'toggleRightPanelCollapsed');
-
- vm.$el.querySelector('.multi-file-commit-panel-collapse-btn').click();
-
- expect(vm.toggleRightPanelCollapsed).toHaveBeenCalled();
- });
-
- it('renders collapsed class', done => {
- vm.$el.querySelector('.multi-file-commit-panel-collapse-btn').click();
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.is-collapsed')).not.toBeNull();
-
- done();
- });
- });
- });
-
- describe('collapsed state', () => {
- beforeEach(done => {
- vm.$store.state.rightPanelCollapsed = true;
-
- Vue.nextTick(done);
- });
-
- it('does not render text & svg', () => {
- expect(vm.$el.querySelector('img')).toBeNull();
- expect(vm.$el.textContent).not.toContain('No changes');
- });
- });
});
diff --git a/spec/javascripts/ide/components/commit_sidebar/form_spec.js b/spec/javascripts/ide/components/commit_sidebar/form_spec.js
new file mode 100644
index 00000000000..8b47a365582
--- /dev/null
+++ b/spec/javascripts/ide/components/commit_sidebar/form_spec.js
@@ -0,0 +1,149 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import CommitForm from '~/ide/components/commit_sidebar/form.vue';
+import { activityBarViews } from '~/ide/constants';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper';
+import { projectData } from 'spec/ide/mock_data';
+import { resetStore } from '../../helpers';
+
+describe('IDE commit form', () => {
+ const Component = Vue.extend(CommitForm);
+ let vm;
+
+ beforeEach(() => {
+ spyOnProperty(window, 'innerHeight').and.returnValue(800);
+
+ store.state.changedFiles.push('test');
+ store.state.currentProjectId = 'abcproject';
+ Vue.set(store.state.projects, 'abcproject', { ...projectData });
+
+ vm = createComponentWithStore(Component, store).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('enables button when has changes', () => {
+ expect(vm.$el.querySelector('[disabled]')).toBe(null);
+ });
+
+ describe('compact', () => {
+ it('renders commit button in compact mode', () => {
+ expect(vm.$el.querySelector('.btn-primary')).not.toBeNull();
+ expect(vm.$el.querySelector('.btn-primary').textContent).toContain('Commit');
+ });
+
+ it('does not render form', () => {
+ expect(vm.$el.querySelector('form')).toBeNull();
+ });
+
+ it('renders overview text', done => {
+ vm.$store.state.stagedFiles.push('test');
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('p').textContent).toContain('1 unstaged and 1 staged changes');
+ done();
+ });
+ });
+
+ it('shows form when clicking commit button', done => {
+ vm.$el.querySelector('.btn-primary').click();
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('form')).not.toBeNull();
+
+ done();
+ });
+ });
+
+ it('toggles activity bar vie when clicking commit button', done => {
+ vm.$el.querySelector('.btn-primary').click();
+
+ vm.$nextTick(() => {
+ expect(store.state.currentActivityView).toBe(activityBarViews.commit);
+
+ done();
+ });
+ });
+ });
+
+ describe('full', () => {
+ beforeEach(done => {
+ vm.isCompact = false;
+
+ vm.$nextTick(done);
+ });
+
+ it('updates commitMessage in store on input', done => {
+ const textarea = vm.$el.querySelector('textarea');
+
+ textarea.value = 'testing commit message';
+
+ textarea.dispatchEvent(new Event('input'));
+
+ getSetTimeoutPromise()
+ .then(() => {
+ expect(vm.$store.state.commit.commitMessage).toBe('testing commit message');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('updating currentActivityView not to commit view sets compact mode', done => {
+ store.state.currentActivityView = 'a';
+
+ vm.$nextTick(() => {
+ expect(vm.isCompact).toBe(true);
+
+ done();
+ });
+ });
+
+ describe('discard draft button', () => {
+ it('hidden when commitMessage is empty', () => {
+ expect(vm.$el.querySelector('.btn-default').textContent).toContain('Collapse');
+ });
+
+ it('resets commitMessage when clicking discard button', done => {
+ vm.$store.state.commit.commitMessage = 'testing commit message';
+
+ getSetTimeoutPromise()
+ .then(() => {
+ vm.$el.querySelector('.btn-default').click();
+ })
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(vm.$store.state.commit.commitMessage).not.toBe('testing commit message');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('when submitting', () => {
+ beforeEach(() => {
+ spyOn(vm, 'commitChanges');
+ vm.$store.state.stagedFiles.push('test');
+ });
+
+ it('calls commitChanges', done => {
+ vm.$store.state.commit.commitMessage = 'testing commit message';
+
+ getSetTimeoutPromise()
+ .then(() => {
+ vm.$el.querySelector('.btn-success').click();
+ })
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(vm.commitChanges).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/commit_sidebar/list_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_spec.js
index 62fc3f90ad1..54625ef90f8 100644
--- a/spec/javascripts/ide/components/commit_sidebar/list_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/list_spec.js
@@ -49,45 +49,4 @@ describe('Multi-file editor commit sidebar list', () => {
expect(vm.$el.textContent).toContain('No changes');
});
});
-
- describe('collapsed', () => {
- beforeEach(done => {
- vm.$store.state.rightPanelCollapsed = true;
-
- Vue.nextTick(done);
- });
-
- it('hides list', () => {
- expect(vm.$el.querySelector('.list-unstyled')).toBeNull();
- expect(vm.$el.querySelector('.help-block')).toBeNull();
- });
- });
-
- describe('with toggle', () => {
- beforeEach(done => {
- spyOn(vm, 'toggleRightPanelCollapsed');
-
- vm.showToggle = true;
-
- Vue.nextTick(done);
- });
-
- it('calls setPanelCollapsedStatus when clickin toggle', () => {
- vm.$el.querySelector('.multi-file-commit-panel-collapse-btn').click();
-
- expect(vm.toggleRightPanelCollapsed).toHaveBeenCalled();
- });
- });
-
- describe('action button', () => {
- beforeEach(() => {
- spyOn(vm, 'stageAllChanges');
- });
-
- it('calls store action when clicked', () => {
- vm.$el.querySelector('.ide-staged-action-btn').click();
-
- expect(vm.stageAllChanges).toHaveBeenCalled();
- });
- });
});
diff --git a/spec/javascripts/ide/components/ide_context_bar_spec.js b/spec/javascripts/ide/components/ide_context_bar_spec.js
deleted file mode 100644
index e17b051f137..00000000000
--- a/spec/javascripts/ide/components/ide_context_bar_spec.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import Vue from 'vue';
-import store from '~/ide/stores';
-import ideContextBar from '~/ide/components/ide_context_bar.vue';
-import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
-
-describe('Multi-file editor right context bar', () => {
- let vm;
-
- beforeEach(() => {
- const Component = Vue.extend(ideContextBar);
-
- vm = createComponentWithStore(Component, store, {
- noChangesStateSvgPath: 'svg',
- committedStateSvgPath: 'svg',
- });
-
- vm.$store.state.rightPanelCollapsed = false;
-
- vm.$mount();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('collapsed', () => {
- beforeEach(done => {
- vm.$store.state.rightPanelCollapsed = true;
-
- Vue.nextTick(done);
- });
-
- it('adds collapsed class', () => {
- expect(vm.$el.querySelector('.is-collapsed')).not.toBeNull();
- });
- });
-});
diff --git a/spec/javascripts/ide/components/ide_external_links_spec.js b/spec/javascripts/ide/components/ide_external_links_spec.js
deleted file mode 100644
index 9f6cb459f3b..00000000000
--- a/spec/javascripts/ide/components/ide_external_links_spec.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import Vue from 'vue';
-import ideExternalLinks from '~/ide/components/ide_external_links.vue';
-import createComponent from 'spec/helpers/vue_mount_component_helper';
-
-describe('ide external links component', () => {
- let vm;
- let fakeReferrer;
- let Component;
-
- const fakeProjectUrl = '/project/';
-
- beforeEach(() => {
- Component = Vue.extend(ideExternalLinks);
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('goBackUrl', () => {
- it('renders the Go Back link with the referrer when present', () => {
- fakeReferrer = '/example/README.md';
- spyOnProperty(document, 'referrer').and.returnValue(fakeReferrer);
-
- vm = createComponent(Component, {
- projectUrl: fakeProjectUrl,
- }).$mount();
-
- expect(vm.goBackUrl).toEqual(fakeReferrer);
- });
-
- it('renders the Go Back link with the project url when referrer is not present', () => {
- fakeReferrer = '';
- spyOnProperty(document, 'referrer').and.returnValue(fakeReferrer);
-
- vm = createComponent(Component, {
- projectUrl: fakeProjectUrl,
- }).$mount();
-
- expect(vm.goBackUrl).toEqual(fakeProjectUrl);
- });
- });
-});
diff --git a/spec/javascripts/ide/components/ide_project_tree_spec.js b/spec/javascripts/ide/components/ide_project_tree_spec.js
deleted file mode 100644
index 657682cb39c..00000000000
--- a/spec/javascripts/ide/components/ide_project_tree_spec.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import Vue from 'vue';
-import ProjectTree from '~/ide/components/ide_project_tree.vue';
-import createComponent from 'spec/helpers/vue_mount_component_helper';
-
-describe('IDE project tree', () => {
- const Component = Vue.extend(ProjectTree);
- let vm;
-
- beforeEach(() => {
- vm = createComponent(Component, {
- project: {
- id: 1,
- name: 'test',
- web_url: gl.TEST_HOST,
- avatar_url: '',
- branches: [],
- },
- });
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('renders identicon when projct has no avatar', () => {
- expect(vm.$el.querySelector('.identicon')).not.toBeNull();
- });
-
- it('renders avatar image if project has avatar', done => {
- vm.project.avatar_url = gl.TEST_HOST;
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.identicon')).toBeNull();
- expect(vm.$el.querySelector('img.avatar')).not.toBeNull();
-
- done();
- });
- });
-});
diff --git a/spec/javascripts/ide/components/ide_repo_tree_spec.js b/spec/javascripts/ide/components/ide_repo_tree_spec.js
deleted file mode 100644
index e0fbc90ca61..00000000000
--- a/spec/javascripts/ide/components/ide_repo_tree_spec.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import Vue from 'vue';
-import ideRepoTree from '~/ide/components/ide_repo_tree.vue';
-import createComponent from '../../helpers/vue_mount_component_helper';
-import { file } from '../helpers';
-
-describe('IdeRepoTree', () => {
- let vm;
- let tree;
-
- beforeEach(() => {
- const IdeRepoTree = Vue.extend(ideRepoTree);
-
- tree = {
- tree: [file()],
- loading: false,
- };
-
- vm = createComponent(IdeRepoTree, {
- tree,
- });
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('renders a sidebar', () => {
- expect(vm.$el.querySelector('.loading-file')).toBeNull();
- expect(vm.$el.querySelector('.file')).not.toBeNull();
- });
-
- it('renders 3 loading files if tree is loading', done => {
- tree.loading = true;
-
- vm.$nextTick(() => {
- expect(
- vm.$el.querySelectorAll('.multi-file-loading-container').length,
- ).toEqual(3);
-
- done();
- });
- });
-});
diff --git a/spec/javascripts/ide/components/ide_review_spec.js b/spec/javascripts/ide/components/ide_review_spec.js
new file mode 100644
index 00000000000..b9ee22b7c1a
--- /dev/null
+++ b/spec/javascripts/ide/components/ide_review_spec.js
@@ -0,0 +1,69 @@
+import Vue from 'vue';
+import IdeReview from '~/ide/components/ide_review.vue';
+import store from '~/ide/stores';
+import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
+import { trimText } from '../../helpers/vue_component_helper';
+import { resetStore, file } from '../helpers';
+import { projectData } from '../mock_data';
+
+describe('IDE review mode', () => {
+ const Component = Vue.extend(IdeReview);
+ let vm;
+
+ beforeEach(() => {
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'master';
+ store.state.projects.abcproject = Object.assign({}, projectData);
+ Vue.set(store.state.trees, 'abcproject/master', {
+ tree: [file('fileName')],
+ loading: false,
+ });
+
+ vm = createComponentWithStore(Component, store).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders list of files', () => {
+ expect(vm.$el.textContent).toContain('fileName');
+ });
+
+ describe('merge request', () => {
+ beforeEach(done => {
+ store.state.currentMergeRequestId = '1';
+ store.state.projects.abcproject.mergeRequests['1'] = {
+ iid: 123,
+ web_url: 'testing123',
+ };
+
+ vm.$nextTick(done);
+ });
+
+ it('renders edit dropdown', () => {
+ expect(vm.$el.querySelector('.btn')).not.toBe(null);
+ });
+
+ it('renders merge request link & IID', () => {
+ const link = vm.$el.querySelector('.ide-review-sub-header');
+
+ expect(link.querySelector('a').getAttribute('href')).toBe('testing123');
+ expect(trimText(link.textContent)).toBe('Merge request (!123)');
+ });
+
+ it('changes text to latest changes when viewer is not mrdiff', done => {
+ store.state.viewer = 'diff';
+
+ vm.$nextTick(() => {
+ expect(trimText(vm.$el.querySelector('.ide-review-sub-header').textContent)).toBe(
+ 'Latest changes',
+ );
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/ide_side_bar_spec.js b/spec/javascripts/ide/components/ide_side_bar_spec.js
index 699dae1ce2f..20ee20bc1d7 100644
--- a/spec/javascripts/ide/components/ide_side_bar_spec.js
+++ b/spec/javascripts/ide/components/ide_side_bar_spec.js
@@ -1,8 +1,10 @@
import Vue from 'vue';
import store from '~/ide/stores';
import ideSidebar from '~/ide/components/ide_side_bar.vue';
+import { activityBarViews } from '~/ide/constants';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore } from '../helpers';
+import { projectData } from '../mock_data';
describe('IdeSidebar', () => {
let vm;
@@ -10,6 +12,9 @@ describe('IdeSidebar', () => {
beforeEach(() => {
const Component = Vue.extend(ideSidebar);
+ store.state.currentProjectId = 'abcproject';
+ store.state.projects.abcproject = projectData;
+
vm = createComponentWithStore(Component, store).$mount();
});
@@ -20,23 +25,33 @@ describe('IdeSidebar', () => {
});
it('renders a sidebar', () => {
- expect(
- vm.$el.querySelector('.multi-file-commit-panel-inner'),
- ).not.toBeNull();
+ expect(vm.$el.querySelector('.multi-file-commit-panel-inner')).not.toBeNull();
});
it('renders loading icon component', done => {
vm.$store.state.loading = true;
vm.$nextTick(() => {
- expect(
- vm.$el.querySelector('.multi-file-loading-container'),
- ).not.toBeNull();
- expect(
- vm.$el.querySelectorAll('.multi-file-loading-container').length,
- ).toBe(3);
+ expect(vm.$el.querySelector('.multi-file-loading-container')).not.toBeNull();
+ expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toBe(3);
done();
});
});
+
+ describe('activityBarComponent', () => {
+ it('renders tree component', () => {
+ expect(vm.$el.querySelector('.ide-file-list')).not.toBeNull();
+ });
+
+ it('renders commit component', done => {
+ vm.$store.state.currentActivityView = activityBarViews.commit;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.multi-file-commit-panel-section')).not.toBeNull();
+
+ done();
+ });
+ });
+ });
});
diff --git a/spec/javascripts/ide/components/ide_spec.js b/spec/javascripts/ide/components/ide_spec.js
index 7bfcfc90572..6f580e1f7af 100644
--- a/spec/javascripts/ide/components/ide_spec.js
+++ b/spec/javascripts/ide/components/ide_spec.js
@@ -4,6 +4,7 @@ import store from '~/ide/stores';
import ide from '~/ide/components/ide.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { file, resetStore } from '../helpers';
+import { projectData } from '../mock_data';
describe('ide component', () => {
let vm;
@@ -11,6 +12,10 @@ describe('ide component', () => {
beforeEach(() => {
const Component = Vue.extend(ide);
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'master';
+ store.state.projects.abcproject = Object.assign({}, projectData);
+
vm = createComponentWithStore(Component, store, {
emptyStateSvgPath: 'svg',
noChangesStateSvgPath: 'svg',
@@ -24,11 +29,11 @@ describe('ide component', () => {
resetStore(vm.$store);
});
- it('does not render panel right when no files open', () => {
+ it('does not render right right when no files open', () => {
expect(vm.$el.querySelector('.panel-right')).toBeNull();
});
- it('renders panel right when files are open', done => {
+ it('renders right panel when files are open', done => {
vm.$store.state.trees['abcproject/mybranch'] = {
tree: [file()],
};
diff --git a/spec/javascripts/ide/components/ide_status_bar_spec.js b/spec/javascripts/ide/components/ide_status_bar_spec.js
new file mode 100644
index 00000000000..770dca9cb0f
--- /dev/null
+++ b/spec/javascripts/ide/components/ide_status_bar_spec.js
@@ -0,0 +1,63 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import ideStatusBar from '~/ide/components/ide_status_bar.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { resetStore } from '../helpers';
+import { projectData } from '../mock_data';
+
+describe('ideStatusBar', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(ideStatusBar);
+
+ store.state.currentProjectId = 'abcproject';
+ store.state.projects.abcproject = projectData;
+
+ vm = createComponentWithStore(Component, store).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders the statusbar', () => {
+ expect(vm.$el.className).toBe('ide-status-bar');
+ });
+
+ describe('mounted', () => {
+ it('triggers a setInterval', () => {
+ expect(vm.intervalId).not.toBe(null);
+ });
+ });
+
+ describe('commitAgeUpdate', () => {
+ beforeEach(function() {
+ jasmine.clock().install();
+ spyOn(vm, 'commitAgeUpdate').and.callFake(() => {});
+ vm.startTimer();
+ });
+
+ afterEach(function() {
+ jasmine.clock().uninstall();
+ });
+
+ it('gets called every second', () => {
+ expect(vm.commitAgeUpdate).not.toHaveBeenCalled();
+
+ jasmine.clock().tick(1100);
+ expect(vm.commitAgeUpdate.calls.count()).toEqual(1);
+
+ jasmine.clock().tick(1000);
+ expect(vm.commitAgeUpdate.calls.count()).toEqual(2);
+ });
+ });
+
+ describe('getCommitPath', () => {
+ it('returns the path to the commit details', () => {
+ expect(vm.getCommitPath('abc123de')).toBe('/commit/abc123de');
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/ide_tree_list_spec.js b/spec/javascripts/ide/components/ide_tree_list_spec.js
new file mode 100644
index 00000000000..4ecbdb8a55e
--- /dev/null
+++ b/spec/javascripts/ide/components/ide_tree_list_spec.js
@@ -0,0 +1,54 @@
+import Vue from 'vue';
+import IdeTreeList from '~/ide/components/ide_tree_list.vue';
+import store from '~/ide/stores';
+import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
+import { resetStore, file } from '../helpers';
+import { projectData } from '../mock_data';
+
+describe('IDE tree list', () => {
+ const Component = Vue.extend(IdeTreeList);
+ let vm;
+
+ beforeEach(() => {
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'master';
+ store.state.projects.abcproject = Object.assign({}, projectData);
+ Vue.set(store.state.trees, 'abcproject/master', {
+ tree: [file('fileName')],
+ loading: false,
+ });
+
+ vm = createComponentWithStore(Component, store, {
+ viewerType: 'edit',
+ });
+
+ spyOn(vm, 'updateViewer').and.callThrough();
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('updates viewer on mount', () => {
+ expect(vm.updateViewer).toHaveBeenCalledWith('edit');
+ });
+
+ it('renders loading indicator', done => {
+ store.state.trees['abcproject/master'].loading = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.multi-file-loading-container')).not.toBeNull();
+ expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toBe(3);
+
+ done();
+ });
+ });
+
+ it('renders list of files', () => {
+ expect(vm.$el.textContent).toContain('fileName');
+ });
+});
diff --git a/spec/javascripts/ide/components/ide_tree_spec.js b/spec/javascripts/ide/components/ide_tree_spec.js
new file mode 100644
index 00000000000..97a0a2432f1
--- /dev/null
+++ b/spec/javascripts/ide/components/ide_tree_spec.js
@@ -0,0 +1,34 @@
+import Vue from 'vue';
+import IdeTree from '~/ide/components/ide_tree.vue';
+import store from '~/ide/stores';
+import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
+import { resetStore, file } from '../helpers';
+import { projectData } from '../mock_data';
+
+describe('IdeRepoTree', () => {
+ let vm;
+
+ beforeEach(() => {
+ const IdeRepoTree = Vue.extend(IdeTree);
+
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'master';
+ store.state.projects.abcproject = Object.assign({}, projectData);
+ Vue.set(store.state.trees, 'abcproject/master', {
+ tree: [file('fileName')],
+ loading: false,
+ });
+
+ vm = createComponentWithStore(IdeRepoTree, store).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders list of files', () => {
+ expect(vm.$el.textContent).toContain('fileName');
+ });
+});
diff --git a/spec/javascripts/ide/components/repo_commit_section_spec.js b/spec/javascripts/ide/components/repo_commit_section_spec.js
index 768f6e99bf1..5e3e00a180b 100644
--- a/spec/javascripts/ide/components/repo_commit_section_spec.js
+++ b/spec/javascripts/ide/components/repo_commit_section_spec.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
import store from '~/ide/stores';
import service from '~/ide/services';
+import router from '~/ide/ide_router';
import repoCommitSection from '~/ide/components/repo_commit_section.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
-import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper';
import { file, resetStore } from '../helpers';
describe('RepoCommitSection', () => {
@@ -12,10 +12,10 @@ describe('RepoCommitSection', () => {
function createComponent() {
const Component = Vue.extend(repoCommitSection);
- vm = createComponentWithStore(Component, store, {
- noChangesStateSvgPath: 'svg',
- committedStateSvgPath: 'commitsvg',
- });
+ store.state.noChangesStateSvgPath = 'svg';
+ store.state.committedStateSvgPath = 'commitsvg';
+
+ vm = createComponentWithStore(Component, store);
vm.$store.state.currentProjectId = 'abcproject';
vm.$store.state.currentBranchId = 'master';
@@ -60,6 +60,8 @@ describe('RepoCommitSection', () => {
}
beforeEach(done => {
+ spyOn(router, 'push');
+
vm = createComponent();
spyOn(service, 'getTreeData').and.returnValue(
@@ -93,61 +95,49 @@ describe('RepoCommitSection', () => {
resetStore(vm.$store);
const Component = Vue.extend(repoCommitSection);
- vm = createComponentWithStore(Component, store, {
- noChangesStateSvgPath: 'nochangessvg',
- committedStateSvgPath: 'svg',
- }).$mount();
+ store.state.noChangesStateSvgPath = 'nochangessvg';
+ store.state.committedStateSvgPath = 'svg';
- expect(
- vm.$el.querySelector('.js-empty-state').textContent.trim(),
- ).toContain('No changes');
- expect(
- vm.$el.querySelector('.js-empty-state img').getAttribute('src'),
- ).toBe('nochangessvg');
+ vm = createComponentWithStore(Component, store).$mount();
+
+ expect(vm.$el.querySelector('.js-empty-state').textContent.trim()).toContain('No changes');
+ expect(vm.$el.querySelector('.js-empty-state img').getAttribute('src')).toBe('nochangessvg');
});
});
it('renders a commit section', () => {
- const changedFileElements = [
- ...vm.$el.querySelectorAll('.multi-file-commit-list li'),
- ];
- const submitCommit = vm.$el.querySelector('form .btn');
- const allFiles = vm.$store.state.changedFiles.concat(
- vm.$store.state.stagedFiles,
- );
+ const changedFileElements = [...vm.$el.querySelectorAll('.multi-file-commit-list li')];
+ const allFiles = vm.$store.state.changedFiles.concat(vm.$store.state.stagedFiles);
- expect(vm.$el.querySelector('.multi-file-commit-form')).not.toBeNull();
expect(changedFileElements.length).toEqual(4);
changedFileElements.forEach((changedFile, i) => {
expect(changedFile.textContent.trim()).toContain(allFiles[i].path);
});
-
- expect(submitCommit.disabled).toBeTruthy();
- expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeNull();
});
it('adds changed files into staged files', done => {
- vm.$el.querySelector('.ide-staged-action-btn').click();
-
- Vue.nextTick(() => {
- expect(
- vm.$el.querySelector('.ide-commit-list-container').textContent,
- ).toContain('No changes');
-
- done();
- });
+ vm.$el.querySelector('.multi-file-discard-btn .btn').click();
+ vm
+ .$nextTick()
+ .then(() => vm.$el.querySelector('.multi-file-discard-btn .btn').click())
+ .then(vm.$nextTick)
+ .then(() => {
+ expect(vm.$el.querySelector('.ide-commit-list-container').textContent).toContain(
+ 'No changes',
+ );
+ })
+ .then(done)
+ .catch(done.fail);
});
it('stages a single file', done => {
vm.$el.querySelector('.multi-file-discard-btn .btn').click();
Vue.nextTick(() => {
- expect(
- vm.$el
- .querySelector('.ide-commit-list-container')
- .querySelectorAll('li').length,
- ).toBe(1);
+ expect(vm.$el.querySelector('.ide-commit-list-container').querySelectorAll('li').length).toBe(
+ 1,
+ );
done();
});
@@ -157,26 +147,10 @@ describe('RepoCommitSection', () => {
vm.$el.querySelectorAll('.multi-file-discard-btn .btn')[1].click();
Vue.nextTick(() => {
- expect(
- vm.$el.querySelector('.ide-commit-list-container').textContent,
- ).not.toContain('file1');
- expect(
- vm.$el
- .querySelector('.ide-commit-list-container')
- .querySelectorAll('li').length,
- ).toBe(1);
-
- done();
- });
- });
-
- it('removes all staged files', done => {
- vm.$el.querySelectorAll('.ide-staged-action-btn')[1].click();
-
- Vue.nextTick(() => {
- expect(
- vm.$el.querySelectorAll('.ide-commit-list-container')[1].textContent,
- ).toContain('No changes');
+ expect(vm.$el.querySelector('.ide-commit-list-container').textContent).not.toContain('file1');
+ expect(vm.$el.querySelector('.ide-commit-list-container').querySelectorAll('li').length).toBe(
+ 1,
+ );
done();
});
@@ -190,75 +164,17 @@ describe('RepoCommitSection', () => {
Vue.nextTick(() => {
expect(
- vm.$el
- .querySelectorAll('.ide-commit-list-container')[1]
- .querySelectorAll('li').length,
+ vm.$el.querySelectorAll('.ide-commit-list-container')[1].querySelectorAll('li').length,
).toBe(1);
done();
});
});
- it('updates commitMessage in store on input', done => {
- const textarea = vm.$el.querySelector('textarea');
-
- textarea.value = 'testing commit message';
-
- textarea.dispatchEvent(new Event('input'));
-
- getSetTimeoutPromise()
- .then(() => {
- expect(vm.$store.state.commit.commitMessage).toBe(
- 'testing commit message',
- );
- })
- .then(done)
- .catch(done.fail);
- });
-
- describe('discard draft button', () => {
- it('hidden when commitMessage is empty', () => {
- expect(
- vm.$el.querySelector('.multi-file-commit-form .btn-default'),
- ).toBeNull();
- });
-
- it('resets commitMessage when clicking discard button', done => {
- vm.$store.state.commit.commitMessage = 'testing commit message';
-
- getSetTimeoutPromise()
- .then(() => {
- vm.$el.querySelector('.multi-file-commit-form .btn-default').click();
- })
- .then(Vue.nextTick)
- .then(() => {
- expect(vm.$store.state.commit.commitMessage).not.toBe(
- 'testing commit message',
- );
- })
- .then(done)
- .catch(done.fail);
- });
- });
-
- describe('when submitting', () => {
- beforeEach(() => {
- spyOn(vm, 'commitChanges');
- });
-
- it('calls commitChanges', done => {
- vm.$store.state.commit.commitMessage = 'testing commit message';
-
- getSetTimeoutPromise()
- .then(() => {
- vm.$el.querySelector('.multi-file-commit-form .btn-success').click();
- })
- .then(Vue.nextTick)
- .then(() => {
- expect(vm.commitChanges).toHaveBeenCalled();
- })
- .then(done)
- .catch(done.fail);
+ describe('mounted', () => {
+ it('opens last opened file', () => {
+ expect(store.state.openFiles.length).toBe(1);
+ expect(store.state.openFiles[0].pending).toBe(true);
});
});
});
diff --git a/spec/javascripts/ide/components/repo_editor_spec.js b/spec/javascripts/ide/components/repo_editor_spec.js
index b06a6c62a1c..ff500acd849 100644
--- a/spec/javascripts/ide/components/repo_editor_spec.js
+++ b/spec/javascripts/ide/components/repo_editor_spec.js
@@ -5,6 +5,7 @@ import store from '~/ide/stores';
import repoEditor from '~/ide/components/repo_editor.vue';
import monacoLoader from '~/ide/monaco_loader';
import Editor from '~/ide/lib/editor';
+import { activityBarViews } from '~/ide/constants';
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
import setTimeoutPromise from '../../helpers/set_timeout_promise_helper';
import { file, resetStore } from '../helpers';
@@ -23,7 +24,7 @@ describe('RepoEditor', () => {
f.active = true;
f.tempFile = true;
vm.$store.state.openFiles.push(f);
- vm.$store.state.entries[f.path] = f;
+ Vue.set(vm.$store.state.entries, f.path, f);
vm.monaco = true;
vm.$mount();
@@ -214,6 +215,30 @@ describe('RepoEditor', () => {
expect(vm.editor.attachModel).toHaveBeenCalledWith(vm.model);
});
+ it('attaches model to merge request editor', () => {
+ vm.$store.state.viewer = 'mrdiff';
+ vm.file.mrChange = true;
+ spyOn(vm.editor, 'attachMergeRequestModel');
+
+ Editor.editorInstance.modelManager.dispose();
+
+ vm.setupEditor();
+
+ expect(vm.editor.attachMergeRequestModel).toHaveBeenCalledWith(vm.model);
+ });
+
+ it('does not attach model to merge request editor when not a MR change', () => {
+ vm.$store.state.viewer = 'mrdiff';
+ vm.file.mrChange = false;
+ spyOn(vm.editor, 'attachMergeRequestModel');
+
+ Editor.editorInstance.modelManager.dispose();
+
+ vm.setupEditor();
+
+ expect(vm.editor.attachMergeRequestModel).not.toHaveBeenCalledWith(vm.model);
+ });
+
it('adds callback methods', () => {
spyOn(vm.editor, 'onPositionChange').and.callThrough();
@@ -295,4 +320,30 @@ describe('RepoEditor', () => {
});
});
});
+
+ describe('show tabs', () => {
+ it('shows tabs in edit mode', () => {
+ expect(vm.$el.querySelector('.nav-links')).not.toBe(null);
+ });
+
+ it('hides tabs in review mode', done => {
+ vm.$store.state.currentActivityView = activityBarViews.review;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.nav-links')).toBe(null);
+
+ done();
+ });
+ });
+
+ it('hides tabs in commit mode', done => {
+ vm.$store.state.currentActivityView = activityBarViews.commit;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.nav-links')).toBe(null);
+
+ done();
+ });
+ });
+ });
});
diff --git a/spec/javascripts/ide/components/repo_file_spec.js b/spec/javascripts/ide/components/repo_file_spec.js
index 28ff06e1f80..156233653ab 100644
--- a/spec/javascripts/ide/components/repo_file_spec.js
+++ b/spec/javascripts/ide/components/repo_file_spec.js
@@ -73,6 +73,43 @@ describe('RepoFile', () => {
expect(treeChangesEl).not.toBeNull();
expect(treeChangesEl.textContent).toContain('1');
});
+
+ it('renders action dropdown', done => {
+ createComponent({
+ file: {
+ ...file('t4'),
+ type: 'tree',
+ branchId: 'master',
+ projectId: 'project',
+ },
+ level: 0,
+ });
+
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.ide-new-btn')).not.toBeNull();
+
+ done();
+ });
+ });
+
+ it('disables action dropdown', done => {
+ createComponent({
+ file: {
+ ...file('t4'),
+ type: 'tree',
+ branchId: 'master',
+ projectId: 'project',
+ },
+ level: 0,
+ disableActionDropdown: true,
+ });
+
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.ide-new-btn')).toBeNull();
+
+ done();
+ });
+ });
});
describe('locked file', () => {
diff --git a/spec/javascripts/ide/components/repo_tabs_spec.js b/spec/javascripts/ide/components/repo_tabs_spec.js
index cb785ba2cd3..583f71e6121 100644
--- a/spec/javascripts/ide/components/repo_tabs_spec.js
+++ b/spec/javascripts/ide/components/repo_tabs_spec.js
@@ -26,60 +26,10 @@ describe('RepoTabs', () => {
const tabs = [...vm.$el.querySelectorAll('.multi-file-tab')];
expect(tabs.length).toEqual(2);
- expect(tabs[0].classList.contains('active')).toEqual(true);
- expect(tabs[1].classList.contains('active')).toEqual(false);
+ expect(tabs[0].parentNode.classList.contains('active')).toEqual(true);
+ expect(tabs[1].parentNode.classList.contains('active')).toEqual(false);
done();
});
});
-
- describe('updated', () => {
- it('sets showShadow as true when scroll width is larger than width', done => {
- const el = document.createElement('div');
- el.innerHTML = '<div id="test-app"></div>';
- document.body.appendChild(el);
-
- const style = document.createElement('style');
- style.innerText = `
- .multi-file-tabs {
- width: 100px;
- }
-
- .multi-file-tabs .list-unstyled {
- display: flex;
- overflow-x: auto;
- }
- `;
- document.head.appendChild(style);
-
- vm = createComponent(
- RepoTabs,
- {
- files: [],
- viewer: 'editor',
- hasChanges: false,
- activeFile: file('activeFile'),
- hasMergeRequest: false,
- },
- '#test-app',
- );
-
- vm
- .$nextTick()
- .then(() => {
- expect(vm.showShadow).toEqual(false);
-
- vm.files = openedFiles;
- })
- .then(vm.$nextTick)
- .then(() => {
- expect(vm.showShadow).toEqual(true);
-
- style.remove();
- el.remove();
- })
- .then(done)
- .catch(done.fail);
- });
- });
});
diff --git a/spec/javascripts/ide/lib/common/model_spec.js b/spec/javascripts/ide/lib/common/model_spec.js
index 7a6c22b6d27..c278bf92b08 100644
--- a/spec/javascripts/ide/lib/common/model_spec.js
+++ b/spec/javascripts/ide/lib/common/model_spec.js
@@ -28,6 +28,10 @@ describe('Multi-file editor library model', () => {
expect(model.originalModel).not.toBeNull();
expect(model.model).not.toBeNull();
expect(model.baseModel).not.toBeNull();
+
+ expect(model.originalModel.uri.path).toBe('original/path--path');
+ expect(model.model.uri.path).toBe('path--path');
+ expect(model.baseModel.uri.path).toBe('target/path--path');
});
it('creates model with head file to compare against', () => {
diff --git a/spec/javascripts/ide/lib/editor_spec.js b/spec/javascripts/ide/lib/editor_spec.js
index 530bdfa2759..b88a12264ca 100644
--- a/spec/javascripts/ide/lib/editor_spec.js
+++ b/spec/javascripts/ide/lib/editor_spec.js
@@ -74,10 +74,10 @@ describe('Multi-file editor library', () => {
scrollBeyondLastLine: false,
quickSuggestions: false,
occurrencesHighlight: false,
- renderLineHighlight: 'none',
- hideCursorInOverviewRuler: true,
wordWrap: 'on',
renderSideBySide: true,
+ renderLineHighlight: 'all',
+ hideCursorInOverviewRuler: false,
});
});
});
diff --git a/spec/javascripts/ide/mock_data.js b/spec/javascripts/ide/mock_data.js
new file mode 100644
index 00000000000..c059862b9d1
--- /dev/null
+++ b/spec/javascripts/ide/mock_data.js
@@ -0,0 +1,16 @@
+// eslint-disable-next-line import/prefer-default-export
+export const projectData = {
+ id: 1,
+ name: 'abcproject',
+ web_url: '',
+ avatar_url: '',
+ path: '',
+ name_with_namespace: 'namespace/abcproject',
+ branches: {
+ master: {
+ treeId: 'abcproject/master',
+ },
+ },
+ mergeRequests: {},
+ merge_requests_enabled: true,
+};
diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js
index 3ee11bd2f03..7bebc2288e3 100644
--- a/spec/javascripts/ide/stores/actions/file_spec.js
+++ b/spec/javascripts/ide/stores/actions/file_spec.js
@@ -511,7 +511,10 @@ describe('IDE store file actions', () => {
actions.stageChange,
'path',
store.state,
- [{ type: types.STAGE_CHANGE, payload: 'path' }],
+ [
+ { type: types.STAGE_CHANGE, payload: 'path' },
+ { type: types.SET_LAST_COMMIT_MSG, payload: '' },
+ ],
[],
done,
);
@@ -524,7 +527,10 @@ describe('IDE store file actions', () => {
actions.unstageChange,
'path',
store.state,
- [{ type: types.UNSTAGE_CHANGE, payload: 'path' }],
+ [
+ { type: types.UNSTAGE_CHANGE, payload: 'path' },
+ { type: types.SET_LAST_COMMIT_MSG, payload: '' },
+ ],
[],
done,
);
@@ -563,6 +569,22 @@ describe('IDE store file actions', () => {
.catch(done.fail);
});
+ it('returns false when already opened', done => {
+ store.state.openFiles.push({
+ ...f,
+ active: true,
+ key: `pending-${f.key}`,
+ });
+
+ store
+ .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' })
+ .then(added => {
+ expect(added).toBe(false);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
it('pushes router URL when added', done => {
store.state.currentBranchId = 'master';
@@ -589,20 +611,6 @@ describe('IDE store file actions', () => {
.then(done)
.catch(done.fail);
});
-
- it('returns false when passed in file is active & viewer is diff', done => {
- f.active = true;
- store.state.openFiles.push(f);
- store.state.viewer = 'diff';
-
- store
- .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' })
- .then(added => {
- expect(added).toBe(false);
- })
- .then(done)
- .catch(done.fail);
- });
});
describe('removePendingTab', () => {
diff --git a/spec/javascripts/ide/stores/actions/project_spec.js b/spec/javascripts/ide/stores/actions/project_spec.js
new file mode 100644
index 00000000000..ebd08d95810
--- /dev/null
+++ b/spec/javascripts/ide/stores/actions/project_spec.js
@@ -0,0 +1,71 @@
+import {
+ refreshLastCommitData,
+} from '~/ide/stores/actions';
+import store from '~/ide/stores';
+import service from '~/ide/services';
+import { resetStore } from '../../helpers';
+import testAction from '../../../helpers/vuex_action_helper';
+
+describe('IDE store project actions', () => {
+ beforeEach(() => {
+ store.state.projects.abcproject = {};
+ });
+
+ afterEach(() => {
+ resetStore(store);
+ });
+
+ describe('refreshLastCommitData', () => {
+ beforeEach(() => {
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'master';
+ store.state.projects.abcproject = {
+ branches: {
+ master: {
+ commit: null,
+ },
+ },
+ };
+ });
+
+ it('calls the service', done => {
+ spyOn(service, 'getBranchData').and.returnValue(
+ Promise.resolve({
+ data: {
+ commit: { id: '123' },
+ },
+ }),
+ );
+
+ store
+ .dispatch('refreshLastCommitData', {
+ projectId: store.state.currentProjectId,
+ branchId: store.state.currentBranchId,
+ })
+ .then(() => {
+ expect(service.getBranchData).toHaveBeenCalledWith('abcproject', 'master');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('commits getBranchData', done => {
+ testAction(
+ refreshLastCommitData,
+ {},
+ {},
+ [{
+ type: 'SET_BRANCH_COMMIT',
+ payload: {
+ projectId: 'abcproject',
+ branchId: 'master',
+ commit: { id: '123' },
+ },
+ }], // mutations
+ [], // action
+ done,
+ );
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js
index a64af5b941b..062c3497623 100644
--- a/spec/javascripts/ide/stores/actions_spec.js
+++ b/spec/javascripts/ide/stores/actions_spec.js
@@ -2,6 +2,9 @@ import actions, {
stageAllChanges,
unstageAllChanges,
toggleFileFinder,
+ setCurrentBranchId,
+ setEmptyStateSvgs,
+ updateActivityBarView,
updateTempFlagForEntry,
} from '~/ide/stores/actions';
import store from '~/ide/stores';
@@ -306,6 +309,7 @@ describe('Multi-file store actions', () => {
null,
store.state,
[
+ { type: types.SET_LAST_COMMIT_MSG, payload: '' },
{ type: types.STAGE_CHANGE, payload: store.state.changedFiles[0].path },
{ type: types.STAGE_CHANGE, payload: store.state.changedFiles[1].path },
],
@@ -345,6 +349,32 @@ describe('Multi-file store actions', () => {
});
});
+ describe('updateActivityBarView', () => {
+ it('commits UPDATE_ACTIVITY_BAR_VIEW', done => {
+ testAction(
+ updateActivityBarView,
+ 'test',
+ {},
+ [{ type: 'UPDATE_ACTIVITY_BAR_VIEW', payload: 'test' }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('setEmptyStateSvgs', () => {
+ it('commits setEmptyStateSvgs', done => {
+ testAction(
+ setEmptyStateSvgs,
+ 'svg',
+ {},
+ [{ type: 'SET_EMPTY_STATE_SVGS', payload: 'svg' }],
+ [],
+ done,
+ );
+ });
+ });
+
describe('updateTempFlagForEntry', () => {
it('commits UPDATE_TEMP_FLAG', done => {
const f = {
@@ -388,6 +418,19 @@ describe('Multi-file store actions', () => {
});
});
+ describe('setCurrentBranchId', () => {
+ it('commits setCurrentBranchId', done => {
+ testAction(
+ setCurrentBranchId,
+ 'branchId',
+ {},
+ [{ type: 'SET_CURRENT_BRANCH', payload: 'branchId' }],
+ [],
+ done,
+ );
+ });
+ });
+
describe('toggleFileFinder', () => {
it('commits TOGGLE_FILE_FINDER', done => {
testAction(
diff --git a/spec/javascripts/ide/stores/getters_spec.js b/spec/javascripts/ide/stores/getters_spec.js
index bd834443730..4833ba3edfd 100644
--- a/spec/javascripts/ide/stores/getters_spec.js
+++ b/spec/javascripts/ide/stores/getters_spec.js
@@ -37,12 +37,6 @@ describe('IDE store getters', () => {
expect(modifiedFiles.length).toBe(1);
expect(modifiedFiles[0].name).toBe('changed');
});
-
- it('returns angle left when collapsed', () => {
- localState.rightPanelCollapsed = true;
-
- expect(getters.collapseButtonIcon(localState)).toBe('angle-double-left');
- });
});
describe('currentMergeRequest', () => {
@@ -147,4 +141,24 @@ describe('IDE store getters', () => {
expect(getters.getChangesInFolder(localState)('test')).toBe(2);
});
});
+
+ describe('lastCommit', () => {
+ it('returns the last commit of the current branch on the current project', () => {
+ const commitTitle = 'Example commit title';
+ const localGetters = {
+ currentProject: {
+ branches: {
+ 'example-branch': {
+ commit: {
+ title: commitTitle,
+ },
+ },
+ },
+ },
+ };
+ localState.currentBranchId = 'example-branch';
+
+ expect(getters.lastCommit(localState, localGetters).title).toBe(commitTitle);
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
index b2b4b85ca42..a2869ff378b 100644
--- a/spec/javascripts/ide/stores/modules/commit/actions_spec.js
+++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
@@ -289,21 +289,6 @@ describe('IDE commit module actions', () => {
.then(done)
.catch(done.fail);
});
-
- it('pushes route to new branch if commitAction is new branch', done => {
- store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH;
-
- store
- .dispatch('commit/updateFilesAfterCommit', {
- data,
- branch,
- })
- .then(() => {
- expect(router.push).toHaveBeenCalledWith(`/project/abcproject/blob/master/${f.path}`);
- })
- .then(done)
- .catch(done.fail);
- });
});
describe('commitChanges', () => {
@@ -391,21 +376,6 @@ describe('IDE commit module actions', () => {
.catch(done.fail);
});
- it('pushes router to new route', done => {
- store
- .dispatch('commit/commitChanges')
- .then(() => {
- expect(router.push).toHaveBeenCalledWith(
- `/project/${store.state.currentProjectId}/blob/${
- store.getters['commit/newBranchName']
- }/changed`,
- );
-
- done();
- })
- .catch(done.fail);
- });
-
it('sets last Commit Msg', done => {
store
.dispatch('commit/commitChanges')
diff --git a/spec/javascripts/ide/stores/mutations/branch_spec.js b/spec/javascripts/ide/stores/mutations/branch_spec.js
index a7167537ef2..29eb859ddaf 100644
--- a/spec/javascripts/ide/stores/mutations/branch_spec.js
+++ b/spec/javascripts/ide/stores/mutations/branch_spec.js
@@ -15,4 +15,26 @@ describe('Multi-file store branch mutations', () => {
expect(localState.currentBranchId).toBe('master');
});
});
+
+ describe('SET_BRANCH_COMMIT', () => {
+ it('sets the last commit on current project', () => {
+ localState.projects = {
+ Example: {
+ branches: {
+ master: {},
+ },
+ },
+ };
+
+ mutations.SET_BRANCH_COMMIT(localState, {
+ projectId: 'Example',
+ branchId: 'master',
+ commit: {
+ title: 'Example commit',
+ },
+ });
+
+ expect(localState.projects.Example.branches.master.commit.title).toBe('Example commit');
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/mutations/file_spec.js b/spec/javascripts/ide/stores/mutations/file_spec.js
index 6fba934810d..e83961fcedc 100644
--- a/spec/javascripts/ide/stores/mutations/file_spec.js
+++ b/spec/javascripts/ide/stores/mutations/file_spec.js
@@ -267,41 +267,23 @@ describe('IDE store file mutations', () => {
it('adds file into openFiles as pending', () => {
mutations.ADD_PENDING_TAB(localState, { file: localFile });
- expect(localState.openFiles.length).toBe(2);
- expect(localState.openFiles[1].pending).toBe(true);
- expect(localState.openFiles[1].key).toBe(`pending-${localFile.key}`);
- });
-
- it('updates open file to pending', () => {
- mutations.ADD_PENDING_TAB(localState, { file: localState.openFiles[0] });
-
expect(localState.openFiles.length).toBe(1);
+ expect(localState.openFiles[0].pending).toBe(true);
+ expect(localState.openFiles[0].key).toBe(`pending-${localFile.key}`);
});
- it('updates pending open file to active', () => {
- localState.openFiles.push({
- ...localFile,
- pending: true,
- });
+ it('only allows 1 open pending file', () => {
+ const newFile = file('test');
+ localState.entries[newFile.path] = newFile;
mutations.ADD_PENDING_TAB(localState, { file: localFile });
- expect(localState.openFiles[1].pending).toBe(true);
- expect(localState.openFiles[1].active).toBe(true);
- });
-
- it('sets all openFiles to not active', () => {
- mutations.ADD_PENDING_TAB(localState, { file: localFile });
+ expect(localState.openFiles.length).toBe(1);
- expect(localState.openFiles.length).toBe(2);
+ mutations.ADD_PENDING_TAB(localState, { file: file('test') });
- localState.openFiles.forEach(f => {
- if (f.pending) {
- expect(f.active).toBe(true);
- } else {
- expect(f.active).toBe(false);
- }
- });
+ expect(localState.openFiles.length).toBe(1);
+ expect(localState.openFiles[0].name).toBe('test');
});
});
diff --git a/spec/javascripts/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js
index 61efb6372c9..972713c5ad2 100644
--- a/spec/javascripts/ide/stores/mutations_spec.js
+++ b/spec/javascripts/ide/stores/mutations_spec.js
@@ -87,6 +87,28 @@ describe('Multi-file store mutations', () => {
});
});
+ describe('UPDATE_ACTIVITY_BAR_VIEW', () => {
+ it('updates currentActivityBar', () => {
+ mutations.UPDATE_ACTIVITY_BAR_VIEW(localState, 'test');
+
+ expect(localState.currentActivityView).toBe('test');
+ });
+ });
+
+ describe('SET_EMPTY_STATE_SVGS', () => {
+ it('updates empty state SVGs', () => {
+ mutations.SET_EMPTY_STATE_SVGS(localState, {
+ emptyStateSvgPath: 'emptyState',
+ noChangesStateSvgPath: 'noChanges',
+ committedStateSvgPath: 'commited',
+ });
+
+ expect(localState.emptyStateSvgPath).toBe('emptyState');
+ expect(localState.noChangesStateSvgPath).toBe('noChanges');
+ expect(localState.committedStateSvgPath).toBe('commited');
+ });
+ });
+
describe('UPDATE_TEMP_FLAG', () => {
beforeEach(() => {
localState.entries.test = {
diff --git a/spec/javascripts/monitoring/graph/flag_spec.js b/spec/javascripts/monitoring/graph/flag_spec.js
index 2d474e9092f..19278312b6d 100644
--- a/spec/javascripts/monitoring/graph/flag_spec.js
+++ b/spec/javascripts/monitoring/graph/flag_spec.js
@@ -22,15 +22,20 @@ const defaultValuesComponent = {
graphHeightOffset: 120,
showFlagContent: true,
realPixelRatio: 1,
- timeSeries: [{
- values: [{
- time: new Date('2017-06-04T18:17:33.501Z'),
- value: '1.49609375',
- }],
- }],
+ timeSeries: [
+ {
+ values: [
+ {
+ time: new Date('2017-06-04T18:17:33.501Z'),
+ value: '1.49609375',
+ },
+ ],
+ },
+ ],
unitOfDisplay: 'ms',
currentDataIndex: 0,
legendTitle: 'Average',
+ currentCoordinates: [],
};
const deploymentFlagData = {
@@ -113,7 +118,7 @@ describe('GraphFlag', () => {
});
it('formatDate', () => {
- expect(component.formatDate).toEqual('Sun, Jun 4');
+ expect(component.formatDate).toEqual('04 Jun 2017, ');
});
it('cursorStyle', () => {
diff --git a/spec/javascripts/monitoring/graph/track_line_spec.js b/spec/javascripts/monitoring/graph/track_line_spec.js
index 45106830a67..27602a861eb 100644
--- a/spec/javascripts/monitoring/graph/track_line_spec.js
+++ b/spec/javascripts/monitoring/graph/track_line_spec.js
@@ -39,14 +39,14 @@ describe('TrackLine component', () => {
const svgEl = vm.$el.querySelector('svg');
const lineEl = vm.$el.querySelector('svg line');
- expect(svgEl.getAttribute('width')).toEqual('15');
- expect(svgEl.getAttribute('height')).toEqual('6');
+ expect(svgEl.getAttribute('width')).toEqual('16');
+ expect(svgEl.getAttribute('height')).toEqual('8');
expect(lineEl.getAttribute('stroke-width')).toEqual('4');
expect(lineEl.getAttribute('x1')).toEqual('0');
- expect(lineEl.getAttribute('x2')).toEqual('15');
- expect(lineEl.getAttribute('y1')).toEqual('2');
- expect(lineEl.getAttribute('y2')).toEqual('2');
+ expect(lineEl.getAttribute('x2')).toEqual('16');
+ expect(lineEl.getAttribute('y1')).toEqual('4');
+ expect(lineEl.getAttribute('y2')).toEqual('4');
});
});
});
diff --git a/spec/javascripts/monitoring/graph_path_spec.js b/spec/javascripts/monitoring/graph_path_spec.js
index c83bd19345f..2515e2ad897 100644
--- a/spec/javascripts/monitoring/graph_path_spec.js
+++ b/spec/javascripts/monitoring/graph_path_spec.js
@@ -23,6 +23,7 @@ describe('Monitoring Paths', () => {
generatedAreaPath: firstTimeSeries.areaPath,
lineColor: firstTimeSeries.lineColor,
areaColor: firstTimeSeries.areaColor,
+ showDot: false,
});
const metricArea = component.$el.querySelector('.metric-area');
const metricLine = component.$el.querySelector('.metric-line');
@@ -40,6 +41,7 @@ describe('Monitoring Paths', () => {
generatedAreaPath: firstTimeSeries.areaPath,
lineColor: firstTimeSeries.lineColor,
areaColor: firstTimeSeries.areaColor,
+ showDot: false,
});
component.lineStyle = 'dashed';
diff --git a/spec/javascripts/monitoring/graph_spec.js b/spec/javascripts/monitoring/graph_spec.js
index 1213c80ba3a..220228e5c08 100644
--- a/spec/javascripts/monitoring/graph_spec.js
+++ b/spec/javascripts/monitoring/graph_spec.js
@@ -30,7 +30,6 @@ describe('Graph', () => {
it('has a title', () => {
const component = createComponent({
graphData: convertedMetrics[1],
- classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
tagsPath,
@@ -46,7 +45,6 @@ describe('Graph', () => {
it('axisTransform translates an element Y position depending of its height', () => {
const component = createComponent({
graphData: convertedMetrics[1],
- classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
tagsPath,
@@ -62,7 +60,6 @@ describe('Graph', () => {
it('outerViewBox gets a width and height property based on the DOM size of the element', () => {
const component = createComponent({
graphData: convertedMetrics[1],
- classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
tagsPath,
@@ -79,7 +76,6 @@ describe('Graph', () => {
it('sends an event to the eventhub when it has finished resizing', done => {
const component = createComponent({
graphData: convertedMetrics[1],
- classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
tagsPath,
@@ -97,7 +93,6 @@ describe('Graph', () => {
it('has a title for the y-axis and the chart legend that comes from the backend', () => {
const component = createComponent({
graphData: convertedMetrics[1],
- classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
tagsPath,
@@ -111,7 +106,6 @@ describe('Graph', () => {
it('sets the currentData object based on the hovered data index', () => {
const component = createComponent({
graphData: convertedMetrics[1],
- classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
graphIdentifier: 0,
@@ -125,6 +119,5 @@ describe('Graph', () => {
component.positionFlag();
expect(component.currentData).toBe(component.timeSeries[0].values[10]);
- expect(component.currentDataIndex).toEqual(10);
});
});
diff --git a/spec/javascripts/pipelines/async_button_spec.js b/spec/javascripts/pipelines/async_button_spec.js
deleted file mode 100644
index e0ea3649646..00000000000
--- a/spec/javascripts/pipelines/async_button_spec.js
+++ /dev/null
@@ -1,62 +0,0 @@
-import Vue from 'vue';
-import asyncButtonComp from '~/pipelines/components/async_button.vue';
-import eventHub from '~/pipelines/event_hub';
-
-describe('Pipelines Async Button', () => {
- let component;
- let AsyncButtonComponent;
-
- beforeEach(() => {
- AsyncButtonComponent = Vue.extend(asyncButtonComp);
-
- component = new AsyncButtonComponent({
- propsData: {
- endpoint: '/foo',
- title: 'Foo',
- icon: 'repeat',
- cssClass: 'bar',
- pipelineId: 123,
- type: 'explode',
- },
- }).$mount();
- });
-
- it('should render a button', () => {
- expect(component.$el.tagName).toEqual('BUTTON');
- });
-
- it('should render svg icon', () => {
- expect(component.$el.querySelector('svg')).not.toBeNull();
- });
-
- it('should render the provided title', () => {
- expect(component.$el.getAttribute('data-original-title')).toContain('Foo');
- expect(component.$el.getAttribute('aria-label')).toContain('Foo');
- });
-
- it('should render the provided cssClass', () => {
- expect(component.$el.getAttribute('class')).toContain('bar');
- });
-
- describe('With confirm dialog', () => {
- it('should call the service when confimation is positive', () => {
- eventHub.$on('openConfirmationModal', (data) => {
- expect(data.pipelineId).toEqual(123);
- expect(data.type).toEqual('explode');
- });
-
- component = new AsyncButtonComponent({
- propsData: {
- endpoint: '/foo',
- title: 'Foo',
- icon: 'fa fa-foo',
- cssClass: 'bar',
- pipelineId: 123,
- type: 'explode',
- },
- }).$mount();
-
- component.$el.click();
- });
- });
-});
diff --git a/spec/javascripts/pipelines/graph/action_component_spec.js b/spec/javascripts/pipelines/graph/action_component_spec.js
index 3de10392472..d646bef96f5 100644
--- a/spec/javascripts/pipelines/graph/action_component_spec.js
+++ b/spec/javascripts/pipelines/graph/action_component_spec.js
@@ -22,7 +22,7 @@ describe('pipeline graph action component', () => {
});
it('should emit an event with the provided link', () => {
- eventHub.$on('graphAction', link => {
+ eventHub.$on('postAction', link => {
expect(link).toEqual('foo');
});
});
diff --git a/spec/javascripts/pipelines/mock_data.js b/spec/javascripts/pipelines/mock_data.js
index 59092e0f041..a5a200973d7 100644
--- a/spec/javascripts/pipelines/mock_data.js
+++ b/spec/javascripts/pipelines/mock_data.js
@@ -321,6 +321,103 @@ export const pipelineWithStages = {
};
export const stageReply = {
- html:
- '\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="karma - failed \u0026lt;br\u0026gt; (script failure)" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62402048"\u003e\u003cspan class="ci-status-icon ci-status-icon-failed"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_failed"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003ekarma\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62402048/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="codequality - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398081"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003ecodequality\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398081/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="db:check-schema-pg - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398066"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003edb:check-schema-pg\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398066/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="db:migrate:reset-mysql - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398065"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003edb:migrate:reset-mysql\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398065/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="db:migrate:reset-pg - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398064"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003edb:migrate:reset-pg\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398064/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="db:rollback-mysql - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398070"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003edb:rollback-mysql\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398070/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="db:rollback-pg - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398069"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003edb:rollback-pg\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398069/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="dependency_scanning - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398083"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003edependency_scanning\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398083/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="docs lint - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398061"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003edocs lint\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398061/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="downtime_check - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398062"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003edowntime_check\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398062/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="ee_compat_check - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398063"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003eee_compat_check\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398063/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="gitlab:assets:compile - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398075"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003egitlab:assets:compile\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398075/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="gitlab:setup-mysql - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398073"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003egitlab:setup-mysql\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398073/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="gitlab:setup-pg - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398071"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003egitlab:setup-pg\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398071/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="gitlab_git_test - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398086"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003egitlab_git_test\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398086/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="migration:path-mysql - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398068"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003emigration:path-mysql\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398068/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="migration:path-pg - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398067"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003emigration:path-pg\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398067/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="qa:internal - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398084"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003eqa:internal\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398084/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="qa:selectors - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398085"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003eqa:selectors\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398085/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 0 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398020"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 0 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398020/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 1 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398022"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 1 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398022/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 10 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398033"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 10 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398033/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 11 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398034"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 11 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398034/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 12 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398035"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 12 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398035/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 13 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398036"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 13 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398036/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 14 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398037"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 14 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398037/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 15 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398038"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 15 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398038/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 16 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398039"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 16 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398039/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 17 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398040"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 17 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398040/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 18 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398041"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 18 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398041/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 19 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398042"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 19 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398042/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 2 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398024"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 2 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398024/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 20 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398043"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 20 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398043/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 21 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398044"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 21 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398044/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 22 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398046"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 22 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398046/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 23 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398047"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 23 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398047/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 24 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398048"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 24 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398048/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 25 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398049"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 25 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398049/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 26 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398050"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 26 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398050/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 27 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398051"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 27 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398051/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 3 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398025"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 3 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398025/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 4 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398027"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 4 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398027/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 5 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398028"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 5 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398028/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 6 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398029"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 6 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398029/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 7 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398030"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 7 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398030/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 8 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398031"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 8 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398031/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 9 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398032"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 9 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398032/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 0 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397981"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 0 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397981/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 1 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397985"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 1 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397985/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 10 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398000"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 10 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398000/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 11 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398001"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 11 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398001/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 12 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398002"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 12 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398002/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 13 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398003"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 13 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398003/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 14 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398004"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 14 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398004/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 15 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398006"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 15 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398006/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 16 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398007"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 16 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398007/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 17 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398008"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 17 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398008/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 18 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398009"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 18 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398009/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 19 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398010"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 19 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398010/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 2 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397986"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 2 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397986/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 20 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398012"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 20 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398012/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 21 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398013"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 21 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398013/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 22 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398014"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 22 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398014/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 23 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398015"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 23 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398015/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 24 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398016"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 24 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398016/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 25 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398017"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 25 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398017/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 26 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398018"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 26 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398018/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 27 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398019"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 27 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398019/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 3 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397988"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 3 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397988/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 4 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397989"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 4 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397989/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 5 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397991"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 5 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397991/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 6 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397993"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 6 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397993/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 7 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397994"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 7 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397994/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 8 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397995"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 8 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397995/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 9 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397996"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 9 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397996/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="sast - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398082"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003esast\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398082/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="spinach-mysql 0 2 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398058"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003espinach-mysql 0 2\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398058/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="spinach-mysql 1 2 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398059"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003espinach-mysql 1 2\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398059/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="spinach-pg 0 2 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398053"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003espinach-pg 0 2\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398053/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="spinach-pg 1 2 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398056"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003espinach-pg 1 2\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398056/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="static-analysis - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398060"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003estatic-analysis\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398060/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n',
+ name: 'deploy',
+ title: 'deploy: running',
+ latest_statuses: [
+ {
+ id: 928,
+ name: 'stop staging',
+ started: false,
+ build_path: '/twitter/flight/-/jobs/928',
+ cancel_path: '/twitter/flight/-/jobs/928/cancel',
+ playable: false,
+ created_at: '2018-04-04T20:02:02.728Z',
+ updated_at: '2018-04-04T20:02:02.766Z',
+ status: {
+ icon: 'status_pending',
+ text: 'pending',
+ label: 'pending',
+ group: 'pending',
+ tooltip: 'pending',
+ has_details: true,
+ details_path: '/twitter/flight/-/jobs/928',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_pending-db32e1faf94b9f89530ac519790920d1f18ea8f6af6cd2e0a26cd6840cacf101.ico',
+ action: {
+ icon: 'cancel',
+ title: 'Cancel',
+ path: '/twitter/flight/-/jobs/928/cancel',
+ method: 'post',
+ },
+ },
+ },
+ {
+ id: 926,
+ name: 'production',
+ started: false,
+ build_path: '/twitter/flight/-/jobs/926',
+ retry_path: '/twitter/flight/-/jobs/926/retry',
+ play_path: '/twitter/flight/-/jobs/926/play',
+ playable: true,
+ created_at: '2018-04-04T20:00:57.202Z',
+ updated_at: '2018-04-04T20:11:13.110Z',
+ status: {
+ icon: 'status_canceled',
+ text: 'canceled',
+ label: 'manual play action',
+ group: 'canceled',
+ tooltip: 'canceled',
+ has_details: true,
+ details_path: '/twitter/flight/-/jobs/926',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_canceled-5491840b9b6feafba0bc599cbd49ee9580321dc809683856cf1b0d51532b1af6.ico',
+ action: {
+ icon: 'play',
+ title: 'Play',
+ path: '/twitter/flight/-/jobs/926/play',
+ method: 'post',
+ },
+ },
+ },
+ {
+ id: 217,
+ name: 'staging',
+ started: '2018-03-07T08:41:46.234Z',
+ build_path: '/twitter/flight/-/jobs/217',
+ retry_path: '/twitter/flight/-/jobs/217/retry',
+ playable: false,
+ created_at: '2018-03-07T14:41:58.093Z',
+ updated_at: '2018-03-07T14:41:58.093Z',
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ tooltip: 'passed',
+ has_details: true,
+ details_path: '/twitter/flight/-/jobs/217',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+ action: {
+ icon: 'retry',
+ title: 'Retry',
+ path: '/twitter/flight/-/jobs/217/retry',
+ method: 'post',
+ },
+ },
+ },
+ ],
+ status: {
+ icon: 'status_running',
+ text: 'running',
+ label: 'running',
+ group: 'running',
+ tooltip: 'running',
+ has_details: true,
+ details_path: '/twitter/flight/pipelines/13#deploy',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico',
+ },
+ path: '/twitter/flight/pipelines/13#deploy',
+ dropdown_path: '/twitter/flight/pipelines/13/stage.json?stage=deploy',
};
diff --git a/spec/javascripts/pipelines/pipelines_table_row_spec.js b/spec/javascripts/pipelines/pipelines_table_row_spec.js
index de744739e42..05ca4cb9044 100644
--- a/spec/javascripts/pipelines/pipelines_table_row_spec.js
+++ b/spec/javascripts/pipelines/pipelines_table_row_spec.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import tableRowComp from '~/pipelines/components/pipelines_table_row.vue';
+import eventHub from '~/pipelines/event_hub';
describe('Pipelines Table Row', () => {
const jsonFixtureName = 'pipelines/pipelines.json';
@@ -151,13 +152,37 @@ describe('Pipelines Table Row', () => {
describe('actions column', () => {
beforeEach(() => {
- component = buildComponent(pipeline);
+ const withActions = Object.assign({}, pipeline);
+ withActions.flags.cancelable = true;
+ withActions.flags.retryable = true;
+ withActions.cancel_path = '/cancel';
+ withActions.retry_path = '/retry';
+
+ component = buildComponent(withActions);
});
it('should render the provided actions', () => {
- expect(
- component.$el.querySelectorAll('.table-section:nth-child(6) ul li').length,
- ).toEqual(pipeline.details.manual_actions.length);
+ expect(component.$el.querySelector('.js-pipelines-retry-button')).not.toBeNull();
+ expect(component.$el.querySelector('.js-pipelines-cancel-button')).not.toBeNull();
+ });
+
+ it('emits `retryPipeline` event when retry button is clicked and toggles loading', () => {
+ eventHub.$on('retryPipeline', (endpoint) => {
+ expect(endpoint).toEqual('/retry');
+ });
+
+ component.$el.querySelector('.js-pipelines-retry-button').click();
+ expect(component.isRetrying).toEqual(true);
+ });
+
+ it('emits `openConfirmationModal` event when cancel button is clicked and toggles loading', () => {
+ eventHub.$on('openConfirmationModal', (data) => {
+ expect(data.endpoint).toEqual('/cancel');
+ expect(data.pipelineId).toEqual(pipeline.id);
+ });
+
+ component.$el.querySelector('.js-pipelines-cancel-button').click();
+ expect(component.isCancelling).toEqual(true);
});
});
});
diff --git a/spec/javascripts/pipelines/stage_spec.js b/spec/javascripts/pipelines/stage_spec.js
index be1632e7206..75156e7bdfd 100644
--- a/spec/javascripts/pipelines/stage_spec.js
+++ b/spec/javascripts/pipelines/stage_spec.js
@@ -4,6 +4,7 @@ import axios from '~/lib/utils/axios_utils';
import stage from '~/pipelines/components/stage.vue';
import eventHub from '~/pipelines/event_hub';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { stageReply } from './mock_data';
describe('Pipelines stage component', () => {
let StageComponent;
@@ -41,7 +42,7 @@ describe('Pipelines stage component', () => {
describe('with successfull request', () => {
beforeEach(() => {
- mock.onGet('path.json').reply(200, { html: 'foo' });
+ mock.onGet('path.json').reply(200, stageReply);
});
it('should render the received data and emit `clickedDropdown` event', done => {
@@ -51,7 +52,7 @@ describe('Pipelines stage component', () => {
setTimeout(() => {
expect(
component.$el.querySelector('.js-builds-dropdown-container ul').textContent.trim(),
- ).toEqual('foo');
+ ).toContain(stageReply.latest_statuses[0].name);
expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown');
done();
}, 0);
@@ -74,7 +75,9 @@ describe('Pipelines stage component', () => {
describe('update endpoint correctly', () => {
beforeEach(() => {
- mock.onGet('bar.json').reply(200, { html: 'this is the updated content' });
+ const copyStage = Object.assign({}, stageReply);
+ copyStage.latest_statuses[0].name = 'this is the updated content';
+ mock.onGet('bar.json').reply(200, copyStage);
});
it('should update the stage to request the new endpoint provided', done => {
@@ -93,7 +96,7 @@ describe('Pipelines stage component', () => {
setTimeout(() => {
expect(
component.$el.querySelector('.js-builds-dropdown-container ul').textContent.trim(),
- ).toEqual('this is the updated content');
+ ).toContain('this is the updated content');
done();
});
});
diff --git a/spec/javascripts/projects_dropdown/components/app_spec.js b/spec/javascripts/projects_dropdown/components/app_spec.js
index 2054fef790b..38b31c3d727 100644
--- a/spec/javascripts/projects_dropdown/components/app_spec.js
+++ b/spec/javascripts/projects_dropdown/components/app_spec.js
@@ -23,17 +23,18 @@ const createComponent = () => {
});
};
-const returnServicePromise = (data, failed) => new Promise((resolve, reject) => {
- if (failed) {
- reject(data);
- } else {
- resolve({
- json() {
- return data;
- },
- });
- }
-});
+const returnServicePromise = (data, failed) =>
+ new Promise((resolve, reject) => {
+ if (failed) {
+ reject(data);
+ } else {
+ resolve({
+ json() {
+ return data;
+ },
+ });
+ }
+ });
describe('AppComponent', () => {
describe('computed', () => {
@@ -185,7 +186,7 @@ describe('AppComponent', () => {
describe('fetchSearchedProjects', () => {
const searchQuery = 'test';
- it('should perform search with provided search query', (done) => {
+ it('should perform search with provided search query', done => {
const mockData = [mockRawProject];
spyOn(vm, 'toggleLoader');
spyOn(vm, 'toggleSearchProjectsList');
@@ -203,7 +204,7 @@ describe('AppComponent', () => {
}, 0);
});
- it('should update props for showing search failure', (done) => {
+ it('should update props for showing search failure', done => {
spyOn(vm, 'toggleSearchProjectsList');
spyOn(vm.service, 'getSearchedProjects').and.returnValue(returnServicePromise({}, true));
@@ -219,7 +220,7 @@ describe('AppComponent', () => {
});
describe('logCurrentProjectAccess', () => {
- it('should log current project access via service', (done) => {
+ it('should log current project access via service', done => {
spyOn(vm.service, 'logProjectAccess');
vm.currentProject = mockProject;
@@ -257,7 +258,7 @@ describe('AppComponent', () => {
});
describe('created', () => {
- it('should bind event listeners on eventHub', (done) => {
+ it('should bind event listeners on eventHub', done => {
spyOn(eventHub, '$on');
createComponent().$mount();
@@ -273,7 +274,7 @@ describe('AppComponent', () => {
});
describe('beforeDestroy', () => {
- it('should unbind event listeners on eventHub', (done) => {
+ it('should unbind event listeners on eventHub', done => {
const vm = createComponent();
spyOn(eventHub, '$off');
@@ -305,7 +306,7 @@ describe('AppComponent', () => {
expect(vm.$el.querySelector('.search-input-container')).toBeDefined();
});
- it('should render loading animation', (done) => {
+ it('should render loading animation', done => {
vm.toggleLoader(true);
Vue.nextTick(() => {
const loadingEl = vm.$el.querySelector('.loading-animation');
@@ -317,7 +318,7 @@ describe('AppComponent', () => {
});
});
- it('should render frequent projects list header', (done) => {
+ it('should render frequent projects list header', done => {
vm.toggleFrequentProjectsList(true);
Vue.nextTick(() => {
const sectionHeaderEl = vm.$el.querySelector('.section-header');
@@ -328,7 +329,7 @@ describe('AppComponent', () => {
});
});
- it('should render frequent projects list', (done) => {
+ it('should render frequent projects list', done => {
vm.toggleFrequentProjectsList(true);
Vue.nextTick(() => {
expect(vm.$el.querySelector('.projects-list-frequent-container')).toBeDefined();
@@ -336,7 +337,7 @@ describe('AppComponent', () => {
});
});
- it('should render searched projects list', (done) => {
+ it('should render searched projects list', done => {
vm.toggleSearchProjectsList(true);
Vue.nextTick(() => {
expect(vm.$el.querySelector('.section-header')).toBe(null);
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_maintainer_edit_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_maintainer_edit_spec.js
deleted file mode 100644
index cee22d5342a..00000000000
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_maintainer_edit_spec.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import Vue from 'vue';
-import maintainerEditComponent from '~/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-describe('RWidgetMaintainerEdit', () => {
- let Component;
- let vm;
-
- beforeEach(() => {
- Component = Vue.extend(maintainerEditComponent);
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('when a maintainer is allowed to edit', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- maintainerEditAllowed: true,
- });
- });
-
- it('it renders the message', () => {
- expect(vm.$el.textContent.trim()).toEqual('Allows edits from maintainers');
- });
- });
-
- describe('when a maintainer is not allowed to edit', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- maintainerEditAllowed: false,
- });
- });
-
- it('hides the message', () => {
- expect(vm.$el.textContent.trim()).toEqual('');
- });
- });
-});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js
index c2c92d8ac56..adeea03481f 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js
@@ -6,6 +6,14 @@ import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetMerged', () => {
let vm;
const targetBranch = 'foo';
+ const selectors = {
+ get copyMergeShaButton() {
+ return vm.$el.querySelector('button.js-mr-merged-copy-sha');
+ },
+ get mergeCommitShaLink() {
+ return vm.$el.querySelector('a.js-mr-merged-commit-sha');
+ },
+ };
beforeEach(() => {
const Component = Vue.extend(mergedComponent);
@@ -31,6 +39,9 @@ describe('MRWidgetMerged', () => {
readableClosedAt: '',
},
updatedAt: 'mergedUpdatedAt',
+ shortMergeCommitSha: 'asdf1234',
+ mergeCommitPath: 'http://localhost:3000/root/nautilus/commit/f7ce827c314c9340b075657fd61c789fb01cf74d',
+ sourceBranch: 'bar',
targetBranch,
};
@@ -140,6 +151,17 @@ describe('MRWidgetMerged', () => {
expect(vm.$el.textContent).toContain('Cherry-pick');
});
+ it('shows button to copy commit SHA to clipboard', () => {
+ expect(selectors.copyMergeShaButton).toExist();
+ expect(selectors.copyMergeShaButton.getAttribute('data-clipboard-text')).toBe(vm.mr.shortMergeCommitSha);
+ });
+
+ it('shows merge commit SHA link', () => {
+ expect(selectors.mergeCommitShaLink).toExist();
+ expect(selectors.mergeCommitShaLink.text).toContain(vm.mr.shortMergeCommitSha);
+ expect(selectors.mergeCommitShaLink.href).toBe(vm.mr.mergeCommitPath);
+ });
+
it('should not show source branch removed text', (done) => {
vm.mr.sourceBranchRemoved = false;
diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js
index 3fc7663b9c2..9d2a15ff009 100644
--- a/spec/javascripts/vue_mr_widget/mock_data.js
+++ b/spec/javascripts/vue_mr_widget/mock_data.js
@@ -18,6 +18,7 @@ export default {
human_total_time_spent: null,
in_progress_merge_commit_sha: null,
merge_commit_sha: '53027d060246c8f47e4a9310fb332aa52f221775',
+ short_merge_commit_sha: '53027d06',
merge_error: null,
merge_params: {
force_remove_source_branch: null,
@@ -215,4 +216,5 @@ export default {
diverged_commits_count: 0,
only_allow_merge_if_pipeline_succeeds: false,
commit_change_content_path: '/root/acets-app/merge_requests/22/commit_change_content',
+ merge_commit_path: 'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775',
};
diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
index e55c7649d40..30918428da2 100644
--- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import mrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options';
+import mrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
import notify from '~/lib/utils/notify';
import { stateKey } from '~/vue_merge_request_widget/stores/state_maps';
diff --git a/spec/lib/backup/repository_spec.rb b/spec/lib/backup/repository_spec.rb
index b3777be312b..b1ea9c0b622 100644
--- a/spec/lib/backup/repository_spec.rb
+++ b/spec/lib/backup/repository_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Backup::Repository do
let(:progress) { StringIO.new }
- let!(:project) { create(:project) }
+ let!(:project) { create(:project, :wiki_repo) }
before do
allow(progress).to receive(:puts)
@@ -102,7 +102,7 @@ describe Backup::Repository do
it 'invalidates the emptiness cache' do
expect(wiki.repository).to receive(:expire_emptiness_caches).once
- wiki.empty?
+ described_class.new.send(:empty_repo?, wiki)
end
context 'wiki repo has content' do
diff --git a/spec/lib/gitlab/auth/blocked_user_tracker_spec.rb b/spec/lib/gitlab/auth/blocked_user_tracker_spec.rb
index 726a3c1c83a..43b68e69131 100644
--- a/spec/lib/gitlab/auth/blocked_user_tracker_spec.rb
+++ b/spec/lib/gitlab/auth/blocked_user_tracker_spec.rb
@@ -17,12 +17,8 @@ describe Gitlab::Auth::BlockedUserTracker do
end
context 'failed login due to blocked user' do
- let(:env) do
- {
- 'warden.options' => { message: User::BLOCKED_MESSAGE },
- described_class::ACTIVE_RECORD_REQUEST_PARAMS => { 'user' => { 'login' => user.username } }
- }
- end
+ let(:base_env) { { 'warden.options' => { message: User::BLOCKED_MESSAGE } } }
+ let(:env) { base_env.merge(request_env) }
subject { described_class.log_if_user_blocked(env) }
@@ -30,23 +26,37 @@ describe Gitlab::Auth::BlockedUserTracker do
expect_any_instance_of(SystemHooksService).to receive(:execute_hooks_for).with(user, :failed_login)
end
- it 'logs a blocked user' do
- user.block!
+ context 'via GitLab login' do
+ let(:request_env) { { described_class::ACTIVE_RECORD_REQUEST_PARAMS => { 'user' => { 'login' => user.username } } } }
- expect(subject).to be_truthy
- end
+ it 'logs a blocked user' do
+ user.block!
+
+ expect(subject).to be_truthy
+ end
- it 'logs a blocked user by e-mail' do
- user.block!
- env[described_class::ACTIVE_RECORD_REQUEST_PARAMS]['user']['login'] = user.email
+ it 'logs a blocked user by e-mail' do
+ user.block!
+ env[described_class::ACTIVE_RECORD_REQUEST_PARAMS]['user']['login'] = user.email
- expect(subject).to be_truthy
+ expect(subject).to be_truthy
+ end
end
- it 'logs a LDAP blocked user' do
- user.ldap_block!
+ context 'via LDAP login' do
+ let(:request_env) { { described_class::ACTIVE_RECORD_REQUEST_PARAMS => { 'username' => user.username } } }
+
+ it 'logs a blocked user' do
+ user.block!
+
+ expect(subject).to be_truthy
+ end
+
+ it 'logs a LDAP blocked user' do
+ user.ldap_block!
- expect(subject).to be_truthy
+ expect(subject).to be_truthy
+ end
end
end
end
diff --git a/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb b/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb
new file mode 100644
index 00000000000..fa209bed74e
--- /dev/null
+++ b/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe Gitlab::Auth::UserAccessDeniedReason do
+ include TermsHelper
+ let(:user) { build(:user) }
+
+ let(:reason) { described_class.new(user) }
+
+ describe '#rejection_message' do
+ subject { reason.rejection_message }
+
+ context 'when a user is blocked' do
+ before do
+ user.block!
+ end
+
+ it { is_expected.to match /blocked/ }
+ end
+
+ context 'a user did not accept the enforced terms' do
+ before do
+ enforce_terms
+ end
+
+ it { is_expected.to match /You must accept the Terms of Service/ }
+ end
+
+ context 'when the user is internal' do
+ let(:user) { User.ghost }
+
+ it { is_expected.to match /This action cannot be performed by internal users/ }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/build_access_spec.rb b/spec/lib/gitlab/build_access_spec.rb
new file mode 100644
index 00000000000..08f50bf4fac
--- /dev/null
+++ b/spec/lib/gitlab/build_access_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe Gitlab::BuildAccess do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ describe '#can_do_action' do
+ subject { described_class.new(user, project: project).can_do_action?(:download_code) }
+
+ context 'when the user can do an action on the project but cannot access git' do
+ before do
+ user.block!
+ project.add_developer(user)
+ end
+
+ it { is_expected.to be(true) }
+ end
+
+ context 'when the user cannot do an action on the project' do
+ it { is_expected.to be(false) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
index 3ae7053a995..85d73e5c382 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
@@ -5,6 +5,10 @@ describe Gitlab::Ci::Pipeline::Chain::Build do
set(:user) { create(:user) }
let(:pipeline) { Ci::Pipeline.new }
+ let(:variables_attributes) do
+ [{ key: 'first', secret_value: 'world' },
+ { key: 'second', secret_value: 'second_world' }]
+ end
let(:command) do
Gitlab::Ci::Pipeline::Chain::Command.new(
source: :push,
@@ -15,7 +19,8 @@ describe Gitlab::Ci::Pipeline::Chain::Build do
trigger_request: nil,
schedule: nil,
project: project,
- current_user: user)
+ current_user: user,
+ variables_attributes: variables_attributes)
end
let(:step) { described_class.new(pipeline, command) }
@@ -39,6 +44,8 @@ describe Gitlab::Ci::Pipeline::Chain::Build do
expect(pipeline.tag).to be false
expect(pipeline.user).to eq user
expect(pipeline.project).to eq project
+ expect(pipeline.variables.map { |var| var.slice(:key, :secret_value) })
+ .to eq variables_attributes.map(&:with_indifferent_access)
end
it 'sets a valid config source' do
diff --git a/spec/lib/gitlab/ci/trace/chunked_io_spec.rb b/spec/lib/gitlab/ci/trace/chunked_io_spec.rb
new file mode 100644
index 00000000000..6259b952add
--- /dev/null
+++ b/spec/lib/gitlab/ci/trace/chunked_io_spec.rb
@@ -0,0 +1,383 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Trace::ChunkedIO, :clean_gitlab_redis_cache do
+ include ChunkedIOHelpers
+
+ set(:build) { create(:ci_build, :running) }
+ let(:chunked_io) { described_class.new(build) }
+
+ before do
+ stub_feature_flags(ci_enable_live_trace: true)
+ end
+
+ context "#initialize" do
+ context 'when a chunk exists' do
+ before do
+ build.trace.set('ABC')
+ end
+
+ it { expect(chunked_io.size).to eq(3) }
+ end
+
+ context 'when two chunks exist' do
+ before do
+ stub_buffer_size(4)
+ build.trace.set('ABCDEF')
+ end
+
+ it { expect(chunked_io.size).to eq(6) }
+ end
+
+ context 'when no chunks exists' do
+ it { expect(chunked_io.size).to eq(0) }
+ end
+ end
+
+ context "#seek" do
+ subject { chunked_io.seek(pos, where) }
+
+ before do
+ build.trace.set(sample_trace_raw)
+ end
+
+ context 'when moves pos to end of the file' do
+ let(:pos) { 0 }
+ let(:where) { IO::SEEK_END }
+
+ it { is_expected.to eq(sample_trace_raw.bytesize) }
+ end
+
+ context 'when moves pos to middle of the file' do
+ let(:pos) { sample_trace_raw.bytesize / 2 }
+ let(:where) { IO::SEEK_SET }
+
+ it { is_expected.to eq(pos) }
+ end
+
+ context 'when moves pos around' do
+ it 'matches the result' do
+ expect(chunked_io.seek(0)).to eq(0)
+ expect(chunked_io.seek(100, IO::SEEK_CUR)).to eq(100)
+ expect { chunked_io.seek(sample_trace_raw.bytesize + 1, IO::SEEK_CUR) }
+ .to raise_error('new position is outside of file')
+ end
+ end
+ end
+
+ context "#eof?" do
+ subject { chunked_io.eof? }
+
+ before do
+ build.trace.set(sample_trace_raw)
+ end
+
+ context 'when current pos is at end of the file' do
+ before do
+ chunked_io.seek(sample_trace_raw.bytesize, IO::SEEK_SET)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when current pos is not at end of the file' do
+ before do
+ chunked_io.seek(0, IO::SEEK_SET)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context "#each_line" do
+ let(:string_io) { StringIO.new(sample_trace_raw) }
+
+ context 'when buffer size is smaller than file size' do
+ before do
+ stub_buffer_size(sample_trace_raw.bytesize / 2)
+ build.trace.set(sample_trace_raw)
+ end
+
+ it 'yields lines' do
+ expect { |b| chunked_io.each_line(&b) }
+ .to yield_successive_args(*string_io.each_line.to_a)
+ end
+ end
+
+ context 'when buffer size is larger than file size' do
+ before do
+ stub_buffer_size(sample_trace_raw.bytesize * 2)
+ build.trace.set(sample_trace_raw)
+ end
+
+ it 'calls get_chunk only once' do
+ expect_any_instance_of(Gitlab::Ci::Trace::ChunkedIO)
+ .to receive(:current_chunk).once.and_call_original
+
+ chunked_io.each_line { |line| }
+ end
+ end
+ end
+
+ context "#read" do
+ subject { chunked_io.read(length) }
+
+ context 'when read the whole size' do
+ let(:length) { nil }
+
+ context 'when buffer size is smaller than file size' do
+ before do
+ stub_buffer_size(sample_trace_raw.bytesize / 2)
+ build.trace.set(sample_trace_raw)
+ end
+
+ it { is_expected.to eq(sample_trace_raw) }
+ end
+
+ context 'when buffer size is larger than file size' do
+ before do
+ stub_buffer_size(sample_trace_raw.bytesize * 2)
+ build.trace.set(sample_trace_raw)
+ end
+
+ it { is_expected.to eq(sample_trace_raw) }
+ end
+ end
+
+ context 'when read only first 100 bytes' do
+ let(:length) { 100 }
+
+ context 'when buffer size is smaller than file size' do
+ before do
+ stub_buffer_size(sample_trace_raw.bytesize / 2)
+ build.trace.set(sample_trace_raw)
+ end
+
+ it 'reads a trace' do
+ is_expected.to eq(sample_trace_raw.byteslice(0, length))
+ end
+ end
+
+ context 'when buffer size is larger than file size' do
+ before do
+ stub_buffer_size(sample_trace_raw.bytesize * 2)
+ build.trace.set(sample_trace_raw)
+ end
+
+ it 'reads a trace' do
+ is_expected.to eq(sample_trace_raw.byteslice(0, length))
+ end
+ end
+ end
+
+ context 'when tries to read oversize' do
+ let(:length) { sample_trace_raw.bytesize + 1000 }
+
+ context 'when buffer size is smaller than file size' do
+ before do
+ stub_buffer_size(sample_trace_raw.bytesize / 2)
+ build.trace.set(sample_trace_raw)
+ end
+
+ it 'reads a trace' do
+ is_expected.to eq(sample_trace_raw)
+ end
+ end
+
+ context 'when buffer size is larger than file size' do
+ before do
+ stub_buffer_size(sample_trace_raw.bytesize * 2)
+ build.trace.set(sample_trace_raw)
+ end
+
+ it 'reads a trace' do
+ is_expected.to eq(sample_trace_raw)
+ end
+ end
+ end
+
+ context 'when tries to read 0 bytes' do
+ let(:length) { 0 }
+
+ context 'when buffer size is smaller than file size' do
+ before do
+ stub_buffer_size(sample_trace_raw.bytesize / 2)
+ build.trace.set(sample_trace_raw)
+ end
+
+ it 'reads a trace' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when buffer size is larger than file size' do
+ before do
+ stub_buffer_size(sample_trace_raw.bytesize * 2)
+ build.trace.set(sample_trace_raw)
+ end
+
+ it 'reads a trace' do
+ is_expected.to be_empty
+ end
+ end
+ end
+ end
+
+ context "#readline" do
+ subject { chunked_io.readline }
+
+ let(:string_io) { StringIO.new(sample_trace_raw) }
+
+ shared_examples 'all line matching' do
+ it do
+ (0...sample_trace_raw.lines.count).each do
+ expect(chunked_io.readline).to eq(string_io.readline)
+ end
+ end
+ end
+
+ context 'when buffer size is smaller than file size' do
+ before do
+ stub_buffer_size(sample_trace_raw.bytesize / 2)
+ build.trace.set(sample_trace_raw)
+ end
+
+ it_behaves_like 'all line matching'
+ end
+
+ context 'when buffer size is larger than file size' do
+ before do
+ stub_buffer_size(sample_trace_raw.bytesize * 2)
+ build.trace.set(sample_trace_raw)
+ end
+
+ it_behaves_like 'all line matching'
+ end
+
+ context 'when pos is at middle of the file' do
+ before do
+ stub_buffer_size(sample_trace_raw.bytesize / 2)
+ build.trace.set(sample_trace_raw)
+
+ chunked_io.seek(chunked_io.size / 2)
+ string_io.seek(string_io.size / 2)
+ end
+
+ it 'reads from pos' do
+ expect(chunked_io.readline).to eq(string_io.readline)
+ end
+ end
+ end
+
+ context "#write" do
+ subject { chunked_io.write(data) }
+
+ let(:data) { sample_trace_raw }
+
+ context 'when data does not exist' do
+ shared_examples 'writes a trace' do
+ it do
+ is_expected.to eq(data.bytesize)
+
+ chunked_io.seek(0, IO::SEEK_SET)
+ expect(chunked_io.read).to eq(data)
+ end
+ end
+
+ context 'when buffer size is smaller than file size' do
+ before do
+ stub_buffer_size(data.bytesize / 2)
+ end
+
+ it_behaves_like 'writes a trace'
+ end
+
+ context 'when buffer size is larger than file size' do
+ before do
+ stub_buffer_size(data.bytesize * 2)
+ end
+
+ it_behaves_like 'writes a trace'
+ end
+ end
+
+ context 'when data already exists' do
+ let(:exist_data) { 'exist data' }
+
+ shared_examples 'appends a trace' do
+ it do
+ chunked_io.seek(0, IO::SEEK_END)
+ is_expected.to eq(data.bytesize)
+
+ chunked_io.seek(0, IO::SEEK_SET)
+ expect(chunked_io.read).to eq(exist_data + data)
+ end
+ end
+
+ context 'when buffer size is smaller than file size' do
+ before do
+ stub_buffer_size(sample_trace_raw.bytesize / 2)
+ build.trace.set(exist_data)
+ end
+
+ it_behaves_like 'appends a trace'
+ end
+
+ context 'when buffer size is larger than file size' do
+ before do
+ stub_buffer_size(sample_trace_raw.bytesize * 2)
+ build.trace.set(exist_data)
+ end
+
+ it_behaves_like 'appends a trace'
+ end
+ end
+ end
+
+ context "#truncate" do
+ let(:offset) { 10 }
+
+ context 'when data does not exist' do
+ shared_examples 'truncates a trace' do
+ it do
+ chunked_io.truncate(offset)
+
+ chunked_io.seek(0, IO::SEEK_SET)
+ expect(chunked_io.read).to eq(sample_trace_raw.byteslice(0, offset))
+ end
+ end
+
+ context 'when buffer size is smaller than file size' do
+ before do
+ stub_buffer_size(sample_trace_raw.bytesize / 2)
+ build.trace.set(sample_trace_raw)
+ end
+
+ it_behaves_like 'truncates a trace'
+ end
+
+ context 'when buffer size is larger than file size' do
+ before do
+ stub_buffer_size(sample_trace_raw.bytesize * 2)
+ build.trace.set(sample_trace_raw)
+ end
+
+ it_behaves_like 'truncates a trace'
+ end
+ end
+ end
+
+ context "#destroy!" do
+ subject { chunked_io.destroy! }
+
+ before do
+ build.trace.set(sample_trace_raw)
+ end
+
+ it 'deletes' do
+ expect { subject }.to change { chunked_io.size }
+ .from(sample_trace_raw.bytesize).to(0)
+
+ expect(Ci::BuildTraceChunk.where(build: build).count).to eq(0)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/trace/stream_spec.rb b/spec/lib/gitlab/ci/trace/stream_spec.rb
index e5555546fa8..4f49958dd33 100644
--- a/spec/lib/gitlab/ci/trace/stream_spec.rb
+++ b/spec/lib/gitlab/ci/trace/stream_spec.rb
@@ -1,6 +1,12 @@
require 'spec_helper'
-describe Gitlab::Ci::Trace::Stream do
+describe Gitlab::Ci::Trace::Stream, :clean_gitlab_redis_cache do
+ set(:build) { create(:ci_build, :running) }
+
+ before do
+ stub_feature_flags(ci_enable_live_trace: true)
+ end
+
describe 'delegates' do
subject { described_class.new { nil } }
@@ -11,337 +17,470 @@ describe Gitlab::Ci::Trace::Stream do
it { is_expected.to delegate_method(:path).to(:stream) }
it { is_expected.to delegate_method(:truncate).to(:stream) }
it { is_expected.to delegate_method(:valid?).to(:stream).as(:present?) }
- it { is_expected.to delegate_method(:file?).to(:path).as(:present?) }
end
describe '#limit' do
- let(:stream) do
- described_class.new do
- StringIO.new((1..8).to_a.join("\n"))
+ shared_examples_for 'limits' do
+ it 'if size is larger we start from beginning' do
+ stream.limit(20)
+
+ expect(stream.tell).to eq(0)
end
- end
- it 'if size is larger we start from beginning' do
- stream.limit(20)
+ it 'if size is smaller we start from the end' do
+ stream.limit(2)
- expect(stream.tell).to eq(0)
- end
+ expect(stream.raw).to eq("8")
+ end
- it 'if size is smaller we start from the end' do
- stream.limit(2)
+ context 'when the trace contains ANSI sequence and Unicode' do
+ let(:stream) do
+ described_class.new do
+ File.open(expand_fixture_path('trace/ansi-sequence-and-unicode'))
+ end
+ end
- expect(stream.raw).to eq("8")
- end
+ it 'forwards to the next linefeed, case 1' do
+ stream.limit(7)
- context 'when the trace contains ANSI sequence and Unicode' do
- let(:stream) do
- described_class.new do
- File.open(expand_fixture_path('trace/ansi-sequence-and-unicode'))
+ result = stream.raw
+
+ expect(result).to eq('')
+ expect(result.encoding).to eq(Encoding.default_external)
end
- end
- it 'forwards to the next linefeed, case 1' do
- stream.limit(7)
+ it 'forwards to the next linefeed, case 2' do
+ stream.limit(29)
- result = stream.raw
+ result = stream.raw
- expect(result).to eq('')
- expect(result.encoding).to eq(Encoding.default_external)
- end
+ expect(result).to eq("\e[01;32m許功蓋\e[0m\n")
+ expect(result.encoding).to eq(Encoding.default_external)
+ end
- it 'forwards to the next linefeed, case 2' do
- stream.limit(29)
+ # See https://gitlab.com/gitlab-org/gitlab-ce/issues/30796
+ it 'reads in binary, output as Encoding.default_external' do
+ stream.limit(52)
- result = stream.raw
+ result = stream.html
- expect(result).to eq("\e[01;32m許功蓋\e[0m\n")
- expect(result.encoding).to eq(Encoding.default_external)
+ expect(result).to eq("ヾ(´༎ຶД༎ຶ`)ノ<br><span class=\"term-fg-green\">許功蓋</span><br>")
+ expect(result.encoding).to eq(Encoding.default_external)
+ end
end
+ end
- # See https://gitlab.com/gitlab-org/gitlab-ce/issues/30796
- it 'reads in binary, output as Encoding.default_external' do
- stream.limit(52)
+ context 'when stream is StringIO' do
+ let(:stream) do
+ described_class.new do
+ StringIO.new((1..8).to_a.join("\n"))
+ end
+ end
- result = stream.html
+ it_behaves_like 'limits'
+ end
- expect(result).to eq("ヾ(´༎ຶД༎ຶ`)ノ<br><span class=\"term-fg-green\">許功蓋</span><br>")
- expect(result.encoding).to eq(Encoding.default_external)
+ context 'when stream is ChunkedIO' do
+ let(:stream) do
+ described_class.new do
+ Gitlab::Ci::Trace::ChunkedIO.new(build).tap do |chunked_io|
+ chunked_io.write((1..8).to_a.join("\n"))
+ chunked_io.seek(0, IO::SEEK_SET)
+ end
+ end
end
+
+ it_behaves_like 'limits'
end
end
describe '#append' do
- let(:tempfile) { Tempfile.new }
+ shared_examples_for 'appends' do
+ it "truncates and append content" do
+ stream.append("89", 4)
+ stream.seek(0)
- let(:stream) do
- described_class.new do
- tempfile.write("12345678")
- tempfile.rewind
- tempfile
+ expect(stream.size).to eq(6)
+ expect(stream.raw).to eq("123489")
end
- end
- after do
- tempfile.unlink
- end
+ it 'appends in binary mode' do
+ '😺'.force_encoding('ASCII-8BIT').each_char.with_index do |byte, offset|
+ stream.append(byte, offset)
+ end
- it "truncates and append content" do
- stream.append("89", 4)
- stream.seek(0)
+ stream.seek(0)
- expect(stream.size).to eq(6)
- expect(stream.raw).to eq("123489")
+ expect(stream.size).to eq(4)
+ expect(stream.raw).to eq('😺')
+ end
end
- it 'appends in binary mode' do
- '😺'.force_encoding('ASCII-8BIT').each_char.with_index do |byte, offset|
- stream.append(byte, offset)
+ context 'when stream is Tempfile' do
+ let(:tempfile) { Tempfile.new }
+
+ let(:stream) do
+ described_class.new do
+ tempfile.write("12345678")
+ tempfile.rewind
+ tempfile
+ end
+ end
+
+ after do
+ tempfile.unlink
end
- stream.seek(0)
+ it_behaves_like 'appends'
+ end
- expect(stream.size).to eq(4)
- expect(stream.raw).to eq('😺')
+ context 'when stream is ChunkedIO' do
+ let(:stream) do
+ described_class.new do
+ Gitlab::Ci::Trace::ChunkedIO.new(build).tap do |chunked_io|
+ chunked_io.write('12345678')
+ chunked_io.seek(0, IO::SEEK_SET)
+ end
+ end
+ end
+
+ it_behaves_like 'appends'
end
end
describe '#set' do
- let(:stream) do
- described_class.new do
- StringIO.new("12345678")
+ shared_examples_for 'sets' do
+ before do
+ stream.set("8901")
+ end
+
+ it "overwrite content" do
+ stream.seek(0)
+
+ expect(stream.size).to eq(4)
+ expect(stream.raw).to eq("8901")
end
end
- before do
- stream.set("8901")
+ context 'when stream is StringIO' do
+ let(:stream) do
+ described_class.new do
+ StringIO.new("12345678")
+ end
+ end
+
+ it_behaves_like 'sets'
end
- it "overwrite content" do
- stream.seek(0)
+ context 'when stream is ChunkedIO' do
+ let(:stream) do
+ described_class.new do
+ Gitlab::Ci::Trace::ChunkedIO.new(build).tap do |chunked_io|
+ chunked_io.write('12345678')
+ chunked_io.seek(0, IO::SEEK_SET)
+ end
+ end
+ end
- expect(stream.size).to eq(4)
- expect(stream.raw).to eq("8901")
+ it_behaves_like 'sets'
end
end
describe '#raw' do
- let(:path) { __FILE__ }
- let(:lines) { File.readlines(path) }
- let(:stream) do
- described_class.new do
- File.open(path)
+ shared_examples_for 'sets' do
+ it 'returns all contents if last_lines is not specified' do
+ result = stream.raw
+
+ expect(result).to eq(lines.join)
+ expect(result.encoding).to eq(Encoding.default_external)
end
- end
- it 'returns all contents if last_lines is not specified' do
- result = stream.raw
+ context 'limit max lines' do
+ before do
+ # specifying BUFFER_SIZE forces to seek backwards
+ allow(described_class).to receive(:BUFFER_SIZE)
+ .and_return(2)
+ end
- expect(result).to eq(lines.join)
- expect(result.encoding).to eq(Encoding.default_external)
- end
+ it 'returns last few lines' do
+ result = stream.raw(last_lines: 2)
- context 'limit max lines' do
- before do
- # specifying BUFFER_SIZE forces to seek backwards
- allow(described_class).to receive(:BUFFER_SIZE)
- .and_return(2)
+ expect(result).to eq(lines.last(2).join)
+ expect(result.encoding).to eq(Encoding.default_external)
+ end
+
+ it 'returns everything if trying to get too many lines' do
+ result = stream.raw(last_lines: lines.size * 2)
+
+ expect(result).to eq(lines.join)
+ expect(result.encoding).to eq(Encoding.default_external)
+ end
end
+ end
- it 'returns last few lines' do
- result = stream.raw(last_lines: 2)
+ let(:path) { __FILE__ }
+ let(:lines) { File.readlines(path) }
- expect(result).to eq(lines.last(2).join)
- expect(result.encoding).to eq(Encoding.default_external)
+ context 'when stream is File' do
+ let(:stream) do
+ described_class.new do
+ File.open(path)
+ end
end
- it 'returns everything if trying to get too many lines' do
- result = stream.raw(last_lines: lines.size * 2)
+ it_behaves_like 'sets'
+ end
- expect(result).to eq(lines.join)
- expect(result.encoding).to eq(Encoding.default_external)
+ context 'when stream is ChunkedIO' do
+ let(:stream) do
+ described_class.new do
+ Gitlab::Ci::Trace::ChunkedIO.new(build).tap do |chunked_io|
+ chunked_io.write(File.binread(path))
+ chunked_io.seek(0, IO::SEEK_SET)
+ end
+ end
end
+
+ it_behaves_like 'sets'
end
end
describe '#html_with_state' do
- let(:stream) do
- described_class.new do
- StringIO.new("1234")
+ shared_examples_for 'html_with_states' do
+ it 'returns html content with state' do
+ result = stream.html_with_state
+
+ expect(result.html).to eq("1234")
end
- end
- it 'returns html content with state' do
- result = stream.html_with_state
+ context 'follow-up state' do
+ let!(:last_result) { stream.html_with_state }
- expect(result.html).to eq("1234")
- end
+ before do
+ stream.append("5678", 4)
+ stream.seek(0)
+ end
- context 'follow-up state' do
- let!(:last_result) { stream.html_with_state }
+ it "returns appended trace" do
+ result = stream.html_with_state(last_result.state)
- before do
- stream.append("5678", 4)
- stream.seek(0)
+ expect(result.append).to be_truthy
+ expect(result.html).to eq("5678")
+ end
+ end
+ end
+
+ context 'when stream is StringIO' do
+ let(:stream) do
+ described_class.new do
+ StringIO.new("1234")
+ end
end
- it "returns appended trace" do
- result = stream.html_with_state(last_result.state)
+ it_behaves_like 'html_with_states'
+ end
- expect(result.append).to be_truthy
- expect(result.html).to eq("5678")
+ context 'when stream is ChunkedIO' do
+ let(:stream) do
+ described_class.new do
+ Gitlab::Ci::Trace::ChunkedIO.new(build).tap do |chunked_io|
+ chunked_io.write("1234")
+ chunked_io.seek(0, IO::SEEK_SET)
+ end
+ end
end
+
+ it_behaves_like 'html_with_states'
end
end
describe '#html' do
- let(:stream) do
- described_class.new do
- StringIO.new("12\n34\n56")
+ shared_examples_for 'htmls' do
+ it "returns html" do
+ expect(stream.html).to eq("12<br>34<br>56")
+ end
+
+ it "returns html for last line only" do
+ expect(stream.html(last_lines: 1)).to eq("56")
end
end
- it "returns html" do
- expect(stream.html).to eq("12<br>34<br>56")
+ context 'when stream is StringIO' do
+ let(:stream) do
+ described_class.new do
+ StringIO.new("12\n34\n56")
+ end
+ end
+
+ it_behaves_like 'htmls'
end
- it "returns html for last line only" do
- expect(stream.html(last_lines: 1)).to eq("56")
+ context 'when stream is ChunkedIO' do
+ let(:stream) do
+ described_class.new do
+ Gitlab::Ci::Trace::ChunkedIO.new(build).tap do |chunked_io|
+ chunked_io.write("12\n34\n56")
+ chunked_io.seek(0, IO::SEEK_SET)
+ end
+ end
+ end
+
+ it_behaves_like 'htmls'
end
end
describe '#extract_coverage' do
- let(:stream) do
- described_class.new do
- StringIO.new(data)
- end
- end
+ shared_examples_for 'extract_coverages' do
+ context 'valid content & regex' do
+ let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered' }
+ let(:regex) { '\(\d+.\d+\%\) covered' }
- subject { stream.extract_coverage(regex) }
+ it { is_expected.to eq("98.29") }
+ end
- context 'valid content & regex' do
- let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered' }
- let(:regex) { '\(\d+.\d+\%\) covered' }
+ context 'valid content & bad regex' do
+ let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered\n' }
+ let(:regex) { 'very covered' }
- it { is_expected.to eq("98.29") }
- end
+ it { is_expected.to be_nil }
+ end
- context 'valid content & bad regex' do
- let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered\n' }
- let(:regex) { 'very covered' }
+ context 'no coverage content & regex' do
+ let(:data) { 'No coverage for today :sad:' }
+ let(:regex) { '\(\d+.\d+\%\) covered' }
- it { is_expected.to be_nil }
- end
+ it { is_expected.to be_nil }
+ end
- context 'no coverage content & regex' do
- let(:data) { 'No coverage for today :sad:' }
- let(:regex) { '\(\d+.\d+\%\) covered' }
+ context 'multiple results in content & regex' do
+ let(:data) do
+ <<~HEREDOC
+ (98.39%) covered
+ (98.29%) covered
+ HEREDOC
+ end
- it { is_expected.to be_nil }
- end
+ let(:regex) { '\(\d+.\d+\%\) covered' }
- context 'multiple results in content & regex' do
- let(:data) do
- <<~HEREDOC
- (98.39%) covered
- (98.29%) covered
- HEREDOC
+ it 'returns the last matched coverage' do
+ is_expected.to eq("98.29")
+ end
end
- let(:regex) { '\(\d+.\d+\%\) covered' }
+ context 'when BUFFER_SIZE is smaller than stream.size' do
+ let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered\n' }
+ let(:regex) { '\(\d+.\d+\%\) covered' }
- it 'returns the last matched coverage' do
- is_expected.to eq("98.29")
+ before do
+ stub_const('Gitlab::Ci::Trace::Stream::BUFFER_SIZE', 5)
+ end
+
+ it { is_expected.to eq("98.29") }
end
- end
- context 'when BUFFER_SIZE is smaller than stream.size' do
- let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered\n' }
- let(:regex) { '\(\d+.\d+\%\) covered' }
+ context 'when regex is multi-byte char' do
+ let(:data) { '95.0 ゴッドファット\n' }
+ let(:regex) { '\d+\.\d+ ゴッドファット' }
- before do
- stub_const('Gitlab::Ci::Trace::Stream::BUFFER_SIZE', 5)
+ before do
+ stub_const('Gitlab::Ci::Trace::Stream::BUFFER_SIZE', 5)
+ end
+
+ it { is_expected.to eq('95.0') }
end
- it { is_expected.to eq("98.29") }
- end
+ context 'when BUFFER_SIZE is equal to stream.size' do
+ let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered\n' }
+ let(:regex) { '\(\d+.\d+\%\) covered' }
- context 'when regex is multi-byte char' do
- let(:data) { '95.0 ゴッドファット\n' }
- let(:regex) { '\d+\.\d+ ゴッドファット' }
+ before do
+ stub_const('Gitlab::Ci::Trace::Stream::BUFFER_SIZE', data.length)
+ end
- before do
- stub_const('Gitlab::Ci::Trace::Stream::BUFFER_SIZE', 5)
+ it { is_expected.to eq("98.29") }
end
- it { is_expected.to eq('95.0') }
- end
-
- context 'when BUFFER_SIZE is equal to stream.size' do
- let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered\n' }
- let(:regex) { '\(\d+.\d+\%\) covered' }
+ context 'using a regex capture' do
+ let(:data) { 'TOTAL 9926 3489 65%' }
+ let(:regex) { 'TOTAL\s+\d+\s+\d+\s+(\d{1,3}\%)' }
- before do
- stub_const('Gitlab::Ci::Trace::Stream::BUFFER_SIZE', data.length)
+ it { is_expected.to eq("65") }
end
- it { is_expected.to eq("98.29") }
- end
+ context 'malicious regexp' do
+ let(:data) { malicious_text }
+ let(:regex) { malicious_regexp }
- context 'using a regex capture' do
- let(:data) { 'TOTAL 9926 3489 65%' }
- let(:regex) { 'TOTAL\s+\d+\s+\d+\s+(\d{1,3}\%)' }
+ include_examples 'malicious regexp'
+ end
- it { is_expected.to eq("65") }
- end
+ context 'multi-line data with rooted regexp' do
+ let(:data) { "\n65%\n" }
+ let(:regex) { '^(\d+)\%$' }
- context 'malicious regexp' do
- let(:data) { malicious_text }
- let(:regex) { malicious_regexp }
+ it { is_expected.to eq('65') }
+ end
- include_examples 'malicious regexp'
- end
+ context 'long line' do
+ let(:data) { 'a' * 80000 + '100%' + 'a' * 80000 }
+ let(:regex) { '\d+\%' }
- context 'multi-line data with rooted regexp' do
- let(:data) { "\n65%\n" }
- let(:regex) { '^(\d+)\%$' }
+ it { is_expected.to eq('100') }
+ end
- it { is_expected.to eq('65') }
- end
+ context 'many lines' do
+ let(:data) { "foo\n" * 80000 + "100%\n" + "foo\n" * 80000 }
+ let(:regex) { '\d+\%' }
- context 'long line' do
- let(:data) { 'a' * 80000 + '100%' + 'a' * 80000 }
- let(:regex) { '\d+\%' }
+ it { is_expected.to eq('100') }
+ end
- it { is_expected.to eq('100') }
- end
+ context 'empty regex' do
+ let(:data) { 'foo' }
+ let(:regex) { '' }
- context 'many lines' do
- let(:data) { "foo\n" * 80000 + "100%\n" + "foo\n" * 80000 }
- let(:regex) { '\d+\%' }
+ it 'skips processing' do
+ expect(stream).not_to receive(:read)
- it { is_expected.to eq('100') }
- end
+ is_expected.to be_nil
+ end
+ end
- context 'empty regex' do
- let(:data) { 'foo' }
- let(:regex) { '' }
+ context 'nil regex' do
+ let(:data) { 'foo' }
+ let(:regex) { nil }
- it 'skips processing' do
- expect(stream).not_to receive(:read)
+ it 'skips processing' do
+ expect(stream).not_to receive(:read)
- is_expected.to be_nil
+ is_expected.to be_nil
+ end
end
end
- context 'nil regex' do
- let(:data) { 'foo' }
- let(:regex) { nil }
+ subject { stream.extract_coverage(regex) }
- it 'skips processing' do
- expect(stream).not_to receive(:read)
+ context 'when stream is StringIO' do
+ let(:stream) do
+ described_class.new do
+ StringIO.new(data)
+ end
+ end
+
+ it_behaves_like 'extract_coverages'
+ end
- is_expected.to be_nil
+ context 'when stream is ChunkedIO' do
+ let(:stream) do
+ described_class.new do
+ Gitlab::Ci::Trace::ChunkedIO.new(build).tap do |chunked_io|
+ chunked_io.write(data)
+ chunked_io.seek(0, IO::SEEK_SET)
+ end
+ end
end
+
+ it_behaves_like 'extract_coverages'
end
end
end
diff --git a/spec/lib/gitlab/ci/trace_spec.rb b/spec/lib/gitlab/ci/trace_spec.rb
index 6a9c6442282..e9d755c2021 100644
--- a/spec/lib/gitlab/ci/trace_spec.rb
+++ b/spec/lib/gitlab/ci/trace_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Ci::Trace do
+describe Gitlab::Ci::Trace, :clean_gitlab_redis_cache do
let(:build) { create(:ci_build) }
let(:trace) { described_class.new(build) }
@@ -9,552 +9,19 @@ describe Gitlab::Ci::Trace do
it { expect(trace).to delegate_method(:old_trace).to(:job) }
end
- describe '#html' do
+ context 'when live trace feature is disabled' do
before do
- trace.set("12\n34")
+ stub_feature_flags(ci_enable_live_trace: false)
end
- it "returns formatted html" do
- expect(trace.html).to eq("12<br>34")
- end
-
- it "returns last line of formatted html" do
- expect(trace.html(last_lines: 1)).to eq("34")
- end
- end
-
- describe '#raw' do
- before do
- trace.set("12\n34")
- end
-
- it "returns raw output" do
- expect(trace.raw).to eq("12\n34")
- end
-
- it "returns last line of raw output" do
- expect(trace.raw(last_lines: 1)).to eq("34")
- end
- end
-
- describe '#extract_coverage' do
- let(:regex) { '\(\d+.\d+\%\) covered' }
-
- context 'matching coverage' do
- before do
- trace.set('Coverage 1033 / 1051 LOC (98.29%) covered')
- end
-
- it "returns valid coverage" do
- expect(trace.extract_coverage(regex)).to eq("98.29")
- end
- end
-
- context 'no coverage' do
- before do
- trace.set('No coverage')
- end
-
- it 'returs nil' do
- expect(trace.extract_coverage(regex)).to be_nil
- end
- end
- end
-
- describe '#extract_sections' do
- let(:log) { 'No sections' }
- let(:sections) { trace.extract_sections }
-
- before do
- trace.set(log)
- end
-
- context 'no sections' do
- it 'returs []' do
- expect(trace.extract_sections).to eq([])
- end
- end
-
- context 'multiple sections available' do
- let(:log) { File.read(expand_fixture_path('trace/trace_with_sections')) }
- let(:sections_data) do
- [
- { name: 'prepare_script', lines: 2, duration: 3.seconds },
- { name: 'get_sources', lines: 4, duration: 1.second },
- { name: 'restore_cache', lines: 0, duration: 0.seconds },
- { name: 'download_artifacts', lines: 0, duration: 0.seconds },
- { name: 'build_script', lines: 2, duration: 1.second },
- { name: 'after_script', lines: 0, duration: 0.seconds },
- { name: 'archive_cache', lines: 0, duration: 0.seconds },
- { name: 'upload_artifacts', lines: 0, duration: 0.seconds }
- ]
- end
-
- it "returns valid sections" do
- expect(sections).not_to be_empty
- expect(sections.size).to eq(sections_data.size),
- "expected #{sections_data.size} sections, got #{sections.size}"
-
- buff = StringIO.new(log)
- sections.each_with_index do |s, i|
- expected = sections_data[i]
-
- expect(s[:name]).to eq(expected[:name])
- expect(s[:date_end] - s[:date_start]).to eq(expected[:duration])
-
- buff.seek(s[:byte_start], IO::SEEK_SET)
- length = s[:byte_end] - s[:byte_start]
- lines = buff.read(length).count("\n")
- expect(lines).to eq(expected[:lines])
- end
- end
- end
-
- context 'logs contains "section_start"' do
- let(:log) { "section_start:1506417476:a_section\r\033[0Klooks like a section_start:invalid\nsection_end:1506417477:a_section\r\033[0K"}
-
- it "returns only one section" do
- expect(sections).not_to be_empty
- expect(sections.size).to eq(1)
-
- section = sections[0]
- expect(section[:name]).to eq('a_section')
- expect(section[:byte_start]).not_to eq(section[:byte_end]), "got an empty section"
- end
- end
-
- context 'missing section_end' do
- let(:log) { "section_start:1506417476:a_section\r\033[0KSome logs\nNo section_end\n"}
-
- it "returns no sections" do
- expect(sections).to be_empty
- end
- end
-
- context 'missing section_start' do
- let(:log) { "Some logs\nNo section_start\nsection_end:1506417476:a_section\r\033[0K"}
-
- it "returns no sections" do
- expect(sections).to be_empty
- end
- end
-
- context 'inverted section_start section_end' do
- let(:log) { "section_end:1506417476:a_section\r\033[0Klooks like a section_start:invalid\nsection_start:1506417477:a_section\r\033[0K"}
-
- it "returns no sections" do
- expect(sections).to be_empty
- end
- end
- end
-
- describe '#set' do
- before do
- trace.set("12")
- end
-
- it "returns trace" do
- expect(trace.raw).to eq("12")
- end
-
- context 'overwrite trace' do
- before do
- trace.set("34")
- end
-
- it "returns new trace" do
- expect(trace.raw).to eq("34")
- end
- end
-
- context 'runners token' do
- let(:token) { 'my_secret_token' }
-
- before do
- build.project.update(runners_token: token)
- trace.set(token)
- end
-
- it "hides token" do
- expect(trace.raw).not_to include(token)
- end
- end
-
- context 'hides build token' do
- let(:token) { 'my_secret_token' }
-
- before do
- build.update(token: token)
- trace.set(token)
- end
-
- it "hides token" do
- expect(trace.raw).not_to include(token)
- end
- end
+ it_behaves_like 'trace with disabled live trace feature'
end
- describe '#append' do
+ context 'when live trace feature is enabled' do
before do
- trace.set("1234")
- end
-
- it "returns correct trace" do
- expect(trace.append("56", 4)).to eq(6)
- expect(trace.raw).to eq("123456")
- end
-
- context 'tries to append trace at different offset' do
- it "fails with append" do
- expect(trace.append("56", 2)).to eq(-4)
- expect(trace.raw).to eq("1234")
- end
- end
-
- context 'runners token' do
- let(:token) { 'my_secret_token' }
-
- before do
- build.project.update(runners_token: token)
- trace.append(token, 0)
- end
-
- it "hides token" do
- expect(trace.raw).not_to include(token)
- end
- end
-
- context 'build token' do
- let(:token) { 'my_secret_token' }
-
- before do
- build.update(token: token)
- trace.append(token, 0)
- end
-
- it "hides token" do
- expect(trace.raw).not_to include(token)
- end
- end
- end
-
- describe '#read' do
- shared_examples 'read successfully with IO' do
- it 'yields with source' do
- trace.read do |stream|
- expect(stream).to be_a(Gitlab::Ci::Trace::Stream)
- expect(stream.stream).to be_a(IO)
- end
- end
- end
-
- shared_examples 'read successfully with StringIO' do
- it 'yields with source' do
- trace.read do |stream|
- expect(stream).to be_a(Gitlab::Ci::Trace::Stream)
- expect(stream.stream).to be_a(StringIO)
- end
- end
- end
-
- shared_examples 'failed to read' do
- it 'yields without source' do
- trace.read do |stream|
- expect(stream).to be_a(Gitlab::Ci::Trace::Stream)
- expect(stream.stream).to be_nil
- end
- end
- end
-
- context 'when trace artifact exists' do
- before do
- create(:ci_job_artifact, :trace, job: build)
- end
-
- it_behaves_like 'read successfully with IO'
- end
-
- context 'when current_path (with project_id) exists' do
- before do
- expect(trace).to receive(:default_path) { expand_fixture_path('trace/sample_trace') }
- end
-
- it_behaves_like 'read successfully with IO'
- end
-
- context 'when current_path (with project_ci_id) exists' do
- before do
- expect(trace).to receive(:deprecated_path) { expand_fixture_path('trace/sample_trace') }
- end
-
- it_behaves_like 'read successfully with IO'
- end
-
- context 'when db trace exists' do
- before do
- build.send(:write_attribute, :trace, "data")
- end
-
- it_behaves_like 'read successfully with StringIO'
- end
-
- context 'when no sources exist' do
- it_behaves_like 'failed to read'
- end
- end
-
- describe 'trace handling' do
- subject { trace.exist? }
-
- context 'trace does not exist' do
- it { expect(trace.exist?).to be(false) }
- end
-
- context 'when trace artifact exists' do
- before do
- create(:ci_job_artifact, :trace, job: build)
- end
-
- it { is_expected.to be_truthy }
-
- context 'when the trace artifact has been erased' do
- before do
- trace.erase!
- end
-
- it { is_expected.to be_falsy }
-
- it 'removes associations' do
- expect(Ci::JobArtifact.exists?(job_id: build.id, file_type: :trace)).to be_falsy
- end
- end
- end
-
- context 'new trace path is used' do
- before do
- trace.send(:ensure_directory)
-
- File.open(trace.send(:default_path), "w") do |file|
- file.write("data")
- end
- end
-
- it "trace exist" do
- expect(trace.exist?).to be(true)
- end
-
- it "can be erased" do
- trace.erase!
- expect(trace.exist?).to be(false)
- end
- end
-
- context 'deprecated path' do
- let(:path) { trace.send(:deprecated_path) }
-
- context 'with valid ci_id' do
- before do
- build.project.update(ci_id: 1000)
-
- FileUtils.mkdir_p(File.dirname(path))
-
- File.open(path, "w") do |file|
- file.write("data")
- end
- end
-
- it "trace exist" do
- expect(trace.exist?).to be(true)
- end
-
- it "can be erased" do
- trace.erase!
- expect(trace.exist?).to be(false)
- end
- end
-
- context 'without valid ci_id' do
- it "does not return deprecated path" do
- expect(path).to be_nil
- end
- end
- end
-
- context 'stored in database' do
- before do
- build.send(:write_attribute, :trace, "data")
- end
-
- it "trace exist" do
- expect(trace.exist?).to be(true)
- end
-
- it "can be erased" do
- trace.erase!
- expect(trace.exist?).to be(false)
- end
-
- it "returns database data" do
- expect(trace.raw).to eq("data")
- end
- end
- end
-
- describe '#archive!' do
- subject { trace.archive! }
-
- shared_examples 'archive trace file' do
- it do
- expect { subject }.to change { Ci::JobArtifact.count }.by(1)
-
- build.reload
- expect(build.trace.exist?).to be_truthy
- expect(build.job_artifacts_trace.file.exists?).to be_truthy
- expect(build.job_artifacts_trace.file.filename).to eq('job.log')
- expect(File.exist?(src_path)).to be_falsy
- expect(src_checksum)
- .to eq(Digest::SHA256.file(build.job_artifacts_trace.file.path).hexdigest)
- expect(build.job_artifacts_trace.file_sha256).to eq(src_checksum)
- end
- end
-
- shared_examples 'source trace file stays intact' do |error:|
- it do
- expect { subject }.to raise_error(error)
-
- build.reload
- expect(build.trace.exist?).to be_truthy
- expect(build.job_artifacts_trace).to be_nil
- expect(File.exist?(src_path)).to be_truthy
- end
- end
-
- shared_examples 'archive trace in database' do
- it do
- expect { subject }.to change { Ci::JobArtifact.count }.by(1)
-
- build.reload
- expect(build.trace.exist?).to be_truthy
- expect(build.job_artifacts_trace.file.exists?).to be_truthy
- expect(build.job_artifacts_trace.file.filename).to eq('job.log')
- expect(build.old_trace).to be_nil
- expect(src_checksum)
- .to eq(Digest::SHA256.file(build.job_artifacts_trace.file.path).hexdigest)
- expect(build.job_artifacts_trace.file_sha256).to eq(src_checksum)
- end
- end
-
- shared_examples 'source trace in database stays intact' do |error:|
- it do
- expect { subject }.to raise_error(error)
-
- build.reload
- expect(build.trace.exist?).to be_truthy
- expect(build.job_artifacts_trace).to be_nil
- expect(build.old_trace).to eq(trace_content)
- end
- end
-
- context 'when job does not have trace artifact' do
- context 'when trace file stored in default path' do
- let!(:build) { create(:ci_build, :success, :trace_live) }
- let!(:src_path) { trace.read { |s| s.path } }
- let!(:src_checksum) { Digest::SHA256.file(src_path).hexdigest }
-
- it_behaves_like 'archive trace file'
-
- context 'when failed to create clone file' do
- before do
- allow(IO).to receive(:copy_stream).and_return(0)
- end
-
- it_behaves_like 'source trace file stays intact', error: Gitlab::Ci::Trace::ArchiveError
- end
-
- context 'when failed to create job artifact record' do
- before do
- allow_any_instance_of(Ci::JobArtifact).to receive(:save).and_return(false)
- allow_any_instance_of(Ci::JobArtifact).to receive_message_chain(:errors, :full_messages)
- .and_return(%w[Error Error])
- end
-
- it_behaves_like 'source trace file stays intact', error: ActiveRecord::RecordInvalid
- end
- end
-
- context 'when trace is stored in database' do
- let(:build) { create(:ci_build, :success) }
- let(:trace_content) { 'Sample trace' }
- let!(:src_checksum) { Digest::SHA256.hexdigest(trace_content) }
-
- before do
- build.update_column(:trace, trace_content)
- end
-
- it_behaves_like 'archive trace in database'
-
- context 'when failed to create clone file' do
- before do
- allow(IO).to receive(:copy_stream).and_return(0)
- end
-
- it_behaves_like 'source trace in database stays intact', error: Gitlab::Ci::Trace::ArchiveError
- end
-
- context 'when failed to create job artifact record' do
- before do
- allow_any_instance_of(Ci::JobArtifact).to receive(:save).and_return(false)
- allow_any_instance_of(Ci::JobArtifact).to receive_message_chain(:errors, :full_messages)
- .and_return(%w[Error Error])
- end
-
- it_behaves_like 'source trace in database stays intact', error: ActiveRecord::RecordInvalid
- end
-
- context 'when there is a validation error on Ci::Build' do
- before do
- allow_any_instance_of(Ci::Build).to receive(:save).and_return(false)
- allow_any_instance_of(Ci::Build).to receive_message_chain(:errors, :full_messages)
- .and_return(%w[Error Error])
- end
-
- context "when erase old trace with 'save'" do
- before do
- build.send(:write_attribute, :trace, nil)
- build.save
- end
-
- it 'old trace is not deleted' do
- build.reload
- expect(build.trace.raw).to eq(trace_content)
- end
- end
-
- it_behaves_like 'archive trace in database'
- end
- end
+ stub_feature_flags(ci_enable_live_trace: true)
end
- context 'when job has trace artifact' do
- before do
- create(:ci_job_artifact, :trace, job: build)
- end
-
- it 'does not archive' do
- expect_any_instance_of(described_class).not_to receive(:archive_stream!)
- expect { subject }.to raise_error('Already archived')
- expect(build.job_artifacts_trace.file.exists?).to be_truthy
- end
- end
-
- context 'when job is not finished yet' do
- let!(:build) { create(:ci_build, :running, :trace_live) }
-
- it 'does not archive' do
- expect_any_instance_of(described_class).not_to receive(:archive_stream!)
- expect { subject }.to raise_error('Job is not finished yet')
- expect(build.trace.exist?).to be_truthy
- end
- end
+ it_behaves_like 'trace with enabled live trace feature'
end
end
diff --git a/spec/lib/gitlab/data_builder/wiki_page_spec.rb b/spec/lib/gitlab/data_builder/wiki_page_spec.rb
index a776d888c47..9c8bdf4b032 100644
--- a/spec/lib/gitlab/data_builder/wiki_page_spec.rb
+++ b/spec/lib/gitlab/data_builder/wiki_page_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::DataBuilder::WikiPage do
- let(:project) { create(:project, :repository) }
+ set(:project) { create(:project, :repository, :wiki_repo) }
let(:wiki_page) { create(:wiki_page, wiki: project.wiki) }
let(:user) { create(:user) }
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 6568a0b1bb0..452249210b0 100644
--- a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
@@ -1,5 +1,4 @@
require 'spec_helper'
-require_relative '../email_shared_blocks'
describe Gitlab::Email::Handler::CreateIssueHandler do
include_context :email_shared_context
diff --git a/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb b/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb
index dc1a93367a4..43c6280f251 100644
--- a/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb
@@ -1,5 +1,4 @@
require 'spec_helper'
-require_relative '../email_shared_blocks'
describe Gitlab::Email::Handler::CreateMergeRequestHandler do
include_context :email_shared_context
diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
index 53899e00b53..950a7dd7d6c 100644
--- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
@@ -1,5 +1,4 @@
require 'spec_helper'
-require_relative '../email_shared_blocks'
describe Gitlab::Email::Handler::CreateNoteHandler do
include_context :email_shared_context
diff --git a/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb b/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb
index 21796694f26..ce160e11de2 100644
--- a/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb
@@ -1,5 +1,4 @@
require 'spec_helper'
-require_relative '../email_shared_blocks'
describe Gitlab::Email::Handler::UnsubscribeHandler do
include_context :email_shared_context
diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb
index 59f43abf26d..0af978eced3 100644
--- a/spec/lib/gitlab/email/receiver_spec.rb
+++ b/spec/lib/gitlab/email/receiver_spec.rb
@@ -1,5 +1,4 @@
require 'spec_helper'
-require_relative 'email_shared_blocks'
describe Gitlab::Email::Receiver do
include_context :email_shared_context
diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb
index 67d898e787e..e2547ed0311 100644
--- a/spec/lib/gitlab/git/blob_spec.rb
+++ b/spec/lib/gitlab/git/blob_spec.rb
@@ -251,6 +251,26 @@ describe Gitlab::Git::Blob, seed_helper: true do
end
end
+ describe '.batch_metadata' do
+ let(:blob_references) do
+ [
+ [SeedRepo::Commit::ID, "files/ruby/popen.rb"],
+ [SeedRepo::Commit::ID, 'six']
+ ]
+ end
+
+ subject { described_class.batch_metadata(repository, blob_references) }
+
+ it 'returns an empty data attribute' do
+ first_blob, last_blob = subject
+
+ expect(first_blob.data).to be_blank
+ expect(first_blob.path).to eq("files/ruby/popen.rb")
+ expect(last_blob.data).to be_blank
+ expect(last_blob.path).to eq("six")
+ end
+ end
+
describe '.batch_lfs_pointers' do
let(:tree_object) { repository.rugged.rev_parse('master^{tree}') }
diff --git a/spec/lib/gitlab/git/raw_diff_change_spec.rb b/spec/lib/gitlab/git/raw_diff_change_spec.rb
index eedde34534f..a0bb37fd84a 100644
--- a/spec/lib/gitlab/git/raw_diff_change_spec.rb
+++ b/spec/lib/gitlab/git/raw_diff_change_spec.rb
@@ -12,7 +12,7 @@ describe Gitlab::Git::RawDiffChange do
expect(change.operation).to eq(:unknown)
expect(change.old_path).to be_blank
expect(change.new_path).to be_blank
- expect(change.blob_size).to be_blank
+ expect(change.blob_size).to eq(0)
end
end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 9924641f829..cce84276fe3 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -1068,41 +1068,51 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe '#raw_changes_between' do
- let(:old_rev) { }
- let(:new_rev) { }
- let(:changes) { repository.raw_changes_between(old_rev, new_rev) }
+ shared_examples 'raw changes' do
+ let(:old_rev) { }
+ let(:new_rev) { }
+ let(:changes) { repository.raw_changes_between(old_rev, new_rev) }
- context 'initial commit' do
- let(:old_rev) { Gitlab::Git::BLANK_SHA }
- let(:new_rev) { '1a0b36b3cdad1d2ee32457c102a8c0b7056fa863' }
+ context 'initial commit' do
+ let(:old_rev) { Gitlab::Git::BLANK_SHA }
+ let(:new_rev) { '1a0b36b3cdad1d2ee32457c102a8c0b7056fa863' }
- it 'returns the changes' do
- expect(changes).to be_present
- expect(changes.size).to eq(3)
+ it 'returns the changes' do
+ expect(changes).to be_present
+ expect(changes.size).to eq(3)
+ end
end
- end
- context 'with an invalid rev' do
- let(:old_rev) { 'foo' }
- let(:new_rev) { 'bar' }
+ context 'with an invalid rev' do
+ let(:old_rev) { 'foo' }
+ let(:new_rev) { 'bar' }
- it 'returns an error' do
- expect { changes }.to raise_error(Gitlab::Git::Repository::GitError)
+ it 'returns an error' do
+ expect { changes }.to raise_error(Gitlab::Git::Repository::GitError)
+ end
end
- end
- context 'with valid revs' do
- let(:old_rev) { 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6' }
- let(:new_rev) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' }
+ context 'with valid revs' do
+ let(:old_rev) { 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6' }
+ let(:new_rev) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' }
- it 'returns the changes' do
- expect(changes.size).to eq(9)
- expect(changes.first.operation).to eq(:modified)
- expect(changes.first.new_path).to eq('.gitmodules')
- expect(changes.last.operation).to eq(:added)
- expect(changes.last.new_path).to eq('files/lfs/picture-invalid.png')
+ it 'returns the changes' do
+ expect(changes.size).to eq(9)
+ expect(changes.first.operation).to eq(:modified)
+ expect(changes.first.new_path).to eq('.gitmodules')
+ expect(changes.last.operation).to eq(:added)
+ expect(changes.last.new_path).to eq('files/lfs/picture-invalid.png')
+ end
end
end
+
+ context 'when gitaly is enabled' do
+ it_behaves_like 'raw changes'
+ end
+
+ context 'when gitaly is disabled', :disable_gitaly do
+ it_behaves_like 'raw changes'
+ end
end
describe '#merge_base' do
@@ -2265,7 +2275,22 @@ describe Gitlab::Git::Repository, seed_helper: true do
expect(empty_repo.checksum).to eq '0000000000000000000000000000000000000000'
end
- it 'raises a no repository exception when there is no repo' do
+ it 'raises Gitlab::Git::Repository::InvalidRepository error for non-valid git repo' do
+ FileUtils.rm_rf(File.join(storage_path, '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')
+
+ File.truncate(File.join(storage_path, 'non-valid.git/HEAD'), 0)
+
+ non_valid = described_class.new('default', 'non-valid.git', '')
+
+ expect { non_valid.checksum }.to raise_error(Gitlab::Git::Repository::InvalidRepository)
+ end
+
+ it 'raises Gitlab::Git::Repository::NoRepository error when there is no repo' do
broken_repo = described_class.new('default', 'a/path.git', '')
expect { broken_repo.checksum }.to raise_error(Gitlab::Git::Repository::NoRepository)
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index 6c625596605..317a932d5a6 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -1,7 +1,9 @@
require 'spec_helper'
describe Gitlab::GitAccess do
- set(:user) { create(:user) }
+ include TermsHelper
+
+ let(:user) { create(:user) }
let(:actor) { user }
let(:project) { create(:project, :repository) }
@@ -1040,6 +1042,96 @@ describe Gitlab::GitAccess do
end
end
+ context 'terms are enforced' do
+ before do
+ enforce_terms
+ end
+
+ shared_examples 'access after accepting terms' do
+ let(:actions) do
+ [-> { pull_access_check },
+ -> { push_access_check }]
+ end
+
+ it 'blocks access when the user did not accept terms', :aggregate_failures do
+ actions.each do |action|
+ expect { action.call }.to raise_unauthorized(/You must accept the Terms of Service in order to perform this action/)
+ end
+ end
+
+ it 'allows access when the user accepted the terms', :aggregate_failures do
+ accept_terms(user)
+
+ actions.each do |action|
+ expect { action.call }.not_to raise_error
+ end
+ end
+ end
+
+ describe 'as an anonymous user to a public project' do
+ let(:actor) { nil }
+ let(:project) { create(:project, :public, :repository) }
+
+ it { expect { pull_access_check }.not_to raise_error }
+ end
+
+ describe 'as a guest to a public project' do
+ let(:project) { create(:project, :public, :repository) }
+
+ it_behaves_like 'access after accepting terms' do
+ let(:actions) { [-> { pull_access_check }] }
+ end
+ end
+
+ describe 'as a reporter to the project' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it_behaves_like 'access after accepting terms' do
+ let(:actions) { [-> { pull_access_check }] }
+ end
+ end
+
+ describe 'as a developer of the project' do
+ before do
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'access after accepting terms'
+ end
+
+ describe 'as a master of the project' do
+ before do
+ project.add_master(user)
+ end
+
+ it_behaves_like 'access after accepting terms'
+ end
+
+ describe 'as an owner of the project' do
+ let(:project) { create(:project, :repository, namespace: user.namespace) }
+
+ it_behaves_like 'access after accepting terms'
+ end
+
+ describe 'when a ci build clones the project' do
+ let(:protocol) { 'http' }
+ let(:authentication_abilities) { [:build_download_code] }
+ let(:auth_result_type) { :build }
+
+ before do
+ project.add_developer(user)
+ end
+
+ it "doesn't block http pull" do
+ aggregate_failures do
+ expect { pull_access_check }.not_to raise_error
+ end
+ end
+ end
+ end
+
private
def raise_unauthorized(message)
diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
index ecd8657c406..1547d447197 100644
--- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
@@ -167,4 +167,15 @@ describe Gitlab::GitalyClient::RepositoryService do
client.create_from_snapshot('http://example.com?wiki=1', 'Custom xyz')
end
end
+
+ describe '#raw_changes_between' do
+ it 'sends a create_repository_from_snapshot message' do
+ expect_any_instance_of(Gitaly::RepositoryService::Stub)
+ .to receive(:get_raw_changes)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_return(double)
+
+ client.raw_changes_between('deadbeef', 'deadpork')
+ end
+ end
end
diff --git a/spec/lib/gitlab/gitaly_client/storage_settings_spec.rb b/spec/lib/gitlab/gitaly_client/storage_settings_spec.rb
new file mode 100644
index 00000000000..c89913ec8e9
--- /dev/null
+++ b/spec/lib/gitlab/gitaly_client/storage_settings_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe Gitlab::GitalyClient::StorageSettings do
+ describe "#initialize" do
+ context 'when the storage contains no path' do
+ it 'raises an error' do
+ expect do
+ described_class.new("foo" => {})
+ end.to raise_error(described_class::InvalidConfigurationError)
+ end
+ end
+
+ context "when the argument isn't a hash" do
+ it 'raises an error' do
+ expect do
+ described_class.new("test")
+ end.to raise_error("expected a Hash, got a String")
+ end
+ end
+
+ context 'when the storage is valid' do
+ it 'raises no error' do
+ expect do
+ described_class.new("path" => Rails.root)
+ end.not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 830d91de983..ad76adcc2e5 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -268,6 +268,7 @@ project:
- pages_domains
- authorized_users
- project_authorizations
+- remote_mirrors
- route
- redirect_routes
- statistics
@@ -276,6 +277,7 @@ project:
- import_state
- members_and_requesters
- build_trace_section_names
+- build_trace_chunks
- root_of_fork_network
- fork_network_member
- fork_network
diff --git a/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb b/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb
index d2bd8ccdf3f..24bc231d5a0 100644
--- a/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::ImportExport::WikiRepoSaver do
describe 'bundle a wiki Git repo' do
let(:user) { create(:user) }
- let!(:project) { create(:project, :public, name: 'searchable_project') }
+ let!(:project) { create(:project, :public, :wiki_repo, name: 'searchable_project') }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:shared) { project.import_export_shared }
let(:wiki_bundler) { described_class.new(project: project, shared: shared) }
diff --git a/spec/lib/gitlab/incoming_email_spec.rb b/spec/lib/gitlab/incoming_email_spec.rb
index c959add7a36..ad087f42e06 100644
--- a/spec/lib/gitlab/incoming_email_spec.rb
+++ b/spec/lib/gitlab/incoming_email_spec.rb
@@ -24,7 +24,7 @@ describe Gitlab::IncomingEmail do
end
describe 'self.supports_wildcard?' do
- context 'address contains the wildard placeholder' do
+ context 'address contains the wildcard placeholder' do
before do
stub_incoming_email_setting(address: 'replies+%{key}@example.com')
end
@@ -49,7 +49,7 @@ describe Gitlab::IncomingEmail do
stub_incoming_email_setting(address: nil)
end
- it 'returns that wildard is not supported' do
+ it 'returns that wildcard is not supported' do
expect(described_class.supports_wildcard?).to be_falsey
end
end
diff --git a/spec/lib/gitlab/metrics/prometheus_spec.rb b/spec/lib/gitlab/metrics/prometheus_spec.rb
new file mode 100644
index 00000000000..3d4dd5fdf01
--- /dev/null
+++ b/spec/lib/gitlab/metrics/prometheus_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe Gitlab::Metrics::Prometheus, :prometheus do
+ let(:all_metrics) { Gitlab::Metrics }
+ let(:registry) { all_metrics.registry }
+
+ describe '#reset_registry!' do
+ it 'clears existing metrics' do
+ registry.counter(:test, 'test metric')
+
+ expect(registry.metrics.count).to eq(1)
+
+ all_metrics.reset_registry!
+
+ expect(all_metrics.registry.metrics.count).to eq(0)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index 8351b967133..e3f705d2299 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -106,6 +106,18 @@ describe Gitlab::ProjectSearchResults do
end
end
+ context 'when the matching content contains multiple null bytes' do
+ let(:search_result) { "master:testdata/foo.txt\x001\x00blah\x001\x00foo" }
+
+ it 'returns a valid FoundBlob' do
+ expect(subject.filename).to eq('testdata/foo.txt')
+ expect(subject.basename).to eq('testdata/foo')
+ expect(subject.ref).to eq('master')
+ expect(subject.startline).to eq(1)
+ expect(subject.data).to eq("blah\x001\x00foo")
+ end
+ end
+
context 'when the search result ends with an empty line' do
let(:results) { project.repository.search_files_by_content('Role models', 'master') }
@@ -175,14 +187,14 @@ describe Gitlab::ProjectSearchResults do
end
describe 'wiki search' do
- let(:project) { create(:project, :public) }
+ let(:project) { create(:project, :public, :wiki_repo) }
let(:wiki) { build(:project_wiki, project: project) }
let!(:wiki_page) { wiki.create_page('Title', 'Content') }
subject(:results) { described_class.new(user, project, 'Content').objects('wiki_blobs') }
context 'when wiki is disabled' do
- let(:project) { create(:project, :public, :wiki_disabled) }
+ let(:project) { create(:project, :public, :wiki_repo, :wiki_disabled) }
it 'hides wiki blobs from members' do
project.add_reporter(user)
@@ -196,7 +208,7 @@ describe Gitlab::ProjectSearchResults do
end
context 'when wiki is internal' do
- let(:project) { create(:project, :public, :wiki_private) }
+ let(:project) { create(:project, :public, :wiki_repo, :wiki_private) }
it 'finds wiki blobs for guest' do
project.add_guest(user)
diff --git a/spec/lib/gitlab/repo_path_spec.rb b/spec/lib/gitlab/repo_path_spec.rb
index f030f371372..13940713dfc 100644
--- a/spec/lib/gitlab/repo_path_spec.rb
+++ b/spec/lib/gitlab/repo_path_spec.rb
@@ -45,25 +45,6 @@ describe ::Gitlab::RepoPath do
end
end
- describe '.strip_storage_path' do
- before do
- allow(Gitlab.config.repositories).to receive(:storages).and_return({
- 'storage1' => Gitlab::GitalyClient::StorageSettings.new('path' => '/foo'),
- 'storage2' => Gitlab::GitalyClient::StorageSettings.new('path' => '/bar')
- })
- end
-
- it 'strips the storage path' do
- expect(described_class.strip_storage_path('/bar/foo/qux/baz.git')).to eq('foo/qux/baz.git')
- end
-
- it 'raises NotFoundError if no storage matches the path' do
- expect { described_class.strip_storage_path('/doesnotexist/foo.git') }.to raise_error(
- described_class::NotFoundError
- )
- end
- end
-
describe '.find_project' do
let(:project) { create(:project) }
let(:redirect) { project.route.create_redirect('foo/bar/baz') }
diff --git a/spec/lib/gitlab/untrusted_regexp_spec.rb b/spec/lib/gitlab/untrusted_regexp_spec.rb
index bed58d407ef..0ee7fa1e570 100644
--- a/spec/lib/gitlab/untrusted_regexp_spec.rb
+++ b/spec/lib/gitlab/untrusted_regexp_spec.rb
@@ -39,6 +39,14 @@ describe Gitlab::UntrustedRegexp do
expect(result).to be_falsy
end
+
+ it 'can handle regular expressions in multiline mode' do
+ regexp = described_class.new('^\d', multiline: true)
+
+ result = regexp === "Header\n\n1. Content"
+
+ expect(result).to be_truthy
+ end
end
describe '#scan' do
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 9e6aa109a4b..a716e6f5434 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -96,6 +96,7 @@ describe Gitlab::UsageData do
pages_domains
protected_branches
releases
+ remote_mirrors
snippets
todos
uploads
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 43e419cd7de..84ddbbbf2ee 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -654,38 +654,6 @@ describe Notify do
allow(Note).to receive(:find).with(note.id).and_return(note)
end
- shared_examples 'a note email' do
- it_behaves_like 'it should have Gmail Actions links'
-
- it 'is sent to the given recipient as the author' do
- sender = subject.header[:from].addrs[0]
-
- aggregate_failures do
- expect(sender.display_name).to eq(note_author.name)
- expect(sender.address).to eq(gitlab_sender)
- expect(subject).to deliver_to(recipient.notification_email)
- end
- end
-
- it 'contains the message from the note' do
- is_expected.to have_html_escaped_body_text note.note
- end
-
- it 'does not contain note author' do
- is_expected.not_to have_body_text note.author_name
- end
-
- context 'when enabled email_author_in_body' do
- before do
- stub_application_setting(email_author_in_body: true)
- end
-
- it 'contains a link to note author' do
- is_expected.to have_html_escaped_body_text note.author_name
- end
- end
- end
-
describe 'on a commit' do
let(:commit) { project.commit }
diff --git a/spec/migrations/add_not_null_constraint_to_project_mirror_data_foreign_key_spec.rb b/spec/migrations/add_not_null_constraint_to_project_mirror_data_foreign_key_spec.rb
new file mode 100644
index 00000000000..6fd3cb1f44e
--- /dev/null
+++ b/spec/migrations/add_not_null_constraint_to_project_mirror_data_foreign_key_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20180508100222_add_not_null_constraint_to_project_mirror_data_foreign_key.rb')
+
+describe AddNotNullConstraintToProjectMirrorDataForeignKey, :migration do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:import_state) { table(:project_mirror_data) }
+
+ before do
+ import_state.create!(id: 1, project_id: nil, status: :started)
+ end
+
+ it 'removes every import state without an associated project_id' do
+ expect do
+ subject.up
+ end.to change { import_state.count }.from(1).to(0)
+ end
+end
diff --git a/spec/migrations/add_pipeline_build_foreign_key_spec.rb b/spec/migrations/add_pipeline_build_foreign_key_spec.rb
new file mode 100644
index 00000000000..e9413f52f19
--- /dev/null
+++ b/spec/migrations/add_pipeline_build_foreign_key_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20180420010016_add_pipeline_build_foreign_key.rb')
+
+describe AddPipelineBuildForeignKey, :migration do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:pipelines) { table(:ci_pipelines) }
+ let(:builds) { table(:ci_builds) }
+
+ before do
+ namespaces.create(id: 10, name: 'gitlab-org', path: 'gitlab-org')
+ projects.create!(id: 11, namespace_id: 10, name: 'gitlab', path: 'gitlab')
+ pipelines.create!(id: 12, project_id: 11, ref: 'master', sha: 'adf43c3a')
+
+ builds.create!(id: 101, commit_id: 12, project_id: 11)
+ builds.create!(id: 102, commit_id: 222, project_id: 11)
+ builds.create!(id: 103, commit_id: 333, project_id: 11)
+ builds.create!(id: 104, commit_id: 12, project_id: 11)
+ builds.create!(id: 106, commit_id: nil, project_id: 11)
+ builds.create!(id: 107, commit_id: 12, project_id: nil)
+ end
+
+ it 'adds foreign key after removing orphans' do
+ expect(builds.all.count).to eq 6
+ expect(foreign_key_exists?(:ci_builds, :ci_pipelines, column: :commit_id)).to be_falsey
+
+ migrate!
+
+ expect(builds.all.pluck(:id)).to eq [101, 104]
+ expect(foreign_key_exists?(:ci_builds, :ci_pipelines, column: :commit_id)).to be_truthy
+ end
+end
diff --git a/spec/migrations/add_unique_constraint_to_project_features_project_id_spec.rb b/spec/migrations/add_unique_constraint_to_project_features_project_id_spec.rb
new file mode 100644
index 00000000000..bf299b70a29
--- /dev/null
+++ b/spec/migrations/add_unique_constraint_to_project_features_project_id_spec.rb
@@ -0,0 +1,59 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20180511174224_add_unique_constraint_to_project_features_project_id.rb')
+
+describe AddUniqueConstraintToProjectFeaturesProjectId, :migration do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:features) { table(:project_features) }
+ let(:migration) { described_class.new }
+
+ describe '#up' do
+ before do
+ (1..3).each do |i|
+ namespaces.create(id: i, name: "ns-test-#{i}", path: "ns-test-i#{i}")
+ projects.create!(id: i, name: "test-#{i}", path: "test-#{i}", namespace_id: i)
+ end
+
+ features.create!(id: 1, project_id: 1)
+ features.create!(id: 2, project_id: 1)
+ features.create!(id: 3, project_id: 2)
+ features.create!(id: 4, project_id: 2)
+ features.create!(id: 5, project_id: 2)
+ features.create!(id: 6, project_id: 3)
+ end
+
+ it 'creates a unique index and removes duplicates' do
+ expect(migration.index_exists?(:project_features, :project_id, unique: false, name: 'index_project_features_on_project_id')).to be true
+
+ expect { migration.up }.to change { features.count }.from(6).to(3)
+
+ expect(migration.index_exists?(:project_features, :project_id, unique: true, name: 'index_project_features_on_project_id')).to be true
+ expect(migration.index_exists?(:project_features, :project_id, name: 'index_project_features_on_project_id_unique')).to be false
+
+ project_1_features = features.where(project_id: 1)
+ expect(project_1_features.count).to eq(1)
+ expect(project_1_features.first.id).to eq(2)
+
+ project_2_features = features.where(project_id: 2)
+ expect(project_2_features.count).to eq(1)
+ expect(project_2_features.first.id).to eq(5)
+
+ project_3_features = features.where(project_id: 3)
+ expect(project_3_features.count).to eq(1)
+ expect(project_3_features.first.id).to eq(6)
+ end
+ end
+
+ describe '#down' do
+ it 'restores the original index' do
+ migration.up
+
+ expect(migration.index_exists?(:project_features, :project_id, unique: true, name: 'index_project_features_on_project_id')).to be true
+
+ migration.down
+
+ expect(migration.index_exists?(:project_features, :project_id, unique: false, name: 'index_project_features_on_project_id')).to be true
+ expect(migration.index_exists?(:project_features, :project_id, name: 'index_project_features_on_project_id_old')).to be false
+ end
+ end
+end
diff --git a/spec/migrations/cleanup_build_stage_migration_spec.rb b/spec/migrations/cleanup_build_stage_migration_spec.rb
new file mode 100644
index 00000000000..4d4d02aaa94
--- /dev/null
+++ b/spec/migrations/cleanup_build_stage_migration_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20180420010616_cleanup_build_stage_migration.rb')
+
+describe CleanupBuildStageMigration, :migration, :sidekiq, :redis do
+ let(:migration) { spy('migration') }
+
+ before do
+ allow(Gitlab::BackgroundMigration::MigrateBuildStage)
+ .to receive(:new).and_return(migration)
+ end
+
+ context 'when there are pending background migrations' do
+ it 'processes pending jobs synchronously' do
+ Sidekiq::Testing.disable! do
+ BackgroundMigrationWorker
+ .perform_in(2.minutes, 'MigrateBuildStage', [1, 1])
+ BackgroundMigrationWorker
+ .perform_async('MigrateBuildStage', [1, 1])
+
+ migrate!
+
+ expect(migration).to have_received(:perform).with(1, 1).twice
+ end
+ end
+ end
+
+ context 'when there are no background migrations pending' do
+ it 'does nothing' do
+ Sidekiq::Testing.disable! do
+ migrate!
+
+ expect(migration).not_to have_received(:perform)
+ end
+ end
+ end
+
+ context 'when there are still unmigrated builds present' do
+ let(:builds) { table('ci_builds') }
+
+ before do
+ builds.create!(name: 'test:1', ref: 'master')
+ builds.create!(name: 'test:2', ref: 'master')
+ end
+
+ it 'migrates stages sequentially in batches' do
+ expect(builds.all).to all(have_attributes(stage_id: nil))
+
+ migrate!
+
+ expect(migration).to have_received(:perform).once
+ end
+ end
+end
diff --git a/spec/models/blob_viewer/readme_spec.rb b/spec/models/blob_viewer/readme_spec.rb
index b9946c0315a..8d11d58cfca 100644
--- a/spec/models/blob_viewer/readme_spec.rb
+++ b/spec/models/blob_viewer/readme_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe BlobViewer::Readme do
include FakeBlobHelpers
- let(:project) { create(:project, :repository) }
+ let(:project) { create(:project, :repository, :wiki_repo) }
let(:blob) { fake_blob(path: 'README.md') }
subject { described_class.new(blob) }
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 3158e006720..dc810489011 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -1518,7 +1518,10 @@ describe Ci::Build do
{ 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_PIPELINE_SOURCE', value: pipeline.source, public: true },
+ { key: 'CI_COMMIT_MESSAGE', value: pipeline.git_commit_message, public: true },
+ { key: 'CI_COMMIT_TITLE', value: pipeline.git_commit_title, public: true },
+ { key: 'CI_COMMIT_DESCRIPTION', value: pipeline.git_commit_description, public: true }
]
end
diff --git a/spec/models/ci/build_trace_chunk_spec.rb b/spec/models/ci/build_trace_chunk_spec.rb
new file mode 100644
index 00000000000..cbcf1e55979
--- /dev/null
+++ b/spec/models/ci/build_trace_chunk_spec.rb
@@ -0,0 +1,396 @@
+require 'spec_helper'
+
+describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
+ set(:build) { create(:ci_build, :running) }
+ let(:chunk_index) { 0 }
+ let(:data_store) { :redis }
+ let(:raw_data) { nil }
+
+ let(:build_trace_chunk) do
+ described_class.new(build: build, chunk_index: chunk_index, data_store: data_store, raw_data: raw_data)
+ end
+
+ before do
+ stub_feature_flags(ci_enable_live_trace: true)
+ end
+
+ context 'FastDestroyAll' do
+ let(:parent) { create(:project) }
+ let(:pipeline) { create(:ci_pipeline, project: parent) }
+ let(:build) { create(:ci_build, :running, :trace_live, pipeline: pipeline, project: parent) }
+ let(:subjects) { build.trace_chunks }
+
+ it_behaves_like 'fast destroyable'
+
+ def external_data_counter
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.scan_each(match: "gitlab:ci:trace:*:chunks:*").to_a.size
+ end
+ end
+ end
+
+ describe 'CHUNK_SIZE' do
+ it 'Chunk size can not be changed without special care' do
+ expect(described_class::CHUNK_SIZE).to eq(128.kilobytes)
+ end
+ end
+
+ describe '#data' do
+ subject { build_trace_chunk.data }
+
+ context 'when data_store is redis' do
+ let(:data_store) { :redis }
+
+ before do
+ build_trace_chunk.send(:redis_set_data, 'Sample data in redis')
+ end
+
+ it { is_expected.to eq('Sample data in redis') }
+ end
+
+ context 'when data_store is database' do
+ let(:data_store) { :db }
+ let(:raw_data) { 'Sample data in db' }
+
+ it { is_expected.to eq('Sample data in db') }
+ end
+
+ context 'when data_store is others' do
+ before do
+ build_trace_chunk.send(:write_attribute, :data_store, -1)
+ end
+
+ it { expect { subject }.to raise_error('Unsupported data store') }
+ end
+ end
+
+ describe '#set_data' do
+ subject { build_trace_chunk.send(:set_data, value) }
+
+ let(:value) { 'Sample data' }
+
+ context 'when value bytesize is bigger than CHUNK_SIZE' do
+ let(:value) { 'a' * (described_class::CHUNK_SIZE + 1) }
+
+ it { expect { subject }.to raise_error('too much data') }
+ end
+
+ context 'when data_store is redis' do
+ let(:data_store) { :redis }
+
+ it do
+ expect(build_trace_chunk.send(:redis_data)).to be_nil
+
+ subject
+
+ expect(build_trace_chunk.send(:redis_data)).to eq(value)
+ end
+
+ context 'when fullfilled chunk size' do
+ let(:value) { 'a' * described_class::CHUNK_SIZE }
+
+ it 'schedules stashing data' do
+ expect(Ci::BuildTraceChunkFlushWorker).to receive(:perform_async).once
+
+ subject
+ end
+ end
+ end
+
+ context 'when data_store is database' do
+ let(:data_store) { :db }
+
+ it 'sets data' do
+ expect(build_trace_chunk.raw_data).to be_nil
+
+ subject
+
+ expect(build_trace_chunk.raw_data).to eq(value)
+ expect(build_trace_chunk.persisted?).to be_truthy
+ end
+
+ context 'when raw_data is not changed' do
+ it 'does not execute UPDATE' do
+ expect(build_trace_chunk.raw_data).to be_nil
+ build_trace_chunk.save!
+
+ # First set
+ expect(ActiveRecord::QueryRecorder.new { subject }.count).to be > 0
+ expect(build_trace_chunk.raw_data).to eq(value)
+ expect(build_trace_chunk.persisted?).to be_truthy
+
+ # Second set
+ build_trace_chunk.reload
+ expect(ActiveRecord::QueryRecorder.new { subject }.count).to be(0)
+ end
+ end
+
+ context 'when fullfilled chunk size' do
+ it 'does not schedule stashing data' do
+ expect(Ci::BuildTraceChunkFlushWorker).not_to receive(:perform_async)
+
+ subject
+ end
+ end
+ end
+
+ context 'when data_store is others' do
+ before do
+ build_trace_chunk.send(:write_attribute, :data_store, -1)
+ end
+
+ it { expect { subject }.to raise_error('Unsupported data store') }
+ end
+ end
+
+ describe '#truncate' do
+ subject { build_trace_chunk.truncate(offset) }
+
+ shared_examples_for 'truncates' do
+ context 'when offset is negative' do
+ let(:offset) { -1 }
+
+ it { expect { subject }.to raise_error('Offset is out of range') }
+ end
+
+ context 'when offset is bigger than data size' do
+ let(:offset) { data.bytesize + 1 }
+
+ it { expect { subject }.to raise_error('Offset is out of range') }
+ end
+
+ context 'when offset is 10' do
+ let(:offset) { 10 }
+
+ it 'truncates' do
+ subject
+
+ expect(build_trace_chunk.data).to eq(data.byteslice(0, offset))
+ end
+ end
+ end
+
+ context 'when data_store is redis' do
+ let(:data_store) { :redis }
+ let(:data) { 'Sample data in redis' }
+
+ before do
+ build_trace_chunk.send(:redis_set_data, data)
+ end
+
+ it_behaves_like 'truncates'
+ end
+
+ context 'when data_store is database' do
+ let(:data_store) { :db }
+ let(:raw_data) { 'Sample data in db' }
+ let(:data) { raw_data }
+
+ it_behaves_like 'truncates'
+ end
+ end
+
+ describe '#append' do
+ subject { build_trace_chunk.append(new_data, offset) }
+
+ let(:new_data) { 'Sample new data' }
+ let(:offset) { 0 }
+ let(:total_data) { data + new_data }
+
+ shared_examples_for 'appends' do
+ context 'when offset is negative' do
+ let(:offset) { -1 }
+
+ it { expect { subject }.to raise_error('Offset is out of range') }
+ end
+
+ context 'when offset is bigger than data size' do
+ let(:offset) { data.bytesize + 1 }
+
+ it { expect { subject }.to raise_error('Offset is out of range') }
+ end
+
+ context 'when offset is bigger than data size' do
+ let(:new_data) { 'a' * (described_class::CHUNK_SIZE + 1) }
+
+ it { expect { subject }.to raise_error('Chunk size overflow') }
+ end
+
+ context 'when offset is EOF' do
+ let(:offset) { data.bytesize }
+
+ it 'appends' do
+ subject
+
+ expect(build_trace_chunk.data).to eq(total_data)
+ end
+ end
+
+ context 'when offset is 10' do
+ let(:offset) { 10 }
+
+ it 'appends' do
+ subject
+
+ expect(build_trace_chunk.data).to eq(data.byteslice(0, offset) + new_data)
+ end
+ end
+ end
+
+ context 'when data_store is redis' do
+ let(:data_store) { :redis }
+ let(:data) { 'Sample data in redis' }
+
+ before do
+ build_trace_chunk.send(:redis_set_data, data)
+ end
+
+ it_behaves_like 'appends'
+ end
+
+ context 'when data_store is database' do
+ let(:data_store) { :db }
+ let(:raw_data) { 'Sample data in db' }
+ let(:data) { raw_data }
+
+ it_behaves_like 'appends'
+ end
+ end
+
+ describe '#size' do
+ subject { build_trace_chunk.size }
+
+ context 'when data_store is redis' do
+ let(:data_store) { :redis }
+
+ context 'when data exists' do
+ let(:data) { 'Sample data in redis' }
+
+ before do
+ build_trace_chunk.send(:redis_set_data, data)
+ end
+
+ it { is_expected.to eq(data.bytesize) }
+ end
+
+ context 'when data exists' do
+ it { is_expected.to eq(0) }
+ end
+ end
+
+ context 'when data_store is database' do
+ let(:data_store) { :db }
+
+ context 'when data exists' do
+ let(:raw_data) { 'Sample data in db' }
+ let(:data) { raw_data }
+
+ it { is_expected.to eq(data.bytesize) }
+ end
+
+ context 'when data does not exist' do
+ it { is_expected.to eq(0) }
+ end
+ end
+ end
+
+ describe '#use_database!' do
+ subject { build_trace_chunk.use_database! }
+
+ context 'when data_store is redis' do
+ let(:data_store) { :redis }
+
+ context 'when data exists' do
+ let(:data) { 'Sample data in redis' }
+
+ before do
+ build_trace_chunk.send(:redis_set_data, data)
+ end
+
+ it 'stashes the data' do
+ expect(build_trace_chunk.data_store).to eq('redis')
+ expect(build_trace_chunk.send(:redis_data)).to eq(data)
+ expect(build_trace_chunk.raw_data).to be_nil
+
+ subject
+
+ expect(build_trace_chunk.data_store).to eq('db')
+ expect(build_trace_chunk.send(:redis_data)).to be_nil
+ expect(build_trace_chunk.raw_data).to eq(data)
+ end
+ end
+
+ context 'when data does not exist' do
+ it 'does not call UPDATE' do
+ expect(ActiveRecord::QueryRecorder.new { subject }.count).to eq(0)
+ end
+ end
+ end
+
+ context 'when data_store is database' do
+ let(:data_store) { :db }
+
+ it 'does not call UPDATE' do
+ expect(ActiveRecord::QueryRecorder.new { subject }.count).to eq(0)
+ end
+ end
+ end
+
+ describe 'ExclusiveLock' do
+ before do
+ allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain) { nil }
+ stub_const('Ci::BuildTraceChunk::WRITE_LOCK_RETRY', 1)
+ end
+
+ it 'raise an error' do
+ expect { build_trace_chunk.append('ABC', 0) }.to raise_error('Failed to obtain write lock')
+ end
+ end
+
+ describe 'deletes data in redis after a parent record destroyed' do
+ let(:project) { create(:project) }
+
+ before do
+ pipeline = create(:ci_pipeline, project: project)
+ create(:ci_build, :running, :trace_live, pipeline: pipeline, project: project)
+ create(:ci_build, :running, :trace_live, pipeline: pipeline, project: project)
+ create(:ci_build, :running, :trace_live, pipeline: pipeline, project: project)
+ end
+
+ shared_examples_for 'deletes all build_trace_chunk and data in redis' do
+ it do
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.scan_each(match: "gitlab:ci:trace:*:chunks:*").to_a.size).to eq(3)
+ end
+
+ expect(described_class.count).to eq(3)
+
+ subject
+
+ expect(described_class.count).to eq(0)
+
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.scan_each(match: "gitlab:ci:trace:*:chunks:*").to_a.size).to eq(0)
+ end
+ end
+ end
+
+ context 'when traces are archived' do
+ let(:subject) do
+ project.builds.each do |build|
+ build.success!
+ end
+ end
+
+ it_behaves_like 'deletes all build_trace_chunk and data in redis'
+ end
+
+ context 'when project is destroyed' do
+ let(:subject) do
+ project.destroy!
+ end
+
+ it_behaves_like 'deletes all build_trace_chunk and data in redis'
+ end
+ end
+end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index dd94515b0a4..ddd66a6be87 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -173,7 +173,7 @@ describe Ci::Pipeline, :mailer do
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]
+ expect(keys).to eq %w[CI_PIPELINE_ID CI_CONFIG_PATH CI_PIPELINE_SOURCE CI_COMMIT_MESSAGE CI_COMMIT_TITLE CI_COMMIT_DESCRIPTION]
end
end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index cc4d4e5e4ae..e2b212f4f4c 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
describe Ci::Runner do
describe 'validation' do
it { is_expected.to validate_presence_of(:access_level) }
+ it { is_expected.to validate_presence_of(:runner_type) }
context 'when runner is not allowed to pick untagged jobs' do
context 'when runner does not have tags' do
@@ -198,16 +199,30 @@ describe Ci::Runner do
end
describe '#assign_to' do
- let!(:project) { FactoryBot.create :project }
- let!(:shared_runner) { FactoryBot.create(:ci_runner, :shared) }
+ let!(:project) { FactoryBot.create(:project) }
- before do
- shared_runner.assign_to(project)
+ subject { runner.assign_to(project) }
+
+ context 'with shared_runner' do
+ let!(:runner) { FactoryBot.create(:ci_runner, :shared) }
+
+ it 'transitions shared runner to project runner and assigns project' do
+ subject
+ expect(runner).to be_specific
+ expect(runner).to be_project_type
+ expect(runner.projects).to eq([project])
+ expect(runner.only_for?(project)).to be_truthy
+ end
end
- it { expect(shared_runner).to be_specific }
- it { expect(shared_runner.projects).to eq([project]) }
- it { expect(shared_runner.only_for?(project)).to be_truthy }
+ context 'with group runner' do
+ let!(:runner) { FactoryBot.create(:ci_runner, runner_type: :group_type) }
+
+ it 'raises an error' do
+ expect { subject }
+ .to raise_error(ArgumentError, 'Transitioning a group runner to a project runner is not supported')
+ end
+ end
end
describe '.online' do
diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb
index 64d995a73c1..3ef59457c5f 100644
--- a/spec/models/clusters/applications/runner_spec.rb
+++ b/spec/models/clusters/applications/runner_spec.rb
@@ -55,13 +55,9 @@ describe Clusters::Applications::Runner do
context 'without a runner' do
let(:project) { create(:project) }
- let(:cluster) { create(:cluster) }
+ let(:cluster) { create(:cluster, projects: [project]) }
let(:gitlab_runner) { create(:clusters_applications_runner, cluster: cluster) }
- before do
- cluster.projects << project
- end
-
it 'creates a runner' do
expect do
subject
@@ -74,9 +70,8 @@ describe Clusters::Applications::Runner do
it 'assigns the new runner to runner' do
subject
- gitlab_runner.reload
- expect(gitlab_runner.runner).not_to be_nil
+ expect(gitlab_runner.reload.runner).to be_project_type
end
end
diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb
index add481b8096..ab7f89f9bf4 100644
--- a/spec/models/clusters/platforms/kubernetes_spec.rb
+++ b/spec/models/clusters/platforms/kubernetes_spec.rb
@@ -239,17 +239,19 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
it { is_expected.to be_nil }
end
- context 'when kubernetes responds with valid pods' do
+ context 'when kubernetes responds with valid pods and deployments' do
before do
stub_kubeclient_pods
+ stub_kubeclient_deployments
end
- it { is_expected.to eq(pods: [kube_pod]) }
+ it { is_expected.to include(pods: [kube_pod]) }
end
context 'when kubernetes responds with 500s' do
before do
stub_kubeclient_pods(status: 500)
+ stub_kubeclient_deployments(status: 500)
end
it { expect { subject }.to raise_error(Kubeclient::HttpError) }
@@ -258,9 +260,10 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
context 'when kubernetes responds with 404s' do
before do
stub_kubeclient_pods(status: 404)
+ stub_kubeclient_deployments(status: 404)
end
- it { is_expected.to eq(pods: []) }
+ it { is_expected.to include(pods: []) }
end
end
end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 05693f067e1..3d3092b8ac9 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -495,6 +495,14 @@ describe Issuable do
expect(issue.total_time_spent).to eq(1800)
end
+
+ it 'updates issues updated_at' do
+ issue
+
+ Timecop.travel(1.minute.from_now) do
+ expect { spend_time(1800) }.to change { issue.updated_at }
+ end
+ end
end
context 'substracting time' do
@@ -510,9 +518,13 @@ describe Issuable do
context 'when time to substract exceeds the total time spent' do
it 'raise a validation error' do
- expect do
- spend_time(-3600)
- end.to raise_error(ActiveRecord::RecordInvalid)
+ Timecop.travel(1.minute.from_now) do
+ expect do
+ expect do
+ spend_time(-3600)
+ end.to raise_error(ActiveRecord::RecordInvalid)
+ end.not_to change { issue.updated_at }
+ end
end
end
end
diff --git a/spec/models/concerns/reactive_caching_spec.rb b/spec/models/concerns/reactive_caching_spec.rb
index a5d505af001..4570dbb1d8e 100644
--- a/spec/models/concerns/reactive_caching_spec.rb
+++ b/spec/models/concerns/reactive_caching_spec.rb
@@ -29,12 +29,6 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
end
end
- let(:now) { Time.now.utc }
-
- around do |example|
- Timecop.freeze(now) { example.run }
- end
-
let(:calculation) { -> { 2 + 2 } }
let(:cache_key) { "foo:666" }
let(:instance) { CacheTest.new(666, &calculation) }
@@ -49,13 +43,15 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
context 'when cache is empty' do
it { is_expected.to be_nil }
- it 'queues a background worker' do
+ it 'enqueues a background worker to bootstrap the cache' do
expect(ReactiveCachingWorker).to receive(:perform_async).with(CacheTest, 666)
go!
end
it 'updates the cache lifespan' do
+ expect(reactive_cache_alive?(instance)).to be_falsy
+
go!
expect(reactive_cache_alive?(instance)).to be_truthy
@@ -69,6 +65,18 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
it { is_expected.to eq(2) }
+ it 'does not enqueue a background worker' do
+ expect(ReactiveCachingWorker).not_to receive(:perform_async)
+
+ go!
+ end
+
+ it 'updates the cache lifespan' do
+ expect(Rails.cache).to receive(:write).with(alive_reactive_cache_key(instance), true, expires_in: anything)
+
+ go!
+ end
+
context 'and expired' do
before do
invalidate_reactive_cache(instance)
diff --git a/spec/models/concerns/sha_attribute_spec.rb b/spec/models/concerns/sha_attribute_spec.rb
index 21893e0cbaa..0d3beb6a6e3 100644
--- a/spec/models/concerns/sha_attribute_spec.rb
+++ b/spec/models/concerns/sha_attribute_spec.rb
@@ -13,33 +13,76 @@ describe ShaAttribute do
end
describe '#sha_attribute' do
- context 'when the table exists' do
+ context 'when in non-production' do
before do
- allow(model).to receive(:table_exists?).and_return(true)
+ allow(Rails.env).to receive(:production?).and_return(false)
end
- it 'defines a SHA attribute for a binary column' do
- expect(model).to receive(:attribute)
- .with(:sha1, an_instance_of(Gitlab::Database::ShaAttribute))
+ context 'when the table exists' do
+ before do
+ allow(model).to receive(:table_exists?).and_return(true)
+ end
- model.sha_attribute(:sha1)
+ it 'defines a SHA attribute for a binary column' do
+ expect(model).to receive(:attribute)
+ .with(:sha1, an_instance_of(Gitlab::Database::ShaAttribute))
+
+ model.sha_attribute(:sha1)
+ end
+
+ it 'raises ArgumentError when the column type is not :binary' do
+ expect { model.sha_attribute(:name) }.to raise_error(ArgumentError)
+ end
+ end
+
+ context 'when the table does not exist' do
+ it 'allows the attribute to be added and issues a warning' do
+ allow(model).to receive(:table_exists?).and_return(false)
+
+ expect(model).not_to receive(:columns)
+ expect(model).to receive(:attribute)
+ expect(model).to receive(:warn)
+
+ model.sha_attribute(:name)
+ end
end
- it 'raises ArgumentError when the column type is not :binary' do
- expect { model.sha_attribute(:name) }.to raise_error(ArgumentError)
+ context 'when the column does not exist' do
+ it 'allows the attribute to be added and issues a warning' do
+ allow(model).to receive(:table_exists?).and_return(true)
+
+ expect(model).to receive(:columns)
+ expect(model).to receive(:attribute)
+ expect(model).to receive(:warn)
+
+ model.sha_attribute(:no_name)
+ end
+ end
+
+ context 'when other execeptions are raised' do
+ it 'logs and re-rasises the error' do
+ allow(model).to receive(:table_exists?).and_raise(ActiveRecord::NoDatabaseError.new('does not exist'))
+
+ expect(model).not_to receive(:columns)
+ expect(model).not_to receive(:attribute)
+ expect(Gitlab::AppLogger).to receive(:error)
+
+ expect { model.sha_attribute(:name) }.to raise_error(ActiveRecord::NoDatabaseError)
+ end
end
end
- context 'when the table does not exist' do
+ context 'when in production' do
before do
- allow(model).to receive(:table_exists?).and_return(false)
+ allow(Rails.env).to receive(:production?).and_return(true)
end
- it 'does nothing' do
+ it 'defines a SHA attribute' do
+ expect(model).not_to receive(:table_exists?)
expect(model).not_to receive(:columns)
- expect(model).not_to receive(:attribute)
+ expect(model).to receive(:attribute).with(:sha1, an_instance_of(Gitlab::Database::ShaAttribute))
- model.sha_attribute(:name)
+ model.sha_attribute(:sha1)
end
end
end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index d620943693c..0907d28d33b 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -424,6 +424,95 @@ describe Group do
end
end
+ describe '#direct_and_indirect_members', :nested_groups do
+ let!(:group) { create(:group, :nested) }
+ let!(:sub_group) { create(:group, parent: group) }
+ let!(:master) { group.parent.add_user(create(:user), GroupMember::MASTER) }
+ let!(:developer) { group.add_user(create(:user), GroupMember::DEVELOPER) }
+ let!(:other_developer) { group.add_user(create(:user), GroupMember::DEVELOPER) }
+
+ it 'returns parents members' do
+ expect(group.direct_and_indirect_members).to include(developer)
+ expect(group.direct_and_indirect_members).to include(master)
+ end
+
+ it 'returns descendant members' do
+ expect(group.direct_and_indirect_members).to include(other_developer)
+ end
+ end
+
+ describe '#users_with_descendants', :nested_groups do
+ let(:user_a) { create(:user) }
+ let(:user_b) { create(:user) }
+
+ let(:group) { create(:group) }
+ let(:nested_group) { create(:group, parent: group) }
+ let(:deep_nested_group) { create(:group, parent: nested_group) }
+
+ it 'returns member users on every nest level without duplication' do
+ group.add_developer(user_a)
+ nested_group.add_developer(user_b)
+ deep_nested_group.add_developer(user_a)
+
+ expect(group.users_with_descendants).to contain_exactly(user_a, user_b)
+ expect(nested_group.users_with_descendants).to contain_exactly(user_a, user_b)
+ expect(deep_nested_group.users_with_descendants).to contain_exactly(user_a)
+ end
+ end
+
+ describe '#direct_and_indirect_users', :nested_groups do
+ let(:user_a) { create(:user) }
+ let(:user_b) { create(:user) }
+ let(:user_c) { create(:user) }
+ let(:user_d) { create(:user) }
+
+ let(:group) { create(:group) }
+ let(:nested_group) { create(:group, parent: group) }
+ let(:deep_nested_group) { create(:group, parent: nested_group) }
+ let(:project) { create(:project, namespace: group) }
+
+ before do
+ group.add_developer(user_a)
+ group.add_developer(user_c)
+ nested_group.add_developer(user_b)
+ deep_nested_group.add_developer(user_a)
+ project.add_developer(user_d)
+ end
+
+ it 'returns member users on every nest level without duplication' do
+ expect(group.direct_and_indirect_users).to contain_exactly(user_a, user_b, user_c, user_d)
+ expect(nested_group.direct_and_indirect_users).to contain_exactly(user_a, user_b, user_c)
+ expect(deep_nested_group.direct_and_indirect_users).to contain_exactly(user_a, user_b, user_c)
+ end
+
+ it 'does not return members of projects belonging to ancestor groups' do
+ expect(nested_group.direct_and_indirect_users).not_to include(user_d)
+ end
+ end
+
+ describe '#project_users_with_descendants', :nested_groups do
+ let(:user_a) { create(:user) }
+ let(:user_b) { create(:user) }
+ let(:user_c) { create(:user) }
+
+ let(:group) { create(:group) }
+ let(:nested_group) { create(:group, parent: group) }
+ let(:deep_nested_group) { create(:group, parent: nested_group) }
+ let(:project_a) { create(:project, namespace: group) }
+ let(:project_b) { create(:project, namespace: nested_group) }
+ let(:project_c) { create(:project, namespace: deep_nested_group) }
+
+ it 'returns members of all projects in group and subgroups' do
+ project_a.add_developer(user_a)
+ project_b.add_developer(user_b)
+ project_c.add_developer(user_c)
+
+ expect(group.project_users_with_descendants).to contain_exactly(user_a, user_b, user_c)
+ expect(nested_group.project_users_with_descendants).to contain_exactly(user_b, user_c)
+ expect(deep_nested_group.project_users_with_descendants).to contain_exactly(user_c)
+ end
+ end
+
describe '#user_ids_for_project_authorizations' do
it 'returns the user IDs for which to refresh authorizations' do
master = create(:user)
diff --git a/spec/models/guest_spec.rb b/spec/models/guest_spec.rb
index 2afdd6751a4..fc30f3056e5 100644
--- a/spec/models/guest_spec.rb
+++ b/spec/models/guest_spec.rb
@@ -1,9 +1,9 @@
require 'spec_helper'
describe Guest do
- let(:public_project) { build_stubbed(:project, :public) }
- let(:private_project) { build_stubbed(:project, :private) }
- let(:internal_project) { build_stubbed(:project, :internal) }
+ set(:public_project) { create(:project, :public) }
+ set(:private_project) { create(:project, :private) }
+ set(:internal_project) { create(:project, :internal) }
describe '.can_pull?' do
context 'when project is private' do
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 5a9aa7c7d1b..04379e7d2c3 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -1069,6 +1069,22 @@ describe MergeRequest do
end
end
+ describe '#short_merge_commit_sha' do
+ let(:merge_request) { build_stubbed(:merge_request) }
+
+ it 'returns short id when there is a merge_commit_sha' do
+ merge_request.merge_commit_sha = 'f7ce827c314c9340b075657fd61c789fb01cf74d'
+
+ expect(merge_request.short_merge_commit_sha).to eq('f7ce827c')
+ end
+
+ it 'returns nil when there is no merge_commit_sha' do
+ merge_request.merge_commit_sha = nil
+
+ expect(merge_request.short_merge_commit_sha).to be_nil
+ end
+ end
+
describe '#can_be_reverted?' do
context 'when there is no merge_commit for the MR' do
before do
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 506057dce87..6f702d8d95e 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -399,6 +399,21 @@ describe Namespace do
end
end
+ describe '#self_and_hierarchy', :nested_groups do
+ let!(:group) { create(:group, path: 'git_lab') }
+ let!(:nested_group) { create(:group, parent: group) }
+ let!(:deep_nested_group) { create(:group, parent: nested_group) }
+ let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
+ let!(:another_group) { create(:group, path: 'gitllab') }
+ let!(:another_group_nested) { create(:group, path: 'foo', parent: another_group) }
+
+ it 'returns the correct tree' do
+ expect(group.self_and_hierarchy).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group)
+ expect(nested_group.self_and_hierarchy).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group)
+ expect(very_deep_nested_group.self_and_hierarchy).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group)
+ end
+ end
+
describe '#ancestors', :nested_groups do
let(:group) { create(:group) }
let(:nested_group) { create(:group, parent: group) }
diff --git a/spec/models/project_services/microsoft_teams_service_spec.rb b/spec/models/project_services/microsoft_teams_service_spec.rb
index 733086e258f..8d9ee96227f 100644
--- a/spec/models/project_services/microsoft_teams_service_spec.rb
+++ b/spec/models/project_services/microsoft_teams_service_spec.rb
@@ -30,7 +30,7 @@ describe MicrosoftTeamsService do
describe "#execute" do
let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
+ set(:project) { create(:project, :repository, :wiki_repo) }
before do
allow(chat_service).to receive_messages(
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index f3cf21cf279..5b452f17979 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -309,12 +309,12 @@ describe Project do
describe 'project token' do
it 'sets an random token if none provided' do
- project = FactoryBot.create :project, runners_token: ''
+ project = FactoryBot.create(:project, runners_token: '')
expect(project.runners_token).not_to eq('')
end
it 'does not set an random token if one provided' do
- project = FactoryBot.create :project, runners_token: 'my-token'
+ project = FactoryBot.create(:project, runners_token: 'my-token')
expect(project.runners_token).to eq('my-token')
end
end
@@ -639,7 +639,7 @@ describe Project do
describe '#to_param' do
context 'with namespace' do
before do
- @group = create :group, name: 'gitlab'
+ @group = create(:group, name: 'gitlab')
@project = create(:project, name: 'gitlabhq', namespace: @group)
end
@@ -866,8 +866,8 @@ describe Project do
describe '#star_count' do
it 'counts stars from multiple users' do
- user1 = create :user
- user2 = create :user
+ user1 = create(:user)
+ user2 = create(:user)
project = create(:project, :public)
expect(project.star_count).to eq(0)
@@ -889,7 +889,7 @@ describe Project do
end
it 'counts stars on the right project' do
- user = create :user
+ user = create(:user)
project1 = create(:project, :public)
project2 = create(:project, :public)
@@ -1148,9 +1148,9 @@ describe Project do
describe '#any_runners?' do
context 'shared runners' do
- let(:project) { create :project, shared_runners_enabled: shared_runners_enabled }
- let(:specific_runner) { create :ci_runner }
- let(:shared_runner) { create :ci_runner, :shared }
+ let(:project) { create(:project, shared_runners_enabled: shared_runners_enabled) }
+ let(:specific_runner) { create(:ci_runner) }
+ let(:shared_runner) { create(:ci_runner, :shared) }
context 'for shared runners disabled' do
let(:shared_runners_enabled) { false }
@@ -1208,9 +1208,9 @@ describe Project do
end
context 'group runners' do
- let(:project) { create :project, group_runners_enabled: group_runners_enabled }
- let(:group) { create :group, projects: [project] }
- let(:group_runner) { create :ci_runner, groups: [group] }
+ let(:project) { create(:project, group_runners_enabled: group_runners_enabled) }
+ let(:group) { create(:group, projects: [project]) }
+ let(:group_runner) { create(:ci_runner, groups: [group]) }
context 'for group runners disabled' do
let(:group_runners_enabled) { false }
@@ -1292,7 +1292,7 @@ describe Project do
end
describe '#pages_deployed?' do
- let(:project) { create :project }
+ let(:project) { create(:project) }
subject { project.pages_deployed? }
@@ -1310,8 +1310,8 @@ describe Project do
end
describe '#pages_url' do
- let(:group) { create :group, name: group_name }
- let(:project) { create :project, namespace: group, name: project_name }
+ let(:group) { create(:group, name: group_name) }
+ let(:project) { create(:project, namespace: group, name: project_name) }
let(:domain) { 'Example.com' }
subject { project.pages_url }
@@ -1337,8 +1337,8 @@ describe Project do
end
describe '#pages_group_url' do
- let(:group) { create :group, name: group_name }
- let(:project) { create :project, namespace: group, name: project_name }
+ let(:group) { create(:group, name: group_name) }
+ let(:project) { create(:project, namespace: group, name: project_name) }
let(:domain) { 'Example.com' }
let(:port) { 1234 }
@@ -1455,8 +1455,8 @@ describe Project do
let(:private_group) { create(:group, visibility_level: 0) }
let(:internal_group) { create(:group, visibility_level: 10) }
- let(:private_project) { create :project, :private, group: private_group }
- let(:internal_project) { create :project, :internal, group: internal_group }
+ let(:private_project) { create(:project, :private, group: private_group) }
+ let(:internal_project) { create(:project, :internal, group: internal_group) }
context 'when group is private project can not be internal' do
it { expect(private_project.visibility_level_allowed?(Gitlab::VisibilityLevel::INTERNAL)).to be_falsey }
@@ -1852,6 +1852,83 @@ describe Project do
it { expect(project.gitea_import?).to be true }
end
+ describe '#has_remote_mirror?' do
+ let(:project) { create(:project, :remote_mirror, :import_started) }
+ subject { project.has_remote_mirror? }
+
+ before do
+ allow_any_instance_of(RemoteMirror).to receive(:refresh_remote)
+ end
+
+ it 'returns true when a remote mirror is enabled' do
+ is_expected.to be_truthy
+ end
+
+ it 'returns false when remote mirror is disabled' do
+ project.remote_mirrors.first.update_attributes(enabled: false)
+
+ is_expected.to be_falsy
+ end
+ end
+
+ describe '#update_remote_mirrors' do
+ let(:project) { create(:project, :remote_mirror, :import_started) }
+ delegate :update_remote_mirrors, to: :project
+
+ before do
+ allow_any_instance_of(RemoteMirror).to receive(:refresh_remote)
+ end
+
+ it 'syncs enabled remote mirror' do
+ expect_any_instance_of(RemoteMirror).to receive(:sync)
+
+ update_remote_mirrors
+ end
+
+ it 'does nothing when remote mirror is disabled globally and not overridden' do
+ stub_application_setting(mirror_available: false)
+ project.remote_mirror_available_overridden = false
+
+ expect_any_instance_of(RemoteMirror).not_to receive(:sync)
+
+ update_remote_mirrors
+ end
+
+ it 'does not sync disabled remote mirrors' do
+ project.remote_mirrors.first.update_attributes(enabled: false)
+
+ expect_any_instance_of(RemoteMirror).not_to receive(:sync)
+
+ update_remote_mirrors
+ end
+ end
+
+ describe '#remote_mirror_available?' do
+ let(:project) { create(:project) }
+
+ context 'when remote mirror global setting is enabled' do
+ it 'returns true' do
+ expect(project.remote_mirror_available?).to be(true)
+ end
+ end
+
+ context 'when remote mirror global setting is disabled' do
+ before do
+ stub_application_setting(mirror_available: false)
+ end
+
+ it 'returns true when overridden' do
+ project.remote_mirror_available_overridden = true
+
+ expect(project.remote_mirror_available?).to be(true)
+ end
+
+ it 'returns false when not overridden' do
+ expect(project.remote_mirror_available?).to be(false)
+ end
+ end
+ end
+
describe '#ancestors_upto', :nested_groups do
let(:parent) { create(:group) }
let(:child) { create(:group, parent: parent) }
@@ -2373,8 +2450,8 @@ describe Project do
end
describe '#pages_url' do
- let(:group) { create :group, name: 'Group' }
- let(:nested_group) { create :group, parent: group }
+ let(:group) { create(:group, name: 'Group') }
+ let(:nested_group) { create(:group, parent: group) }
let(:domain) { 'Example.com' }
subject { project.pages_url }
@@ -2385,7 +2462,7 @@ describe Project do
end
context 'top-level group' do
- let(:project) { create :project, namespace: group, name: project_name }
+ let(:project) { create(:project, namespace: group, name: project_name) }
context 'group page' do
let(:project_name) { 'group.example.com' }
@@ -2401,7 +2478,7 @@ describe Project do
end
context 'nested group' do
- let(:project) { create :project, namespace: nested_group, name: project_name }
+ let(:project) { create(:project, namespace: nested_group, name: project_name) }
let(:expected_url) { "http://group.example.com/#{nested_group.path}/#{project.path}" }
context 'group page' do
@@ -2419,7 +2496,7 @@ describe Project do
end
describe '#http_url_to_repo' do
- let(:project) { create :project }
+ let(:project) { create(:project) }
it 'returns the url to the repo without a username' do
expect(project.http_url_to_repo).to eq("#{project.web_url}.git")
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index cbe7d111fcd..f1142832f1a 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -1,7 +1,7 @@
require "spec_helper"
describe ProjectWiki do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :wiki_repo) }
let(:repository) { project.repository }
let(:user) { project.owner }
let(:gitlab_shell) { Gitlab::Shell.new }
@@ -159,6 +159,17 @@ describe ProjectWiki do
expect(page.title).to eq("autre pagé")
end
end
+
+ context 'pages with invalidly-encoded content' do
+ before do
+ create_page("encoding is fun", "f\xFCr".b)
+ end
+
+ it "can find the page" do
+ page = subject.find_page("encoding is fun")
+ expect(page.content).to eq("fr")
+ end
+ end
end
context 'when Gitaly wiki_find_page is enabled' do
@@ -328,6 +339,8 @@ describe ProjectWiki do
end
describe '#create_repo!' do
+ let(:project) { create(:project) }
+
it 'creates a repository' do
expect(raw_repository.exists?).to eq(false)
expect(subject.repository).to receive(:after_create)
@@ -339,6 +352,8 @@ describe ProjectWiki do
end
describe '#ensure_repository' do
+ let(:project) { create(:project) }
+
it 'creates the repository if it not exist' do
expect(raw_repository.exists?).to eq(false)
diff --git a/spec/models/remote_mirror_spec.rb b/spec/models/remote_mirror_spec.rb
new file mode 100644
index 00000000000..a80800c6c92
--- /dev/null
+++ b/spec/models/remote_mirror_spec.rb
@@ -0,0 +1,267 @@
+require 'rails_helper'
+
+describe RemoteMirror do
+ describe 'URL validation' do
+ context 'with a valid URL' do
+ it 'should be valid' do
+ remote_mirror = build(:remote_mirror)
+ expect(remote_mirror).to be_valid
+ end
+ end
+
+ context 'with an invalid URL' do
+ it 'should not be valid' do
+ remote_mirror = build(:remote_mirror, url: 'ftp://invalid.invalid')
+ expect(remote_mirror).not_to be_valid
+ expect(remote_mirror.errors[:url].size).to eq(2)
+ end
+ end
+ end
+
+ describe 'encrypting credentials' do
+ context 'when setting URL for a first time' do
+ it 'stores the URL without credentials' do
+ mirror = create_mirror(url: 'http://foo:bar@test.com')
+
+ expect(mirror.read_attribute(:url)).to eq('http://test.com')
+ end
+
+ it 'stores the credentials on a separate field' do
+ mirror = create_mirror(url: 'http://foo:bar@test.com')
+
+ expect(mirror.credentials).to eq({ user: 'foo', password: 'bar' })
+ end
+
+ it 'handles credentials with large content' do
+ mirror = create_mirror(url: 'http://bxnhm8dote33ct932r3xavslj81wxmr7o8yux8do10oozckkif:9ne7fuvjn40qjt35dgt8v86q9m9g9essryxj76sumg2ccl2fg26c0krtz2gzfpyq4hf22h328uhq6npuiq6h53tpagtsj7vsrz75@test.com')
+
+ expect(mirror.credentials).to eq({
+ user: 'bxnhm8dote33ct932r3xavslj81wxmr7o8yux8do10oozckkif',
+ password: '9ne7fuvjn40qjt35dgt8v86q9m9g9essryxj76sumg2ccl2fg26c0krtz2gzfpyq4hf22h328uhq6npuiq6h53tpagtsj7vsrz75'
+ })
+ end
+ end
+
+ context 'when updating the URL' do
+ it 'allows a new URL without credentials' do
+ mirror = create_mirror(url: 'http://foo:bar@test.com')
+
+ mirror.update_attribute(:url, 'http://test.com')
+
+ expect(mirror.url).to eq('http://test.com')
+ expect(mirror.credentials).to eq({ user: nil, password: nil })
+ end
+
+ it 'allows a new URL with credentials' do
+ mirror = create_mirror(url: 'http://test.com')
+
+ mirror.update_attribute(:url, 'http://foo:bar@test.com')
+
+ expect(mirror.url).to eq('http://foo:bar@test.com')
+ expect(mirror.credentials).to eq({ user: 'foo', password: 'bar' })
+ end
+
+ it 'updates the remote config if credentials changed' do
+ mirror = create_mirror(url: 'http://foo:bar@test.com')
+ repo = mirror.project.repository
+
+ mirror.update_attribute(:url, 'http://foo:baz@test.com')
+
+ config = repo.raw_repository.rugged.config
+ expect(config["remote.#{mirror.remote_name}.url"]).to eq('http://foo:baz@test.com')
+ end
+
+ it 'removes previous remote' do
+ mirror = create_mirror(url: 'http://foo:bar@test.com')
+
+ expect(RepositoryRemoveRemoteWorker).to receive(:perform_async).with(mirror.project.id, mirror.remote_name).and_call_original
+
+ mirror.update_attributes(url: 'http://test.com')
+ end
+ end
+ end
+
+ describe '#remote_name' do
+ context 'when remote name is persisted in the database' do
+ it 'returns remote name with random value' do
+ allow(SecureRandom).to receive(:hex).and_return('secret')
+
+ remote_mirror = create(:remote_mirror)
+
+ expect(remote_mirror.remote_name).to eq("remote_mirror_secret")
+ end
+ end
+
+ context 'when remote name is not persisted in the database' do
+ it 'returns remote name with remote mirror id' do
+ remote_mirror = create(:remote_mirror)
+ remote_mirror.remote_name = nil
+
+ expect(remote_mirror.remote_name).to eq("remote_mirror_#{remote_mirror.id}")
+ end
+ end
+
+ context 'when remote is not persisted in the database' do
+ it 'returns nil' do
+ remote_mirror = build(:remote_mirror, remote_name: nil)
+
+ expect(remote_mirror.remote_name).to be_nil
+ end
+ end
+ end
+
+ describe '#safe_url' do
+ context 'when URL contains credentials' do
+ it 'masks the credentials' do
+ mirror = create_mirror(url: 'http://foo:bar@test.com')
+
+ expect(mirror.safe_url).to eq('http://*****:*****@test.com')
+ end
+ end
+
+ context 'when URL does not contain credentials' do
+ it 'shows the full URL' do
+ mirror = create_mirror(url: 'http://test.com')
+
+ expect(mirror.safe_url).to eq('http://test.com')
+ end
+ end
+ end
+
+ context 'when remote mirror gets destroyed' do
+ it 'removes remote' do
+ mirror = create_mirror(url: 'http://foo:bar@test.com')
+
+ expect(RepositoryRemoveRemoteWorker).to receive(:perform_async).with(mirror.project.id, mirror.remote_name).and_call_original
+
+ mirror.destroy!
+ end
+ end
+
+ context 'stuck mirrors' do
+ it 'includes mirrors stuck in started with no last_update_at set' do
+ mirror = create_mirror(url: 'http://cantbeblank',
+ update_status: 'started',
+ last_update_at: nil,
+ updated_at: 25.hours.ago)
+
+ expect(described_class.stuck.last).to eq(mirror)
+ end
+ end
+
+ context '#sync' do
+ let(:remote_mirror) { create(:project, :repository, :remote_mirror).remote_mirrors.first }
+
+ around do |example|
+ Timecop.freeze { example.run }
+ end
+
+ context 'with remote mirroring disabled' do
+ it 'returns nil' do
+ remote_mirror.update_attributes(enabled: false)
+
+ expect(remote_mirror.sync).to be_nil
+ end
+ end
+
+ context 'with remote mirroring enabled' do
+ context 'with only protected branches enabled' do
+ context 'when it did not update in the last minute' do
+ it 'schedules a RepositoryUpdateRemoteMirrorWorker to run now' do
+ expect(RepositoryUpdateRemoteMirrorWorker).to receive(:perform_async).with(remote_mirror.id, Time.now)
+
+ remote_mirror.sync
+ end
+ end
+
+ context 'when it did update in the last minute' do
+ it 'schedules a RepositoryUpdateRemoteMirrorWorker to run in the next minute' do
+ remote_mirror.last_update_started_at = Time.now - 30.seconds
+
+ expect(RepositoryUpdateRemoteMirrorWorker).to receive(:perform_in).with(RemoteMirror::PROTECTED_BACKOFF_DELAY, remote_mirror.id, Time.now)
+
+ remote_mirror.sync
+ end
+ end
+ end
+
+ context 'with only protected branches disabled' do
+ before do
+ remote_mirror.only_protected_branches = false
+ end
+
+ context 'when it did not update in the last 5 minutes' do
+ it 'schedules a RepositoryUpdateRemoteMirrorWorker to run now' do
+ expect(RepositoryUpdateRemoteMirrorWorker).to receive(:perform_async).with(remote_mirror.id, Time.now)
+
+ remote_mirror.sync
+ end
+ end
+
+ context 'when it did update within the last 5 minutes' do
+ it 'schedules a RepositoryUpdateRemoteMirrorWorker to run in the next 5 minutes' do
+ remote_mirror.last_update_started_at = Time.now - 30.seconds
+
+ expect(RepositoryUpdateRemoteMirrorWorker).to receive(:perform_in).with(RemoteMirror::UNPROTECTED_BACKOFF_DELAY, remote_mirror.id, Time.now)
+
+ remote_mirror.sync
+ end
+ end
+ end
+ end
+ end
+
+ context '#updated_since?' do
+ let(:remote_mirror) { create(:project, :repository, :remote_mirror).remote_mirrors.first }
+ let(:timestamp) { Time.now - 5.minutes }
+
+ around do |example|
+ Timecop.freeze { example.run }
+ end
+
+ before do
+ remote_mirror.update_attributes(last_update_started_at: Time.now)
+ end
+
+ context 'when remote mirror does not have status failed' do
+ it 'returns true when last update started after the timestamp' do
+ expect(remote_mirror.updated_since?(timestamp)).to be true
+ end
+
+ it 'returns false when last update started before the timestamp' do
+ expect(remote_mirror.updated_since?(Time.now + 5.minutes)).to be false
+ end
+ end
+
+ context 'when remote mirror has status failed' do
+ it 'returns false when last update started after the timestamp' do
+ remote_mirror.update_attributes(update_status: 'failed')
+
+ expect(remote_mirror.updated_since?(timestamp)).to be false
+ end
+ end
+ end
+
+ context 'no project' do
+ it 'includes mirror with a project in pending_delete' do
+ mirror = create_mirror(url: 'http://cantbeblank',
+ update_status: 'finished',
+ enabled: true,
+ last_update_at: nil,
+ updated_at: 25.hours.ago)
+ project = mirror.project
+ project.pending_delete = true
+ project.save
+ mirror.reload
+
+ expect(mirror.sync).to be_nil
+ expect(mirror.valid?).to be_truthy
+ expect(mirror.update_status).to eq('finished')
+ end
+ end
+
+ def create_mirror(params)
+ project = FactoryBot.create(:project, :repository)
+ project.remote_mirrors.create!(params)
+ end
+end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 630b9e0519f..a7755a505d8 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -758,6 +758,38 @@ describe Repository do
end
end
+ describe '#async_remove_remote' do
+ before do
+ masterrev = repository.find_branch('master').dereferenced_target
+ create_remote_branch('joe', 'remote_branch', masterrev)
+ end
+
+ context 'when worker is scheduled successfully' do
+ before do
+ masterrev = repository.find_branch('master').dereferenced_target
+ create_remote_branch('remote_name', 'remote_branch', masterrev)
+
+ allow(RepositoryRemoveRemoteWorker).to receive(:perform_async).and_return('1234')
+ end
+
+ it 'returns job_id' do
+ expect(repository.async_remove_remote('joe')).to eq('1234')
+ end
+ end
+
+ context 'when worker does not schedule successfully' do
+ before do
+ allow(RepositoryRemoveRemoteWorker).to receive(:perform_async).and_return(nil)
+ end
+
+ it 'returns nil' do
+ expect(Rails.logger).to receive(:info).with("Remove remote job failed to create for #{project.id} with remote name joe.")
+
+ expect(repository.async_remove_remote('joe')).to be_nil
+ end
+ end
+ end
+
describe '#fetch_ref' do
let(:broken_repository) { create(:project, :broken_storage).repository }
@@ -1701,7 +1733,8 @@ describe Repository do
:gitlab_ci,
:avatar,
:issue_template,
- :merge_request_template
+ :merge_request_template,
+ :xcode_config
])
repository.after_change_head
@@ -2026,6 +2059,36 @@ describe Repository do
end
end
+ describe '#xcode_project?' do
+ before do
+ allow(repository).to receive(:tree).with(:head).and_return(double(:tree, blobs: [blob]))
+ end
+
+ context 'when the root contains a *.xcodeproj file' do
+ let(:blob) { double(:blob, 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') }
+
+ 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') }
+
+ it 'returns false' do
+ expect(repository.xcode_project?).to be_falsey
+ end
+ end
+ end
+
describe "#keep_around" do
it "does not fail if we attempt to reference bad commit" do
expect(repository.kept_around?('abc1234')).to be_falsey
@@ -2338,6 +2401,11 @@ describe Repository do
end
end
+ def create_remote_branch(remote_name, branch_name, target)
+ rugged = repository.rugged
+ rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", target.id)
+ end
+
describe '#ancestor?' do
let(:commit) { repository.commit }
let(:ancestor) { commit.parents.first }
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 3f2eb58f009..bb5308221f0 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe User do
include ProjectForksHelper
+ include TermsHelper
describe 'modules' do
subject { described_class }
@@ -2728,4 +2729,44 @@ describe User do
.to change { RedirectRoute.where(path: 'foo').count }.by(-1)
end
end
+
+ describe '#required_terms_not_accepted?' do
+ let(:user) { build(:user) }
+ subject { user.required_terms_not_accepted? }
+
+ context "when terms are not enforced" do
+ it { is_expected.to be_falsy }
+ end
+
+ context "when terms are enforced and accepted by the user" do
+ before do
+ enforce_terms
+ accept_terms(user)
+ end
+
+ it { is_expected.to be_falsy }
+ end
+
+ context "when terms are enforced but the user has not accepted" do
+ before do
+ enforce_terms
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ describe '#increment_failed_attempts!' do
+ subject(:user) { create(:user, failed_attempts: 0) }
+
+ it 'logs failed sign-in attempts' do
+ expect { user.increment_failed_attempts! }.to change(user, :failed_attempts).from(0).to(1)
+ end
+
+ it 'does not log failed sign-in attempts when in a GitLab read-only instance' do
+ allow(Gitlab::Database).to receive(:read_only?) { true }
+
+ expect { user.increment_failed_attempts! }.not_to change(user, :failed_attempts)
+ end
+ end
end
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index 90b7e7715a8..1c765ceac2f 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -1,7 +1,7 @@
require "spec_helper"
describe WikiPage do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :wiki_repo) }
let(:user) { project.owner }
let(:wiki) { ProjectWiki.new(project, user) }
diff --git a/spec/policies/ci/build_policy_spec.rb b/spec/policies/ci/build_policy_spec.rb
index 41cf2ef7225..9ca156deaa0 100644
--- a/spec/policies/ci/build_policy_spec.rb
+++ b/spec/policies/ci/build_policy_spec.rb
@@ -94,6 +94,19 @@ describe Ci::BuildPolicy do
end
end
end
+
+ context 'when maintainer is allowed to push to pipeline branch' do
+ let(:project) { create(:project, :public) }
+ let(:owner) { user }
+
+ it 'enables update_build if user is maintainer' do
+ allow_any_instance_of(Project).to receive(:empty_repo?).and_return(false)
+ allow_any_instance_of(Project).to receive(:branch_allows_maintainer_push?).and_return(true)
+
+ expect(policy).to be_allowed :update_build
+ expect(policy).to be_allowed :update_commit_status
+ end
+ end
end
describe 'rules for protected ref' do
diff --git a/spec/policies/ci/pipeline_policy_spec.rb b/spec/policies/ci/pipeline_policy_spec.rb
index 48a8064c5fc..a5e509cfa0f 100644
--- a/spec/policies/ci/pipeline_policy_spec.rb
+++ b/spec/policies/ci/pipeline_policy_spec.rb
@@ -62,5 +62,17 @@ describe Ci::PipelinePolicy, :models do
end
end
end
+
+ context 'when maintainer is allowed to push to pipeline branch' do
+ let(:project) { create(:project, :public) }
+ let(:owner) { user }
+
+ it 'enables update_pipeline if user is maintainer' do
+ allow_any_instance_of(Project).to receive(:empty_repo?).and_return(false)
+ allow_any_instance_of(Project).to receive(:branch_allows_maintainer_push?).and_return(true)
+
+ expect(policy).to be_allowed :update_pipeline
+ end
+ end
end
end
diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb
index ec26810e371..873673b50ef 100644
--- a/spec/policies/global_policy_spec.rb
+++ b/spec/policies/global_policy_spec.rb
@@ -90,4 +90,94 @@ describe GlobalPolicy do
it { is_expected.to be_allowed(:update_custom_attribute) }
end
end
+
+ shared_examples 'access allowed when terms accepted' do |ability|
+ it { is_expected.not_to be_allowed(ability) }
+
+ it "allows #{ability} when the user accepted the terms" do
+ accept_terms(current_user)
+
+ is_expected.to be_allowed(ability)
+ end
+ end
+
+ describe 'API access' do
+ context 'regular user' do
+ it { is_expected.to be_allowed(:access_api) }
+ end
+
+ context 'admin' do
+ let(:current_user) { create(:admin) }
+
+ it { is_expected.to be_allowed(:access_api) }
+ end
+
+ context 'anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_allowed(:access_api) }
+ end
+
+ context 'when terms are enforced' do
+ before do
+ enforce_terms
+ end
+
+ context 'regular user' do
+ it_behaves_like 'access allowed when terms accepted', :access_api
+ end
+
+ context 'admin' do
+ let(:current_user) { create(:admin) }
+
+ it_behaves_like 'access allowed when terms accepted', :access_api
+ end
+
+ context 'anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_allowed(:access_api) }
+ end
+ end
+ end
+
+ describe 'git access' do
+ describe 'regular user' do
+ it { is_expected.to be_allowed(:access_git) }
+ end
+
+ describe 'admin' do
+ let(:current_user) { create(:admin) }
+
+ it { is_expected.to be_allowed(:access_git) }
+ end
+
+ describe 'anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_allowed(:access_git) }
+ end
+
+ context 'when terms are enforced' do
+ before do
+ enforce_terms
+ end
+
+ context 'regular user' do
+ it_behaves_like 'access allowed when terms accepted', :access_git
+ end
+
+ context 'admin' do
+ let(:current_user) { create(:admin) }
+
+ it_behaves_like 'access allowed when terms accepted', :access_git
+ end
+
+ context 'anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_allowed(:access_git) }
+ end
+ end
+ end
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 8b9c4ac0b4b..6609f5f7afd 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -404,7 +404,7 @@ describe ProjectPolicy do
)
end
let(:maintainer_abilities) do
- %w(create_build update_build create_pipeline update_pipeline)
+ %w(create_build create_pipeline)
end
subject { described_class.new(user, project) }
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index 837389451e8..d3ab44c0d7e 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -6,6 +6,7 @@ describe API::Helpers do
include API::APIGuard::HelperMethods
include described_class
include SentryHelper
+ include TermsHelper
let(:user) { create(:user) }
let(:admin) { create(:admin) }
@@ -163,6 +164,23 @@ describe API::Helpers do
expect { current_user }.to raise_error /403/
end
+ context 'when terms are enforced' do
+ before do
+ enforce_terms
+ env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ end
+
+ it 'returns a 403 when a user has not accepted the terms' do
+ expect { current_user }.to raise_error /You must accept the Terms of Service/
+ end
+
+ it 'sets the current user when the user accepted the terms' do
+ accept_terms(user)
+
+ expect(current_user).to eq(user)
+ end
+ end
+
it "sets current_user" do
env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect(current_user).to eq(user)
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 90f9c4ad214..60e174ff92a 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -64,12 +64,32 @@ describe API::Issues do
describe "GET /issues" do
context "when unauthenticated" do
- it "returns authentication error" do
+ it "returns an array of all issues" do
+ get api("/issues"), scope: 'all'
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ end
+
+ it "returns authentication error without any scope" do
get api("/issues")
- expect(response).to have_gitlab_http_status(401)
+ expect(response).to have_http_status(401)
+ end
+
+ it "returns authentication error when scope is assigned-to-me" do
+ get api("/issues"), scope: 'assigned-to-me'
+
+ expect(response).to have_http_status(401)
+ end
+
+ it "returns authentication error when scope is created-by-me" do
+ get api("/issues"), scope: 'created-by-me'
+
+ expect(response).to have_http_status(401)
end
end
+
context "when authenticated" do
let(:first_issue) { json_response.first }
@@ -379,9 +399,6 @@ describe API::Issues do
end
let!(:group_note) { create(:note_on_issue, author: user, project: group_project, noteable: group_issue) }
- before do
- group_project.add_reporter(user)
- end
let(:base_url) { "/groups/#{group.id}/issues" }
context 'when group has subgroups', :nested_groups do
@@ -408,178 +425,201 @@ describe API::Issues do
end
end
- it 'returns all group issues (including opened and closed)' do
- get api(base_url, admin)
+ context 'when user is unauthenticated' do
+ it 'lists all issues in public projects' do
+ get api(base_url)
- expect_paginated_array_response(size: 3)
+ expect_paginated_array_response(size: 2)
+ end
end
- it 'returns group issues without confidential issues for non project members' do
- get api("#{base_url}?state=opened", non_member)
+ context 'when user is a group member' do
+ before do
+ group_project.add_reporter(user)
+ end
- expect_paginated_array_response(size: 1)
- expect(json_response.first['title']).to eq(group_issue.title)
- end
+ it 'returns all group issues (including opened and closed)' do
+ get api(base_url, admin)
- it 'returns group confidential issues for author' do
- get api("#{base_url}?state=opened", author)
+ expect_paginated_array_response(size: 3)
+ end
- expect_paginated_array_response(size: 2)
- end
+ it 'returns group issues without confidential issues for non project members' do
+ get api("#{base_url}?state=opened", non_member)
- it 'returns group confidential issues for assignee' do
- get api("#{base_url}?state=opened", assignee)
+ expect_paginated_array_response(size: 1)
+ expect(json_response.first['title']).to eq(group_issue.title)
+ end
- expect_paginated_array_response(size: 2)
- end
+ it 'returns group confidential issues for author' do
+ get api("#{base_url}?state=opened", author)
- it 'returns group issues with confidential issues for project members' do
- get api("#{base_url}?state=opened", user)
+ expect_paginated_array_response(size: 2)
+ end
- expect_paginated_array_response(size: 2)
- end
+ it 'returns group confidential issues for assignee' do
+ get api("#{base_url}?state=opened", assignee)
- it 'returns group confidential issues for admin' do
- get api("#{base_url}?state=opened", admin)
+ expect_paginated_array_response(size: 2)
+ end
- expect_paginated_array_response(size: 2)
- end
+ it 'returns group issues with confidential issues for project members' do
+ get api("#{base_url}?state=opened", user)
- it 'returns an array of labeled group issues' do
- get api("#{base_url}?labels=#{group_label.title}", user)
+ expect_paginated_array_response(size: 2)
+ end
- expect_paginated_array_response(size: 1)
- expect(json_response.first['labels']).to eq([group_label.title])
- end
+ it 'returns group confidential issues for admin' do
+ get api("#{base_url}?state=opened", admin)
- it 'returns an array of labeled group issues where all labels match' do
- get api("#{base_url}?labels=#{group_label.title},foo,bar", user)
+ expect_paginated_array_response(size: 2)
+ end
- expect_paginated_array_response(size: 0)
- end
+ it 'returns an array of labeled group issues' do
+ get api("#{base_url}?labels=#{group_label.title}", user)
- it 'returns issues matching given search string for title' do
- get api("#{base_url}?search=#{group_issue.title}", user)
+ expect_paginated_array_response(size: 1)
+ expect(json_response.first['labels']).to eq([group_label.title])
+ end
- expect_paginated_array_response(size: 1)
- expect(json_response.first['id']).to eq(group_issue.id)
- end
+ it 'returns an array of labeled group issues where all labels match' do
+ get api("#{base_url}?labels=#{group_label.title},foo,bar", user)
- it 'returns issues matching given search string for description' do
- get api("#{base_url}?search=#{group_issue.description}", user)
+ expect_paginated_array_response(size: 0)
+ end
- expect_paginated_array_response(size: 1)
- expect(json_response.first['id']).to eq(group_issue.id)
- end
+ it 'returns issues matching given search string for title' do
+ get api("#{base_url}?search=#{group_issue.title}", user)
- it 'returns an array of labeled issues when all labels matches' do
- label_b = create(:label, title: 'foo', project: group_project)
- label_c = create(:label, title: 'bar', project: group_project)
+ expect_paginated_array_response(size: 1)
+ expect(json_response.first['id']).to eq(group_issue.id)
+ end
- create(:label_link, label: label_b, target: group_issue)
- create(:label_link, label: label_c, target: group_issue)
+ it 'returns issues matching given search string for description' do
+ get api("#{base_url}?search=#{group_issue.description}", user)
- get api("#{base_url}", user), labels: "#{group_label.title},#{label_b.title},#{label_c.title}"
+ expect_paginated_array_response(size: 1)
+ expect(json_response.first['id']).to eq(group_issue.id)
+ end
- expect_paginated_array_response(size: 1)
- expect(json_response.first['labels']).to eq([label_c.title, label_b.title, group_label.title])
- end
+ it 'returns an array of labeled issues when all labels matches' do
+ label_b = create(:label, title: 'foo', project: group_project)
+ label_c = create(:label, title: 'bar', project: group_project)
- it 'returns an array of issues found by iids' do
- get api(base_url, user), iids: [group_issue.iid]
+ create(:label_link, label: label_b, target: group_issue)
+ create(:label_link, label: label_c, target: group_issue)
- expect_paginated_array_response(size: 1)
- expect(json_response.first['id']).to eq(group_issue.id)
- end
+ get api("#{base_url}", user), labels: "#{group_label.title},#{label_b.title},#{label_c.title}"
- it 'returns an empty array if iid does not exist' do
- get api(base_url, user), iids: [99999]
+ expect_paginated_array_response(size: 1)
+ expect(json_response.first['labels']).to eq([label_c.title, label_b.title, group_label.title])
+ end
- expect_paginated_array_response(size: 0)
- end
+ it 'returns an array of issues found by iids' do
+ get api(base_url, user), iids: [group_issue.iid]
- it 'returns an empty array if no group issue matches labels' do
- get api("#{base_url}?labels=foo,bar", user)
+ expect_paginated_array_response(size: 1)
+ expect(json_response.first['id']).to eq(group_issue.id)
+ end
- expect_paginated_array_response(size: 0)
- end
+ it 'returns an empty array if iid does not exist' do
+ get api(base_url, user), iids: [99999]
- it 'returns an empty array if no issue matches milestone' do
- get api("#{base_url}?milestone=#{group_empty_milestone.title}", user)
+ expect_paginated_array_response(size: 0)
+ end
- expect_paginated_array_response(size: 0)
- end
+ it 'returns an empty array if no group issue matches labels' do
+ get api("#{base_url}?labels=foo,bar", user)
- it 'returns an empty array if milestone does not exist' do
- get api("#{base_url}?milestone=foo", user)
+ expect_paginated_array_response(size: 0)
+ end
- expect_paginated_array_response(size: 0)
- end
+ it 'returns an empty array if no issue matches milestone' do
+ get api("#{base_url}?milestone=#{group_empty_milestone.title}", user)
- it 'returns an array of issues in given milestone' do
- get api("#{base_url}?state=opened&milestone=#{group_milestone.title}", user)
+ expect_paginated_array_response(size: 0)
+ end
- expect_paginated_array_response(size: 1)
- expect(json_response.first['id']).to eq(group_issue.id)
- end
+ it 'returns an empty array if milestone does not exist' do
+ get api("#{base_url}?milestone=foo", user)
- it 'returns an array of issues matching state in milestone' do
- get api("#{base_url}?milestone=#{group_milestone.title}"\
- '&state=closed', user)
+ expect_paginated_array_response(size: 0)
+ end
- expect_paginated_array_response(size: 1)
- expect(json_response.first['id']).to eq(group_closed_issue.id)
- end
+ it 'returns an array of issues in given milestone' do
+ get api("#{base_url}?state=opened&milestone=#{group_milestone.title}", user)
- it 'returns an array of issues with no milestone' do
- get api("#{base_url}?milestone=#{no_milestone_title}", user)
+ expect_paginated_array_response(size: 1)
+ expect(json_response.first['id']).to eq(group_issue.id)
+ end
- expect(response).to have_gitlab_http_status(200)
+ it 'returns an array of issues matching state in milestone' do
+ get api("#{base_url}?milestone=#{group_milestone.title}"\
+ '&state=closed', user)
- expect_paginated_array_response(size: 1)
- expect(json_response.first['id']).to eq(group_confidential_issue.id)
- end
+ expect_paginated_array_response(size: 1)
+ expect(json_response.first['id']).to eq(group_closed_issue.id)
+ end
- it 'sorts by created_at descending by default' do
- get api(base_url, user)
+ it 'returns an array of issues with no milestone' do
+ get api("#{base_url}?milestone=#{no_milestone_title}", user)
- response_dates = json_response.map { |issue| issue['created_at'] }
+ expect(response).to have_gitlab_http_status(200)
- expect_paginated_array_response(size: 3)
- expect(response_dates).to eq(response_dates.sort.reverse)
- end
+ expect_paginated_array_response(size: 1)
+ expect(json_response.first['id']).to eq(group_confidential_issue.id)
+ end
- it 'sorts ascending when requested' do
- get api("#{base_url}?sort=asc", user)
+ it 'sorts by created_at descending by default' do
+ get api(base_url, user)
- response_dates = json_response.map { |issue| issue['created_at'] }
+ response_dates = json_response.map { |issue| issue['created_at'] }
- expect_paginated_array_response(size: 3)
- expect(response_dates).to eq(response_dates.sort)
- end
+ expect_paginated_array_response(size: 3)
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
- it 'sorts by updated_at descending when requested' do
- get api("#{base_url}?order_by=updated_at", user)
+ it 'sorts ascending when requested' do
+ get api("#{base_url}?sort=asc", user)
- response_dates = json_response.map { |issue| issue['updated_at'] }
+ response_dates = json_response.map { |issue| issue['created_at'] }
- expect_paginated_array_response(size: 3)
- expect(response_dates).to eq(response_dates.sort.reverse)
- end
+ expect_paginated_array_response(size: 3)
+ expect(response_dates).to eq(response_dates.sort)
+ end
- it 'sorts by updated_at ascending when requested' do
- get api("#{base_url}?order_by=updated_at&sort=asc", user)
+ it 'sorts by updated_at descending when requested' do
+ get api("#{base_url}?order_by=updated_at", user)
- response_dates = json_response.map { |issue| issue['updated_at'] }
+ response_dates = json_response.map { |issue| issue['updated_at'] }
- expect_paginated_array_response(size: 3)
- expect(response_dates).to eq(response_dates.sort)
+ expect_paginated_array_response(size: 3)
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts by updated_at ascending when requested' do
+ get api("#{base_url}?order_by=updated_at&sort=asc", user)
+
+ response_dates = json_response.map { |issue| issue['updated_at'] }
+
+ expect_paginated_array_response(size: 3)
+ expect(response_dates).to eq(response_dates.sort)
+ end
end
end
describe "GET /projects/:id/issues" do
let(:base_url) { "/projects/#{project.id}" }
+ context 'when unauthenticated' do
+ it 'returns public project issues' do
+ get api("/projects/#{project.id}/issues")
+
+ expect_paginated_array_response(size: 2)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+ end
+
it 'avoids N+1 queries' do
control_count = ActiveRecord::QueryRecorder.new do
get api("/projects/#{project.id}/issues", user)
@@ -789,6 +829,14 @@ describe API::Issues do
end
describe "GET /projects/:id/issues/:issue_iid" do
+ context 'when unauthenticated' do
+ it 'returns public issues' do
+ get api("/projects/#{project.id}/issues/#{issue.iid}")
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
it 'exposes known attributes' do
get api("/projects/#{project.id}/issues/#{issue.iid}", user)
@@ -1581,6 +1629,14 @@ describe API::Issues do
create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request)
end
+ context 'when unauthenticated' do
+ it 'return public project issues' do
+ get api("/projects/#{project.id}/issues/#{issue.iid}/closed_by")
+
+ expect_paginated_array_response(size: 1)
+ end
+ end
+
it 'returns merge requests that will close issue on merge' do
get api("/projects/#{project.id}/issues/#{issue.iid}/closed_by", user)
@@ -1605,6 +1661,14 @@ describe API::Issues do
describe "GET /projects/:id/issues/:issue_iid/user_agent_detail" do
let!(:user_agent_detail) { create(:user_agent_detail, subject: issue) }
+ context 'when unauthenticated' do
+ it "returns unautorized" do
+ get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail")
+
+ expect(response).to have_gitlab_http_status(401)
+ end
+ end
+
it 'exposes known attributes' do
get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail", admin)
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index 9ae39f2ef44..da392c5ab81 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -1,11 +1,13 @@
require 'spec_helper'
-describe API::Runner do
+describe API::Runner, :clean_gitlab_redis_shared_state do
include StubGitlabCalls
+ include RedisHelpers
let(:registration_token) { 'abcdefg123456' }
before do
+ stub_feature_flags(ci_enable_live_trace: true)
stub_gitlab_calls
stub_application_setting(runners_registration_token: registration_token)
allow_any_instance_of(Ci::Runner).to receive(:cache_attributes)
@@ -39,6 +41,7 @@ describe API::Runner do
expect(json_response['id']).to eq(runner.id)
expect(json_response['token']).to eq(runner.token)
expect(runner.run_untagged).to be true
+ expect(runner.active).to be true
expect(runner.token).not_to eq(registration_token)
expect(runner).to be_instance_type
end
@@ -127,6 +130,28 @@ describe API::Runner do
end
end
+ context 'when option for activating a Runner is provided' do
+ context 'when active is set to true' do
+ it 'creates runner' do
+ post api('/runners'), token: registration_token,
+ active: true
+
+ expect(response).to have_gitlab_http_status 201
+ expect(Ci::Runner.first.active).to be true
+ end
+ end
+
+ context 'when active is set to false' do
+ it 'creates runner' do
+ post api('/runners'), token: registration_token,
+ active: false
+
+ expect(response).to have_gitlab_http_status 201
+ expect(Ci::Runner.first.active).to be false
+ end
+ end
+ end
+
context 'when maximum job timeout is specified' do
it 'creates runner' do
post api('/runners'), token: registration_token,
@@ -882,6 +907,49 @@ describe API::Runner do
expect(response.status).to eq(403)
end
end
+
+ context 'when trace is patched' do
+ before do
+ patch_the_trace
+ end
+
+ it 'has valid trace' do
+ expect(response.status).to eq(202)
+ expect(job.reload.trace.raw).to eq 'BUILD TRACE appended appended'
+ end
+
+ context 'when redis data are flushed' do
+ before do
+ redis_shared_state_cleanup!
+ end
+
+ it 'has empty trace' do
+ expect(job.reload.trace.raw).to eq ''
+ end
+
+ context 'when we perform partial patch' do
+ before do
+ patch_the_trace('hello', headers.merge({ 'Content-Range' => "28-32/5" }))
+ end
+
+ it 'returns an error' do
+ expect(response.status).to eq(416)
+ expect(response.header['Range']).to eq('0-0')
+ end
+ end
+
+ context 'when we resend full trace' do
+ before do
+ patch_the_trace('BUILD TRACE appended appended hello', headers.merge({ 'Content-Range' => "0-34/35" }))
+ end
+
+ it 'succeeds with updating trace' do
+ expect(response.status).to eq(202)
+ expect(job.reload.trace.raw).to eq 'BUILD TRACE appended appended hello'
+ end
+ end
+ end
+ end
end
context 'when Runner makes a force-patch' do
@@ -898,7 +966,7 @@ describe API::Runner do
end
context 'when content-range start is too big' do
- let(:headers_with_range) { headers.merge({ 'Content-Range' => '15-20' }) }
+ let(:headers_with_range) { headers.merge({ 'Content-Range' => '15-20/6' }) }
it 'gets 416 error response with range headers' do
expect(response.status).to eq 416
@@ -908,7 +976,7 @@ describe API::Runner do
end
context 'when content-range start is too small' do
- let(:headers_with_range) { headers.merge({ 'Content-Range' => '8-20' }) }
+ let(:headers_with_range) { headers.merge({ 'Content-Range' => '8-20/13' }) }
it 'gets 416 error response with range headers' do
expect(response.status).to eq 416
diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb
index f22fec31514..981ac768e3a 100644
--- a/spec/requests/api/runners_spec.rb
+++ b/spec/requests/api/runners_spec.rb
@@ -46,6 +46,7 @@ describe API::Runners do
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
+ expect(json_response[0]).to have_key('ip_address')
expect(descriptions).to contain_exactly(
'Project runner', 'Two projects runner'
)
@@ -59,6 +60,7 @@ describe API::Runners do
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
+ expect(json_response[0]).to have_key('ip_address')
expect(shared).to be_falsey
end
@@ -87,6 +89,7 @@ describe API::Runners do
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
+ expect(json_response[0]).to have_key('ip_address')
expect(shared).to be_truthy
end
end
@@ -106,6 +109,7 @@ describe API::Runners do
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
+ expect(json_response[0]).to have_key('ip_address')
expect(shared).to be_falsey
end
@@ -515,6 +519,7 @@ describe API::Runners do
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
+ expect(json_response[0]).to have_key('ip_address')
expect(shared).to be_truthy
end
end
diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb
index f8d5258a8d9..aca4aa40027 100644
--- a/spec/requests/api/search_spec.rb
+++ b/spec/requests/api/search_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe API::Search do
set(:user) { create(:user) }
set(:group) { create(:group) }
- set(:project) { create(:project, :public, name: 'awesome project', group: group) }
+ set(:project) { create(:project, :wiki_repo, :public, name: 'awesome project', group: group) }
set(:repo_project) { create(:project, :public, :repository, group: group) }
shared_examples 'response is correct' do |schema:, size: 1|
diff --git a/spec/requests/api/wikis_spec.rb b/spec/requests/api/wikis_spec.rb
index fb0806ff9f1..850ba696098 100644
--- a/spec/requests/api/wikis_spec.rb
+++ b/spec/requests/api/wikis_spec.rb
@@ -143,7 +143,7 @@ describe API::Wikis do
let(:url) { "/projects/#{project.id}/wikis" }
context 'when wiki is disabled' do
- let(:project) { create(:project, :wiki_disabled) }
+ let(:project) { create(:project, :wiki_repo, :wiki_disabled) }
context 'when user is guest' do
before do
@@ -175,7 +175,7 @@ describe API::Wikis do
end
context 'when wiki is available only for team members' do
- let(:project) { create(:project, :wiki_private) }
+ let(:project) { create(:project, :wiki_repo, :wiki_private) }
context 'when user is guest' do
before do
@@ -203,7 +203,7 @@ describe API::Wikis do
end
context 'when wiki is available for everyone with access' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :wiki_repo) }
context 'when user is guest' do
before do
@@ -236,7 +236,7 @@ describe API::Wikis do
let(:url) { "/projects/#{project.id}/wikis/#{page.slug}" }
context 'when wiki is disabled' do
- let(:project) { create(:project, :wiki_disabled) }
+ let(:project) { create(:project, :wiki_repo, :wiki_disabled) }
context 'when user is guest' do
before do
@@ -268,7 +268,7 @@ describe API::Wikis do
end
context 'when wiki is available only for team members' do
- let(:project) { create(:project, :wiki_private) }
+ let(:project) { create(:project, :wiki_repo, :wiki_private) }
context 'when user is guest' do
before do
@@ -311,7 +311,7 @@ describe API::Wikis do
end
context 'when wiki is available for everyone with access' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :wiki_repo) }
context 'when user is guest' do
before do
@@ -360,7 +360,7 @@ describe API::Wikis do
let(:url) { "/projects/#{project.id}/wikis" }
context 'when wiki is disabled' do
- let(:project) { create(:project, :wiki_disabled) }
+ let(:project) { create(:project, :wiki_disabled, :wiki_repo) }
context 'when user is guest' do
before do
@@ -390,7 +390,7 @@ describe API::Wikis do
end
context 'when wiki is available only for team members' do
- let(:project) { create(:project, :wiki_private) }
+ let(:project) { create(:project, :wiki_private, :wiki_repo) }
context 'when user is guest' do
before do
@@ -418,7 +418,7 @@ describe API::Wikis do
end
context 'when wiki is available for everyone with access' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :wiki_repo) }
context 'when user is guest' do
before do
@@ -452,7 +452,7 @@ describe API::Wikis do
let(:url) { "/projects/#{project.id}/wikis/#{page.slug}" }
context 'when wiki is disabled' do
- let(:project) { create(:project, :wiki_disabled) }
+ let(:project) { create(:project, :wiki_disabled, :wiki_repo) }
context 'when user is guest' do
before do
@@ -484,7 +484,7 @@ describe API::Wikis do
end
context 'when wiki is available only for team members' do
- let(:project) { create(:project, :wiki_private) }
+ let(:project) { create(:project, :wiki_private, :wiki_repo) }
context 'when user is guest' do
before do
@@ -528,7 +528,7 @@ describe API::Wikis do
end
context 'when wiki is available for everyone with access' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :wiki_repo) }
context 'when user is guest' do
before do
@@ -572,7 +572,7 @@ describe API::Wikis do
end
context 'when wiki belongs to a group project' do
- let(:project) { create(:project, namespace: group) }
+ let(:project) { create(:project, :wiki_repo, namespace: group) }
before do
put(api(url, user), payload)
@@ -587,7 +587,7 @@ describe API::Wikis do
let(:url) { "/projects/#{project.id}/wikis/#{page.slug}" }
context 'when wiki is disabled' do
- let(:project) { create(:project, :wiki_disabled) }
+ let(:project) { create(:project, :wiki_disabled, :wiki_repo) }
context 'when user is guest' do
before do
@@ -619,7 +619,7 @@ describe API::Wikis do
end
context 'when wiki is available only for team members' do
- let(:project) { create(:project, :wiki_private) }
+ let(:project) { create(:project, :wiki_private, :wiki_repo) }
context 'when user is guest' do
before do
@@ -651,7 +651,7 @@ describe API::Wikis do
end
context 'when wiki is available for everyone with access' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :wiki_repo) }
context 'when user is guest' do
before do
@@ -689,7 +689,7 @@ describe API::Wikis do
end
context 'when wiki belongs to a group project' do
- let(:project) { create(:project, namespace: group) }
+ let(:project) { create(:project, :wiki_repo, namespace: group) }
before do
delete(api(url, user))
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 494db30e8e0..2514dab1714 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -1,6 +1,7 @@
require "spec_helper"
describe 'Git HTTP requests' do
+ include TermsHelper
include GitHttpHelpers
include WorkhorseHelpers
include UserActivitiesHelpers
@@ -824,4 +825,56 @@ describe 'Git HTTP requests' do
end
end
end
+
+ context 'when terms are enforced' do
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+ let(:path) { "#{project.full_path}.git" }
+ let(:env) { { user: user.username, password: user.password } }
+
+ before do
+ project.add_master(user)
+ enforce_terms
+ end
+
+ it 'blocks git access when the user did not accept terms', :aggregate_failures do
+ clone_get(path, env) do |response|
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+
+ download(path, env) do |response|
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+
+ upload(path, env) do |response|
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'when the user accepted the terms' do
+ before do
+ accept_terms(user)
+ end
+
+ it 'allows clones' do
+ clone_get(path, env) do |response|
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ it_behaves_like 'pulls are allowed'
+ it_behaves_like 'pushes are allowed'
+ end
+
+ context 'from CI' do
+ let(:build) { create(:ci_build, :running) }
+ let(:env) { { user: 'gitlab-ci-token', password: build.token } }
+
+ before do
+ build.update!(user: user, project: project)
+ end
+
+ it_behaves_like 'pulls are allowed'
+ end
+ end
end
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
index e88e86c2998..b741308e2c5 100644
--- a/spec/serializers/pipeline_serializer_spec.rb
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -114,7 +114,9 @@ describe PipelineSerializer do
Gitlab::GitalyClient.reset_counts
end
- shared_examples 'no N+1 queries' do
+ context 'with the same ref' do
+ let(:ref) { 'feature' }
+
it 'verifies number of queries', :request_store do
recorded = ActiveRecord::QueryRecorder.new { subject }
@@ -123,12 +125,6 @@ describe PipelineSerializer do
end
end
- context 'with the same ref' do
- let(:ref) { 'feature' }
-
- it_behaves_like 'no N+1 queries'
- end
-
context 'with different refs' do
def ref
@sequence ||= 0
@@ -136,7 +132,16 @@ describe PipelineSerializer do
"feature-#{@sequence}"
end
- it_behaves_like 'no N+1 queries'
+ it 'verifies number of queries', :request_store do
+ recorded = ActiveRecord::QueryRecorder.new { subject }
+
+ # For each ref there is a permission check if maintainer can update
+ # pipeline. With the same ref this check is cached but if refs are
+ # different then there is an extra query per ref
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/46368
+ expect(recorded.count).to be_within(1).of(51)
+ expect(recorded.cached_count).to eq(0)
+ end
end
def create_pipeline(status)
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 267258b33a8..9a0b6efd8a9 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -17,11 +17,13 @@ describe Ci::CreatePipelineService do
after: project.commit.id,
message: 'Message',
ref: ref_name,
- trigger_request: nil)
+ trigger_request: nil,
+ variables_attributes: nil)
params = { ref: ref,
before: '00000000',
after: after,
- commits: [{ message: message }] }
+ commits: [{ message: message }],
+ variables_attributes: variables_attributes }
described_class.new(project, user, params).execute(
source, trigger_request: trigger_request)
@@ -545,5 +547,19 @@ describe Ci::CreatePipelineService do
expect(pipeline.tag?).to be true
end
end
+
+ context 'when pipeline variables are specified' do
+ let(:variables_attributes) do
+ [{ key: 'first', secret_value: 'world' },
+ { key: 'second', secret_value: 'second_world' }]
+ end
+
+ subject { execute_service(variables_attributes: variables_attributes) }
+
+ it 'creates a pipeline with specified variables' do
+ expect(subject.variables.map { |var| var.slice(:key, :secret_value) })
+ .to eq variables_attributes.map(&:with_indifferent_access)
+ end
+ end
end
end
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index 5bc6031388e..e1cb7ed8110 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -32,7 +32,7 @@ describe Ci::RetryBuildService do
runner_id tag_taggings taggings tags trigger_request_id
user_id auto_canceled_by_id retried failure_reason
artifacts_file_store artifacts_metadata_store
- metadata].freeze
+ metadata trace_chunks].freeze
shared_examples 'build duplication' do
let(:another_pipeline) { create(:ci_empty_pipeline, project: project) }
diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb
index f1acfc48468..a73bd7a0268 100644
--- a/spec/services/ci/retry_pipeline_service_spec.rb
+++ b/spec/services/ci/retry_pipeline_service_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Ci::RetryPipelineService, '#execute' do
+ include ProjectForksHelper
+
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project) }
@@ -266,6 +268,33 @@ describe Ci::RetryPipelineService, '#execute' do
end
end
+ context 'when maintainer is allowed to push to forked project' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:forked_project) { fork_project(project) }
+ let(:pipeline) { create(:ci_pipeline, project: forked_project, ref: 'fixes') }
+
+ before do
+ project.add_master(user)
+ create(:merge_request,
+ source_project: forked_project,
+ target_project: project,
+ source_branch: 'fixes',
+ allow_maintainer_to_push: true)
+ create_build('rspec 1', :failed, 1)
+ end
+
+ it 'allows to retry failed pipeline' do
+ allow_any_instance_of(Project).to receive(:fetch_branch_allows_maintainer_push?).and_return(true)
+ allow_any_instance_of(Project).to receive(:empty_repo?).and_return(false)
+
+ service.execute(pipeline)
+
+ expect(build('rspec 1')).to be_pending
+ expect(pipeline.reload).to be_running
+ end
+ end
+
def statuses
pipeline.reload.statuses
end
diff --git a/spec/services/clusters/create_service_spec.rb b/spec/services/clusters/create_service_spec.rb
index 1c2f9c5cf43..1685dc748bd 100644
--- a/spec/services/clusters/create_service_spec.rb
+++ b/spec/services/clusters/create_service_spec.rb
@@ -8,80 +8,22 @@ describe Clusters::CreateService do
subject { described_class.new(project, user, params).execute(access_token) }
context 'when provider is gcp' do
- shared_context 'valid params' do
- let(:params) do
- {
- name: 'test-cluster',
- provider_type: :gcp,
- provider_gcp_attributes: {
- gcp_project_id: 'gcp-project',
- zone: 'us-central1-a',
- num_nodes: 1,
- machine_type: 'machine_type-a'
- }
- }
- end
- end
-
- shared_context 'invalid params' do
- let(:params) do
- {
- name: 'test-cluster',
- provider_type: :gcp,
- provider_gcp_attributes: {
- gcp_project_id: '!!!!!!!',
- zone: 'us-central1-a',
- num_nodes: 1,
- machine_type: 'machine_type-a'
- }
- }
- end
- end
-
- shared_examples 'create cluster' do
- it 'creates a cluster object and performs a worker' do
- expect(ClusterProvisionWorker).to receive(:perform_async)
-
- expect { subject }
- .to change { Clusters::Cluster.count }.by(1)
- .and change { Clusters::Providers::Gcp.count }.by(1)
-
- expect(subject.name).to eq('test-cluster')
- expect(subject.user).to eq(user)
- expect(subject.project).to eq(project)
- expect(subject.provider.gcp_project_id).to eq('gcp-project')
- expect(subject.provider.zone).to eq('us-central1-a')
- expect(subject.provider.num_nodes).to eq(1)
- expect(subject.provider.machine_type).to eq('machine_type-a')
- expect(subject.provider.access_token).to eq(access_token)
- expect(subject.platform).to be_nil
- end
- end
-
- shared_examples 'error' do
- it 'returns an error' do
- expect(ClusterProvisionWorker).not_to receive(:perform_async)
- expect { subject }.to change { Clusters::Cluster.count }.by(0)
- expect(subject.errors[:"provider_gcp.gcp_project_id"]).to be_present
- end
- end
-
context 'when project has no clusters' do
context 'when correct params' do
- include_context 'valid params'
+ include_context 'valid cluster create params'
- include_examples 'create cluster'
+ include_examples 'create cluster service success'
end
context 'when invalid params' do
- include_context 'invalid params'
+ include_context 'invalid cluster create params'
- include_examples 'error'
+ include_examples 'create cluster service error'
end
end
context 'when project has a cluster' do
- include_context 'valid params'
+ include_context 'valid cluster create params'
let!(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, projects: [project]) }
it 'does not create a cluster' do
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index 26fdf8d4b24..35826de5814 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -14,6 +14,72 @@ describe GitPushService, services: true do
project.add_master(user)
end
+ describe 'with remote mirrors' do
+ let(:project) { create(:project, :repository, :remote_mirror) }
+
+ subject do
+ described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
+ end
+
+ context 'when remote mirror feature is enabled' do
+ it 'fails stuck remote mirrors' do
+ allow(project).to receive(:update_remote_mirrors).and_return(project.remote_mirrors)
+ expect(project).to receive(:mark_stuck_remote_mirrors_as_failed!)
+
+ subject.execute
+ end
+
+ it 'updates remote mirrors' do
+ expect(project).to receive(:update_remote_mirrors)
+
+ subject.execute
+ end
+ end
+
+ context 'when remote mirror feature is disabled' do
+ before do
+ stub_application_setting(mirror_available: false)
+ end
+
+ context 'with remote mirrors global setting overridden' do
+ before do
+ project.remote_mirror_available_overridden = true
+ end
+
+ it 'fails stuck remote mirrors' do
+ allow(project).to receive(:update_remote_mirrors).and_return(project.remote_mirrors)
+ expect(project).to receive(:mark_stuck_remote_mirrors_as_failed!)
+
+ subject.execute
+ end
+
+ it 'updates remote mirrors' do
+ expect(project).to receive(:update_remote_mirrors)
+
+ subject.execute
+ end
+ end
+
+ context 'without remote mirrors global setting overridden' do
+ before do
+ project.remote_mirror_available_overridden = false
+ end
+
+ it 'does not fails stuck remote mirrors' do
+ expect(project).not_to receive(:mark_stuck_remote_mirrors_as_failed!)
+
+ subject.execute
+ end
+
+ it 'does not updates remote mirrors' do
+ expect(project).not_to receive(:update_remote_mirrors)
+
+ subject.execute
+ end
+ end
+ end
+ end
+
describe 'Push branches' do
subject do
execute_service(project, user, oldrev, newrev, ref)
diff --git a/spec/services/issuable/common_system_notes_service_spec.rb b/spec/services/issuable/common_system_notes_service_spec.rb
index b8fa3e3d124..dcf4503ef9c 100644
--- a/spec/services/issuable/common_system_notes_service_spec.rb
+++ b/spec/services/issuable/common_system_notes_service_spec.rb
@@ -5,34 +5,6 @@ describe Issuable::CommonSystemNotesService do
let(:project) { create(:project) }
let(:issuable) { create(:issue) }
- shared_examples 'system note creation' do |update_params, note_text|
- subject { described_class.new(project, user).execute(issuable, [])}
-
- before do
- issuable.assign_attributes(update_params)
- issuable.save
- end
-
- it 'creates 1 system note with the correct content' do
- expect { subject }.to change { Note.count }.from(0).to(1)
-
- note = Note.last
- expect(note.note).to match(note_text)
- expect(note.noteable_type).to eq(issuable.class.name)
- end
- end
-
- shared_examples 'WIP notes creation' do |wip_action|
- subject { described_class.new(project, user).execute(issuable, []) }
-
- it 'creates WIP toggle and title change notes' do
- expect { subject }.to change { Note.count }.from(0).to(2)
-
- expect(Note.first.note).to match("#{wip_action} as a **Work In Progress**")
- expect(Note.second.note).to match('changed title')
- end
- end
-
describe '#execute' do
it_behaves_like 'system note creation', { title: 'New title' }, 'changed title'
it_behaves_like 'system note creation', { description: 'New description' }, 'changed the description'
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 48ef5f3c115..5f28bc123f3 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe NotificationService, :mailer do
include EmailSpec::Matchers
+ include NotificationHelpers
let(:notification) { described_class.new }
let(:assignee) { create(:user) }
@@ -13,12 +14,6 @@ describe NotificationService, :mailer do
end
shared_examples 'notifications for new mentions' do
- def send_notifications(*new_mentions)
- mentionable.description = new_mentions.map(&:to_reference).join(' ')
-
- notification.send(notification_method, mentionable, new_mentions, @u_disabled)
- end
-
it 'sends no emails when no new mentions are present' do
send_notifications
should_not_email_anyone
@@ -1914,30 +1909,6 @@ describe NotificationService, :mailer do
group
end
- def create_global_setting_for(user, level)
- setting = user.global_notification_setting
- setting.level = level
- setting.save
-
- user
- end
-
- def create_user_with_notification(level, username, resource = project)
- user = create(:user, username: username)
- setting = user.notification_settings_for(resource)
- setting.level = level
- setting.save
-
- user
- end
-
- # Create custom notifications
- # When resource is nil it means global notification
- def update_custom_notification(event, user, resource: nil, value: true)
- setting = user.notification_settings_for(resource)
- setting.update!(event => value)
- end
-
def add_users_with_subscription(project, issuable)
@subscriber = create :user
@unsubscriber = create :user
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index b2c52214f48..b63f409579e 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -65,6 +65,19 @@ describe Projects::DestroyService do
Sidekiq::Testing.inline! { destroy_project(project, user, {}) }
end
+ context 'when has remote mirrors' do
+ let!(:project) do
+ create(:project, :repository, namespace: user.namespace).tap do |project|
+ project.remote_mirrors.create(url: 'http://test.com')
+ end
+ end
+ let!(:async) { true }
+
+ it 'destroys them' do
+ expect(RemoteMirror.count).to eq(0)
+ end
+ end
+
it_behaves_like 'deleting the project'
it 'invalidates personal_project_count cache' do
diff --git a/spec/services/projects/update_remote_mirror_service_spec.rb b/spec/services/projects/update_remote_mirror_service_spec.rb
new file mode 100644
index 00000000000..be09afd9f36
--- /dev/null
+++ b/spec/services/projects/update_remote_mirror_service_spec.rb
@@ -0,0 +1,355 @@
+require 'spec_helper'
+
+describe Projects::UpdateRemoteMirrorService do
+ let(:project) { create(:project, :repository) }
+ let(:remote_project) { create(:forked_project_with_submodules) }
+ let(:repository) { project.repository }
+ let(:raw_repository) { repository.raw }
+ let(:remote_mirror) { project.remote_mirrors.create!(url: remote_project.http_url_to_repo, enabled: true, only_protected_branches: false) }
+
+ subject { described_class.new(project, project.creator) }
+
+ describe "#execute", :skip_gitaly_mock do
+ before do
+ create_branch(repository, 'existing-branch')
+ allow(raw_repository).to receive(:remote_tags) do
+ generate_tags(repository, 'v1.0.0', 'v1.1.0')
+ end
+ allow(raw_repository).to receive(:push_remote_branches).and_return(true)
+ end
+
+ it "fetches the remote repository" do
+ expect(repository).to receive(:fetch_remote).with(remote_mirror.remote_name, no_tags: true) do
+ sync_remote(repository, remote_mirror.remote_name, local_branch_names)
+ end
+
+ subject.execute(remote_mirror)
+ end
+
+ it "succeeds" do
+ allow(repository).to receive(:fetch_remote) { sync_remote(repository, remote_mirror.remote_name, local_branch_names) }
+
+ result = subject.execute(remote_mirror)
+
+ expect(result[:status]).to eq(:success)
+ end
+
+ describe 'Syncing branches' do
+ it "push all the branches the first time" do
+ allow(repository).to receive(:fetch_remote)
+
+ expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, local_branch_names)
+
+ subject.execute(remote_mirror)
+ end
+
+ it "does not push anything is remote is up to date" do
+ allow(repository).to receive(:fetch_remote) { sync_remote(repository, remote_mirror.remote_name, local_branch_names) }
+
+ expect(raw_repository).not_to receive(:push_remote_branches)
+
+ subject.execute(remote_mirror)
+ end
+
+ it "sync new branches" do
+ # call local_branch_names early so it is not called after the new branch has been created
+ current_branches = local_branch_names
+ allow(repository).to receive(:fetch_remote) { sync_remote(repository, remote_mirror.remote_name, current_branches) }
+ create_branch(repository, 'my-new-branch')
+
+ expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, ['my-new-branch'])
+
+ subject.execute(remote_mirror)
+ end
+
+ it "sync updated branches" do
+ allow(repository).to receive(:fetch_remote) do
+ sync_remote(repository, remote_mirror.remote_name, local_branch_names)
+ update_branch(repository, 'existing-branch')
+ end
+
+ expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, ['existing-branch'])
+
+ subject.execute(remote_mirror)
+ end
+
+ context 'when push only protected branches option is set' do
+ let(:unprotected_branch_name) { 'existing-branch' }
+ let(:protected_branch_name) do
+ project.repository.branch_names.find { |n| n != unprotected_branch_name }
+ end
+ let!(:protected_branch) do
+ create(:protected_branch, project: project, name: protected_branch_name)
+ end
+
+ before do
+ project.reload
+ remote_mirror.only_protected_branches = true
+ end
+
+ it "sync updated protected branches" do
+ allow(repository).to receive(:fetch_remote) do
+ sync_remote(repository, remote_mirror.remote_name, local_branch_names)
+ update_branch(repository, protected_branch_name)
+ end
+
+ expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, [protected_branch_name])
+
+ subject.execute(remote_mirror)
+ end
+
+ it 'does not sync unprotected branches' do
+ allow(repository).to receive(:fetch_remote) do
+ sync_remote(repository, remote_mirror.remote_name, local_branch_names)
+ update_branch(repository, unprotected_branch_name)
+ end
+
+ expect(raw_repository).not_to receive(:push_remote_branches).with(remote_mirror.remote_name, [unprotected_branch_name])
+
+ subject.execute(remote_mirror)
+ end
+ end
+
+ context 'when branch exists in local and remote repo' do
+ context 'when it has diverged' do
+ it 'syncs branches' do
+ allow(repository).to receive(:fetch_remote) do
+ sync_remote(repository, remote_mirror.remote_name, local_branch_names)
+ update_remote_branch(repository, remote_mirror.remote_name, 'markdown')
+ end
+
+ expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, ['markdown'])
+
+ subject.execute(remote_mirror)
+ end
+ end
+ end
+
+ describe 'for delete' do
+ context 'when branch exists in local and remote repo' do
+ it 'deletes the branch from remote repo' do
+ allow(repository).to receive(:fetch_remote) do
+ sync_remote(repository, remote_mirror.remote_name, local_branch_names)
+ delete_branch(repository, 'existing-branch')
+ end
+
+ expect(raw_repository).to receive(:delete_remote_branches).with(remote_mirror.remote_name, ['existing-branch'])
+
+ subject.execute(remote_mirror)
+ end
+ end
+
+ context 'when push only protected branches option is set' do
+ before do
+ remote_mirror.only_protected_branches = true
+ end
+
+ context 'when branch exists in local and remote repo' do
+ let!(:protected_branch_name) { local_branch_names.first }
+
+ before do
+ create(:protected_branch, project: project, name: protected_branch_name)
+ project.reload
+ end
+
+ it 'deletes the protected branch from remote repo' do
+ allow(repository).to receive(:fetch_remote) do
+ sync_remote(repository, remote_mirror.remote_name, local_branch_names)
+ delete_branch(repository, protected_branch_name)
+ end
+
+ expect(raw_repository).not_to receive(:delete_remote_branches).with(remote_mirror.remote_name, [protected_branch_name])
+
+ subject.execute(remote_mirror)
+ end
+
+ it 'does not delete the unprotected branch from remote repo' do
+ allow(repository).to receive(:fetch_remote) do
+ sync_remote(repository, remote_mirror.remote_name, local_branch_names)
+ delete_branch(repository, 'existing-branch')
+ end
+
+ expect(raw_repository).not_to receive(:delete_remote_branches).with(remote_mirror.remote_name, ['existing-branch'])
+
+ subject.execute(remote_mirror)
+ end
+ end
+
+ context 'when branch only exists on remote repo' do
+ let!(:protected_branch_name) { 'remote-branch' }
+
+ before do
+ create(:protected_branch, project: project, name: protected_branch_name)
+ end
+
+ context 'when it has diverged' do
+ it 'does not delete the remote branch' do
+ allow(repository).to receive(:fetch_remote) do
+ sync_remote(repository, remote_mirror.remote_name, local_branch_names)
+
+ rev = repository.find_branch('markdown').dereferenced_target
+ create_remote_branch(repository, remote_mirror.remote_name, 'remote-branch', rev.id)
+ end
+
+ expect(raw_repository).not_to receive(:delete_remote_branches)
+
+ subject.execute(remote_mirror)
+ end
+ end
+
+ context 'when it has not diverged' do
+ it 'deletes the remote branch' do
+ allow(repository).to receive(:fetch_remote) do
+ sync_remote(repository, remote_mirror.remote_name, local_branch_names)
+
+ masterrev = repository.find_branch('master').dereferenced_target
+ create_remote_branch(repository, remote_mirror.remote_name, protected_branch_name, masterrev.id)
+ end
+
+ expect(raw_repository).to receive(:delete_remote_branches).with(remote_mirror.remote_name, [protected_branch_name])
+
+ subject.execute(remote_mirror)
+ end
+ end
+ end
+ end
+
+ context 'when branch only exists on remote repo' do
+ context 'when it has diverged' do
+ it 'does not delete the remote branch' do
+ allow(repository).to receive(:fetch_remote) do
+ sync_remote(repository, remote_mirror.remote_name, local_branch_names)
+
+ rev = repository.find_branch('markdown').dereferenced_target
+ create_remote_branch(repository, remote_mirror.remote_name, 'remote-branch', rev.id)
+ end
+
+ expect(raw_repository).not_to receive(:delete_remote_branches)
+
+ subject.execute(remote_mirror)
+ end
+ end
+
+ context 'when it has not diverged' do
+ it 'deletes the remote branch' do
+ allow(repository).to receive(:fetch_remote) do
+ sync_remote(repository, remote_mirror.remote_name, local_branch_names)
+
+ masterrev = repository.find_branch('master').dereferenced_target
+ create_remote_branch(repository, remote_mirror.remote_name, 'remote-branch', masterrev.id)
+ end
+
+ expect(raw_repository).to receive(:delete_remote_branches).with(remote_mirror.remote_name, ['remote-branch'])
+
+ subject.execute(remote_mirror)
+ end
+ end
+ end
+ end
+ end
+
+ describe 'Syncing tags' do
+ before do
+ allow(repository).to receive(:fetch_remote) { sync_remote(repository, remote_mirror.remote_name, local_branch_names) }
+ end
+
+ context 'when there are not tags to push' do
+ it 'does not try to push tags' do
+ allow(repository).to receive(:remote_tags) { {} }
+ allow(repository).to receive(:tags) { [] }
+
+ expect(repository).not_to receive(:push_tags)
+
+ subject.execute(remote_mirror)
+ end
+ end
+
+ context 'when there are some tags to push' do
+ it 'pushes tags to remote' do
+ allow(raw_repository).to receive(:remote_tags) { {} }
+
+ expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, ['v1.0.0', 'v1.1.0'])
+
+ subject.execute(remote_mirror)
+ end
+ end
+
+ context 'when there are some tags to delete' do
+ it 'deletes tags from remote' do
+ remote_tags = generate_tags(repository, 'v1.0.0', 'v1.1.0')
+ allow(raw_repository).to receive(:remote_tags) { remote_tags }
+
+ repository.rm_tag(create(:user), 'v1.0.0')
+
+ expect(raw_repository).to receive(:delete_remote_branches).with(remote_mirror.remote_name, ['v1.0.0'])
+
+ subject.execute(remote_mirror)
+ end
+ end
+ end
+ end
+
+ def create_branch(repository, branch_name)
+ rugged = repository.rugged
+ masterrev = repository.find_branch('master').dereferenced_target
+ parentrev = repository.commit(masterrev).parent_id
+
+ rugged.references.create("refs/heads/#{branch_name}", parentrev)
+
+ repository.expire_branches_cache
+ end
+
+ def create_remote_branch(repository, remote_name, branch_name, source_id)
+ rugged = repository.rugged
+
+ rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", source_id)
+ end
+
+ def sync_remote(repository, remote_name, local_branch_names)
+ rugged = repository.rugged
+
+ local_branch_names.each do |branch|
+ target = repository.find_branch(branch).try(:dereferenced_target)
+ rugged.references.create("refs/remotes/#{remote_name}/#{branch}", target.id) if target
+ end
+ end
+
+ def update_remote_branch(repository, remote_name, branch)
+ rugged = repository.rugged
+ masterrev = repository.find_branch('master').dereferenced_target.id
+
+ rugged.references.create("refs/remotes/#{remote_name}/#{branch}", masterrev, force: true)
+ repository.expire_branches_cache
+ end
+
+ def update_branch(repository, branch)
+ rugged = repository.rugged
+ masterrev = repository.find_branch('master').dereferenced_target.id
+
+ # Updated existing branch
+ rugged.references.create("refs/heads/#{branch}", masterrev, force: true)
+ repository.expire_branches_cache
+ end
+
+ def delete_branch(repository, branch)
+ rugged = repository.rugged
+
+ rugged.references.delete("refs/heads/#{branch}")
+ repository.expire_branches_cache
+ end
+
+ def generate_tags(repository, *tag_names)
+ tag_names.each_with_object([]) do |name, tags|
+ 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)
+ end
+ end
+
+ def local_branch_names
+ branch_names = repository.branches.map(&:name)
+ # we want the protected branch to be pushed first
+ branch_names.unshift(branch_names.delete('master'))
+ end
+end
diff --git a/spec/services/test_hooks/project_service_spec.rb b/spec/services/test_hooks/project_service_spec.rb
index 28dfa9cf59c..962b9f40c4f 100644
--- a/spec/services/test_hooks/project_service_spec.rb
+++ b/spec/services/test_hooks/project_service_spec.rb
@@ -170,6 +170,7 @@ describe TestHooks::ProjectService do
end
context 'wiki_page_events' do
+ let(:project) { create(:project, :wiki_repo) }
let(:trigger) { 'wiki_page_events' }
let(:trigger_key) { :wiki_page_hooks }
diff --git a/spec/services/wiki_pages/create_service_spec.rb b/spec/services/wiki_pages/create_service_spec.rb
index b270194d9b8..259f445247e 100644
--- a/spec/services/wiki_pages/create_service_spec.rb
+++ b/spec/services/wiki_pages/create_service_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe WikiPages::CreateService do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :wiki_repo) }
let(:user) { create(:user) }
let(:opts) do
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index cc61cd7d838..d3de2331244 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -86,6 +86,7 @@ RSpec.configure do |config|
config.include WaitForRequests, :js
config.include LiveDebugger, :js
config.include MigrationsHelpers, :migration
+ config.include RedisHelpers
if ENV['CI']
# This includes the first try, i.e. tests will be run 4 times before failing.
@@ -136,6 +137,13 @@ RSpec.configure do |config|
reset_delivered_emails!
end
+ config.before(:example, :prometheus) do
+ matching_files = File.join(::Prometheus::Client.configuration.multiprocess_files_dir, "*.db")
+ Dir[matching_files].map { |filename| File.delete(filename) if File.file?(filename) }
+
+ Gitlab::Metrics.reset_registry!
+ end
+
config.around(:each, :use_clean_rails_memory_store_caching) do |example|
caching_store = Rails.cache
Rails.cache = ActiveSupport::Cache::MemoryStore.new
@@ -146,21 +154,27 @@ RSpec.configure do |config|
end
config.around(:each, :clean_gitlab_redis_cache) do |example|
- Gitlab::Redis::Cache.with(&:flushall)
+ redis_cache_cleanup!
example.run
- Gitlab::Redis::Cache.with(&:flushall)
+ redis_cache_cleanup!
end
config.around(:each, :clean_gitlab_redis_shared_state) do |example|
- Gitlab::Redis::SharedState.with(&:flushall)
- Sidekiq.redis(&:flushall)
+ redis_shared_state_cleanup!
+
+ example.run
+
+ redis_shared_state_cleanup!
+ end
+
+ config.around(:each, :clean_gitlab_redis_queues) do |example|
+ redis_queues_cleanup!
example.run
- Gitlab::Redis::SharedState.with(&:flushall)
- Sidekiq.redis(&:flushall)
+ redis_queues_cleanup!
end
# The :each scope runs "inside" the example, so this hook ensures the DB is in the
diff --git a/spec/support/api/time_tracking_shared_examples.rb b/spec/support/api/time_tracking_shared_examples.rb
index dd3089d22e5..52e1bc55191 100644
--- a/spec/support/api/time_tracking_shared_examples.rb
+++ b/spec/support/api/time_tracking_shared_examples.rb
@@ -70,8 +70,12 @@ shared_examples 'time tracking endpoints' do |issuable_name|
end
it "add spent time for #{issuable_name}" do
- post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user),
- duration: '2h'
+ Timecop.travel(1.minute.from_now) do
+ expect do
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user),
+ duration: '2h'
+ end.to change { issuable.reload.updated_at }
+ end
expect(response).to have_gitlab_http_status(201)
expect(json_response['human_total_time_spent']).to eq('2h')
@@ -79,7 +83,11 @@ shared_examples 'time tracking endpoints' do |issuable_name|
context 'when subtracting time' do
it 'subtracts time of the total spent time' do
- issuable.update_attributes!(spend_time: { duration: 7200, user_id: user.id })
+ Timecop.travel(1.minute.from_now) do
+ expect do
+ issuable.update_attributes!(spend_time: { duration: 7200, user_id: user.id })
+ end.to change { issuable.reload.updated_at }
+ end
post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user),
duration: '-1h'
@@ -93,8 +101,12 @@ shared_examples 'time tracking endpoints' do |issuable_name|
it 'does not modify the total time spent' do
issuable.update_attributes!(spend_time: { duration: 7200, user_id: user.id })
- post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user),
- duration: '-1w'
+ Timecop.travel(1.minute.from_now) do
+ expect do
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user),
+ duration: '-1w'
+ end.not_to change { issuable.reload.updated_at }
+ end
expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['time_spent'].first).to match(/exceeds the total time spent/)
@@ -110,7 +122,11 @@ shared_examples 'time tracking endpoints' do |issuable_name|
end
it "resets spent time for #{issuable_name}" do
- post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/reset_spent_time", user)
+ Timecop.travel(1.minute.from_now) do
+ expect do
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/reset_spent_time", user)
+ end.to change { issuable.reload.updated_at }
+ end
expect(response).to have_gitlab_http_status(200)
expect(json_response['total_time_spent']).to eq(0)
diff --git a/spec/support/chunked_io/chunked_io_helpers.rb b/spec/support/chunked_io/chunked_io_helpers.rb
new file mode 100644
index 00000000000..fec1f951563
--- /dev/null
+++ b/spec/support/chunked_io/chunked_io_helpers.rb
@@ -0,0 +1,11 @@
+module ChunkedIOHelpers
+ def sample_trace_raw
+ @sample_trace_raw ||= File.read(expand_fixture_path('trace/sample_trace'))
+ .force_encoding(Encoding::BINARY)
+ end
+
+ def stub_buffer_size(size)
+ stub_const('Ci::BuildTraceChunk::CHUNK_SIZE', size)
+ stub_const('Gitlab::Ci::Trace::ChunkedIO::CHUNK_SIZE', size)
+ end
+end
diff --git a/spec/support/forgery_protection.rb b/spec/support/forgery_protection.rb
index a5e7b761651..fa87d5fa881 100644
--- a/spec/support/forgery_protection.rb
+++ b/spec/support/forgery_protection.rb
@@ -1,11 +1,18 @@
+module ForgeryProtection
+ def with_forgery_protection
+ ActionController::Base.allow_forgery_protection = true
+ yield
+ ensure
+ ActionController::Base.allow_forgery_protection = false
+ end
+
+ module_function :with_forgery_protection
+end
+
RSpec.configure do |config|
config.around(:each, :allow_forgery_protection) do |example|
- begin
- ActionController::Base.allow_forgery_protection = true
-
+ ForgeryProtection.with_forgery_protection do
example.call
- ensure
- ActionController::Base.allow_forgery_protection = false
end
end
end
diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb
index e46b61b6461..683a64504a1 100644
--- a/spec/support/helpers/kubernetes_helpers.rb
+++ b/spec/support/helpers/kubernetes_helpers.rb
@@ -9,8 +9,13 @@ module KubernetesHelpers
kube_response(kube_pods_body)
end
+ def kube_deployments_response
+ kube_response(kube_deployments_body)
+ end
+
def stub_kubeclient_discover(api_url)
WebMock.stub_request(:get, api_url + '/api/v1').to_return(kube_response(kube_v1_discovery_body))
+ WebMock.stub_request(:get, api_url + '/apis/extensions/v1beta1').to_return(kube_response(kube_v1beta1_discovery_body))
end
def stub_kubeclient_pods(response = nil)
@@ -20,6 +25,13 @@ module KubernetesHelpers
WebMock.stub_request(:get, pods_url).to_return(response || kube_pods_response)
end
+ def stub_kubeclient_deployments(response = nil)
+ stub_kubeclient_discover(service.api_url)
+ deployments_url = service.api_url + "/apis/extensions/v1beta1/namespaces/#{service.actual_namespace}/deployments"
+
+ WebMock.stub_request(:get, deployments_url).to_return(response || kube_deployments_response)
+ end
+
def stub_kubeclient_get_secrets(api_url, **options)
WebMock.stub_request(:get, api_url + '/api/v1/secrets')
.to_return(kube_response(kube_v1_secrets_body(options)))
@@ -53,6 +65,18 @@ module KubernetesHelpers
"kind" => "APIResourceList",
"resources" => [
{ "name" => "pods", "namespaced" => true, "kind" => "Pod" },
+ { "name" => "deployments", "namespaced" => true, "kind" => "Deployment" },
+ { "name" => "secrets", "namespaced" => true, "kind" => "Secret" }
+ ]
+ }
+ end
+
+ def kube_v1beta1_discovery_body
+ {
+ "kind" => "APIResourceList",
+ "resources" => [
+ { "name" => "pods", "namespaced" => true, "kind" => "Pod" },
+ { "name" => "deployments", "namespaced" => true, "kind" => "Deployment" },
{ "name" => "secrets", "namespaced" => true, "kind" => "Secret" }
]
}
@@ -65,14 +89,25 @@ module KubernetesHelpers
}
end
+ def kube_deployments_body
+ {
+ "kind" => "DeploymentList",
+ "items" => [kube_deployment]
+ }
+ end
+
# This is a partial response, it will have many more elements in reality but
# these are the ones we care about at the moment
- def kube_pod(name: "kube-pod", app: "valid-pod-label")
+ def kube_pod(name: "kube-pod", app: "valid-pod-label", status: "Running", track: nil)
{
"metadata" => {
"name" => name,
+ "generate_name" => "generated-name-with-suffix",
"creationTimestamp" => "2016-11-25T19:55:19Z",
- "labels" => { "app" => app }
+ "labels" => {
+ "app" => app,
+ "track" => track
+ }
},
"spec" => {
"containers" => [
@@ -80,7 +115,27 @@ module KubernetesHelpers
{ "name" => "container-1" }
]
},
- "status" => { "phase" => "Running" }
+ "status" => { "phase" => status }
+ }
+ end
+
+ def kube_deployment(name: "kube-deployment", app: "valid-deployment-label", track: nil)
+ {
+ "metadata" => {
+ "name" => name,
+ "generation" => 4,
+ "labels" => {
+ "app" => app,
+ "track" => track
+ }.compact
+ },
+ "spec" => { "replicas" => 3 },
+ "status" => {
+ "observedGeneration" => 4,
+ "replicas" => 3,
+ "updatedReplicas" => 3,
+ "availableReplicas" => 3
+ }
}
end
@@ -101,4 +156,12 @@ module KubernetesHelpers
terminal
end
end
+
+ def kube_deployment_rollout_status
+ ::Gitlab::Kubernetes::RolloutStatus.from_deployments(kube_deployment)
+ end
+
+ def empty_deployment_rollout_status
+ ::Gitlab::Kubernetes::RolloutStatus.from_deployments()
+ end
end
diff --git a/spec/support/helpers/migrations_helpers.rb b/spec/support/helpers/migrations_helpers.rb
index 5d6f662e8fe..84abec75c26 100644
--- a/spec/support/helpers/migrations_helpers.rb
+++ b/spec/support/helpers/migrations_helpers.rb
@@ -24,6 +24,16 @@ module MigrationsHelpers
end
end
+ def foreign_key_exists?(source, target = nil, column: nil)
+ ActiveRecord::Base.connection.foreign_keys(source).any? do |key|
+ if column
+ key.options[:column].to_s == column.to_s
+ else
+ key.to_table.to_s == target.to_s
+ end
+ end
+ end
+
def reset_column_in_all_models
clear_schema_cache!
diff --git a/spec/support/helpers/notification_helpers.rb b/spec/support/helpers/notification_helpers.rb
new file mode 100644
index 00000000000..8d84510fb73
--- /dev/null
+++ b/spec/support/helpers/notification_helpers.rb
@@ -0,0 +1,33 @@
+module NotificationHelpers
+ extend self
+
+ def send_notifications(*new_mentions)
+ mentionable.description = new_mentions.map(&:to_reference).join(' ')
+
+ notification.send(notification_method, mentionable, new_mentions, @u_disabled)
+ end
+
+ def create_global_setting_for(user, level)
+ setting = user.global_notification_setting
+ setting.level = level
+ setting.save
+
+ user
+ end
+
+ def create_user_with_notification(level, username, resource = project)
+ user = create(:user, username: username)
+ setting = user.notification_settings_for(resource)
+ setting.level = level
+ setting.save
+
+ user
+ end
+
+ # Create custom notifications
+ # When resource is nil it means global notification
+ def update_custom_notification(event, user, resource: nil, value: true)
+ setting = user.notification_settings_for(resource)
+ setting.update!(event => value)
+ end
+end
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index 1dad39fdab3..57aa07cf4fa 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -159,7 +159,11 @@ module TestEnv
end
spawn_script = Rails.root.join('scripts/gitaly-test-spawn').to_s
- @gitaly_pid = Bundler.with_original_env { IO.popen([spawn_script], &:read).to_i }
+ Bundler.with_original_env do
+ raise "gitaly spawn failed" unless system(spawn_script)
+ end
+ @gitaly_pid = Integer(File.read('tmp/tests/gitaly.pid'))
+
Kernel.at_exit { stop_gitaly }
wait_gitaly
diff --git a/spec/support/redis/redis_helpers.rb b/spec/support/redis/redis_helpers.rb
new file mode 100644
index 00000000000..0457e8487d8
--- /dev/null
+++ b/spec/support/redis/redis_helpers.rb
@@ -0,0 +1,18 @@
+module RedisHelpers
+ # config/README.md
+
+ # Usage: performance enhancement
+ def redis_cache_cleanup!
+ Gitlab::Redis::Cache.with(&:flushall)
+ end
+
+ # Usage: SideKiq, Mailroom, CI Runner, Workhorse, push services
+ def redis_queues_cleanup!
+ Gitlab::Redis::Queues.with(&:flushall)
+ end
+
+ # Usage: session state, rate limiting
+ def redis_shared_state_cleanup!
+ Gitlab::Redis::SharedState.with(&:flushall)
+ end
+end
diff --git a/spec/support/services/clusters/create_service_shared.rb b/spec/support/services/clusters/create_service_shared.rb
new file mode 100644
index 00000000000..43a2fd05498
--- /dev/null
+++ b/spec/support/services/clusters/create_service_shared.rb
@@ -0,0 +1,57 @@
+shared_context 'valid cluster create params' do
+ let(:params) do
+ {
+ name: 'test-cluster',
+ provider_type: :gcp,
+ provider_gcp_attributes: {
+ gcp_project_id: 'gcp-project',
+ zone: 'us-central1-a',
+ num_nodes: 1,
+ machine_type: 'machine_type-a'
+ }
+ }
+ end
+end
+
+shared_context 'invalid cluster create params' do
+ let(:params) do
+ {
+ name: 'test-cluster',
+ provider_type: :gcp,
+ provider_gcp_attributes: {
+ gcp_project_id: '!!!!!!!',
+ zone: 'us-central1-a',
+ num_nodes: 1,
+ machine_type: 'machine_type-a'
+ }
+ }
+ end
+end
+
+shared_examples 'create cluster service success' do
+ it 'creates a cluster object and performs a worker' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+
+ expect { subject }
+ .to change { Clusters::Cluster.count }.by(1)
+ .and change { Clusters::Providers::Gcp.count }.by(1)
+
+ expect(subject.name).to eq('test-cluster')
+ expect(subject.user).to eq(user)
+ expect(subject.project).to eq(project)
+ expect(subject.provider.gcp_project_id).to eq('gcp-project')
+ expect(subject.provider.zone).to eq('us-central1-a')
+ expect(subject.provider.num_nodes).to eq(1)
+ expect(subject.provider.machine_type).to eq('machine_type-a')
+ expect(subject.provider.access_token).to eq(access_token)
+ expect(subject.platform).to be_nil
+ end
+end
+
+shared_examples 'create cluster service error' do
+ it 'returns an error' do
+ expect(ClusterProvisionWorker).not_to receive(:perform_async)
+ expect { subject }.to change { Clusters::Cluster.count }.by(0)
+ expect(subject.errors[:"provider_gcp.gcp_project_id"]).to be_present
+ end
+end
diff --git a/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb b/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb
index adfd256dff1..1478c6b5a47 100644
--- a/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb
+++ b/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb
@@ -86,7 +86,7 @@ shared_examples "migrating a deleted user's associated records to the ghost user
end
it "blocks the user before #{record_class_name} migration begins" do
- expect(service).to receive("migrate_#{record_class_name.parameterize('_')}s".to_sym) do
+ expect(service).to receive("migrate_#{record_class_name.parameterize('_').pluralize}".to_sym) do
expect(user.reload).to be_blocked
end
diff --git a/spec/lib/gitlab/email/email_shared_blocks.rb b/spec/support/shared_contexts/email_shared_blocks.rb
index 9d806fc524d..9d806fc524d 100644
--- a/spec/lib/gitlab/email/email_shared_blocks.rb
+++ b/spec/support/shared_contexts/email_shared_blocks.rb
diff --git a/spec/support/shared_examples/ci_trace_shared_examples.rb b/spec/support/shared_examples/ci_trace_shared_examples.rb
new file mode 100644
index 00000000000..21c6f3c829f
--- /dev/null
+++ b/spec/support/shared_examples/ci_trace_shared_examples.rb
@@ -0,0 +1,741 @@
+shared_examples_for 'common trace features' do
+ describe '#html' do
+ before do
+ trace.set("12\n34")
+ end
+
+ it "returns formatted html" do
+ expect(trace.html).to eq("12<br>34")
+ end
+
+ it "returns last line of formatted html" do
+ expect(trace.html(last_lines: 1)).to eq("34")
+ end
+ end
+
+ describe '#raw' do
+ before do
+ trace.set("12\n34")
+ end
+
+ it "returns raw output" do
+ expect(trace.raw).to eq("12\n34")
+ end
+
+ it "returns last line of raw output" do
+ expect(trace.raw(last_lines: 1)).to eq("34")
+ end
+ end
+
+ describe '#extract_coverage' do
+ let(:regex) { '\(\d+.\d+\%\) covered' }
+
+ context 'matching coverage' do
+ before do
+ trace.set('Coverage 1033 / 1051 LOC (98.29%) covered')
+ end
+
+ it "returns valid coverage" do
+ expect(trace.extract_coverage(regex)).to eq("98.29")
+ end
+ end
+
+ context 'no coverage' do
+ before do
+ trace.set('No coverage')
+ end
+
+ it 'returs nil' do
+ expect(trace.extract_coverage(regex)).to be_nil
+ end
+ end
+ end
+
+ describe '#extract_sections' do
+ let(:log) { 'No sections' }
+ let(:sections) { trace.extract_sections }
+
+ before do
+ trace.set(log)
+ end
+
+ context 'no sections' do
+ it 'returs []' do
+ expect(trace.extract_sections).to eq([])
+ end
+ end
+
+ context 'multiple sections available' do
+ let(:log) { File.read(expand_fixture_path('trace/trace_with_sections')) }
+ let(:sections_data) do
+ [
+ { name: 'prepare_script', lines: 2, duration: 3.seconds },
+ { name: 'get_sources', lines: 4, duration: 1.second },
+ { name: 'restore_cache', lines: 0, duration: 0.seconds },
+ { name: 'download_artifacts', lines: 0, duration: 0.seconds },
+ { name: 'build_script', lines: 2, duration: 1.second },
+ { name: 'after_script', lines: 0, duration: 0.seconds },
+ { name: 'archive_cache', lines: 0, duration: 0.seconds },
+ { name: 'upload_artifacts', lines: 0, duration: 0.seconds }
+ ]
+ end
+
+ it "returns valid sections" do
+ expect(sections).not_to be_empty
+ expect(sections.size).to eq(sections_data.size),
+ "expected #{sections_data.size} sections, got #{sections.size}"
+
+ buff = StringIO.new(log)
+ sections.each_with_index do |s, i|
+ expected = sections_data[i]
+
+ expect(s[:name]).to eq(expected[:name])
+ expect(s[:date_end] - s[:date_start]).to eq(expected[:duration])
+
+ buff.seek(s[:byte_start], IO::SEEK_SET)
+ length = s[:byte_end] - s[:byte_start]
+ lines = buff.read(length).count("\n")
+ expect(lines).to eq(expected[:lines])
+ end
+ end
+ end
+
+ context 'logs contains "section_start"' do
+ let(:log) { "section_start:1506417476:a_section\r\033[0Klooks like a section_start:invalid\nsection_end:1506417477:a_section\r\033[0K"}
+
+ it "returns only one section" do
+ expect(sections).not_to be_empty
+ expect(sections.size).to eq(1)
+
+ section = sections[0]
+ expect(section[:name]).to eq('a_section')
+ expect(section[:byte_start]).not_to eq(section[:byte_end]), "got an empty section"
+ end
+ end
+
+ context 'missing section_end' do
+ let(:log) { "section_start:1506417476:a_section\r\033[0KSome logs\nNo section_end\n"}
+
+ it "returns no sections" do
+ expect(sections).to be_empty
+ end
+ end
+
+ context 'missing section_start' do
+ let(:log) { "Some logs\nNo section_start\nsection_end:1506417476:a_section\r\033[0K"}
+
+ it "returns no sections" do
+ expect(sections).to be_empty
+ end
+ end
+
+ context 'inverted section_start section_end' do
+ let(:log) { "section_end:1506417476:a_section\r\033[0Klooks like a section_start:invalid\nsection_start:1506417477:a_section\r\033[0K"}
+
+ it "returns no sections" do
+ expect(sections).to be_empty
+ end
+ end
+ end
+
+ describe '#set' do
+ before do
+ trace.set("12")
+ end
+
+ it "returns trace" do
+ expect(trace.raw).to eq("12")
+ end
+
+ context 'overwrite trace' do
+ before do
+ trace.set("34")
+ end
+
+ it "returns new trace" do
+ expect(trace.raw).to eq("34")
+ end
+ end
+
+ context 'runners token' do
+ let(:token) { 'my_secret_token' }
+
+ before do
+ build.project.update(runners_token: token)
+ trace.set(token)
+ end
+
+ it "hides token" do
+ expect(trace.raw).not_to include(token)
+ end
+ end
+
+ context 'hides build token' do
+ let(:token) { 'my_secret_token' }
+
+ before do
+ build.update(token: token)
+ trace.set(token)
+ end
+
+ it "hides token" do
+ expect(trace.raw).not_to include(token)
+ end
+ end
+ end
+
+ describe '#append' do
+ before do
+ trace.set("1234")
+ end
+
+ it "returns correct trace" do
+ expect(trace.append("56", 4)).to eq(6)
+ expect(trace.raw).to eq("123456")
+ end
+
+ context 'tries to append trace at different offset' do
+ it "fails with append" do
+ expect(trace.append("56", 2)).to eq(4)
+ expect(trace.raw).to eq("1234")
+ end
+ end
+
+ context 'runners token' do
+ let(:token) { 'my_secret_token' }
+
+ before do
+ build.project.update(runners_token: token)
+ trace.append(token, 0)
+ end
+
+ it "hides token" do
+ expect(trace.raw).not_to include(token)
+ end
+ end
+
+ context 'build token' do
+ let(:token) { 'my_secret_token' }
+
+ before do
+ build.update(token: token)
+ trace.append(token, 0)
+ end
+
+ it "hides token" do
+ expect(trace.raw).not_to include(token)
+ end
+ end
+ end
+end
+
+shared_examples_for 'trace with disabled live trace feature' do
+ it_behaves_like 'common trace features'
+
+ describe '#read' do
+ shared_examples 'read successfully with IO' do
+ it 'yields with source' do
+ trace.read do |stream|
+ expect(stream).to be_a(Gitlab::Ci::Trace::Stream)
+ expect(stream.stream).to be_a(IO)
+ end
+ end
+ end
+
+ shared_examples 'read successfully with StringIO' do
+ it 'yields with source' do
+ trace.read do |stream|
+ expect(stream).to be_a(Gitlab::Ci::Trace::Stream)
+ expect(stream.stream).to be_a(StringIO)
+ end
+ end
+ end
+
+ shared_examples 'failed to read' do
+ it 'yields without source' do
+ trace.read do |stream|
+ expect(stream).to be_a(Gitlab::Ci::Trace::Stream)
+ expect(stream.stream).to be_nil
+ end
+ end
+ end
+
+ context 'when trace artifact exists' do
+ before do
+ create(:ci_job_artifact, :trace, job: build)
+ end
+
+ it_behaves_like 'read successfully with IO'
+ end
+
+ context 'when current_path (with project_id) exists' do
+ before do
+ expect(trace).to receive(:default_path) { expand_fixture_path('trace/sample_trace') }
+ end
+
+ it_behaves_like 'read successfully with IO'
+ end
+
+ context 'when current_path (with project_ci_id) exists' do
+ before do
+ expect(trace).to receive(:deprecated_path) { expand_fixture_path('trace/sample_trace') }
+ end
+
+ it_behaves_like 'read successfully with IO'
+ end
+
+ context 'when db trace exists' do
+ before do
+ build.send(:write_attribute, :trace, "data")
+ end
+
+ it_behaves_like 'read successfully with StringIO'
+ end
+
+ context 'when no sources exist' do
+ it_behaves_like 'failed to read'
+ end
+ end
+
+ describe 'trace handling' do
+ subject { trace.exist? }
+
+ context 'trace does not exist' do
+ it { expect(trace.exist?).to be(false) }
+ end
+
+ context 'when trace artifact exists' do
+ before do
+ create(:ci_job_artifact, :trace, job: build)
+ end
+
+ it { is_expected.to be_truthy }
+
+ context 'when the trace artifact has been erased' do
+ before do
+ trace.erase!
+ end
+
+ it { is_expected.to be_falsy }
+
+ it 'removes associations' do
+ expect(Ci::JobArtifact.exists?(job_id: build.id, file_type: :trace)).to be_falsy
+ end
+ end
+ end
+
+ context 'new trace path is used' do
+ before do
+ trace.send(:ensure_directory)
+
+ File.open(trace.send(:default_path), "w") do |file|
+ file.write("data")
+ end
+ end
+
+ it "trace exist" do
+ expect(trace.exist?).to be(true)
+ end
+
+ it "can be erased" do
+ trace.erase!
+ expect(trace.exist?).to be(false)
+ end
+ end
+
+ context 'deprecated path' do
+ let(:path) { trace.send(:deprecated_path) }
+
+ context 'with valid ci_id' do
+ before do
+ build.project.update(ci_id: 1000)
+
+ FileUtils.mkdir_p(File.dirname(path))
+
+ File.open(path, "w") do |file|
+ file.write("data")
+ end
+ end
+
+ it "trace exist" do
+ expect(trace.exist?).to be(true)
+ end
+
+ it "can be erased" do
+ trace.erase!
+ expect(trace.exist?).to be(false)
+ end
+ end
+
+ context 'without valid ci_id' do
+ it "does not return deprecated path" do
+ expect(path).to be_nil
+ end
+ end
+ end
+
+ context 'stored in database' do
+ before do
+ build.send(:write_attribute, :trace, "data")
+ end
+
+ it "trace exist" do
+ expect(trace.exist?).to be(true)
+ end
+
+ it "can be erased" do
+ trace.erase!
+ expect(trace.exist?).to be(false)
+ end
+
+ it "returns database data" do
+ expect(trace.raw).to eq("data")
+ end
+ end
+ end
+
+ describe '#archive!' do
+ subject { trace.archive! }
+
+ shared_examples 'archive trace file' do
+ it do
+ expect { subject }.to change { Ci::JobArtifact.count }.by(1)
+
+ build.reload
+ expect(build.trace.exist?).to be_truthy
+ expect(build.job_artifacts_trace.file.exists?).to be_truthy
+ expect(build.job_artifacts_trace.file.filename).to eq('job.log')
+ expect(File.exist?(src_path)).to be_falsy
+ expect(src_checksum)
+ .to eq(Digest::SHA256.file(build.job_artifacts_trace.file.path).hexdigest)
+ expect(build.job_artifacts_trace.file_sha256).to eq(src_checksum)
+ end
+ end
+
+ shared_examples 'source trace file stays intact' do |error:|
+ it do
+ expect { subject }.to raise_error(error)
+
+ build.reload
+ expect(build.trace.exist?).to be_truthy
+ expect(build.job_artifacts_trace).to be_nil
+ expect(File.exist?(src_path)).to be_truthy
+ end
+ end
+
+ shared_examples 'archive trace in database' do
+ it do
+ expect { subject }.to change { Ci::JobArtifact.count }.by(1)
+
+ build.reload
+ expect(build.trace.exist?).to be_truthy
+ expect(build.job_artifacts_trace.file.exists?).to be_truthy
+ expect(build.job_artifacts_trace.file.filename).to eq('job.log')
+ expect(build.old_trace).to be_nil
+ expect(src_checksum)
+ .to eq(Digest::SHA256.file(build.job_artifacts_trace.file.path).hexdigest)
+ expect(build.job_artifacts_trace.file_sha256).to eq(src_checksum)
+ end
+ end
+
+ shared_examples 'source trace in database stays intact' do |error:|
+ it do
+ expect { subject }.to raise_error(error)
+
+ build.reload
+ expect(build.trace.exist?).to be_truthy
+ expect(build.job_artifacts_trace).to be_nil
+ expect(build.old_trace).to eq(trace_content)
+ end
+ end
+
+ context 'when job does not have trace artifact' do
+ context 'when trace file stored in default path' do
+ let!(:build) { create(:ci_build, :success, :trace_live) }
+ let!(:src_path) { trace.read { |s| s.path } }
+ let!(:src_checksum) { Digest::SHA256.file(src_path).hexdigest }
+
+ it_behaves_like 'archive trace file'
+
+ context 'when failed to create clone file' do
+ before do
+ allow(IO).to receive(:copy_stream).and_return(0)
+ end
+
+ it_behaves_like 'source trace file stays intact', error: Gitlab::Ci::Trace::ArchiveError
+ end
+
+ context 'when failed to create job artifact record' do
+ before do
+ allow_any_instance_of(Ci::JobArtifact).to receive(:save).and_return(false)
+ allow_any_instance_of(Ci::JobArtifact).to receive_message_chain(:errors, :full_messages)
+ .and_return(%w[Error Error])
+ end
+
+ it_behaves_like 'source trace file stays intact', error: ActiveRecord::RecordInvalid
+ end
+ end
+
+ context 'when trace is stored in database' do
+ let(:build) { create(:ci_build, :success) }
+ let(:trace_content) { 'Sample trace' }
+ let(:src_checksum) { Digest::SHA256.hexdigest(trace_content) }
+
+ before do
+ build.update_column(:trace, trace_content)
+ end
+
+ it_behaves_like 'archive trace in database'
+
+ context 'when failed to create clone file' do
+ before do
+ allow(IO).to receive(:copy_stream).and_return(0)
+ end
+
+ it_behaves_like 'source trace in database stays intact', error: Gitlab::Ci::Trace::ArchiveError
+ end
+
+ context 'when failed to create job artifact record' do
+ before do
+ allow_any_instance_of(Ci::JobArtifact).to receive(:save).and_return(false)
+ allow_any_instance_of(Ci::JobArtifact).to receive_message_chain(:errors, :full_messages)
+ .and_return(%w[Error Error])
+ end
+
+ it_behaves_like 'source trace in database stays intact', error: ActiveRecord::RecordInvalid
+ end
+
+ context 'when there is a validation error on Ci::Build' do
+ before do
+ allow_any_instance_of(Ci::Build).to receive(:save).and_return(false)
+ allow_any_instance_of(Ci::Build).to receive_message_chain(:errors, :full_messages)
+ .and_return(%w[Error Error])
+ end
+
+ context "when erase old trace with 'save'" do
+ before do
+ build.send(:write_attribute, :trace, nil)
+ build.save
+ end
+
+ it 'old trace is not deleted' do
+ build.reload
+ expect(build.trace.raw).to eq(trace_content)
+ end
+ end
+
+ it_behaves_like 'archive trace in database'
+ end
+ end
+ end
+
+ context 'when job has trace artifact' do
+ before do
+ create(:ci_job_artifact, :trace, job: build)
+ end
+
+ it 'does not archive' do
+ expect_any_instance_of(described_class).not_to receive(:archive_stream!)
+ expect { subject }.to raise_error('Already archived')
+ expect(build.job_artifacts_trace.file.exists?).to be_truthy
+ end
+ end
+
+ context 'when job is not finished yet' do
+ let!(:build) { create(:ci_build, :running, :trace_live) }
+
+ it 'does not archive' do
+ expect_any_instance_of(described_class).not_to receive(:archive_stream!)
+ expect { subject }.to raise_error('Job is not finished yet')
+ expect(build.trace.exist?).to be_truthy
+ end
+ end
+ end
+end
+
+shared_examples_for 'trace with enabled live trace feature' do
+ it_behaves_like 'common trace features'
+
+ describe '#read' do
+ shared_examples 'read successfully with IO' do
+ it 'yields with source' do
+ trace.read do |stream|
+ expect(stream).to be_a(Gitlab::Ci::Trace::Stream)
+ expect(stream.stream).to be_a(IO)
+ end
+ end
+ end
+
+ shared_examples 'read successfully with ChunkedIO' do
+ it 'yields with source' do
+ trace.read do |stream|
+ expect(stream).to be_a(Gitlab::Ci::Trace::Stream)
+ expect(stream.stream).to be_a(Gitlab::Ci::Trace::ChunkedIO)
+ end
+ end
+ end
+
+ shared_examples 'failed to read' do
+ it 'yields without source' do
+ trace.read do |stream|
+ expect(stream).to be_a(Gitlab::Ci::Trace::Stream)
+ expect(stream.stream).to be_nil
+ end
+ end
+ end
+
+ context 'when trace artifact exists' do
+ before do
+ create(:ci_job_artifact, :trace, job: build)
+ end
+
+ it_behaves_like 'read successfully with IO'
+ end
+
+ context 'when live trace exists' do
+ before do
+ Gitlab::Ci::Trace::ChunkedIO.new(build) do |stream|
+ stream.write('abc')
+ end
+ end
+
+ it_behaves_like 'read successfully with ChunkedIO'
+ end
+
+ context 'when no sources exist' do
+ it_behaves_like 'failed to read'
+ end
+ end
+
+ describe 'trace handling' do
+ subject { trace.exist? }
+
+ context 'trace does not exist' do
+ it { expect(trace.exist?).to be(false) }
+ end
+
+ context 'when trace artifact exists' do
+ before do
+ create(:ci_job_artifact, :trace, job: build)
+ end
+
+ it { is_expected.to be_truthy }
+
+ context 'when the trace artifact has been erased' do
+ before do
+ trace.erase!
+ end
+
+ it { is_expected.to be_falsy }
+
+ it 'removes associations' do
+ expect(Ci::JobArtifact.exists?(job_id: build.id, file_type: :trace)).to be_falsy
+ end
+ end
+ end
+
+ context 'stored in live trace' do
+ before do
+ Gitlab::Ci::Trace::ChunkedIO.new(build) do |stream|
+ stream.write('abc')
+ end
+ end
+
+ it "trace exist" do
+ expect(trace.exist?).to be(true)
+ end
+
+ it "can be erased" do
+ trace.erase!
+ expect(trace.exist?).to be(false)
+ expect(Ci::BuildTraceChunk.where(build: build)).not_to be_exist
+ end
+
+ it "returns live trace data" do
+ expect(trace.raw).to eq("abc")
+ end
+ end
+ end
+
+ describe '#archive!' do
+ subject { trace.archive! }
+
+ shared_examples 'archive trace file in ChunkedIO' do
+ it do
+ expect { subject }.to change { Ci::JobArtifact.count }.by(1)
+
+ build.reload
+ expect(build.trace.exist?).to be_truthy
+ expect(build.job_artifacts_trace.file.exists?).to be_truthy
+ expect(build.job_artifacts_trace.file.filename).to eq('job.log')
+ expect(Ci::BuildTraceChunk.where(build: build)).not_to be_exist
+ expect(src_checksum)
+ .to eq(Digest::SHA256.file(build.job_artifacts_trace.file.path).hexdigest)
+ expect(build.job_artifacts_trace.file_sha256).to eq(src_checksum)
+ end
+ end
+
+ shared_examples 'source trace in ChunkedIO stays intact' do |error:|
+ it do
+ expect { subject }.to raise_error(error)
+
+ build.reload
+ expect(build.trace.exist?).to be_truthy
+ expect(build.job_artifacts_trace).to be_nil
+ Gitlab::Ci::Trace::ChunkedIO.new(build) do |stream|
+ expect(stream.read).to eq(trace_raw)
+ end
+ end
+ end
+
+ context 'when job does not have trace artifact' do
+ context 'when trace is stored in ChunkedIO' do
+ let!(:build) { create(:ci_build, :success, :trace_live) }
+ let!(:trace_raw) { build.trace.raw }
+ let!(:src_checksum) { Digest::SHA256.hexdigest(trace_raw) }
+
+ it_behaves_like 'archive trace file in ChunkedIO'
+
+ context 'when failed to create clone file' do
+ before do
+ allow(IO).to receive(:copy_stream).and_return(0)
+ end
+
+ it_behaves_like 'source trace in ChunkedIO stays intact', error: Gitlab::Ci::Trace::ArchiveError
+ end
+
+ context 'when failed to create job artifact record' do
+ before do
+ allow_any_instance_of(Ci::JobArtifact).to receive(:save).and_return(false)
+ allow_any_instance_of(Ci::JobArtifact).to receive_message_chain(:errors, :full_messages)
+ .and_return(%w[Error Error])
+ end
+
+ it_behaves_like 'source trace in ChunkedIO stays intact', error: ActiveRecord::RecordInvalid
+ end
+ end
+ end
+
+ context 'when job has trace artifact' do
+ before do
+ create(:ci_job_artifact, :trace, job: build)
+ end
+
+ it 'does not archive' do
+ expect_any_instance_of(described_class).not_to receive(:archive_stream!)
+ expect { subject }.to raise_error('Already archived')
+ expect(build.job_artifacts_trace.file.exists?).to be_truthy
+ end
+ end
+
+ context 'when job is not finished yet' do
+ let!(:build) { create(:ci_build, :running, :trace_live) }
+
+ it 'does not archive' do
+ expect_any_instance_of(described_class).not_to receive(:archive_stream!)
+ expect { subject }.to raise_error('Job is not finished yet')
+ expect(build.trace.exist?).to be_truthy
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/common_system_notes_examples.rb b/spec/support/shared_examples/common_system_notes_examples.rb
new file mode 100644
index 00000000000..96ef30b7513
--- /dev/null
+++ b/spec/support/shared_examples/common_system_notes_examples.rb
@@ -0,0 +1,27 @@
+shared_examples 'system note creation' do |update_params, note_text|
+ subject { described_class.new(project, user).execute(issuable, [])}
+
+ before do
+ issuable.assign_attributes(update_params)
+ issuable.save
+ end
+
+ it 'creates 1 system note with the correct content' do
+ expect { subject }.to change { Note.count }.from(0).to(1)
+
+ note = Note.last
+ expect(note.note).to match(note_text)
+ expect(note.noteable_type).to eq(issuable.class.name)
+ end
+end
+
+shared_examples 'WIP notes creation' do |wip_action|
+ subject { described_class.new(project, user).execute(issuable, []) }
+
+ it 'creates WIP toggle and title change notes' do
+ expect { subject }.to change { Note.count }.from(0).to(2)
+
+ expect(Note.first.note).to match("#{wip_action} as a **Work In Progress**")
+ expect(Note.second.note).to match('changed title')
+ end
+end
diff --git a/spec/support/shared_examples/fast_destroy_all.rb b/spec/support/shared_examples/fast_destroy_all.rb
new file mode 100644
index 00000000000..5448ddcfe33
--- /dev/null
+++ b/spec/support/shared_examples/fast_destroy_all.rb
@@ -0,0 +1,38 @@
+shared_examples_for 'fast destroyable' do
+ describe 'Forbid #destroy and #destroy_all' do
+ it 'does not delete database rows and associted external data' do
+ expect(external_data_counter).to be > 0
+ expect(subjects.count).to be > 0
+
+ expect { subjects.first.destroy }.to raise_error('`destroy` and `destroy_all` are forbbiden. Please use `fast_destroy_all`')
+ expect { subjects.destroy_all }.to raise_error('`destroy` and `destroy_all` are forbbiden. Please use `fast_destroy_all`')
+
+ expect(subjects.count).to be > 0
+ expect(external_data_counter).to be > 0
+ end
+ end
+
+ describe '.fast_destroy_all' do
+ it 'deletes database rows and associted external data' do
+ expect(external_data_counter).to be > 0
+ expect(subjects.count).to be > 0
+
+ expect { subjects.fast_destroy_all }.not_to raise_error
+
+ expect(subjects.count).to eq(0)
+ expect(external_data_counter).to eq(0)
+ end
+ end
+
+ describe '.use_fast_destroy' do
+ it 'performs cascading delete with fast_destroy_all' do
+ expect(external_data_counter).to be > 0
+ expect(subjects.count).to be > 0
+
+ expect { parent.destroy }.not_to raise_error
+
+ expect(subjects.count).to eq(0)
+ expect(external_data_counter).to eq(0)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/notify_shared_examples.rb b/spec/support/shared_examples/notify_shared_examples.rb
index e2c23607406..43fdaddf545 100644
--- a/spec/support/shared_examples/notify_shared_examples.rb
+++ b/spec/support/shared_examples/notify_shared_examples.rb
@@ -197,3 +197,35 @@ end
shared_examples 'an email with a labels subscriptions link in its footer' do
it { is_expected.to have_body_text('label subscriptions') }
end
+
+shared_examples 'a note email' do
+ it_behaves_like 'it should have Gmail Actions links'
+
+ it 'is sent to the given recipient as the author' do
+ sender = subject.header[:from].addrs[0]
+
+ aggregate_failures do
+ expect(sender.display_name).to eq(note_author.name)
+ expect(sender.address).to eq(gitlab_sender)
+ expect(subject).to deliver_to(recipient.notification_email)
+ end
+ end
+
+ it 'contains the message from the note' do
+ is_expected.to have_html_escaped_body_text note.note
+ end
+
+ it 'does not contain note author' do
+ is_expected.not_to have_body_text note.author_name
+ end
+
+ context 'when enabled email_author_in_body' do
+ before do
+ stub_application_setting(email_author_in_body: true)
+ end
+
+ it 'contains a link to note author' do
+ is_expected.to have_html_escaped_body_text note.author_name
+ end
+ end
+end
diff --git a/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb b/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb
index 07bc3a51fd8..2228e872926 100644
--- a/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb
+++ b/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb
@@ -35,7 +35,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
describe "#execute" do
let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
+ let(:project) { create(:project, :repository, :wiki_repo) }
let(:username) { 'slack_username' }
let(:channel) { 'slack_channel' }
let(:issue_service_options) { { title: 'Awesome issue', description: 'please fix' } }
diff --git a/spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb b/spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb
index 7a7dcb71680..aed62f97448 100644
--- a/spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb
+++ b/spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb
@@ -7,113 +7,138 @@ describe ObjectStorage::MigrateUploadsWorker, :sidekiq do
end
end
- let!(:projects) { create_list(:project, 10, :with_avatar) }
- let(:uploads) { Upload.all }
let(:model_class) { Project }
- let(:mounted_as) { :avatar }
+ let(:uploads) { Upload.all }
let(:to_store) { ObjectStorage::Store::REMOTE }
- before do
- stub_uploads_object_storage(AvatarUploader)
- end
-
- describe '.enqueue!' do
- def enqueue!
- described_class.enqueue!(uploads, Project, mounted_as, to_store)
- end
+ shared_examples "uploads migration worker" do
+ describe '.enqueue!' do
+ def enqueue!
+ described_class.enqueue!(uploads, Project, mounted_as, to_store)
+ end
- it 'is guarded by .sanity_check!' do
- expect(described_class).to receive(:perform_async)
- expect(described_class).to receive(:sanity_check!)
+ it 'is guarded by .sanity_check!' do
+ expect(described_class).to receive(:perform_async)
+ expect(described_class).to receive(:sanity_check!)
- enqueue!
- end
+ enqueue!
+ end
- context 'sanity_check! fails' do
- include_context 'sanity_check! fails'
+ context 'sanity_check! fails' do
+ include_context 'sanity_check! fails'
- it 'does not enqueue a job' do
- expect(described_class).not_to receive(:perform_async)
+ it 'does not enqueue a job' do
+ expect(described_class).not_to receive(:perform_async)
- expect { enqueue! }.to raise_error(described_class::SanityCheckError)
+ expect { enqueue! }.to raise_error(described_class::SanityCheckError)
+ end
end
end
- end
- describe '.sanity_check!' do
- shared_examples 'raises a SanityCheckError' do
- let(:mount_point) { nil }
+ describe '.sanity_check!' do
+ shared_examples 'raises a SanityCheckError' do
+ let(:mount_point) { nil }
- it do
- expect { described_class.sanity_check!(uploads, model_class, mount_point) }
- .to raise_error(described_class::SanityCheckError)
+ it do
+ expect { described_class.sanity_check!(uploads, model_class, mount_point) }
+ .to raise_error(described_class::SanityCheckError)
+ end
end
- end
- context 'uploader types mismatch' do
- let!(:outlier) { create(:upload, uploader: 'FileUploader') }
+ before do
+ stub_const("WrongModel", Class.new)
+ end
- include_examples 'raises a SanityCheckError'
- end
+ context 'uploader types mismatch' do
+ let!(:outlier) { create(:upload, uploader: 'GitlabUploader') }
- context 'model types mismatch' do
- let!(:outlier) { create(:upload, model_type: 'Potato') }
+ include_examples 'raises a SanityCheckError'
+ end
- include_examples 'raises a SanityCheckError'
- end
+ context 'model types mismatch' do
+ let!(:outlier) { create(:upload, model_type: 'WrongModel') }
- context 'mount point not found' do
- include_examples 'raises a SanityCheckError' do
- let(:mount_point) { :potato }
+ include_examples 'raises a SanityCheckError'
end
- end
- end
- describe '#perform' do
- def perform
- described_class.new.perform(uploads.ids, model_class.to_s, mounted_as, to_store)
- rescue ObjectStorage::MigrateUploadsWorker::Report::MigrationFailures
- # swallow
+ context 'mount point not found' do
+ include_examples 'raises a SanityCheckError' do
+ let(:mount_point) { :potato }
+ end
+ end
end
- shared_examples 'outputs correctly' do |success: 0, failures: 0|
- total = success + failures
+ describe '#perform' do
+ def perform
+ described_class.new.perform(uploads.ids, model_class.to_s, mounted_as, to_store)
+ rescue ObjectStorage::MigrateUploadsWorker::Report::MigrationFailures
+ # swallow
+ end
+
+ shared_examples 'outputs correctly' do |success: 0, failures: 0|
+ total = success + failures
- if success > 0
- it 'outputs the reports' do
- expect(Rails.logger).to receive(:info).with(%r{Migrated #{success}/#{total} files})
+ if success > 0
+ it 'outputs the reports' do
+ expect(Rails.logger).to receive(:info).with(%r{Migrated #{success}/#{total} files})
- perform
+ perform
+ end
end
- end
- if failures > 0
- it 'outputs upload failures' do
- expect(Rails.logger).to receive(:warn).with(/Error .* I am a teapot/)
+ if failures > 0
+ it 'outputs upload failures' do
+ expect(Rails.logger).to receive(:warn).with(/Error .* I am a teapot/)
- perform
+ perform
+ end
end
end
- end
- it_behaves_like 'outputs correctly', success: 10
+ it_behaves_like 'outputs correctly', success: 10
+
+ it 'migrates files' do
+ perform
- it 'migrates files' do
- perform
+ expect(Upload.where(store: ObjectStorage::Store::LOCAL).count).to eq(0)
+ end
- aggregate_failures do
- projects.each do |project|
- expect(project.reload.avatar.upload.local?).to be_falsey
+ context 'migration is unsuccessful' do
+ before do
+ allow_any_instance_of(ObjectStorage::Concern)
+ .to receive(:migrate!).and_raise(CarrierWave::UploadError, "I am a teapot.")
end
+
+ it_behaves_like 'outputs correctly', failures: 10
end
end
+ end
- context 'migration is unsuccessful' do
- before do
- allow_any_instance_of(ObjectStorage::Concern).to receive(:migrate!).and_raise(CarrierWave::UploadError, "I am a teapot.")
- end
+ context "for AvatarUploader" do
+ let!(:projects) { create_list(:project, 10, :with_avatar) }
+ let(:mounted_as) { :avatar }
- it_behaves_like 'outputs correctly', failures: 10
+ before do
+ stub_uploads_object_storage(AvatarUploader)
+ end
+
+ it_behaves_like "uploads migration worker"
+ end
+
+ context "for FileUploader" do
+ let!(:projects) { create_list(:project, 10) }
+ let(:secret) { SecureRandom.hex }
+ let(:mounted_as) { nil }
+
+ before do
+ stub_uploads_object_storage(FileUploader)
+
+ projects.map do |project|
+ uploader = FileUploader.new(project)
+ uploader.store!(fixture_file_upload('spec/fixtures/doc_sample.txt'))
+ end
end
+
+ it_behaves_like "uploads migration worker"
end
end
diff --git a/spec/workers/admin_email_worker_spec.rb b/spec/workers/admin_email_worker_spec.rb
new file mode 100644
index 00000000000..27687f069ea
--- /dev/null
+++ b/spec/workers/admin_email_worker_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe AdminEmailWorker do
+ subject(:worker) { described_class.new }
+
+ describe '.perform' do
+ it 'does not attempt to send repository check mail when they are disabled' do
+ stub_application_setting(repository_checks_enabled: false)
+
+ expect(worker).not_to receive(:send_repository_check_mail)
+
+ worker.perform
+ end
+
+ context 'repository_checks enabled' do
+ before do
+ stub_application_setting(repository_checks_enabled: true)
+ end
+
+ it 'checks if repository check mail should be sent' do
+ expect(worker).to receive(:send_repository_check_mail)
+
+ worker.perform
+ end
+
+ it 'does not send mail when there are no failed repos' do
+ expect(RepositoryCheckMailer).not_to receive(:notify)
+
+ worker.perform
+ end
+
+ it 'send mail when there is a failed repo' do
+ create(:project, last_repository_check_failed: true, last_repository_check_at: Date.yesterday)
+
+ expect(RepositoryCheckMailer).to receive(:notify).and_return(spy)
+
+ worker.perform
+ end
+ end
+ end
+end
diff --git a/spec/workers/repository_check/batch_worker_spec.rb b/spec/workers/repository_check/batch_worker_spec.rb
index 850b8cd8f5c..6cd27d2fafb 100644
--- a/spec/workers/repository_check/batch_worker_spec.rb
+++ b/spec/workers/repository_check/batch_worker_spec.rb
@@ -31,8 +31,8 @@ describe RepositoryCheck::BatchWorker do
it 'does nothing when repository checks are disabled' do
create(:project, created_at: 1.week.ago)
- current_settings = double('settings', repository_checks_enabled: false)
- expect(subject).to receive(:current_settings) { current_settings }
+
+ stub_application_setting(repository_checks_enabled: false)
expect(subject.perform).to eq(nil)
end
diff --git a/spec/workers/repository_check/single_repository_worker_spec.rb b/spec/workers/repository_check/single_repository_worker_spec.rb
index 1d9bbf2ca62..a021235aed6 100644
--- a/spec/workers/repository_check/single_repository_worker_spec.rb
+++ b/spec/workers/repository_check/single_repository_worker_spec.rb
@@ -2,44 +2,60 @@ require 'spec_helper'
require 'fileutils'
describe RepositoryCheck::SingleRepositoryWorker do
- subject { described_class.new }
+ subject(:worker) { described_class.new }
- it 'passes when the project has no push events' do
- project = create(:project_empty_repo, :wiki_disabled)
+ it 'skips when the project has no push events' do
+ project = create(:project, :repository, :wiki_disabled)
project.events.destroy_all
- break_repo(project)
+ break_project(project)
- subject.perform(project.id)
+ expect(worker).not_to receive(:git_fsck)
+
+ worker.perform(project.id)
expect(project.reload.last_repository_check_failed).to eq(false)
end
it 'fails when the project has push events and a broken repository' do
- project = create(:project_empty_repo)
+ project = create(:project, :repository)
create_push_event(project)
- break_repo(project)
+ break_project(project)
- subject.perform(project.id)
+ worker.perform(project.id)
expect(project.reload.last_repository_check_failed).to eq(true)
end
+ it 'succeeds when the project repo is valid' do
+ project = create(:project, :repository, :wiki_disabled)
+ create_push_event(project)
+
+ expect(worker).to receive(:git_fsck).and_call_original
+
+ expect do
+ worker.perform(project.id)
+ end.to change { project.reload.last_repository_check_at }
+
+ expect(project.reload.last_repository_check_failed).to eq(false)
+ end
+
it 'fails if the wiki repository is broken' do
- project = create(:project_empty_repo, :wiki_enabled)
+ project = create(:project, :repository, :wiki_enabled)
project.create_wiki
+ create_push_event(project)
# Test sanity: everything should be fine before the wiki repo is broken
- subject.perform(project.id)
+ worker.perform(project.id)
expect(project.reload.last_repository_check_failed).to eq(false)
break_wiki(project)
- subject.perform(project.id)
+ worker.perform(project.id)
expect(project.reload.last_repository_check_failed).to eq(true)
end
it 'skips wikis when disabled' do
- project = create(:project_empty_repo, :wiki_disabled)
+ project = create(:project, :wiki_disabled)
# Make sure the test would fail if the wiki repo was checked
break_wiki(project)
@@ -49,8 +65,8 @@ describe RepositoryCheck::SingleRepositoryWorker do
end
it 'creates missing wikis' do
- project = create(:project_empty_repo, :wiki_enabled)
- FileUtils.rm_rf(wiki_path(project))
+ project = create(:project, :wiki_enabled)
+ Gitlab::Shell.new.rm_directory(project.repository_storage, project.wiki.path)
subject.perform(project.id)
@@ -58,34 +74,39 @@ describe RepositoryCheck::SingleRepositoryWorker do
end
it 'does not create a wiki if the main repo does not exist at all' do
- project = create(:project_empty_repo)
- create_push_event(project)
- FileUtils.rm_rf(project.repository.path_to_repo)
- FileUtils.rm_rf(wiki_path(project))
+ project = create(:project, :repository)
+ Gitlab::Shell.new.rm_directory(project.repository_storage, project.path)
+ Gitlab::Shell.new.rm_directory(project.repository_storage, project.wiki.path)
subject.perform(project.id)
- expect(File.exist?(wiki_path(project))).to eq(false)
+ expect(Gitlab::Shell.new.exists?(project.repository_storage, project.wiki.path)).to eq(false)
end
- def break_wiki(project)
- objects_dir = wiki_path(project) + '/objects'
+ def create_push_event(project)
+ project.events.create(action: Event::PUSHED, author_id: create(:user).id)
+ end
- # Replace the /objects directory with a file so that the repo is
- # invalid, _and_ 'git init' cannot fix it.
- FileUtils.rm_rf(objects_dir)
- FileUtils.touch(objects_dir) if File.directory?(wiki_path(project))
+ def break_wiki(project)
+ break_repo(wiki_path(project))
end
def wiki_path(project)
project.wiki.repository.path_to_repo
end
- def create_push_event(project)
- project.events.create(action: Event::PUSHED, author_id: create(:user).id)
+ def break_project(project)
+ break_repo(project.repository.path_to_repo)
end
- def break_repo(project)
- FileUtils.rm_rf(File.join(project.repository.path_to_repo, 'objects'))
+ def break_repo(repo)
+ # Create or replace blob ffffffffffffffffffffffffffffffffffffffff with an empty file
+ # This will make the repo invalid, _and_ 'git init' cannot fix it.
+ path = File.join(repo, 'objects', 'ff')
+ file = File.join(path, 'ffffffffffffffffffffffffffffffffffffff')
+
+ FileUtils.mkdir_p(path)
+ FileUtils.rm_f(file)
+ FileUtils.touch(file)
end
end
diff --git a/spec/workers/repository_remove_remote_worker_spec.rb b/spec/workers/repository_remove_remote_worker_spec.rb
new file mode 100644
index 00000000000..f22d7c1d073
--- /dev/null
+++ b/spec/workers/repository_remove_remote_worker_spec.rb
@@ -0,0 +1,50 @@
+require 'rails_helper'
+
+describe RepositoryRemoveRemoteWorker do
+ subject(:worker) { described_class.new }
+
+ describe '#perform' do
+ let(:remote_name) { 'joe'}
+ let!(:project) { create(:project, :repository) }
+
+ context 'when it cannot obtain lease' do
+ it 'logs error' do
+ allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain) { nil }
+
+ expect_any_instance_of(Repository).not_to receive(:remove_remote)
+ expect(worker).to receive(:log_error).with('Cannot obtain an exclusive lease. There must be another instance already in execution.')
+
+ worker.perform(project.id, remote_name)
+ end
+ end
+
+ context 'when it gets the lease' do
+ before do
+ allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(true)
+ end
+
+ context 'when project does not exist' do
+ it 'returns nil' do
+ expect(worker.perform(-1, 'remote_name')).to be_nil
+ end
+ end
+
+ context 'when project exists' do
+ it 'removes remote from repository' do
+ masterrev = project.repository.find_branch('master').dereferenced_target
+
+ create_remote_branch(remote_name, 'remote_branch', masterrev)
+
+ expect_any_instance_of(Repository).to receive(:remove_remote).with(remote_name).and_call_original
+
+ worker.perform(project.id, remote_name)
+ end
+ end
+ end
+ end
+
+ def create_remote_branch(remote_name, branch_name, target)
+ rugged = project.repository.rugged
+ rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", target.id)
+ end
+end
diff --git a/spec/workers/repository_update_remote_mirror_worker_spec.rb b/spec/workers/repository_update_remote_mirror_worker_spec.rb
new file mode 100644
index 00000000000..152ba2509b9
--- /dev/null
+++ b/spec/workers/repository_update_remote_mirror_worker_spec.rb
@@ -0,0 +1,84 @@
+require 'rails_helper'
+
+describe RepositoryUpdateRemoteMirrorWorker do
+ subject { described_class.new }
+
+ let(:remote_mirror) { create(:project, :repository, :remote_mirror).remote_mirrors.first }
+ let(:scheduled_time) { Time.now - 5.minutes }
+
+ around do |example|
+ Timecop.freeze(Time.now) { example.run }
+ end
+
+ describe '#perform' do
+ context 'with status none' do
+ before do
+ remote_mirror.update_attributes(update_status: 'none')
+ end
+
+ it 'sets status as finished when update remote mirror service executes successfully' do
+ expect_any_instance_of(Projects::UpdateRemoteMirrorService).to receive(:execute).with(remote_mirror).and_return(status: :success)
+
+ expect { subject.perform(remote_mirror.id, Time.now) }.to change { remote_mirror.reload.update_status }.to('finished')
+ end
+
+ it 'sets status as failed when update remote mirror service executes with errors' do
+ error_message = 'fail!'
+
+ expect_any_instance_of(Projects::UpdateRemoteMirrorService).to receive(:execute).with(remote_mirror).and_return(status: :error, message: error_message)
+ expect do
+ subject.perform(remote_mirror.id, Time.now)
+ end.to raise_error(RepositoryUpdateRemoteMirrorWorker::UpdateError, error_message)
+
+ expect(remote_mirror.reload.update_status).to eq('failed')
+ end
+
+ it 'does nothing if last_update_started_at is higher than the time the job was scheduled in' do
+ remote_mirror.update_attributes(last_update_started_at: Time.now)
+
+ expect_any_instance_of(RemoteMirror).to receive(:updated_since?).with(scheduled_time).and_return(true)
+ expect_any_instance_of(Projects::UpdateRemoteMirrorService).not_to receive(:execute).with(remote_mirror)
+
+ expect(subject.perform(remote_mirror.id, scheduled_time)).to be_nil
+ end
+ end
+
+ context 'with unexpected error' do
+ it 'marks mirror as failed' do
+ allow_any_instance_of(Projects::UpdateRemoteMirrorService).to receive(:execute).with(remote_mirror).and_raise(RuntimeError)
+
+ expect do
+ subject.perform(remote_mirror.id, Time.now)
+ end.to raise_error(RepositoryUpdateRemoteMirrorWorker::UpdateError)
+ expect(remote_mirror.reload.update_status).to eq('failed')
+ end
+ end
+
+ context 'with another worker already running' do
+ before do
+ remote_mirror.update_attributes(update_status: 'started')
+ end
+
+ it 'raises RemoteMirrorUpdateAlreadyInProgressError' do
+ expect do
+ subject.perform(remote_mirror.id, Time.now)
+ end.to raise_error(RepositoryUpdateRemoteMirrorWorker::UpdateAlreadyInProgressError)
+ end
+ end
+
+ context 'with status failed' do
+ before do
+ remote_mirror.update_attributes(update_status: 'failed')
+ end
+
+ it 'sets status as finished if last_update_started_at is higher than the time the job was scheduled in' do
+ remote_mirror.update_attributes(last_update_started_at: Time.now)
+
+ expect_any_instance_of(RemoteMirror).to receive(:updated_since?).with(scheduled_time).and_return(false)
+ expect_any_instance_of(Projects::UpdateRemoteMirrorService).to receive(:execute).with(remote_mirror).and_return(status: :success)
+
+ expect { subject.perform(remote_mirror.id, scheduled_time) }.to change { remote_mirror.reload.update_status }.to('finished')
+ end
+ end
+ end
+end
diff --git a/vendor/gitignore/Global/JetBrains.gitignore b/vendor/gitignore/Global/JetBrains.gitignore
index a83a428c844..b09cb3dbc04 100644
--- a/vendor/gitignore/Global/JetBrains.gitignore
+++ b/vendor/gitignore/Global/JetBrains.gitignore
@@ -4,7 +4,8 @@
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
-.idea/dictionaries
+.idea/**/dictionaries
+.idea/**/shelf
# Sensitive or high-churn files
.idea/**/dataSources/
diff --git a/vendor/gitignore/Global/Vim.gitignore b/vendor/gitignore/Global/Vim.gitignore
index 6d21783d471..19cfe22f583 100644
--- a/vendor/gitignore/Global/Vim.gitignore
+++ b/vendor/gitignore/Global/Vim.gitignore
@@ -12,3 +12,5 @@ Session.vim
*~
# Auto-generated tag files
tags
+# Persistent undo
+[._]*.un~
diff --git a/vendor/gitignore/Java.gitignore b/vendor/gitignore/Java.gitignore
index 6143e53f9e3..a1c2a238a96 100644
--- a/vendor/gitignore/Java.gitignore
+++ b/vendor/gitignore/Java.gitignore
@@ -13,6 +13,7 @@
# Package Files #
*.jar
*.war
+*.nar
*.ear
*.zip
*.tar.gz
diff --git a/vendor/gitignore/Objective-C.gitignore b/vendor/gitignore/Objective-C.gitignore
index 09dfede4814..86de6aa3f5f 100644
--- a/vendor/gitignore/Objective-C.gitignore
+++ b/vendor/gitignore/Objective-C.gitignore
@@ -52,7 +52,7 @@ Carthage/Build
fastlane/report.xml
fastlane/Preview.html
-fastlane/screenshots
+fastlane/screenshots/**/*.png
fastlane/test_output
# Code Injection
diff --git a/vendor/gitignore/Swift.gitignore b/vendor/gitignore/Swift.gitignore
index 161179bff3e..312d1f652c6 100644
--- a/vendor/gitignore/Swift.gitignore
+++ b/vendor/gitignore/Swift.gitignore
@@ -64,5 +64,5 @@ Carthage/Build
fastlane/report.xml
fastlane/Preview.html
-fastlane/screenshots
+fastlane/screenshots/**/*.png
fastlane/test_output
diff --git a/vendor/gitignore/TeX.gitignore b/vendor/gitignore/TeX.gitignore
index c560658e45c..e6598ba1727 100644
--- a/vendor/gitignore/TeX.gitignore
+++ b/vendor/gitignore/TeX.gitignore
@@ -198,6 +198,9 @@ pythontex-files-*/
# easy-todo
*.lod
+# xmpincl
+*.xmpi
+
# xindy
*.xdy
@@ -234,3 +237,6 @@ TSWLatexianTemp*
# standalone packages
*.sta
+
+# generated if using elsarticle.cls
+*.spl
diff --git a/vendor/gitignore/Unity.gitignore b/vendor/gitignore/Unity.gitignore
index a7c0c70a0b4..0210746b1a5 100644
--- a/vendor/gitignore/Unity.gitignore
+++ b/vendor/gitignore/Unity.gitignore
@@ -6,7 +6,7 @@
Assets/AssetStoreTools*
# Visual Studio cache directory
-/.vs/
+.vs/
# Autogenerated VS/MD/Consulo solution and project files
ExportedObj/
@@ -22,6 +22,7 @@ ExportedObj/
*.booproj
*.svd
*.pdb
+*.opendb
# Unity3D generated meta files
*.pidb.meta
diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore
index 29063cf6072..3e759b75bf4 100644
--- a/vendor/gitignore/VisualStudio.gitignore
+++ b/vendor/gitignore/VisualStudio.gitignore
@@ -240,6 +240,7 @@ Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
+*.rptproj.bak
# SQL Server files
*.mdf
diff --git a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
index 3b77055b644..a00c6e89a1d 100644
--- a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
@@ -12,8 +12,10 @@
# AUTO_DEVOPS_DOMAIN must also be set as a variable at the group or project
# level, or manually added below.
#
-# If you want to deploy to staging first, or enable canary deploys,
-# uncomment the relevant jobs in the pipeline below.
+# Continuous deployment to production is enabled by default.
+# If you want to deploy to staging first, or enable incremental rollouts,
+# set STAGING_ENABLED or INCREMENTAL_ROLLOUT_ENABLED environment variables.
+# If you want to use canary deployments, uncomment the canary job.
#
# If Auto DevOps fails to detect the proper buildpack, or if you want to
# specify a custom buildpack, set a project variable `BUILDPACK_URL` to the
@@ -133,6 +135,7 @@ dependency_scanning:
- dependency_scanning
artifacts:
paths: [gl-dependency-scanning-report.json]
+
sast:container:
image: docker:stable
variables:
@@ -214,10 +217,10 @@ stop_review:
# Staging deploys are disabled by default since
# continuous deployment to production is enabled by default
# If you prefer to automatically deploy to staging and
-# only manually promote to production, enable this job by removing the dot (.),
-# and uncomment the `when: manual` line in the `production` job.
+# only manually promote to production, enable this job by setting
+# STAGING_ENABLED.
-.staging:
+staging:
stage: staging
script:
- check_kube_domain
@@ -234,10 +237,11 @@ stop_review:
refs:
- master
kubernetes: active
+ variables:
+ - $STAGING_ENABLED
# Canaries are disabled by default, but if you want them,
-# and know what the downsides are, enable this job by removing the dot (.),
-# and uncomment the `when: manual` line in the `production` job.
+# and know what the downsides are, enable this job by removing the dot (.).
.canary:
stage: canary
@@ -258,12 +262,53 @@ stop_review:
- master
kubernetes: active
-# This job continuously deploys to production on every push to `master`.
-# To make this a manual process, either because you're enabling `staging`
-# or `canary` deploys, or you simply want more control over when you deploy
-# to production, uncomment the `when: manual` line in the `production` job.
+.production: &production_template
+ stage: production
+ script:
+ - check_kube_domain
+ - install_dependencies
+ - download_chart
+ - ensure_namespace
+ - install_tiller
+ - create_secret
+ - deploy
+ - delete canary
+ - delete rollout
+ - persist_environment_url
+ environment:
+ name: production
+ url: http://$CI_PROJECT_PATH_SLUG.$AUTO_DEVOPS_DOMAIN
+ artifacts:
+ paths: [environment_url.txt]
production:
+ <<: *production_template
+ only:
+ refs:
+ - master
+ kubernetes: active
+ except:
+ variables:
+ - $STAGING_ENABLED
+ - $INCREMENTAL_ROLLOUT_ENABLED
+
+production_manual:
+ <<: *production_template
+ when: manual
+ allow_failure: false
+ only:
+ refs:
+ - master
+ kubernetes: active
+ variables:
+ - $STAGING_ENABLED
+ except:
+ variables:
+ - $INCREMENTAL_ROLLOUT_ENABLED
+
+# This job implements incremental rollout on for every push to `master`.
+
+.rollout: &rollout_template
stage: production
script:
- check_kube_domain
@@ -272,7 +317,8 @@ production:
- ensure_namespace
- install_tiller
- create_secret
- - deploy
+ - deploy rollout $ROLLOUT_PERCENTAGE
+ - scale stable $((100-ROLLOUT_PERCENTAGE))
- delete canary
- persist_environment_url
environment:
@@ -280,11 +326,53 @@ production:
url: http://$CI_PROJECT_PATH_SLUG.$AUTO_DEVOPS_DOMAIN
artifacts:
paths: [environment_url.txt]
-# when: manual
+
+rollout 10%:
+ <<: *rollout_template
+ variables:
+ ROLLOUT_PERCENTAGE: 10
+ when: manual
only:
refs:
- master
kubernetes: active
+ variables:
+ - $INCREMENTAL_ROLLOUT_ENABLED
+
+rollout 25%:
+ <<: *rollout_template
+ variables:
+ ROLLOUT_PERCENTAGE: 25
+ when: manual
+ only:
+ refs:
+ - master
+ kubernetes: active
+ variables:
+ - $INCREMENTAL_ROLLOUT_ENABLED
+
+rollout 50%:
+ <<: *rollout_template
+ variables:
+ ROLLOUT_PERCENTAGE: 50
+ when: manual
+ only:
+ refs:
+ - master
+ kubernetes: active
+ variables:
+ - $INCREMENTAL_ROLLOUT_ENABLED
+
+rollout 100%:
+ <<: *production_template
+ when: manual
+ allow_failure: false
+ only:
+ refs:
+ - master
+ kubernetes: active
+ variables:
+ - $INCREMENTAL_ROLLOUT_ENABLED
# ---------------------------------------------------------------------------
@@ -308,7 +396,7 @@ production:
fi
docker run -d --name db arminc/clair-db:latest
- docker run -p 6060:6060 --link db:postgres -d --name clair arminc/clair-local-scan:v2.0.1
+ docker run -p 6060:6060 --link db:postgres -d --name clair --restart on-failure arminc/clair-local-scan:v2.0.1
apk add -U wget ca-certificates
docker pull ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG}
wget https://github.com/arminc/clair-scanner/releases/download/v8/clair-scanner_linux_amd64
@@ -363,30 +451,19 @@ production:
esac
}
- function deploy() {
- track="${1-stable}"
- name="$CI_ENVIRONMENT_SLUG"
-
- if [[ "$track" != "stable" ]]; then
- name="$name-$track"
- fi
-
- replicas="1"
- service_enabled="false"
- postgres_enabled="$POSTGRES_ENABLED"
- # canary uses stable db
- [[ "$track" == "canary" ]] && postgres_enabled="false"
+ function get_replicas() {
+ track="${1:-stable}"
+ percentage="${2:-100}"
env_track=$( echo $track | tr -s '[:lower:]' '[:upper:]' )
env_slug=$( echo ${CI_ENVIRONMENT_SLUG//-/_} | tr -s '[:lower:]' '[:upper:]' )
- if [[ "$track" == "stable" ]]; then
+ if [[ "$track" == "stable" ]] || [[ "$track" == "rollout" ]]; then
# for stable track get number of replicas from `PRODUCTION_REPLICAS`
eval new_replicas=\$${env_slug}_REPLICAS
if [[ -z "$new_replicas" ]]; then
new_replicas=$REPLICAS
fi
- service_enabled="true"
else
# for all tracks get number of replicas from `CANARY_PRODUCTION_REPLICAS`
eval new_replicas=\$${env_track}_${env_slug}_REPLICAS
@@ -394,10 +471,37 @@ production:
eval new_replicas=\${env_track}_REPLICAS
fi
fi
- if [[ -n "$new_replicas" ]]; then
- replicas="$new_replicas"
+
+ replicas="${new_replicas:-1}"
+ replicas="$(($replicas * $percentage / 100))"
+
+ # always return at least one replicas
+ if [[ $replicas -gt 0 ]]; then
+ echo "$replicas"
+ else
+ echo 1
+ fi
+ }
+
+ function deploy() {
+ track="${1-stable}"
+ percentage="${2:-100}"
+ name="$CI_ENVIRONMENT_SLUG"
+
+ replicas="1"
+ service_enabled="true"
+ postgres_enabled="$POSTGRES_ENABLED"
+
+ # if track is different than stable,
+ # re-use all attached resources
+ if [[ "$track" != "stable" ]]; then
+ name="$name-$track"
+ service_enabled="false"
+ postgres_enabled="false"
fi
+ replicas=$(get_replicas "$track" "$percentage")
+
if [[ "$CI_PROJECT_VISIBILITY" != "public" ]]; then
secret_name='gitlab-registry'
else
@@ -427,6 +531,27 @@ production:
chart/
}
+ function scale() {
+ track="${1-stable}"
+ percentage="${2-100}"
+ name="$CI_ENVIRONMENT_SLUG"
+
+ if [[ "$track" != "stable" ]]; then
+ name="$name-$track"
+ fi
+
+ replicas=$(get_replicas "$track" "$percentage")
+
+ if [[ -n "$(helm ls -q "^$name$")" ]]; then
+ helm upgrade --reuse-values \
+ --wait \
+ --set replicaCount="$replicas" \
+ --namespace="$KUBE_NAMESPACE" \
+ "$name" \
+ chart/
+ fi
+ }
+
function install_dependencies() {
apk add -U openssl curl tar gzip bash ca-certificates git
wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://raw.githubusercontent.com/sgerrand/alpine-pkg-glibc/master/sgerrand.rsa.pub
@@ -548,8 +673,8 @@ production:
kubectl create secret -n "$KUBE_NAMESPACE" \
docker-registry gitlab-registry \
--docker-server="$CI_REGISTRY" \
- --docker-username="$CI_REGISTRY_USER" \
- --docker-password="$CI_REGISTRY_PASSWORD" \
+ --docker-username="${CI_DEPLOY_USER:-$CI_REGISTRY_USER}" \
+ --docker-password="${CI_DEPLOY_PASSWORD:-$CI_REGISTRY_PASSWORD}" \
--docker-email="$GITLAB_USER_EMAIL" \
-o yaml --dry-run | kubectl replace -n "$KUBE_NAMESPACE" --force -f -
}
diff --git a/vendor/gitlab-ci-yml/Chef.gitlab-ci.yml b/vendor/gitlab-ci-yml/Chef.gitlab-ci.yml
index ff7c87c29f0..4d5b6484d6e 100644
--- a/vendor/gitlab-ci-yml/Chef.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Chef.gitlab-ci.yml
@@ -7,7 +7,7 @@
image: "chef/chefdk"
services:
- - docker:stable-dind
+ - docker:dind
variables:
DOCKER_HOST: "tcp://docker:2375"
diff --git a/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml b/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml
index 58d48d1284b..eeefadaa019 100644
--- a/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml
@@ -2,7 +2,7 @@
image: docker:latest
services:
- - docker:stable-dind
+ - docker:dind
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
diff --git a/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml b/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml
index ba2efbd03a0..d5ee7ed2c13 100644
--- a/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml
@@ -24,7 +24,7 @@ variables:
MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end --show-version -DinstallAtEnd=true -DdeployAtEnd=true"
# Cache downloaded dependencies and plugins between builds.
-# To keep cache across branches add 'key: "$CI_JOB_REF_NAME"'
+# To keep cache across branches add 'key: "$CI_JOB_NAME"'
cache:
paths:
- .m2/repository
diff --git a/vendor/gitlab-ci-yml/Swift.gitlab-ci.yml b/vendor/gitlab-ci-yml/Swift.gitlab-ci.yml
index c9c35906d1c..10d0b05d9f8 100644
--- a/vendor/gitlab-ci-yml/Swift.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Swift.gitlab-ci.yml
@@ -1,5 +1,5 @@
# Lifted from: https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/
-# This file assumes an own GitLab CI runner, setup on an OS X system.
+# This file assumes an own GitLab CI runner, setup on an macOS system.
stages:
- build
- archive
@@ -8,11 +8,11 @@ build_project:
stage: build
script:
- xcodebuild clean -project ProjectName.xcodeproj -scheme SchemeName | xcpretty
- - xcodebuild test -project ProjectName.xcodeproj -scheme SchemeName -destination 'platform=iOS Simulator,name=iPhone 6s,OS=9.2' | xcpretty -s
+ - xcodebuild test -project ProjectName.xcodeproj -scheme SchemeName -destination 'platform=iOS Simulator,name=iPhone 8,OS=11.3' | xcpretty -s
tags:
- - ios_9-2
- - xcode_7-2
- - osx_10-11
+ - ios_11-3
+ - xcode_9-3
+ - macos_10-13
archive_project:
stage: archive
@@ -25,6 +25,6 @@ archive_project:
paths:
- build/ProjectName.ipa
tags:
- - ios_9-2
- - xcode_7-2
- - osx_10-11
+ - ios_11-3
+ - xcode_9-3
+ - macos_10-13
diff --git a/vendor/ingress/values.yaml b/vendor/ingress/values.yaml
index a6b499953bf..d0c1673cefc 100644
--- a/vendor/ingress/values.yaml
+++ b/vendor/ingress/values.yaml
@@ -7,3 +7,8 @@ controller:
podAnnotations:
prometheus.io/scrape: "true"
prometheus.io/port: "10254"
+
+rbac:
+ create: false
+ createRole: false
+ createClusterRole: false
diff --git a/vendor/licenses.csv b/vendor/licenses.csv
index ca88f867fe5..07861631a07 100644
--- a/vendor/licenses.csv
+++ b/vendor/licenses.csv
@@ -4,22 +4,19 @@
@babel/template,7.0.0-beta.32,MIT
@babel/traverse,7.0.0-beta.32,MIT
@babel/types,7.0.0-beta.32,MIT
-@gitlab-org/gitlab-svgs,1.17.0,SEE LICENSE IN LICENSE
+@gitlab-org/gitlab-svgs,1.20.0,SEE LICENSE IN LICENSE
+@mrmlnc/readdir-enhanced,2.2.1,MIT
+@sindresorhus/is,0.7.0,MIT
@types/jquery,2.0.48,MIT
-JSONStream,1.3.2,MIT
RedCloth,4.3.2,MIT
abbrev,1.0.9,ISC
abbrev,1.1.1,ISC
-accepts,1.3.3,MIT
accepts,1.3.4,MIT
ace-rails-ap,4.1.2,MIT
acorn,3.3.0,MIT
-acorn,4.0.13,MIT
-acorn,5.1.1,MIT
acorn,5.4.1,MIT
-acorn-dynamic-import,2.0.2,MIT
+acorn-dynamic-import,3.0.0,MIT
acorn-jsx,3.0.1,MIT
-acorn-node,1.3.0,Apache 2.0
actionmailer,4.2.10,MIT
actionpack,4.2.10,MIT
actionview,4.2.10,MIT
@@ -28,14 +25,12 @@ activemodel,4.2.10,MIT
activerecord,4.2.10,MIT
activesupport,4.2.10,MIT
acts-as-taggable-on,5.0.0,MIT
-address,1.0.3,MIT
addressable,2.5.2,Apache 2.0
addressparser,1.0.1,MIT
aes_key_wrap,1.0.1,MIT
after,0.8.2,MIT
agent-base,2.1.1,MIT
ajv,4.11.8,MIT
-ajv,5.2.2,MIT
ajv,5.5.2,MIT
ajv,6.1.1,MIT
ajv-keywords,1.5.1,MIT
@@ -52,8 +47,10 @@ ansi-escapes,3.0.0,MIT
ansi-html,0.0.7,Apache 2.0
ansi-regex,2.1.1,MIT
ansi-regex,3.0.0,MIT
+ansi-styles,1.0.0,MIT
ansi-styles,2.2.1,MIT
-ansi-styles,3.2.0,MIT
+ansi-styles,3.2.1,MIT
+any-observable,0.2.0,MIT
anymatch,1.3.2,ISC
anymatch,2.0.0,ISC
append-transform,0.4.0,MIT
@@ -65,14 +62,12 @@ arr-diff,2.0.0,MIT
arr-diff,4.0.0,MIT
arr-flatten,1.1.0,MIT
arr-union,3.1.0,MIT
-array-filter,0.0.1,MIT
+array-differ,1.0.0,MIT
array-find,1.0.0,MIT
array-find-index,1.0.2,MIT
array-flatten,1.1.1,MIT
array-flatten,2.1.1,MIT
array-includes,3.0.3,MIT
-array-map,0.0.0,MIT
-array-reduce,0.0.0,MIT
array-slice,0.2.3,MIT
array-union,1.0.2,MIT
array-uniq,1.0.3,MIT
@@ -88,11 +83,11 @@ asn1.js,4.10.1,MIT
assert,1.4.1,MIT
assert-plus,0.2.0,MIT
assert-plus,1.0.0,MIT
-asset_sync,2.2.0,MIT
+asset_sync,2.4.0,MIT
assign-symbols,1.0.0,MIT
+ast-types,0.10.1,MIT
ast-types,0.11.1,MIT
-astw,2.2.0,MIT
-async,0.9.2,MIT
+ast-types,0.11.3,MIT
async,1.5.2,MIT
async,2.1.5,MIT
async,2.4.1,MIT
@@ -102,7 +97,7 @@ async-limiter,1.0.0,MIT
asynckit,0.4.0,MIT
atob,2.0.3,(MIT OR Apache-2.0)
atomic,1.1.99,Apache 2.0
-attr_encrypted,3.0.3,MIT
+attr_encrypted,3.1.0,MIT
attr_required,1.0.0,MIT
autoprefixer,6.7.7,MIT
autoprefixer-rails,6.2.3,MIT
@@ -115,7 +110,7 @@ axios,0.15.3,MIT
axios,0.17.1,MIT
axios-mock-adapter,1.10.0,MIT
babel-code-frame,6.26.0,MIT
-babel-core,6.26.0,MIT
+babel-core,6.26.3,MIT
babel-eslint,8.0.2,MIT
babel-generator,6.26.0,MIT
babel-helper-bindify-decorators,6.24.1,MIT
@@ -132,20 +127,25 @@ babel-helper-regex,6.26.0,MIT
babel-helper-remap-async-to-generator,6.24.1,MIT
babel-helper-replace-supers,6.24.1,MIT
babel-helpers,6.24.1,MIT
-babel-loader,7.1.2,MIT
+babel-loader,7.1.4,MIT
babel-messages,6.23.0,MIT
babel-plugin-check-es2015-constants,6.22.0,MIT
-babel-plugin-istanbul,4.1.5,New BSD
+babel-plugin-istanbul,4.1.6,New BSD
+babel-plugin-rewire,1.1.0,ISC
babel-plugin-syntax-async-functions,6.13.0,MIT
babel-plugin-syntax-async-generators,6.13.0,MIT
+babel-plugin-syntax-class-constructor-call,6.18.0,MIT
babel-plugin-syntax-class-properties,6.13.0,MIT
babel-plugin-syntax-decorators,6.13.0,MIT
babel-plugin-syntax-dynamic-import,6.18.0,MIT
babel-plugin-syntax-exponentiation-operator,6.13.0,MIT
+babel-plugin-syntax-export-extensions,6.13.0,MIT
+babel-plugin-syntax-flow,6.18.0,MIT
babel-plugin-syntax-object-rest-spread,6.13.0,MIT
babel-plugin-syntax-trailing-function-commas,6.22.0,MIT
babel-plugin-transform-async-generator-functions,6.24.1,MIT
babel-plugin-transform-async-to-generator,6.24.1,MIT
+babel-plugin-transform-class-constructor-call,6.24.1,MIT
babel-plugin-transform-class-properties,6.24.1,MIT
babel-plugin-transform-decorators,6.24.1,MIT
babel-plugin-transform-define,1.3.0,MIT
@@ -172,6 +172,8 @@ babel-plugin-transform-es2015-template-literals,6.22.0,MIT
babel-plugin-transform-es2015-typeof-symbol,6.23.0,MIT
babel-plugin-transform-es2015-unicode-regex,6.24.1,MIT
babel-plugin-transform-exponentiation-operator,6.24.1,MIT
+babel-plugin-transform-export-extensions,6.22.0,MIT
+babel-plugin-transform-flow-strip-types,6.22.0,MIT
babel-plugin-transform-object-rest-spread,6.23.0,MIT
babel-plugin-transform-regenerator,6.26.0,MIT
babel-plugin-transform-strict-mode,6.24.1,MIT
@@ -179,6 +181,7 @@ babel-preset-es2015,6.24.1,MIT
babel-preset-es2016,6.24.1,MIT
babel-preset-es2017,6.24.1,MIT
babel-preset-latest,6.24.1,MIT
+babel-preset-stage-1,6.24.1,MIT
babel-preset-stage-2,6.24.1,MIT
babel-preset-stage-3,6.24.1,MIT
babel-register,6.26.0,MIT
@@ -189,6 +192,7 @@ babel-types,6.26.0,MIT
babosa,1.0.2,MIT
babylon,6.18.0,MIT
babylon,7.0.0-beta.32,MIT
+babylon,7.0.0-beta.44,MIT
backo2,1.0.2,MIT
balanced-match,0.4.2,MIT
balanced-match,1.0.0,MIT
@@ -206,13 +210,13 @@ better-assert,1.0.2,MIT
bfj-node4,5.2.1,MIT
big.js,3.1.3,MIT
binary-extensions,1.11.0,MIT
+binaryextensions,2.1.1,MIT
bindata,2.4.3,ruby
bitsyntax,0.0.4,UNKNOWN
bl,1.1.2,MIT
blackst0ne-mermaid,7.1.0-fixed,MIT
blob,0.0.4,MIT*
block-stream,0.0.9,ISC
-bluebird,3.5.0,MIT
bluebird,3.5.1,MIT
bn.js,4.11.8,MIT
body-parser,1.18.2,MIT
@@ -224,25 +228,19 @@ bootstrap-sass,3.3.6,MIT
bootstrap_form,2.7.0,MIT
boxen,1.3.0,MIT
brace-expansion,1.1.11,MIT
-brace-expansion,1.1.8,MIT
braces,0.1.5,MIT
braces,1.8.5,MIT
braces,2.3.1,MIT
brorand,1.1.0,MIT
browser,2.2.0,MIT
-browser-pack,6.0.4,MIT
-browser-resolve,1.11.2,MIT
-browserify,14.5.0,MIT
browserify-aes,1.1.1,MIT
browserify-cipher,1.0.0,MIT
browserify-des,1.0.0,MIT
browserify-rsa,4.0.1,MIT
browserify-sign,4.0.4,ISC
browserify-zlib,0.1.4,MIT
-browserify-zlib,0.2.0,MIT
browserslist,1.7.7,MIT
buffer,4.9.1,MIT
-buffer,5.1.0,MIT
buffer-indexof,1.1.0,MIT
buffer-more-ints,0.0.2,MIT
buffer-xor,1.0.3,MIT
@@ -254,13 +252,13 @@ bytes,2.5.0,MIT
bytes,3.0.0,MIT
cacache,10.0.4,ISC
cache-base,1.0.1,MIT
-cached-path-relative,1.0.1,MIT
+cacheable-request,2.1.4,MIT
+call-me-maybe,1.0.1,MIT
caller-path,0.1.0,MIT
callsite,1.0.0,MIT*
callsites,0.2.0,MIT
camelcase,1.2.1,MIT
camelcase,2.1.1,MIT
-camelcase,3.0.0,MIT
camelcase,4.1.0,MIT
camelcase-keys,2.1.0,MIT
caniuse-api,1.6.1,MIT
@@ -271,16 +269,19 @@ caseless,0.11.0,Apache 2.0
caseless,0.12.0,Apache 2.0
cause,0.1,MIT
center-align,0.1.3,MIT
+chalk,0.4.0,MIT
chalk,1.1.3,MIT
-chalk,2.3.0,MIT
-chalk,2.3.1,MIT
+chalk,2.4.0,MIT
+chalk,2.4.1,MIT
chardet,0.4.2,MIT
+charenc,0.0.2,New BSD
charlock_holmes,0.7.6,MIT
chart.js,1.0.2,MIT
check-types,7.3.0,MIT
chokidar,1.7.0,MIT
chokidar,2.0.2,MIT
chownr,1.0.1,ISC
+chrome-trace-event,0.1.2,MIT
chronic,0.10.2,MIT
chronic_duration,0.10.6,MIT
chunky_png,1.3.5,MIT
@@ -294,11 +295,21 @@ classlist-polyfill,1.2.0,Unlicense
cli-boxes,1.0.0,MIT
cli-cursor,1.0.2,MIT
cli-cursor,2.1.0,MIT
+cli-spinners,0.1.2,MIT
+cli-table,0.3.1,MIT
+cli-truncate,0.2.1,MIT
cli-width,2.1.0,ISC
clipboard,1.7.1,MIT
cliui,2.1.0,ISC
-cliui,3.2.0,ISC
+cliui,4.0.0,ISC
clone,1.0.2,MIT
+clone,1.0.3,MIT
+clone,2.1.1,MIT
+clone-buffer,1.0.0,MIT
+clone-response,1.0.2,MIT
+clone-stats,0.0.1,MIT
+clone-stats,1.0.0,MIT
+cloneable-readable,1.0.0,MIT
co,3.0.6,MIT
co,4.6.0,MIT
coa,1.0.1,MIT
@@ -310,11 +321,11 @@ color-convert,1.9.1,MIT
color-name,1.1.2,MIT
color-string,0.3.0,MIT
colormin,1.1.2,MIT
+colors,1.0.3,MIT
colors,1.1.2,MIT
combine-lists,1.0.1,MIT
-combine-source-map,0.7.2,MIT
-combine-source-map,0.8.0,MIT
combined-stream,1.0.6,MIT
+commander,2.13.0,MIT
commander,2.15.1,MIT
commondir,1.0.1,MIT
commonmarker,0.17.8,MIT
@@ -323,9 +334,8 @@ component-emitter,1.2.1,MIT
component-inherit,0.0.3,MIT*
compressible,2.0.11,MIT
compression,1.7.0,MIT
-compression-webpack-plugin,1.1.7,MIT
+compression-webpack-plugin,1.1.11,MIT
concat-map,0.0.1,MIT
-concat-stream,1.5.2,MIT
concat-stream,1.6.0,MIT
concurrent-ruby-ext,1.0.5,MIT
configstore,3.1.1,Simplified BSD
@@ -339,21 +349,18 @@ constants-browserify,1.0.0,MIT
contains-path,0.1.0,MIT
content-disposition,0.5.2,MIT
content-type,1.0.4,MIT
-convert-source-map,1.1.3,MIT
-convert-source-map,1.5.0,MIT
+convert-source-map,1.5.1,MIT
cookie,0.3.1,MIT
cookie-signature,1.0.6,MIT
copy-concurrently,1.0.5,ISC
copy-descriptor,0.1.1,MIT
-copy-webpack-plugin,4.4.1,MIT
+copy-webpack-plugin,4.5.1,MIT
core-js,2.3.0,MIT
-core-js,2.4.1,MIT
-core-js,2.5.1,MIT
core-js,2.5.3,MIT
core-util-is,1.0.2,MIT
-cosmiconfig,2.1.1,MIT
+cosmiconfig,2.2.2,MIT
crack,0.4.3,MIT
-crass,1.0.3,MIT
+crass,1.0.4,MIT
create-ecdh,4.0.0,MIT
create-error-class,3.0.2,MIT
create-hash,1.1.3,MIT
@@ -361,13 +368,14 @@ create-hmac,1.1.6,MIT
creole,0.5.0,ruby
cropper,2.3.0,MIT
cross-spawn,5.1.0,MIT
+cross-spawn,6.0.5,MIT
+crypt,0.0.2,New BSD
cryptiles,2.0.5,New BSD
cryptiles,3.1.2,New BSD
-crypto-browserify,3.11.0,MIT
crypto-browserify,3.12.0,MIT
crypto-random-string,1.0.0,MIT
css-color-names,0.0.4,MIT
-css-loader,0.28.9,MIT
+css-loader,0.28.11,MIT
css-selector-tokenizer,0.7.0,MIT
css_parser,1.5.0,MIT
cssesc,0.1.0,MIT
@@ -400,10 +408,13 @@ d3-transition,1.1.1,New BSD
d3_rails,3.5.11,MIT
dagre-d3-renderer,0.4.24,MIT
dagre-layout,0.8.0,MIT
+dargs,5.1.0,MIT
dashdash,1.14.1,MIT
data-uri-to-buffer,1.2.0,MIT
+date-fns,1.29.0,MIT
date-format,1.2.0,MIT
date-now,0.1.4,MIT
+dateformat,3.0.3,MIT
de-indent,1.0.2,MIT
debug,2.2.0,MIT
debug,2.6.8,MIT
@@ -418,6 +429,7 @@ decode-uri-component,0.2.0,MIT
decompress-response,3.3.0,MIT
deep-equal,1.0.1,MIT
deep-extend,0.4.2,MIT
+deep-extend,0.5.1,MIT
deep-is,0.1.3,MIT
default-require-extensions,1.0.0,MIT
default_value_for,3.0.2,MIT
@@ -433,19 +445,19 @@ delayed-stream,1.0.0,MIT
delegate,3.1.2,MIT
delegates,1.0.0,MIT
depd,1.1.1,MIT
-deps-sort,2.0.0,MIT
des.js,1.0.0,MIT
descendants_tracker,0.0.4,MIT
destroy,1.0.4,MIT
+detect-conflict,1.0.1,MIT
detect-indent,4.0.0,MIT
detect-libc,1.0.3,Apache 2.0
detect-node,2.0.3,ISC
-detect-port-alt,1.1.5,MIT
-detective,4.7.1,MIT
-devise,4.2.0,MIT
+device_detector,1.0.0,LGPL
+devise,4.4.3,MIT
devise-two-factor,3.0.0,MIT
di,0.0.1,MIT
diff,3.4.0,New BSD
+diff,3.5.0,New BSD
diff-lcs,1.3,"MIT,Artistic-2.0,GPL-2.0+"
diffie-hellman,5.0.2,MIT
diffy,3.1.0,MIT
@@ -464,38 +476,43 @@ domelementtype,1.1.3,Simplified BSD
domelementtype,1.3.0,Simplified BSD
domhandler,2.4.1,Simplified BSD
domutils,1.6.2,Simplified BSD
-doorkeeper,4.3.1,MIT
+doorkeeper,4.3.2,MIT
doorkeeper-openid_connect,1.3.0,MIT
dot-prop,4.2.0,MIT
double-ended-queue,2.1.0-0,MIT
dropzone,4.2.0,MIT
dropzonejs-rails,0.7.2,MIT
duplexer,0.1.1,MIT
-duplexer2,0.1.4,New BSD
duplexer3,0.1.4,New BSD
duplexify,3.5.3,MIT
ecc-jsbn,0.1.1,MIT
+editions,1.3.4,MIT
ee-first,1.1.1,MIT
ejs,2.5.7,Apache 2.0
+ejs,2.5.9,Apache 2.0
electron-to-chromium,1.3.3,ISC
+elegant-spinner,1.0.1,MIT
elliptic,6.4.0,MIT
email_reply_trimmer,0.1.6,MIT
emoji-unicode-version,0.2.1,MIT
emojis-list,2.1.0,MIT
encodeurl,1.0.2,MIT
encryptor,3.0.0,MIT
-end-of-stream,1.4.0,MIT
end-of-stream,1.4.1,MIT
engine.io,3.1.5,MIT
engine.io-client,3.1.5,MIT
engine.io-parser,2.1.2,MIT
enhanced-resolve,0.9.1,MIT
-enhanced-resolve,3.4.1,MIT
+enhanced-resolve,4.0.0,MIT
ent,2.2.0,MIT
entities,1.1.1,Simplified BSD
+envinfo,4.4.2,MIT
equalizer,0.0.11,MIT
errno,0.1.4,MIT
+errno,0.1.7,MIT
+error,7.0.2,MIT
error-ex,1.3.0,MIT
+error-ex,1.3.1,MIT
erubis,2.7.0,MIT
es-abstract,1.10.0,MIT
es-to-primitive,1.1.1,MIT
@@ -525,7 +542,6 @@ eslint-plugin-promise,3.5.0,ISC
eslint-plugin-vue,4.0.1,MIT
eslint-scope,3.7.1,Simplified BSD
eslint-visitor-keys,1.0.0,Apache 2.0
-espree,3.5.0,Simplified BSD
espree,3.5.2,Simplified BSD
esprima,2.7.3,Simplified BSD
esprima,3.1.3,Simplified BSD
@@ -545,7 +561,7 @@ eventemitter3,1.2.0,MIT
events,1.1.1,MIT
eventsource,0.1.6,MIT
evp_bytestokey,1.0.3,MIT
-excon,0.60.0,MIT
+excon,0.62.0,MIT
execa,0.7.0,MIT
execjs,2.6.0,MIT
exit-hook,1.1.1,MIT
@@ -562,14 +578,16 @@ extend,3.0.1,MIT
extend-shallow,2.0.1,MIT
extend-shallow,3.0.2,MIT
external-editor,2.1.0,MIT
+external-editor,2.2.0,MIT
extglob,0.3.2,MIT
extglob,2.0.4,MIT
extsprintf,1.3.0,MIT
extsprintf,1.4.0,MIT
faraday,0.12.2,MIT
-faraday_middleware,0.11.0.1,MIT
+faraday_middleware,0.12.2,MIT
faraday_middleware-multi_json,0.0.6,MIT
fast-deep-equal,1.0.0,MIT
+fast-glob,2.2.1,MIT
fast-json-stable-stringify,2.0.0,MIT
fast-levenshtein,2.0.6,MIT
fast_blank,1.0.0,MIT
@@ -581,11 +599,10 @@ ffi,1.9.18,New BSD
figures,1.7.0,MIT
figures,2.0.0,MIT
file-entry-cache,2.0.0,MIT
-file-loader,1.1.8,MIT
+file-loader,1.1.11,MIT
file-uri-to-path,1.0.0,MIT
filename-regex,2.0.1,MIT
fileset,2.0.3,MIT
-filesize,3.5.11,New BSD
filesize,3.6.0,New BSD
fill-range,2.2.3,MIT
fill-range,4.0.0,MIT
@@ -594,11 +611,13 @@ find-cache-dir,1.0.0,MIT
find-root,0.1.2,MIT
find-up,1.1.2,MIT
find-up,2.1.0,MIT
+first-chunk-stream,2.0.0,MIT
flat-cache,1.2.2,MIT
flatten,1.0.2,MIT
flipper,0.13.0,MIT
flipper-active_record,0.13.0,MIT
flipper-active_support_cache_store,0.13.0,MIT
+flow-parser,0.66.0,MIT
flowdock,0.7.1,MIT
flush-write-stream,1.0.2,MIT
fog-aliyun,0.2.0,MIT
@@ -649,19 +668,25 @@ get_process_mem,0.2.0,MIT
getpass,0.1.7,MIT
gettext_i18n_rails,1.8.0,MIT
gettext_i18n_rails_js,1.3.0,MIT
-gitaly-proto,0.94.0,MIT
+gh-got,6.0.0,MIT
+gitaly-proto,0.99.0,MIT
github-linguist,5.3.3,MIT
-github-markup,1.6.1,MIT
+github-markup,1.7.0,MIT
+github-username,4.1.0,MIT
gitlab-flowdock-git-hook,1.0.1,MIT
+gitlab-gollum-lib,4.2.7.2,MIT
+gitlab-gollum-rugged_adapter,0.4.4,MIT
gitlab-grit,2.8.2,MIT
gitlab-markup,1.6.3,MIT
gitlab_omniauth-ldap,2.0.4,MIT
glob,5.0.15,ISC
glob,7.1.1,ISC
glob,7.1.2,ISC
+glob-all,3.1.0,MIT
glob-base,0.3.0,MIT
glob-parent,2.0.0,ISC
glob-parent,3.1.0,ISC
+glob-to-regexp,0.3.0,BSD
global-dirs,0.1.1,MIT
global-modules,1.0.0,MIT
global-prefix,1.0.2,MIT
@@ -671,10 +696,8 @@ globals,9.18.0,MIT
globby,5.0.0,MIT
globby,6.1.0,MIT
globby,7.1.1,MIT
-goldiloader,2.0.1,MIT
+globby,8.0.1,MIT
gollum-grit_adapter,1.0.1,MIT
-gollum-lib,4.2.7,MIT
-gollum-rugged_adapter,0.4.4,MIT
gon,6.1.0,MIT
good-listener,1.2.2,MIT
google-api-client,0.19.8,Apache 2.0
@@ -683,15 +706,16 @@ googleapis-common-protos-types,1.0.1,Apache 2.0
googleauth,0.6.2,Apache 2.0
got,6.7.1,MIT
got,7.1.0,MIT
+got,8.3.0,MIT
gpgme,2.0.13,LGPL-2.1+
graceful-fs,4.1.11,ISC
grape,1.0.2,MIT
-grape-entity,0.6.0,MIT
+grape-entity,0.7.1,MIT
grape-route-helpers,2.1.0,MIT
grape_logging,1.7.0,MIT
graphlib,2.1.1,MIT
-grpc,1.10.0,Apache 2.0
-gzip-size,3.0.0,MIT
+grouped-queue,0.3.3,MIT
+grpc,1.11.0,Apache 2.0
gzip-size,4.1.0,MIT
hamlit,2.6.1,MIT
handle-thing,1.2.5,MIT
@@ -704,6 +728,7 @@ har-validator,5.0.3,ISC
has,1.0.1,MIT
has-ansi,2.0.0,MIT
has-binary2,1.0.2,MIT
+has-color,0.1.7,MIT
has-cors,1.1.0,MIT
has-flag,1.0.0,MIT
has-flag,2.0.0,MIT
@@ -739,16 +764,16 @@ html-entities,1.2.0,MIT
html-pipeline,2.7.1,MIT
html2text,0.2.0,MIT
htmlentities,4.3.4,MIT
-htmlescape,1.1.1,MIT
htmlparser2,3.9.2,MIT
http,2.2.2,MIT
+http-cache-semantics,3.8.1,Simplified BSD
http-cookie,1.0.3,MIT
http-deceiver,1.2.7,MIT
http-errors,1.6.2,MIT
http-form_data,1.0.3,MIT
http-proxy,1.16.2,MIT
http-proxy-agent,1.0.0,MIT
-http-proxy-middleware,0.17.4,MIT
+http-proxy-middleware,0.18.0,MIT
http-signature,1.1.1,MIT
http-signature,1.2.0,MIT
http_parser.rb,0.6.0,MIT
@@ -757,7 +782,6 @@ httpclient,2.8.3,ruby
httpntlm,1.6.1,MIT
httpreq,0.4.24,MIT
https-browserify,0.0.1,MIT
-https-browserify,1.0.0,MIT
https-proxy-agent,1.0.0,MIT
i18n,0.9.5,MIT
ice_nine,0.11.2,MIT
@@ -767,7 +791,6 @@ icss-replace-symbols,1.1.0,ISC
icss-utils,2.1.0,ISC
ieee754,1.1.8,New BSD
iferr,0.1.5,MIT
-ignore,3.3.3,MIT
ignore,3.3.7,MIT
ignore-by-default,1.0.1,ISC
immediate,3.0.6,MIT
@@ -776,6 +799,7 @@ import-local,1.0.0,MIT
imports-loader,0.8.0,MIT
imurmurhash,0.1.4,MIT
indent-string,2.1.0,MIT
+indent-string,3.2.0,MIT
indexes-of,1.0.1,MIT
indexof,0.0.1,MIT*
inflection,1.10.0,MIT
@@ -785,12 +809,13 @@ influxdb,0.2.3,MIT
inherits,2.0.1,ISC
inherits,2.0.3,ISC
ini,1.3.5,ISC
-inline-source-map,0.6.2,MIT
inquirer,0.12.0,MIT
inquirer,3.3.0,MIT
-insert-module-globals,7.0.1,MIT
+inquirer,5.2.0,MIT
internal-ip,1.2.0,MIT
interpret,1.0.1,MIT
+interpret,1.1.0,MIT
+into-stream,3.1.0,MIT
invariant,2.2.2,New BSD
invert-kv,1.0.0,MIT
ip,1.0.1,MIT
@@ -803,7 +828,6 @@ is-accessor-descriptor,0.1.6,MIT
is-accessor-descriptor,1.0.0,MIT
is-arrayish,0.2.1,MIT
is-binary-path,1.0.1,MIT
-is-buffer,1.1.5,MIT
is-buffer,1.1.6,MIT
is-builtin-module,1.0.0,MIT
is-callable,1.1.3,MIT
@@ -812,6 +836,7 @@ is-data-descriptor,1.0.0,MIT
is-date-object,1.0.1,MIT
is-descriptor,0.1.6,MIT
is-descriptor,1.0.2,MIT
+is-directory,0.3.1,MIT
is-dotfile,1.0.3,MIT
is-equal-shallow,0.1.3,MIT
is-extendable,0.1.1,MIT
@@ -826,7 +851,6 @@ is-glob,3.1.0,MIT
is-glob,4.0.0,MIT
is-installed-globally,0.1.0,MIT
is-my-ip-valid,1.0.0,MIT
-is-my-json-valid,2.16.0,MIT
is-my-json-valid,2.17.2,MIT
is-npm,1.0.0,MIT
is-number,0.1.1,MIT
@@ -835,6 +859,7 @@ is-number,3.0.0,MIT
is-number,4.0.0,MIT
is-obj,1.0.1,MIT
is-object,1.0.1,MIT
+is-observable,0.2.0,MIT
is-odd,2.0.0,MIT
is-path-cwd,1.0.0,MIT
is-path-in-cwd,1.0.0,MIT
@@ -850,7 +875,7 @@ is-regex,1.0.4,MIT
is-relative,0.2.1,MIT
is-resolvable,1.0.0,MIT
is-retry-allowed,1.1.0,MIT
-is-root,1.0.0,MIT
+is-scoped,1.0.0,MIT
is-stream,1.1.0,MIT
is-svg,2.1.0,MIT
is-symbol,1.0.1,MIT
@@ -864,7 +889,6 @@ isarray,0.0.1,MIT
isarray,1.0.0,MIT
isarray,2.0.1,MIT
isbinaryfile,3.0.2,MIT
-isexe,1.1.2,ISC
isexe,2.0.0,ISC
isobject,2.1.0,MIT
isobject,3.0.1,MIT
@@ -872,11 +896,14 @@ isstream,0.1.2,MIT
istanbul,0.4.5,New BSD
istanbul-api,1.2.1,New BSD
istanbul-lib-coverage,1.1.1,New BSD
+istanbul-lib-coverage,1.2.0,New BSD
istanbul-lib-hook,1.1.0,New BSD
+istanbul-lib-instrument,1.10.1,New BSD
istanbul-lib-instrument,1.9.1,New BSD
istanbul-lib-report,1.1.2,New BSD
istanbul-lib-source-maps,1.2.2,New BSD
istanbul-reports,1.1.3,New BSD
+istextorbinary,2.2.1,MIT
isurl,1.0.0,MIT
jasmine-core,2.9.0,MIT
jasmine-jquery,2.1.1,MIT
@@ -889,23 +916,25 @@ jquery.waitforimages,2.2.0,MIT
js-base64,2.1.9,New BSD
js-cookie,2.1.3,MIT
js-tokens,3.0.2,MIT
+js-yaml,3.11.0,MIT
js-yaml,3.7.0,MIT
js-yaml,3.9.1,MIT
jsbn,0.1.1,MIT
+jscodeshift,0.4.1,New BSD
+jscodeshift,0.5.0,New BSD
jsesc,0.5.0,MIT
jsesc,1.3.0,MIT
json,1.8.6,ruby
+json-buffer,3.0.0,MIT
json-jwt,1.9.2,MIT
-json-loader,0.5.7,MIT
+json-parse-better-errors,1.0.2,MIT
json-schema,0.2.3,BSD
json-schema-traverse,0.3.1,MIT
-json-stable-stringify,0.0.1,MIT
json-stable-stringify,1.0.1,MIT
json-stringify-safe,5.0.1,ISC
json3,3.3.2,MIT
json5,0.5.1,MIT
jsonify,0.0.0,Public Domain
-jsonparse,1.3.1,MIT
jsonpointer,4.0.1,MIT
jsprim,1.4.1,MIT
jszip,3.1.3,(MIT OR GPL-3.0)
@@ -915,14 +944,15 @@ kaminari,1.0.1,MIT
kaminari-actionview,1.0.1,MIT
kaminari-activerecord,1.0.1,MIT
kaminari-core,1.0.1,MIT
-karma,2.0.0,MIT
+karma,2.0.2,MIT
karma-chrome-launcher,2.2.0,MIT
-karma-coverage-istanbul-reporter,1.4.1,MIT
+karma-coverage-istanbul-reporter,1.4.2,MIT
karma-jasmine,1.1.1,MIT
karma-mocha-reporter,2.2.5,MIT
karma-sourcemap-loader,0.3.7,MIT
-karma-webpack,2.0.7,MIT
+karma-webpack,3.0.0,MIT
katex,0.8.3,MIT
+keyv,3.0.0,MIT
kgio,2.10.0,LGPL-2.1+
killable,1.0.0,ISC
kind-of,3.2.2,MIT
@@ -930,27 +960,28 @@ kind-of,4.0.0,MIT
kind-of,5.1.0,MIT
kind-of,6.0.2,MIT
kubeclient,3.0.0,MIT
-labeled-stream-splicer,2.0.0,MIT
latest-version,3.1.0,MIT
lazy-cache,1.0.4,MIT
lazy-cache,2.0.2,MIT
lcid,1.0.0,MIT
levn,0.3.0,MIT
-lexical-scope,1.2.0,MIT
libbase64,0.1.0,MIT
libmime,3.0.0,MIT
libqp,1.1.0,MIT
licensee,8.9.2,MIT
lie,3.1.1,MIT
+listr,0.13.0,MIT
+listr-silent-renderer,1.1.1,MIT
+listr-update-renderer,0.4.0,MIT
+listr-verbose-renderer,0.4.1,MIT
little-plugger,1.1.4,MIT
load-json-file,1.1.0,MIT
-load-json-file,2.0.0,MIT
+load-json-file,4.0.0,MIT
loader-runner,2.3.0,MIT
-loader-utils,0.2.16,MIT
loader-utils,1.1.0,MIT
locale,2.1.2,"ruby,LGPLv3+"
locate-path,2.0.0,MIT
-lodash,3.10.1,MIT
+lodash,4.17.10,MIT
lodash,4.17.4,MIT
lodash,4.17.5,MIT
lodash._baseget,3.7.2,MIT
@@ -968,24 +999,28 @@ lodash.isarray,3.0.4,MIT
lodash.isfunction,3.0.9,MIT
lodash.isstring,4.0.1,MIT
lodash.kebabcase,4.0.1,MIT
-lodash.memoize,3.0.4,MIT
lodash.memoize,4.1.2,MIT
lodash.mergewith,4.6.0,MIT
lodash.snakecase,4.0.1,MIT
lodash.startswith,4.2.1,MIT
lodash.uniq,4.5.0,MIT
lodash.words,4.2.0,MIT
+log-symbols,1.0.2,MIT
log-symbols,2.1.0,MIT
+log-symbols,2.2.0,MIT
+log-update,1.0.2,MIT
log4js,2.5.3,Apache 2.0
logging,2.2.2,MIT
loggly,1.1.1,MIT
loglevel,1.4.1,MIT
-lograge,0.5.1,MIT
+loglevelnext,1.0.3,MIT
+lograge,0.10.0,MIT
longest,1.0.1,MIT
loofah,2.2.2,MIT
loose-envify,1.3.1,MIT
loud-rejection,1.6.0,MIT
lowercase-keys,1.0.0,MIT
+lru-cache,2.2.4,MIT
lru-cache,2.6.5,ISC
lru-cache,4.1.1,ISC
macaddress,0.2.8,MIT
@@ -994,6 +1029,7 @@ mail_room,0.9.1,MIT
mailcomposer,4.0.1,MIT
mailgun-js,0.7.15,MIT
make-dir,1.0.0,MIT
+make-dir,1.2.0,MIT
map-cache,0.2.2,MIT
map-obj,1.0.1,MIT
map-stream,0.1.0,UNKNOWN
@@ -1004,19 +1040,25 @@ math-expression-evaluator,1.2.16,MIT
md5.js,1.3.4,MIT
media-typer,0.3.0,MIT
mem,1.1.0,MIT
+mem-fs,1.1.3,MIT
+mem-fs-editor,4.0.1,MIT
memoist,0.16.0,MIT
memory-fs,0.2.0,MIT
memory-fs,0.4.1,MIT
meow,3.7.0,MIT
merge-descriptors,1.0.1,MIT
+merge2,1.2.2,MIT
method_source,0.8.2,MIT
methods,1.1.2,MIT
micromatch,2.3.11,MIT
+micromatch,3.1.10,MIT
micromatch,3.1.6,MIT
+micromatch,3.1.9,MIT
miller-rabin,4.0.1,MIT
mime,1.4.1,MIT
mime,1.6.0,MIT
-mime-db,1.29.0,MIT
+mime,2.2.0,MIT
+mime,2.3.1,MIT
mime-db,1.33.0,MIT
mime-types,2.1.18,MIT
mime-types,3.1,MIT
@@ -1028,15 +1070,14 @@ mini_mime,1.0.0,MIT
mini_portile2,2.3.0,MIT
minimalistic-assert,1.0.0,ISC
minimalistic-crypto-utils,1.0.1,MIT
-minimatch,3.0.3,ISC
minimatch,3.0.4,ISC
minimist,0.0.10,MIT
minimist,0.0.8,MIT
+minimist,0.1.0,MIT
minimist,1.2.0,MIT
mississippi,2.0.0,Simplified BSD
mixin-deep,1.3.1,MIT
mkdirp,0.5.1,MIT
-module-deps,4.1.1,MIT
moment,2.19.2,MIT
monaco-editor,0.10.0,MIT
mousetrap,1.4.6,Apache 2.0
@@ -1048,21 +1089,24 @@ multi_json,1.13.1,MIT
multi_xml,0.6.0,MIT
multicast-dns,6.1.1,MIT
multicast-dns-service-types,1.1.0,MIT
+multimatch,2.1.0,MIT
multipart-post,2.0.0,MIT
mustermann,1.0.2,MIT
mustermann-grape,1.0.0,MIT
mute-stream,0.0.5,ISC
mute-stream,0.0.7,ISC
mysql2,0.4.10,MIT
-name-all-modules-plugin,1.0.1,MIT
nan,2.8.0,MIT
nanomatch,1.2.9,MIT
natural-compare,1.4.0,MIT
negotiator,0.6.1,MIT
+neo-async,2.5.0,MIT
net-ldap,0.16.0,MIT
net-ssh,4.2.0,MIT
netmask,1.0.6,MIT
netrc,0.11.0,MIT
+nice-try,1.0.4,MIT
+node-dir,0.1.8,MIT
node-forge,0.6.33,New BSD
node-libs-browser,1.1.1,MIT
node-libs-browser,2.0.0,MIT
@@ -1075,8 +1119,9 @@ nodemailer-shared,1.1.0,MIT
nodemailer-smtp-pool,2.8.2,MIT
nodemailer-smtp-transport,2.7.2,MIT
nodemailer-wellknown,0.1.10,MIT
-nodemon,1.15.1,MIT
+nodemon,1.17.3,MIT
nokogiri,1.8.2,MIT
+nomnom,1.8.1,MIT
nopt,1.0.10,MIT
nopt,3.0.6,ISC
nopt,4.0.1,ISC
@@ -1084,6 +1129,7 @@ normalize-package-data,2.4.0,Simplified BSD
normalize-path,2.1.1,MIT
normalize-range,0.1.2,MIT
normalize-url,1.9.1,MIT
+normalize-url,2.0.1,MIT
npm-run-path,2.0.2,MIT
npmlog,4.1.2,ISC
null-check,1.0.0,MIT
@@ -1108,10 +1154,9 @@ omniauth-authentiq,0.3.1,MIT
omniauth-azure-oauth2,0.0.9,MIT
omniauth-cas3,1.1.4,MIT
omniauth-facebook,4.0.0,MIT
-omniauth-github,1.1.2,MIT
+omniauth-github,1.3.0,MIT
omniauth-gitlab,1.0.2,MIT
omniauth-google-oauth2,0.5.3,MIT
-omniauth-jwt,0.0.2,MIT
omniauth-kerberos,0.3.0,MIT
omniauth-multipassword,0.4.2,MIT
omniauth-oauth,1.1.0,MIT
@@ -1130,36 +1175,39 @@ opener,1.4.3,(WTFPL OR MIT)
opn,5.2.0,MIT
optimist,0.6.1,MIT
optionator,0.8.2,MIT
+ora,0.2.3,MIT
org-ruby,0.9.12,MIT
original,1.0.0,MIT
orm_adapter,0.5.0,MIT
os,0.9.6,MIT
os-browserify,0.2.1,MIT
-os-browserify,0.3.0,MIT
os-homedir,1.0.2,MIT
-os-locale,1.4.0,MIT
os-locale,2.1.0,MIT
os-tmpdir,1.0.2,MIT
osenv,0.1.5,ISC
p-cancelable,0.3.0,MIT
+p-cancelable,0.4.1,MIT
+p-each-series,1.0.0,MIT
p-finally,1.0.0,MIT
-p-limit,1.1.0,MIT
+p-is-promise,1.1.0,MIT
+p-lazy,1.0.0,MIT
p-limit,1.2.0,MIT
p-locate,2.0.0,MIT
p-map,1.1.1,MIT
-p-timeout,1.2.0,MIT
+p-reduce,1.0.0,MIT
+p-timeout,1.2.1,MIT
+p-timeout,2.0.1,MIT
p-try,1.0.0,MIT
pac-proxy-agent,1.1.0,MIT
pac-resolver,2.0.0,MIT
package-json,4.0.1,MIT
pako,0.2.9,MIT
-pako,1.0.5,(MIT AND Zlib)
pako,1.0.6,(MIT AND Zlib)
parallel-transform,1.1.0,MIT
-parents,1.0.1,MIT
parse-asn1,5.1.0,ISC
parse-glob,3.0.4,MIT
parse-json,2.2.0,MIT
+parse-json,4.0.0,MIT
parse-passwd,1.0.0,MIT
parseqs,0.0.5,MIT
parseuri,0.0.5,MIT
@@ -1173,18 +1221,15 @@ path-is-absolute,1.0.1,MIT
path-is-inside,1.0.2,(WTFPL OR MIT)
path-key,2.0.1,MIT
path-parse,1.0.5,MIT
-path-platform,0.11.15,MIT
path-proxy,1.0.0,MIT
path-to-regexp,0.1.7,MIT
path-type,1.1.0,MIT
-path-type,2.0.0,MIT
path-type,3.0.0,MIT
pause-stream,0.0.11,Apache 2.0
pbkdf2,3.0.14,MIT
peek,1.0.1,MIT
peek-gc,0.0.2,MIT
peek-mysql2,1.1.0,MIT
-peek-performance_bar,1.3.1,MIT
peek-pg,1.3.0,MIT
peek-rblineprof,0.2.0,MIT
peek-redis,1.2.0,MIT
@@ -1206,9 +1251,8 @@ portfinder,1.0.13,MIT
posix-character-classes,0.1.1,MIT
posix-spawn,0.3.13,MIT
postcss,5.2.16,MIT
-postcss,6.0.14,MIT
-postcss,6.0.15,MIT
postcss,6.0.19,MIT
+postcss,6.0.21,MIT
postcss-calc,5.3.1,MIT
postcss-colormin,2.2.2,MIT
postcss-convert-values,2.6.1,MIT
@@ -1248,13 +1292,15 @@ prelude-ls,1.1.2,MIT
premailer,1.10.4,New BSD
premailer-rails,1.9.7,MIT
prepend-http,1.0.4,MIT
+prepend-http,2.0.0,MIT
preserve,0.2.0,MIT
+prettier,1.10.2,MIT
prettier,1.11.1,MIT
prettier,1.8.2,MIT
+pretty-bytes,4.0.2,MIT
prismjs,1.6.0,MIT
private,0.1.8,MIT
process,0.11.10,MIT
-process,0.11.9,MIT
process-nextick-args,1.0.7,MIT
process-nextick-args,2.0.0,MIT
progress,1.1.8,MIT
@@ -1263,6 +1309,7 @@ promise-inflight,1.0.1,ISC
proxy-addr,2.0.3,MIT
proxy-agent,2.0.0,MIT
prr,0.0.0,MIT
+prr,1.0.1,MIT
ps-tree,1.1.0,MIT
pseudomap,1.0.2,ISC
pstree.remy,1.1.0,MIT
@@ -1280,11 +1327,12 @@ qs,6.2.3,New BSD
qs,6.4.0,New BSD
qs,6.5.1,New BSD
query-string,4.3.2,MIT
+query-string,5.1.1,MIT
querystring,0.2.0,MIT
querystring-es3,0.2.1,MIT
querystringify,0.0.4,MIT
querystringify,1.0.0,MIT
-rack,1.6.9,MIT
+rack,1.6.10,MIT
rack-accept,0.4.5,MIT
rack-attack,4.4.1,MIT
rack-cors,1.0.2,MIT
@@ -1300,7 +1348,7 @@ rails-i18n,4.0.9,MIT
railties,4.2.10,MIT
rainbow,2.2.2,MIT
raindrops,0.18.0,LGPL-2.1+
-rake,12.3.0,MIT
+rake,12.3.1,MIT
randomatic,1.1.7,MIT
randombytes,2.0.6,MIT
randomfill,1.0.4,MIT
@@ -1313,27 +1361,24 @@ rb-fsevent,0.10.2,MIT
rb-inotify,0.9.10,MIT
rbnacl,4.0.2,MIT
rbnacl-libsodium,1.0.11,MIT
-rc,1.2.1,(BSD-2-Clause OR MIT OR Apache-2.0)
rc,1.2.5,(BSD-2-Clause OR MIT OR Apache-2.0)
rdoc,4.2.2,ruby
re2,1.1.1,New BSD
-react-dev-utils,5.0.0,MIT
-react-error-overlay,4.0.0,MIT
-read-only-stream,2.0.0,MIT
+read-chunk,2.1.0,MIT
read-pkg,1.1.0,MIT
-read-pkg,2.0.0,MIT
+read-pkg,3.0.0,MIT
read-pkg-up,1.0.1,MIT
-read-pkg-up,2.0.0,MIT
+read-pkg-up,3.0.0,MIT
readable-stream,1.1.14,MIT
readable-stream,2.0.6,MIT
-readable-stream,2.3.3,MIT
readable-stream,2.3.4,MIT
readdirp,2.1.0,MIT
readline2,1.0.1,MIT
recaptcha,3.0.0,MIT
+recast,0.12.9,MIT
+recast,0.14.7,MIT
rechoir,0.6.2,MIT
recursive-open-struct,1.0.5,MIT
-recursive-readdir,2.2.1,MIT
redcarpet,3.4.0,MIT
redent,1.0.0,MIT
redis,2.8.0,MIT
@@ -1364,6 +1409,8 @@ repeat-element,1.1.2,MIT
repeat-string,0.2.2,MIT
repeat-string,1.6.1,MIT
repeating,2.0.1,MIT
+replace-ext,0.0.1,MIT
+replace-ext,1.0.0,MIT
representable,3.0.4,MIT
request,2.75.0,Apache 2.0
request,2.81.0,Apache 2.0
@@ -1378,24 +1425,27 @@ require-uncached,1.0.3,MIT
requires-port,1.0.0,MIT
resolve,1.1.7,MIT
resolve,1.5.0,MIT
+resolve,1.7.1,MIT
resolve-cwd,2.0.0,MIT
resolve-dir,1.0.1,MIT
resolve-from,1.0.1,MIT
resolve-from,3.0.0,MIT
resolve-url,0.2.1,MIT
-responders,2.3.0,MIT
+responders,2.4.0,MIT
+responselike,1.0.2,MIT
rest-client,2.0.2,MIT
restore-cursor,1.0.1,MIT
restore-cursor,2.0.0,MIT
ret,0.1.15,MIT
retriable,3.1.1,MIT
right-align,0.1.3,MIT
+rimraf,2.2.8,MIT
rimraf,2.6.1,ISC
rimraf,2.6.2,ISC
rinku,2.0.0,ISC
ripemd160,2.0.1,MIT
rotp,2.1.2,MIT
-rouge,2.2.1,MIT
+rouge,3.1.1,MIT
rqrcode,0.7.0,MIT
rqrcode-rails3,0.1.7,MIT
ruby-enum,0.7.2,MIT
@@ -1413,6 +1463,7 @@ run-queue,1.0.3,ISC
rx-lite,3.1.2,Apache 2.0
rx-lite,4.0.8,Apache 2.0
rx-lite-aggregates,4.0.8,Apache 2.0
+rxjs,5.5.10,Apache 2.0
safe-buffer,5.1.1,MIT
safe-regex,1.1.0,MIT
safe_yaml,1.0.4,MIT
@@ -1423,8 +1474,8 @@ sass-listen,4.0.0,MIT
sass-rails,5.0.6,MIT
sawyer,0.8.1,MIT
sax,1.2.2,ISC
-schema-utils,0.3.0,MIT
schema-utils,0.4.5,MIT
+scoped-regex,1.0.0,MIT
securecompare,1.0.0,MIT
seed-fu,2.3.7,MIT
select,1.1.2,MIT
@@ -1452,11 +1503,11 @@ setprototypeof,1.1.0,ISC
settingslogic,2.0.9,MIT
sexp_processor,4.9.0,MIT
sha.js,2.4.10,MIT
-shasum,1.0.2,MIT
+sha1,1.1.1,New BSD
shebang-command,1.2.0,MIT
shebang-regex,1.0.0,MIT
-shell-quote,1.6.1,MIT
shelljs,0.7.8,New BSD
+shelljs,0.8.1,New BSD
sidekiq,5.0.5,LGPL
sidekiq-cron,0.6.0,MIT
sidekiq-limit_fetch,3.4.0,MIT
@@ -1466,6 +1517,7 @@ slack-node,0.2.0,MIT
slack-notifier,1.5.1,MIT
slash,1.0.0,MIT
slice-ansi,0.0.4,MIT
+slide,1.1.6,ISC
smart-buffer,1.1.15,MIT
smtp-connection,2.12.0,MIT
snapdragon,0.8.1,MIT
@@ -1483,6 +1535,7 @@ socks,1.1.10,MIT
socks,1.1.9,MIT
socks-proxy-agent,2.1.1,MIT
sort-keys,1.1.2,MIT
+sort-keys,2.0.0,MIT
source-list-map,2.0.0,MIT
source-map,0.2.0,New BSD
source-map,0.4.4,New BSD
@@ -1516,50 +1569,51 @@ statuses,1.3.1,MIT
statuses,1.4.0,MIT
stream-browserify,2.0.1,MIT
stream-combiner,0.0.4,MIT
-stream-combiner2,1.1.1,MIT
stream-each,1.2.2,MIT
-stream-http,2.6.3,MIT
stream-http,2.8.0,MIT
stream-shift,1.0.0,MIT
-stream-splicer,2.0.0,MIT
+stream-to-observable,0.2.0,MIT
streamroller,0.7.0,MIT
strict-uri-encode,1.1.0,MIT
+string-template,0.2.1,MIT
string-width,1.0.2,MIT
-string-width,2.0.0,MIT
string-width,2.1.1,MIT
string_decoder,0.10.31,MIT
string_decoder,1.0.3,MIT
-stringex,2.7.1,MIT
+stringex,2.8.4,MIT
stringstream,0.0.5,MIT
+strip-ansi,0.1.1,MIT
strip-ansi,3.0.1,MIT
strip-ansi,4.0.0,MIT
strip-bom,2.0.0,MIT
strip-bom,3.0.0,MIT
+strip-bom-stream,2.0.0,MIT
strip-eof,1.0.0,MIT
strip-indent,1.0.1,MIT
strip-json-comments,2.0.1,MIT
-style-loader,0.20.2,MIT
-subarg,1.0.0,MIT
+style-loader,0.21.0,MIT
supports-color,2.0.0,MIT
supports-color,3.2.3,MIT
-supports-color,4.2.1,MIT
-supports-color,4.5.0,MIT
supports-color,5.1.0,MIT
supports-color,5.2.0,MIT
+supports-color,5.4.0,MIT
svg4everybody,2.1.9,CC0-1.0
svgo,0.7.2,MIT
-syntax-error,1.4.0,MIT
+symbol-observable,0.2.4,MIT
+symbol-observable,1.0.1,MIT
sys-filesystem,1.1.6,Artistic 2.0
table,3.8.3,New BSD
tapable,0.1.10,MIT
-tapable,0.2.8,MIT
+tapable,1.0.0,MIT
tar,2.2.1,ISC
tar-pack,3.4.1,Simplified BSD
+temp,0.8.3,MIT
temple,0.7.7,MIT
term-size,1.2.0,MIT
-test-exclude,4.1.1,ISC
+test-exclude,4.2.1,ISC
text,1.3.1,MIT
text-table,0.2.0,MIT
+textextensions,2.2.0,MIT
thor,0.19.4,MIT
thread_safe,0.3.6,Apache 2.0
three,0.84.0,MIT
@@ -1570,7 +1624,6 @@ through2,2.0.3,MIT
thunkify,2.1.2,MIT
thunky,0.1.0,MIT*
tilt,2.0.6,MIT
-time-stamp,2.0.0,MIT
timeago.js,3.0.2,MIT
timed-out,4.0.1,MIT
timers-browserify,1.4.2,MIT
@@ -1585,6 +1638,7 @@ to-fast-properties,1.0.3,MIT
to-fast-properties,2.0.0,MIT
to-object-path,0.3.0,MIT
to-regex,3.0.1,MIT
+to-regex,3.0.2,MIT
to-regex-range,2.1.1,MIT
toml-rb,1.0.0,MIT
touch,3.1.0,ISC
@@ -1597,7 +1651,6 @@ tryer,1.0.0,MIT
tryit,1.0.3,MIT
tsscmp,1.0.5,MIT
tty-browserify,0.0.0,MIT
-tty-browserify,0.0.1,MIT
tunnel-agent,0.4.3,Apache 2.0
tunnel-agent,0.6.0,Apache 2.0
tweetnacl,0.14.5,Unlicense
@@ -1608,16 +1661,17 @@ tzinfo,1.2.5,MIT
u2f,0.2.1,MIT
uber,0.1.0,MIT
uglifier,2.7.2,MIT
+uglify-es,3.3.9,Simplified BSD
uglify-js,2.8.29,Simplified BSD
uglify-to-browserify,1.0.2,MIT
-uglifyjs-webpack-plugin,0.4.6,MIT
+uglifyjs-webpack-plugin,1.2.5,MIT
uid-number,0.0.6,ISC
ultron,1.1.1,MIT
-umd,3.0.1,MIT
unc-path-regex,0.1.2,MIT
undefsafe,2.0.2,MIT
+underscore,1.6.0,MIT
underscore,1.7.0,MIT
-underscore,1.8.3,MIT
+underscore,1.9.0,MIT
unf,0.1.4,BSD
unf_ext,0.0.7.5,MIT
unicorn,5.1.0,ruby
@@ -1631,25 +1685,30 @@ unique-slug,2.0.0,ISC
unique-string,1.0.0,MIT
unpipe,1.0.0,MIT
unset-value,1.0.0,MIT
+untildify,3.0.2,MIT
unzip-response,2.0.1,MIT
upath,1.0.2,MIT
update-notifier,2.3.0,Simplified BSD
urix,0.1.0,MIT
url,0.11.0,MIT
-url-loader,0.6.2,MIT
+url-join,2.0.5,MIT
+url-join,4.0.0,MIT
+url-loader,1.0.1,MIT
url-parse,1.0.5,MIT
url-parse,1.1.9,MIT
url-parse-lax,1.0.0,MIT
+url-parse-lax,3.0.0,MIT
url-to-options,1.0.1,MIT
url_safe_base64,0.2.2,MIT
use,2.0.2,MIT
user-home,2.0.0,MIT
-useragent,2.3.0,MIT
+useragent,2.2.1,MIT
util,0.10.3,MIT
util-deprecate,1.0.2,MIT
utils-merge,1.0.1,MIT
uuid,3.2.1,MIT
uws,9.14.0,Zlib
+v8-compile-cache,1.1.2,MIT
validate-npm-package-license,3.0.1,Apache 2.0
validates_hostname,1.0.6,MIT
vary,1.1.1,MIT
@@ -1657,38 +1716,46 @@ vary,1.1.2,MIT
vendors,1.0.1,MIT
verror,1.10.0,MIT
version_sorter,2.1.0,MIT
+vinyl,1.2.0,MIT
+vinyl,2.1.0,MIT
+vinyl-file,2.0.0,MIT
virtus,1.0.5,MIT
visibilityjs,1.2.4,MIT
vm-browserify,0.0.4,MIT
vmstat,2.3.0,MIT
void-elements,2.0.1,MIT
-vue,2.5.13,MIT
+vue,2.5.16,MIT
vue-eslint-parser,2.0.1,MIT
-vue-hot-reload-api,2.2.4,MIT
-vue-loader,14.1.1,MIT
-vue-resource,1.3.5,MIT
+vue-hot-reload-api,2.3.0,MIT
+vue-loader,14.2.2,MIT
+vue-resource,1.5.0,MIT
vue-router,3.0.1,MIT
-vue-style-loader,4.0.2,MIT
-vue-template-compiler,2.5.13,MIT
+vue-style-loader,4.1.0,MIT
+vue-template-compiler,2.5.16,MIT
vue-template-es2015-compiler,1.6.0,MIT
+vue-virtual-scroll-list,1.2.5,MIT
vuex,3.0.1,MIT
-warden,1.2.6,MIT
-watchpack,1.4.0,MIT
+warden,1.2.7,MIT
+watchpack,1.5.0,MIT
wbuf,1.7.2,MIT
-webpack,3.11.0,MIT
-webpack-bundle-analyzer,2.10.0,MIT
-webpack-dev-middleware,1.12.2,MIT
-webpack-dev-server,2.11.2,MIT
+webpack,4.7.0,MIT
+webpack-addons,1.1.5,MIT
+webpack-bundle-analyzer,2.11.1,MIT
+webpack-cli,2.1.2,MIT
+webpack-dev-middleware,2.0.6,MIT
+webpack-dev-middleware,3.1.3,MIT
+webpack-dev-server,3.1.4,MIT
+webpack-log,1.1.2,MIT
+webpack-log,1.2.0,MIT
webpack-rails,0.9.10,MIT
webpack-sources,1.0.1,MIT
-webpack-stats-plugin,0.1.5,MIT
+webpack-sources,1.1.0,MIT
+webpack-stats-plugin,0.2.1,MIT
websocket-driver,0.6.5,MIT
websocket-extensions,0.1.1,MIT
when,3.7.8,MIT
whet.extend,0.9.9,MIT
-which,1.2.12,ISC
which,1.3.0,ISC
-which-module,1.0.0,ISC
which-module,2.0.0,ISC
wide-align,1.1.2,ISC
widest-line,2.0.0,MIT
@@ -1697,10 +1764,12 @@ window-size,0.1.0,MIT
wordwrap,0.0.2,MIT
wordwrap,0.0.3,MIT
wordwrap,1.0.0,MIT
-worker-loader,1.1.0,MIT
+worker-farm,1.5.2,MIT
+worker-loader,1.1.1,MIT
wrap-ansi,2.1.0,MIT
wrappy,1.0.2,ISC
write,0.2.1,MIT
+write-file-atomic,1.3.4,ISC
write-file-atomic,2.3.0,ISC
ws,3.3.3,MIT
ws,4.0.0,MIT
@@ -1712,9 +1781,12 @@ xtend,4.0.1,MIT
y18n,3.2.1,ISC
y18n,4.0.0,ISC
yallist,2.1.2,ISC
+yargs,1.2.6,MIT
+yargs,11.0.0,MIT
+yargs,11.1.0,MIT
yargs,3.10.0,MIT
-yargs,6.6.0,MIT
-yargs,8.0.2,MIT
-yargs-parser,4.2.1,ISC
-yargs-parser,7.0.0,ISC
+yargs-parser,9.0.2,ISC
yeast,0.1.2,MIT
+yeoman-environment,2.0.5,Simplified BSD
+yeoman-environment,2.0.6,Simplified BSD
+yeoman-generator,2.0.5,Simplified BSD
diff --git a/yarn.lock b/yarn.lock
index 1e6ffa5f524..f34bc81067d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -54,9 +54,16 @@
lodash "^4.2.0"
to-fast-properties "^2.0.0"
-"@gitlab-org/gitlab-svgs@^1.18.0":
- version "1.18.0"
- resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.18.0.tgz#7829f0e6de0647dace54c1fcd597ee3424afb233"
+"@gitlab-org/gitlab-svgs@^1.22.0":
+ version "1.22.0"
+ resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.22.0.tgz#9f2daefebcda911cba8341313c8c464c8087fe44"
+
+"@mrmlnc/readdir-enhanced@^2.2.1":
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
+ dependencies:
+ call-me-maybe "^1.0.1"
+ glob-to-regexp "^0.3.0"
"@sindresorhus/is@^0.7.0":
version "0.7.0"
@@ -81,11 +88,11 @@ accepts@~1.3.3, accepts@~1.3.4:
mime-types "~2.1.16"
negotiator "0.6.1"
-acorn-dynamic-import@^2.0.0:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz#c752bd210bef679501b6c6cb7fc84f8f47158cc4"
+acorn-dynamic-import@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-3.0.0.tgz#901ceee4c7faaef7e07ad2a47e890675da50a278"
dependencies:
- acorn "^4.0.3"
+ acorn "^5.0.0"
acorn-jsx@^3.0.0:
version "3.0.1"
@@ -97,18 +104,10 @@ acorn@^3.0.4:
version "3.3.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
-acorn@^4.0.3:
- version "4.0.13"
- resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787"
-
acorn@^5.0.0, acorn@^5.2.1, acorn@^5.3.0:
version "5.4.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.4.1.tgz#fdc58d9d17f4a4e98d102ded826a9b9759125102"
-address@1.0.3, address@^1.0.1:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/address/-/address-1.0.3.tgz#b5f50631f8d6cec8bd20c963963afb55e06cbce9"
-
addressparser@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/addressparser/-/addressparser-1.0.1.tgz#47afbe1a2a9262191db6838e4fd1d39b40821746"
@@ -139,7 +138,7 @@ ajv@^4.7.0, ajv@^4.9.1:
co "^4.6.0"
json-stable-stringify "^1.0.1"
-ajv@^5.0.0, ajv@^5.1.0:
+ajv@^5.1.0:
version "5.5.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
dependencies:
@@ -188,7 +187,7 @@ ansi-align@^2.0.0:
dependencies:
string-width "^2.0.0"
-ansi-escapes@^1.1.0:
+ansi-escapes@^1.0.0, ansi-escapes@^1.1.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e"
@@ -218,6 +217,14 @@ ansi-styles@^3.2.1:
dependencies:
color-convert "^1.9.0"
+ansi-styles@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.0.0.tgz#cb102df1c56f5123eab8b67cd7b98027a0279178"
+
+any-observable@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/any-observable/-/any-observable-0.2.0.tgz#c67870058003579009083f54ac0abafb5c33d242"
+
anymatch@^1.3.0:
version "1.3.2"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a"
@@ -273,9 +280,9 @@ arr-union@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
-array-filter@~0.0.0:
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec"
+array-differ@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-1.0.0.tgz#eff52e3758249d33be402b8bb8e564bb2b5d4031"
array-find-index@^1.0.1:
version "1.0.2"
@@ -300,14 +307,6 @@ array-includes@^3.0.3:
define-properties "^1.1.2"
es-abstract "^1.7.0"
-array-map@~0.0.0:
- version "0.0.0"
- resolved "https://registry.yarnpkg.com/array-map/-/array-map-0.0.0.tgz#88a2bab73d1cf7bcd5c1b118a003f66f665fa662"
-
-array-reduce@~0.0.0:
- version "0.0.0"
- resolved "https://registry.yarnpkg.com/array-reduce/-/array-reduce-0.0.0.tgz#173899d3ffd1c7d9383e4479525dbe278cab5f2b"
-
array-slice@^0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-0.2.3.tgz#dd3cfb80ed7973a75117cdac69b0b99ec86186f5"
@@ -368,6 +367,14 @@ assign-symbols@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
+ast-types@0.10.1:
+ version "0.10.1"
+ resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.10.1.tgz#f52fca9715579a14f841d67d7f8d25432ab6a3dd"
+
+ast-types@0.11.3:
+ version "0.11.3"
+ resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.11.3.tgz#c20757fe72ee71278ea0ff3d87e5c2ca30d9edf8"
+
ast-types@0.x.x:
version "0.11.1"
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.11.1.tgz#5bb3a8d5ba292c3f4ae94d46df37afc30300b990"
@@ -380,16 +387,22 @@ async-limiter@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8"
-async@1.x, async@^1.4.0, async@^1.5.2:
+async@1.x, async@^1.4.0, async@^1.5.0, async@^1.5.2:
version "1.5.2"
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
-async@^2.0.0, async@^2.1.2, async@^2.1.4, async@^2.4.1:
+async@^2.0.0, async@^2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4"
dependencies:
lodash "^4.14.0"
+async@^2.1.4:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/async/-/async-2.4.1.tgz#62a56b279c98a11d0987096a01cc3eeb8eb7bbd7"
+ dependencies:
+ lodash "^4.14.0"
+
async@~2.1.2:
version "2.1.5"
resolved "https://registry.yarnpkg.com/async/-/async-2.1.5.tgz#e587c68580994ac67fc56ff86d3ac56bdbe810bc"
@@ -431,9 +444,9 @@ aws4@^1.2.1, aws4@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
-axios-mock-adapter@^1.10.0:
- version "1.10.0"
- resolved "https://registry.yarnpkg.com/axios-mock-adapter/-/axios-mock-adapter-1.10.0.tgz#3ccee65466439a2c7567e932798fc0377d39209d"
+axios-mock-adapter@^1.15.0:
+ version "1.15.0"
+ resolved "https://registry.yarnpkg.com/axios-mock-adapter/-/axios-mock-adapter-1.15.0.tgz#fbc06825d8302c95c3334d21023bba996255d45d"
dependencies:
deep-equal "^1.0.1"
@@ -450,7 +463,7 @@ axios@^0.17.1:
follow-redirects "^1.2.5"
is-buffer "^1.1.5"
-babel-code-frame@6.26.0, babel-code-frame@^6.16.0, babel-code-frame@^6.26.0:
+babel-code-frame@^6.16.0, babel-code-frame@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
dependencies:
@@ -458,9 +471,9 @@ babel-code-frame@6.26.0, babel-code-frame@^6.16.0, babel-code-frame@^6.26.0:
esutils "^2.0.2"
js-tokens "^3.0.2"
-babel-core@^6.26.0:
- version "6.26.0"
- resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.0.tgz#af32f78b31a6fcef119c87b0fd8d9753f03a0bb8"
+babel-core@^6.26.0, babel-core@^6.26.3:
+ version "6.26.3"
+ resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.3.tgz#b2e2f09e342d0f0c88e2f02e067794125e75c207"
dependencies:
babel-code-frame "^6.26.0"
babel-generator "^6.26.0"
@@ -472,15 +485,15 @@ babel-core@^6.26.0:
babel-traverse "^6.26.0"
babel-types "^6.26.0"
babylon "^6.18.0"
- convert-source-map "^1.5.0"
- debug "^2.6.8"
+ convert-source-map "^1.5.1"
+ debug "^2.6.9"
json5 "^0.5.1"
lodash "^4.17.4"
minimatch "^3.0.4"
path-is-absolute "^1.0.1"
- private "^0.1.7"
+ private "^0.1.8"
slash "^1.0.0"
- source-map "^0.5.6"
+ source-map "^0.5.7"
babel-eslint@^8.0.2:
version "8.0.2"
@@ -622,9 +635,9 @@ babel-helpers@^6.24.1:
babel-runtime "^6.22.0"
babel-template "^6.24.1"
-babel-loader@^7.1.2:
- version "7.1.2"
- resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.2.tgz#f6cbe122710f1aa2af4d881c6d5b54358ca24126"
+babel-loader@^7.1.4:
+ version "7.1.4"
+ resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.4.tgz#e3463938bd4e6d55d1c174c5485d406a188ed015"
dependencies:
find-cache-dir "^1.0.0"
loader-utils "^1.0.2"
@@ -663,6 +676,10 @@ babel-plugin-syntax-async-generators@^6.5.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz#6bc963ebb16eccbae6b92b596eb7f35c342a8b9a"
+babel-plugin-syntax-class-constructor-call@^6.18.0:
+ version "6.18.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-constructor-call/-/babel-plugin-syntax-class-constructor-call-6.18.0.tgz#9cb9d39fe43c8600bec8146456ddcbd4e1a76416"
+
babel-plugin-syntax-class-properties@^6.8.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz#d7eb23b79a317f8543962c505b827c7d6cac27de"
@@ -679,6 +696,14 @@ babel-plugin-syntax-exponentiation-operator@^6.8.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de"
+babel-plugin-syntax-export-extensions@^6.8.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-export-extensions/-/babel-plugin-syntax-export-extensions-6.13.0.tgz#70a1484f0f9089a4e84ad44bac353c95b9b12721"
+
+babel-plugin-syntax-flow@^6.18.0:
+ version "6.18.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz#4c3ab20a2af26aa20cd25995c398c4eb70310c8d"
+
babel-plugin-syntax-object-rest-spread@^6.13.0, babel-plugin-syntax-object-rest-spread@^6.8.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5"
@@ -703,6 +728,14 @@ babel-plugin-transform-async-to-generator@^6.24.1:
babel-plugin-syntax-async-functions "^6.8.0"
babel-runtime "^6.22.0"
+babel-plugin-transform-class-constructor-call@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-constructor-call/-/babel-plugin-transform-class-constructor-call-6.24.1.tgz#80dc285505ac067dcb8d6c65e2f6f11ab7765ef9"
+ dependencies:
+ babel-plugin-syntax-class-constructor-call "^6.18.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
babel-plugin-transform-class-properties@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz#6a79763ea61d33d36f37b611aa9def81a81b46ac"
@@ -905,6 +938,20 @@ babel-plugin-transform-exponentiation-operator@^6.24.1:
babel-plugin-syntax-exponentiation-operator "^6.8.0"
babel-runtime "^6.22.0"
+babel-plugin-transform-export-extensions@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-export-extensions/-/babel-plugin-transform-export-extensions-6.22.0.tgz#53738b47e75e8218589eea946cbbd39109bbe653"
+ dependencies:
+ babel-plugin-syntax-export-extensions "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-flow-strip-types@^6.8.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz#84cb672935d43714fdc32bce84568d87441cf7cf"
+ dependencies:
+ babel-plugin-syntax-flow "^6.18.0"
+ babel-runtime "^6.22.0"
+
babel-plugin-transform-object-rest-spread@^6.22.0:
version "6.23.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.23.0.tgz#875d6bc9be761c58a2ae3feee5dc4895d8c7f921"
@@ -925,7 +972,7 @@ babel-plugin-transform-strict-mode@^6.24.1:
babel-runtime "^6.22.0"
babel-types "^6.24.1"
-babel-preset-es2015@^6.24.1:
+babel-preset-es2015@^6.24.1, babel-preset-es2015@^6.9.0:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz#d44050d6bc2c9feea702aaf38d727a0210538939"
dependencies:
@@ -975,6 +1022,14 @@ babel-preset-latest@^6.24.1:
babel-preset-es2016 "^6.24.1"
babel-preset-es2017 "^6.24.1"
+babel-preset-stage-1@^6.5.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-preset-stage-1/-/babel-preset-stage-1-6.24.1.tgz#7692cd7dcd6849907e6ae4a0a85589cfb9e2bfb0"
+ dependencies:
+ babel-plugin-transform-class-constructor-call "^6.24.1"
+ babel-plugin-transform-export-extensions "^6.22.0"
+ babel-preset-stage-2 "^6.24.1"
+
babel-preset-stage-2@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-preset-stage-2/-/babel-preset-stage-2-6.24.1.tgz#d9e2960fb3d71187f0e64eec62bc07767219bdc1"
@@ -994,7 +1049,7 @@ babel-preset-stage-3@^6.24.1:
babel-plugin-transform-exponentiation-operator "^6.24.1"
babel-plugin-transform-object-rest-spread "^6.22.0"
-babel-register@^6.26.0:
+babel-register@^6.26.0, babel-register@^6.9.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071"
dependencies:
@@ -1050,10 +1105,14 @@ babylon@7.0.0-beta.32, babylon@^7.0.0-beta.31:
version "7.0.0-beta.32"
resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.32.tgz#e9033cb077f64d6895f4125968b37dc0a8c3bc6e"
-babylon@^6.18.0:
+babylon@^6.17.3, babylon@^6.18.0:
version "6.18.0"
resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
+babylon@^7.0.0-beta.30:
+ version "7.0.0-beta.44"
+ resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.44.tgz#89159e15e6e30c5096e22d738d8c0af8a0e8ca1d"
+
backo2@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
@@ -1122,6 +1181,10 @@ binary-extensions@^1.0.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205"
+binaryextensions@2:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.1.1.tgz#3209a51ca4a4ad541a3b8d3d6a6d5b83a2485935"
+
bitsyntax@~0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/bitsyntax/-/bitsyntax-0.0.4.tgz#eb10cc6f82b8c490e3e85698f07e83d46e0cba82"
@@ -1223,7 +1286,7 @@ boxen@^1.2.1:
term-size "^1.2.0"
widest-line "^2.0.0"
-brace-expansion@^1.0.0, brace-expansion@^1.1.7, brace-expansion@^1.1.8:
+brace-expansion@^1.1.7, brace-expansion@^1.1.8:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
dependencies:
@@ -1372,7 +1435,7 @@ bytes@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
-cacache@^10.0.1:
+cacache@^10.0.1, cacache@^10.0.4:
version "10.0.4"
resolved "https://registry.yarnpkg.com/cacache/-/cacache-10.0.4.tgz#6452367999eff9d4188aefd9a14e9d7c6a263460"
dependencies:
@@ -1416,6 +1479,10 @@ cacheable-request@^2.1.1:
normalize-url "2.0.1"
responselike "1.0.2"
+call-me-maybe@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b"
+
caller-path@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f"
@@ -1445,10 +1512,6 @@ camelcase@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
-camelcase@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a"
-
camelcase@^4.0.0, camelcase@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
@@ -1485,7 +1548,7 @@ center-align@^0.1.1:
align-text "^0.1.3"
lazy-cache "^1.0.3"
-chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
+chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
dependencies:
@@ -1495,7 +1558,7 @@ chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
strip-ansi "^3.0.0"
supports-color "^2.0.0"
-chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.3.2, chalk@^2.4.1:
+chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e"
dependencies:
@@ -1503,10 +1566,30 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.3
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
+chalk@^2.3.2:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.0.tgz#a060a297a6b57e15b61ca63ce84995daa0fe6e52"
+ dependencies:
+ ansi-styles "^3.2.1"
+ escape-string-regexp "^1.0.5"
+ supports-color "^5.3.0"
+
+chalk@~0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.4.0.tgz#5199a3ddcd0c1efe23bc08c1b027b06176e0c64f"
+ dependencies:
+ ansi-styles "~1.0.0"
+ has-color "~0.1.0"
+ strip-ansi "~0.1.0"
+
chardet@^0.4.0:
version "0.4.2"
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2"
+"charenc@>= 0.0.1":
+ version "0.0.2"
+ resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
+
chart.js@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-1.0.2.tgz#ad57d2229cfd8ccf5955147e8121b4911e69dfe7"
@@ -1515,7 +1598,7 @@ check-types@^7.3.0:
version "7.3.0"
resolved "https://registry.yarnpkg.com/check-types/-/check-types-7.3.0.tgz#468f571a4435c24248f5fd0cb0e8d87c3c341e7d"
-chokidar@^1.4.1, chokidar@^1.7.0:
+chokidar@^1.4.1:
version "1.7.0"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468"
dependencies:
@@ -1552,6 +1635,10 @@ chownr@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181"
+chrome-trace-event@^0.1.1:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-0.1.2.tgz#90f36885d5345a50621332f0717b595883d5d982"
+
cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de"
@@ -1590,7 +1677,7 @@ cli-boxes@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143"
-cli-cursor@^1.0.1:
+cli-cursor@^1.0.1, cli-cursor@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987"
dependencies:
@@ -1602,6 +1689,23 @@ cli-cursor@^2.1.0:
dependencies:
restore-cursor "^2.0.0"
+cli-spinners@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-0.1.2.tgz#bb764d88e185fb9e1e6a2a1f19772318f605e31c"
+
+cli-table@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23"
+ dependencies:
+ colors "1.0.3"
+
+cli-truncate@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-0.2.1.tgz#9f15cfbb0705005369216c626ac7d05ab90dd574"
+ dependencies:
+ slice-ansi "0.0.4"
+ string-width "^1.0.1"
+
cli-width@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a"
@@ -1622,24 +1726,52 @@ cliui@^2.1.0:
right-align "^0.1.1"
wordwrap "0.0.2"
-cliui@^3.2.0:
- version "3.2.0"
- resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d"
+cliui@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.0.0.tgz#743d4650e05f36d1ed2575b59638d87322bfbbcc"
dependencies:
- string-width "^1.0.1"
- strip-ansi "^3.0.1"
+ string-width "^2.1.1"
+ strip-ansi "^4.0.0"
wrap-ansi "^2.0.0"
+clone-buffer@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58"
+
clone-response@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b"
dependencies:
mimic-response "^1.0.0"
+clone-stats@^0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-0.0.1.tgz#b88f94a82cf38b8791d58046ea4029ad88ca99d1"
+
+clone-stats@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680"
+
+clone@^1.0.0:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.3.tgz#298d7e2231660f40c003c2ed3140decf3f53085f"
+
clone@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149"
+clone@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.1.tgz#d217d1e961118e3ac9a4b8bba3285553bf647cdb"
+
+cloneable-readable@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/cloneable-readable/-/cloneable-readable-1.0.0.tgz#a6290d413f217a61232f95e458ff38418cfb0117"
+ dependencies:
+ inherits "^2.0.1"
+ process-nextick-args "^1.0.6"
+ through2 "^2.0.1"
+
co@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
@@ -1697,7 +1829,11 @@ colormin@^1.0.5:
css-color-names "0.0.4"
has "^1.0.1"
-colors@^1.1.0, colors@~1.1.2:
+colors@1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
+
+colors@^1.1.0, colors@^1.1.2, colors@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
@@ -1717,6 +1853,10 @@ commander@^2.13.0, commander@^2.15.1, commander@^2.9.0:
version "2.15.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f"
+commander@~2.13.0:
+ version "2.13.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c"
+
commondir@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
@@ -1739,13 +1879,13 @@ compressible@~2.0.10:
dependencies:
mime-db ">= 1.29.0 < 2"
-compression-webpack-plugin@^1.1.7:
- version "1.1.7"
- resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-1.1.7.tgz#b0dfb97cf1d26baab997b584b8c36fe91872abe2"
+compression-webpack-plugin@^1.1.11:
+ version "1.1.11"
+ resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-1.1.11.tgz#8384c7a6ead1d2e2efb190bdfcdcf35878ed8266"
dependencies:
- async "^2.4.1"
cacache "^10.0.1"
find-cache-dir "^1.0.0"
+ neo-async "^2.5.0"
serialize-javascript "^1.4.0"
webpack-sources "^1.0.1"
@@ -1829,9 +1969,9 @@ content-type@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
-convert-source-map@^1.5.0:
- version "1.5.0"
- resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5"
+convert-source-map@^1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5"
cookie-signature@1.0.6:
version "1.0.6"
@@ -1856,15 +1996,15 @@ copy-descriptor@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
-copy-webpack-plugin@^4.4.1:
- version "4.4.1"
- resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-4.4.1.tgz#1e8c366211db6dc2ddee40e5a3e4fc661dd149e8"
+copy-webpack-plugin@^4.5.1:
+ version "4.5.1"
+ resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-4.5.1.tgz#fc4f68f4add837cc5e13d111b20715793225d29c"
dependencies:
- cacache "^10.0.1"
+ cacache "^10.0.4"
find-cache-dir "^1.0.0"
globby "^7.1.1"
is-glob "^4.0.0"
- loader-utils "^0.2.15"
+ loader-utils "^1.1.0"
minimatch "^3.0.4"
p-limit "^1.0.0"
serialize-javascript "^1.4.0"
@@ -1932,7 +2072,7 @@ cropper@^2.3.0:
dependencies:
jquery ">= 1.9.1"
-cross-spawn@5.1.0, cross-spawn@^5.0.1:
+cross-spawn@^5.0.1:
version "5.1.0"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
dependencies:
@@ -1940,6 +2080,20 @@ cross-spawn@5.1.0, cross-spawn@^5.0.1:
shebang-command "^1.2.0"
which "^1.2.9"
+cross-spawn@^6.0.5:
+ version "6.0.5"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
+ dependencies:
+ nice-try "^1.0.4"
+ path-key "^2.0.1"
+ semver "^5.5.0"
+ shebang-command "^1.2.0"
+ which "^1.2.9"
+
+"crypt@>= 0.0.1":
+ version "0.0.2"
+ resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b"
+
cryptiles@2.x.x:
version "2.0.5"
resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8"
@@ -1976,9 +2130,9 @@ css-color-names@0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0"
-css-loader@^0.28.9:
- version "0.28.9"
- resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-0.28.9.tgz#68064b85f4e271d7ce4c48a58300928e535d1c95"
+css-loader@^0.28.11:
+ version "0.28.11"
+ resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-0.28.11.tgz#c3f9864a700be2711bb5a2462b2389b1a392dab7"
dependencies:
babel-code-frame "^6.26.0"
css-selector-tokenizer "^0.7.0"
@@ -2199,6 +2353,10 @@ dagre-layout@^0.8.0:
graphlib "^2.1.1"
lodash "^4.17.4"
+dargs@^5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/dargs/-/dargs-5.1.0.tgz#ec7ea50c78564cd36c9d5ec18f66329fade27829"
+
dashdash@^1.12.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
@@ -2209,6 +2367,10 @@ data-uri-to-buffer@1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-1.2.0.tgz#77163ea9c20d8641b4707e8f18abdf9a78f34835"
+date-fns@^1.27.2:
+ version "1.29.0"
+ resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.29.0.tgz#12e609cdcb935127311d04d33334e2960a2a54e6"
+
date-format@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/date-format/-/date-format-1.2.0.tgz#615e828e233dd1ab9bb9ae0950e0ceccfa6ecad8"
@@ -2217,11 +2379,15 @@ date-now@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
+dateformat@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
+
de-indent@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
-debug@2, debug@2.6.9, debug@^2.1.1, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.6, debug@^2.6.8, debug@~2.6.4, debug@~2.6.6:
+debug@2, debug@2.6.9, debug@^2.1.1, debug@^2.2.0, debug@^2.3.3, debug@^2.6.6, debug@^2.6.8, debug@^2.6.9, debug@~2.6.4, debug@~2.6.6:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
dependencies:
@@ -2257,7 +2423,7 @@ decode-uri-component@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
-decompress-response@^3.3.0:
+decompress-response@^3.2.0, decompress-response@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3"
dependencies:
@@ -2267,6 +2433,10 @@ deep-equal@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
+deep-extend@^0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.5.1.tgz#b894a9dd90d3023fbf1c55a394fb858eb2066f1f"
+
deep-extend@~0.4.0:
version "0.4.2"
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f"
@@ -2369,6 +2539,10 @@ destroy@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
+detect-conflict@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/detect-conflict/-/detect-conflict-1.0.1.tgz#088657a66a961c05019db7c4230883b1c6b4176e"
+
detect-indent@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208"
@@ -2383,21 +2557,18 @@ detect-node@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.3.tgz#a2033c09cc8e158d37748fbde7507832bd6ce127"
-detect-port-alt@1.1.5:
- version "1.1.5"
- resolved "https://registry.yarnpkg.com/detect-port-alt/-/detect-port-alt-1.1.5.tgz#a1aa8fc805a4a5df9b905b7ddc7eed036bcce889"
- dependencies:
- address "^1.0.1"
- debug "^2.6.0"
-
di@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c"
-diff@^3.4.0:
+diff@^3.3.1, diff@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c"
+diff@^3.5.0:
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
+
diffie-hellman@^5.0.0:
version "5.0.2"
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e"
@@ -2526,6 +2697,10 @@ ecc-jsbn@~0.1.1:
dependencies:
jsbn "~0.1.0"
+editions@^1.3.3:
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/editions/-/editions-1.3.4.tgz#3662cb592347c3168eb8e498a0ff73271d67f50b"
+
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@@ -2534,10 +2709,18 @@ ejs@^2.5.7:
version "2.5.7"
resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.7.tgz#cc872c168880ae3c7189762fd5ffc00896c9518a"
+ejs@^2.5.9:
+ version "2.5.9"
+ resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.9.tgz#7ba254582a560d267437109a68354112475b0ce5"
+
electron-to-chromium@^1.2.7:
version "1.3.3"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.3.tgz#651eb63fe89f39db70ffc8dbd5d9b66958bc6a0e"
+elegant-spinner@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e"
+
elliptic@^6.0.0:
version "6.4.0"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df"
@@ -2607,14 +2790,13 @@ engine.io@~3.1.0:
optionalDependencies:
uws "~9.14.0"
-enhanced-resolve@^3.4.0:
- version "3.4.1"
- resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz#0421e339fd71419b3da13d129b3979040230476e"
+enhanced-resolve@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.0.0.tgz#e34a6eaa790f62fccd71d93959f56b2b432db10a"
dependencies:
graceful-fs "^4.1.2"
memory-fs "^0.4.0"
- object-assign "^4.0.1"
- tapable "^0.2.7"
+ tapable "^1.0.0"
enhanced-resolve@~0.9.0:
version "0.9.1"
@@ -2632,18 +2814,41 @@ entities@^1.1.1, entities@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
+envinfo@^4.4.2:
+ version "4.4.2"
+ resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-4.4.2.tgz#472c49f3a8b9bca73962641ce7cb692bf623cd1c"
+
errno@^0.1.3:
version "0.1.4"
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d"
dependencies:
prr "~0.0.0"
+errno@^0.1.4:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
+ dependencies:
+ prr "~1.0.1"
+
error-ex@^1.2.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.0.tgz#e67b43f3e82c96ea3a584ffee0b9fc3325d802d9"
dependencies:
is-arrayish "^0.2.1"
+error-ex@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc"
+ dependencies:
+ is-arrayish "^0.2.1"
+
+error@^7.0.2:
+ version "7.0.2"
+ resolved "https://registry.yarnpkg.com/error/-/error-7.0.2.tgz#a5f75fff4d9926126ddac0ea5dc38e689153cb02"
+ dependencies:
+ string-template "~0.2.1"
+ xtend "~4.0.0"
+
es-abstract@^1.7.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.10.0.tgz#1ecb36c197842a00d8ee4c2dfd8646bb97d60864"
@@ -2702,7 +2907,7 @@ es6-set@~0.1.5:
es6-symbol "3.1.1"
event-emitter "~0.3.5"
-es6-symbol@3, es6-symbol@3.1.1, es6-symbol@^3.1, es6-symbol@^3.1.1, es6-symbol@~3.1, es6-symbol@~3.1.1:
+es6-symbol@3, es6-symbol@3.1.1, es6-symbol@^3.1, es6-symbol@~3.1, es6-symbol@~3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77"
dependencies:
@@ -2722,7 +2927,7 @@ escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
-escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
+escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
@@ -2902,7 +3107,7 @@ esprima@3.x.x, esprima@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
-esprima@^4.0.0:
+esprima@^4.0.0, esprima@~4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804"
@@ -3111,6 +3316,14 @@ external-editor@^2.0.4:
iconv-lite "^0.4.17"
tmp "^0.0.33"
+external-editor@^2.1.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.2.0.tgz#045511cfd8d133f3846673d1047c154e214ad3d5"
+ dependencies:
+ chardet "^0.4.0"
+ iconv-lite "^0.4.17"
+ tmp "^0.0.33"
+
extglob@^0.3.1:
version "0.3.2"
resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1"
@@ -3142,6 +3355,16 @@ fast-deep-equal@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
+fast-glob@^2.0.2:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.1.tgz#686c2345be88f3741e174add0be6f2e5b6078889"
+ dependencies:
+ "@mrmlnc/readdir-enhanced" "^2.2.1"
+ glob-parent "^3.1.0"
+ is-glob "^4.0.0"
+ merge2 "^1.2.1"
+ micromatch "^3.1.10"
+
fast-json-stable-stringify@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
@@ -3166,7 +3389,7 @@ faye-websocket@~0.11.0:
dependencies:
websocket-driver ">=0.5.1"
-figures@^1.3.5:
+figures@^1.3.5, figures@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e"
dependencies:
@@ -3186,9 +3409,9 @@ file-entry-cache@^2.0.0:
flat-cache "^1.2.1"
object-assign "^4.0.1"
-file-loader@^1.1.8:
- version "1.1.8"
- resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-1.1.8.tgz#a62592ed732667d7482dc3268c381c7f0c913086"
+file-loader@^1.1.11:
+ version "1.1.11"
+ resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-1.1.11.tgz#6fe886449b0f2a936e43cabaac0cdbfb369506f8"
dependencies:
loader-utils "^1.0.2"
schema-utils "^0.4.5"
@@ -3208,10 +3431,6 @@ fileset@^2.0.2:
glob "^7.0.3"
minimatch "^3.0.3"
-filesize@3.5.11:
- version "3.5.11"
- resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.5.11.tgz#1919326749433bb3cf77368bd158caabcc19e9ee"
-
filesize@^3.5.11:
version "3.6.0"
resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.6.0.tgz#22d079615624bb6fd3c04026120628a41b3f4efa"
@@ -3272,6 +3491,12 @@ find-up@^2.0.0, find-up@^2.1.0:
dependencies:
locate-path "^2.0.0"
+first-chunk-stream@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/first-chunk-stream/-/first-chunk-stream-2.0.0.tgz#1bdecdb8e083c0664b91945581577a43a9f31d70"
+ dependencies:
+ readable-stream "^2.0.2"
+
flat-cache@^1.2.1:
version "1.2.2"
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.2.2.tgz#fa86714e72c21db88601761ecf2f555d1abc6b96"
@@ -3285,6 +3510,10 @@ flatten@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782"
+flow-parser@^0.*:
+ version "0.66.0"
+ resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.66.0.tgz#be583fefb01192aa5164415d31a6241b35718983"
+
flush-write-stream@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.2.tgz#c81b90d8746766f1a609a46809946c45dd8ae417"
@@ -3485,6 +3714,26 @@ getpass@^0.1.1:
dependencies:
assert-plus "^1.0.0"
+gh-got@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/gh-got/-/gh-got-6.0.0.tgz#d74353004c6ec466647520a10bd46f7299d268d0"
+ dependencies:
+ got "^7.0.0"
+ is-plain-obj "^1.1.0"
+
+github-username@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/github-username/-/github-username-4.1.0.tgz#cbe280041883206da4212ae9e4b5f169c30bf417"
+ dependencies:
+ gh-got "^6.0.0"
+
+glob-all@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/glob-all/-/glob-all-3.1.0.tgz#8913ddfb5ee1ac7812656241b03d5217c64b02ab"
+ dependencies:
+ glob "^7.0.5"
+ yargs "~1.2.6"
+
glob-base@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
@@ -3505,6 +3754,10 @@ glob-parent@^3.1.0:
is-glob "^3.1.0"
path-dirname "^1.0.0"
+glob-to-regexp@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
+
glob@^5.0.15:
version "5.0.15"
resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1"
@@ -3515,7 +3768,18 @@ glob@^5.0.15:
once "^1.3.0"
path-is-absolute "^1.0.0"
-glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2:
+glob@^7.0.0, glob@^7.0.3:
+ version "7.1.1"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8"
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.0.2"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+glob@^7.0.5, glob@^7.1.1, glob@^7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
dependencies:
@@ -3532,7 +3796,7 @@ global-dirs@^0.1.0:
dependencies:
ini "^1.3.4"
-global-modules@1.0.0, global-modules@^1.0.0:
+global-modules@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea"
dependencies:
@@ -3590,6 +3854,18 @@ globby@^7.1.1:
pify "^3.0.0"
slash "^1.0.0"
+globby@^8.0.0:
+ version "8.0.1"
+ resolved "https://registry.yarnpkg.com/globby/-/globby-8.0.1.tgz#b5ad48b8aa80b35b814fc1281ecc851f1d2b5b50"
+ dependencies:
+ array-union "^1.0.1"
+ dir-glob "^2.0.0"
+ fast-glob "^2.0.2"
+ glob "^7.1.2"
+ ignore "^3.3.5"
+ pify "^3.0.0"
+ slash "^1.0.0"
+
good-listener@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50"
@@ -3612,7 +3888,26 @@ got@^6.7.1:
unzip-response "^2.0.1"
url-parse-lax "^1.0.0"
-got@^8.0.3:
+got@^7.0.0:
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/got/-/got-7.1.0.tgz#05450fd84094e6bbea56f451a43a9c289166385a"
+ dependencies:
+ decompress-response "^3.2.0"
+ duplexer3 "^0.1.4"
+ get-stream "^3.0.0"
+ is-plain-obj "^1.1.0"
+ is-retry-allowed "^1.0.0"
+ is-stream "^1.0.0"
+ isurl "^1.0.0-alpha5"
+ lowercase-keys "^1.0.0"
+ p-cancelable "^0.3.0"
+ p-timeout "^1.1.1"
+ safe-buffer "^5.0.1"
+ timed-out "^4.0.0"
+ url-parse-lax "^1.0.0"
+ url-to-options "^1.0.1"
+
+got@^8.0.3, got@^8.2.0:
version "8.3.0"
resolved "https://registry.yarnpkg.com/got/-/got-8.3.0.tgz#6ba26e75f8a6cc4c6b3eb1fe7ce4fec7abac8533"
dependencies:
@@ -3644,11 +3939,11 @@ graphlib@^2.1.1:
dependencies:
lodash "^4.11.1"
-gzip-size@3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-3.0.0.tgz#546188e9bdc337f673772f81660464b389dce520"
+grouped-queue@^0.3.3:
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/grouped-queue/-/grouped-queue-0.3.3.tgz#c167d2a5319c5a0e0964ef6a25b7c2df8996c85c"
dependencies:
- duplexer "^0.1.1"
+ lodash "^4.17.2"
gzip-size@^4.1.0:
version "4.1.0"
@@ -3714,6 +4009,10 @@ has-binary2@~1.0.2:
dependencies:
isarray "2.0.1"
+has-color@~0.1.0:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/has-color/-/has-color-0.1.7.tgz#67144a5260c34fc3cca677d041daf52fe7b78b2f"
+
has-cors@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39"
@@ -3734,10 +4033,6 @@ has-symbol-support-x@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/has-symbol-support-x/-/has-symbol-support-x-1.3.0.tgz#588bd6927eaa0e296afae24160659167fc2be4f8"
-has-symbols@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44"
-
has-to-string-tag-x@^1.2.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/has-to-string-tag-x/-/has-to-string-tag-x-1.3.0.tgz#78e3d98c3c0ec9413e970eb8d766249a1e13058f"
@@ -3920,14 +4215,14 @@ http-proxy-agent@1:
debug "2"
extend "3"
-http-proxy-middleware@~0.17.4:
- version "0.17.4"
- resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.17.4.tgz#642e8848851d66f09d4f124912846dbaeb41b833"
+http-proxy-middleware@~0.18.0:
+ version "0.18.0"
+ resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz#0987e6bb5a5606e5a69168d8f967a87f15dd8aab"
dependencies:
http-proxy "^1.16.2"
- is-glob "^3.1.0"
- lodash "^4.17.2"
- micromatch "^2.3.11"
+ is-glob "^4.0.0"
+ lodash "^4.17.5"
+ micromatch "^3.1.9"
http-proxy@^1.13.0, http-proxy@^1.16.2:
version "1.16.2"
@@ -4041,6 +4336,10 @@ indent-string@^2.1.0:
dependencies:
repeating "^2.0.0"
+indent-string@^3.0.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289"
+
indexes-of@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
@@ -4076,7 +4375,25 @@ ini@^1.3.4, ini@~1.3.0:
version "1.3.5"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
-inquirer@3.3.0:
+inquirer@^0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e"
+ dependencies:
+ ansi-escapes "^1.1.0"
+ ansi-regex "^2.0.0"
+ chalk "^1.0.0"
+ cli-cursor "^1.0.1"
+ cli-width "^2.0.0"
+ figures "^1.3.5"
+ lodash "^4.3.0"
+ readline2 "^1.0.1"
+ run-async "^0.1.0"
+ rx-lite "^3.1.2"
+ string-width "^1.0.1"
+ strip-ansi "^3.0.0"
+ through "^2.3.6"
+
+inquirer@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9"
dependencies:
@@ -4095,22 +4412,22 @@ inquirer@3.3.0:
strip-ansi "^4.0.0"
through "^2.3.6"
-inquirer@^0.12.0:
- version "0.12.0"
- resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e"
+inquirer@^5.1.0:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-5.2.0.tgz#db350c2b73daca77ff1243962e9f22f099685726"
dependencies:
- ansi-escapes "^1.1.0"
- ansi-regex "^2.0.0"
- chalk "^1.0.0"
- cli-cursor "^1.0.1"
+ ansi-escapes "^3.0.0"
+ chalk "^2.0.0"
+ cli-cursor "^2.1.0"
cli-width "^2.0.0"
- figures "^1.3.5"
+ external-editor "^2.1.0"
+ figures "^2.0.0"
lodash "^4.3.0"
- readline2 "^1.0.1"
- run-async "^0.1.0"
- rx-lite "^3.1.2"
- string-width "^1.0.1"
- strip-ansi "^3.0.0"
+ mute-stream "0.0.7"
+ run-async "^2.2.0"
+ rxjs "^5.5.2"
+ string-width "^2.1.0"
+ strip-ansi "^4.0.0"
through "^2.3.6"
internal-ip@1.2.0:
@@ -4123,6 +4440,10 @@ interpret@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.1.tgz#d579fb7f693b858004947af39fa0db49f795602c"
+interpret@^1.0.4:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614"
+
into-stream@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/into-stream/-/into-stream-3.1.0.tgz#96fb0a936c12babd6ff1752a17d05616abd094c6"
@@ -4350,6 +4671,12 @@ is-object@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.1.tgz#8952688c5ec2ffd6b03ecc85e769e02903083470"
+is-observable@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/is-observable/-/is-observable-0.2.0.tgz#b361311d83c6e5d726cabf5e250b0237106f5ae2"
+ dependencies:
+ symbol-observable "^0.2.2"
+
is-odd@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/is-odd/-/is-odd-2.0.0.tgz#7646624671fd7ea558ccd9a2795182f2958f1b24"
@@ -4372,7 +4699,7 @@ is-path-inside@^1.0.0:
dependencies:
path-is-inside "^1.0.1"
-is-plain-obj@^1.0.0:
+is-plain-obj@^1.0.0, is-plain-obj@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
@@ -4424,9 +4751,11 @@ is-retry-allowed@^1.0.0, is-retry-allowed@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34"
-is-root@1.0.0:
+is-scoped@^1.0.0:
version "1.0.0"
- resolved "https://registry.yarnpkg.com/is-root/-/is-root-1.0.0.tgz#07b6c233bc394cd9d02ba15c966bd6660d6342d5"
+ resolved "https://registry.yarnpkg.com/is-scoped/-/is-scoped-1.0.0.tgz#449ca98299e713038256289ecb2b540dc437cb30"
+ dependencies:
+ scoped-regex "^1.0.0"
is-stream@^1.0.0, is-stream@^1.1.0:
version "1.1.0"
@@ -4480,7 +4809,7 @@ isarray@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e"
-isbinaryfile@^3.0.0:
+isbinaryfile@^3.0.0, isbinaryfile@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-3.0.2.tgz#4a3e974ec0cba9004d3fc6cde7209ea69368a621"
@@ -4600,6 +4929,14 @@ istanbul@^0.4.5:
which "^1.1.1"
wordwrap "^1.0.0"
+istextorbinary@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/istextorbinary/-/istextorbinary-2.2.1.tgz#a5231a08ef6dd22b268d0895084cf8d58b5bec53"
+ dependencies:
+ binaryextensions "2"
+ editions "^1.3.3"
+ textextensions "2"
+
isurl@^1.0.0-alpha5:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isurl/-/isurl-1.0.0.tgz#b27f4f49f3cdaa3ea44a0a5b7f3462e6edc39d67"
@@ -4670,6 +5007,46 @@ jsbn@~0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
+jscodeshift@^0.4.0:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/jscodeshift/-/jscodeshift-0.4.1.tgz#da91a1c2eccfa03a3387a21d39948e251ced444a"
+ dependencies:
+ async "^1.5.0"
+ babel-plugin-transform-flow-strip-types "^6.8.0"
+ babel-preset-es2015 "^6.9.0"
+ babel-preset-stage-1 "^6.5.0"
+ babel-register "^6.9.0"
+ babylon "^6.17.3"
+ colors "^1.1.2"
+ flow-parser "^0.*"
+ lodash "^4.13.1"
+ micromatch "^2.3.7"
+ node-dir "0.1.8"
+ nomnom "^1.8.1"
+ recast "^0.12.5"
+ temp "^0.8.1"
+ write-file-atomic "^1.2.0"
+
+jscodeshift@^0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/jscodeshift/-/jscodeshift-0.5.0.tgz#bdb7b6cc20dd62c16aa728c3fa2d2fe66ca7c748"
+ dependencies:
+ babel-plugin-transform-flow-strip-types "^6.8.0"
+ babel-preset-es2015 "^6.9.0"
+ babel-preset-stage-1 "^6.5.0"
+ babel-register "^6.9.0"
+ babylon "^7.0.0-beta.30"
+ colors "^1.1.2"
+ flow-parser "^0.*"
+ lodash "^4.13.1"
+ micromatch "^2.3.7"
+ neo-async "^2.5.0"
+ node-dir "0.1.8"
+ nomnom "^1.8.1"
+ recast "^0.14.1"
+ temp "^0.8.1"
+ write-file-atomic "^1.2.0"
+
jsesc@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b"
@@ -4682,9 +5059,9 @@ json-buffer@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898"
-json-loader@^0.5.4:
- version "0.5.7"
- resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d"
+json-parse-better-errors@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
json-schema-traverse@^0.3.0:
version "0.3.1"
@@ -4905,6 +5282,54 @@ lie@~3.1.0:
dependencies:
immediate "~3.0.5"
+listr-silent-renderer@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e"
+
+listr-update-renderer@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/listr-update-renderer/-/listr-update-renderer-0.4.0.tgz#344d980da2ca2e8b145ba305908f32ae3f4cc8a7"
+ dependencies:
+ chalk "^1.1.3"
+ cli-truncate "^0.2.1"
+ elegant-spinner "^1.0.1"
+ figures "^1.7.0"
+ indent-string "^3.0.0"
+ log-symbols "^1.0.2"
+ log-update "^1.0.2"
+ strip-ansi "^3.0.1"
+
+listr-verbose-renderer@^0.4.0:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz#8206f4cf6d52ddc5827e5fd14989e0e965933a35"
+ dependencies:
+ chalk "^1.1.3"
+ cli-cursor "^1.0.2"
+ date-fns "^1.27.2"
+ figures "^1.7.0"
+
+listr@^0.13.0:
+ version "0.13.0"
+ resolved "https://registry.yarnpkg.com/listr/-/listr-0.13.0.tgz#20bb0ba30bae660ee84cc0503df4be3d5623887d"
+ dependencies:
+ chalk "^1.1.3"
+ cli-truncate "^0.2.1"
+ figures "^1.7.0"
+ indent-string "^2.1.0"
+ is-observable "^0.2.0"
+ is-promise "^2.1.0"
+ is-stream "^1.1.0"
+ listr-silent-renderer "^1.1.1"
+ listr-update-renderer "^0.4.0"
+ listr-verbose-renderer "^0.4.0"
+ log-symbols "^1.0.2"
+ log-update "^1.0.2"
+ ora "^0.2.3"
+ p-map "^1.1.1"
+ rxjs "^5.4.2"
+ stream-to-observable "^0.2.0"
+ strip-ansi "^3.0.1"
+
load-json-file@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
@@ -4915,28 +5340,19 @@ load-json-file@^1.0.0:
pinkie-promise "^2.0.0"
strip-bom "^2.0.0"
-load-json-file@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8"
+load-json-file@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
dependencies:
graceful-fs "^4.1.2"
- parse-json "^2.2.0"
- pify "^2.0.0"
+ parse-json "^4.0.0"
+ pify "^3.0.0"
strip-bom "^3.0.0"
loader-runner@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2"
-loader-utils@^0.2.15:
- version "0.2.16"
- resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.16.tgz#f08632066ed8282835dff88dfb52704765adee6d"
- dependencies:
- big.js "^3.1.3"
- emojis-list "^2.0.0"
- json5 "^0.5.0"
- object-assign "^4.0.1"
-
loader-utils@^1.0.0, loader-utils@^1.0.2, loader-utils@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd"
@@ -4990,10 +5406,6 @@ lodash.deburr@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/lodash.deburr/-/lodash.deburr-4.1.0.tgz#ddb1bbb3ef07458c0177ba07de14422cb033ff9b"
-lodash.endswith@^4.2.1:
- version "4.2.1"
- resolved "https://registry.yarnpkg.com/lodash.endswith/-/lodash.endswith-4.2.1.tgz#fed59ac1738ed3e236edd7064ec456448b37bc09"
-
lodash.escaperegexp@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347"
@@ -5009,14 +5421,6 @@ lodash.isarray@^3.0.0:
version "3.0.4"
resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55"
-lodash.isfunction@^3.0.8:
- version "3.0.9"
- resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051"
-
-lodash.isstring@^4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
-
lodash.kebabcase@4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.0.1.tgz#5e63bc9aa2a5562ff3b97ca7af2f803de1bcb90e"
@@ -5039,10 +5443,6 @@ lodash.snakecase@4.0.1:
lodash.deburr "^4.0.0"
lodash.words "^4.0.0"
-lodash.startswith@^4.2.1:
- version "4.2.1"
- resolved "https://registry.yarnpkg.com/lodash.startswith/-/lodash.startswith-4.2.1.tgz#c598c4adce188a27e53145731cdc6c0e7177600c"
-
lodash.uniq@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
@@ -5055,16 +5455,39 @@ lodash@4.17.4:
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
-lodash@^4.0.0, lodash@^4.11.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0:
+lodash@^4.0.0, lodash@^4.11.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0:
version "4.17.5"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
+lodash@^4.17.10:
+ version "4.17.10"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
+
+log-symbols@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18"
+ dependencies:
+ chalk "^1.0.0"
+
log-symbols@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.1.0.tgz#f35fa60e278832b538dc4dddcbb478a45d3e3be6"
dependencies:
chalk "^2.0.1"
+log-symbols@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a"
+ dependencies:
+ chalk "^2.0.1"
+
+log-update@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/log-update/-/log-update-1.0.2.tgz#19929f64c4093d2d2e7075a1dad8af59c296b8d1"
+ dependencies:
+ ansi-escapes "^1.0.0"
+ cli-cursor "^1.0.2"
+
log4js@^2.3.9:
version "2.5.3"
resolved "https://registry.yarnpkg.com/log4js/-/log4js-2.5.3.tgz#38bb7bde5e9c1c181bd75e8bc128c5cd0409caf1"
@@ -5097,11 +5520,8 @@ loglevel@^1.4.1:
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.4.1.tgz#95b383f91a3c2756fd4ab093667e4309161f2bcd"
loglevelnext@^1.0.1:
- version "1.0.5"
- resolved "https://registry.yarnpkg.com/loglevelnext/-/loglevelnext-1.0.5.tgz#36fc4f5996d6640f539ff203ba819641680d75a2"
- dependencies:
- es6-symbol "^3.1.1"
- object.assign "^4.1.0"
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/loglevelnext/-/loglevelnext-1.0.3.tgz#0f69277e73bbbf2cd61b94d82313216bf87ac66e"
longest@^1.0.1:
version "1.0.1"
@@ -5170,6 +5590,12 @@ make-dir@^1.0.0:
dependencies:
pify "^2.3.0"
+make-dir@^1.1.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.2.0.tgz#6d6a49eead4aae296c53bbf3a1a008bd6c89469b"
+ dependencies:
+ pify "^3.0.0"
+
map-cache@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
@@ -5211,6 +5637,30 @@ media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
+mem-fs-editor@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/mem-fs-editor/-/mem-fs-editor-4.0.1.tgz#27e6b59df91b37248e9be2145b1bea84695103ed"
+ dependencies:
+ commondir "^1.0.1"
+ deep-extend "^0.5.1"
+ ejs "^2.5.9"
+ glob "^7.0.3"
+ globby "^8.0.0"
+ isbinaryfile "^3.0.2"
+ mkdirp "^0.5.0"
+ multimatch "^2.0.0"
+ rimraf "^2.2.8"
+ through2 "^2.0.0"
+ vinyl "^2.0.1"
+
+mem-fs@^1.1.0:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/mem-fs/-/mem-fs-1.1.3.tgz#b8ae8d2e3fcb6f5d3f9165c12d4551a065d989cc"
+ dependencies:
+ through2 "^2.0.0"
+ vinyl "^1.1.0"
+ vinyl-file "^2.0.0"
+
mem@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76"
@@ -5247,11 +5697,15 @@ merge-descriptors@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
+merge2@^1.2.1:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.2.2.tgz#03212e3da8d86c4d8523cebd6318193414f94e34"
+
methods@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
-micromatch@^2.1.5, micromatch@^2.3.11:
+micromatch@^2.1.5, micromatch@^2.3.7:
version "2.3.11"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565"
dependencies:
@@ -5269,6 +5723,24 @@ micromatch@^2.1.5, micromatch@^2.3.11:
parse-glob "^3.0.4"
regex-cache "^0.4.2"
+micromatch@^3.1.10, micromatch@^3.1.9:
+ version "3.1.10"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
+ dependencies:
+ arr-diff "^4.0.0"
+ array-unique "^0.3.2"
+ braces "^2.3.1"
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ extglob "^2.0.4"
+ fragment-cache "^0.2.1"
+ kind-of "^6.0.2"
+ nanomatch "^1.2.9"
+ object.pick "^1.3.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.2"
+
micromatch@^3.1.4:
version "3.1.6"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.6.tgz#8d7c043b48156f408ca07a4715182b79b99420bf"
@@ -5288,8 +5760,8 @@ micromatch@^3.1.4:
to-regex "^3.0.1"
micromatch@^3.1.8:
- version "3.1.10"
- resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
+ version "3.1.9"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.9.tgz#15dc93175ae39e52e93087847096effc73efcf89"
dependencies:
arr-diff "^4.0.0"
array-unique "^0.3.2"
@@ -5303,7 +5775,7 @@ micromatch@^3.1.8:
object.pick "^1.3.0"
regex-not "^1.0.0"
snapdragon "^0.8.1"
- to-regex "^3.0.2"
+ to-regex "^3.0.1"
miller-rabin@^4.0.0:
version "4.0.1"
@@ -5326,14 +5798,18 @@ mime@1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
-mime@^1.3.4, mime@^1.4.1, mime@^1.5.0:
+mime@^1.3.4:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
-mime@^2.1.0:
+mime@^2.0.3:
version "2.3.1"
resolved "https://registry.yarnpkg.com/mime/-/mime-2.3.1.tgz#b1621c54d63b97c47d3cfe7f7215f7d64517c369"
+mime@^2.1.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-2.2.0.tgz#161e541965551d3b549fa1114391e3a3d55b923b"
+
mimic-fn@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18"
@@ -5356,16 +5832,14 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
dependencies:
brace-expansion "^1.1.7"
-minimatch@3.0.3:
- version "3.0.3"
- resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774"
- dependencies:
- brace-expansion "^1.0.0"
-
minimist@0.0.8:
version "0.0.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
+minimist@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.1.0.tgz#99df657a52574c21c9057497df742790b2b4c0de"
+
minimist@^1.1.3, minimist@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
@@ -5444,6 +5918,15 @@ multicast-dns@^6.0.1:
dns-packet "^1.0.1"
thunky "^0.1.0"
+multimatch@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-2.1.0.tgz#9c7906a22fb4c02919e2f5f75161b4cdbd4b2a2b"
+ dependencies:
+ array-differ "^1.0.0"
+ array-union "^1.0.1"
+ arrify "^1.0.0"
+ minimatch "^3.0.0"
+
mute-stream@0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0"
@@ -5452,10 +5935,6 @@ mute-stream@0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
-name-all-modules-plugin@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/name-all-modules-plugin/-/name-all-modules-plugin-1.0.1.tgz#0abfb6ad835718b9fb4def0674e06657a954375c"
-
nan@^2.3.0:
version "2.8.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a"
@@ -5485,10 +5964,22 @@ negotiator@0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
+neo-async@^2.5.0:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.5.0.tgz#76b1c823130cca26acfbaccc8fbaf0a2fa33b18f"
+
netmask@~1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/netmask/-/netmask-1.0.6.tgz#20297e89d86f6f6400f250d9f4f6b4c1945fcd35"
+nice-try@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.4.tgz#d93962f6c52f2c1558c0fbda6d512819f1efe1c4"
+
+node-dir@0.1.8:
+ version "0.1.8"
+ resolved "https://registry.yarnpkg.com/node-dir/-/node-dir-0.1.8.tgz#55fb8deb699070707fb67f91a460f0448294c77d"
+
node-forge@0.6.33:
version "0.6.33"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.6.33.tgz#463811879f573d45155ad6a9f43dc296e8e85ebc"
@@ -5618,20 +6109,28 @@ nodemailer@^2.5.0:
nodemailer-smtp-transport "2.7.2"
socks "1.1.9"
-nodemon@^1.15.1:
- version "1.15.1"
- resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.15.1.tgz#54daa72443d8d5a548f130866b92e65cded0ed58"
+nodemon@^1.17.3:
+ version "1.17.3"
+ resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.17.3.tgz#3b0bbc2ee05ccb43b1aef15ba05c63c7bc9b8530"
dependencies:
chokidar "^2.0.2"
debug "^3.1.0"
ignore-by-default "^1.0.1"
minimatch "^3.0.4"
pstree.remy "^1.1.0"
- semver "^5.4.1"
+ semver "^5.5.0"
+ supports-color "^5.2.0"
touch "^3.1.0"
- undefsafe "^2.0.1"
+ undefsafe "^2.0.2"
update-notifier "^2.3.0"
+nomnom@^1.8.1:
+ version "1.8.1"
+ resolved "https://registry.yarnpkg.com/nomnom/-/nomnom-1.8.1.tgz#2151f722472ba79e50a76fc125bb8c8f2e4dc2a7"
+ dependencies:
+ chalk "~0.4.0"
+ underscore "~1.6.0"
+
nopt@3.x:
version "3.0.6"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
@@ -5734,7 +6233,7 @@ object-copy@^0.1.0:
define-property "^0.2.5"
kind-of "^3.0.3"
-object-keys@^1.0.11, object-keys@^1.0.8:
+object-keys@^1.0.8:
version "1.0.11"
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d"
@@ -5744,15 +6243,6 @@ object-visit@^1.0.0:
dependencies:
isobject "^3.0.0"
-object.assign@^4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
- dependencies:
- define-properties "^1.1.2"
- function-bind "^1.1.1"
- has-symbols "^1.0.0"
- object-keys "^1.0.11"
-
object.omit@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa"
@@ -5800,7 +6290,7 @@ opener@^1.4.3:
version "1.4.3"
resolved "https://registry.yarnpkg.com/opener/-/opener-1.4.3.tgz#5c6da2c5d7e5831e8ffa3964950f8d6674ac90b8"
-opn@5.2.0, opn@^5.1.0:
+opn@^5.1.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/opn/-/opn-5.2.0.tgz#71fdf934d6827d676cecbea1531f95d354641225"
dependencies:
@@ -5824,6 +6314,15 @@ optionator@^0.8.1, optionator@^0.8.2:
type-check "~0.3.2"
wordwrap "~1.0.0"
+ora@^0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/ora/-/ora-0.2.3.tgz#37527d220adcd53c39b73571d754156d5db657a4"
+ dependencies:
+ chalk "^1.1.1"
+ cli-cursor "^1.0.2"
+ cli-spinners "^0.1.2"
+ object-assign "^4.0.1"
+
original@>=0.0.5:
version "1.0.0"
resolved "https://registry.yarnpkg.com/original/-/original-1.0.0.tgz#9147f93fa1696d04be61e01bd50baeaca656bd3b"
@@ -5838,12 +6337,6 @@ os-homedir@^1.0.0, os-homedir@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
-os-locale@^1.4.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9"
- dependencies:
- lcid "^1.0.0"
-
os-locale@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2"
@@ -5863,10 +6356,20 @@ osenv@^0.1.4:
os-homedir "^1.0.0"
os-tmpdir "^1.0.0"
+p-cancelable@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.3.0.tgz#b9e123800bcebb7ac13a479be195b507b98d30fa"
+
p-cancelable@^0.4.0:
version "0.4.1"
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.4.1.tgz#35f363d67d52081c8d9585e37bcceb7e0bbcb2a0"
+p-each-series@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-1.0.0.tgz#930f3d12dd1f50e7434457a22cd6f04ac6ad7f71"
+ dependencies:
+ p-reduce "^1.0.0"
+
p-finally@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
@@ -5875,6 +6378,10 @@ p-is-promise@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e"
+p-lazy@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-lazy/-/p-lazy-1.0.0.tgz#ec53c802f2ee3ac28f166cc82d0b2b02de27a835"
+
p-limit@^1.0.0, p-limit@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.2.0.tgz#0e92b6bedcb59f022c13d0f1949dc82d15909f1c"
@@ -5891,6 +6398,16 @@ p-map@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.1.1.tgz#05f5e4ae97a068371bc2a5cc86bfbdbc19c4ae7a"
+p-reduce@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-1.0.0.tgz#18c2b0dd936a4690a529f8231f58a0fdb6a47dfa"
+
+p-timeout@^1.1.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-1.2.1.tgz#5eb3b353b7fce99f101a1038880bb054ebbea386"
+ dependencies:
+ p-finally "^1.0.0"
+
p-timeout@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-2.0.1.tgz#d8dd1979595d2dc0139e1fe46b8b646cb3cdf038"
@@ -5975,6 +6492,13 @@ parse-json@^2.2.0:
dependencies:
error-ex "^1.2.0"
+parse-json@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
+ dependencies:
+ error-ex "^1.3.1"
+ json-parse-better-errors "^1.0.1"
+
parse-passwd@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
@@ -6025,7 +6549,7 @@ path-is-inside@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
-path-key@^2.0.0:
+path-key@^2.0.0, path-key@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
@@ -6051,12 +6575,6 @@ path-type@^1.0.0:
pify "^2.0.0"
pinkie-promise "^2.0.0"
-path-type@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73"
- dependencies:
- pify "^2.0.0"
-
path-type@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
@@ -6442,9 +6960,17 @@ prettier@1.11.1:
version "1.11.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.11.1.tgz#61e43fc4cd44e68f2b0dfc2c38cd4bb0fccdcc75"
+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.12.1"
- resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.12.1.tgz#c1ad20e803e7749faf905a409d2367e06bbe7325"
+ 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"
prismjs@^1.6.0:
version "1.6.0"
@@ -6452,11 +6978,11 @@ prismjs@^1.6.0:
optionalDependencies:
clipboard "^1.5.5"
-private@^0.1.6, private@^0.1.7:
+private@^0.1.6, private@^0.1.8, private@~0.1.5:
version "0.1.8"
resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
-process-nextick-args@~1.0.6:
+process-nextick-args@^1.0.6, process-nextick-args@~1.0.6:
version "1.0.7"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3"
@@ -6500,6 +7026,10 @@ prr@~0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a"
+prr@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
+
ps-tree@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/ps-tree/-/ps-tree-1.1.0.tgz#b421b24140d6203f1ed3c76996b4427b08e8c014"
@@ -6660,32 +7190,12 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.1.7:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
-react-dev-utils@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-5.0.0.tgz#425ac7c9c40c2603bc4f7ab8836c1406e96bb473"
- dependencies:
- address "1.0.3"
- babel-code-frame "6.26.0"
- chalk "1.1.3"
- cross-spawn "5.1.0"
- detect-port-alt "1.1.5"
- escape-string-regexp "1.0.5"
- filesize "3.5.11"
- global-modules "1.0.0"
- gzip-size "3.0.0"
- inquirer "3.3.0"
- is-root "1.0.0"
- opn "5.2.0"
- react-error-overlay "^4.0.0"
- recursive-readdir "2.2.1"
- shell-quote "1.6.1"
- sockjs-client "1.1.4"
- strip-ansi "3.0.1"
- text-table "0.2.0"
-
-react-error-overlay@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-4.0.0.tgz#d198408a85b4070937a98667f500c832f86bd5d4"
+read-chunk@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/read-chunk/-/read-chunk-2.1.0.tgz#6a04c0928005ed9d42e1a6ac5600e19cbc7ff655"
+ dependencies:
+ pify "^3.0.0"
+ safe-buffer "^5.1.1"
read-pkg-up@^1.0.1:
version "1.0.1"
@@ -6694,12 +7204,12 @@ read-pkg-up@^1.0.1:
find-up "^1.0.0"
read-pkg "^1.0.0"
-read-pkg-up@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be"
+read-pkg-up@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07"
dependencies:
find-up "^2.0.0"
- read-pkg "^2.0.0"
+ read-pkg "^3.0.0"
read-pkg@^1.0.0:
version "1.1.0"
@@ -6709,13 +7219,13 @@ read-pkg@^1.0.0:
normalize-package-data "^2.3.2"
path-type "^1.0.0"
-read-pkg@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8"
+read-pkg@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389"
dependencies:
- load-json-file "^2.0.0"
+ load-json-file "^4.0.0"
normalize-package-data "^2.3.2"
- path-type "^2.0.0"
+ path-type "^3.0.0"
"readable-stream@1 || 2", readable-stream@2, readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.0, readable-stream@^2.3.3:
version "2.3.4"
@@ -6766,18 +7276,31 @@ readline2@^1.0.1:
is-fullwidth-code-point "^1.0.0"
mute-stream "0.0.5"
+recast@^0.12.5:
+ version "0.12.9"
+ resolved "https://registry.yarnpkg.com/recast/-/recast-0.12.9.tgz#e8e52bdb9691af462ccbd7c15d5a5113647a15f1"
+ dependencies:
+ ast-types "0.10.1"
+ core-js "^2.4.1"
+ esprima "~4.0.0"
+ private "~0.1.5"
+ source-map "~0.6.1"
+
+recast@^0.14.1:
+ version "0.14.7"
+ resolved "https://registry.yarnpkg.com/recast/-/recast-0.14.7.tgz#4f1497c2b5826d42a66e8e3c9d80c512983ff61d"
+ dependencies:
+ ast-types "0.11.3"
+ esprima "~4.0.0"
+ private "~0.1.5"
+ source-map "~0.6.1"
+
rechoir@^0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"
dependencies:
resolve "^1.1.6"
-recursive-readdir@2.2.1:
- version "2.2.1"
- resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.1.tgz#90ef231d0778c5ce093c9a48d74e5c5422d13a99"
- dependencies:
- minimatch "3.0.3"
-
redent@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde"
@@ -6905,6 +7428,14 @@ repeating@^2.0.0:
dependencies:
is-finite "^1.0.0"
+replace-ext@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-0.0.1.tgz#29bbd92078a739f0bcce2b4ee41e837953522924"
+
+replace-ext@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb"
+
request@2.75.x:
version "2.75.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.75.0.tgz#d2b8268a286da13eaa5d01adf5d18cc90f657d93"
@@ -7092,12 +7623,22 @@ right-align@^0.1.1:
dependencies:
align-text "^0.1.1"
-rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.2:
+rimraf@2, rimraf@^2.5.1, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.2:
version "2.6.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
dependencies:
glob "^7.0.5"
+rimraf@^2.2.8:
+ version "2.6.1"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d"
+ dependencies:
+ glob "^7.0.5"
+
+rimraf@~2.2.6:
+ version "2.2.8"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.2.8.tgz#e439be2aaee327321952730f99a8929e4fc50582"
+
ripemd160@^2.0.0, ripemd160@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7"
@@ -7111,7 +7652,7 @@ run-async@^0.1.0:
dependencies:
once "^1.3.0"
-run-async@^2.2.0:
+run-async@^2.0.0, run-async@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"
dependencies:
@@ -7137,6 +7678,12 @@ rx-lite@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102"
+rxjs@^5.4.2, rxjs@^5.5.2:
+ version "5.5.10"
+ resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.10.tgz#fde02d7a614f6c8683d0d1957827f492e09db045"
+ dependencies:
+ symbol-observable "1.0.1"
+
safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
@@ -7163,19 +7710,17 @@ sax@~1.2.1:
version "1.2.2"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.2.tgz#fd8631a23bc7826bef5d871bdb87378c95647828"
-schema-utils@^0.3.0:
- version "0.3.0"
- resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.3.0.tgz#f5877222ce3e931edae039f17eb3716e7137f8cf"
- dependencies:
- ajv "^5.0.0"
-
-schema-utils@^0.4.3, schema-utils@^0.4.5:
+schema-utils@^0.4.0, 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:
ajv "^6.1.0"
ajv-keywords "^3.1.0"
+scoped-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/scoped-regex/-/scoped-regex-1.0.0.tgz#a346bb1acd4207ae70bd7c0c7ca9e566b6baddb8"
+
select-hose@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
@@ -7200,7 +7745,11 @@ semver-diff@^2.0.0:
dependencies:
semver "^5.0.3"
-"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1:
+"semver@2 || 3 || 4 || 5", semver@^5.0.3:
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
+
+semver@^5.1.0, semver@^5.3.0, semver@^5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab"
@@ -7302,6 +7851,13 @@ sha.js@^2.4.0, sha.js@^2.4.8:
inherits "^2.0.1"
safe-buffer "^5.0.1"
+sha1@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/sha1/-/sha1-1.1.1.tgz#addaa7a93168f393f19eb2b15091618e2700f848"
+ dependencies:
+ charenc ">= 0.0.1"
+ crypt ">= 0.0.1"
+
shebang-command@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
@@ -7312,15 +7868,6 @@ shebang-regex@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
-shell-quote@1.6.1:
- version "1.6.1"
- resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.6.1.tgz#f4781949cce402697127430ea3b3c5476f481767"
- dependencies:
- array-filter "~0.0.0"
- array-map "~0.0.0"
- array-reduce "~0.0.0"
- jsonify "~0.0.0"
-
shelljs@^0.7.5:
version "0.7.8"
resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.8.tgz#decbcf874b0d1e5fb72e14b164a9683048e9acb3"
@@ -7329,6 +7876,14 @@ shelljs@^0.7.5:
interpret "^1.0.0"
rechoir "^0.6.2"
+shelljs@^0.8.0:
+ version "0.8.1"
+ resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.1.tgz#729e038c413a2254c4078b95ed46e0397154a9f1"
+ dependencies:
+ glob "^7.0.0"
+ interpret "^1.0.0"
+ rechoir "^0.6.2"
+
signal-exit@^3.0.0, signal-exit@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
@@ -7347,6 +7902,10 @@ slice-ansi@0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35"
+slide@^1.1.5:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707"
+
smart-buffer@^1.0.13, smart-buffer@^1.0.4:
version "1.1.15"
resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-1.1.15.tgz#7f114b5b65fab3e2a35aa775bb12f0d1c649bf16"
@@ -7528,7 +8087,11 @@ source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1:
version "0.5.6"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
-source-map@^0.6.1:
+source-map@^0.5.7, source-map@~0.5.3, source-map@~0.5.6:
+ version "0.5.7"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
+
+source-map@^0.6.1, source-map@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
@@ -7538,10 +8101,6 @@ source-map@~0.2.0:
dependencies:
amdefine ">=0.0.4"
-source-map@~0.5.3, source-map@~0.5.6:
- version "0.5.7"
- resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
-
spdx-correct@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40"
@@ -7675,6 +8234,12 @@ stream-shift@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952"
+stream-to-observable@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/stream-to-observable/-/stream-to-observable-0.2.0.tgz#59d6ea393d87c2c0ddac10aa0d561bc6ba6f0e10"
+ dependencies:
+ any-observable "^0.2.0"
+
streamroller@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-0.7.0.tgz#a1d1b7cf83d39afb0d63049a5acbf93493bdf64b"
@@ -7688,6 +8253,10 @@ strict-uri-encode@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
+string-template@~0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add"
+
string-width@^1.0.1, string-width@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
@@ -7717,7 +8286,7 @@ stringstream@~0.0.4, stringstream@~0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
-strip-ansi@3.0.1, strip-ansi@^3.0.0, strip-ansi@^3.0.1:
+strip-ansi@^3.0.0, strip-ansi@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
dependencies:
@@ -7729,6 +8298,17 @@ strip-ansi@^4.0.0:
dependencies:
ansi-regex "^3.0.0"
+strip-ansi@~0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.1.1.tgz#39e8a98d044d150660abe4a6808acf70bb7bc991"
+
+strip-bom-stream@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom-stream/-/strip-bom-stream-2.0.0.tgz#f87db5ef2613f6968aa545abfe1ec728b6a829ca"
+ dependencies:
+ first-chunk-stream "^2.0.0"
+ strip-bom "^2.0.0"
+
strip-bom@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"
@@ -7753,12 +8333,12 @@ strip-json-comments@~2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
-style-loader@^0.20.2:
- version "0.20.2"
- resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.20.2.tgz#851b373c187890331776e9cde359eea9c95ecd00"
+style-loader@^0.21.0:
+ version "0.21.0"
+ resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.21.0.tgz#68c52e5eb2afc9ca92b6274be277ee59aea3a852"
dependencies:
loader-utils "^1.1.0"
- schema-utils "^0.4.3"
+ schema-utils "^0.4.5"
supports-color@^2.0.0:
version "2.0.0"
@@ -7770,13 +8350,13 @@ supports-color@^3.1.0, supports-color@^3.1.2, supports-color@^3.2.3:
dependencies:
has-flag "^1.0.0"
-supports-color@^4.2.1:
- version "4.5.0"
- resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b"
+supports-color@^5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.1.0.tgz#058a021d1b619f7ddf3980d712ea3590ce7de3d5"
dependencies:
has-flag "^2.0.0"
-supports-color@^5.1.0, supports-color@^5.2.0:
+supports-color@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.2.0.tgz#b0d5333b1184dd3666cbe5aa0b45c5ac7ac17a4a"
dependencies:
@@ -7804,6 +8384,14 @@ svgo@^0.7.0:
sax "~1.2.1"
whet.extend "~0.9.9"
+symbol-observable@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4"
+
+symbol-observable@^0.2.2:
+ version "0.2.4"
+ resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-0.2.4.tgz#95a83db26186d6af7e7a18dbd9760a2f86d08f40"
+
table@^3.7.8:
version "3.8.3"
resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f"
@@ -7819,9 +8407,9 @@ tapable@^0.1.8:
version "0.1.10"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.1.10.tgz#29c35707c2b70e50d07482b5d202e8ed446dafd4"
-tapable@^0.2.7:
- version "0.2.8"
- resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.8.tgz#99372a5c999bf2df160afc0d74bed4f47948cd22"
+tapable@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.0.0.tgz#cbb639d9002eed9c6b5975eb20598d7936f1f9f2"
tar-pack@^3.4.0:
version "3.4.1"
@@ -7844,6 +8432,13 @@ tar@^2.2.1:
fstream "^1.0.2"
inherits "2"
+temp@^0.8.1:
+ version "0.8.3"
+ resolved "https://registry.yarnpkg.com/temp/-/temp-0.8.3.tgz#e0c6bc4d26b903124410e4fed81103014dfc1f59"
+ dependencies:
+ os-tmpdir "^1.0.0"
+ rimraf "~2.2.6"
+
term-size@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/term-size/-/term-size-1.2.0.tgz#458b83887f288fc56d6fffbfad262e26638efa69"
@@ -7860,10 +8455,14 @@ test-exclude@^4.2.1:
read-pkg-up "^1.0.1"
require-main-filename "^1.0.1"
-text-table@0.2.0, text-table@~0.2.0:
+text-table@^0.2.0, text-table@~0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
+textextensions@2:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-2.2.0.tgz#38ac676151285b658654581987a0ce1a4490d286"
+
three-orbit-controls@^82.1.0:
version "82.1.0"
resolved "https://registry.yarnpkg.com/three-orbit-controls/-/three-orbit-controls-82.1.0.tgz#11a7f33d0a20ecec98f098b37780f6537374fab4"
@@ -7876,7 +8475,7 @@ three@^0.84.0:
version "0.84.0"
resolved "https://registry.yarnpkg.com/three/-/three-0.84.0.tgz#95be85a55a0fa002aa625ed559130957dcffd918"
-through2@^2.0.0:
+through2@^2.0.0, through2@^2.0.1:
version "2.0.3"
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be"
dependencies:
@@ -7895,10 +8494,6 @@ thunky@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/thunky/-/thunky-0.1.0.tgz#bf30146824e2b6e67b0f2d7a4ac8beb26908684e"
-time-stamp@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-2.0.0.tgz#95c6a44530e15ba8d6f4a3ecb8c3a3fac46da357"
-
timeago.js@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/timeago.js/-/timeago.js-3.0.2.tgz#32a67e7c0d887ea42ca588d3aae26f77de5e76cc"
@@ -8052,7 +8647,14 @@ typedarray@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
-uglify-js@^2.6, uglify-js@^2.8.29:
+uglify-es@^3.3.4:
+ version "3.3.9"
+ resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677"
+ dependencies:
+ commander "~2.13.0"
+ source-map "~0.6.1"
+
+uglify-js@^2.6:
version "2.8.29"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd"
dependencies:
@@ -8065,13 +8667,18 @@ uglify-to-browserify@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7"
-uglifyjs-webpack-plugin@^0.4.6:
- version "0.4.6"
- resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz#b951f4abb6bd617e66f63eb891498e391763e309"
+uglifyjs-webpack-plugin@^1.2.4:
+ version "1.2.5"
+ resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.2.5.tgz#2ef8387c8f1a903ec5e44fa36f9f3cbdcea67641"
dependencies:
- source-map "^0.5.6"
- uglify-js "^2.8.29"
- webpack-sources "^1.0.1"
+ cacache "^10.0.4"
+ find-cache-dir "^1.0.0"
+ schema-utils "^0.4.5"
+ serialize-javascript "^1.4.0"
+ source-map "^0.6.1"
+ uglify-es "^3.3.4"
+ webpack-sources "^1.1.0"
+ worker-farm "^1.5.2"
uid-number@^0.0.6:
version "0.0.6"
@@ -8085,15 +8692,19 @@ unc-path-regex@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa"
-undefsafe@^2.0.1:
+undefsafe@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.2.tgz#225f6b9e0337663e0d8e7cfd686fc2836ccace76"
dependencies:
debug "^2.2.0"
-underscore@^1.8.3:
- version "1.8.3"
- resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022"
+underscore@^1.9.0:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.0.tgz#31dbb314cfcc88f169cd3692d9149d81a00a73e4"
+
+underscore@~1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8"
underscore@~1.7.0:
version "1.7.0"
@@ -8151,18 +8762,17 @@ unset-value@^1.0.0:
has-value "^0.3.1"
isobject "^3.0.0"
+untildify@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/untildify/-/untildify-3.0.2.tgz#7f1f302055b3fea0f3e81dc78eb36766cb65e3f1"
+
unzip-response@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97"
upath@^1.0.0:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/upath/-/upath-1.0.2.tgz#80aaae5395abc5fd402933ae2f58694f0860204c"
- dependencies:
- lodash.endswith "^4.2.1"
- lodash.isfunction "^3.0.8"
- lodash.isstring "^4.0.1"
- lodash.startswith "^4.2.1"
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/upath/-/upath-1.0.5.tgz#02cab9ecebe95bbec6d5fc2566325725ab6d1a73"
update-notifier@^2.3.0:
version "2.3.0"
@@ -8186,13 +8796,17 @@ url-join@^2.0.2:
version "2.0.5"
resolved "https://registry.yarnpkg.com/url-join/-/url-join-2.0.5.tgz#5af22f18c052a000a48d7b82c5e9c2e2feeda728"
-url-loader@^0.6.2:
- version "0.6.2"
- resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-0.6.2.tgz#a007a7109620e9d988d14bce677a1decb9a993f7"
+url-join@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.0.tgz#4d3340e807d3773bda9991f8305acdcc2a665d2a"
+
+url-loader@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-1.0.1.tgz#61bc53f1f184d7343da2728a1289ef8722ea45ee"
dependencies:
- loader-utils "^1.0.2"
- mime "^1.4.1"
- schema-utils "^0.3.0"
+ loader-utils "^1.1.0"
+ mime "^2.0.3"
+ schema-utils "^0.4.3"
url-parse-lax@^1.0.0:
version "1.0.0"
@@ -8274,6 +8888,10 @@ uws@~9.14.0:
version "9.14.0"
resolved "https://registry.yarnpkg.com/uws/-/uws-9.14.0.tgz#fac8386befc33a7a3705cbd58dc47b430ca4dd95"
+v8-compile-cache@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-1.1.2.tgz#8d32e4f16974654657e676e0e467a348e89b0dc4"
+
validate-npm-package-license@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc"
@@ -8301,6 +8919,36 @@ verror@1.10.0:
core-util-is "1.0.2"
extsprintf "^1.2.0"
+vinyl-file@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/vinyl-file/-/vinyl-file-2.0.0.tgz#a7ebf5ffbefda1b7d18d140fcb07b223efb6751a"
+ dependencies:
+ graceful-fs "^4.1.2"
+ pify "^2.3.0"
+ pinkie-promise "^2.0.0"
+ strip-bom "^2.0.0"
+ strip-bom-stream "^2.0.0"
+ vinyl "^1.1.0"
+
+vinyl@^1.1.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-1.2.0.tgz#5c88036cf565e5df05558bfc911f8656df218884"
+ dependencies:
+ clone "^1.0.0"
+ clone-stats "^0.0.1"
+ replace-ext "0.0.1"
+
+vinyl@^2.0.1:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.1.0.tgz#021f9c2cf951d6b939943c89eb5ee5add4fd924c"
+ dependencies:
+ clone "^2.1.1"
+ clone-buffer "^1.0.0"
+ clone-stats "^1.0.0"
+ cloneable-readable "^1.0.0"
+ remove-trailing-separator "^1.0.1"
+ replace-ext "^1.0.0"
+
visibilityjs@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/visibilityjs/-/visibilityjs-1.2.4.tgz#bff8663da62c8c10ad4ee5ae6a1ae6fac4259d63"
@@ -8388,13 +9036,13 @@ vuex@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.0.1.tgz#e761352ebe0af537d4bb755a9b9dc4be3df7efd2"
-watchpack@^1.4.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.4.0.tgz#4a1472bcbb952bd0a9bb4036801f954dfb39faac"
+watchpack@^1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.5.0.tgz#231e783af830a22f8966f65c4c4bacc814072eed"
dependencies:
- async "^2.1.2"
- chokidar "^1.7.0"
+ chokidar "^2.0.2"
graceful-fs "^4.1.2"
+ neo-async "^2.5.0"
wbuf@^1.1.0, wbuf@^1.7.2:
version "1.7.2"
@@ -8402,9 +9050,15 @@ wbuf@^1.1.0, wbuf@^1.7.2:
dependencies:
minimalistic-assert "^1.0.0"
-webpack-bundle-analyzer@^2.10.0:
- version "2.10.0"
- resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-2.10.0.tgz#d0646cda342939f6f05eb632a090abbd90317446"
+webpack-addons@^1.1.5:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/webpack-addons/-/webpack-addons-1.1.5.tgz#2b178dfe873fb6e75e40a819fa5c26e4a9bc837a"
+ dependencies:
+ jscodeshift "^0.4.0"
+
+webpack-bundle-analyzer@^2.11.1:
+ version "2.11.1"
+ resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-2.11.1.tgz#b9fbfb6a32c0a8c1c3237223e90890796b950ab9"
dependencies:
acorn "^5.3.0"
bfj-node4 "^5.2.0"
@@ -8419,15 +9073,48 @@ webpack-bundle-analyzer@^2.10.0:
opener "^1.4.3"
ws "^4.0.0"
-webpack-dev-middleware@1.12.2:
- version "1.12.2"
- resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.12.2.tgz#f8fc1120ce3b4fc5680ceecb43d777966b21105e"
+webpack-cli@^2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-2.1.2.tgz#9c9a4b90584f7b8acaf591238ef0667e04c817f6"
dependencies:
+ chalk "^2.3.2"
+ cross-spawn "^6.0.5"
+ diff "^3.5.0"
+ enhanced-resolve "^4.0.0"
+ envinfo "^4.4.2"
+ glob-all "^3.1.0"
+ global-modules "^1.0.0"
+ got "^8.2.0"
+ import-local "^1.0.0"
+ inquirer "^5.1.0"
+ interpret "^1.0.4"
+ jscodeshift "^0.5.0"
+ listr "^0.13.0"
+ loader-utils "^1.1.0"
+ lodash "^4.17.5"
+ log-symbols "^2.2.0"
+ mkdirp "^0.5.1"
+ p-each-series "^1.0.0"
+ p-lazy "^1.0.0"
+ prettier "^1.5.3"
+ supports-color "^5.3.0"
+ v8-compile-cache "^1.1.2"
+ webpack-addons "^1.1.5"
+ yargs "^11.1.0"
+ yeoman-environment "^2.0.0"
+ yeoman-generator "^2.0.4"
+
+webpack-dev-middleware@3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.1.3.tgz#8b32aa43da9ae79368c1bf1183f2b6cf5e1f39ed"
+ dependencies:
+ loud-rejection "^1.6.0"
memory-fs "~0.4.1"
- mime "^1.5.0"
+ mime "^2.1.0"
path-is-absolute "^1.0.0"
range-parser "^1.0.3"
- time-stamp "^2.0.0"
+ url-join "^4.0.0"
+ webpack-log "^1.0.1"
webpack-dev-middleware@^2.0.6:
version "2.0.6"
@@ -8441,9 +9128,9 @@ webpack-dev-middleware@^2.0.6:
url-join "^2.0.2"
webpack-log "^1.0.1"
-webpack-dev-server@^2.11.2:
- version "2.11.2"
- resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.11.2.tgz#1f4f4c78bf1895378f376815910812daf79a216f"
+webpack-dev-server@^3.1.4:
+ version "3.1.4"
+ resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.1.4.tgz#9a08d13c4addd1e3b6d8ace116e86715094ad5b4"
dependencies:
ansi-html "0.0.7"
array-includes "^3.0.3"
@@ -8455,7 +9142,7 @@ webpack-dev-server@^2.11.2:
del "^3.0.0"
express "^4.16.2"
html-entities "^1.2.0"
- http-proxy-middleware "~0.17.4"
+ http-proxy-middleware "~0.18.0"
import-local "^1.0.0"
internal-ip "1.2.0"
ip "^1.1.5"
@@ -8470,8 +9157,9 @@ webpack-dev-server@^2.11.2:
spdy "^3.4.1"
strip-ansi "^3.0.0"
supports-color "^5.1.0"
- webpack-dev-middleware "1.12.2"
- yargs "6.6.0"
+ webpack-dev-middleware "3.1.3"
+ webpack-log "^1.1.2"
+ yargs "11.0.0"
webpack-log@^1.0.1:
version "1.2.0"
@@ -8482,6 +9170,15 @@ webpack-log@^1.0.1:
loglevelnext "^1.0.1"
uuid "^3.1.0"
+webpack-log@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-1.1.2.tgz#cdc76016537eed24708dc6aa3d1e52189efee107"
+ dependencies:
+ chalk "^2.1.0"
+ log-symbols "^2.1.0"
+ loglevelnext "^1.0.1"
+ uuid "^3.1.0"
+
webpack-sources@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.0.1.tgz#c7356436a4d13123be2e2426a05d1dad9cbe65cf"
@@ -8489,36 +9186,40 @@ webpack-sources@^1.0.1:
source-list-map "^2.0.0"
source-map "~0.5.3"
-webpack-stats-plugin@^0.1.5:
- version "0.1.5"
- resolved "https://registry.yarnpkg.com/webpack-stats-plugin/-/webpack-stats-plugin-0.1.5.tgz#29e5f12ebfd53158d31d656a113ac1f7b86179d9"
+webpack-sources@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.1.0.tgz#a101ebae59d6507354d71d8013950a3a8b7a5a54"
+ dependencies:
+ source-list-map "^2.0.0"
+ source-map "~0.6.1"
-webpack@^3.11.0:
- version "3.11.0"
- resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.11.0.tgz#77da451b1d7b4b117adaf41a1a93b5742f24d894"
+webpack-stats-plugin@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/webpack-stats-plugin/-/webpack-stats-plugin-0.2.1.tgz#1f5bac13fc25d62cbb5fd0ff646757dc802b8595"
+
+webpack@^4.7.0:
+ version "4.7.0"
+ resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.7.0.tgz#a04f68dab86d5545fd0277d07ffc44e4078154c9"
dependencies:
acorn "^5.0.0"
- acorn-dynamic-import "^2.0.0"
+ acorn-dynamic-import "^3.0.0"
ajv "^6.1.0"
ajv-keywords "^3.1.0"
- async "^2.1.2"
- enhanced-resolve "^3.4.0"
- escope "^3.6.0"
- interpret "^1.0.0"
- json-loader "^0.5.4"
- json5 "^0.5.1"
+ chrome-trace-event "^0.1.1"
+ enhanced-resolve "^4.0.0"
+ eslint-scope "^3.7.1"
loader-runner "^2.3.0"
loader-utils "^1.1.0"
memory-fs "~0.4.1"
+ micromatch "^3.1.8"
mkdirp "~0.5.0"
+ neo-async "^2.5.0"
node-libs-browser "^2.0.0"
- source-map "^0.5.3"
- supports-color "^4.2.1"
- tapable "^0.2.7"
- uglifyjs-webpack-plugin "^0.4.6"
- watchpack "^1.4.0"
+ schema-utils "^0.4.4"
+ tapable "^1.0.0"
+ uglifyjs-webpack-plugin "^1.2.4"
+ watchpack "^1.5.0"
webpack-sources "^1.0.1"
- yargs "^8.0.2"
websocket-driver@>=0.5.1:
version "0.6.5"
@@ -8538,10 +9239,6 @@ whet.extend@~0.9.9:
version "0.9.9"
resolved "https://registry.yarnpkg.com/whet.extend/-/whet.extend-0.9.9.tgz#f877d5bf648c97e5aa542fadc16d6a259b9c11a1"
-which-module@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f"
-
which-module@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
@@ -8580,12 +9277,19 @@ wordwrap@~0.0.2:
version "0.0.3"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
-worker-loader@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-1.1.0.tgz#8cf21869a07add84d66f821d948d23c1eb98e809"
+worker-farm@^1.5.2:
+ version "1.5.2"
+ resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.5.2.tgz#32b312e5dc3d5d45d79ef44acc2587491cd729ae"
+ dependencies:
+ errno "^0.1.4"
+ xtend "^4.0.1"
+
+worker-loader@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-1.1.1.tgz#920d74ddac6816fc635392653ed8b4af1929fd92"
dependencies:
loader-utils "^1.0.0"
- schema-utils "^0.3.0"
+ schema-utils "^0.4.0"
wrap-ansi@^2.0.0:
version "2.1.0"
@@ -8598,6 +9302,14 @@ wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+write-file-atomic@^1.2.0:
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-1.3.4.tgz#f807a4f0b1d9e913ae7a48112e6cc3af1991b45f"
+ dependencies:
+ graceful-fs "^4.1.11"
+ imurmurhash "^0.1.4"
+ slide "^1.1.5"
+
write-file-atomic@^2.0.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.3.0.tgz#1ff61575c2e2a4e8e510d6fa4e243cce183999ab"
@@ -8640,7 +9352,7 @@ xregexp@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943"
-xtend@^4.0.0, xtend@~4.0.1:
+xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.0, xtend@~4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
@@ -8656,53 +9368,51 @@ yallist@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
-yargs-parser@^4.2.0:
- version "4.2.1"
- resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-4.2.1.tgz#29cceac0dc4f03c6c87b4a9f217dd18c9f74871c"
- dependencies:
- camelcase "^3.0.0"
-
-yargs-parser@^7.0.0:
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9"
+yargs-parser@^9.0.2:
+ version "9.0.2"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-9.0.2.tgz#9ccf6a43460fe4ed40a9bb68f48d43b8a68cc077"
dependencies:
camelcase "^4.1.0"
-yargs@6.6.0:
- version "6.6.0"
- resolved "https://registry.yarnpkg.com/yargs/-/yargs-6.6.0.tgz#782ec21ef403345f830a808ca3d513af56065208"
+yargs@11.0.0:
+ version "11.0.0"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-11.0.0.tgz#c052931006c5eee74610e5fc0354bedfd08a201b"
dependencies:
- camelcase "^3.0.0"
- cliui "^3.2.0"
+ cliui "^4.0.0"
decamelize "^1.1.1"
+ find-up "^2.1.0"
get-caller-file "^1.0.1"
- os-locale "^1.4.0"
- read-pkg-up "^1.0.1"
+ os-locale "^2.0.0"
require-directory "^2.1.1"
require-main-filename "^1.0.1"
set-blocking "^2.0.0"
- string-width "^1.0.2"
- which-module "^1.0.0"
+ string-width "^2.0.0"
+ which-module "^2.0.0"
y18n "^3.2.1"
- yargs-parser "^4.2.0"
+ yargs-parser "^9.0.2"
-yargs@^8.0.2:
- version "8.0.2"
- resolved "https://registry.yarnpkg.com/yargs/-/yargs-8.0.2.tgz#6299a9055b1cefc969ff7e79c1d918dceb22c360"
+yargs@^11.1.0:
+ version "11.1.0"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-11.1.0.tgz#90b869934ed6e871115ea2ff58b03f4724ed2d77"
dependencies:
- camelcase "^4.1.0"
- cliui "^3.2.0"
+ cliui "^4.0.0"
decamelize "^1.1.1"
+ find-up "^2.1.0"
get-caller-file "^1.0.1"
os-locale "^2.0.0"
- read-pkg-up "^2.0.0"
require-directory "^2.1.1"
require-main-filename "^1.0.1"
set-blocking "^2.0.0"
string-width "^2.0.0"
which-module "^2.0.0"
y18n "^3.2.1"
- yargs-parser "^7.0.0"
+ yargs-parser "^9.0.2"
+
+yargs@~1.2.6:
+ version "1.2.6"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-1.2.6.tgz#9c7b4a82fd5d595b2bf17ab6dcc43135432fe34b"
+ dependencies:
+ minimist "^0.1.0"
yargs@~3.10.0:
version "3.10.0"
@@ -8716,3 +9426,69 @@ yargs@~3.10.0:
yeast@0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"
+
+yeoman-environment@^2.0.0:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/yeoman-environment/-/yeoman-environment-2.0.5.tgz#84f22bafa84088971fe99ea85f654a3a3dd2b693"
+ dependencies:
+ chalk "^2.1.0"
+ debug "^3.1.0"
+ diff "^3.3.1"
+ escape-string-regexp "^1.0.2"
+ globby "^6.1.0"
+ grouped-queue "^0.3.3"
+ inquirer "^3.3.0"
+ is-scoped "^1.0.0"
+ lodash "^4.17.4"
+ log-symbols "^2.1.0"
+ mem-fs "^1.1.0"
+ text-table "^0.2.0"
+ untildify "^3.0.2"
+
+yeoman-environment@^2.0.5:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/yeoman-environment/-/yeoman-environment-2.0.6.tgz#ae1b21d826b363f3d637f88a7fc9ea7414cb5377"
+ dependencies:
+ chalk "^2.1.0"
+ debug "^3.1.0"
+ diff "^3.3.1"
+ escape-string-regexp "^1.0.2"
+ globby "^6.1.0"
+ grouped-queue "^0.3.3"
+ inquirer "^3.3.0"
+ is-scoped "^1.0.0"
+ lodash "^4.17.4"
+ log-symbols "^2.1.0"
+ mem-fs "^1.1.0"
+ text-table "^0.2.0"
+ untildify "^3.0.2"
+
+yeoman-generator@^2.0.4:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/yeoman-generator/-/yeoman-generator-2.0.5.tgz#57b0b3474701293cc9ec965288f3400b00887c81"
+ dependencies:
+ async "^2.6.0"
+ chalk "^2.3.0"
+ cli-table "^0.3.1"
+ cross-spawn "^6.0.5"
+ dargs "^5.1.0"
+ dateformat "^3.0.3"
+ debug "^3.1.0"
+ detect-conflict "^1.0.0"
+ error "^7.0.2"
+ find-up "^2.1.0"
+ github-username "^4.0.0"
+ istextorbinary "^2.2.1"
+ lodash "^4.17.10"
+ make-dir "^1.1.0"
+ mem-fs-editor "^4.0.0"
+ minimist "^1.2.0"
+ pretty-bytes "^4.0.2"
+ read-chunk "^2.1.0"
+ read-pkg-up "^3.0.0"
+ rimraf "^2.6.2"
+ run-async "^2.0.0"
+ shelljs "^0.8.0"
+ text-table "^0.2.0"
+ through2 "^2.0.0"
+ yeoman-environment "^2.0.5"