summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKamil Trzcinski <ayufan@ayufan.eu>2017-11-06 22:38:35 +0100
committerKamil Trzcinski <ayufan@ayufan.eu>2017-11-06 22:38:35 +0100
commit7599009c2726bfdbd73da360961e4d8611641b02 (patch)
tree1578ea9216c4430e2d5d5043edc85f445716c28b
parentc708931a327edcc70b92be7fcddb84fbbe2c0394 (diff)
parent83c3c19ce5eb6a88cd9ed428fa0f8d68d5fa7167 (diff)
downloadgitlab-ce-7599009c2726bfdbd73da360961e4d8611641b02.tar.gz
Merge remote-tracking branch 'origin/move-kubernetes-from-service-to-clusters-page-10-2-ver' into 35616-move-gke-form-1st-iteration
-rw-r--r--.gitlab-ci.yml8
-rw-r--r--.gitlab/route-map.yml3
-rw-r--r--.nvmrc2
-rw-r--r--.ruby-version2
-rw-r--r--.scss-lint.yml5
-rw-r--r--CHANGELOG.md42
-rw-r--r--CONTRIBUTING.md17
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile10
-rw-r--r--Gemfile.lock32
-rw-r--r--app/assets/javascripts/api.js1
-rw-r--r--app/assets/javascripts/autosave.js31
-rw-r--r--app/assets/javascripts/behaviors/autosize.js6
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js4
-rw-r--r--app/assets/javascripts/clusters.js24
-rw-r--r--app/assets/javascripts/dispatcher.js19
-rw-r--r--app/assets/javascripts/droplab/plugins/filter.js2
-rw-r--r--app/assets/javascripts/droplab/utils.js2
-rw-r--r--app/assets/javascripts/dropzone_input.js4
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue22
-rw-r--r--app/assets/javascripts/gl_dropdown.js1
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors.js3
-rw-r--r--app/assets/javascripts/header.js14
-rw-r--r--app/assets/javascripts/importer_status.js144
-rw-r--r--app/assets/javascripts/init_issuable_sidebar.js4
-rw-r--r--app/assets/javascripts/init_legacy_filters.js6
-rw-r--r--app/assets/javascripts/issuable_bulk_update_actions.js1
-rw-r--r--app/assets/javascripts/issuable_bulk_update_sidebar.js8
-rw-r--r--app/assets/javascripts/issuable_context.js97
-rw-r--r--app/assets/javascripts/issuable_form.js197
-rw-r--r--app/assets/javascripts/issuable_index.js201
-rw-r--r--app/assets/javascripts/issue.js4
-rw-r--r--app/assets/javascripts/issue_status_select.js57
-rw-r--r--app/assets/javascripts/labels_select.js826
-rw-r--r--app/assets/javascripts/lazy_loader.js15
-rw-r--r--app/assets/javascripts/lib/utils/axios_utils.js6
-rw-r--r--app/assets/javascripts/logo.js8
-rw-r--r--app/assets/javascripts/main.js17
-rw-r--r--app/assets/javascripts/namespace_select.js134
-rw-r--r--app/assets/javascripts/notes.js11
-rw-r--r--app/assets/javascripts/notes/components/issue_comment_form.vue7
-rw-r--r--app/assets/javascripts/notes/components/issue_note.vue4
-rw-r--r--app/assets/javascripts/notes/mixins/autosave.js3
-rw-r--r--app/assets/javascripts/pipelines/components/graph/action_component.vue24
-rw-r--r--app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue14
-rw-r--r--app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_component.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/stage.vue10
-rw-r--r--app/assets/javascripts/profile/account/components/delete_account_modal.vue2
-rw-r--r--app/assets/javascripts/project_find_file.js3
-rw-r--r--app/assets/javascripts/project_select.js24
-rw-r--r--app/assets/javascripts/repo/components/new_branch_form.vue49
-rw-r--r--app/assets/javascripts/repo/components/new_dropdown/index.vue30
-rw-r--r--app/assets/javascripts/repo/components/new_dropdown/modal.vue18
-rw-r--r--app/assets/javascripts/repo/components/new_dropdown/upload.vue68
-rw-r--r--app/assets/javascripts/repo/components/repo.vue93
-rw-r--r--app/assets/javascripts/repo/components/repo_commit_section.vue170
-rw-r--r--app/assets/javascripts/repo/components/repo_edit_button.vue75
-rw-r--r--app/assets/javascripts/repo/components/repo_editor.vue169
-rw-r--r--app/assets/javascripts/repo/components/repo_file.vue20
-rw-r--r--app/assets/javascripts/repo/components/repo_file_buttons.vue49
-rw-r--r--app/assets/javascripts/repo/components/repo_loading_file.vue12
-rw-r--r--app/assets/javascripts/repo/components/repo_prev_directory.vue30
-rw-r--r--app/assets/javascripts/repo/components/repo_preview.vue49
-rw-r--r--app/assets/javascripts/repo/components/repo_sidebar.vue120
-rw-r--r--app/assets/javascripts/repo/components/repo_tab.vue26
-rw-r--r--app/assets/javascripts/repo/components/repo_tabs.vue12
-rw-r--r--app/assets/javascripts/repo/event_hub.js3
-rw-r--r--app/assets/javascripts/repo/helpers/monaco_loader_helper.js25
-rw-r--r--app/assets/javascripts/repo/helpers/repo_helper.js317
-rw-r--r--app/assets/javascripts/repo/index.js108
-rw-r--r--app/assets/javascripts/repo/mixins/repo_mixin.js17
-rw-r--r--app/assets/javascripts/repo/services/index.js33
-rw-r--r--app/assets/javascripts/repo/services/repo_service.js101
-rw-r--r--app/assets/javascripts/repo/stores/actions.js129
-rw-r--r--app/assets/javascripts/repo/stores/actions/branch.js20
-rw-r--r--app/assets/javascripts/repo/stores/actions/file.js108
-rw-r--r--app/assets/javascripts/repo/stores/actions/tree.js110
-rw-r--r--app/assets/javascripts/repo/stores/getters.js36
-rw-r--r--app/assets/javascripts/repo/stores/index.js15
-rw-r--r--app/assets/javascripts/repo/stores/mutation_types.js28
-rw-r--r--app/assets/javascripts/repo/stores/mutations.js54
-rw-r--r--app/assets/javascripts/repo/stores/mutations/branch.js9
-rw-r--r--app/assets/javascripts/repo/stores/mutations/file.js54
-rw-r--r--app/assets/javascripts/repo/stores/mutations/tree.js45
-rw-r--r--app/assets/javascripts/repo/stores/repo_store.js189
-rw-r--r--app/assets/javascripts/repo/stores/state.js23
-rw-r--r--app/assets/javascripts/repo/stores/utils.js108
-rw-r--r--app/assets/javascripts/settings_panels.js45
-rw-r--r--app/assets/javascripts/sidebar/components/participants/participants.vue125
-rw-r--r--app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue26
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue45
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue60
-rw-r--r--app/assets/javascripts/sidebar/services/sidebar_service.js5
-rw-r--r--app/assets/javascripts/sidebar/sidebar_bundle.js34
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js16
-rw-r--r--app/assets/javascripts/sidebar/stores/sidebar_store.js22
-rw-r--r--app/assets/javascripts/test_utils/index.js2
-rw-r--r--app/assets/javascripts/test_utils/simulate_input.js23
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js12
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js2
-rw-r--r--app/assets/javascripts/vue_shared/ci_action_icons.js21
-rw-r--r--app/assets/javascripts/vue_shared/ci_status_icons.js43
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_badge_link.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_icon.vue15
-rw-r--r--app/assets/javascripts/vue_shared/components/icon.vue52
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue29
-rw-r--r--app/assets/stylesheets/framework.scss2
-rw-r--r--app/assets/stylesheets/framework/animations.scss7
-rw-r--r--app/assets/stylesheets/framework/blocks.scss17
-rw-r--r--app/assets/stylesheets/framework/callout.scss14
-rw-r--r--app/assets/stylesheets/framework/common.scss100
-rw-r--r--app/assets/stylesheets/framework/contextual-sidebar.scss17
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss18
-rw-r--r--app/assets/stylesheets/framework/files.scss52
-rw-r--r--app/assets/stylesheets/framework/filters.scss12
-rw-r--r--app/assets/stylesheets/framework/gitlab-theme.scss7
-rw-r--r--app/assets/stylesheets/framework/header.scss78
-rw-r--r--app/assets/stylesheets/framework/layout.scss38
-rw-r--r--app/assets/stylesheets/framework/lists.scss52
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss26
-rw-r--r--app/assets/stylesheets/framework/responsive_tables.scss (renamed from app/assets/stylesheets/framework/responsive-tables.scss)94
-rw-r--r--app/assets/stylesheets/framework/secondary-navigation-elements.scss63
-rw-r--r--app/assets/stylesheets/framework/selects.scss92
-rw-r--r--app/assets/stylesheets/highlight/white.scss26
-rw-r--r--app/assets/stylesheets/mailers/highlighted_diff_email.scss26
-rw-r--r--app/assets/stylesheets/pages/boards.scss13
-rw-r--r--app/assets/stylesheets/pages/builds.scss36
-rw-r--r--app/assets/stylesheets/pages/clusters.scss4
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss10
-rw-r--r--app/assets/stylesheets/pages/diff.scss8
-rw-r--r--app/assets/stylesheets/pages/environments.scss87
-rw-r--r--app/assets/stylesheets/pages/issuable.scss28
-rw-r--r--app/assets/stylesheets/pages/login.scss98
-rw-r--r--app/assets/stylesheets/pages/members.scss18
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss62
-rw-r--r--app/assets/stylesheets/pages/milestone.scss36
-rw-r--r--app/assets/stylesheets/pages/note_form.scss26
-rw-r--r--app/assets/stylesheets/pages/notes.scss159
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss176
-rw-r--r--app/assets/stylesheets/pages/projects.scss54
-rw-r--r--app/assets/stylesheets/pages/repo.scss56
-rw-r--r--app/assets/stylesheets/pages/runners.scss7
-rw-r--r--app/assets/stylesheets/pages/search.scss4
-rw-r--r--app/assets/stylesheets/pages/settings.scss21
-rw-r--r--app/assets/stylesheets/pages/sherlock.scss16
-rw-r--r--app/assets/stylesheets/pages/stat_graph.scss16
-rw-r--r--app/assets/stylesheets/pages/wiki.scss6
-rw-r--r--app/assets/stylesheets/test.scss5
-rw-r--r--app/controllers/admin/applications_controller.rb15
-rw-r--r--app/controllers/admin/impersonation_tokens_controller.rb2
-rw-r--r--app/controllers/application_controller.rb7
-rw-r--r--app/controllers/concerns/issuable_actions.rb72
-rw-r--r--app/controllers/concerns/lfs_request.rb7
-rw-r--r--app/controllers/concerns/notes_actions.rb5
-rw-r--r--app/controllers/confirmations_controller.rb2
-rw-r--r--app/controllers/jwt_controller.rb6
-rw-r--r--app/controllers/oauth/applications_controller.rb21
-rw-r--r--app/controllers/profiles/keys_controller.rb10
-rw-r--r--app/controllers/profiles/personal_access_tokens_controller.rb2
-rw-r--r--app/controllers/profiles_controller.rb14
-rw-r--r--app/controllers/projects/branches_controller.rb2
-rw-r--r--app/controllers/projects/clusters_controller.rb28
-rw-r--r--app/controllers/projects/git_http_client_controller.rb6
-rw-r--r--app/controllers/projects/group_links_controller.rb15
-rw-r--r--app/controllers/projects/issues_controller.rb65
-rw-r--r--app/controllers/projects/merge_requests_controller.rb12
-rw-r--r--app/controllers/projects/milestones_controller.rb12
-rw-r--r--app/helpers/ci_status_helper.rb24
-rw-r--r--app/helpers/gitlab_routing_helper.rb6
-rw-r--r--app/helpers/issuables_helper.rb50
-rw-r--r--app/helpers/nav_helper.rb6
-rw-r--r--app/helpers/projects_helper.rb14
-rw-r--r--app/models/application_setting.rb2
-rw-r--r--app/models/ci/pipeline.rb4
-rw-r--r--app/models/clusters/cluster.rb73
-rw-r--r--app/models/clusters/platforms/kubernetes.rb101
-rw-r--r--app/models/clusters/project.rb8
-rw-r--r--app/models/clusters/providers/gcp.rb79
-rw-r--r--app/models/concerns/cache_markdown_field.rb3
-rw-r--r--app/models/concerns/issuable.rb3
-rw-r--r--app/models/concerns/repository_mirroring.rb32
-rw-r--r--app/models/concerns/subscribable.rb2
-rw-r--r--app/models/email.rb2
-rw-r--r--app/models/environment.rb7
-rw-r--r--app/models/epic.rb7
-rw-r--r--app/models/fork_network.rb4
-rw-r--r--app/models/gcp/cluster.rb116
-rw-r--r--app/models/group.rb17
-rw-r--r--app/models/identity.rb5
-rw-r--r--app/models/issue.rb3
-rw-r--r--app/models/merge_request.rb9
-rw-r--r--app/models/merge_request_diff.rb4
-rw-r--r--app/models/merge_request_diff_commit.rb4
-rw-r--r--app/models/note.rb8
-rw-r--r--app/models/oauth_access_token.rb10
-rw-r--r--app/models/project.rb70
-rw-r--r--app/models/project_services/kubernetes_service.rb5
-rw-r--r--app/models/project_services/packagist_service.rb65
-rw-r--r--app/models/project_wiki.rb2
-rw-r--r--app/models/repository.rb84
-rw-r--r--app/models/service.rb1
-rw-r--r--app/models/user.rb11
-rw-r--r--app/policies/clusters/cluster_policy.rb (renamed from app/policies/gcp/cluster_policy.rb)4
-rw-r--r--app/presenters/clusters/cluster_presenter.rb (renamed from app/presenters/gcp/cluster_presenter.rb)4
-rw-r--r--app/serializers/issuable_entity.rb8
-rw-r--r--app/serializers/issuable_sidebar_entity.rb16
-rw-r--r--app/serializers/issue_entity.rb4
-rw-r--r--app/serializers/issue_serializer.rb15
-rw-r--r--app/serializers/issue_sidebar_entity.rb3
-rw-r--r--app/serializers/merge_request_basic_entity.rb6
-rw-r--r--app/serializers/merge_request_entity.rb4
-rw-r--r--app/serializers/merge_request_serializer.rb9
-rw-r--r--app/serializers/time_trackable_entity.rb11
-rw-r--r--app/services/access_token_validation_service.rb7
-rw-r--r--app/services/applications/create_service.rb13
-rw-r--r--app/services/ci/create_cluster_service.rb15
-rw-r--r--app/services/ci/fetch_gcp_operation_service.rb17
-rw-r--r--app/services/ci/finalize_cluster_creation_service.rb33
-rw-r--r--app/services/ci/integrate_cluster_service.rb26
-rw-r--r--app/services/ci/provision_cluster_service.rb36
-rw-r--r--app/services/ci/update_cluster_service.rb22
-rw-r--r--app/services/clusters/create_service.rb29
-rw-r--r--app/services/clusters/gcp/fetch_operation_service.rb16
-rw-r--r--app/services/clusters/gcp/finalize_creation_service.rb56
-rw-r--r--app/services/clusters/gcp/provision_service.rb47
-rw-r--r--app/services/clusters/gcp/verify_provision_status_service.rb48
-rw-r--r--app/services/clusters/update_service.rb7
-rw-r--r--app/services/issuable/common_system_notes_service.rb81
-rw-r--r--app/services/issuable_base_service.rb93
-rw-r--r--app/services/issues/reopen_service.rb1
-rw-r--r--app/services/issues/update_service.rb4
-rw-r--r--app/services/keys/base_service.rb1
-rw-r--r--app/services/merge_requests/merge_service.rb9
-rw-r--r--app/services/merge_requests/reopen_service.rb1
-rw-r--r--app/services/merge_requests/update_service.rb10
-rw-r--r--app/services/metrics_service.rb3
-rw-r--r--app/services/milestones/promote_service.rb80
-rw-r--r--app/services/projects/group_links/create_service.rb15
-rw-r--r--app/services/projects/group_links/destroy_service.rb10
-rw-r--r--app/services/projects/hashed_storage_migration_service.rb2
-rw-r--r--app/services/projects/import_service.rb2
-rw-r--r--app/services/projects/unlink_fork_service.rb16
-rw-r--r--app/services/system_hooks_service.rb50
-rw-r--r--app/uploaders/file_uploader.rb2
-rw-r--r--app/validators/cluster_name_validator.rb24
-rw-r--r--app/views/admin/hook_logs/_index.html.haml2
-rw-r--r--app/views/admin/projects/index.html.haml2
-rw-r--r--app/views/admin/projects/show.html.haml2
-rw-r--r--app/views/admin/runners/index.html.haml33
-rw-r--r--app/views/ci/status/_badge.html.haml4
-rw-r--r--app/views/ci/status/_dropdown_graph_badge.html.haml8
-rw-r--r--app/views/dashboard/todos/index.html.haml2
-rw-r--r--app/views/groups/milestones/_form.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml2
-rw-r--r--app/views/profiles/accounts/_reset_token.html.haml11
-rw-r--r--app/views/profiles/accounts/show.html.haml16
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml37
-rw-r--r--app/views/projects/_export.html.haml4
-rw-r--r--app/views/projects/_home_panel.html.haml9
-rw-r--r--app/views/projects/branches/_branch.html.haml5
-rw-r--r--app/views/projects/branches/index.html.haml2
-rw-r--r--app/views/projects/clusters/_form.html.haml40
-rw-r--r--app/views/projects/clusters/show.html.haml18
-rw-r--r--app/views/projects/deploy_keys/_index.html.haml4
-rw-r--r--app/views/projects/edit.html.haml21
-rw-r--r--app/views/projects/graphs/show.html.haml14
-rw-r--r--app/views/projects/hook_logs/_index.html.haml2
-rw-r--r--app/views/projects/issues/edit.html.haml7
-rw-r--r--app/views/projects/jobs/_sidebar.html.haml5
-rw-r--r--app/views/projects/merge_requests/_mr_title.html.haml2
-rw-r--r--app/views/projects/milestones/show.html.haml11
-rw-r--r--app/views/projects/protected_branches/shared/_index.html.haml4
-rw-r--r--app/views/projects/protected_tags/shared/_index.html.haml4
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml16
-rw-r--r--app/views/projects/tree/_tree_header.html.haml4
-rw-r--r--app/views/shared/_mini_pipeline_graph.html.haml2
-rw-r--r--app/views/shared/hook_logs/_content.html.haml2
-rw-r--r--app/views/shared/icons/_icon_autodevops.svg4
-rw-r--r--app/views/shared/issuable/_participants.html.haml18
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml13
-rw-r--r--app/views/shared/milestones/_milestone.html.haml7
-rw-r--r--app/views/shared/repo/_editable_mode.html.haml2
-rw-r--r--app/views/shared/repo/_repo.html.haml7
-rw-r--r--app/workers/cluster_provision_worker.rb6
-rw-r--r--app/workers/stuck_merge_jobs_worker.rb2
-rw-r--r--app/workers/update_merge_requests_worker.rb9
-rw-r--r--app/workers/wait_for_cluster_creation_worker.rb21
-rw-r--r--changelogs/unreleased/23206-load-participants-async.yml5
-rw-r--r--changelogs/unreleased/31454-missing-project-id-pipeline-hook-data.yml5
-rw-r--r--changelogs/unreleased/3274-geo-route-whitelisting.yml5
-rw-r--r--changelogs/unreleased/35914-merge-request-update-worker-is-slow.yml5
-rw-r--r--changelogs/unreleased/3674-hashed-storage-attachments.yml5
-rw-r--r--changelogs/unreleased/37473-expose-project-visibility-as-ci-variable.yml5
-rw-r--r--changelogs/unreleased/37631-add-a-merge_request_diff_id-column-to-merge_requests.yml5
-rw-r--r--changelogs/unreleased/38677-render-new-discussions-on-diff-tab.yml5
-rw-r--r--changelogs/unreleased/39188-change-default-disabled-merge-message.yml5
-rw-r--r--changelogs/unreleased/39417-todos-spelled-correctly-on-todos-list-page.yml5
-rw-r--r--changelogs/unreleased/39495-fix-bitbucket-login.yml5
-rw-r--r--changelogs/unreleased/39509-fix-wiki-create-sidebar-overlap.yml5
-rw-r--r--changelogs/unreleased/39570-performance-bar-appears-enabled-even-though-it-won-t-show-up.yml5
-rw-r--r--changelogs/unreleased/39580-bump-carrierwave-to-1-2-1.yml5
-rw-r--r--changelogs/unreleased/39582-nestingdepth-6.yml5
-rw-r--r--changelogs/unreleased/39583-reopen-issue-count-cache.yml5
-rw-r--r--changelogs/unreleased/39593-emails-on-push-are-sent-to-only-the-first-recipient-when-using-aws-ses.yml5
-rw-r--r--changelogs/unreleased/39619-cancel-merge-when-pipeline-succeeds-from-the-api-fails.yml5
-rw-r--r--changelogs/unreleased/39704_fix_webhooks_log_time.yml5
-rw-r--r--changelogs/unreleased/39776-remove-responsive-table-bottom-border.yml5
-rw-r--r--changelogs/unreleased/add-packagist-project-service.yml5
-rw-r--r--changelogs/unreleased/backport-workhorse-show-all-refs.yml5
-rw-r--r--changelogs/unreleased/bvl-circuitbreaker-backoff.yml6
-rw-r--r--changelogs/unreleased/bvl-circuitbreaker-improvements.yml5
-rw-r--r--changelogs/unreleased/bvl-do-not-use-redis-keys.yml5
-rw-r--r--changelogs/unreleased/bvl-dont-rename-free-names.yml5
-rw-r--r--changelogs/unreleased/bvl-fix-push-event-service-for-forks.yml5
-rw-r--r--changelogs/unreleased/bvl-unlink-fixes.yml5
-rw-r--r--changelogs/unreleased/dm-add-sudo-scope.yml6
-rw-r--r--changelogs/unreleased/dm-convert-private-tokens.yml5
-rw-r--r--changelogs/unreleased/dm-remove-private-token-from-interface.yml5
-rw-r--r--changelogs/unreleased/dm-remove-private-token.yml5
-rw-r--r--changelogs/unreleased/enable-scss-lint-mergeable-selector.yml4
-rw-r--r--changelogs/unreleased/feature-plantuml-restructured-text-captions.yml5
-rw-r--r--changelogs/unreleased/fix-500-on-old-merge-requests.yml5
-rw-r--r--changelogs/unreleased/fix-add-path-attr-to-wiki-file.yml5
-rw-r--r--changelogs/unreleased/fix-project-select-js-without-button.yml5
-rw-r--r--changelogs/unreleased/fix-user-tab-activity-mobile.yml5
-rw-r--r--changelogs/unreleased/fix_global_board_routes_39073.yml5
-rw-r--r--changelogs/unreleased/fix_migration_that_adds_ff_merge_field.yml5
-rw-r--r--changelogs/unreleased/go-get-ssh.yml5
-rw-r--r--changelogs/unreleased/issue_38777.yml5
-rw-r--r--changelogs/unreleased/issue_39176.yml5
-rw-r--r--changelogs/unreleased/jivl-fix-cancel-button-file-upload-new-issue.yml5
-rw-r--r--changelogs/unreleased/jivl-mobile-friendly-table-runners.yml5
-rw-r--r--changelogs/unreleased/pawel-disable_nfs_metrics_checks_39730.yml5
-rw-r--r--changelogs/unreleased/ph-multi-file-upload-file.yml5
-rw-r--r--changelogs/unreleased/refactor-group_links_controller.yml5
-rw-r--r--changelogs/unreleased/sh-disable-unicorn-sampling-sidekiq.yml5
-rw-r--r--changelogs/unreleased/sh-fix-broken-redirection-relative-url-root.yml5
-rw-r--r--changelogs/unreleased/sh-fix-container-registry-destroy.yml5
-rw-r--r--changelogs/unreleased/sh-fix-environment-slug-generation.yml5
-rw-r--r--changelogs/unreleased/sh-fix-environment-write-ref.yml5
-rw-r--r--changelogs/unreleased/update-fe-i18n-guide.yml5
-rw-r--r--changelogs/unreleased/use-git-branch-merged.yml5
-rw-r--r--changelogs/unreleased/winh-admin-projects-namespace-filter.yml5
-rw-r--r--changelogs/unreleased/winh-i18n-contributors-page.yml5
-rw-r--r--changelogs/unreleased/winh-namespace-rename-hooks.yml5
-rw-r--r--changelogs/unreleased/zj-commit-cache.yml5
-rw-r--r--changelogs/unreleased/zj-ruby-2-3-5.yml5
-rw-r--r--config/environments/test.rb1
-rw-r--r--config/gitlab.yml.example2
-rw-r--r--config/initializers/8_metrics.rb4
-rw-r--r--config/locales/doorkeeper.en.yml5
-rw-r--r--config/routes/ci.rb2
-rw-r--r--config/routes/profile.rb1
-rw-r--r--config/routes/project.rb3
-rw-r--r--config/routes/snippets.rb2
-rw-r--r--config/routes/user.rb12
-rw-r--r--db/migrate/20150827121444_add_fast_forward_option_to_project.rb6
-rw-r--r--db/migrate/20171012125712_migrate_user_authentication_token_to_personal_access_token.rb78
-rw-r--r--db/migrate/20171013094327_create_new_clusters_architectures.rb68
-rw-r--r--db/migrate/20171025110159_add_latest_merge_request_diff_id_to_merge_requests.rb26
-rw-r--r--db/post_migrate/20171012150314_remove_user_authentication_token.rb20
-rw-r--r--db/post_migrate/20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb99
-rw-r--r--db/post_migrate/20171026082505_populate_merge_requests_latest_merge_request_diff_id.rb27
-rw-r--r--db/schema.rb69
-rw-r--r--doc/README.md1
-rw-r--r--doc/administration/auth/README.md4
-rw-r--r--doc/administration/integration/plantuml.md37
-rw-r--r--doc/administration/monitoring/performance/performance_bar.md6
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_metrics.md4
-rw-r--r--doc/administration/operations/sidekiq_memory_killer.md4
-rw-r--r--doc/administration/repository_storage_types.md25
-rw-r--r--doc/administration/troubleshooting/debug.md2
-rw-r--r--doc/api/README.md126
-rw-r--r--doc/api/pipelines.md2
-rw-r--r--doc/api/services.md34
-rw-r--r--doc/api/session.md55
-rw-r--r--doc/api/users.md3
-rw-r--r--doc/ci/README.md4
-rw-r--r--doc/ci/docker/README.md4
-rw-r--r--doc/ci/docker/using_docker_build.md8
-rw-r--r--doc/ci/docker/using_docker_images.md4
-rw-r--r--doc/ci/enable_or_disable_ci.md6
-rw-r--r--doc/ci/examples/README.md4
-rw-r--r--doc/ci/examples/deployment/composer-npm-deploy.md14
-rw-r--r--doc/ci/examples/php.md4
-rw-r--r--doc/ci/examples/test-and-deploy-python-application-to-heroku.md23
-rw-r--r--doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md24
-rw-r--r--doc/ci/examples/test-clojure-application.md12
-rw-r--r--doc/ci/examples/test-phoenix-application.md10
-rw-r--r--doc/ci/git_submodules.md4
-rw-r--r--doc/ci/permissions/README.md2
-rw-r--r--doc/ci/quick_start/README.md2
-rw-r--r--doc/ci/runners/README.md2
-rw-r--r--doc/ci/services/README.md6
-rw-r--r--doc/ci/services/docker-services.md12
-rw-r--r--doc/ci/variables/README.md7
-rw-r--r--doc/development/README.md4
-rw-r--r--doc/development/doc_styleguide.md4
-rw-r--r--doc/development/ee_features.md382
-rw-r--r--doc/development/fe_guide/index.md4
-rw-r--r--doc/development/i18n/externalization.md40
-rw-r--r--doc/development/testing_guide/best_practices.md29
-rw-r--r--doc/development/testing_guide/testing_levels.md2
-rw-r--r--doc/development/testing_guide/testing_rake_tasks.md2
-rw-r--r--doc/development/ux_guide/users.md75
-rw-r--r--doc/gitlab-basics/README.md4
-rw-r--r--doc/gitlab-basics/add-merge-request.md23
-rw-r--r--doc/gitlab-basics/img/merge_request_new.pngbin2234 -> 0 bytes
-rw-r--r--doc/gitlab-basics/img/merge_request_select_branch.pngbin20332 -> 16668 bytes
-rw-r--r--doc/gitlab-basics/img/project_navbar.pngbin3259 -> 0 bytes
-rw-r--r--doc/install/README.md4
-rw-r--r--doc/install/relative_url.md16
-rw-r--r--doc/install/requirements.md4
-rw-r--r--doc/integration/README.md4
-rw-r--r--doc/intro/README.md4
-rw-r--r--doc/legal/README.md4
-rw-r--r--doc/legal/corporate_contributor_license_agreement.md31
-rw-r--r--doc/legal/individual_contributor_license_agreement.md27
-rw-r--r--doc/migrate_ci_to_ce/README.md9
-rw-r--r--doc/policy/maintenance.md31
-rw-r--r--doc/raketasks/README.md4
-rw-r--r--doc/raketasks/backup_restore.md123
-rw-r--r--doc/raketasks/user_management.md15
-rw-r--r--doc/security/README.md4
-rw-r--r--doc/ssh/README.md8
-rw-r--r--doc/system_hooks/system_hooks.md70
-rw-r--r--doc/topics/autodevops/index.md8
-rw-r--r--doc/university/README.md16
-rw-r--r--doc/university/bookclub/booklist.md4
-rw-r--r--doc/university/bookclub/index.md4
-rw-r--r--doc/university/glossary/README.md8
-rw-r--r--doc/university/high-availability/aws/README.md4
-rw-r--r--doc/university/process/README.md6
-rw-r--r--doc/university/support/README.md6
-rw-r--r--doc/university/training/end-user/README.md4
-rw-r--r--doc/university/training/gitlab_flow.md4
-rw-r--r--doc/university/training/index.md4
-rw-r--r--doc/university/training/topics/additional_resources.md6
-rw-r--r--doc/university/training/topics/agile_git.md4
-rw-r--r--doc/university/training/topics/bisect.md4
-rw-r--r--doc/university/training/topics/cherry_picking.md4
-rw-r--r--doc/university/training/topics/env_setup.md4
-rw-r--r--doc/university/training/topics/explore_gitlab.md4
-rw-r--r--doc/university/training/topics/feature_branching.md4
-rw-r--r--doc/university/training/topics/getting_started.md4
-rw-r--r--doc/university/training/topics/git_add.md4
-rw-r--r--doc/university/training/topics/git_intro.md4
-rw-r--r--doc/university/training/topics/git_log.md8
-rw-r--r--doc/university/training/topics/gitlab_flow.md4
-rw-r--r--doc/university/training/topics/merge_conflicts.md4
-rw-r--r--doc/university/training/topics/merge_requests.md4
-rw-r--r--doc/university/training/topics/rollback_commits.md4
-rw-r--r--doc/university/training/topics/stash.md4
-rw-r--r--doc/university/training/topics/subtree.md8
-rw-r--r--doc/university/training/topics/tags.md4
-rw-r--r--doc/university/training/topics/unstage.md4
-rw-r--r--doc/university/training/user_training.md4
-rw-r--r--doc/update/10.0-to-10.1.md4
-rw-r--r--doc/update/2.6-to-3.0.md4
-rw-r--r--doc/update/2.9-to-3.0.md4
-rw-r--r--doc/update/3.0-to-3.1.md4
-rw-r--r--doc/update/3.1-to-4.0.md4
-rw-r--r--doc/update/4.0-to-4.1.md4
-rw-r--r--doc/update/4.1-to-4.2.md4
-rw-r--r--doc/update/4.2-to-5.0.md4
-rw-r--r--doc/update/5.0-to-5.1.md4
-rw-r--r--doc/update/5.1-to-5.2.md4
-rw-r--r--doc/update/5.1-to-5.4.md4
-rw-r--r--doc/update/5.1-to-6.0.md4
-rw-r--r--doc/update/5.2-to-5.3.md4
-rw-r--r--doc/update/5.3-to-5.4.md4
-rw-r--r--doc/update/5.4-to-6.0.md4
-rw-r--r--doc/update/6.0-to-6.1.md4
-rw-r--r--doc/update/6.1-to-6.2.md4
-rw-r--r--doc/update/6.2-to-6.3.md4
-rw-r--r--doc/update/6.3-to-6.4.md4
-rw-r--r--doc/update/6.4-to-6.5.md4
-rw-r--r--doc/update/6.5-to-6.6.md4
-rw-r--r--doc/update/6.6-to-6.7.md4
-rw-r--r--doc/update/6.7-to-6.8.md4
-rw-r--r--doc/update/6.8-to-6.9.md4
-rw-r--r--doc/update/6.9-to-7.0.md4
-rw-r--r--doc/update/6.x-or-7.x-to-7.14.md4
-rw-r--r--doc/update/7.0-to-7.1.md4
-rw-r--r--doc/update/7.1-to-7.2.md4
-rw-r--r--doc/update/7.10-to-7.11.md4
-rw-r--r--doc/update/7.11-to-7.12.md4
-rw-r--r--doc/update/7.12-to-7.13.md4
-rw-r--r--doc/update/7.13-to-7.14.md4
-rw-r--r--doc/update/7.14-to-8.0.md4
-rw-r--r--doc/update/7.2-to-7.3.md4
-rw-r--r--doc/update/7.3-to-7.4.md4
-rw-r--r--doc/update/7.4-to-7.5.md4
-rw-r--r--doc/update/7.5-to-7.6.md4
-rw-r--r--doc/update/7.6-to-7.7.md4
-rw-r--r--doc/update/7.7-to-7.8.md4
-rw-r--r--doc/update/7.8-to-7.9.md4
-rw-r--r--doc/update/7.9-to-7.10.md4
-rw-r--r--doc/update/8.0-to-8.1.md4
-rw-r--r--doc/update/8.1-to-8.2.md4
-rw-r--r--doc/update/8.10-to-8.11.md4
-rw-r--r--doc/update/8.11-to-8.12.md4
-rw-r--r--doc/update/8.12-to-8.13.md4
-rw-r--r--doc/update/8.13-to-8.14.md4
-rw-r--r--doc/update/8.14-to-8.15.md4
-rw-r--r--doc/update/8.15-to-8.16.md4
-rw-r--r--doc/update/8.16-to-8.17.md4
-rw-r--r--doc/update/8.17-to-9.0.md4
-rw-r--r--doc/update/8.2-to-8.3.md4
-rw-r--r--doc/update/8.3-to-8.4.md4
-rw-r--r--doc/update/8.4-to-8.5.md4
-rw-r--r--doc/update/8.5-to-8.6.md4
-rw-r--r--doc/update/8.6-to-8.7.md4
-rw-r--r--doc/update/8.7-to-8.8.md4
-rw-r--r--doc/update/8.8-to-8.9.md4
-rw-r--r--doc/update/8.9-to-8.10.md4
-rw-r--r--doc/update/9.0-to-9.1.md4
-rw-r--r--doc/update/9.1-to-9.2.md4
-rw-r--r--doc/update/9.2-to-9.3.md4
-rw-r--r--doc/update/9.3-to-9.4.md4
-rw-r--r--doc/update/9.4-to-9.5.md4
-rw-r--r--doc/update/9.5-to-10.0.md4
-rw-r--r--doc/update/patch_versions.md4
-rw-r--r--doc/update/upgrader.md4
-rw-r--r--doc/user/profile/index.md2
-rw-r--r--doc/user/profile/personal_access_tokens.md12
-rw-r--r--doc/user/project/integrations/img/webhook_logs.pngbin24066 -> 132319 bytes
-rw-r--r--doc/user/project/integrations/project_services.md1
-rw-r--r--doc/user/project/integrations/webhooks.md22
-rw-r--r--doc/user/project/milestones/index.md3
-rw-r--r--doc/user/project/pipelines/job_artifacts.md6
-rw-r--r--doc/workflow/README.md4
-rw-r--r--doc/workflow/gitlab_flow.md2
-rw-r--r--doc/workflow/shortcuts.md2
-rw-r--r--features/steps/profile/notifications.rb2
-rw-r--r--features/steps/project/commits/branches.rb8
-rw-r--r--features/steps/project/issues/issues.rb6
-rw-r--r--features/steps/project/issues/labels.rb2
-rw-r--r--features/steps/project/issues/milestones.rb3
-rw-r--r--features/steps/shared/diff_note.rb9
-rw-r--r--features/steps/shared/note.rb2
-rw-r--r--features/support/capybara.rb29
-rw-r--r--features/support/capybara_helpers.rb10
-rw-r--r--lib/additional_email_headers_interceptor.rb6
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/api_guard.rb108
-rw-r--r--lib/api/entities.rb4
-rw-r--r--lib/api/helpers.rb21
-rw-r--r--lib/api/merge_requests.rb2
-rw-r--r--lib/api/services.rb21
-rw-r--r--lib/api/session.rb20
-rw-r--r--lib/api/users.rb4
-rw-r--r--lib/api/v3/services.rb20
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb6
-rw-r--r--lib/banzai/filter/reference_filter.rb4
-rw-r--r--lib/banzai/filter/user_reference_filter.rb42
-rw-r--r--lib/github/import.rb4
-rw-r--r--lib/gitlab/auth.rb16
-rw-r--r--lib/gitlab/ci/status/build/cancelable.rb2
-rw-r--r--lib/gitlab/ci/status/build/failed_allowed.rb2
-rw-r--r--lib/gitlab/ci/status/build/play.rb2
-rw-r--r--lib/gitlab/ci/status/build/retryable.rb2
-rw-r--r--lib/gitlab/ci/status/build/stop.rb2
-rw-r--r--lib/gitlab/ci/status/canceled.rb2
-rw-r--r--lib/gitlab/ci/status/created.rb2
-rw-r--r--lib/gitlab/ci/status/failed.rb2
-rw-r--r--lib/gitlab/ci/status/manual.rb2
-rw-r--r--lib/gitlab/ci/status/pending.rb2
-rw-r--r--lib/gitlab/ci/status/running.rb2
-rw-r--r--lib/gitlab/ci/status/skipped.rb2
-rw-r--r--lib/gitlab/ci/status/success.rb2
-rw-r--r--lib/gitlab/ci/status/success_warning.rb2
-rw-r--r--lib/gitlab/database.rb8
-rw-r--r--lib/gitlab/diff/position.rb8
-rw-r--r--lib/gitlab/ee_compat_check.rb24
-rw-r--r--lib/gitlab/gcp/model.rb13
-rw-r--r--lib/gitlab/git/blob.rb57
-rw-r--r--lib/gitlab/git/branch.rb8
-rw-r--r--lib/gitlab/git/commit.rb3
-rw-r--r--lib/gitlab/git/lfs_changes.rb36
-rw-r--r--lib/gitlab/git/repository.rb77
-rw-r--r--lib/gitlab/git/repository_mirroring.rb95
-rw-r--r--lib/gitlab/git/rev_list.rb45
-rw-r--r--lib/gitlab/git/wiki.rb94
-rw-r--r--lib/gitlab/gitaly_client.rb6
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb2
-rw-r--r--lib/gitlab/gitaly_client/operation_service.rb17
-rw-r--r--lib/gitlab/gitaly_client/wiki_file.rb17
-rw-r--r--lib/gitlab/gitaly_client/wiki_page.rb25
-rw-r--r--lib/gitlab/gitaly_client/wiki_service.rb85
-rw-r--r--lib/gitlab/import_export/import_export.yml2
-rw-r--r--lib/gitlab/import_export/relation_factory.rb4
-rw-r--r--lib/gitlab/ldap/auth_hash.rb2
-rw-r--r--lib/gitlab/ldap/user.rb5
-rw-r--r--lib/gitlab/metrics/sidekiq_middleware.rb2
-rw-r--r--lib/gitlab/metrics/system.rb4
-rw-r--r--lib/gitlab/middleware/go.rb15
-rw-r--r--lib/gitlab/middleware/read_only.rb5
-rw-r--r--lib/gitlab/performance_bar/peek_query_tracker.rb2
-rw-r--r--lib/gitlab/sherlock/transaction.rb4
-rw-r--r--lib/gitlab/sidekiq_middleware/memory_killer.rb41
-rw-r--r--lib/gitlab/testing/request_blocker_middleware.rb12
-rw-r--r--lib/gitlab/testing/request_inspector_middleware.rb71
-rw-r--r--lib/gitlab/usage_data.rb6
-rw-r--r--lib/gitlab/workhorse.rb5
-rw-r--r--lib/google_api/cloud_platform/client.rb1
-rw-r--r--lib/system_check/app/git_user_default_ssh_config_check.rb4
-rw-r--r--lib/system_check/app/ruby_version_check.rb2
-rw-r--r--lib/tasks/gitlab/dev.rake7
-rw-r--r--lib/tasks/gitlab/users.rake11
-rw-r--r--lib/tasks/tokens.rake12
-rw-r--r--package.json6
-rw-r--r--qa/qa.rb6
-rw-r--r--qa/qa/page/mattermost/login.rb19
-rw-r--r--qa/qa/page/mattermost/main.rb11
-rw-r--r--qa/qa/runtime/scenario.rb8
-rw-r--r--qa/qa/scenario/test/integration/mattermost.rb5
-rw-r--r--qa/qa/specs/features/mattermost/login_spec.rb12
-rw-r--r--qa/spec/scenario/entrypoint_spec.rb46
-rw-r--r--spec/controllers/application_controller_spec.rb86
-rw-r--r--spec/controllers/concerns/lfs_request_spec.rb50
-rw-r--r--spec/controllers/metrics_controller_spec.rb11
-rw-r--r--spec/controllers/projects/clusters_controller_spec.rb567
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb62
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb62
-rw-r--r--spec/controllers/projects/milestones_controller_spec.rb28
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb18
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb2
-rw-r--r--spec/factories/ci/builds.rb2
-rw-r--r--spec/factories/clusters/cluster.rb39
-rw-r--r--spec/factories/clusters/platforms/kubernetes.rb20
-rw-r--r--spec/factories/clusters/providers/gcp.rb32
-rw-r--r--spec/factories/gcp/cluster.rb38
-rw-r--r--spec/features/admin/admin_disables_two_factor_spec.rb2
-rw-r--r--spec/features/admin/admin_groups_spec.rb2
-rw-r--r--spec/features/admin/admin_hooks_spec.rb4
-rw-r--r--spec/features/admin/admin_labels_spec.rb2
-rw-r--r--spec/features/admin/admin_settings_spec.rb25
-rw-r--r--spec/features/admin/admin_users_impersonation_tokens_spec.rb4
-rw-r--r--spec/features/admin/admin_users_spec.rb4
-rw-r--r--spec/features/admin/admin_uses_repository_checks_spec.rb2
-rw-r--r--spec/features/atom/dashboard_issues_spec.rb6
-rw-r--r--spec/features/atom/dashboard_spec.rb6
-rw-r--r--spec/features/atom/issues_spec.rb6
-rw-r--r--spec/features/atom/users_spec.rb6
-rw-r--r--spec/features/boards/add_issues_modal_spec.rb2
-rw-r--r--spec/features/boards/boards_spec.rb18
-rw-r--r--spec/features/boards/sidebar_spec.rb4
-rw-r--r--spec/features/calendar_spec.rb6
-rw-r--r--spec/features/container_registry_spec.rb2
-rw-r--r--spec/features/dashboard/group_spec.rb2
-rw-r--r--spec/features/dashboard/groups_list_spec.rb2
-rw-r--r--spec/features/dashboard/issues_spec.rb8
-rw-r--r--spec/features/dashboard/merge_requests_spec.rb2
-rw-r--r--spec/features/dashboard/todos/todos_spec.rb16
-rw-r--r--spec/features/discussion_comments/commit_spec.rb2
-rw-r--r--spec/features/discussion_comments/snippets_spec.rb2
-rw-r--r--spec/features/explore/new_menu_spec.rb10
-rw-r--r--spec/features/groups/milestone_spec.rb13
-rw-r--r--spec/features/groups_spec.rb2
-rw-r--r--spec/features/issues/bulk_assignment_labels_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/dropdown_assignee_spec.rb7
-rw-r--r--spec/features/issues/filtered_search/dropdown_author_spec.rb6
-rw-r--r--spec/features/issues/filtered_search/dropdown_emoji_spec.rb6
-rw-r--r--spec/features/issues/filtered_search/dropdown_label_spec.rb6
-rw-r--r--spec/features/issues/filtered_search/dropdown_milestone_spec.rb7
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/recent_searches_spec.rb24
-rw-r--r--spec/features/issues/filtered_search/visual_tokens_spec.rb3
-rw-r--r--spec/features/issues/form_spec.rb49
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb31
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb10
-rw-r--r--spec/features/issues/move_spec.rb6
-rw-r--r--spec/features/issues/update_issues_spec.rb2
-rw-r--r--spec/features/issues/user_uses_slash_commands_spec.rb4
-rw-r--r--spec/features/issues_spec.rb122
-rw-r--r--spec/features/merge_requests/conflicts_spec.rb6
-rw-r--r--spec/features/merge_requests/diff_notes_avatars_spec.rb20
-rw-r--r--spec/features/merge_requests/diff_notes_resolve_spec.rb12
-rw-r--r--spec/features/merge_requests/diffs_spec.rb10
-rw-r--r--spec/features/merge_requests/form_spec.rb2
-rw-r--r--spec/features/merge_requests/mini_pipeline_graph_spec.rb2
-rw-r--r--spec/features/merge_requests/update_merge_requests_spec.rb2
-rw-r--r--spec/features/merge_requests/user_posts_diff_notes_spec.rb9
-rw-r--r--spec/features/merge_requests/user_posts_notes_spec.rb2
-rw-r--r--spec/features/merge_requests/versions_spec.rb8
-rw-r--r--spec/features/merge_requests/widget_deployments_spec.rb2
-rw-r--r--spec/features/profile_spec.rb37
-rw-r--r--spec/features/profiles/oauth_applications_spec.rb4
-rw-r--r--spec/features/profiles/personal_access_tokens_spec.rb6
-rw-r--r--spec/features/profiles/user_visits_notifications_tab_spec.rb2
-rw-r--r--spec/features/profiles/user_visits_profile_preferences_page_spec.rb2
-rw-r--r--spec/features/projects/artifacts/download_spec.rb2
-rw-r--r--spec/features/projects/artifacts/file_spec.rb1
-rw-r--r--spec/features/projects/branches_spec.rb2
-rw-r--r--spec/features/projects/clusters_spec.rb19
-rw-r--r--spec/features/projects/commit/diff_notes_spec.rb4
-rw-r--r--spec/features/projects/deploy_keys_spec.rb2
-rw-r--r--spec/features/projects/environments/environment_spec.rb8
-rw-r--r--spec/features/projects/environments/environments_spec.rb2
-rw-r--r--spec/features/projects/features_visibility_spec.rb4
-rw-r--r--spec/features/projects/files/edit_file_soft_wrap_spec.rb26
-rw-r--r--spec/features/projects/import_export/export_file_spec.rb2
-rw-r--r--spec/features/projects/import_export/import_file_spec.rb2
-rw-r--r--spec/features/projects/import_export/namespace_export_file_spec.rb2
-rw-r--r--spec/features/projects/jobs/user_browses_job_spec.rb4
-rw-r--r--spec/features/projects/jobs_spec.rb38
-rw-r--r--spec/features/projects/members/groups_with_access_list_spec.rb3
-rw-r--r--spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb4
-rw-r--r--spec/features/projects/members/share_with_group_spec.rb4
-rw-r--r--spec/features/projects/members/user_requests_access_spec.rb2
-rw-r--r--spec/features/projects/merge_requests/user_comments_on_diff_spec.rb11
-rw-r--r--spec/features/projects/merge_requests/user_edits_merge_request_spec.rb5
-rw-r--r--spec/features/projects/merge_requests/user_manages_subscription_spec.rb2
-rw-r--r--spec/features/projects/new_project_spec.rb4
-rw-r--r--spec/features/projects/pipeline_schedules_spec.rb2
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb26
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb26
-rw-r--r--spec/features/projects/ref_switcher_spec.rb2
-rw-r--r--spec/features/projects/services/user_activates_jira_spec.rb2
-rw-r--r--spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb8
-rw-r--r--spec/features/projects/services/user_activates_packagist_spec.rb24
-rw-r--r--spec/features/projects/services/user_views_services_spec.rb1
-rw-r--r--spec/features/projects/settings/forked_project_settings_spec.rb40
-rw-r--r--spec/features/projects/settings/merge_requests_settings_spec.rb6
-rw-r--r--spec/features/projects/settings/pipelines_settings_spec.rb2
-rw-r--r--spec/features/projects/settings/repository_settings_spec.rb3
-rw-r--r--spec/features/projects/snippets/create_snippet_spec.rb4
-rw-r--r--spec/features/projects/tree/create_directory_spec.rb5
-rw-r--r--spec/features/projects/tree/create_file_spec.rb7
-rw-r--r--spec/features/projects/tree/upload_file_spec.rb46
-rw-r--r--spec/features/projects/user_browses_files_spec.rb5
-rw-r--r--spec/features/projects/wiki/markdown_preview_spec.rb6
-rw-r--r--spec/features/projects/wiki/user_creates_wiki_page_spec.rb4
-rw-r--r--spec/features/projects/wiki/user_views_wiki_page_spec.rb13
-rw-r--r--spec/features/projects_spec.rb4
-rw-r--r--spec/features/protected_branches_spec.rb8
-rw-r--r--spec/features/raven_js_spec.rb6
-rw-r--r--spec/features/search/user_searches_for_code_spec.rb4
-rw-r--r--spec/features/search/user_searches_for_issues_spec.rb8
-rw-r--r--spec/features/search/user_searches_for_merge_requests_spec.rb6
-rw-r--r--spec/features/search/user_searches_for_milestones_spec.rb6
-rw-r--r--spec/features/search/user_searches_for_wiki_pages_spec.rb4
-rw-r--r--spec/features/search/user_uses_search_filters_spec.rb6
-rw-r--r--spec/features/security/project/internal_access_spec.rb15
-rw-r--r--spec/features/security/project/private_access_spec.rb15
-rw-r--r--spec/features/security/project/public_access_spec.rb15
-rw-r--r--spec/features/snippets/notes_on_personal_snippets_spec.rb7
-rw-r--r--spec/features/snippets/user_creates_snippet_spec.rb16
-rw-r--r--spec/features/tags/master_creates_tag_spec.rb2
-rw-r--r--spec/features/tags/master_deletes_tag_spec.rb2
-rw-r--r--spec/features/triggers_spec.rb22
-rw-r--r--spec/features/u2f_spec.rb24
-rw-r--r--spec/features/uploads/user_uploads_file_to_note_spec.rb26
-rw-r--r--spec/features/users_spec.rb1
-rw-r--r--spec/features/variables_spec.rb2
-rw-r--r--spec/fixtures/api/schemas/entities/issue.json44
-rw-r--r--spec/fixtures/api/schemas/entities/issue_sidebar.json21
-rw-r--r--spec/fixtures/api/schemas/entities/label.json26
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_basic.json4
-rw-r--r--spec/fixtures/api/schemas/issue.json27
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/user/login.json6
-rw-r--r--spec/fixtures/clusters/sample_cert.pem33
-rw-r--r--spec/helpers/ci_status_helper_spec.rb12
-rw-r--r--spec/helpers/gitlab_routing_helper_spec.rb26
-rw-r--r--spec/helpers/issuables_helper_spec.rb32
-rw-r--r--spec/javascripts/fixtures/clusters.rb2
-rw-r--r--spec/javascripts/gl_form_spec.js4
-rw-r--r--spec/javascripts/groups/components/app_spec.js4
-rw-r--r--spec/javascripts/header_spec.js3
-rw-r--r--spec/javascripts/helpers/vue_mount_component_helper.js5
-rw-r--r--spec/javascripts/issuable_context_spec.js34
-rw-r--r--spec/javascripts/issuable_spec.js102
-rw-r--r--spec/javascripts/issue_spec.js34
-rw-r--r--spec/javascripts/jobs/mock_data.js2
-rw-r--r--spec/javascripts/labels_issue_sidebar_spec.js6
-rw-r--r--spec/javascripts/merge_request_notes_spec.js2
-rw-r--r--spec/javascripts/namespace_select_spec.js65
-rw-r--r--spec/javascripts/notes/components/issue_comment_form_spec.js6
-rw-r--r--spec/javascripts/notes_spec.js23
-rw-r--r--spec/javascripts/pipelines/graph/action_component_spec.js2
-rw-r--r--spec/javascripts/pipelines/graph/dropdown_action_component_spec.js2
-rw-r--r--spec/javascripts/pipelines/graph/job_component_spec.js2
-rw-r--r--spec/javascripts/pipelines/graph/mock_data.js12
-rw-r--r--spec/javascripts/pipelines/graph/stage_column_component_spec.js2
-rw-r--r--spec/javascripts/repo/components/new_branch_form_spec.js76
-rw-r--r--spec/javascripts/repo/components/new_dropdown/index_spec.js138
-rw-r--r--spec/javascripts/repo/components/new_dropdown/modal_spec.js178
-rw-r--r--spec/javascripts/repo/components/new_dropdown/upload_spec.js103
-rw-r--r--spec/javascripts/repo/components/repo_commit_section_spec.js207
-rw-r--r--spec/javascripts/repo/components/repo_edit_button_spec.js82
-rw-r--r--spec/javascripts/repo/components/repo_editor_spec.js62
-rw-r--r--spec/javascripts/repo/components/repo_file_buttons_spec.js83
-rw-r--r--spec/javascripts/repo/components/repo_file_spec.js69
-rw-r--r--spec/javascripts/repo/components/repo_loading_file_spec.js42
-rw-r--r--spec/javascripts/repo/components/repo_prev_directory_spec.js56
-rw-r--r--spec/javascripts/repo/components/repo_preview_spec.js32
-rw-r--r--spec/javascripts/repo/components/repo_sidebar_spec.js168
-rw-r--r--spec/javascripts/repo/components/repo_spec.js87
-rw-r--r--spec/javascripts/repo/components/repo_tab_spec.js104
-rw-r--r--spec/javascripts/repo/components/repo_tabs_spec.js39
-rw-r--r--spec/javascripts/repo/helpers.js20
-rw-r--r--spec/javascripts/repo/mock_data.js14
-rw-r--r--spec/javascripts/repo/services/repo_service_spec.js171
-rw-r--r--spec/javascripts/search_autocomplete_spec.js1
-rw-r--r--spec/javascripts/sidebar/mock_data.js2
-rw-r--r--spec/javascripts/sidebar/participants_spec.js174
-rw-r--r--spec/javascripts/sidebar/sidebar_mediator_spec.js17
-rw-r--r--spec/javascripts/sidebar/sidebar_service_spec.js17
-rw-r--r--spec/javascripts/sidebar/sidebar_store_spec.js93
-rw-r--r--spec/javascripts/sidebar/sidebar_subscriptions_spec.js36
-rw-r--r--spec/javascripts/sidebar/subscriptions_spec.js42
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js10
-rw-r--r--spec/javascripts/vue_shared/ci_action_icons_spec.js27
-rw-r--r--spec/javascripts/vue_shared/ci_status_icon_spec.js27
-rw-r--r--spec/javascripts/vue_shared/components/ci_badge_link_spec.js18
-rw-r--r--spec/javascripts/vue_shared/components/icon_spec.js48
-rw-r--r--spec/javascripts/vue_shared/components/markdown/field_spec.js14
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js39
-rw-r--r--spec/lib/additional_email_headers_interceptor_spec.rb21
-rw-r--r--spec/lib/banzai/filter/issue_reference_filter_spec.rb62
-rw-r--r--spec/lib/banzai/filter/label_reference_filter_spec.rb12
-rw-r--r--spec/lib/banzai/filter/merge_request_reference_filter_spec.rb10
-rw-r--r--spec/lib/banzai/filter/milestone_reference_filter_spec.rb11
-rw-r--r--spec/lib/banzai/filter/snippet_reference_filter_spec.rb10
-rw-r--r--spec/lib/banzai/filter/user_reference_filter_spec.rb33
-rw-r--r--spec/lib/gitlab/auth_spec.rb14
-rw-r--r--spec/lib/gitlab/ci/cron_parser_spec.rb48
-rw-r--r--spec/lib/gitlab/ci/status/build/cancelable_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/build/factory_spec.rb18
-rw-r--r--spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/build/play_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/build/retryable_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/build/stop_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/canceled_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/created_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/failed_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/manual_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/pending_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/running_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/skipped_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/success_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/success_warning_spec.rb2
-rw-r--r--spec/lib/gitlab/database_spec.rb22
-rw-r--r--spec/lib/gitlab/diff/position_spec.rb37
-rw-r--r--spec/lib/gitlab/git/blob_spec.rb55
-rw-r--r--spec/lib/gitlab/git/branch_spec.rb32
-rw-r--r--spec/lib/gitlab/git/lfs_changes_spec.rb48
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb199
-rw-r--r--spec/lib/gitlab/git/rev_list_spec.rb87
-rw-r--r--spec/lib/gitlab/gitaly_client/operation_service_spec.rb34
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml18
-rw-r--r--spec/lib/gitlab/import_export/project.json8
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml48
-rw-r--r--spec/lib/gitlab/ldap/authentication_spec.rb4
-rw-r--r--spec/lib/gitlab/ldap/user_spec.rb8
-rw-r--r--spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb48
-rw-r--r--spec/lib/gitlab/middleware/go_spec.rb138
-rw-r--r--spec/lib/gitlab/middleware/read_only_spec.rb26
-rw-r--r--spec/lib/gitlab/o_auth/user_spec.rb2
-rw-r--r--spec/lib/gitlab/saml/user_spec.rb2
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb63
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb6
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb18
-rw-r--r--spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb8
-rw-r--r--spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb166
-rw-r--r--spec/migrations/migrate_user_authentication_token_to_personal_access_token_spec.rb25
-rw-r--r--spec/migrations/populate_merge_requests_latest_merge_request_diff_id_spec.rb61
-rw-r--r--spec/models/ci/build_spec.rb1
-rw-r--r--spec/models/clusters/cluster_spec.rb181
-rw-r--r--spec/models/clusters/platforms/kubernetes_spec.rb188
-rw-r--r--spec/models/clusters/project_spec.rb6
-rw-r--r--spec/models/clusters/providers/gcp_spec.rb183
-rw-r--r--spec/models/concerns/routable_spec.rb1
-rw-r--r--spec/models/concerns/subscribable_spec.rb6
-rw-r--r--spec/models/concerns/token_authenticatable_spec.rb2
-rw-r--r--spec/models/email_spec.rb8
-rw-r--r--spec/models/environment_spec.rb16
-rw-r--r--spec/models/fork_network_spec.rb10
-rw-r--r--spec/models/gcp/cluster_spec.rb264
-rw-r--r--spec/models/group_spec.rb41
-rw-r--r--spec/models/identity_spec.rb14
-rw-r--r--spec/models/merge_request_diff_commit_spec.rb83
-rw-r--r--spec/models/merge_request_spec.rb10
-rw-r--r--spec/models/project_services/kubernetes_service_spec.rb43
-rw-r--r--spec/models/project_services/packagist_service_spec.rb46
-rw-r--r--spec/models/project_spec.rb82
-rw-r--r--spec/models/project_wiki_spec.rb131
-rw-r--r--spec/models/repository_spec.rb58
-rw-r--r--spec/models/user_spec.rb45
-rw-r--r--spec/models/wiki_page_spec.rb2
-rw-r--r--spec/policies/clusters/cluster_policy_spec.rb (renamed from spec/policies/gcp/cluster_policy_spec.rb)6
-rw-r--r--spec/presenters/clusters/cluster_presenter_spec.rb (renamed from spec/presenters/gcp/cluster_presenter_spec.rb)11
-rw-r--r--spec/requests/api/doorkeeper_access_spec.rb10
-rw-r--r--spec/requests/api/helpers_spec.rb432
-rw-r--r--spec/requests/api/merge_requests_spec.rb24
-rw-r--r--spec/requests/api/runner_spec.rb16
-rw-r--r--spec/requests/api/session_spec.rb107
-rw-r--r--spec/requests/api/users_spec.rb30
-rw-r--r--spec/routing/project_routing_spec.rb19
-rw-r--r--spec/routing/routing_spec.rb5
-rw-r--r--spec/serializers/cluster_entity_spec.rb38
-rw-r--r--spec/serializers/cluster_serializer_spec.rb19
-rw-r--r--spec/serializers/issue_entity_spec.rb20
-rw-r--r--spec/serializers/issue_serializer_spec.rb27
-rw-r--r--spec/serializers/merge_request_basic_serializer_spec.rb10
-rw-r--r--spec/serializers/merge_request_entity_spec.rb11
-rw-r--r--spec/serializers/merge_request_serializer_spec.rb12
-rw-r--r--spec/services/applications/create_service_spec.rb13
-rw-r--r--spec/services/ci/create_cluster_service_spec.rb47
-rw-r--r--spec/services/ci/fetch_gcp_operation_service_spec.rb36
-rw-r--r--spec/services/ci/finalize_cluster_creation_service_spec.rb61
-rw-r--r--spec/services/ci/integrate_cluster_service_spec.rb42
-rw-r--r--spec/services/ci/provision_cluster_service_spec.rb85
-rw-r--r--spec/services/ci/update_cluster_service_spec.rb37
-rw-r--r--spec/services/clusters/create_service_spec.rb117
-rw-r--r--spec/services/clusters/gcp/fetch_operation_service_spec.rb43
-rw-r--r--spec/services/clusters/gcp/finalize_creation_service_spec.rb111
-rw-r--r--spec/services/clusters/gcp/provision_service_spec.rb69
-rw-r--r--spec/services/clusters/gcp/verify_provision_status_service_spec.rb107
-rw-r--r--spec/services/clusters/update_service_spec.rb59
-rw-r--r--spec/services/issuable/common_system_notes_service_spec.rb49
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb49
-rw-r--r--spec/services/milestones/promote_service_spec.rb77
-rw-r--r--spec/services/projects/group_links/create_service_spec.rb22
-rw-r--r--spec/services/projects/group_links/destroy_service_spec.rb16
-rw-r--r--spec/services/projects/hashed_storage_migration_service_spec.rb2
-rw-r--r--spec/services/projects/unlink_fork_service_spec.rb18
-rw-r--r--spec/services/system_hooks_service_spec.rb44
-rw-r--r--spec/spec_helper.rb4
-rw-r--r--spec/support/api_helpers.rb28
-rw-r--r--spec/support/bare_repo_operations.rb60
-rw-r--r--spec/support/capybara.rb44
-rw-r--r--spec/support/capybara_helpers.rb2
-rw-r--r--spec/support/cookie_helper.rb17
-rw-r--r--spec/support/features/discussion_comments_shared_example.rb27
-rw-r--r--spec/support/features/issuable_slash_commands_shared_examples.rb4
-rw-r--r--spec/support/features/reportable_note_shared_examples.rb2
-rw-r--r--spec/support/gitlab-git-test.git/objects/88/3e379fcaa5f818fca81cdbabd7a497794d6535bin0 -> 304 bytes
-rw-r--r--spec/support/gitlab-git-test.git/objects/c8/b1ab16c858c67b680eea4644cf652485f555cfbin0 -> 597 bytes
-rw-r--r--spec/support/gitlab-git-test.git/objects/e3/7697aea12699f0b44544332a7c0f41ace5fb162
-rw-r--r--spec/support/gitlab-git-test.git/objects/eb/a0c153ed20d927bab00507f356043b6b4be31ebin0 -> 185 bytes
-rw-r--r--spec/support/gitlab-git-test.git/objects/f6/5ad228d96e2a2ae7088e8557fe8906f6dd2b3fbin0 -> 642 bytes
-rw-r--r--spec/support/gitlab_stubs/session.json4
-rw-r--r--spec/support/gitlab_stubs/user.json6
-rw-r--r--spec/support/google_api/cloud_platform_helpers.rb155
-rw-r--r--spec/support/helpers/merge_request_diff_helpers.rb2
-rw-r--r--spec/support/helpers/note_interaction_helpers.rb2
-rw-r--r--spec/support/input_helper.rb7
-rw-r--r--spec/support/inspect_requests.rb17
-rw-r--r--spec/support/kubernetes_helpers.rb37
-rw-r--r--spec/support/live_debugger.rb17
-rw-r--r--spec/support/login_helpers.rb18
-rw-r--r--spec/support/mobile_helpers.rb2
-rw-r--r--spec/support/protected_tags/access_control_ce_shared_examples.rb2
-rw-r--r--spec/support/quick_actions_helpers.rb2
-rw-r--r--spec/support/shared_examples/features/protected_branches_access_control_ce.rb6
-rw-r--r--spec/support/stub_configuration.rb4
-rw-r--r--spec/support/test_env.rb2
-rw-r--r--spec/support/time_tracking_shared_examples.rb2
-rw-r--r--spec/support/update_invalid_issuable.rb27
-rw-r--r--spec/support/wait_for_requests.rb36
-rw-r--r--spec/tasks/gitlab/users_rake_spec.rb38
-rw-r--r--spec/tasks/tokens_spec.rb6
-rw-r--r--spec/uploaders/file_uploader_spec.rb52
-rw-r--r--spec/views/shared/issuable/_participants.html.haml.rb26
-rw-r--r--spec/workers/cluster_provision_worker_spec.rb19
-rw-r--r--spec/workers/stuck_merge_jobs_worker_spec.rb9
-rw-r--r--spec/workers/wait_for_cluster_creation_worker_spec.rb61
-rw-r--r--vendor/assets/javascripts/autosize.js243
-rw-r--r--vendor/assets/javascripts/fuzzaldrin-plus.js1161
-rw-r--r--vendor/assets/javascripts/peek.js28
-rw-r--r--yarn.lock20
977 files changed, 15020 insertions, 10136 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 87d73fc0c52..fed5971233d 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,7 +1,7 @@
-image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.13-phantomjs-2.1-node-8.x-yarn-1.0-postgresql-9.6"
+image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.5-golang-1.8-git-2.13-chrome-62.0-node-8.x-yarn-1.2-postgresql-9.6"
.default-cache: &default-cache
- key: "ruby-233-with-yarn"
+ key: "ruby-235-with-yarn"
paths:
- vendor/ruby
- .yarn-cache/
@@ -23,7 +23,6 @@ variables:
SIMPLECOV: "true"
GIT_DEPTH: "20"
GIT_SUBMODULE_STRATEGY: "none"
- PHANTOMJS_VERSION: "2.1.1"
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
@@ -455,7 +454,7 @@ db:migrate:reset-mysql:
variables:
SETUP_DB: "false"
script:
- - git fetch origin v8.14.10
+ - git fetch https://gitlab.com/gitlab-org/gitlab-ce.git v9.3.0
- git checkout -f FETCH_HEAD
- bundle install $BUNDLE_INSTALL_FLAGS
- cp config/gitlab.yml.example config/gitlab.yml
@@ -551,7 +550,6 @@ karma:
<<: *dedicated-runner
<<: *except-docs
<<: *pull-cache
- image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.13-chrome-61.0-node-8.x-yarn-1.0-postgresql-9.6"
stage: test
variables:
BABEL_ENV: "coverage"
diff --git a/.gitlab/route-map.yml b/.gitlab/route-map.yml
new file mode 100644
index 00000000000..0b37dc68f8b
--- /dev/null
+++ b/.gitlab/route-map.yml
@@ -0,0 +1,3 @@
+# Documentation
+- source: /doc/(.+?)\.md/ # doc/administration/build_artifacts.md
+ public: '\1.html' # doc/administration/build_artifacts.html
diff --git a/.nvmrc b/.nvmrc
index 72906051c5c..f7ee06693c1 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-7.5 \ No newline at end of file
+9.0.0
diff --git a/.ruby-version b/.ruby-version
index 0bee604df76..cc6c9a491e0 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-2.3.3
+2.3.5
diff --git a/.scss-lint.yml b/.scss-lint.yml
index 73f8d27f78c..16a168b7c60 100644
--- a/.scss-lint.yml
+++ b/.scss-lint.yml
@@ -112,7 +112,7 @@ linters:
# Reports when you define the same selector twice in a single sheet.
MergeableSelector:
- enabled: false
+ enabled: true
# Functions, mixins, variables, and placeholders should be declared
# with all lowercase letters and hyphens instead of underscores.
@@ -121,7 +121,8 @@ linters:
# Avoid nesting selectors too deeply.
NestingDepth:
- enabled: false
+ enabled: true
+ max_depth: 6
# Always use placeholder selectors in @extend.
PlaceholderInExtend:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6bca9944bb1..2f13eca2caf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,30 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 10.1.1 (2017-10-31)
+
+- [FIXED] Auto Devops kubernetes default namespace is now correctly built out of gitlab project group-name. !14642 (Mircea Danila Dumitrescu)
+- [FIXED] Forbid the usage of `Redis#keys`. !14889
+- [FIXED] Make the circuitbreaker more robust by adding higher thresholds, and multiple access attempts. !14933
+- [FIXED] Only cache last push event for existing projects when pushing to a fork. !14989
+- [FIXED] Fix bug preventing secondary emails from being confirmed. !15010
+- [FIXED] Fix broken wiki pages that link to a wiki file. !15019
+- [FIXED] Don't rename paths that were freed up when upgrading. !15029
+- [FIXED] Fix bitbucket login. !15051
+- [FIXED] Update gitaly in Gitlab 10.1 to 0.43.1 for temp file cleanup. !15055
+- [FIXED] Use the correct visibility attribute for projects in system hooks. !15065
+- [FIXED] Normalize LDAP DN when looking up identity.
+- [FIXED] Adds callback functions for initial request in clusters page.
+- [FIXED] Fix missing Import/Export issue assignees.
+- [FIXED] Allow boards as top level route.
+- [FIXED] Fix widget of locked merge requests not being presented.
+- [FIXED] Fix editing issue description in mobile view.
+- [FIXED] Fix deletion of container registry or images returning an error.
+- [FIXED] Fix the writing of invalid environment refs.
+- [CHANGED] Store circuitbreaker settings in the database instead of config. !14842
+- [CHANGED] Update default disabled merge request widget message to reflect a general failure. !14960
+- [PERFORMANCE] Stop merge requests with thousands of commits from timing out. !15063
+
## 10.1.0 (2017-10-22)
- [SECURITY] Use a timeout on certain git operations. !14872
@@ -194,6 +218,24 @@ entry.
- creation of keys moved to services. !13331 (haseebeqx)
- Add username as GL_USERNAME in hooks.
+## 10.0.5 (2017-11-03)
+
+- [FIXED] Fix incorrect X-axis labels in Prometheus graphs. !14258
+- [FIXED] Fix `rake gitlab:incoming_email:check` and make it report the actual error. !14423
+- [FIXED] Does not check if an invariant hashed storage path exists on disk when renaming projects. !14428
+- [FIXED] Fix bottom spacing for dropdowns that open upwards. !14535
+- [FIXED] Fix the project import with issues and milestones. !14657
+- [FIXED] Fix broken Y-axis scaling in some Prometheus graphs. !14693
+- [FIXED] Fixed duplicate notifications when added multiple labels on an issue. !14798
+- [FIXED] Don't rename paths that were freed up when upgrading. !15029
+- [FIXED] Fixed issue/merge request breadcrumb titles not having links.
+- [FIXED] Fix application setting to cache nil object.
+- [FIXED] Fix missing Import/Export issue assignees.
+- [FIXED] Allow boards as top level route.
+- [FIXED] Fixed milestone breadcrumb links.
+- [FIXED] Fixed merge request widget merged & closed date tooltip text.
+- [FIXED] fix merge request widget status icon for failed CI.
+
## 10.0.4 (2017-10-16)
- [SECURITY] Move project repositories between namespaces when renaming users.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9b2ee157193..c4e5fd842df 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,9 +1,13 @@
-## Contributor license agreement
+## Developer Certificate of Origin + License
-By submitting code as an individual you agree to the
-[individual contributor license agreement](doc/legal/individual_contributor_license_agreement.md).
-By submitting code as an entity you agree to the
-[corporate contributor license agreement](doc/legal/corporate_contributor_license_agreement.md).
+By contributing to GitLab B.V., You accept and agree to the following terms and
+conditions for Your present and future Contributions submitted to GitLab B.V.
+Except for the license granted herein to GitLab B.V. and recipients of software
+distributed by GitLab B.V., You reserve all right, title, and interest in and to
+Your Contributions. All Contributions are subject to the following DCO + License
+terms.
+
+[DCO + License](https://gitlab.com/gitlab-org/dco/blob/master/README.md)
_This notice should stay as the first item in the CONTRIBUTING.md file._
@@ -100,8 +104,7 @@ the remaining issues on the GitHub issue tracker.
## I want to contribute!
-If you want to contribute to GitLab, but are not sure where to start,
-look for [issues with the label `Accepting Merge Requests` and small weight][accepting-mrs-weight].
+If you want to contribute to GitLab, [issues with the label `Accepting Merge Requests` and small weight][accepting-mrs-weight] is a great place to start. Issues with a lower weight (1 or 2) are deemed suitable for beginners.
These issues will be of reasonable size and challenge, for anyone to start
contributing to GitLab.
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index fbaaafa001b..c5d4cee36a1 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.49.0 \ No newline at end of file
+0.51.0
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 944880fa15e..15a27998172 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-3.2.0
+3.3.0
diff --git a/Gemfile b/Gemfile
index 5adba4d58df..63d3d214a5a 100644
--- a/Gemfile
+++ b/Gemfile
@@ -90,7 +90,7 @@ gem 'kaminari', '~> 1.0'
gem 'hamlit', '~> 2.6.1'
# Files attachments
-gem 'carrierwave', '~> 1.1'
+gem 'carrierwave', '~> 1.2'
# Drag and Drop UI
gem 'dropzonejs-rails', '~> 0.7.1'
@@ -281,7 +281,7 @@ group :metrics do
gem 'influxdb', '~> 0.2', require: false
# Prometheus
- gem 'prometheus-client-mmap', '~>0.7.0.beta17'
+ gem 'prometheus-client-mmap', '~>0.7.0.beta18'
gem 'raindrops', '~> 0.18'
end
@@ -324,9 +324,9 @@ group :development, :test do
# Generate Fake data
gem 'ffaker', '~> 2.4'
- gem 'capybara', '~> 2.15.0'
+ gem 'capybara', '~> 2.15'
gem 'capybara-screenshot', '~> 1.0.0'
- gem 'poltergeist', '~> 1.9.0'
+ gem 'selenium-webdriver', '~> 3.5'
gem 'spring', '~> 2.0.0'
gem 'spring-commands-rspec', '~> 1.0.4'
@@ -398,7 +398,7 @@ group :ed25519 do
end
# Gitaly GRPC client
-gem 'gitaly-proto', '~> 0.48.0', require: 'gitaly'
+gem 'gitaly-proto', '~> 0.51.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index 53efb1c76c2..ae145ca5f69 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -107,18 +107,19 @@ GEM
capybara-screenshot (1.0.14)
capybara (>= 1.0, < 3)
launchy
- carrierwave (1.1.0)
+ carrierwave (1.2.1)
activemodel (>= 4.0.0)
activesupport (>= 4.0.0)
mime-types (>= 1.16)
cause (0.1)
charlock_holmes (0.7.5)
+ childprocess (0.7.0)
+ ffi (~> 1.0, >= 1.0.11)
chronic (0.10.2)
chronic_duration (0.10.6)
numerizer (~> 0.1.1)
chunky_png (1.3.5)
citrus (3.0.2)
- cliver (0.3.2)
coderay (1.1.1)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
@@ -273,7 +274,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
- gitaly-proto (0.48.0)
+ gitaly-proto (0.51.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
@@ -291,7 +292,7 @@ GEM
diff-lcs (~> 1.1)
mime-types (>= 1.16)
posix-spawn (~> 0.3)
- gitlab-markup (1.6.2)
+ gitlab-markup (1.6.3)
gitlab_omniauth-ldap (2.0.4)
net-ldap (~> 0.16)
omniauth (~> 1.3)
@@ -604,11 +605,6 @@ GEM
pg (0.18.4)
po_to_json (1.0.1)
json (>= 1.6.0)
- poltergeist (1.9.0)
- capybara (~> 2.1)
- cliver (~> 0.3.1)
- multi_json (~> 1.0)
- websocket-driver (>= 0.2.0)
posix-spawn (0.3.13)
powerpack (0.1.1)
premailer (1.10.4)
@@ -623,7 +619,7 @@ GEM
parser
unparser
procto (0.0.3)
- prometheus-client-mmap (0.7.0.beta17)
+ prometheus-client-mmap (0.7.0.beta18)
mmap2 (~> 2.2, >= 2.2.7)
pry (0.10.4)
coderay (~> 1.1.0)
@@ -818,6 +814,9 @@ GEM
activesupport (>= 3.1)
select2-rails (3.5.9.3)
thor (~> 0.14)
+ selenium-webdriver (3.5.0)
+ childprocess (~> 0.5)
+ rubyzip (~> 1.0)
sentry-raven (2.5.3)
faraday (>= 0.7.6, < 1.0)
settingslogic (2.0.9)
@@ -949,9 +948,6 @@ GEM
hashdiff
webpack-rails (0.9.10)
railties (>= 3.2.0)
- websocket-driver (0.6.3)
- websocket-extensions (>= 0.1.0)
- websocket-extensions (0.1.2)
wikicloth (0.8.1)
builder
expression_parser
@@ -988,9 +984,9 @@ DEPENDENCIES
browser (~> 2.2)
bullet (~> 5.5.0)
bundler-audit (~> 0.5.0)
- capybara (~> 2.15.0)
+ capybara (~> 2.15)
capybara-screenshot (~> 1.0.0)
- carrierwave (~> 1.1)
+ carrierwave (~> 1.2)
charlock_holmes (~> 0.7.5)
chronic (~> 0.10.2)
chronic_duration (~> 0.10.6)
@@ -1030,7 +1026,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
- gitaly-proto (~> 0.48.0)
+ gitaly-proto (~> 0.51.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.6.2)
@@ -1104,9 +1100,8 @@ DEPENDENCIES
peek-redis (~> 1.2.0)
peek-sidekiq (~> 1.0.3)
pg (~> 0.18.2)
- poltergeist (~> 1.9.0)
premailer-rails (~> 1.9.7)
- prometheus-client-mmap (~> 0.7.0.beta17)
+ prometheus-client-mmap (~> 0.7.0.beta18)
pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1)
@@ -1150,6 +1145,7 @@ DEPENDENCIES
scss_lint (~> 0.54.0)
seed-fu (~> 2.3.5)
select2-rails (~> 3.5.9)
+ selenium-webdriver (~> 3.5)
sentry-raven (~> 2.5.3)
settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6)
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 242b3e2b990..d963101028a 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -16,6 +16,7 @@ const Api = {
usersPath: '/api/:version/users.json',
commitPath: '/api/:version/projects/:id/repository/commits',
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
+ createBranchPath: '/api/:version/projects/:id/repository/branches',
group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath)
diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js
index 4d2d4db7c0e..0f28bd233ac 100644
--- a/app/assets/javascripts/autosave.js
+++ b/app/assets/javascripts/autosave.js
@@ -1,8 +1,9 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, quotes, prefer-template, no-var, one-var, no-unused-vars, one-var-declaration-per-line, no-void, consistent-return, no-empty, max-len */
+/* eslint-disable no-param-reassign, prefer-template, no-var, no-void, consistent-return */
+
import AccessorUtilities from './lib/utils/accessor';
-window.Autosave = (function() {
- function Autosave(field, key, resource) {
+export default class Autosave {
+ constructor(field, key, resource) {
this.field = field;
this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
this.resource = resource;
@@ -12,14 +13,10 @@ window.Autosave = (function() {
this.key = 'autosave/' + key;
this.field.data('autosave', this);
this.restore();
- this.field.on('input', (function(_this) {
- return function() {
- return _this.save();
- };
- })(this));
+ this.field.on('input', () => this.save());
}
- Autosave.prototype.restore = function() {
+ restore() {
var text;
if (!this.isLocalStorageAvailable) return;
@@ -40,9 +37,9 @@ window.Autosave = (function() {
field.dispatchEvent(event);
}
}
- };
+ }
- Autosave.prototype.save = function() {
+ save() {
var text;
text = this.field.val();
@@ -51,15 +48,11 @@ window.Autosave = (function() {
}
return this.reset();
- };
+ }
- Autosave.prototype.reset = function() {
+ reset() {
if (!this.isLocalStorageAvailable) return;
return window.localStorage.removeItem(this.key);
- };
-
- return Autosave;
-})();
-
-export default window.Autosave;
+ }
+}
diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js
index e00af4b2fa8..add43b81f6d 100644
--- a/app/assets/javascripts/behaviors/autosize.js
+++ b/app/assets/javascripts/behaviors/autosize.js
@@ -1,8 +1,8 @@
-import autosize from 'vendor/autosize';
+import Autosize from 'autosize';
document.addEventListener('DOMContentLoaded', () => {
const autosizeEls = document.querySelectorAll('.js-autosize');
- autosize(autosizeEls);
- autosize.update(autosizeEls);
+ Autosize(autosizeEls);
+ Autosize.update(autosizeEls);
});
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index c1f902a785a..9ae5e270a4b 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -1,7 +1,5 @@
/* eslint-disable comma-dangle, space-before-function-paren, no-new */
-/* global IssuableContext */
/* global MilestoneSelect */
-/* global LabelsSelect */
/* global Sidebar */
import Vue from 'vue';
@@ -11,6 +9,8 @@ import AssigneeTitle from '../../sidebar/components/assignees/assignee_title';
import Assignees from '../../sidebar/components/assignees/assignees';
import DueDateSelectors from '../../due_date_select';
import './sidebar/remove_issue';
+import IssuableContext from '../../issuable_context';
+import LabelsSelect from '../../labels_select';
const Store = gl.issueBoards.BoardsStore;
diff --git a/app/assets/javascripts/clusters.js b/app/assets/javascripts/clusters.js
index 180aa30e98c..c9fef94efea 100644
--- a/app/assets/javascripts/clusters.js
+++ b/app/assets/javascripts/clusters.js
@@ -1,6 +1,7 @@
/* globals Flash */
import Visibility from 'visibilityjs';
import axios from 'axios';
+import setAxiosCsrfToken from './lib/utils/axios_utils';
import Poll from './lib/utils/poll';
import { s__ } from './locale';
import initSettingsPanels from './settings_panels';
@@ -17,6 +18,7 @@ import Flash from './flash';
class ClusterService {
constructor(options = {}) {
this.options = options;
+ setAxiosCsrfToken();
}
fetchData() {
return axios.get(this.options.endpoint);
@@ -64,19 +66,16 @@ export default class Clusters {
this.poll = new Poll({
resource: this.service,
method: 'fetchData',
- successCallback: (data) => {
- const { status, status_reason } = data.data;
- this.updateContainer(status, status_reason);
- },
- errorCallback: () => {
- Flash(s__('ClusterIntegration|Something went wrong on our end.'));
- },
+ successCallback: data => this.handleSuccess(data),
+ errorCallback: () => Clusters.handleError(),
});
if (!Visibility.hidden()) {
this.poll.makeRequest();
} else {
- this.service.fetchData();
+ this.service.fetchData()
+ .then(data => this.handleSuccess(data))
+ .catch(() => Clusters.handleError());
}
Visibility.change(() => {
@@ -88,6 +87,15 @@ export default class Clusters {
});
}
+ static handleError() {
+ Flash(s__('ClusterIntegration|Something went wrong on our end.'));
+ }
+
+ handleSuccess(data) {
+ const { status, status_reason } = data.data;
+ this.updateContainer(status, status_reason);
+ }
+
hideAll() {
this.errorContainer.classList.add('hidden');
this.successContainer.classList.add('hidden');
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 970e83c0ecb..760fb0cdf67 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -1,9 +1,9 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */
/* global ProjectSelect */
-/* global IssuableIndex */
+import IssuableIndex from './issuable_index';
/* global Milestone */
-/* global IssuableForm */
-/* global LabelsSelect */
+import IssuableForm from './issuable_form';
+import LabelsSelect from './labels_select';
/* global MilestoneSelect */
/* global NewBranchForm */
/* global NotificationsForm */
@@ -16,7 +16,7 @@ import CILintEditor from './ci_lint_editor';
import groupsSelect from './groups_select';
/* global Search */
/* global Admin */
-/* global NamespaceSelects */
+import NamespaceSelect from './namespace_select';
/* global NewCommitForm */
/* global NewBranchForm */
/* global Project */
@@ -173,7 +173,7 @@ import Diff from './diff';
filteredSearchManager.setup();
}
const pagePrefix = page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_';
- IssuableIndex.init(pagePrefix);
+ new IssuableIndex(pagePrefix);
shortcut_handler = new ShortcutsNavigation();
new UsersSelect();
@@ -231,12 +231,16 @@ import Diff from './diff';
case 'projects:milestones:new':
case 'projects:milestones:edit':
case 'projects:milestones:update':
+ new ZenMode();
+ new DueDateSelectors();
+ new GLForm($('.milestone-form'), true);
+ break;
case 'groups:milestones:new':
case 'groups:milestones:edit':
case 'groups:milestones:update':
new ZenMode();
new DueDateSelectors();
- new GLForm($('.milestone-form'), true);
+ new GLForm($('.milestone-form'), false);
break;
case 'projects:compare:show':
new Diff();
@@ -571,7 +575,8 @@ import Diff from './diff';
new UsersSelect();
break;
case 'projects':
- new NamespaceSelects();
+ document.querySelectorAll('.js-namespace-select')
+ .forEach(dropdown => new NamespaceSelect({ dropdown }));
break;
case 'labels':
switch (path[2]) {
diff --git a/app/assets/javascripts/droplab/plugins/filter.js b/app/assets/javascripts/droplab/plugins/filter.js
index d6a1aadd49c..404d707cf7a 100644
--- a/app/assets/javascripts/droplab/plugins/filter.js
+++ b/app/assets/javascripts/droplab/plugins/filter.js
@@ -79,8 +79,6 @@ const Filter = {
this.hook.trigger.addEventListener('keydown.dl', this.eventWrapper.debounceKeydown);
this.hook.trigger.addEventListener('mousedown.dl', this.eventWrapper.debounceKeydown);
-
- this.debounceKeydown({ detail: { hook: this.hook } });
},
destroy: function destroy() {
diff --git a/app/assets/javascripts/droplab/utils.js b/app/assets/javascripts/droplab/utils.js
index 4da7344604e..bfe056a0fcc 100644
--- a/app/assets/javascripts/droplab/utils.js
+++ b/app/assets/javascripts/droplab/utils.js
@@ -30,7 +30,7 @@ const utils = {
},
isDropDownParts(target) {
- if (!target || target.tagName === 'HTML') return false;
+ if (!target || !target.hasAttribute || target.tagName === 'HTML') return false;
return target.hasAttribute(DATA_TRIGGER) || target.hasAttribute(DATA_DROPDOWN);
},
};
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index 7a17adcd44e..b7747ee3f83 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -119,11 +119,9 @@ export default function dropzoneInput(form) {
// removeAllFiles(true) stops uploading files (if any)
// and remove them from dropzone files queue.
$cancelButton.on('click', (e) => {
- const target = e.target.closest('.js-main-target-form').querySelector('.div-dropzone');
-
e.preventDefault();
e.stopPropagation();
- Dropzone.forElement(target).removeAllFiles(true);
+ Dropzone.forElement($formDropzone.get(0)).removeAllFiles(true);
});
// If 'error' event is fired, we store a failed files,
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index 6de01fa53d0..fc0308b81ba 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -421,7 +421,11 @@ export default {
</script>
<template>
<div
- :class="{ 'js-child-row environment-child-row': model.isChildren, 'folder-row': model.isFolder, 'gl-responsive-table-row': !model.isFolder }"
+ class="gl-responsive-table-row"
+ :class="{
+ 'js-child-row environment-child-row': model.isChildren,
+ 'folder-row': model.isFolder,
+ }"
role="row">
<div class="table-section section-10" role="gridcell">
<div
@@ -495,15 +499,16 @@ export default {
</a>
</div>
- <div class="table-section section-25" role="gridcell">
+ <div
+ v-if="!model.isFolder"
+ class="table-section section-25" role="gridcell">
<div
- v-if="!model.isFolder"
role="rowheader"
class="table-mobile-header">
Commit
</div>
<div
- v-if="!model.isFolder && hasLastDeploymentKey"
+ v-if="hasLastDeploymentKey"
class="js-commit-component table-mobile-content">
<commit-component
:tag="commitTag"
@@ -514,21 +519,22 @@ export default {
:author="commitAuthor"/>
</div>
<div
- v-if="!model.isFolder && !hasLastDeploymentKey"
+ v-if="!hasLastDeploymentKey"
class="commit-title table-mobile-content">
No deployments yet
</div>
</div>
- <div class="table-section section-10" role="gridcell">
+ <div
+ v-if="!model.isFolder"
+ class="table-section section-10" role="gridcell">
<div
- v-if="!model.isFolder"
role="rowheader"
class="table-mobile-header">
Updated
</div>
<span
- v-if="!model.isFolder && canShowDate"
+ v-if="canShowDate"
class="environment-created-date-timeago table-mobile-content">
{{createdDate}}
</span>
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index e8d8fef8579..c4202f92443 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -1,6 +1,7 @@
/* eslint-disable func-names, no-underscore-dangle, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, prefer-rest-params, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func, no-mixed-operators */
/* global fuzzaldrinPlus */
import _ from 'underscore';
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { isObject } from './lib/utils/type_utility';
var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, GitLabDropdownInput;
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js
index cdc4fcf6573..e7232ca3712 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors.js
@@ -4,6 +4,7 @@ import _ from 'underscore';
import d3 from 'd3';
import { ContributorsGraph, ContributorsAuthorGraph, ContributorsMasterGraph } from './stat_graph_contributors_graph';
import ContributorsStatGraphUtil from './stat_graph_contributors_util';
+import { n__ } from '../locale';
export default (function() {
function ContributorsStatGraph() {}
@@ -44,7 +45,7 @@ export default (function() {
commits = $('<span/>', {
"class": 'graph-author-commits-count'
});
- commits.text(author.commits + " commits");
+ commits.text(n__('%d commit', '%d commits', author.commits));
return $('<span/>').append(commits);
};
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index ea2e2205077..33a352e158a 100644
--- a/app/assets/javascripts/header.js
+++ b/app/assets/javascripts/header.js
@@ -7,10 +7,12 @@ import { highCountTrim } from '~/lib/utils/text_utility';
* @param {jQuery.Event} e
* @param {String} count
*/
-$(document).on('todo:toggle', (e, count) => {
- const parsedCount = parseInt(count, 10);
- const $todoPendingCount = $('.todos-count');
+export default function initTodoToggle() {
+ $(document).on('todo:toggle', (e, count) => {
+ const parsedCount = parseInt(count, 10);
+ const $todoPendingCount = $('.todos-count');
- $todoPendingCount.text(highCountTrim(parsedCount));
- $todoPendingCount.toggleClass('hidden', parsedCount === 0);
-});
+ $todoPendingCount.text(highCountTrim(parsedCount));
+ $todoPendingCount.toggleClass('hidden', parsedCount === 0);
+ });
+}
diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js
index 5b4ca94ed30..1dc70872d92 100644
--- a/app/assets/javascripts/importer_status.js
+++ b/app/assets/javascripts/importer_status.js
@@ -1,83 +1,81 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, camelcase, no-var, one-var, one-var-declaration-per-line, prefer-template, quotes, object-shorthand, comma-dangle, no-unused-vars, prefer-arrow-callback, no-else-return, vars-on-top, no-new, max-len */
+class ImporterStatus {
+ constructor(jobsUrl, importUrl) {
+ this.jobsUrl = jobsUrl;
+ this.importUrl = importUrl;
+ this.initStatusPage();
+ this.setAutoUpdate();
+ }
-(function() {
- window.ImporterStatus = (function() {
- function ImporterStatus(jobs_url, import_url) {
- this.jobs_url = jobs_url;
- this.import_url = import_url;
- this.initStatusPage();
- this.setAutoUpdate();
- }
+ initStatusPage() {
+ $('.js-add-to-import')
+ .off('click')
+ .on('click', (event) => {
+ const $btn = $(event.currentTarget);
+ const $tr = $btn.closest('tr');
+ const $targetField = $tr.find('.import-target');
+ const $namespaceInput = $targetField.find('.js-select-namespace option:selected');
+ const id = $tr.attr('id').replace('repo_', '');
+ let targetNamespace;
+ let newName;
+ if ($namespaceInput.length > 0) {
+ targetNamespace = $namespaceInput[0].innerHTML;
+ newName = $targetField.find('#path').prop('value');
+ $targetField.empty().append(`${targetNamespace}/${newName}`);
+ }
+ $btn.disable().addClass('is-loading');
- ImporterStatus.prototype.initStatusPage = function() {
- $('.js-add-to-import').off('click').on('click', (function(_this) {
- return function(e) {
- var $btn, $namespace_input, $target_field, $tr, id, target_namespace, newName;
- $btn = $(e.currentTarget);
- $tr = $btn.closest('tr');
- $target_field = $tr.find('.import-target');
- $namespace_input = $target_field.find('.js-select-namespace option:selected');
- id = $tr.attr('id').replace('repo_', '');
- target_namespace = null;
- newName = null;
- if ($namespace_input.length > 0) {
- target_namespace = $namespace_input[0].innerHTML;
- newName = $target_field.find('#path').prop('value');
- $target_field.empty().append(target_namespace + "/" + newName);
- }
- $btn.disable().addClass('is-loading');
- return $.post(_this.import_url, {
- repo_id: id,
- target_namespace: target_namespace,
- new_name: newName
- }, {
- dataType: 'script'
- });
- };
- })(this));
- return $('.js-import-all').off('click').on('click', function(e) {
- var $btn;
- $btn = $(this);
+ return $.post(this.importUrl, {
+ repo_id: id,
+ target_namespace: targetNamespace,
+ new_name: newName,
+ }, {
+ dataType: 'script',
+ });
+ });
+
+ $('.js-import-all')
+ .off('click')
+ .on('click', function onClickImportAll() {
+ const $btn = $(this);
$btn.disable().addClass('is-loading');
- return $('.js-add-to-import').each(function() {
+ return $('.js-add-to-import').each(function triggerAddImport() {
return $(this).trigger('click');
});
});
- };
+ }
+
+ setAutoUpdate() {
+ return setInterval(() => $.get(this.jobsUrl, data => $.each(data, (i, job) => {
+ const jobItem = $(`#project_${job.id}`);
+ const statusField = jobItem.find('.job-status');
- ImporterStatus.prototype.setAutoUpdate = function() {
- return setInterval(((function(_this) {
- return function() {
- return $.get(_this.jobs_url, function(data) {
- return $.each(data, function(i, job) {
- var job_item, status_field;
- job_item = $("#project_" + job.id);
- status_field = job_item.find(".job-status");
- if (job.import_status === 'finished') {
- job_item.removeClass("active").addClass("success");
- return status_field.html('<span><i class="fa fa-check"></i> done</span>');
- } else if (job.import_status === 'scheduled') {
- return status_field.html("<i class='fa fa-spinner fa-spin'></i> scheduled");
- } else if (job.import_status === 'started') {
- return status_field.html("<i class='fa fa-spinner fa-spin'></i> started");
- } else {
- return status_field.html(job.import_status);
- }
- });
- });
- };
- })(this)), 4000);
- };
+ const spinner = '<i class="fa fa-spinner fa-spin"></i>';
- return ImporterStatus;
- })();
+ switch (job.import_status) {
+ case 'finished':
+ jobItem.removeClass('active').addClass('success');
+ statusField.html('<span><i class="fa fa-check"></i> done</span>');
+ break;
+ case 'scheduled':
+ statusField.html(`${spinner} scheduled`);
+ break;
+ case 'started':
+ statusField.html(`${spinner} started`);
+ break;
+ default:
+ statusField.html(job.import_status);
+ break;
+ }
+ })), 4000);
+ }
+}
- $(function() {
- if ($('.js-importer-status').length) {
- var jobsImportPath = $('.js-importer-status').data('jobs-import-path');
- var importPath = $('.js-importer-status').data('import-path');
+// eslint-disable-next-line consistent-return
+export default function initImporterStatus() {
+ const importerStatus = document.querySelector('.js-importer-status');
- new window.ImporterStatus(jobsImportPath, importPath);
- }
- });
-}).call(window);
+ if (importerStatus) {
+ const data = importerStatus.dataset;
+ return new ImporterStatus(data.jobsImportPath, data.importPath);
+ }
+}
diff --git a/app/assets/javascripts/init_issuable_sidebar.js b/app/assets/javascripts/init_issuable_sidebar.js
index 32a1a269f9a..1191e0b895e 100644
--- a/app/assets/javascripts/init_issuable_sidebar.js
+++ b/app/assets/javascripts/init_issuable_sidebar.js
@@ -1,7 +1,7 @@
/* eslint-disable no-new */
/* global MilestoneSelect */
-/* global LabelsSelect */
-/* global IssuableContext */
+import LabelsSelect from './labels_select';
+import IssuableContext from './issuable_context';
/* global Sidebar */
import DueDateSelectors from './due_date_select';
diff --git a/app/assets/javascripts/init_legacy_filters.js b/app/assets/javascripts/init_legacy_filters.js
index 1211c2c802c..1b265721581 100644
--- a/app/assets/javascripts/init_legacy_filters.js
+++ b/app/assets/javascripts/init_legacy_filters.js
@@ -1,15 +1,15 @@
/* eslint-disable no-new */
-/* global LabelsSelect */
+import LabelsSelect from './labels_select';
/* global MilestoneSelect */
-/* global IssueStatusSelect */
/* global SubscriptionSelect */
import UsersSelect from './users_select';
+import issueStatusSelect from './issue_status_select';
export default () => {
new UsersSelect();
new LabelsSelect();
new MilestoneSelect();
- new IssueStatusSelect();
+ issueStatusSelect();
new SubscriptionSelect();
};
diff --git a/app/assets/javascripts/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable_bulk_update_actions.js
index eb15949603f..b124fafec70 100644
--- a/app/assets/javascripts/issuable_bulk_update_actions.js
+++ b/app/assets/javascripts/issuable_bulk_update_actions.js
@@ -1,5 +1,4 @@
/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, max-len, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */
-/* global IssuableIndex */
import _ from 'underscore';
import Flash from './flash';
diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js
index 0e8a0519928..af6358953cf 100644
--- a/app/assets/javascripts/issuable_bulk_update_sidebar.js
+++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js
@@ -1,10 +1,12 @@
/* eslint-disable class-methods-use-this, no-new */
-/* global LabelsSelect */
/* global MilestoneSelect */
-/* global IssueStatusSelect */
/* global SubscriptionSelect */
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
+import './milestone_select';
+import issueStatusSelect from './issue_status_select';
+import './subscription_select';
+import LabelsSelect from './labels_select';
const HIDDEN_CLASS = 'hidden';
const DISABLED_CONTENT_CLASS = 'disabled-content';
@@ -45,7 +47,7 @@ export default class IssuableBulkUpdateSidebar {
initDropdowns() {
new LabelsSelect();
new MilestoneSelect();
- new IssueStatusSelect();
+ issueStatusSelect();
new SubscriptionSelect();
}
diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js
index 73791edaebb..da99394ff90 100644
--- a/app/assets/javascripts/issuable_context.js
+++ b/app/assets/javascripts/issuable_context.js
@@ -1,33 +1,32 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, max-len */
import Cookies from 'js-cookie';
import bp from './breakpoints';
import UsersSelect from './users_select';
-const PARTICIPANTS_ROW_COUNT = 7;
-
-(function() {
- this.IssuableContext = (function() {
- function IssuableContext(currentUser) {
- this.initParticipants();
- new UsersSelect(currentUser);
- $('select.select2').select2({
- width: 'resolve',
- dropdownAutoWidth: true
- });
- $(".issuable-sidebar .inline-update").on("change", "select", function() {
- return $(this).submit();
- });
- $(".issuable-sidebar .inline-update").on("change", ".js-assignee", function() {
- return $(this).submit();
- });
- $(document).off('click', '.issuable-sidebar .dropdown-content a').on('click', '.issuable-sidebar .dropdown-content a', function(e) {
- return e.preventDefault();
- });
- $(document).off('click', '.edit-link').on('click', '.edit-link', function(e) {
- var $block, $selectbox;
+export default class IssuableContext {
+ constructor(currentUser) {
+ this.userSelect = new UsersSelect(currentUser);
+
+ $('select.select2').select2({
+ width: 'resolve',
+ dropdownAutoWidth: true,
+ });
+
+ $('.issuable-sidebar .inline-update').on('change', 'select', function onClickSelect() {
+ return $(this).submit();
+ });
+ $('.issuable-sidebar .inline-update').on('change', '.js-assignee', function onClickAssignee() {
+ return $(this).submit();
+ });
+ $(document)
+ .off('click', '.issuable-sidebar .dropdown-content a')
+ .on('click', '.issuable-sidebar .dropdown-content a', e => e.preventDefault());
+
+ $(document)
+ .off('click', '.edit-link')
+ .on('click', '.edit-link', function onClickEdit(e) {
e.preventDefault();
- $block = $(this).parents('.block');
- $selectbox = $block.find('.selectbox');
+ const $block = $(this).parents('.block');
+ const $selectbox = $block.find('.selectbox');
if ($selectbox.is(':visible')) {
$selectbox.hide();
$block.find('.value').show();
@@ -35,46 +34,18 @@ const PARTICIPANTS_ROW_COUNT = 7;
$selectbox.show();
$block.find('.value').hide();
}
- if ($selectbox.is(':visible')) {
- return setTimeout(function() {
- return $block.find('.dropdown-menu-toggle').trigger('click');
- }, 0);
- }
- });
- window.addEventListener('beforeunload', function() {
- // collapsed_gutter cookie hides the sidebar
- var bpBreakpoint = bp.getBreakpointSize();
- if (bpBreakpoint === 'xs' || bpBreakpoint === 'sm') {
- Cookies.set('collapsed_gutter', true);
- }
- });
- }
- IssuableContext.prototype.initParticipants = function() {
- $(document).on('click', '.js-participants-more', this.toggleHiddenParticipants);
- return $('.js-participants-author').each(function(i) {
- if (i >= PARTICIPANTS_ROW_COUNT) {
- return $(this).addClass('js-participants-hidden').hide();
+ if ($selectbox.is(':visible')) {
+ setTimeout(() => $block.find('.dropdown-menu-toggle').trigger('click'), 0);
}
});
- };
-
- IssuableContext.prototype.toggleHiddenParticipants = function() {
- const currentText = $(this).text().trim();
- const lessText = $(this).data('less-text');
- const originalText = $(this).data('original-text');
- if (currentText === originalText) {
- $(this).text(lessText);
-
- if (gl.lazyLoader) gl.lazyLoader.loadCheck();
- } else {
- $(this).text(originalText);
+ window.addEventListener('beforeunload', () => {
+ // collapsed_gutter cookie hides the sidebar
+ const bpBreakpoint = bp.getBreakpointSize();
+ if (bpBreakpoint === 'xs' || bpBreakpoint === 'sm') {
+ Cookies.set('collapsed_gutter', true);
}
-
- $('.js-participants-hidden').toggle();
- };
-
- return IssuableContext;
- })();
-}).call(window);
+ });
+ }
+}
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index cd2562bc6a9..57dcaa0e1ac 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -1,110 +1,107 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, max-len */
+/* eslint-disable func-names, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, object-shorthand, no-unused-vars, comma-dangle, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, max-len */
/* global GitLab */
-/* global Autosave */
import Pikaday from 'pikaday';
+import Autosave from './autosave';
import UsersSelect from './users_select';
import GfmAutoComplete from './gfm_auto_complete';
import ZenMode from './zen_mode';
import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
-(function() {
- this.IssuableForm = (function() {
- IssuableForm.prototype.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i;
-
- function IssuableForm(form) {
- var $issuableDueDate, calendar;
- this.form = form;
- this.toggleWip = this.toggleWip.bind(this);
- this.renderWipExplanation = this.renderWipExplanation.bind(this);
- this.resetAutosave = this.resetAutosave.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
- new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup();
- new UsersSelect();
- new ZenMode();
- this.titleField = this.form.find("input[name*='[title]']");
- this.descriptionField = this.form.find("textarea[name*='[description]']");
- if (!(this.titleField.length && this.descriptionField.length)) {
- return;
- }
- this.initAutosave();
- this.form.on("submit", this.handleSubmit);
- this.form.on("click", ".btn-cancel", this.resetAutosave);
- this.initWip();
- $issuableDueDate = $('#issuable-due-date');
- if ($issuableDueDate.length) {
- calendar = new Pikaday({
- field: $issuableDueDate.get(0),
- theme: 'gitlab-theme animate-picker',
- format: 'yyyy-mm-dd',
- container: $issuableDueDate.parent().get(0),
- parse: dateString => parsePikadayDate(dateString),
- toString: date => pikadayToString(date),
- onSelect: function(dateText) {
- $issuableDueDate.val(calendar.toString(dateText));
- }
- });
- calendar.setDate(parsePikadayDate($issuableDueDate.val()));
- }
+export default class IssuableForm {
+ constructor(form) {
+ this.form = form;
+ this.toggleWip = this.toggleWip.bind(this);
+ this.renderWipExplanation = this.renderWipExplanation.bind(this);
+ this.resetAutosave = this.resetAutosave.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i;
+
+ new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup();
+ new UsersSelect();
+ new ZenMode();
+
+ this.titleField = this.form.find('input[name*="[title]"]');
+ this.descriptionField = this.form.find('textarea[name*="[description]"]');
+ if (!(this.titleField.length && this.descriptionField.length)) {
+ return;
}
- IssuableForm.prototype.initAutosave = function() {
- new Autosave(this.titleField, [document.location.pathname, document.location.search, "title"]);
- return new Autosave(this.descriptionField, [document.location.pathname, document.location.search, "description"]);
- };
-
- IssuableForm.prototype.handleSubmit = function() {
- return this.resetAutosave();
- };
-
- IssuableForm.prototype.resetAutosave = function() {
- this.titleField.data("autosave").reset();
- return this.descriptionField.data("autosave").reset();
- };
-
- IssuableForm.prototype.initWip = function() {
- this.$wipExplanation = this.form.find(".js-wip-explanation");
- this.$noWipExplanation = this.form.find(".js-no-wip-explanation");
- if (!(this.$wipExplanation.length && this.$noWipExplanation.length)) {
- return;
- }
- this.form.on("click", ".js-toggle-wip", this.toggleWip);
- this.titleField.on("keyup blur", this.renderWipExplanation);
- return this.renderWipExplanation();
- };
-
- IssuableForm.prototype.workInProgress = function() {
- return this.wipRegex.test(this.titleField.val());
- };
-
- IssuableForm.prototype.renderWipExplanation = function() {
- if (this.workInProgress()) {
- this.$wipExplanation.show();
- return this.$noWipExplanation.hide();
- } else {
- this.$wipExplanation.hide();
- return this.$noWipExplanation.show();
- }
- };
-
- IssuableForm.prototype.toggleWip = function(event) {
- event.preventDefault();
- if (this.workInProgress()) {
- this.removeWip();
- } else {
- this.addWip();
- }
- return this.renderWipExplanation();
- };
-
- IssuableForm.prototype.removeWip = function() {
- return this.titleField.val(this.titleField.val().replace(this.wipRegex, ""));
- };
-
- IssuableForm.prototype.addWip = function() {
- return this.titleField.val("WIP: " + (this.titleField.val()));
- };
-
- return IssuableForm;
- })();
-}).call(window);
+ this.initAutosave();
+ this.form.on('submit', this.handleSubmit);
+ this.form.on('click', '.btn-cancel', this.resetAutosave);
+ this.initWip();
+
+ const $issuableDueDate = $('#issuable-due-date');
+
+ if ($issuableDueDate.length) {
+ const calendar = new Pikaday({
+ field: $issuableDueDate.get(0),
+ theme: 'gitlab-theme animate-picker',
+ format: 'yyyy-mm-dd',
+ container: $issuableDueDate.parent().get(0),
+ parse: dateString => parsePikadayDate(dateString),
+ toString: date => pikadayToString(date),
+ onSelect: dateText => $issuableDueDate.val(calendar.toString(dateText)),
+ });
+ calendar.setDate(parsePikadayDate($issuableDueDate.val()));
+ }
+ }
+
+ initAutosave() {
+ new Autosave(this.titleField, [document.location.pathname, document.location.search, 'title']);
+ return new Autosave(this.descriptionField, [document.location.pathname, document.location.search, 'description']);
+ }
+
+ handleSubmit() {
+ return this.resetAutosave();
+ }
+
+ resetAutosave() {
+ this.titleField.data('autosave').reset();
+ return this.descriptionField.data('autosave').reset();
+ }
+
+ initWip() {
+ this.$wipExplanation = this.form.find('.js-wip-explanation');
+ this.$noWipExplanation = this.form.find('.js-no-wip-explanation');
+ if (!(this.$wipExplanation.length && this.$noWipExplanation.length)) {
+ return;
+ }
+ this.form.on('click', '.js-toggle-wip', this.toggleWip);
+ this.titleField.on('keyup blur', this.renderWipExplanation);
+ return this.renderWipExplanation();
+ }
+
+ workInProgress() {
+ return this.wipRegex.test(this.titleField.val());
+ }
+
+ renderWipExplanation() {
+ if (this.workInProgress()) {
+ this.$wipExplanation.show();
+ return this.$noWipExplanation.hide();
+ } else {
+ this.$wipExplanation.hide();
+ return this.$noWipExplanation.show();
+ }
+ }
+
+ toggleWip(event) {
+ event.preventDefault();
+ if (this.workInProgress()) {
+ this.removeWip();
+ } else {
+ this.addWip();
+ }
+ return this.renderWipExplanation();
+ }
+
+ removeWip() {
+ return this.titleField.val(this.titleField.val().replace(this.wipRegex, ''));
+ }
+
+ addWip() {
+ this.titleField.val(`WIP: ${(this.titleField.val())}`);
+ }
+}
diff --git a/app/assets/javascripts/issuable_index.js b/app/assets/javascripts/issuable_index.js
index ece0220c927..0b123a11a3b 100644
--- a/app/assets/javascripts/issuable_index.js
+++ b/app/assets/javascripts/issuable_index.js
@@ -1,171 +1,42 @@
-/* eslint-disable no-param-reassign, func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, wrap-iife, max-len */
-/* global IssuableIndex */
-import _ from 'underscore';
import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
-((global) => {
- var issuable_created;
-
- issuable_created = false;
-
- global.IssuableIndex = {
- init: function(pagePrefix) {
- IssuableIndex.initTemplates();
- IssuableIndex.initSearch();
- IssuableIndex.initBulkUpdate(pagePrefix);
- IssuableIndex.initResetFilters();
- IssuableIndex.resetIncomingEmailToken();
- IssuableIndex.initLabelFilterRemove();
- },
- initTemplates: function() {
- return IssuableIndex.labelRow = _.template('<% _.each(labels, function(label){ %> <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;"> <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body"> <%- label.title %> </a> <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>"> <i class="fa fa-times"></i> </button> </span> <% }); %>');
- },
- initSearch: function() {
- const $searchInput = $('#issuable_search');
-
- IssuableIndex.initSearchState($searchInput);
-
- // `immediate` param set to false debounces on the `trailing` edge, lets user finish typing
- const debouncedExecSearch = _.debounce(IssuableIndex.executeSearch, 1000, false);
-
- $searchInput.off('keyup').on('keyup', debouncedExecSearch);
-
- // ensures existing filters are preserved when manually submitted
- $('#issuable_search_form').on('submit', (e) => {
- e.preventDefault();
- debouncedExecSearch(e);
- });
- },
- initSearchState: function($searchInput) {
- const currentSearchVal = $searchInput.val();
-
- IssuableIndex.searchState = {
- elem: $searchInput,
- current: currentSearchVal
- };
-
- IssuableIndex.maybeFocusOnSearch();
- },
- accessSearchPristine: function(set) {
- // store reference to previous value to prevent search on non-mutating keyup
- const state = IssuableIndex.searchState;
- const currentSearchVal = state.elem.val();
-
- if (set) {
- state.current = currentSearchVal;
- } else {
- return state.current === currentSearchVal;
- }
- },
- maybeFocusOnSearch: function() {
- const currentSearchVal = IssuableIndex.searchState.current;
- if (currentSearchVal && currentSearchVal !== '') {
- const queryLength = currentSearchVal.length;
- const $searchInput = IssuableIndex.searchState.elem;
-
- /* The following ensures that the cursor is initially placed at
- * the end of search input when focus is applied. It accounts
- * for differences in browser implementations of `setSelectionRange`
- * and cursor placement for elements in focus.
- */
- $searchInput.focus();
- if ($searchInput.setSelectionRange) {
- $searchInput.setSelectionRange(queryLength, queryLength);
- } else {
- $searchInput.val(currentSearchVal);
- }
- }
- },
- executeSearch: function(e) {
- const $search = $('#issuable_search');
- const $searchName = $search.attr('name');
- const $searchValue = $search.val();
- const $filtersForm = $('.js-filter-form');
- const $input = $(`input[name='${$searchName}']`, $filtersForm);
- const isPristine = IssuableIndex.accessSearchPristine();
-
- if (isPristine) {
- return;
- }
-
- if (!$input.length) {
- $filtersForm.append(`<input type='hidden' name='${$searchName}' value='${_.escape($searchValue)}'/>`);
- } else {
- $input.val($searchValue);
- }
-
- IssuableIndex.filterResults($filtersForm);
- },
- initLabelFilterRemove: function() {
- return $(document).off('click', '.js-label-filter-remove').on('click', '.js-label-filter-remove', function(e) {
- var $button;
- $button = $(this);
- // Remove the label input box
- $('input[name="label_name[]"]').filter(function() {
- return this.value === $button.data('label');
- }).remove();
- // Submit the form to get new data
- IssuableIndex.filterResults($('.filter-form'));
- });
- },
- filterResults: (function(_this) {
- return function(form) {
- var formAction, formData, issuesUrl;
- formData = form.serializeArray();
- formData = formData.filter(function(data) {
- return data.value !== '';
- });
- formData = $.param(formData);
- formAction = form.attr('action');
- issuesUrl = formAction;
- issuesUrl += "" + (formAction.indexOf('?') === -1 ? '?' : '&');
- issuesUrl += formData;
- return gl.utils.visitUrl(issuesUrl);
- };
- })(this),
- initResetFilters: function() {
- $('.reset-filters').on('click', function(e) {
- e.preventDefault();
- const target = e.target;
- const $form = $(target).parents('.js-filter-form');
- const baseIssuesUrl = target.href;
-
- $form.attr('action', baseIssuesUrl);
- gl.utils.visitUrl(baseIssuesUrl);
+export default class IssuableIndex {
+ constructor(pagePrefix) {
+ this.initBulkUpdate(pagePrefix);
+ IssuableIndex.resetIncomingEmailToken();
+ }
+ initBulkUpdate(pagePrefix) {
+ const userCanBulkUpdate = $('.issues-bulk-update').length > 0;
+ const alreadyInitialized = !!this.bulkUpdateSidebar;
+
+ if (userCanBulkUpdate && !alreadyInitialized) {
+ IssuableBulkUpdateActions.init({
+ prefixId: pagePrefix,
});
- },
- initBulkUpdate: function(pagePrefix) {
- const userCanBulkUpdate = $('.issues-bulk-update').length > 0;
- const alreadyInitialized = !!this.bulkUpdateSidebar;
-
- if (userCanBulkUpdate && !alreadyInitialized) {
- IssuableBulkUpdateActions.init({
- prefixId: pagePrefix,
- });
-
- this.bulkUpdateSidebar = new IssuableBulkUpdateSidebar();
- }
- },
- resetIncomingEmailToken: function() {
- $('.incoming-email-token-reset').on('click', function(e) {
- e.preventDefault();
- $.ajax({
- type: 'PUT',
- url: $('.incoming-email-token-reset').attr('href'),
- dataType: 'json',
- success: function(response) {
- $('#issue_email').val(response.new_issue_address).focus();
- },
- beforeSend: function() {
- $('.incoming-email-token-reset').text('resetting...');
- },
- complete: function() {
- $('.incoming-email-token-reset').text('reset it');
- }
- });
- });
+ this.bulkUpdateSidebar = new IssuableBulkUpdateSidebar();
}
- };
-})(window);
+ }
+
+ static resetIncomingEmailToken() {
+ $('.incoming-email-token-reset').on('click', (e) => {
+ e.preventDefault();
+
+ $.ajax({
+ type: 'PUT',
+ url: $('.incoming-email-token-reset').attr('href'),
+ dataType: 'json',
+ success(response) {
+ $('#issue_email').val(response.new_issue_address).focus();
+ },
+ beforeSend() {
+ $('.incoming-email-token-reset').text('resetting...');
+ },
+ complete() {
+ $('.incoming-email-token-reset').text('reset it');
+ },
+ });
+ });
+ }
+}
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index 3fc29f9a661..acd5730cf3c 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -6,7 +6,7 @@ import TaskList from './task_list';
import CreateMergeRequestDropdown from './create_merge_request_dropdown';
import IssuablesHelper from './helpers/issuables_helper';
-class Issue {
+export default class Issue {
constructor() {
if ($('a.btn-close').length) {
this.taskList = new TaskList({
@@ -147,5 +147,3 @@ class Issue {
});
}
}
-
-export default Issue;
diff --git a/app/assets/javascripts/issue_status_select.js b/app/assets/javascripts/issue_status_select.js
index 56cb536dcde..03546f61d1f 100644
--- a/app/assets/javascripts/issue_status_select.js
+++ b/app/assets/javascripts/issue_status_select.js
@@ -1,34 +1,23 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, max-len */
-(function() {
- this.IssueStatusSelect = (function() {
- function IssueStatusSelect() {
- $('.js-issue-status').each(function(i, el) {
- var fieldName;
- fieldName = $(el).data("field-name");
- return $(el).glDropdown({
- selectable: true,
- fieldName: fieldName,
- toggleLabel: (function(_this) {
- return function(selected, el, instance) {
- var $item, label;
- label = 'Author';
- $item = instance.dropdown.find('.is-active');
- if ($item.length) {
- label = $item.text();
- }
- return label;
- };
- })(this),
- clicked: function(options) {
- return options.e.preventDefault();
- },
- id: function(obj, el) {
- return $(el).data("id");
- }
- });
- });
- }
-
- return IssueStatusSelect;
- })();
-}).call(window);
+export default function issueStatusSelect() {
+ $('.js-issue-status').each((i, el) => {
+ const fieldName = $(el).data('field-name');
+ return $(el).glDropdown({
+ selectable: true,
+ fieldName,
+ toggleLabel(selected, element, instance) {
+ let label = 'Author';
+ const $item = instance.dropdown.find('.is-active');
+ if ($item.length) {
+ label = $item.text();
+ }
+ return label;
+ },
+ clicked(options) {
+ return options.e.preventDefault();
+ },
+ id(obj, element) {
+ return $(element).data('id');
+ },
+ });
+ });
+}
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 84602cf9207..9b35efcb499 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -6,474 +6,470 @@ import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import DropdownUtils from './filtered_search/dropdown_utils';
import CreateLabelDropdown from './create_label';
-(function() {
- this.LabelsSelect = (function() {
- function LabelsSelect(els) {
- var _this, $els;
- _this = this;
+export default class LabelsSelect {
+ constructor(els) {
+ var _this, $els;
+ _this = this;
- $els = $(els);
+ $els = $(els);
- if (!els) {
- $els = $('.js-label-select');
- }
+ if (!els) {
+ $els = $('.js-label-select');
+ }
- $els.each(function(i, dropdown) {
- var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer;
- $dropdown = $(dropdown);
- $dropdownContainer = $dropdown.closest('.labels-filter');
- $toggleText = $dropdown.find('.dropdown-toggle-text');
- namespacePath = $dropdown.data('namespace-path');
- projectPath = $dropdown.data('project-path');
- labelUrl = $dropdown.data('labels');
- issueUpdateURL = $dropdown.data('issueUpdate');
- selectedLabel = $dropdown.data('selected');
- if ((selectedLabel != null) && !$dropdown.hasClass('js-multiselect')) {
- selectedLabel = selectedLabel.split(',');
- }
- showNo = $dropdown.data('show-no');
- showAny = $dropdown.data('show-any');
- showMenuAbove = $dropdown.data('showMenuAbove');
- defaultLabel = $dropdown.data('default-label');
- abilityName = $dropdown.data('ability-name');
- $selectbox = $dropdown.closest('.selectbox');
- $block = $selectbox.closest('.block');
- $form = $dropdown.closest('form, .js-issuable-update');
- $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span');
- $sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip');
- $value = $block.find('.value');
- $loading = $block.find('.block-loading').fadeOut();
- fieldName = $dropdown.data('field-name');
- useId = $dropdown.is('.js-issuable-form-dropdown, .js-filter-bulk-update, .js-label-sidebar-dropdown');
- propertyName = useId ? 'id' : 'title';
- initialSelected = $selectbox
- .find('input[name="' + $dropdown.data('field-name') + '"]')
- .map(function () {
- return this.value;
- }).get();
- if (issueUpdateURL != null) {
- issueURLSplit = issueUpdateURL.split('/');
- }
- if (issueUpdateURL) {
- labelHTMLTemplate = _.template('<% _.each(labels, function(label){ %> <a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- encodeURIComponent(label.title) %>"> <span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;"> <%- label.title %> </span> </a> <% }); %>');
- labelNoneHTMLTemplate = '<span class="no-value">None</span>';
- }
+ $els.each(function(i, dropdown) {
+ var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer;
+ $dropdown = $(dropdown);
+ $dropdownContainer = $dropdown.closest('.labels-filter');
+ $toggleText = $dropdown.find('.dropdown-toggle-text');
+ namespacePath = $dropdown.data('namespace-path');
+ projectPath = $dropdown.data('project-path');
+ labelUrl = $dropdown.data('labels');
+ issueUpdateURL = $dropdown.data('issueUpdate');
+ selectedLabel = $dropdown.data('selected');
+ if ((selectedLabel != null) && !$dropdown.hasClass('js-multiselect')) {
+ selectedLabel = selectedLabel.split(',');
+ }
+ showNo = $dropdown.data('show-no');
+ showAny = $dropdown.data('show-any');
+ showMenuAbove = $dropdown.data('showMenuAbove');
+ defaultLabel = $dropdown.data('default-label');
+ abilityName = $dropdown.data('ability-name');
+ $selectbox = $dropdown.closest('.selectbox');
+ $block = $selectbox.closest('.block');
+ $form = $dropdown.closest('form, .js-issuable-update');
+ $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span');
+ $sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip');
+ $value = $block.find('.value');
+ $loading = $block.find('.block-loading').fadeOut();
+ fieldName = $dropdown.data('field-name');
+ useId = $dropdown.is('.js-issuable-form-dropdown, .js-filter-bulk-update, .js-label-sidebar-dropdown');
+ propertyName = useId ? 'id' : 'title';
+ initialSelected = $selectbox
+ .find('input[name="' + $dropdown.data('field-name') + '"]')
+ .map(function () {
+ return this.value;
+ }).get();
+ if (issueUpdateURL != null) {
+ issueURLSplit = issueUpdateURL.split('/');
+ }
+ if (issueUpdateURL) {
+ labelHTMLTemplate = _.template('<% _.each(labels, function(label){ %> <a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- encodeURIComponent(label.title) %>"> <span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;"> <%- label.title %> </span> </a> <% }); %>');
+ labelNoneHTMLTemplate = '<span class="no-value">None</span>';
+ }
- $sidebarLabelTooltip.tooltip();
+ $sidebarLabelTooltip.tooltip();
- if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) {
- new CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), namespacePath, projectPath);
- }
+ if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) {
+ new CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), namespacePath, projectPath);
+ }
- saveLabelData = function() {
- var data, selected;
- selected = $dropdown.closest('.selectbox').find("input[name='" + fieldName + "']").map(function() {
- return this.value;
- }).get();
+ saveLabelData = function() {
+ var data, selected;
+ selected = $dropdown.closest('.selectbox').find("input[name='" + fieldName + "']").map(function() {
+ return this.value;
+ }).get();
- if (_.isEqual(initialSelected, selected)) return;
- initialSelected = selected;
+ if (_.isEqual(initialSelected, selected)) return;
+ initialSelected = selected;
- data = {};
- data[abilityName] = {};
- data[abilityName].label_ids = selected;
- if (!selected.length) {
- data[abilityName].label_ids = [''];
+ data = {};
+ data[abilityName] = {};
+ data[abilityName].label_ids = selected;
+ if (!selected.length) {
+ data[abilityName].label_ids = [''];
+ }
+ $loading.removeClass('hidden').fadeIn();
+ $dropdown.trigger('loading.gl.dropdown');
+ return $.ajax({
+ type: 'PUT',
+ url: issueUpdateURL,
+ dataType: 'JSON',
+ data: data
+ }).done(function(data) {
+ var labelCount, template, labelTooltipTitle, labelTitles;
+ $loading.fadeOut();
+ $dropdown.trigger('loaded.gl.dropdown');
+ $selectbox.hide();
+ data.issueURLSplit = issueURLSplit;
+ labelCount = 0;
+ if (data.labels.length) {
+ template = labelHTMLTemplate(data);
+ labelCount = data.labels.length;
}
- $loading.removeClass('hidden').fadeIn();
- $dropdown.trigger('loading.gl.dropdown');
- return $.ajax({
- type: 'PUT',
- url: issueUpdateURL,
- dataType: 'JSON',
- data: data
- }).done(function(data) {
- var labelCount, template, labelTooltipTitle, labelTitles;
- $loading.fadeOut();
- $dropdown.trigger('loaded.gl.dropdown');
- $selectbox.hide();
- data.issueURLSplit = issueURLSplit;
- labelCount = 0;
- if (data.labels.length) {
- template = labelHTMLTemplate(data);
- labelCount = data.labels.length;
- }
- else {
- template = labelNoneHTMLTemplate;
- }
- $value.removeAttr('style').html(template);
- $sidebarCollapsedValue.text(labelCount);
-
- if (data.labels.length) {
- labelTitles = data.labels.map(function(label) {
- return label.title;
- });
+ else {
+ template = labelNoneHTMLTemplate;
+ }
+ $value.removeAttr('style').html(template);
+ $sidebarCollapsedValue.text(labelCount);
- if (labelTitles.length > 5) {
- labelTitles = labelTitles.slice(0, 5);
- labelTitles.push('and ' + (data.labels.length - 5) + ' more');
- }
+ if (data.labels.length) {
+ labelTitles = data.labels.map(function(label) {
+ return label.title;
+ });
- labelTooltipTitle = labelTitles.join(', ');
- }
- else {
- labelTooltipTitle = '';
- $sidebarLabelTooltip.tooltip('destroy');
+ if (labelTitles.length > 5) {
+ labelTitles = labelTitles.slice(0, 5);
+ labelTitles.push('and ' + (data.labels.length - 5) + ' more');
}
- $sidebarLabelTooltip
- .attr('title', labelTooltipTitle)
- .tooltip('fixTitle');
+ labelTooltipTitle = labelTitles.join(', ');
+ }
+ else {
+ labelTooltipTitle = '';
+ $sidebarLabelTooltip.tooltip('destroy');
+ }
- $('.has-tooltip', $value).tooltip({
- container: 'body'
- });
+ $sidebarLabelTooltip
+ .attr('title', labelTooltipTitle)
+ .tooltip('fixTitle');
+
+ $('.has-tooltip', $value).tooltip({
+ container: 'body'
});
- };
- $dropdown.glDropdown({
- showMenuAbove: showMenuAbove,
- data: function(term, callback) {
- return $.ajax({
- url: labelUrl
- }).done(function(data) {
- data = _.chain(data).groupBy(function(label) {
- return label.title;
- }).map(function(label) {
- var color;
- color = _.map(label, function(dup) {
- return dup.color;
+ });
+ };
+ $dropdown.glDropdown({
+ showMenuAbove: showMenuAbove,
+ data: function(term, callback) {
+ return $.ajax({
+ url: labelUrl
+ }).done(function(data) {
+ data = _.chain(data).groupBy(function(label) {
+ return label.title;
+ }).map(function(label) {
+ var color;
+ color = _.map(label, function(dup) {
+ return dup.color;
+ });
+ return {
+ id: label[0].id,
+ title: label[0].title,
+ color: color,
+ duplicate: color.length > 1
+ };
+ }).value();
+ if ($dropdown.hasClass('js-extra-options')) {
+ var extraData = [];
+ if (showNo) {
+ extraData.unshift({
+ id: 0,
+ title: 'No Label'
});
- return {
- id: label[0].id,
- title: label[0].title,
- color: color,
- duplicate: color.length > 1
- };
- }).value();
- if ($dropdown.hasClass('js-extra-options')) {
- var extraData = [];
- if (showNo) {
- extraData.unshift({
- id: 0,
- title: 'No Label'
- });
- }
- if (showAny) {
- extraData.unshift({
- isAny: true,
- title: 'Any Label'
- });
- }
- if (extraData.length) {
- extraData.push('divider');
- data = extraData.concat(data);
- }
- }
-
- callback(data);
- if (showMenuAbove) {
- $dropdown.data('glDropdown').positionMenuAbove();
- }
- });
- },
- renderRow: function(label, instance) {
- var $a, $li, color, colorEl, indeterminate, removesAll, selectedClass, spacing, i, marked, dropdownName, dropdownValue;
- $li = $('<li>');
- $a = $('<a href="#">');
- selectedClass = [];
- removesAll = label.id <= 0 || (label.id == null);
- if ($dropdown.hasClass('js-filter-bulk-update')) {
- indeterminate = $dropdown.data('indeterminate') || [];
- marked = $dropdown.data('marked') || [];
-
- if (indeterminate.indexOf(label.id) !== -1) {
- selectedClass.push('is-indeterminate');
}
-
- if (marked.indexOf(label.id) !== -1) {
- // Remove is-indeterminate class if the item will be marked as active
- i = selectedClass.indexOf('is-indeterminate');
- if (i !== -1) {
- selectedClass.splice(i, 1);
- }
- selectedClass.push('is-active');
+ if (showAny) {
+ extraData.unshift({
+ isAny: true,
+ title: 'Any Label'
+ });
}
- } else {
- if (this.id(label)) {
- dropdownName = $dropdown.data('fieldName');
- dropdownValue = this.id(label).toString().replace(/'/g, '\\\'');
-
- if ($form.find("input[type='hidden'][name='" + dropdownName + "'][value='" + dropdownValue + "']").length) {
- selectedClass.push('is-active');
- }
+ if (extraData.length) {
+ extraData.push('divider');
+ data = extraData.concat(data);
}
+ }
- if ($dropdown.hasClass('js-multiselect') && removesAll) {
- selectedClass.push('dropdown-clear-active');
- }
+ callback(data);
+ if (showMenuAbove) {
+ $dropdown.data('glDropdown').positionMenuAbove();
}
- if (label.duplicate) {
- color = gl.DropdownUtils.duplicateLabelColor(label.color);
+ });
+ },
+ renderRow: function(label, instance) {
+ var $a, $li, color, colorEl, indeterminate, removesAll, selectedClass, spacing, i, marked, dropdownName, dropdownValue;
+ $li = $('<li>');
+ $a = $('<a href="#">');
+ selectedClass = [];
+ removesAll = label.id <= 0 || (label.id == null);
+ if ($dropdown.hasClass('js-filter-bulk-update')) {
+ indeterminate = $dropdown.data('indeterminate') || [];
+ marked = $dropdown.data('marked') || [];
+
+ if (indeterminate.indexOf(label.id) !== -1) {
+ selectedClass.push('is-indeterminate');
}
- else {
- if (label.color != null) {
- color = label.color[0];
+
+ if (marked.indexOf(label.id) !== -1) {
+ // Remove is-indeterminate class if the item will be marked as active
+ i = selectedClass.indexOf('is-indeterminate');
+ if (i !== -1) {
+ selectedClass.splice(i, 1);
}
+ selectedClass.push('is-active');
}
- if (color) {
- colorEl = "<span class='dropdown-label-box' style='background: " + color + "'></span>";
- }
- else {
- colorEl = '';
- }
- // We need to identify which items are actually labels
- if (label.id) {
- selectedClass.push('label-item');
- $a.attr('data-label-id', label.id);
- }
- $a.addClass(selectedClass.join(' ')).html(colorEl + " " + label.title);
- // Return generated html
- return $li.html($a).prop('outerHTML');
- },
- search: {
- fields: ['title']
- },
- selectable: true,
- filterable: true,
- selected: $dropdown.data('selected') || [],
- toggleLabel: function(selected, el) {
- var isSelected = el !== null ? el.hasClass('is-active') : false;
- var title = selected.title;
- var selectedLabels = this.selected;
-
- if (selected.id === 0) {
- this.selected = [];
- return 'No Label';
+ } else {
+ if (this.id(label)) {
+ dropdownName = $dropdown.data('fieldName');
+ dropdownValue = this.id(label).toString().replace(/'/g, '\\\'');
+
+ if ($form.find("input[type='hidden'][name='" + dropdownName + "'][value='" + dropdownValue + "']").length) {
+ selectedClass.push('is-active');
+ }
}
- else if (isSelected) {
- this.selected.push(title);
+
+ if ($dropdown.hasClass('js-multiselect') && removesAll) {
+ selectedClass.push('dropdown-clear-active');
}
- else {
- var index = this.selected.indexOf(title);
- this.selected.splice(index, 1);
+ }
+ if (label.duplicate) {
+ color = gl.DropdownUtils.duplicateLabelColor(label.color);
+ }
+ else {
+ if (label.color != null) {
+ color = label.color[0];
}
+ }
+ if (color) {
+ colorEl = "<span class='dropdown-label-box' style='background: " + color + "'></span>";
+ }
+ else {
+ colorEl = '';
+ }
+ // We need to identify which items are actually labels
+ if (label.id) {
+ selectedClass.push('label-item');
+ $a.attr('data-label-id', label.id);
+ }
+ $a.addClass(selectedClass.join(' ')).html(colorEl + " " + label.title);
+ // Return generated html
+ return $li.html($a).prop('outerHTML');
+ },
+ search: {
+ fields: ['title']
+ },
+ selectable: true,
+ filterable: true,
+ selected: $dropdown.data('selected') || [],
+ toggleLabel: function(selected, el) {
+ var isSelected = el !== null ? el.hasClass('is-active') : false;
+ var title = selected.title;
+ var selectedLabels = this.selected;
+
+ if (selected.id === 0) {
+ this.selected = [];
+ return 'No Label';
+ }
+ else if (isSelected) {
+ this.selected.push(title);
+ }
+ else {
+ var index = this.selected.indexOf(title);
+ this.selected.splice(index, 1);
+ }
+
+ if (selectedLabels.length === 1) {
+ return selectedLabels;
+ }
+ else if (selectedLabels.length) {
+ return selectedLabels[0] + " +" + (selectedLabels.length - 1) + " more";
+ }
+ else {
+ return defaultLabel;
+ }
+ },
+ fieldName: $dropdown.data('field-name'),
+ id: function(label) {
+ if (label.id <= 0) return label.title;
+
+ if ($dropdown.hasClass('js-issuable-form-dropdown')) {
+ return label.id;
+ }
- if (selectedLabels.length === 1) {
- return selectedLabels;
+ if ($dropdown.hasClass("js-filter-submit") && (label.isAny == null)) {
+ return label.title;
+ }
+ else {
+ return label.id;
+ }
+ },
+ hidden: function() {
+ var isIssueIndex, isMRIndex, page, selectedLabels;
+ page = $('body').attr('data-page');
+ isIssueIndex = page === 'projects:issues:index';
+ isMRIndex = page === 'projects:merge_requests:index';
+ $selectbox.hide();
+ // display:block overrides the hide-collapse rule
+ $value.removeAttr('style');
+
+ if ($dropdown.hasClass('js-issuable-form-dropdown')) {
+ return;
+ }
+
+ if ($('html').hasClass('issue-boards-page')) {
+ return;
+ }
+ if ($dropdown.hasClass('js-multiselect')) {
+ if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
+ selectedLabels = $dropdown.closest('form').find("input:hidden[name='" + ($dropdown.data('fieldName')) + "']");
+ Issuable.filterResults($dropdown.closest('form'));
}
- else if (selectedLabels.length) {
- return selectedLabels[0] + " +" + (selectedLabels.length - 1) + " more";
+ else if ($dropdown.hasClass('js-filter-submit')) {
+ $dropdown.closest('form').submit();
}
else {
- return defaultLabel;
+ if (!$dropdown.hasClass('js-filter-bulk-update')) {
+ saveLabelData();
+ }
}
- },
- fieldName: $dropdown.data('field-name'),
- id: function(label) {
- if (label.id <= 0) return label.title;
+ }
+ },
+ multiSelect: $dropdown.hasClass('js-multiselect'),
+ vue: $dropdown.hasClass('js-issue-board-sidebar'),
+ clicked: function(options) {
+ const { $el, e, isMarking } = options;
+ const label = options.selectedObj;
+
+ var isIssueIndex, isMRIndex, page, boardsModel;
+ var fadeOutLoader = () => {
+ $loading.fadeOut();
+ };
- if ($dropdown.hasClass('js-issuable-form-dropdown')) {
- return label.id;
- }
+ page = $('body').attr('data-page');
+ isIssueIndex = page === 'projects:issues:index';
+ isMRIndex = page === 'projects:merge_requests:index';
- if ($dropdown.hasClass("js-filter-submit") && (label.isAny == null)) {
- return label.title;
- }
- else {
- return label.id;
- }
- },
- hidden: function() {
- var isIssueIndex, isMRIndex, page, selectedLabels;
- page = $('body').attr('data-page');
- isIssueIndex = page === 'projects:issues:index';
- isMRIndex = page === 'projects:merge_requests:index';
- $selectbox.hide();
- // display:block overrides the hide-collapse rule
- $value.removeAttr('style');
-
- if ($dropdown.hasClass('js-issuable-form-dropdown')) {
- return;
- }
+ if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) {
+ $dropdown.parent()
+ .find('.dropdown-clear-active')
+ .removeClass('is-active');
+ }
- if ($('html').hasClass('issue-boards-page')) {
- return;
- }
- if ($dropdown.hasClass('js-multiselect')) {
- if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
- selectedLabels = $dropdown.closest('form').find("input:hidden[name='" + ($dropdown.data('fieldName')) + "']");
- Issuable.filterResults($dropdown.closest('form'));
- }
- else if ($dropdown.hasClass('js-filter-submit')) {
- $dropdown.closest('form').submit();
- }
- else {
- if (!$dropdown.hasClass('js-filter-bulk-update')) {
- saveLabelData();
- }
- }
- }
- },
- multiSelect: $dropdown.hasClass('js-multiselect'),
- vue: $dropdown.hasClass('js-issue-board-sidebar'),
- clicked: function(options) {
- const { $el, e, isMarking } = options;
- const label = options.selectedObj;
-
- var isIssueIndex, isMRIndex, page, boardsModel;
- var fadeOutLoader = () => {
- $loading.fadeOut();
- };
-
- page = $('body').attr('data-page');
- isIssueIndex = page === 'projects:issues:index';
- isMRIndex = page === 'projects:merge_requests:index';
-
- if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) {
- $dropdown.parent()
- .find('.dropdown-clear-active')
- .removeClass('is-active');
- }
+ if ($dropdown.hasClass('js-issuable-form-dropdown')) {
+ return;
+ }
- if ($dropdown.hasClass('js-issuable-form-dropdown')) {
- return;
- }
+ if ($dropdown.hasClass('js-filter-bulk-update')) {
+ _this.enableBulkLabelDropdown();
+ _this.setDropdownData($dropdown, isMarking, label.id);
+ return;
+ }
- if ($dropdown.hasClass('js-filter-bulk-update')) {
- _this.enableBulkLabelDropdown();
- _this.setDropdownData($dropdown, isMarking, label.id);
- return;
- }
+ if ($dropdown.closest('.add-issues-modal').length) {
+ boardsModel = gl.issueBoards.ModalStore.store.filter;
+ }
- if ($dropdown.closest('.add-issues-modal').length) {
- boardsModel = gl.issueBoards.ModalStore.store.filter;
+ if (boardsModel) {
+ if (label.isAny) {
+ boardsModel['label_name'] = [];
+ } else if ($el.hasClass('is-active')) {
+ boardsModel['label_name'].push(label.title);
}
- if (boardsModel) {
- if (label.isAny) {
- boardsModel['label_name'] = [];
- } else if ($el.hasClass('is-active')) {
- boardsModel['label_name'].push(label.title);
- }
-
- e.preventDefault();
- return;
+ e.preventDefault();
+ return;
+ }
+ else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
+ if (!$dropdown.hasClass('js-multiselect')) {
+ selectedLabel = label.title;
+ return Issuable.filterResults($dropdown.closest('form'));
}
- else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
- if (!$dropdown.hasClass('js-multiselect')) {
- selectedLabel = label.title;
- return Issuable.filterResults($dropdown.closest('form'));
- }
+ }
+ else if ($dropdown.hasClass('js-filter-submit')) {
+ return $dropdown.closest('form').submit();
+ }
+ else if ($dropdown.hasClass('js-issue-board-sidebar')) {
+ if ($el.hasClass('is-active')) {
+ gl.issueBoards.BoardsStore.detail.issue.labels.push(new ListLabel({
+ id: label.id,
+ title: label.title,
+ color: label.color[0],
+ textColor: '#fff'
+ }));
}
- else if ($dropdown.hasClass('js-filter-submit')) {
- return $dropdown.closest('form').submit();
+ else {
+ var labels = gl.issueBoards.BoardsStore.detail.issue.labels;
+ labels = labels.filter(function (selectedLabel) {
+ return selectedLabel.id !== label.id;
+ });
+ gl.issueBoards.BoardsStore.detail.issue.labels = labels;
}
- else if ($dropdown.hasClass('js-issue-board-sidebar')) {
- if ($el.hasClass('is-active')) {
- gl.issueBoards.BoardsStore.detail.issue.labels.push(new ListLabel({
- id: label.id,
- title: label.title,
- color: label.color[0],
- textColor: '#fff'
- }));
- }
- else {
- var labels = gl.issueBoards.BoardsStore.detail.issue.labels;
- labels = labels.filter(function (selectedLabel) {
- return selectedLabel.id !== label.id;
- });
- gl.issueBoards.BoardsStore.detail.issue.labels = labels;
- }
- $loading.fadeIn();
+ $loading.fadeIn();
+
+ gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
+ .then(fadeOutLoader)
+ .catch(fadeOutLoader);
+ }
+ else {
+ if ($dropdown.hasClass('js-multiselect')) {
- gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
- .then(fadeOutLoader)
- .catch(fadeOutLoader);
}
else {
- if ($dropdown.hasClass('js-multiselect')) {
-
- }
- else {
- return saveLabelData();
- }
+ return saveLabelData();
}
- },
- });
-
- // Set dropdown data
- _this.setOriginalDropdownData($dropdownContainer, $dropdown);
+ }
+ },
});
- this.bindEvents();
- }
- LabelsSelect.prototype.bindEvents = function() {
- return $('body').on('change', '.selected_issue', this.onSelectCheckboxIssue);
- };
-
- LabelsSelect.prototype.onSelectCheckboxIssue = function() {
- if ($('.selected_issue:checked').length) {
- return;
+ // Set dropdown data
+ _this.setOriginalDropdownData($dropdownContainer, $dropdown);
+ });
+ this.bindEvents();
+ }
+
+ bindEvents() {
+ return $('body').on('change', '.selected_issue', this.onSelectCheckboxIssue);
+ }
+ // eslint-disable-next-line class-methods-use-this
+ onSelectCheckboxIssue() {
+ if ($('.selected_issue:checked').length) {
+ return;
+ }
+ return $('.issues-bulk-update .labels-filter .dropdown-toggle-text').text('Label');
+ }
+ // eslint-disable-next-line class-methods-use-this
+ enableBulkLabelDropdown() {
+ IssuableBulkUpdateActions.willUpdateLabels = true;
+ }
+ // eslint-disable-next-line class-methods-use-this
+ setDropdownData($dropdown, isMarking, value) {
+ var i, markedIds, unmarkedIds, indeterminateIds;
+
+ markedIds = $dropdown.data('marked') || [];
+ unmarkedIds = $dropdown.data('unmarked') || [];
+ indeterminateIds = $dropdown.data('indeterminate') || [];
+
+ if (isMarking) {
+ markedIds.push(value);
+
+ i = indeterminateIds.indexOf(value);
+ if (i > -1) {
+ indeterminateIds.splice(i, 1);
}
- return $('.issues-bulk-update .labels-filter .dropdown-toggle-text').text('Label');
- };
- LabelsSelect.prototype.enableBulkLabelDropdown = function() {
- IssuableBulkUpdateActions.willUpdateLabels = true;
- };
-
- LabelsSelect.prototype.setDropdownData = function($dropdown, isMarking, value) {
- var i, markedIds, unmarkedIds, indeterminateIds;
-
- markedIds = $dropdown.data('marked') || [];
- unmarkedIds = $dropdown.data('unmarked') || [];
- indeterminateIds = $dropdown.data('indeterminate') || [];
-
- if (isMarking) {
- markedIds.push(value);
-
- i = indeterminateIds.indexOf(value);
- if (i > -1) {
- indeterminateIds.splice(i, 1);
- }
-
- i = unmarkedIds.indexOf(value);
- if (i > -1) {
- unmarkedIds.splice(i, 1);
- }
- } else {
- // If marked item (not common) is unmarked
- i = markedIds.indexOf(value);
- if (i > -1) {
- markedIds.splice(i, 1);
- }
-
- // If an indeterminate item is being unmarked
- if (IssuableBulkUpdateActions.getOriginalIndeterminateIds().indexOf(value) > -1) {
- unmarkedIds.push(value);
- }
-
- // If a marked item is being unmarked
- // (a marked item could also be a label that is present in all selection)
- if (IssuableBulkUpdateActions.getOriginalCommonIds().indexOf(value) > -1) {
- unmarkedIds.push(value);
- }
+ i = unmarkedIds.indexOf(value);
+ if (i > -1) {
+ unmarkedIds.splice(i, 1);
+ }
+ } else {
+ // If marked item (not common) is unmarked
+ i = markedIds.indexOf(value);
+ if (i > -1) {
+ markedIds.splice(i, 1);
}
- $dropdown.data('marked', markedIds);
- $dropdown.data('unmarked', unmarkedIds);
- $dropdown.data('indeterminate', indeterminateIds);
- };
+ // If an indeterminate item is being unmarked
+ if (IssuableBulkUpdateActions.getOriginalIndeterminateIds().indexOf(value) > -1) {
+ unmarkedIds.push(value);
+ }
- LabelsSelect.prototype.setOriginalDropdownData = function($container, $dropdown) {
- var labels = [];
- $container.find('[name="label_name[]"]').map(function() {
- return labels.push(this.value);
- });
- $dropdown.data('marked', labels);
- };
+ // If a marked item is being unmarked
+ // (a marked item could also be a label that is present in all selection)
+ if (IssuableBulkUpdateActions.getOriginalCommonIds().indexOf(value) > -1) {
+ unmarkedIds.push(value);
+ }
+ }
- return LabelsSelect;
- })();
-}).call(window);
+ $dropdown.data('marked', markedIds);
+ $dropdown.data('unmarked', unmarkedIds);
+ $dropdown.data('indeterminate', indeterminateIds);
+ }
+ // eslint-disable-next-line class-methods-use-this
+ setOriginalDropdownData($container, $dropdown) {
+ const labels = [];
+ $container.find('[name="label_name[]"]').map(function() {
+ return labels.push(this.value);
+ });
+ $dropdown.data('marked', labels);
+ }
+}
diff --git a/app/assets/javascripts/lazy_loader.js b/app/assets/javascripts/lazy_loader.js
index 3d64b121fa7..dbbf1637a47 100644
--- a/app/assets/javascripts/lazy_loader.js
+++ b/app/assets/javascripts/lazy_loader.js
@@ -1,5 +1,3 @@
-/* eslint-disable one-export, one-var, one-var-declaration-per-line */
-
import _ from 'underscore';
export const placeholderImage = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
@@ -21,7 +19,10 @@ export default class LazyLoader {
}
searchLazyImages() {
this.lazyImages = [].slice.call(document.querySelectorAll('.lazy'));
- this.checkElementsInView();
+
+ if (this.lazyImages.length) {
+ this.checkElementsInView();
+ }
}
startContentObserver() {
const contentNode = document.querySelector(this.observerNode) || document.querySelector('body');
@@ -45,15 +46,13 @@ export default class LazyLoader {
checkElementsInView() {
const scrollTop = pageYOffset;
const visHeight = scrollTop + innerHeight + SCROLL_THRESHOLD;
- let imgBoundRect, imgTop, imgBound;
// Loading Images which are in the current viewport or close to them
this.lazyImages = this.lazyImages.filter((selectedImage) => {
if (selectedImage.getAttribute('data-src')) {
- imgBoundRect = selectedImage.getBoundingClientRect();
-
- imgTop = scrollTop + imgBoundRect.top;
- imgBound = imgTop + imgBoundRect.height;
+ const imgBoundRect = selectedImage.getBoundingClientRect();
+ const imgTop = scrollTop + imgBoundRect.top;
+ const imgBound = imgTop + imgBoundRect.height;
if (scrollTop < imgBound && visHeight > imgTop) {
LazyLoader.loadImage(selectedImage);
diff --git a/app/assets/javascripts/lib/utils/axios_utils.js b/app/assets/javascripts/lib/utils/axios_utils.js
new file mode 100644
index 00000000000..45bff245827
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/axios_utils.js
@@ -0,0 +1,6 @@
+import axios from 'axios';
+import csrf from './csrf';
+
+export default function setAxiosCsrfToken() {
+ axios.defaults.headers.common[csrf.headerKey] = csrf.token;
+}
diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js
index 729baa2e1a7..3688a57937e 100644
--- a/app/assets/javascripts/logo.js
+++ b/app/assets/javascripts/logo.js
@@ -1,7 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback */
-
-(function() {
- window.addEventListener('beforeunload', function() {
+export default function initLogoAnimation() {
+ window.addEventListener('beforeunload', () => {
$('.tanuki-logo').addClass('animate');
});
-}).call(window);
+}
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 38403fdaf6e..9117f033c9f 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -12,7 +12,6 @@ import svg4everybody from 'svg4everybody';
// libraries with import side-effects
import 'mousetrap';
import 'mousetrap/plugins/pause/mousetrap-pause';
-import 'vendor/fuzzaldrin-plus';
// expose common libraries as globals (TODO: remove these)
window.jQuery = jQuery;
@@ -41,7 +40,6 @@ import './behaviors/';
import './activities';
import './admin';
import './aside';
-import './autosave';
import loadAwardsHandler from './awards_handler';
import bp from './breakpoints';
import './commits';
@@ -55,18 +53,12 @@ import './gl_dropdown';
import './gl_field_error';
import './gl_field_errors';
import './gl_form';
-import './header';
-import './importer_status';
-import './issuable_index';
-import './issuable_context';
-import './issuable_form';
-import './issue';
-import './issue_status_select';
-import './labels_select';
+import initTodoToggle from './header';
+import initImporterStatus from './importer_status';
import './layout_nav';
import LazyLoader from './lazy_loader';
import './line_highlighter';
-import './logo';
+import initLogoAnimation from './logo';
import './merge_request';
import './merge_request_tabs';
import './milestone';
@@ -139,6 +131,9 @@ $(function () {
var fitSidebarForSize;
initBreadcrumbs();
+ initImporterStatus();
+ initTodoToggle();
+ initLogoAnimation();
// Set the default path for all cookies to GitLab's root directory
Cookies.defaults.path = gon.relative_url_root || '/';
diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js
index 5da2db063a4..1d496c64e53 100644
--- a/app/assets/javascripts/namespace_select.js
+++ b/app/assets/javascripts/namespace_select.js
@@ -1,85 +1,57 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, vars-on-top, one-var-declaration-per-line, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, no-param-reassign, no-cond-assign, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, max-len */
import Api from './api';
+import './lib/utils/url_utility';
-(function() {
- window.NamespaceSelect = (function() {
- function NamespaceSelect(opts) {
- this.onSelectItem = this.onSelectItem.bind(this);
- var fieldName, showAny;
- this.dropdown = opts.dropdown;
- showAny = true;
- fieldName = 'namespace_id';
- if (this.dropdown.attr('data-field-name')) {
- fieldName = this.dropdown.data('fieldName');
- }
- if (this.dropdown.attr('data-show-any')) {
- showAny = this.dropdown.data('showAny');
- }
- this.dropdown.glDropdown({
- filterable: true,
- selectable: true,
- filterRemote: true,
- search: {
- fields: ['path']
- },
- fieldName: fieldName,
- toggleLabel: function(selected) {
- if (selected.id == null) {
- return selected.text;
- } else {
- return selected.kind + ": " + selected.full_path;
- }
- },
- data: function(term, dataCallback) {
- return Api.namespaces(term, function(namespaces) {
- var anyNamespace;
- if (showAny) {
- anyNamespace = {
- text: 'Any namespace',
- id: null
- };
- namespaces.unshift(anyNamespace);
- namespaces.splice(1, 0, 'divider');
- }
- return dataCallback(namespaces);
- });
- },
- text: function(namespace) {
- if (namespace.id == null) {
- return namespace.text;
- } else {
- return namespace.kind + ": " + namespace.full_path;
- }
- },
- renderRow: this.renderRow,
- clicked: this.onSelectItem
- });
- }
-
- NamespaceSelect.prototype.onSelectItem = function(options) {
- const { e } = options;
- return e.preventDefault();
- };
+export default class NamespaceSelect {
+ constructor(opts) {
+ const isFilter = opts.dropdown.dataset.isFilter === 'true';
+ const fieldName = opts.dropdown.dataset.fieldName || 'namespace_id';
- return NamespaceSelect;
- })();
-
- window.NamespaceSelects = (function() {
- function NamespaceSelects(opts) {
- var ref;
- if (opts == null) {
- opts = {};
- }
- this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-namespace-select');
- this.$dropdowns.each(function(i, dropdown) {
- var $dropdown;
- $dropdown = $(dropdown);
- return new window.NamespaceSelect({
- dropdown: $dropdown
+ $(opts.dropdown).glDropdown({
+ filterable: true,
+ selectable: true,
+ filterRemote: true,
+ search: {
+ fields: ['path']
+ },
+ fieldName: fieldName,
+ toggleLabel: function(selected) {
+ if (selected.id == null) {
+ return selected.text;
+ } else {
+ return selected.kind + ": " + selected.full_path;
+ }
+ },
+ data: function(term, dataCallback) {
+ return Api.namespaces(term, function(namespaces) {
+ if (isFilter) {
+ const anyNamespace = {
+ text: 'Any namespace',
+ id: null
+ };
+ namespaces.unshift(anyNamespace);
+ namespaces.splice(1, 0, 'divider');
+ }
+ return dataCallback(namespaces);
});
- });
- }
-
- return NamespaceSelects;
- })();
-}).call(window);
+ },
+ text: function(namespace) {
+ if (namespace.id == null) {
+ return namespace.text;
+ } else {
+ return namespace.kind + ": " + namespace.full_path;
+ }
+ },
+ renderRow: this.renderRow,
+ clicked(options) {
+ if (!isFilter) {
+ const { e } = options;
+ e.preventDefault();
+ }
+ },
+ url(namespace) {
+ return gl.utils.mergeUrlParams({ [fieldName]: namespace.id }, window.location.href);
+ },
+ });
+ }
+}
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 5a6868be444..e1ab28978e8 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -5,14 +5,14 @@ default-case, prefer-template, consistent-return, no-alert, no-return-assign,
no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new,
brace-style, no-lonely-if, vars-on-top, no-unused-vars, no-sequences, no-shadow,
newline-per-chained-call, no-useless-escape, class-methods-use-this */
-/* global Autosave */
+
/* global ResolveService */
/* global mrRefreshWidgetUrl */
import $ from 'jquery';
import _ from 'underscore';
import Cookies from 'js-cookie';
-import autosize from 'vendor/autosize';
+import Autosize from 'autosize';
import 'vendor/jquery.caret'; // required by jquery.atwho
import 'vendor/jquery.atwho';
import AjaxCache from '~/lib/utils/ajax_cache';
@@ -20,12 +20,12 @@ import Flash from './flash';
import CommentTypeToggle from './comment_type_toggle';
import GLForm from './gl_form';
import loadAwardsHandler from './awards_handler';
-import './autosave';
+import Autosave from './autosave';
import TaskList from './task_list';
import { ajaxPost, isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils';
import imageDiffHelper from './image_diff/helpers/index';
-window.autosize = autosize;
+window.autosize = Autosize;
function normalizeNewlines(str) {
return str.replace(/\r\n/g, '\n');
@@ -413,8 +413,9 @@ export default class Notes {
return;
}
this.note_ids.push(noteEntity.id);
+
form = $form || $(`.js-discussion-note-form[data-discussion-id="${noteEntity.discussion_id}"]`);
- row = form.closest('tr');
+ row = (form.length || !noteEntity.discussion_line_code) ? form.closest('tr') : $(`#${noteEntity.discussion_line_code}`);
if (noteEntity.on_image) {
row = form;
diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue
index 2ce52e4538a..db8f85759b2 100644
--- a/app/assets/javascripts/notes/components/issue_comment_form.vue
+++ b/app/assets/javascripts/notes/components/issue_comment_form.vue
@@ -1,10 +1,9 @@
<script>
- /* global Autosave */
import { mapActions, mapGetters } from 'vuex';
import _ from 'underscore';
- import autosize from 'vendor/autosize';
+ import Autosize from 'autosize';
import Flash from '../../flash';
- import '../../autosave';
+ import Autosave from '../../autosave';
import TaskList from '../../task_list';
import * as constants from '../constants';
import eventHub from '../event_hub';
@@ -220,7 +219,7 @@
},
resizeTextarea() {
this.$nextTick(() => {
- autosize.update(this.$refs.textarea);
+ Autosize.update(this.$refs.textarea);
});
},
},
diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue
index 0ddbd672bed..40318f9a600 100644
--- a/app/assets/javascripts/notes/components/issue_note.vue
+++ b/app/assets/javascripts/notes/components/issue_note.vue
@@ -122,7 +122,9 @@
// we need to do this to prevent noteForm inconsistent content warning
// this is something we intentionally do so we need to recover the content
this.note.note = noteText;
- this.$refs.noteBody.$refs.noteForm.note = noteText; // TODO: This could be better
+ if (this.$refs.noteBody) {
+ this.$refs.noteBody.$refs.noteForm.note = noteText; // TODO: This could be better
+ }
},
},
created() {
diff --git a/app/assets/javascripts/notes/mixins/autosave.js b/app/assets/javascripts/notes/mixins/autosave.js
index 5843b97f225..a008171beda 100644
--- a/app/assets/javascripts/notes/mixins/autosave.js
+++ b/app/assets/javascripts/notes/mixins/autosave.js
@@ -1,5 +1,4 @@
-/* globals Autosave */
-import '../../autosave';
+import Autosave from '../../autosave';
export default {
methods: {
diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue
index 54227425d2a..547140b1a43 100644
--- a/app/assets/javascripts/pipelines/components/graph/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue
@@ -1,6 +1,6 @@
<script>
- import getActionIcon from '../../../vue_shared/ci_action_icons';
import tooltip from '../../../vue_shared/directives/tooltip';
+ import icon from '../../../vue_shared/components/icon.vue';
/**
* Renders either a cancel, retry or play icon pointing to the given path.
@@ -29,17 +29,18 @@
},
},
+ components: {
+ icon,
+ },
+
directives: {
tooltip,
},
computed: {
- actionIconSvg() {
- return getActionIcon(this.actionIcon);
- },
-
cssClass() {
- return `js-${gl.text.dasherize(this.actionIcon)}`;
+ const actionIconDash = gl.text.dasherize(this.actionIcon);
+ return `${actionIconDash} js-icon-${actionIconDash}`;
},
},
};
@@ -50,14 +51,9 @@
:data-method="actionMethod"
:title="tooltipText"
:href="link"
- class="ci-action-icon-container"
+ class="ci-action-icon-container ci-action-icon-wrapper"
+ :class="cssClass"
data-container="body">
-
- <i
- class="ci-action-icon-wrapper"
- :class="cssClass"
- v-html="actionIconSvg"
- aria-hidden="true"
- />
+ <icon :name="actionIcon"/>
</a>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue
index 18fe1847eef..1c0944d45fc 100644
--- a/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue
@@ -1,5 +1,5 @@
<script>
- import getActionIcon from '../../../vue_shared/ci_action_icons';
+ import icon from '../../../vue_shared/components/icon.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
/**
@@ -29,14 +29,12 @@
},
},
- directives: {
- tooltip,
+ components: {
+ icon,
},
- computed: {
- actionIconSvg() {
- return getActionIcon(this.actionIcon);
- },
+ directives: {
+ tooltip,
},
};
</script>
@@ -49,7 +47,7 @@
rel="nofollow"
class="ci-action-icon-wrapper js-ci-status-icon"
data-container="body"
- v-html="actionIconSvg"
aria-label="Job's action">
+ <icon :name="actionIcon"/>
</a>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
index 3e5d6d15909..7006d05e7b2 100644
--- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
@@ -18,7 +18,7 @@
* "group": "success",
* "details_path": "/root/ci-mock/builds/4256",
* "action": {
- * "icon": "icon_action_retry",
+ * "icon": "retry",
* "title": "Retry",
* "path": "/root/ci-mock/builds/4256/retry",
* "method": "post"
diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue
index 3933509a6f4..5dea4555515 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue
@@ -19,7 +19,7 @@
* "group": "success",
* "details_path": "/root/ci-mock/builds/4256",
* "action": {
- * "icon": "icon_action_retry",
+ * "icon": "retry",
* "title": "Retry",
* "path": "/root/ci-mock/builds/4256/retry",
* "method": "post"
diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue
index 1a7a5c2a415..ac9d9c901ca 100644
--- a/app/assets/javascripts/pipelines/components/stage.vue
+++ b/app/assets/javascripts/pipelines/components/stage.vue
@@ -14,7 +14,7 @@
*/
import Flash from '../../flash';
-import { borderlessStatusIconEntityMap } from '../../vue_shared/ci_status_icons';
+import icon from '../../vue_shared/components/icon.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
@@ -45,6 +45,7 @@ export default {
components: {
loadingIcon,
+ icon,
},
updated() {
@@ -122,8 +123,8 @@ export default {
return `ci-status-icon-${this.stage.status.group}`;
},
- svgIcon() {
- return borderlessStatusIconEntityMap[this.stage.status.icon];
+ borderlessIcon() {
+ return `${this.stage.status.icon}_borderless`;
},
},
};
@@ -145,9 +146,10 @@ export default {
aria-expanded="false">
<span
- v-html="svgIcon"
aria-hidden="true"
:aria-label="stage.title">
+ <icon
+ :name="borderlessIcon"/>
</span>
<i
diff --git a/app/assets/javascripts/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
index b2b34cb83e1..6348a2e331d 100644
--- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue
+++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
@@ -98,7 +98,7 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
@toggle="toggleOpen"
@submit="onSubmit">
- <template slot="body" scope="props">
+ <template slot="body" slot-scope="props">
<p v-html="props.text"></p>
<form
diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js
index 11f9754780d..19682b20a4a 100644
--- a/app/assets/javascripts/project_find_file.js
+++ b/app/assets/javascripts/project_find_file.js
@@ -1,5 +1,6 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, consistent-return, one-var, one-var-declaration-per-line, no-cond-assign, max-len, object-shorthand, no-param-reassign, comma-dangle, prefer-template, no-unused-vars, no-return-assign */
-/* global fuzzaldrinPlus */
+
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
(function() {
this.ProjectFindFile = (function() {
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index fb01390f91c..bffc85e6315 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -2,13 +2,15 @@
import Api from './api';
import ProjectSelectComboButton from './project_select_combo_button';
-(function() {
- this.ProjectSelect = (function() {
+(function () {
+ this.ProjectSelect = (function () {
function ProjectSelect() {
$('.ajax-project-select').each(function(i, select) {
var placeholder;
+ const simpleFilter = $(select).data('simple-filter') || false;
this.groupId = $(select).data('group-id');
this.includeGroups = $(select).data('include-groups');
+ this.allProjects = $(select).data('all-projects') || false;
this.orderBy = $(select).data('order-by') || 'id';
this.withIssuesEnabled = $(select).data('with-issues-enabled');
this.withMergeRequestsEnabled = $(select).data('with-merge-requests-enabled');
@@ -21,10 +23,10 @@ import ProjectSelectComboButton from './project_select_combo_button';
$(select).select2({
placeholder: placeholder,
minimumInputLength: 0,
- query: (function(_this) {
- return function(query) {
+ query: (function (_this) {
+ return function (query) {
var finalCallback, projectsCallback;
- finalCallback = function(projects) {
+ finalCallback = function (projects) {
var data;
data = {
results: projects
@@ -32,9 +34,9 @@ import ProjectSelectComboButton from './project_select_combo_button';
return query.callback(data);
};
if (_this.includeGroups) {
- projectsCallback = function(projects) {
+ projectsCallback = function (projects) {
var groupsCallback;
- groupsCallback = function(groups) {
+ groupsCallback = function (groups) {
var data;
data = groups.concat(projects);
return finalCallback(data);
@@ -50,23 +52,25 @@ import ProjectSelectComboButton from './project_select_combo_button';
return Api.projects(query.term, {
order_by: _this.orderBy,
with_issues_enabled: _this.withIssuesEnabled,
- with_merge_requests_enabled: _this.withMergeRequestsEnabled
+ with_merge_requests_enabled: _this.withMergeRequestsEnabled,
+ membership: !_this.allProjects,
}, projectsCallback);
}
};
})(this),
id: function(project) {
+ if (simpleFilter) return project.id;
return JSON.stringify({
name: project.name,
url: project.web_url,
});
},
- text: function(project) {
+ text: function (project) {
return project.name_with_namespace || project.name;
},
dropdownCssClass: "ajax-project-dropdown"
});
-
+ if (simpleFilter) return select;
return new ProjectSelectComboButton(select);
});
}
diff --git a/app/assets/javascripts/repo/components/new_branch_form.vue b/app/assets/javascripts/repo/components/new_branch_form.vue
index eac43e692b0..ba7090e4a9d 100644
--- a/app/assets/javascripts/repo/components/new_branch_form.vue
+++ b/app/assets/javascripts/repo/components/new_branch_form.vue
@@ -1,18 +1,12 @@
<script>
+ import { mapState, mapActions } from 'vuex';
import flash, { hideFlash } from '../../flash';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
- import eventHub from '../event_hub';
export default {
components: {
loadingIcon,
},
- props: {
- currentBranch: {
- type: String,
- required: true,
- },
- },
data() {
return {
branchName: '',
@@ -20,11 +14,17 @@
};
},
computed: {
+ ...mapState([
+ 'currentBranch',
+ ]),
btnDisabled() {
return this.loading || this.branchName === '';
},
},
methods: {
+ ...mapActions([
+ 'createNewBranch',
+ ]),
toggleDropdown() {
this.$dropdown.dropdown('toggle');
},
@@ -38,19 +38,21 @@
hideFlash(flashEl, false);
}
- eventHub.$emit('createNewBranch', this.branchName);
- },
- showErrorMessage(message) {
- this.loading = false;
- flash(message, 'alert', this.$el);
- },
- createdNewBranch(newBranchName) {
- this.loading = false;
- this.branchName = '';
+ this.createNewBranch(this.branchName)
+ .then(() => {
+ this.loading = false;
+ this.branchName = '';
- if (this.dropdownText) {
- this.dropdownText.textContent = newBranchName;
- }
+ if (this.dropdownText) {
+ this.dropdownText.textContent = this.currentBranch;
+ }
+
+ this.toggleDropdown();
+ })
+ .catch(res => res.json().then((data) => {
+ this.loading = false;
+ flash(data.message, 'alert', this.$el);
+ }));
},
},
created() {
@@ -59,15 +61,6 @@
// text element is outside Vue app
this.dropdownText = document.querySelector('.project-refs-form .dropdown-toggle-text');
-
- eventHub.$on('createNewBranchSuccess', this.createdNewBranch);
- eventHub.$on('createNewBranchError', this.showErrorMessage);
- eventHub.$on('toggleNewBranchDropdown', this.toggleDropdown);
- },
- destroyed() {
- eventHub.$off('createNewBranchSuccess', this.createdNewBranch);
- eventHub.$off('toggleNewBranchDropdown', this.toggleDropdown);
- eventHub.$off('createNewBranchError', this.showErrorMessage);
},
};
</script>
diff --git a/app/assets/javascripts/repo/components/new_dropdown/index.vue b/app/assets/javascripts/repo/components/new_dropdown/index.vue
index 3ccb50213ab..a5ee4f71281 100644
--- a/app/assets/javascripts/repo/components/new_dropdown/index.vue
+++ b/app/assets/javascripts/repo/components/new_dropdown/index.vue
@@ -1,20 +1,24 @@
<script>
- import RepoStore from '../../stores/repo_store';
- import RepoHelper from '../../helpers/repo_helper';
- import eventHub from '../../event_hub';
+ import { mapState } from 'vuex';
import newModal from './modal.vue';
+ import upload from './upload.vue';
export default {
components: {
newModal,
+ upload,
},
data() {
return {
openModal: false,
modalType: '',
- currentPath: RepoStore.path,
};
},
+ computed: {
+ ...mapState([
+ 'path',
+ ]),
+ },
methods: {
createNewItem(type) {
this.modalType = type;
@@ -23,17 +27,6 @@
toggleModalOpen() {
this.openModal = !this.openModal;
},
- createNewEntryInStore(name, type) {
- RepoHelper.createNewEntry(name, type);
-
- this.toggleModalOpen();
- },
- },
- created() {
- eventHub.$on('createNewEntry', this.createNewEntryInStore);
- },
- beforeDestroy() {
- eventHub.$off('createNewEntry', this.createNewEntryInStore);
},
};
</script>
@@ -65,6 +58,11 @@
</a>
</li>
<li>
+ <upload
+ :path="path"
+ />
+ </li>
+ <li>
<a
href="#"
role="button"
@@ -79,7 +77,7 @@
<new-modal
v-if="openModal"
:type="modalType"
- :current-path="currentPath"
+ :path="path"
@toggle="toggleModalOpen"
/>
</div>
diff --git a/app/assets/javascripts/repo/components/new_dropdown/modal.vue b/app/assets/javascripts/repo/components/new_dropdown/modal.vue
index 5ef629e0dde..ac1f613bb71 100644
--- a/app/assets/javascripts/repo/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/repo/components/new_dropdown/modal.vue
@@ -1,30 +1,38 @@
<script>
+ import { mapActions } from 'vuex';
import { __ } from '../../../locale';
import popupDialog from '../../../vue_shared/components/popup_dialog.vue';
- import eventHub from '../../event_hub';
export default {
props: {
- currentPath: {
+ type: {
type: String,
required: true,
},
- type: {
+ path: {
type: String,
required: true,
},
},
data() {
return {
- entryName: this.currentPath !== '' ? `${this.currentPath}/` : '',
+ entryName: this.path !== '' ? `${this.path}/` : '',
};
},
components: {
popupDialog,
},
methods: {
+ ...mapActions([
+ 'createTempEntry',
+ ]),
createEntryInStore() {
- eventHub.$emit('createNewEntry', this.entryName, this.type);
+ this.createTempEntry({
+ name: this.entryName.replace(new RegExp(`^${this.path}/`), ''),
+ type: this.type,
+ });
+
+ this.toggleModalOpen();
},
toggleModalOpen() {
this.$emit('toggle');
diff --git a/app/assets/javascripts/repo/components/new_dropdown/upload.vue b/app/assets/javascripts/repo/components/new_dropdown/upload.vue
new file mode 100644
index 00000000000..14ad32f4ae0
--- /dev/null
+++ b/app/assets/javascripts/repo/components/new_dropdown/upload.vue
@@ -0,0 +1,68 @@
+<script>
+ import { mapActions } from 'vuex';
+
+ export default {
+ props: {
+ path: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ ...mapActions([
+ 'createTempEntry',
+ ]),
+ createFile(target, file, isText) {
+ const { name } = file;
+ let { result } = target;
+
+ if (!isText) {
+ result = result.split('base64,')[1];
+ }
+
+ this.createTempEntry({
+ name,
+ type: 'blob',
+ content: result,
+ base64: !isText,
+ });
+ },
+ readFile(file) {
+ const reader = new FileReader();
+ const isText = file.type.match(/text.*/) !== null;
+
+ reader.addEventListener('load', e => this.createFile(e.target, file, isText), { once: true });
+
+ if (isText) {
+ reader.readAsText(file);
+ } else {
+ reader.readAsDataURL(file);
+ }
+ },
+ openFile() {
+ Array.from(this.$refs.fileUpload.files).forEach(file => this.readFile(file));
+ },
+ },
+ mounted() {
+ this.$refs.fileUpload.addEventListener('change', this.openFile);
+ },
+ beforeDestroy() {
+ this.$refs.fileUpload.removeEventListener('change', this.openFile);
+ },
+ };
+</script>
+
+<template>
+ <label
+ role="button"
+ class="menu-item"
+ >
+ {{ __('Upload file') }}
+ <input
+ id="file-upload"
+ type="file"
+ class="hidden"
+ ref="fileUpload"
+ />
+ </label>
+</template>
diff --git a/app/assets/javascripts/repo/components/repo.vue b/app/assets/javascripts/repo/components/repo.vue
index 788976a9804..98117802016 100644
--- a/app/assets/javascripts/repo/components/repo.vue
+++ b/app/assets/javascripts/repo/components/repo.vue
@@ -1,102 +1,59 @@
<script>
+import { mapState, mapGetters } from 'vuex';
import RepoSidebar from './repo_sidebar.vue';
import RepoCommitSection from './repo_commit_section.vue';
import RepoTabs from './repo_tabs.vue';
import RepoFileButtons from './repo_file_buttons.vue';
import RepoPreview from './repo_preview.vue';
-import RepoMixin from '../mixins/repo_mixin';
-import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
-import Store from '../stores/repo_store';
-import Helper from '../helpers/repo_helper';
-import Service from '../services/repo_service';
-import MonacoLoaderHelper from '../helpers/monaco_loader_helper';
-import eventHub from '../event_hub';
+import repoEditor from './repo_editor.vue';
export default {
- data() {
- return Store;
+ computed: {
+ ...mapState([
+ 'currentBlobView',
+ ]),
+ ...mapGetters([
+ 'isCollapsed',
+ 'changedFiles',
+ ]),
},
- mixins: [RepoMixin],
components: {
RepoSidebar,
RepoTabs,
RepoFileButtons,
- 'repo-editor': MonacoLoaderHelper.repoEditorLoader,
+ repoEditor,
RepoCommitSection,
- PopupDialog,
RepoPreview,
},
- created() {
- eventHub.$on('createNewBranch', this.createNewBranch);
- },
mounted() {
- Helper.getContent().catch(Helper.loadingError);
- },
- destroyed() {
- eventHub.$off('createNewBranch', this.createNewBranch);
- },
- methods: {
- getCurrentLocation() {
- return location.href;
- },
- toggleDialogOpen(toggle) {
- this.dialog.open = toggle;
- },
-
- dialogSubmitted(status) {
- this.toggleDialogOpen(false);
- this.dialog.status = status;
-
- // remove tmp files
- Helper.removeAllTmpFiles('openedFiles');
- Helper.removeAllTmpFiles('files');
- },
- toggleBlobView: Store.toggleBlobView,
- createNewBranch(branch) {
- Service.createBranch({
- branch,
- ref: Store.currentBranch,
- }).then((res) => {
- const newBranchName = res.data.name;
- const newUrl = this.getCurrentLocation().replace(Store.currentBranch, newBranchName);
-
- Store.currentBranch = newBranchName;
-
- history.pushState({ key: Helper.key }, '', newUrl);
+ const returnValue = 'Are you sure you want to lose unsaved changes?';
+ window.onbeforeunload = (e) => {
+ if (!this.changedFiles.length) return undefined;
- eventHub.$emit('createNewBranchSuccess', newBranchName);
- eventHub.$emit('toggleNewBranchDropdown');
- }).catch((err) => {
- eventHub.$emit('createNewBranchError', err.response.data.message);
+ Object.assign(e, {
+ returnValue,
});
- },
+ return returnValue;
+ };
},
};
</script>
<template>
<div class="repository-view">
- <div class="tree-content-holder" :class="{'tree-content-holder-mini' : isMini}">
+ <div class="tree-content-holder" :class="{'tree-content-holder-mini' : isCollapsed}">
<repo-sidebar/>
- <div v-if="isMini"
- class="panel-right"
- :class="{'edit-mode': editMode}">
+ <div
+ v-if="isCollapsed"
+ class="panel-right"
+ >
<repo-tabs/>
<component
:is="currentBlobView"
- class="blob-viewer-container"/>
+ />
<repo-file-buttons/>
</div>
</div>
- <repo-commit-section/>
- <popup-dialog
- v-show="dialog.open"
- :primary-button-label="__('Discard changes')"
- kind="warning"
- :title="__('Are you sure?')"
- :text="__('Are you sure you want to discard your changes?')"
- @toggle="toggleDialogOpen"
- @submit="dialogSubmitted"
- />
+ <repo-commit-section v-if="changedFiles.length" />
</div>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_commit_section.vue b/app/assets/javascripts/repo/components/repo_commit_section.vue
index 0d6259a37a8..377e3d65348 100644
--- a/app/assets/javascripts/repo/components/repo_commit_section.vue
+++ b/app/assets/javascripts/repo/components/repo_commit_section.vue
@@ -1,141 +1,100 @@
<script>
-import Flash from '../../flash';
-import Store from '../stores/repo_store';
-import RepoMixin from '../mixins/repo_mixin';
-import Service from '../services/repo_service';
+import { mapGetters, mapState, mapActions } from 'vuex';
import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
-import { visitUrl } from '../../lib/utils/url_utility';
+import { n__ } from '../../locale';
export default {
- mixins: [RepoMixin],
-
- data() {
- return Store;
- },
-
components: {
PopupDialog,
},
-
+ data() {
+ return {
+ showNewBranchDialog: false,
+ submitCommitsLoading: false,
+ startNewMR: false,
+ commitMessage: '',
+ };
+ },
computed: {
- showCommitable() {
- return this.isCommitable && this.changedFiles.length;
- },
-
- branchPaths() {
- return this.changedFiles.map(f => f.path);
- },
-
- cantCommitYet() {
+ ...mapState([
+ 'currentBranch',
+ ]),
+ ...mapGetters([
+ 'changedFiles',
+ ]),
+ commitButtonDisabled() {
return !this.commitMessage || this.submitCommitsLoading;
},
-
- filePluralize() {
- return this.changedFiles.length > 1 ? 'files' : 'file';
+ commitButtonText() {
+ return n__('Commit %d file', 'Commit %d files', this.changedFiles.length);
},
},
-
methods: {
- commitToNewBranch(status) {
- if (status) {
- this.showNewBranchDialog = false;
- this.tryCommit(null, true, true);
- } else {
- // reset the state
- }
- },
+ ...mapActions([
+ 'checkCommitStatus',
+ 'commitChanges',
+ 'getTreeData',
+ ]),
+ makeCommit(newBranch = false) {
+ const createNewBranch = newBranch || this.startNewMR;
- makeCommit(newBranch) {
- // see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
- const commitMessage = this.commitMessage;
- const actions = this.changedFiles.map(f => ({
- action: f.tempFile ? 'create' : 'update',
- file_path: f.path,
- content: f.newContent,
- }));
- const branch = newBranch ? `${this.currentBranch}-${this.currentShortHash}` : this.currentBranch;
const payload = {
- branch,
- commit_message: commitMessage,
- actions,
+ branch: createNewBranch ? `${this.currentBranch}-${new Date().getTime().toString()}` : this.currentBranch,
+ commit_message: this.commitMessage,
+ actions: this.changedFiles.map(f => ({
+ action: f.tempFile ? 'create' : 'update',
+ file_path: f.path,
+ content: f.content,
+ encoding: f.base64 ? 'base64' : 'text',
+ })),
+ start_branch: createNewBranch ? this.currentBranch : undefined,
};
- if (newBranch) {
- payload.start_branch = this.currentBranch;
- }
- Service.commitFiles(payload)
+
+ this.showNewBranchDialog = false;
+ this.submitCommitsLoading = true;
+
+ this.commitChanges({ payload, newMr: this.startNewMR })
.then(() => {
- this.resetCommitState();
- if (this.startNewMR) {
- this.redirectToNewMr(branch);
- } else {
- this.redirectToBranch(branch);
- }
+ this.submitCommitsLoading = false;
+ this.getTreeData();
})
.catch(() => {
- Flash('An error occurred while committing your changes');
+ this.submitCommitsLoading = false;
});
},
-
- tryCommit(e, skipBranchCheck = false, newBranch = false) {
+ tryCommit() {
this.submitCommitsLoading = true;
- if (skipBranchCheck) {
- this.makeCommit(newBranch);
- } else {
- Store.setBranchHash()
- .then(() => {
- if (Store.branchChanged) {
- Store.showNewBranchDialog = true;
- return;
- }
- this.makeCommit(newBranch);
- })
- .catch(() => {
- this.submitCommitsLoading = false;
- Flash('An error occurred while committing your changes');
- });
- }
- },
-
- redirectToNewMr(branch) {
- visitUrl(this.newMrTemplateUrl.replace('{{source_branch}}', branch));
- },
-
- redirectToBranch(branch) {
- visitUrl(this.customBranchURL.replace('{{branch}}', branch));
- },
-
- resetCommitState() {
- this.submitCommitsLoading = false;
- this.openedFiles = this.openedFiles.map((file) => {
- const f = file;
- f.changed = false;
- return f;
- });
- this.changedFiles = [];
- this.commitMessage = '';
- this.editMode = false;
- window.scrollTo(0, 0);
+ this.checkCommitStatus()
+ .then((branchChanged) => {
+ if (branchChanged) {
+ this.showNewBranchDialog = true;
+ } else {
+ this.makeCommit();
+ }
+ })
+ .catch(() => {
+ this.submitCommitsLoading = false;
+ });
},
},
};
</script>
<template>
-<div
- v-if="showCommitable"
- id="commit-area">
+<div id="commit-area">
<popup-dialog
v-if="showNewBranchDialog"
:primary-button-label="__('Create new branch')"
kind="primary"
:title="__('Branch has changed')"
:text="__('This branch has changed since you started editing. Would you like to create a new branch?')"
- @submit="commitToNewBranch"
+ @toggle="showNewBranchDialog = false"
+ @submit="makeCommit(true)"
/>
<form
class="form-horizontal"
- @submit.prevent="tryCommit">
+ @submit.prevent="tryCommit()">
<fieldset>
<div class="form-group">
<label class="col-md-4 control-label staged-files">
@@ -144,10 +103,10 @@ export default {
<div class="col-md-6">
<ul class="list-unstyled changed-files">
<li
- v-for="branchPath in branchPaths"
- :key="branchPath">
+ v-for="(file, index) in changedFiles"
+ :key="index">
<span class="help-block">
- {{branchPath}}
+ {{ file.path }}
</span>
</li>
</ul>
@@ -182,9 +141,8 @@ export default {
</div>
<div class="col-md-offset-4 col-md-6">
<button
- ref="submitCommit"
type="submit"
- :disabled="cantCommitYet"
+ :disabled="commitButtonDisabled"
class="btn btn-success">
<i
v-if="submitCommitsLoading"
@@ -193,7 +151,7 @@ export default {
aria-label="loading">
</i>
<span class="commit-summary">
- Commit {{changedFiles.length}} {{filePluralize}}
+ {{ commitButtonText }}
</span>
</button>
</div>
diff --git a/app/assets/javascripts/repo/components/repo_edit_button.vue b/app/assets/javascripts/repo/components/repo_edit_button.vue
index e6e8b2e5205..6c1bb4b8566 100644
--- a/app/assets/javascripts/repo/components/repo_edit_button.vue
+++ b/app/assets/javascripts/repo/components/repo_edit_button.vue
@@ -1,50 +1,57 @@
<script>
-import Store from '../stores/repo_store';
-import RepoMixin from '../mixins/repo_mixin';
+import { mapGetters, mapActions, mapState } from 'vuex';
+import popupDialog from '../../vue_shared/components/popup_dialog.vue';
export default {
- data() {
- return Store;
+ components: {
+ popupDialog,
},
- mixins: [RepoMixin],
computed: {
+ ...mapState([
+ 'editMode',
+ 'discardPopupOpen',
+ ]),
+ ...mapGetters([
+ 'canEditFile',
+ ]),
buttonLabel() {
return this.editMode ? this.__('Cancel edit') : this.__('Edit');
},
-
- showButton() {
- return this.isCommitable &&
- !this.activeFile.render_error &&
- !this.binary &&
- this.openedFiles.length;
- },
},
methods: {
- editCancelClicked() {
- if (this.changedFiles.length) {
- this.dialog.open = true;
- return;
- }
- this.editMode = !this.editMode;
- Store.toggleBlobView();
- },
+ ...mapActions([
+ 'toggleEditMode',
+ 'closeDiscardPopup',
+ ]),
},
};
</script>
<template>
-<button
- v-if="showButton"
- class="btn btn-default"
- type="button"
- @click.prevent="editCancelClicked">
- <i
- v-if="!editMode"
- class="fa fa-pencil"
- aria-hidden="true">
- </i>
- <span>
- {{buttonLabel}}
- </span>
-</button>
+ <div class="editable-mode">
+ <button
+ v-if="canEditFile"
+ class="btn btn-default"
+ type="button"
+ @click.prevent="toggleEditMode()">
+ <i
+ v-if="!editMode"
+ class="fa fa-pencil"
+ aria-hidden="true">
+ </i>
+ <span>
+ {{buttonLabel}}
+ </span>
+ </button>
+ <popup-dialog
+ v-if="discardPopupOpen"
+ class="text-left"
+ :primary-button-label="__('Discard changes')"
+ kind="warning"
+ :title="__('Are you sure?')"
+ :text="__('Are you sure you want to discard your changes?')"
+ @toggle="closeDiscardPopup"
+ @submit="toggleEditMode(true)"
+ />
+ </div>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_editor.vue b/app/assets/javascripts/repo/components/repo_editor.vue
index df4caba51d8..1c864b176b1 100644
--- a/app/assets/javascripts/repo/components/repo_editor.vue
+++ b/app/assets/javascripts/repo/components/repo_editor.vue
@@ -1,124 +1,107 @@
<script>
/* global monaco */
-import Store from '../stores/repo_store';
-import Service from '../services/repo_service';
-import Helper from '../helpers/repo_helper';
-
-const RepoEditor = {
- data() {
- return Store;
- },
+import { mapGetters, mapActions } from 'vuex';
+import flash from '../../flash';
+import monacoLoader from '../monaco_loader';
+export default {
destroyed() {
- if (Helper.monacoInstance) {
- Helper.monacoInstance.destroy();
+ if (this.monacoInstance) {
+ this.monacoInstance.destroy();
}
},
-
mounted() {
- Service.getRaw(this.activeFile)
- .then((rawResponse) => {
- Store.blobRaw = rawResponse.data;
- Store.activeFile.plain = rawResponse.data;
-
- const monacoInstance = Helper.monaco.editor.create(this.$el, {
- model: null,
- readOnly: false,
- contextmenu: true,
- scrollBeyondLastLine: false,
- });
+ if (this.monaco) {
+ this.initMonaco();
+ } else {
+ monacoLoader(['vs/editor/editor.main'], () => {
+ this.monaco = monaco;
+
+ this.initMonaco();
+ });
+ }
+ },
+ methods: {
+ ...mapActions([
+ 'getRawFileData',
+ 'changeFileContent',
+ ]),
+ initMonaco() {
+ if (this.shouldHideEditor) return;
+
+ if (this.monacoInstance) {
+ this.monacoInstance.setModel(null);
+ }
- Helper.monacoInstance = monacoInstance;
+ this.getRawFileData(this.activeFile)
+ .then(() => {
+ if (!this.monacoInstance) {
+ this.monacoInstance = this.monaco.editor.create(this.$el, {
+ model: null,
+ readOnly: false,
+ contextmenu: true,
+ scrollBeyondLastLine: false,
+ });
- this.addMonacoEvents();
+ this.languages = this.monaco.languages.getLanguages();
- this.setupEditor();
- })
- .catch(Helper.loadingError);
- },
+ this.addMonacoEvents();
+ }
- methods: {
+ this.setupEditor();
+ })
+ .catch(() => flash('Error setting up monaco. Please try again.'));
+ },
setupEditor() {
- this.showHide();
+ if (!this.activeFile) return;
+ const content = this.activeFile.content !== '' ? this.activeFile.content : this.activeFile.raw;
- Helper.setMonacoModelFromLanguage();
- },
+ const foundLang = this.languages.find(lang =>
+ lang.extensions && lang.extensions.indexOf(this.activeFileExtension) === 0,
+ );
+ const newModel = this.monaco.editor.createModel(
+ content, foundLang ? foundLang.id : 'plaintext',
+ );
- showHide() {
- if (!this.openedFiles.length || (this.binary && !this.activeFile.raw)) {
- this.$el.style.display = 'none';
- } else {
- this.$el.style.display = 'inline-block';
- }
+ this.monacoInstance.setModel(newModel);
},
-
addMonacoEvents() {
- Helper.monacoInstance.onMouseUp(this.onMonacoEditorMouseUp);
- Helper.monacoInstance.onKeyUp(this.onMonacoEditorKeysPressed.bind(this));
- },
-
- onMonacoEditorKeysPressed() {
- Store.setActiveFileContents(Helper.monacoInstance.getValue());
- },
-
- onMonacoEditorMouseUp(e) {
- if (!e.target.position) return;
- const lineNumber = e.target.position.lineNumber;
- if (e.target.element.classList.contains('line-numbers')) {
- location.hash = `L${lineNumber}`;
- Store.setActiveLine(lineNumber);
- }
+ this.monacoInstance.onKeyUp(() => {
+ this.changeFileContent({
+ file: this.activeFile,
+ content: this.monacoInstance.getValue(),
+ });
+ });
},
},
-
watch: {
- dialog: {
- handler(obj) {
- const newObj = obj;
- if (newObj.status) {
- newObj.status = false;
- this.openedFiles = this.openedFiles.map((file) => {
- const f = file;
- if (f.active) {
- this.blobRaw = f.plain;
- }
- f.changed = false;
- delete f.newContent;
-
- return f;
- });
- this.editMode = false;
- Store.toggleBlobView();
- }
- },
- deep: true,
- },
-
- blobRaw() {
- if (Helper.monacoInstance) {
- this.setupEditor();
- }
- },
-
- activeLine() {
- if (Helper.monacoInstance) {
- Helper.monacoInstance.setPosition({
- lineNumber: this.activeLine,
- column: 1,
- });
+ activeFile(oldVal, newVal) {
+ if (newVal && !newVal.active) {
+ this.initMonaco();
}
},
},
computed: {
+ ...mapGetters([
+ 'activeFile',
+ 'activeFileExtension',
+ ]),
shouldHideEditor() {
- return !this.openedFiles.length || (this.binary && !this.activeFile.raw);
+ return this.activeFile.binary && !this.activeFile.raw;
},
},
};
-
-export default RepoEditor;
</script>
<template>
-<div id="ide" v-if='!shouldHideEditor'></div>
+ <div
+ id="ide"
+ class="blob-viewer-container blob-editor-container"
+ >
+ <div
+ v-if="shouldHideEditor"
+ v-html="activeFile.html"
+ >
+ </div>
+ </div>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_file.vue b/app/assets/javascripts/repo/components/repo_file.vue
index 8c86e87ed3a..7a23154b340 100644
--- a/app/assets/javascripts/repo/components/repo_file.vue
+++ b/app/assets/javascripts/repo/components/repo_file.vue
@@ -1,11 +1,9 @@
<script>
+ import { mapActions, mapGetters } from 'vuex';
import timeAgoMixin from '../../vue_shared/mixins/timeago';
- import eventHub from '../event_hub';
- import repoMixin from '../mixins/repo_mixin';
export default {
mixins: [
- repoMixin,
timeAgoMixin,
],
props: {
@@ -15,13 +13,15 @@
},
},
computed: {
+ ...mapGetters([
+ 'isCollapsed',
+ ]),
fileIcon() {
- const classObj = {
+ return {
'fa-spinner fa-spin': this.file.loading,
[this.file.icon]: !this.file.loading,
'fa-folder-open': !this.file.loading && this.file.opened,
};
- return classObj;
},
levelIndentation() {
return {
@@ -33,9 +33,9 @@
},
},
methods: {
- linkClicked(file) {
- eventHub.$emit('fileNameClicked', file);
- },
+ ...mapActions([
+ 'clickedTreeRow',
+ ]),
},
};
</script>
@@ -43,7 +43,7 @@
<template>
<tr
class="file"
- @click.prevent="linkClicked(file)">
+ @click.prevent="clickedTreeRow(file)">
<td>
<i
class="fa fa-fw file-icon"
@@ -71,7 +71,7 @@
</template>
</td>
- <template v-if="!isMini">
+ <template v-if="!isCollapsed">
<td class="hidden-sm hidden-xs">
<a
@click.stop
diff --git a/app/assets/javascripts/repo/components/repo_file_buttons.vue b/app/assets/javascripts/repo/components/repo_file_buttons.vue
index c98f641c853..dd948ee84fb 100644
--- a/app/assets/javascripts/repo/components/repo_file_buttons.vue
+++ b/app/assets/javascripts/repo/components/repo_file_buttons.vue
@@ -1,37 +1,22 @@
<script>
-import Store from '../stores/repo_store';
-import Helper from '../helpers/repo_helper';
-import RepoMixin from '../mixins/repo_mixin';
-
-const RepoFileButtons = {
- data() {
- return Store;
- },
-
- mixins: [RepoMixin],
+import { mapGetters } from 'vuex';
+export default {
computed: {
+ ...mapGetters([
+ 'activeFile',
+ ]),
showButtons() {
- return this.activeFile.raw_path ||
- this.activeFile.blame_path ||
- this.activeFile.commits_path ||
+ return this.activeFile.rawPath ||
+ this.activeFile.blamePath ||
+ this.activeFile.commitsPath ||
this.activeFile.permalink;
},
rawDownloadButtonLabel() {
- return this.binary ? 'Download' : 'Raw';
- },
-
- canPreview() {
- return Helper.isRenderable();
+ return this.activeFile.binary ? 'Download' : 'Raw';
},
},
-
- methods: {
- rawPreviewToggle: Store.toggleRawPreview,
- },
};
-
-export default RepoFileButtons;
</script>
<template>
@@ -40,11 +25,11 @@ export default RepoFileButtons;
class="repo-file-buttons"
>
<a
- :href="activeFile.raw_path"
+ :href="activeFile.rawPath"
target="_blank"
class="btn btn-default raw"
rel="noopener noreferrer">
- {{rawDownloadButtonLabel}}
+ {{ rawDownloadButtonLabel }}
</a>
<div
@@ -52,12 +37,12 @@ export default RepoFileButtons;
role="group"
aria-label="File actions">
<a
- :href="activeFile.blame_path"
+ :href="activeFile.blamePath"
class="btn btn-default blame">
Blame
</a>
<a
- :href="activeFile.commits_path"
+ :href="activeFile.commitsPath"
class="btn btn-default history">
History
</a>
@@ -67,13 +52,5 @@ export default RepoFileButtons;
Permalink
</a>
</div>
-
- <a
- v-if="canPreview"
- href="#"
- @click.prevent="rawPreviewToggle"
- class="btn btn-default preview">
- {{activeFileLabel}}
- </a>
</div>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_loading_file.vue b/app/assets/javascripts/repo/components/repo_loading_file.vue
index 832b45b2b29..1e6c405f292 100644
--- a/app/assets/javascripts/repo/components/repo_loading_file.vue
+++ b/app/assets/javascripts/repo/components/repo_loading_file.vue
@@ -1,10 +1,12 @@
<script>
- import repoMixin from '../mixins/repo_mixin';
+ import { mapGetters } from 'vuex';
export default {
- mixins: [
- repoMixin,
- ],
+ computed: {
+ ...mapGetters([
+ 'isCollapsed',
+ ]),
+ },
methods: {
lineOfCode(n) {
return `skeleton-line-${n}`;
@@ -28,7 +30,7 @@
</div>
</div>
</td>
- <template v-if="!isMini">
+ <template v-if="!isCollapsed">
<td
class="hidden-sm hidden-xs">
<div class="animation-container">
diff --git a/app/assets/javascripts/repo/components/repo_prev_directory.vue b/app/assets/javascripts/repo/components/repo_prev_directory.vue
index c4bf6dcdec2..a2b305bbd05 100644
--- a/app/assets/javascripts/repo/components/repo_prev_directory.vue
+++ b/app/assets/javascripts/repo/components/repo_prev_directory.vue
@@ -1,26 +1,22 @@
<script>
- import eventHub from '../event_hub';
- import repoMixin from '../mixins/repo_mixin';
+ import { mapGetters, mapState, mapActions } from 'vuex';
export default {
- mixins: [
- repoMixin,
- ],
- props: {
- prevUrl: {
- type: String,
- required: true,
- },
- },
computed: {
+ ...mapState([
+ 'parentTreeUrl',
+ ]),
+ ...mapGetters([
+ 'isCollapsed',
+ ]),
colSpanCondition() {
- return this.isMini ? undefined : 3;
+ return this.isCollapsed ? undefined : 3;
},
},
methods: {
- linkClicked(file) {
- eventHub.$emit('goToPreviousDirectoryClicked', file);
- },
+ ...mapActions([
+ 'getTreeData',
+ ]),
},
};
</script>
@@ -30,9 +26,9 @@
<td
:colspan="colSpanCondition"
class="table-cell"
- @click.prevent="linkClicked(prevUrl)"
+ @click.prevent="getTreeData({ endpoint: parentTreeUrl })"
>
- <a :href="prevUrl">...</a>
+ <a :href="parentTreeUrl">...</a>
</td>
</tr>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_preview.vue b/app/assets/javascripts/repo/components/repo_preview.vue
index b5be771d539..d1883299bd9 100644
--- a/app/assets/javascripts/repo/components/repo_preview.vue
+++ b/app/assets/javascripts/repo/components/repo_preview.vue
@@ -1,26 +1,20 @@
<script>
/* global LineHighlighter */
-
-import Store from '../stores/repo_store';
+import { mapGetters } from 'vuex';
export default {
- data() {
- return Store;
- },
computed: {
- html() {
- return this.activeFile.html;
+ ...mapGetters([
+ 'activeFile',
+ ]),
+ renderErrorTooLarge() {
+ return this.activeFile.renderError === 'too_large';
},
},
methods: {
highlightFile() {
$(this.$el).find('.file-content').syntaxHighlight();
},
- highlightLine() {
- if (Store.activeLine > -1) {
- this.lineHighlighter.highlightHash(`#L${Store.activeLine}`);
- }
- },
},
mounted() {
this.highlightFile();
@@ -29,38 +23,39 @@ export default {
scrollFileHolder: true,
});
},
- watch: {
- html() {
- this.$nextTick(() => {
- this.highlightFile();
- this.highlightLine();
- });
- },
- activeLine() {
- this.highlightLine();
- },
+ updated() {
+ this.$nextTick(() => {
+ this.highlightFile();
+ });
},
};
</script>
<template>
-<div>
+<div class="blob-viewer-container">
<div
- v-if="!activeFile.render_error"
+ v-if="!activeFile.renderError"
v-html="activeFile.html">
</div>
<div
- v-else-if="activeFile.tooLarge"
+ v-else-if="activeFile.tempFile"
+ class="vertical-center render-error">
+ <p class="text-center">
+ The source could not be displayed for this temporary file.
+ </p>
+ </div>
+ <div
+ v-else-if="renderErrorTooLarge"
class="vertical-center render-error">
<p class="text-center">
- The source could not be displayed because it is too large. You can <a :href="activeFile.raw_path">download</a> it instead.
+ The source could not be displayed because it is too large. You can <a :href="activeFile.rawPath" download>download</a> it instead.
</p>
</div>
<div
v-else
class="vertical-center render-error">
<p class="text-center">
- The source could not be displayed because a rendering error occurred. You can <a :href="activeFile.raw_path">download</a> it instead.
+ The source could not be displayed because a rendering error occurred. You can <a :href="activeFile.rawPath" download>download</a> it instead.
</p>
</div>
</div>
diff --git a/app/assets/javascripts/repo/components/repo_sidebar.vue b/app/assets/javascripts/repo/components/repo_sidebar.vue
index 09dc9ee25d7..63c0d70f5c0 100644
--- a/app/assets/javascripts/repo/components/repo_sidebar.vue
+++ b/app/assets/javascripts/repo/components/repo_sidebar.vue
@@ -1,120 +1,55 @@
<script>
-import _ from 'underscore';
-import Service from '../services/repo_service';
-import Helper from '../helpers/repo_helper';
-import Store from '../stores/repo_store';
-import eventHub from '../event_hub';
+import { mapState, mapGetters, mapActions } from 'vuex';
import RepoPreviousDirectory from './repo_prev_directory.vue';
import RepoFile from './repo_file.vue';
import RepoLoadingFile from './repo_loading_file.vue';
-import RepoMixin from '../mixins/repo_mixin';
export default {
- mixins: [RepoMixin],
components: {
'repo-previous-directory': RepoPreviousDirectory,
'repo-file': RepoFile,
'repo-loading-file': RepoLoadingFile,
},
created() {
- window.addEventListener('popstate', this.checkHistory);
+ window.addEventListener('popstate', this.popHistoryState);
},
destroyed() {
- eventHub.$off('fileNameClicked', this.fileClicked);
- eventHub.$off('goToPreviousDirectoryClicked', this.goToPreviousDirectoryClicked);
- window.removeEventListener('popstate', this.checkHistory);
+ window.removeEventListener('popstate', this.popHistoryState);
},
mounted() {
- eventHub.$on('fileNameClicked', this.fileClicked);
- eventHub.$on('goToPreviousDirectoryClicked', this.goToPreviousDirectoryClicked);
- },
- data() {
- return Store;
+ this.getTreeData();
},
computed: {
- flattendFiles() {
- const mapFiles = arr => (!arr.files.length ? [] : _.map(arr.files, a => [a, mapFiles(a)]));
-
- return _.chain(this.files)
- .map(arr => [arr, mapFiles(arr)])
- .flatten()
- .value();
- },
+ ...mapState([
+ 'loading',
+ 'isRoot',
+ ]),
+ ...mapState({
+ projectName(state) {
+ return state.project.name;
+ },
+ }),
+ ...mapGetters([
+ 'treeList',
+ 'isCollapsed',
+ ]),
},
methods: {
- checkHistory() {
- let selectedFile = this.files.find(file => location.pathname.indexOf(file.url) > -1);
- if (!selectedFile) {
- // Maybe it is not in the current tree but in the opened tabs
- selectedFile = Helper.getFileFromPath(location.pathname);
- }
-
- let lineNumber = null;
- if (location.hash.indexOf('#L') > -1) lineNumber = Number(location.hash.substr(2));
-
- if (selectedFile) {
- if (selectedFile.url !== this.activeFile.url) {
- this.fileClicked(selectedFile, lineNumber);
- } else {
- Store.setActiveLine(lineNumber);
- }
- } else {
- // Not opened at all lets open new tab
- this.fileClicked({
- url: location.href,
- }, lineNumber);
- }
- },
-
- fileClicked(clickedFile, lineNumber) {
- const file = clickedFile;
-
- if (file.loading) return;
-
- if (file.type === 'tree' && file.opened) {
- Helper.setDirectoryToClosed(file);
- Store.setActiveLine(lineNumber);
- } else if (file.type === 'submodule') {
- file.loading = true;
-
- gl.utils.visitUrl(file.url);
- } else {
- const openFile = Helper.getFileFromPath(file.url);
-
- if (openFile) {
- Store.setActiveFiles(openFile);
- Store.setActiveLine(lineNumber);
- } else {
- file.loading = true;
- Service.url = file.url;
- Helper.getContent(file)
- .then(() => {
- file.loading = false;
- Helper.scrollTabsRight();
- Store.setActiveLine(lineNumber);
- })
- .catch(Helper.loadingError);
- }
- }
- },
-
- goToPreviousDirectoryClicked(prevURL) {
- Service.url = prevURL;
- Helper.getContent(null, true)
- .then(() => Helper.scrollTabsRight())
- .catch(Helper.loadingError);
- },
+ ...mapActions([
+ 'getTreeData',
+ 'popHistoryState',
+ ]),
},
};
</script>
<template>
-<div id="sidebar" :class="{'sidebar-mini' : isMini}">
+<div id="sidebar" :class="{'sidebar-mini' : isCollapsed}">
<table class="table">
<thead>
<tr>
<th
- v-if="isMini"
+ v-if="isCollapsed"
class="repo-file-options title"
>
<strong class="clgray">
@@ -136,17 +71,16 @@ export default {
</thead>
<tbody>
<repo-previous-directory
- v-if="!isRoot && !loading.tree"
- :prev-url="prevURL"
+ v-if="!isRoot && treeList.length"
/>
<repo-loading-file
- v-if="!flattendFiles.length && loading.tree"
+ v-if="!treeList.length && loading"
v-for="n in 5"
:key="n"
/>
<repo-file
- v-for="file in flattendFiles"
- :key="file.id"
+ v-for="(file, index) in treeList"
+ :key="index"
:file="file"
/>
</tbody>
diff --git a/app/assets/javascripts/repo/components/repo_tab.vue b/app/assets/javascripts/repo/components/repo_tab.vue
index 405d7b4cf86..da0714c368c 100644
--- a/app/assets/javascripts/repo/components/repo_tab.vue
+++ b/app/assets/javascripts/repo/components/repo_tab.vue
@@ -1,7 +1,7 @@
<script>
-import Store from '../stores/repo_store';
+import { mapActions } from 'vuex';
-const RepoTab = {
+export default {
props: {
tab: {
type: Object,
@@ -11,7 +11,7 @@ const RepoTab = {
computed: {
closeLabel() {
- if (this.tab.changed) {
+ if (this.tab.changed || this.tab.tempFile) {
return `${this.tab.name} changed`;
}
return `Close ${this.tab.name}`;
@@ -26,29 +26,23 @@ const RepoTab = {
},
methods: {
- tabClicked(file) {
- Store.setActiveFiles(file);
- },
- closeTab(file) {
- if (file.changed || file.tempFile) return;
-
- Store.removeFromOpenedFiles(file);
- },
+ ...mapActions([
+ 'setFileActive',
+ 'closeFile',
+ ]),
},
};
-
-export default RepoTab;
</script>
<template>
<li
:class="{ active : tab.active }"
- @click="tabClicked(tab)"
+ @click="setFileActive(tab)"
>
<button
type="button"
class="close-btn"
- @click.stop.prevent="closeTab(tab)"
+ @click.stop.prevent="closeFile({ file: tab })"
:aria-label="closeLabel">
<i
class="fa"
@@ -61,7 +55,7 @@ export default RepoTab;
href="#"
class="repo-tab"
:title="tab.url"
- @click.prevent="tabClicked(tab)">
+ @click.prevent.stop="setFileActive(tab)">
{{tab.name}}
</a>
</li>
diff --git a/app/assets/javascripts/repo/components/repo_tabs.vue b/app/assets/javascripts/repo/components/repo_tabs.vue
index b57cd0960de..59beae53e8d 100644
--- a/app/assets/javascripts/repo/components/repo_tabs.vue
+++ b/app/assets/javascripts/repo/components/repo_tabs.vue
@@ -1,15 +1,15 @@
<script>
- import Store from '../stores/repo_store';
+ import { mapState } from 'vuex';
import RepoTab from './repo_tab.vue';
- import RepoMixin from '../mixins/repo_mixin';
export default {
- mixins: [RepoMixin],
components: {
'repo-tab': RepoTab,
},
- data() {
- return Store;
+ computed: {
+ ...mapState([
+ 'openFiles',
+ ]),
},
};
</script>
@@ -20,7 +20,7 @@
class="list-unstyled"
>
<repo-tab
- v-for="tab in openedFiles"
+ v-for="tab in openFiles"
:key="tab.id"
:tab="tab"
/>
diff --git a/app/assets/javascripts/repo/event_hub.js b/app/assets/javascripts/repo/event_hub.js
deleted file mode 100644
index 0948c2e5352..00000000000
--- a/app/assets/javascripts/repo/event_hub.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import Vue from 'vue';
-
-export default new Vue();
diff --git a/app/assets/javascripts/repo/helpers/monaco_loader_helper.js b/app/assets/javascripts/repo/helpers/monaco_loader_helper.js
deleted file mode 100644
index f8729bbf585..00000000000
--- a/app/assets/javascripts/repo/helpers/monaco_loader_helper.js
+++ /dev/null
@@ -1,25 +0,0 @@
-/* global monaco */
-import RepoEditor from '../components/repo_editor.vue';
-import Store from '../stores/repo_store';
-import Helper from '../helpers/repo_helper';
-import monacoLoader from '../monaco_loader';
-
-function repoEditorLoader() {
- Store.monacoLoading = true;
- return new Promise((resolve, reject) => {
- monacoLoader(['vs/editor/editor.main'], () => {
- Helper.monaco = monaco;
- Store.monacoLoading = false;
- resolve(RepoEditor);
- }, () => {
- Store.monacoLoading = false;
- reject();
- });
- });
-}
-
-const MonacoLoaderHelper = {
- repoEditorLoader,
-};
-
-export default MonacoLoaderHelper;
diff --git a/app/assets/javascripts/repo/helpers/repo_helper.js b/app/assets/javascripts/repo/helpers/repo_helper.js
deleted file mode 100644
index fb26f3b7380..00000000000
--- a/app/assets/javascripts/repo/helpers/repo_helper.js
+++ /dev/null
@@ -1,317 +0,0 @@
-import Service from '../services/repo_service';
-import Store from '../stores/repo_store';
-import Flash from '../../flash';
-
-const RepoHelper = {
- monacoInstance: null,
-
- getDefaultActiveFile() {
- return {
- id: '',
- active: true,
- binary: false,
- extension: '',
- html: '',
- mime_type: '',
- name: '',
- plain: '',
- size: 0,
- url: '',
- raw: false,
- newContent: '',
- changed: false,
- loading: false,
- };
- },
-
- key: '',
-
- Time: window.performance
- && window.performance.now
- ? window.performance
- : Date,
-
- getFileExtension(fileName) {
- return fileName.split('.').pop();
- },
-
- getLanguageIDForFile(file, langs) {
- const ext = RepoHelper.getFileExtension(file.name);
- const foundLang = RepoHelper.findLanguage(ext, langs);
-
- return foundLang ? foundLang.id : 'plaintext';
- },
-
- setMonacoModelFromLanguage() {
- RepoHelper.monacoInstance.setModel(null);
- const languages = RepoHelper.monaco.languages.getLanguages();
- const languageID = RepoHelper.getLanguageIDForFile(Store.activeFile, languages);
- const newModel = RepoHelper.monaco.editor.createModel(Store.blobRaw, languageID);
- RepoHelper.monacoInstance.setModel(newModel);
- },
-
- findLanguage(ext, langs) {
- return langs.find(lang => lang.extensions && lang.extensions.indexOf(`.${ext}`) > -1);
- },
-
- setDirectoryOpen(tree, title) {
- if (!tree) return;
-
- Object.assign(tree, {
- opened: true,
- });
-
- RepoHelper.updateHistoryEntry(tree.url, title);
- Store.path = tree.path;
- },
-
- setDirectoryToClosed(entry) {
- Object.assign(entry, {
- opened: false,
- files: [],
- });
- },
-
- isRenderable() {
- const okExts = ['md', 'svg'];
- return okExts.indexOf(Store.activeFile.extension) > -1;
- },
-
- setBinaryDataAsBase64(file) {
- Service.getBase64Content(file.raw_path)
- .then((response) => {
- Store.blobRaw = response;
- file.base64 = response; // eslint-disable-line no-param-reassign
- })
- .catch(RepoHelper.loadingError);
- },
-
- getContent(treeOrFile, emptyFiles = false) {
- let file = treeOrFile;
-
- if (!Store.files.length) {
- Store.loading.tree = true;
- }
-
- return Service.getContent()
- .then((response) => {
- const data = response.data;
- if (response.headers && response.headers['page-title']) data.pageTitle = decodeURI(response.headers['page-title']);
- if (data.path && !Store.isInitialRoot) {
- Store.isRoot = data.path === '/';
- Store.isInitialRoot = Store.isRoot;
- }
-
- if (file && file.type === 'blob') {
- if (!file) file = data;
- Store.binary = data.binary;
-
- if (data.binary) {
- // file might be undefined
- RepoHelper.setBinaryDataAsBase64(data);
- Store.setViewToPreview();
- } else if (!Store.isPreviewView() && !data.render_error) {
- Service.getRaw(data)
- .then((rawResponse) => {
- Store.blobRaw = rawResponse.data;
- data.plain = rawResponse.data;
- RepoHelper.setFile(data, file);
- }).catch(RepoHelper.loadingError);
- }
-
- if (Store.isPreviewView()) {
- RepoHelper.setFile(data, file);
- }
- } else {
- Store.loading.tree = false;
- RepoHelper.setDirectoryOpen(file, data.pageTitle || data.name);
-
- if (emptyFiles) {
- Store.files = [];
- }
-
- this.addToDirectory(file, data);
-
- Store.prevURL = Service.blobURLtoParentTree(Service.url);
- }
- }).catch(RepoHelper.loadingError);
- },
-
- addToDirectory(file, data) {
- const tree = file || Store;
-
- // TODO: Figure out why `popstate` is being trigger in the specs
- if (!tree.files) return;
-
- const files = tree.files.concat(this.dataToListOfFiles(data, file ? file.level + 1 : 0));
-
- tree.files = files;
- },
-
- setFile(data, file) {
- const newFile = data;
- newFile.url = file.url || Service.url; // Grab the URL from service, happens on page refresh.
-
- if (newFile.render_error === 'too_large' || newFile.render_error === 'collapsed') {
- newFile.tooLarge = true;
- }
- newFile.newContent = '';
-
- Store.addToOpenedFiles(newFile);
- Store.setActiveFiles(newFile);
- },
-
- serializeRepoEntity(type, entity, level = 0) {
- const {
- id,
- url,
- name,
- icon,
- last_commit,
- tree_url,
- path,
- tempFile,
- active,
- opened,
- } = entity;
-
- return {
- id,
- type,
- name,
- url,
- tree_url,
- path,
- level,
- tempFile,
- icon: `fa-${icon}`,
- files: [],
- loading: false,
- opened,
- active,
- // eslint-disable-next-line camelcase
- lastCommit: last_commit ? {
- url: `${Store.projectUrl}/commit/${last_commit.id}`,
- message: last_commit.message,
- updatedAt: last_commit.committed_date,
- } : {},
- };
- },
-
- scrollTabsRight() {
- const tabs = document.getElementById('tabs');
- if (!tabs) return;
- tabs.scrollLeft = tabs.scrollWidth;
- },
-
- dataToListOfFiles(data, level) {
- const { blobs, trees, submodules } = data;
- return [
- ...trees.map(tree => RepoHelper.serializeRepoEntity('tree', tree, level)),
- ...submodules.map(submodule => RepoHelper.serializeRepoEntity('submodule', submodule, level)),
- ...blobs.map(blob => RepoHelper.serializeRepoEntity('blob', blob, level)),
- ];
- },
-
- genKey() {
- return RepoHelper.Time.now().toFixed(3);
- },
-
- updateHistoryEntry(url, title) {
- const history = window.history;
-
- RepoHelper.key = RepoHelper.genKey();
-
- if (document.location.pathname !== url) {
- history.pushState({ key: RepoHelper.key }, '', url);
- }
-
- if (title) {
- document.title = title;
- }
- },
-
- findOpenedFileFromActive() {
- return Store.openedFiles.find(openedFile => Store.activeFile.id === openedFile.id);
- },
-
- getFileFromPath(path) {
- return Store.openedFiles.find(file => file.url === path);
- },
-
- loadingError() {
- Flash('Unable to load this content at this time.');
- },
- openEditMode() {
- Store.editMode = true;
- Store.currentBlobView = 'repo-editor';
- },
- updateStorePath(path) {
- Store.path = path;
- },
- findOrCreateEntry(type, tree, name) {
- let exists = true;
- let foundEntry = tree.files.find(dir => dir.type === type && dir.name === name);
-
- if (!foundEntry) {
- foundEntry = RepoHelper.serializeRepoEntity(type, {
- id: name,
- name,
- path: tree.path ? `${tree.path}/${name}` : name,
- icon: type === 'tree' ? 'folder' : 'file-text-o',
- tempFile: true,
- opened: true,
- active: true,
- }, tree.level !== undefined ? tree.level + 1 : 0);
-
- exists = false;
- tree.files.push(foundEntry);
- }
-
- return {
- entry: foundEntry,
- exists,
- };
- },
- removeAllTmpFiles(storeFilesKey) {
- Store[storeFilesKey] = Store[storeFilesKey].filter(f => !f.tempFile);
- },
- createNewEntry(name, type) {
- const originalPath = Store.path;
- let entryName = name;
-
- if (entryName.indexOf(`${originalPath}/`) !== 0) {
- this.updateStorePath('');
- } else {
- entryName = entryName.replace(`${originalPath}/`, '');
- }
-
- if (entryName === '') return;
-
- const fileName = type === 'tree' ? '.gitkeep' : entryName;
- let tree = Store;
-
- if (type === 'tree') {
- const dirNames = entryName.split('/');
-
- dirNames.forEach((dirName) => {
- if (dirName === '') return;
-
- tree = this.findOrCreateEntry('tree', tree, dirName).entry;
- });
- }
-
- if ((type === 'tree' && tree.tempFile) || type === 'blob') {
- const file = this.findOrCreateEntry('blob', tree, fileName);
-
- if (!file.exists) {
- this.setFile(file.entry, file.entry);
- this.openEditMode();
- }
- }
-
- this.updateStorePath(originalPath);
- },
-};
-
-export default RepoHelper;
diff --git a/app/assets/javascripts/repo/index.js b/app/assets/javascripts/repo/index.js
index 72fc5a70648..b6801af7fcb 100644
--- a/app/assets/javascripts/repo/index.js
+++ b/app/assets/javascripts/repo/index.js
@@ -1,55 +1,50 @@
-import $ from 'jquery';
import Vue from 'vue';
+import { mapActions } from 'vuex';
import { convertPermissionToBoolean } from '../lib/utils/common_utils';
-import Service from './services/repo_service';
-import Store from './stores/repo_store';
import Repo from './components/repo.vue';
import RepoEditButton from './components/repo_edit_button.vue';
import newBranchForm from './components/new_branch_form.vue';
import newDropdown from './components/new_dropdown/index.vue';
+import store from './stores';
import Translate from '../vue_shared/translate';
-function initDropdowns() {
- $('.js-tree-ref-target-holder').hide();
-}
-
-function addEventsForNonVueEls() {
- window.onbeforeunload = function confirmUnload(e) {
- const hasChanged = Store.openedFiles
- .some(file => file.changed);
- if (!hasChanged) return undefined;
- const event = e || window.event;
- if (event) event.returnValue = 'Are you sure you want to lose unsaved changes?';
- // For Safari
- return 'Are you sure you want to lose unsaved changes?';
- };
-}
-
-function setInitialStore(data) {
- Store.service = Service;
- Store.service.url = data.url;
- Store.service.refsUrl = data.refsUrl;
- Store.path = data.currentPath;
- Store.projectId = data.projectId;
- Store.projectName = data.projectName;
- Store.projectUrl = data.projectUrl;
- Store.canCommit = data.canCommit;
- Store.onTopOfBranch = data.onTopOfBranch;
- Store.newMrTemplateUrl = decodeURIComponent(data.newMrTemplateUrl);
- Store.customBranchURL = decodeURIComponent(data.blobUrl);
- Store.isRoot = convertPermissionToBoolean(data.root);
- Store.isInitialRoot = convertPermissionToBoolean(data.root);
- Store.currentBranch = $('button.dropdown-menu-toggle').attr('data-ref');
- Store.checkIsCommitable();
- Store.setBranchHash();
-}
-
function initRepo(el) {
+ if (!el) return null;
+
return new Vue({
el,
+ store,
components: {
repo: Repo,
},
+ methods: {
+ ...mapActions([
+ 'setInitialData',
+ ]),
+ },
+ created() {
+ const data = el.dataset;
+
+ this.setInitialData({
+ project: {
+ id: data.projectId,
+ name: data.projectName,
+ url: data.projectUrl,
+ },
+ endpoints: {
+ rootEndpoint: data.url,
+ newMergeRequestUrl: data.newMergeRequestUrl,
+ rootUrl: data.rootUrl,
+ },
+ canCommit: convertPermissionToBoolean(data.canCommit),
+ onTopOfBranch: convertPermissionToBoolean(data.onTopOfBranch),
+ currentRef: data.ref,
+ path: data.currentPath,
+ currentBranch: data.currentBranch,
+ isRoot: convertPermissionToBoolean(data.root),
+ isInitialRoot: convertPermissionToBoolean(data.root),
+ });
+ },
render(createElement) {
return createElement('repo');
},
@@ -59,15 +54,20 @@ function initRepo(el) {
function initRepoEditButton(el) {
return new Vue({
el,
+ store,
components: {
repoEditButton: RepoEditButton,
},
+ render(createElement) {
+ return createElement('repo-edit-button');
+ },
});
}
function initNewDropdown(el) {
return new Vue({
el,
+ store,
components: {
newDropdown,
},
@@ -87,32 +87,20 @@ function initNewBranchForm() {
components: {
newBranchForm,
},
+ store,
render(createElement) {
- return createElement('new-branch-form', {
- props: {
- currentBranch: Store.currentBranch,
- },
- });
+ return createElement('new-branch-form');
},
});
}
-function initRepoBundle() {
- const repo = document.getElementById('repo');
- const editButton = document.querySelector('.editable-mode');
- const newDropdownHolder = document.querySelector('.js-new-dropdown');
- setInitialStore(repo.dataset);
- addEventsForNonVueEls();
- initDropdowns();
-
- Vue.use(Translate);
-
- initRepo(repo);
- initRepoEditButton(editButton);
- initNewBranchForm();
- initNewDropdown(newDropdownHolder);
-}
+const repo = document.getElementById('repo');
+const editButton = document.querySelector('.editable-mode');
+const newDropdownHolder = document.querySelector('.js-new-dropdown');
-$(initRepoBundle);
+Vue.use(Translate);
-export default initRepoBundle;
+initRepo(repo);
+initRepoEditButton(editButton);
+initNewBranchForm();
+initNewDropdown(newDropdownHolder);
diff --git a/app/assets/javascripts/repo/mixins/repo_mixin.js b/app/assets/javascripts/repo/mixins/repo_mixin.js
deleted file mode 100644
index efeda426b96..00000000000
--- a/app/assets/javascripts/repo/mixins/repo_mixin.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import Store from '../stores/repo_store';
-
-const RepoMixin = {
- computed: {
- isMini() {
- return !!Store.openedFiles.length;
- },
-
- changedFiles() {
- const changedFileList = this.openedFiles
- .filter(file => file.changed || file.tempFile);
- return changedFileList;
- },
- },
-};
-
-export default RepoMixin;
diff --git a/app/assets/javascripts/repo/services/index.js b/app/assets/javascripts/repo/services/index.js
new file mode 100644
index 00000000000..dc222ccac01
--- /dev/null
+++ b/app/assets/javascripts/repo/services/index.js
@@ -0,0 +1,33 @@
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+import Api from '../../api';
+
+Vue.use(VueResource);
+
+export default {
+ getTreeData(endpoint) {
+ return Vue.http.get(endpoint, { params: { format: 'json' } });
+ },
+ getFileData(endpoint) {
+ return Vue.http.get(endpoint, { params: { format: 'json' } });
+ },
+ getRawFileData(file) {
+ if (file.tempFile) {
+ return Promise.resolve(file.content);
+ }
+
+ return Vue.http.get(file.rawPath, { params: { format: 'json' } })
+ .then(res => res.text());
+ },
+ getBranchData(projectId, currentBranch) {
+ return Api.branchSingle(projectId, currentBranch);
+ },
+ createBranch(projectId, payload) {
+ const url = Api.buildUrl(Api.createBranchPath).replace(':id', projectId);
+
+ return Vue.http.post(url, payload);
+ },
+ commit(projectId, payload) {
+ return Api.commitMultiple(projectId, payload);
+ },
+};
diff --git a/app/assets/javascripts/repo/services/repo_service.js b/app/assets/javascripts/repo/services/repo_service.js
deleted file mode 100644
index c9fa5cc8bf8..00000000000
--- a/app/assets/javascripts/repo/services/repo_service.js
+++ /dev/null
@@ -1,101 +0,0 @@
-import axios from 'axios';
-import csrf from '../../lib/utils/csrf';
-import Store from '../stores/repo_store';
-import Api from '../../api';
-import Helper from '../helpers/repo_helper';
-
-axios.defaults.headers.common[csrf.headerKey] = csrf.token;
-
-const RepoService = {
- url: '',
- options: {
- params: {
- format: 'json',
- },
- },
- createBranchPath: '/api/:version/projects/:id/repository/branches',
- richExtensionRegExp: /md/,
-
- getRaw(file) {
- if (file.tempFile) {
- return Promise.resolve({
- data: '',
- });
- }
-
- return axios.get(file.raw_path, {
- // Stop Axios from parsing a JSON file into a JS object
- transformResponse: [res => res],
- });
- },
-
- buildParams(url = this.url) {
- // shallow clone object without reference
- const params = Object.assign({}, this.options.params);
-
- if (this.urlIsRichBlob(url)) params.viewer = 'rich';
-
- return params;
- },
-
- urlIsRichBlob(url = this.url) {
- const extension = Helper.getFileExtension(url);
-
- return this.richExtensionRegExp.test(extension);
- },
-
- getContent(url = this.url) {
- const params = this.buildParams(url);
-
- return axios.get(url, {
- params,
- });
- },
-
- getBase64Content(url = this.url) {
- const request = axios.get(url, {
- responseType: 'arraybuffer',
- });
-
- return request.then(response => this.bufferToBase64(response.data));
- },
-
- bufferToBase64(data) {
- return new Buffer(data, 'binary').toString('base64');
- },
-
- blobURLtoParentTree(url) {
- const urlArray = url.split('/');
- urlArray.pop();
- const blobIndex = urlArray.lastIndexOf('blob');
-
- if (blobIndex > -1) urlArray[blobIndex] = 'tree';
-
- return urlArray.join('/');
- },
-
- getBranch() {
- return Api.branchSingle(Store.projectId, Store.currentBranch);
- },
-
- commitFiles(payload) {
- return Api.commitMultiple(Store.projectId, payload)
- .then(this.commitFlash);
- },
-
- createBranch(payload) {
- const url = Api.buildUrl(this.createBranchPath)
- .replace(':id', Store.projectId);
- return axios.post(url, payload);
- },
-
- commitFlash(data) {
- if (data.short_id && data.stats) {
- window.Flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
- } else {
- window.Flash(data.message);
- }
- },
-};
-
-export default RepoService;
diff --git a/app/assets/javascripts/repo/stores/actions.js b/app/assets/javascripts/repo/stores/actions.js
new file mode 100644
index 00000000000..ca2f2a5ce7a
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/actions.js
@@ -0,0 +1,129 @@
+import Vue from 'vue';
+import flash from '../../flash';
+import service from '../services';
+import * as types from './mutation_types';
+
+export const redirectToUrl = url => gl.utils.visitUrl(url);
+
+export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
+
+export const closeDiscardPopup = ({ commit }) => commit(types.TOGGLE_DISCARD_POPUP, false);
+
+export const discardAllChanges = ({ commit, getters, dispatch }) => {
+ const changedFiles = getters.changedFiles;
+
+ changedFiles.forEach((file) => {
+ commit(types.DISCARD_FILE_CHANGES, file);
+
+ if (file.tempFile) {
+ dispatch('closeFile', { file, force: true });
+ }
+ });
+};
+
+export const closeAllFiles = ({ state, dispatch }) => {
+ state.openFiles.forEach(file => dispatch('closeFile', { file }));
+};
+
+export const toggleEditMode = ({ state, commit, getters, dispatch }, force = false) => {
+ const changedFiles = getters.changedFiles;
+
+ if (changedFiles.length && !force) {
+ commit(types.TOGGLE_DISCARD_POPUP, true);
+ } else {
+ commit(types.TOGGLE_EDIT_MODE);
+ commit(types.TOGGLE_DISCARD_POPUP, false);
+ dispatch('toggleBlobView');
+
+ if (!state.editMode) {
+ dispatch('discardAllChanges');
+ }
+ }
+};
+
+export const toggleBlobView = ({ commit, state }) => {
+ if (state.editMode) {
+ commit(types.SET_EDIT_MODE);
+ } else {
+ commit(types.SET_PREVIEW_MODE);
+ }
+};
+
+export const checkCommitStatus = ({ state }) => service.getBranchData(
+ state.project.id,
+ state.currentBranch,
+)
+ .then((data) => {
+ const { id } = data.commit;
+
+ if (state.currentRef !== id) {
+ return true;
+ }
+
+ return false;
+ })
+ .catch(() => flash('Error checking branch data. Please try again.'));
+
+export const commitChanges = ({ commit, state, dispatch }, { payload, newMr }) =>
+ service.commit(state.project.id, payload)
+ .then((data) => {
+ const { branch } = payload;
+ if (!data.short_id) {
+ flash(data.message);
+ return;
+ }
+
+ flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
+
+ if (newMr) {
+ redirectToUrl(`${state.endpoints.newMergeRequestUrl}${branch}`);
+ } else {
+ commit(types.SET_COMMIT_REF, data.id);
+ dispatch('discardAllChanges');
+ dispatch('closeAllFiles');
+ dispatch('toggleEditMode');
+
+ window.scrollTo(0, 0);
+ }
+ })
+ .catch(() => flash('Error committing changes. Please try again.'));
+
+export const createTempEntry = ({ state, dispatch }, { name, type, content = '', base64 = false }) => {
+ if (type === 'tree') {
+ dispatch('createTempTree', name);
+ } else if (type === 'blob') {
+ dispatch('createTempFile', {
+ tree: state,
+ name,
+ base64,
+ content,
+ });
+ }
+};
+
+export const popHistoryState = ({ state, dispatch, getters }) => {
+ const treeList = getters.treeList;
+ const tree = treeList.find(file => file.url === state.previousUrl);
+
+ if (!tree) return;
+
+ if (tree.type === 'tree') {
+ dispatch('toggleTreeOpen', { endpoint: tree.url, tree });
+ }
+};
+
+export const scrollToTab = () => {
+ Vue.nextTick(() => {
+ const tabs = document.getElementById('tabs');
+
+ if (tabs) {
+ const tabEl = tabs.querySelector('.active .repo-tab');
+
+ tabEl.focus();
+ }
+ });
+};
+
+export * from './actions/tree';
+export * from './actions/file';
+export * from './actions/branch';
diff --git a/app/assets/javascripts/repo/stores/actions/branch.js b/app/assets/javascripts/repo/stores/actions/branch.js
new file mode 100644
index 00000000000..b81a70dfd1e
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/actions/branch.js
@@ -0,0 +1,20 @@
+import service from '../../services';
+import * as types from '../mutation_types';
+import { pushState } from '../utils';
+
+// eslint-disable-next-line import/prefer-default-export
+export const createNewBranch = ({ rootState, commit }, branch) => service.createBranch(
+ rootState.project.id,
+ {
+ branch,
+ ref: rootState.currentBranch,
+ },
+).then(res => res.json())
+.then((data) => {
+ const branchName = data.name;
+ const url = location.href.replace(rootState.currentBranch, branchName);
+
+ pushState(url);
+
+ commit(types.SET_CURRENT_BRANCH, branchName);
+});
diff --git a/app/assets/javascripts/repo/stores/actions/file.js b/app/assets/javascripts/repo/stores/actions/file.js
new file mode 100644
index 00000000000..afbe0b78a82
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/actions/file.js
@@ -0,0 +1,108 @@
+import { normalizeHeaders } from '../../../lib/utils/common_utils';
+import flash from '../../../flash';
+import service from '../../services';
+import * as types from '../mutation_types';
+import {
+ findEntry,
+ pushState,
+ setPageTitle,
+ createTemp,
+ findIndexOfFile,
+} from '../utils';
+
+export const closeFile = ({ commit, state, dispatch }, { file, force = false }) => {
+ if ((file.changed || file.tempFile) && !force) return;
+
+ const indexOfClosedFile = findIndexOfFile(state.openFiles, file);
+ const fileWasActive = file.active;
+
+ commit(types.TOGGLE_FILE_OPEN, file);
+ commit(types.SET_FILE_ACTIVE, { file, active: false });
+
+ if (state.openFiles.length > 0 && fileWasActive) {
+ const nextIndexToOpen = indexOfClosedFile === 0 ? 0 : indexOfClosedFile - 1;
+ const nextFileToOpen = state.openFiles[nextIndexToOpen];
+
+ dispatch('setFileActive', nextFileToOpen);
+ } else if (!state.openFiles.length) {
+ pushState(file.parentTreeUrl);
+ }
+};
+
+export const setFileActive = ({ commit, state, getters, dispatch }, file) => {
+ const currentActiveFile = getters.activeFile;
+
+ if (file.active) return;
+
+ if (currentActiveFile) {
+ commit(types.SET_FILE_ACTIVE, { file: currentActiveFile, active: false });
+ }
+
+ commit(types.SET_FILE_ACTIVE, { file, active: true });
+ dispatch('scrollToTab');
+
+ // reset hash for line highlighting
+ location.hash = '';
+};
+
+export const getFileData = ({ state, commit, dispatch }, file) => {
+ commit(types.TOGGLE_LOADING, file);
+
+ service.getFileData(file.url)
+ .then((res) => {
+ const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
+
+ setPageTitle(pageTitle);
+
+ return res.json();
+ })
+ .then((data) => {
+ commit(types.SET_FILE_DATA, { data, file });
+ commit(types.TOGGLE_FILE_OPEN, file);
+ dispatch('setFileActive', file);
+ commit(types.TOGGLE_LOADING, file);
+
+ pushState(file.url);
+ })
+ .catch(() => {
+ commit(types.TOGGLE_LOADING, file);
+ flash('Error loading file data. Please try again.');
+ });
+};
+
+export const getRawFileData = ({ commit, dispatch }, file) => service.getRawFileData(file)
+ .then((raw) => {
+ commit(types.SET_FILE_RAW_DATA, { file, raw });
+ })
+ .catch(() => flash('Error loading file content. Please try again.'));
+
+export const changeFileContent = ({ commit }, { file, content }) => {
+ commit(types.UPDATE_FILE_CONTENT, { file, content });
+};
+
+export const createTempFile = ({ state, commit, dispatch }, { tree, name, content = '', base64 = '' }) => {
+ const file = createTemp({
+ name: name.replace(`${state.path}/`, ''),
+ path: tree.path,
+ type: 'blob',
+ level: tree.level !== undefined ? tree.level + 1 : 0,
+ changed: true,
+ content,
+ base64,
+ });
+
+ if (findEntry(tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`);
+
+ commit(types.CREATE_TMP_FILE, {
+ parent: tree,
+ file,
+ });
+ commit(types.TOGGLE_FILE_OPEN, file);
+ dispatch('setFileActive', file);
+
+ if (!state.editMode && !file.base64) {
+ dispatch('toggleEditMode', true);
+ }
+
+ return Promise.resolve(file);
+};
diff --git a/app/assets/javascripts/repo/stores/actions/tree.js b/app/assets/javascripts/repo/stores/actions/tree.js
new file mode 100644
index 00000000000..129743c66c2
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/actions/tree.js
@@ -0,0 +1,110 @@
+import { normalizeHeaders } from '../../../lib/utils/common_utils';
+import flash from '../../../flash';
+import service from '../../services';
+import * as types from '../mutation_types';
+import {
+ pushState,
+ setPageTitle,
+ findEntry,
+ createTemp,
+} from '../utils';
+
+export const getTreeData = (
+ { commit, state },
+ { endpoint = state.endpoints.rootEndpoint, tree = state } = {},
+) => {
+ commit(types.TOGGLE_LOADING, tree);
+
+ service.getTreeData(endpoint)
+ .then((res) => {
+ const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
+
+ setPageTitle(pageTitle);
+
+ return res.json();
+ })
+ .then((data) => {
+ if (!state.isInitialRoot) {
+ commit(types.SET_ROOT, data.path === '/');
+ }
+
+ commit(types.SET_DIRECTORY_DATA, { data, tree });
+ commit(types.SET_PARENT_TREE_URL, data.parent_tree_url);
+ commit(types.TOGGLE_LOADING, tree);
+
+ pushState(endpoint);
+ })
+ .catch(() => {
+ flash('Error loading tree data. Please try again.');
+ commit(types.TOGGLE_LOADING, tree);
+ });
+};
+
+export const toggleTreeOpen = ({ commit, dispatch }, { endpoint, tree }) => {
+ if (tree.opened) {
+ // send empty data to clear the tree
+ const data = { trees: [], blobs: [], submodules: [] };
+
+ pushState(tree.parentTreeUrl);
+
+ commit(types.SET_PREVIOUS_URL, tree.parentTreeUrl);
+ commit(types.SET_DIRECTORY_DATA, { data, tree });
+ } else {
+ commit(types.SET_PREVIOUS_URL, endpoint);
+ dispatch('getTreeData', { endpoint, tree });
+ }
+
+ commit(types.TOGGLE_TREE_OPEN, tree);
+};
+
+export const clickedTreeRow = ({ commit, dispatch }, row) => {
+ if (row.type === 'tree') {
+ dispatch('toggleTreeOpen', {
+ endpoint: row.url,
+ tree: row,
+ });
+ } else if (row.type === 'submodule') {
+ commit(types.TOGGLE_LOADING, row);
+
+ gl.utils.visitUrl(row.url);
+ } else if (row.type === 'blob' && row.opened) {
+ dispatch('setFileActive', row);
+ } else {
+ dispatch('getFileData', row);
+ }
+};
+
+export const createTempTree = ({ state, commit, dispatch }, name) => {
+ let tree = state;
+ const dirNames = name.replace(new RegExp(`^${state.path}/`), '').split('/');
+
+ dirNames.forEach((dirName) => {
+ const foundEntry = findEntry(tree, 'tree', dirName);
+
+ if (!foundEntry) {
+ const tmpEntry = createTemp({
+ name: dirName,
+ path: tree.path,
+ type: 'tree',
+ level: tree.level !== undefined ? tree.level + 1 : 0,
+ });
+
+ commit(types.CREATE_TMP_TREE, {
+ parent: tree,
+ tmpEntry,
+ });
+ commit(types.TOGGLE_TREE_OPEN, tmpEntry);
+
+ tree = tmpEntry;
+ } else {
+ tree = foundEntry;
+ }
+ });
+
+ if (tree.tempFile) {
+ dispatch('createTempFile', {
+ tree,
+ name: '.gitkeep',
+ });
+ }
+};
diff --git a/app/assets/javascripts/repo/stores/getters.js b/app/assets/javascripts/repo/stores/getters.js
new file mode 100644
index 00000000000..1ed05ac6e35
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/getters.js
@@ -0,0 +1,36 @@
+import _ from 'underscore';
+
+/*
+ Takes the multi-dimensional tree and returns a flattened array.
+ This allows for the table to recursively render the table rows but keeps the data
+ structure nested to make it easier to add new files/directories.
+*/
+export const treeList = (state) => {
+ const mapTree = arr => (!arr.tree.length ? [] : _.map(arr.tree, a => [a, mapTree(a)]));
+
+ return _.chain(state.tree)
+ .map(arr => [arr, mapTree(arr)])
+ .flatten()
+ .value();
+};
+
+export const changedFiles = state => state.openFiles.filter(file => file.changed);
+
+export const activeFile = state => state.openFiles.find(file => file.active);
+
+export const activeFileExtension = (state) => {
+ const file = activeFile(state);
+ return file ? `.${file.path.split('.').pop()}` : '';
+};
+
+export const isCollapsed = state => !!state.openFiles.length;
+
+export const canEditFile = (state) => {
+ const currentActiveFile = activeFile(state);
+ const openedFiles = state.openFiles;
+
+ return state.canCommit &&
+ state.onTopOfBranch &&
+ openedFiles.length &&
+ (currentActiveFile && !currentActiveFile.renderError && !currentActiveFile.binary);
+};
diff --git a/app/assets/javascripts/repo/stores/index.js b/app/assets/javascripts/repo/stores/index.js
new file mode 100644
index 00000000000..6ac9bfd8189
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/index.js
@@ -0,0 +1,15 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import state from './state';
+import * as actions from './actions';
+import * as getters from './getters';
+import mutations from './mutations';
+
+Vue.use(Vuex);
+
+export default new Vuex.Store({
+ state: state(),
+ actions,
+ mutations,
+ getters,
+});
diff --git a/app/assets/javascripts/repo/stores/mutation_types.js b/app/assets/javascripts/repo/stores/mutation_types.js
new file mode 100644
index 00000000000..4722a7dd0df
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/mutation_types.js
@@ -0,0 +1,28 @@
+export const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
+export const TOGGLE_LOADING = 'TOGGLE_LOADING';
+export const SET_COMMIT_REF = 'SET_COMMIT_REF';
+export const SET_PARENT_TREE_URL = 'SET_PARENT_TREE_URL';
+export const SET_ROOT = 'SET_ROOT';
+export const SET_PREVIOUS_URL = 'SET_PREVIOUS_URL';
+
+// Tree mutation types
+export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA';
+export const TOGGLE_TREE_OPEN = 'TOGGLE_TREE_OPEN';
+export const CREATE_TMP_TREE = 'CREATE_TMP_TREE';
+
+// File mutation types
+export const SET_FILE_DATA = 'SET_FILE_DATA';
+export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN';
+export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE';
+export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA';
+export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
+export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES';
+export const CREATE_TMP_FILE = 'CREATE_TMP_FILE';
+
+// Viewer mutation types
+export const SET_PREVIEW_MODE = 'SET_PREVIEW_MODE';
+export const SET_EDIT_MODE = 'SET_EDIT_MODE';
+export const TOGGLE_EDIT_MODE = 'TOGGLE_EDIT_MODE';
+export const TOGGLE_DISCARD_POPUP = 'TOGGLE_DISCARD_POPUP';
+
+export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
diff --git a/app/assets/javascripts/repo/stores/mutations.js b/app/assets/javascripts/repo/stores/mutations.js
new file mode 100644
index 00000000000..2f9b038322b
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/mutations.js
@@ -0,0 +1,54 @@
+import * as types from './mutation_types';
+import fileMutations from './mutations/file';
+import treeMutations from './mutations/tree';
+import branchMutations from './mutations/branch';
+
+export default {
+ [types.SET_INITIAL_DATA](state, data) {
+ Object.assign(state, data);
+ },
+ [types.SET_PREVIEW_MODE](state) {
+ Object.assign(state, {
+ currentBlobView: 'repo-preview',
+ });
+ },
+ [types.SET_EDIT_MODE](state) {
+ Object.assign(state, {
+ currentBlobView: 'repo-editor',
+ });
+ },
+ [types.TOGGLE_LOADING](state, entry) {
+ Object.assign(entry, {
+ loading: !entry.loading,
+ });
+ },
+ [types.TOGGLE_EDIT_MODE](state) {
+ Object.assign(state, {
+ editMode: !state.editMode,
+ });
+ },
+ [types.TOGGLE_DISCARD_POPUP](state, discardPopupOpen) {
+ Object.assign(state, {
+ discardPopupOpen,
+ });
+ },
+ [types.SET_COMMIT_REF](state, ref) {
+ Object.assign(state, {
+ currentRef: ref,
+ });
+ },
+ [types.SET_ROOT](state, isRoot) {
+ Object.assign(state, {
+ isRoot,
+ isInitialRoot: isRoot,
+ });
+ },
+ [types.SET_PREVIOUS_URL](state, previousUrl) {
+ Object.assign(state, {
+ previousUrl,
+ });
+ },
+ ...fileMutations,
+ ...treeMutations,
+ ...branchMutations,
+};
diff --git a/app/assets/javascripts/repo/stores/mutations/branch.js b/app/assets/javascripts/repo/stores/mutations/branch.js
new file mode 100644
index 00000000000..d8229e8a620
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/mutations/branch.js
@@ -0,0 +1,9 @@
+import * as types from '../mutation_types';
+
+export default {
+ [types.SET_CURRENT_BRANCH](state, currentBranch) {
+ Object.assign(state, {
+ currentBranch,
+ });
+ },
+};
diff --git a/app/assets/javascripts/repo/stores/mutations/file.js b/app/assets/javascripts/repo/stores/mutations/file.js
new file mode 100644
index 00000000000..f9ba80b9dc2
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/mutations/file.js
@@ -0,0 +1,54 @@
+import * as types from '../mutation_types';
+import { findIndexOfFile } from '../utils';
+
+export default {
+ [types.SET_FILE_ACTIVE](state, { file, active }) {
+ Object.assign(file, {
+ active,
+ });
+ },
+ [types.TOGGLE_FILE_OPEN](state, file) {
+ Object.assign(file, {
+ opened: !file.opened,
+ });
+
+ if (file.opened) {
+ state.openFiles.push(file);
+ } else {
+ state.openFiles.splice(findIndexOfFile(state.openFiles, file), 1);
+ }
+ },
+ [types.SET_FILE_DATA](state, { data, file }) {
+ Object.assign(file, {
+ blamePath: data.blame_path,
+ commitsPath: data.commits_path,
+ permalink: data.permalink,
+ rawPath: data.raw_path,
+ binary: data.binary,
+ html: data.html,
+ renderError: data.render_error,
+ });
+ },
+ [types.SET_FILE_RAW_DATA](state, { file, raw }) {
+ Object.assign(file, {
+ raw,
+ });
+ },
+ [types.UPDATE_FILE_CONTENT](state, { file, content }) {
+ const changed = content !== file.raw;
+
+ Object.assign(file, {
+ content,
+ changed,
+ });
+ },
+ [types.DISCARD_FILE_CHANGES](state, file) {
+ Object.assign(file, {
+ content: '',
+ changed: false,
+ });
+ },
+ [types.CREATE_TMP_FILE](state, { file, parent }) {
+ parent.tree.push(file);
+ },
+};
diff --git a/app/assets/javascripts/repo/stores/mutations/tree.js b/app/assets/javascripts/repo/stores/mutations/tree.js
new file mode 100644
index 00000000000..52be2673107
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/mutations/tree.js
@@ -0,0 +1,45 @@
+import * as types from '../mutation_types';
+import * as utils from '../utils';
+
+export default {
+ [types.TOGGLE_TREE_OPEN](state, tree) {
+ Object.assign(tree, {
+ opened: !tree.opened,
+ });
+ },
+ [types.SET_DIRECTORY_DATA](state, { data, tree }) {
+ const level = tree.level !== undefined ? tree.level + 1 : 0;
+ const parentTreeUrl = data.parent_tree_url ? `${data.parent_tree_url}${data.path}` : state.endpoints.rootUrl;
+
+ Object.assign(tree, {
+ tree: [
+ ...data.trees.map(t => utils.decorateData({
+ ...t,
+ type: 'tree',
+ parentTreeUrl,
+ level,
+ }, state.project.url)),
+ ...data.submodules.map(m => utils.decorateData({
+ ...m,
+ type: 'submodule',
+ parentTreeUrl,
+ level,
+ }, state.project.url)),
+ ...data.blobs.map(b => utils.decorateData({
+ ...b,
+ type: 'blob',
+ parentTreeUrl,
+ level,
+ }, state.project.url)),
+ ],
+ });
+ },
+ [types.SET_PARENT_TREE_URL](state, url) {
+ Object.assign(state, {
+ parentTreeUrl: url,
+ });
+ },
+ [types.CREATE_TMP_TREE](state, { parent, tmpEntry }) {
+ parent.tree.push(tmpEntry);
+ },
+};
diff --git a/app/assets/javascripts/repo/stores/repo_store.js b/app/assets/javascripts/repo/stores/repo_store.js
deleted file mode 100644
index 38df1e3e0d2..00000000000
--- a/app/assets/javascripts/repo/stores/repo_store.js
+++ /dev/null
@@ -1,189 +0,0 @@
-import Helper from '../helpers/repo_helper';
-import Service from '../services/repo_service';
-
-const RepoStore = {
- monacoLoading: false,
- service: '',
- canCommit: false,
- onTopOfBranch: false,
- editMode: false,
- isRoot: null,
- isInitialRoot: null,
- prevURL: '',
- projectId: '',
- projectName: '',
- projectUrl: '',
- branchUrl: '',
- blobRaw: '',
- currentBlobView: 'repo-preview',
- openedFiles: [],
- submitCommitsLoading: false,
- dialog: {
- open: false,
- title: '',
- status: false,
- },
- showNewBranchDialog: false,
- activeFile: Helper.getDefaultActiveFile(),
- activeFileIndex: 0,
- activeLine: -1,
- activeFileLabel: 'Raw',
- files: [],
- isCommitable: false,
- binary: false,
- currentBranch: '',
- startNewMR: false,
- currentHash: '',
- currentShortHash: '',
- customBranchURL: '',
- newMrTemplateUrl: '',
- branchChanged: false,
- commitMessage: '',
- path: '',
- loading: {
- tree: false,
- blob: false,
- },
-
- setBranchHash() {
- return Service.getBranch()
- .then((data) => {
- if (RepoStore.currentHash !== '' && data.commit.id !== RepoStore.currentHash) {
- RepoStore.branchChanged = true;
- }
- RepoStore.currentHash = data.commit.id;
- RepoStore.currentShortHash = data.commit.short_id;
- });
- },
-
- // mutations
- checkIsCommitable() {
- RepoStore.isCommitable = RepoStore.onTopOfBranch && RepoStore.canCommit;
- },
-
- toggleRawPreview() {
- RepoStore.activeFile.raw = !RepoStore.activeFile.raw;
- RepoStore.activeFileLabel = RepoStore.activeFile.raw ? 'Display rendered file' : 'Display source';
- },
-
- setActiveFiles(file) {
- if (RepoStore.isActiveFile(file)) return;
- RepoStore.openedFiles = RepoStore.openedFiles
- .map((openedFile, i) => RepoStore.setFileActivity(file, openedFile, i));
-
- RepoStore.setActiveToRaw();
-
- if (file.binary) {
- RepoStore.blobRaw = file.base64;
- } else if (file.newContent || file.plain) {
- RepoStore.blobRaw = file.newContent || file.plain;
- } else {
- Service.getRaw(file)
- .then((rawResponse) => {
- RepoStore.blobRaw = rawResponse.data;
- Helper.findOpenedFileFromActive().plain = rawResponse.data;
- }).catch(Helper.loadingError);
- }
-
- if (!file.loading && !file.tempFile) {
- Helper.updateHistoryEntry(file.url, file.pageTitle || file.name);
- }
- RepoStore.binary = file.binary;
- RepoStore.setActiveLine(-1);
- },
-
- setFileActivity(file, openedFile, i) {
- const activeFile = openedFile;
- activeFile.active = file.id === activeFile.id;
-
- if (activeFile.active) RepoStore.setActiveFile(activeFile, i);
-
- return activeFile;
- },
-
- setActiveFile(activeFile, i) {
- RepoStore.activeFile = Object.assign({}, Helper.getDefaultActiveFile(), activeFile);
- RepoStore.activeFileIndex = i;
- },
-
- setActiveLine(activeLine) {
- if (!isNaN(activeLine)) RepoStore.activeLine = activeLine;
- },
-
- setActiveToRaw() {
- RepoStore.activeFile.raw = false;
- // can't get vue to listen to raw for some reason so RepoStore for now.
- RepoStore.activeFileLabel = 'Display source';
- },
-
- removeFromOpenedFiles(file) {
- if (file.type === 'tree') return;
- let foundIndex;
- RepoStore.openedFiles = RepoStore.openedFiles.filter((openedFile, i) => {
- if (openedFile.path === file.path) foundIndex = i;
- return openedFile.path !== file.path;
- });
-
- // remove the file from the sidebar if it is a tempFile
- if (file.tempFile) {
- RepoStore.files = RepoStore.files.filter(f => !(f.tempFile && f.path === file.path));
- }
-
- // now activate the right tab based on what you closed.
- if (RepoStore.openedFiles.length === 0) {
- RepoStore.activeFile = {};
- return;
- }
-
- if (RepoStore.openedFiles.length === 1 || foundIndex === 0) {
- RepoStore.setActiveFiles(RepoStore.openedFiles[0]);
- return;
- }
-
- if (foundIndex && foundIndex > 0) {
- RepoStore.setActiveFiles(RepoStore.openedFiles[foundIndex - 1]);
- }
- },
-
- addToOpenedFiles(file) {
- const openFile = file;
-
- const openedFilesAlreadyExists = RepoStore.openedFiles
- .some(openedFile => openedFile.path === openFile.path);
-
- if (openedFilesAlreadyExists) return;
-
- openFile.changed = false;
- openFile.active = true;
- RepoStore.openedFiles.push(openFile);
- },
-
- setActiveFileContents(contents) {
- if (!RepoStore.editMode) return;
- const currentFile = RepoStore.openedFiles[RepoStore.activeFileIndex];
- RepoStore.activeFile.newContent = contents;
- RepoStore.activeFile.changed = RepoStore.activeFile.plain !== RepoStore.activeFile.newContent;
- currentFile.changed = RepoStore.activeFile.changed;
- currentFile.newContent = contents;
- },
-
- toggleBlobView() {
- RepoStore.currentBlobView = RepoStore.isPreviewView() ? 'repo-editor' : 'repo-preview';
- },
-
- setViewToPreview() {
- RepoStore.currentBlobView = 'repo-preview';
- },
-
- // getters
-
- isActiveFile(file) {
- return file && file.id === RepoStore.activeFile.id;
- },
-
- isPreviewView() {
- return RepoStore.currentBlobView === 'repo-preview';
- },
-};
-
-export default RepoStore;
diff --git a/app/assets/javascripts/repo/stores/state.js b/app/assets/javascripts/repo/stores/state.js
new file mode 100644
index 00000000000..aab74754f02
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/state.js
@@ -0,0 +1,23 @@
+export default () => ({
+ canCommit: false,
+ currentBranch: '',
+ currentBlobView: 'repo-preview',
+ currentRef: '',
+ discardPopupOpen: false,
+ editMode: false,
+ endpoints: {},
+ isRoot: false,
+ isInitialRoot: false,
+ loading: false,
+ onTopOfBranch: false,
+ openFiles: [],
+ path: '',
+ project: {
+ id: 0,
+ name: '',
+ url: '',
+ },
+ parentTreeUrl: '',
+ previousUrl: '',
+ tree: [],
+});
diff --git a/app/assets/javascripts/repo/stores/utils.js b/app/assets/javascripts/repo/stores/utils.js
new file mode 100644
index 00000000000..797c2b1e5b9
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/utils.js
@@ -0,0 +1,108 @@
+export const dataStructure = () => ({
+ id: '',
+ type: '',
+ name: '',
+ url: '',
+ path: '',
+ level: 0,
+ tempFile: false,
+ icon: '',
+ tree: [],
+ loading: false,
+ opened: false,
+ active: false,
+ changed: false,
+ lastCommit: {},
+ tree_url: '',
+ blamePath: '',
+ commitsPath: '',
+ permalink: '',
+ rawPath: '',
+ binary: false,
+ html: '',
+ raw: '',
+ content: '',
+ parentTreeUrl: '',
+ renderError: false,
+ base64: false,
+});
+
+export const decorateData = (entity, projectUrl = '') => {
+ const {
+ id,
+ type,
+ url,
+ name,
+ icon,
+ last_commit,
+ tree_url,
+ path,
+ renderError,
+ content = '',
+ tempFile = false,
+ active = false,
+ opened = false,
+ changed = false,
+ parentTreeUrl = '',
+ level = 0,
+ base64 = false,
+ } = entity;
+
+ return {
+ ...dataStructure(),
+ id,
+ type,
+ name,
+ url,
+ tree_url,
+ path,
+ level,
+ tempFile,
+ icon: `fa-${icon}`,
+ opened,
+ active,
+ parentTreeUrl,
+ changed,
+ renderError,
+ content,
+ base64,
+ // eslint-disable-next-line camelcase
+ lastCommit: last_commit ? {
+ url: `${projectUrl}/commit/${last_commit.id}`,
+ message: last_commit.message,
+ updatedAt: last_commit.committed_date,
+ } : {},
+ };
+};
+
+export const findEntry = (state, type, name) => state.tree.find(
+ f => f.type === type && f.name === name,
+);
+export const findIndexOfFile = (state, file) => state.findIndex(f => f.path === file.path);
+
+export const setPageTitle = (title) => {
+ document.title = title;
+};
+
+export const pushState = (url) => {
+ history.pushState({ url }, '', url);
+};
+
+export const createTemp = ({ name, path, type, level, changed, content, base64 }) => {
+ const treePath = path ? `${path}/${name}` : name;
+
+ return decorateData({
+ id: new Date().getTime().toString(),
+ name,
+ type,
+ tempFile: true,
+ path: treePath,
+ icon: type === 'tree' ? 'folder' : 'file-text-o',
+ changed,
+ content,
+ parentTreeUrl: '',
+ level,
+ base64,
+ renderError: base64,
+ });
+};
diff --git a/app/assets/javascripts/settings_panels.js b/app/assets/javascripts/settings_panels.js
index 8635ccece6e..d34a21b37e1 100644
--- a/app/assets/javascripts/settings_panels.js
+++ b/app/assets/javascripts/settings_panels.js
@@ -1,34 +1,26 @@
-function expandSectionParent($section, $content) {
- $section.addClass('expanded');
- $content.off('animationend.expandSectionParent');
-}
-
function expandSection($section) {
$section.find('.js-settings-toggle').text('Collapse');
-
- const $content = $section.find('.settings-content');
- $content.addClass('expanded').off('scroll.expandSection').scrollTop(0);
-
- if ($content.hasClass('no-animate')) {
- expandSectionParent($section, $content);
- } else {
- $content.on('animationend.expandSectionParent', () => expandSectionParent($section, $content));
+ $section.find('.settings-content').off('scroll.expandSection').scrollTop(0);
+ $section.addClass('expanded');
+ if (!$section.hasClass('no-animate')) {
+ $section.addClass('animating')
+ .one('animationend.animateSection', () => $section.removeClass('animating'));
}
}
function closeSection($section) {
$section.find('.js-settings-toggle').text('Expand');
-
- const $content = $section.find('.settings-content');
- $content.removeClass('expanded').on('scroll.expandSection', () => expandSection($section));
-
+ $section.find('.settings-content').on('scroll.expandSection', () => expandSection($section));
$section.removeClass('expanded');
+ if (!$section.hasClass('no-animate')) {
+ $section.addClass('animating')
+ .one('animationend.animateSection', () => $section.removeClass('animating'));
+ }
}
function toggleSection($section) {
- const $content = $section.find('.settings-content');
- $content.removeClass('no-animate');
- if ($content.hasClass('expanded')) {
+ $section.removeClass('no-animate');
+ if ($section.hasClass('expanded')) {
closeSection($section);
} else {
expandSection($section);
@@ -39,10 +31,19 @@ export default function initSettingsPanels() {
$('.settings').each((i, elm) => {
const $section = $(elm);
$section.on('click.toggleSection', '.js-settings-toggle', () => toggleSection($section));
- $section.find('.settings-content:not(.expanded)').on('scroll.expandSection', () => expandSection($section));
+
+ if (!$section.hasClass('expanded')) {
+ $section.find('.settings-content').on('scroll.expandSection', () => {
+ $section.removeClass('no-animate');
+ expandSection($section);
+ });
+ }
});
if (location.hash) {
- expandSection($(location.hash));
+ const $target = $(location.hash);
+ if ($target.length && $target.hasClass('.settings')) {
+ expandSection($target);
+ }
}
}
diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue
new file mode 100644
index 00000000000..b8510a6ce3a
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/participants/participants.vue
@@ -0,0 +1,125 @@
+<script>
+import { __, n__, sprintf } from '../../../locale';
+import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
+import userAvatarImage from '../../../vue_shared/components/user_avatar/user_avatar_image.vue';
+
+export default {
+ props: {
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ participants: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ numberOfLessParticipants: {
+ type: Number,
+ required: false,
+ default: 7,
+ },
+ },
+ data() {
+ return {
+ isShowingMoreParticipants: false,
+ };
+ },
+ components: {
+ loadingIcon,
+ userAvatarImage,
+ },
+ computed: {
+ lessParticipants() {
+ return this.participants.slice(0, this.numberOfLessParticipants);
+ },
+ visibleParticipants() {
+ return this.isShowingMoreParticipants ? this.participants : this.lessParticipants;
+ },
+ hasMoreParticipants() {
+ return this.participants.length > this.numberOfLessParticipants;
+ },
+ toggleLabel() {
+ let label = '';
+ if (this.isShowingMoreParticipants) {
+ label = __('- show less');
+ } else {
+ label = sprintf(__('+ %{moreCount} more'), {
+ moreCount: this.participants.length - this.numberOfLessParticipants,
+ });
+ }
+
+ return label;
+ },
+ participantLabel() {
+ return sprintf(
+ n__('%{count} participant', '%{count} participants', this.participants.length),
+ { count: this.loading ? '' : this.participantCount },
+ );
+ },
+ participantCount() {
+ return this.participants.length;
+ },
+ },
+ methods: {
+ toggleMoreParticipants() {
+ this.isShowingMoreParticipants = !this.isShowingMoreParticipants;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="sidebar-collapsed-icon">
+ <i
+ class="fa fa-users"
+ aria-hidden="true">
+ </i>
+ <loading-icon
+ v-if="loading"
+ class="js-participants-collapsed-loading-icon" />
+ <span
+ v-else
+ class="js-participants-collapsed-count">
+ {{ participantCount }}
+ </span>
+ </div>
+ <div class="title hide-collapsed">
+ <loading-icon
+ v-if="loading"
+ :inline="true"
+ class="js-participants-expanded-loading-icon" />
+ {{ participantLabel }}
+ </div>
+ <div class="participants-list hide-collapsed">
+ <div
+ v-for="participant in visibleParticipants"
+ :key="participant.id"
+ class="participants-author js-participants-author">
+ <a
+ class="author_link"
+ :href="participant.web_url">
+ <user-avatar-image
+ :lazy="true"
+ :img-src="participant.avatar_url"
+ css-classes="avatar-inline"
+ :size="24"
+ :tooltip-text="participant.name"
+ tooltip-placement="bottom" />
+ </a>
+ </div>
+ </div>
+ <div
+ v-if="hasMoreParticipants"
+ class="participants-more hide-collapsed">
+ <button
+ type="button"
+ class="btn-transparent btn-blank js-toggle-participants-button"
+ @click="toggleMoreParticipants">
+ {{ toggleLabel }}
+ </button>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue b/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue
new file mode 100644
index 00000000000..c1296b28db7
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue
@@ -0,0 +1,26 @@
+<script>
+import Store from '../../stores/sidebar_store';
+import Mediator from '../../sidebar_mediator';
+import participants from './participants.vue';
+
+export default {
+ data() {
+ return {
+ mediator: new Mediator(),
+ store: new Store(),
+ };
+ },
+ components: {
+ participants,
+ },
+};
+</script>
+
+<template>
+ <div class="block participants">
+ <participants
+ :loading="store.isFetching.participants"
+ :participants="store.participants"
+ :number-of-less-participants="7" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
new file mode 100644
index 00000000000..4ad3d469f25
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
@@ -0,0 +1,45 @@
+<script>
+import Store from '../../stores/sidebar_store';
+import Mediator from '../../sidebar_mediator';
+import eventHub from '../../event_hub';
+import Flash from '../../../flash';
+import subscriptions from './subscriptions.vue';
+
+export default {
+ data() {
+ return {
+ mediator: new Mediator(),
+ store: new Store(),
+ };
+ },
+
+ components: {
+ subscriptions,
+ },
+
+ methods: {
+ onToggleSubscription() {
+ this.mediator.toggleSubscription()
+ .catch(() => {
+ Flash('Error occurred when toggling the notification subscription');
+ });
+ },
+ },
+
+ created() {
+ eventHub.$on('toggleSubscription', this.onToggleSubscription);
+ },
+
+ beforeDestroy() {
+ eventHub.$off('toggleSubscription', this.onToggleSubscription);
+ },
+};
+</script>
+
+<template>
+ <div class="block subscriptions">
+ <subscriptions
+ :loading="store.isFetching.subscriptions"
+ :subscribed="store.subscribed" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
new file mode 100644
index 00000000000..a3a8213d63a
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
@@ -0,0 +1,60 @@
+<script>
+import { __ } from '../../../locale';
+import eventHub from '../../event_hub';
+import loadingButton from '../../../vue_shared/components/loading_button.vue';
+
+export default {
+ props: {
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ subscribed: {
+ type: Boolean,
+ required: false,
+ },
+ },
+ components: {
+ loadingButton,
+ },
+ computed: {
+ buttonLabel() {
+ let label;
+ if (this.subscribed === false) {
+ label = __('Subscribe');
+ } else if (this.subscribed === true) {
+ label = __('Unsubscribe');
+ }
+
+ return label;
+ },
+ },
+ methods: {
+ toggleSubscription() {
+ eventHub.$emit('toggleSubscription');
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="sidebar-collapsed-icon">
+ <i
+ class="fa fa-rss"
+ aria-hidden="true">
+ </i>
+ </div>
+ <span class="issuable-header-text hide-collapsed pull-left">
+ {{ __('Notifications') }}
+ </span>
+ <loading-button
+ ref="loadingButton"
+ class="btn btn-default pull-right hide-collapsed js-issuable-subscribe-button"
+ :loading="loading"
+ :label="buttonLabel"
+ @click="toggleSubscription"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/services/sidebar_service.js b/app/assets/javascripts/sidebar/services/sidebar_service.js
index 604648407a4..37c97225bfd 100644
--- a/app/assets/javascripts/sidebar/services/sidebar_service.js
+++ b/app/assets/javascripts/sidebar/services/sidebar_service.js
@@ -7,6 +7,7 @@ export default class SidebarService {
constructor(endpointMap) {
if (!SidebarService.singleton) {
this.endpoint = endpointMap.endpoint;
+ this.toggleSubscriptionEndpoint = endpointMap.toggleSubscriptionEndpoint;
this.moveIssueEndpoint = endpointMap.moveIssueEndpoint;
this.projectsAutocompleteEndpoint = endpointMap.projectsAutocompleteEndpoint;
@@ -36,6 +37,10 @@ export default class SidebarService {
});
}
+ toggleSubscription() {
+ return Vue.http.post(this.toggleSubscriptionEndpoint);
+ }
+
moveIssue(moveToProjectId) {
return Vue.http.post(this.moveIssueEndpoint, {
move_to_project_id: moveToProjectId,
diff --git a/app/assets/javascripts/sidebar/sidebar_bundle.js b/app/assets/javascripts/sidebar/sidebar_bundle.js
index 09b9d75c02d..2650bb725d4 100644
--- a/app/assets/javascripts/sidebar/sidebar_bundle.js
+++ b/app/assets/javascripts/sidebar/sidebar_bundle.js
@@ -4,6 +4,8 @@ import SidebarAssignees from './components/assignees/sidebar_assignees';
import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue';
import SidebarMoveIssue from './lib/sidebar_move_issue';
import LockIssueSidebar from './components/lock/lock_issue_sidebar.vue';
+import sidebarParticipants from './components/participants/sidebar_participants.vue';
+import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptions.vue';
import Translate from '../vue_shared/translate';
import Mediator from './sidebar_mediator';
@@ -49,6 +51,36 @@ function mountLockComponent(mediator) {
}).$mount(el);
}
+function mountParticipantsComponent() {
+ const el = document.querySelector('.js-sidebar-participants-entry-point');
+
+ if (!el) return;
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ components: {
+ sidebarParticipants,
+ },
+ render: createElement => createElement('sidebar-participants', {}),
+ });
+}
+
+function mountSubscriptionsComponent() {
+ const el = document.querySelector('.js-sidebar-subscriptions-entry-point');
+
+ if (!el) return;
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ components: {
+ sidebarSubscriptions,
+ },
+ render: createElement => createElement('sidebar-subscriptions', {}),
+ });
+}
+
function domContentLoaded() {
const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
const mediator = new Mediator(sidebarOptions);
@@ -63,6 +95,8 @@ function domContentLoaded() {
mountConfidentialComponent(mediator);
mountLockComponent(mediator);
+ mountParticipantsComponent();
+ mountSubscriptionsComponent();
new SidebarMoveIssue(
mediator,
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index ede3a0de144..2bda5a47791 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -8,6 +8,7 @@ export default class SidebarMediator {
this.store = new Store(options);
this.service = new Service({
endpoint: options.endpoint,
+ toggleSubscriptionEndpoint: options.toggleSubscriptionEndpoint,
moveIssueEndpoint: options.moveIssueEndpoint,
projectsAutocompleteEndpoint: options.projectsAutocompleteEndpoint,
});
@@ -39,10 +40,25 @@ export default class SidebarMediator {
.then((data) => {
this.store.setAssigneeData(data);
this.store.setTimeTrackingData(data);
+ this.store.setParticipantsData(data);
+ this.store.setSubscriptionsData(data);
})
.catch(() => new Flash('Error occurred when fetching sidebar data'));
}
+ toggleSubscription() {
+ this.store.setFetchingState('subscriptions', true);
+ return this.service.toggleSubscription()
+ .then(() => {
+ this.store.setSubscribedState(!this.store.subscribed);
+ this.store.setFetchingState('subscriptions', false);
+ })
+ .catch((err) => {
+ this.store.setFetchingState('subscriptions', false);
+ throw err;
+ });
+ }
+
fetchAutocompleteProjects(searchTerm) {
return this.service.getProjectsAutocomplete(searchTerm)
.then(response => response.json())
diff --git a/app/assets/javascripts/sidebar/stores/sidebar_store.js b/app/assets/javascripts/sidebar/stores/sidebar_store.js
index d5d04103f3f..3150221b685 100644
--- a/app/assets/javascripts/sidebar/stores/sidebar_store.js
+++ b/app/assets/javascripts/sidebar/stores/sidebar_store.js
@@ -12,10 +12,14 @@ export default class SidebarStore {
this.assignees = [];
this.isFetching = {
assignees: true,
+ participants: true,
+ subscriptions: true,
};
this.autocompleteProjects = [];
this.moveToProjectId = 0;
this.isLockDialogOpen = false;
+ this.participants = [];
+ this.subscribed = null;
SidebarStore.singleton = this;
}
@@ -37,6 +41,20 @@ export default class SidebarStore {
this.humanTotalTimeSpent = data.human_total_time_spent;
}
+ setParticipantsData(data) {
+ this.isFetching.participants = false;
+ this.participants = data.participants || [];
+ }
+
+ setSubscriptionsData(data) {
+ this.isFetching.subscriptions = false;
+ this.subscribed = data.subscribed || false;
+ }
+
+ setFetchingState(key, value) {
+ this.isFetching[key] = value;
+ }
+
addAssignee(assignee) {
if (!this.findAssignee(assignee)) {
this.assignees.push(assignee);
@@ -61,6 +79,10 @@ export default class SidebarStore {
this.autocompleteProjects = projects;
}
+ setSubscribedState(subscribed) {
+ this.subscribed = subscribed;
+ }
+
setMoveToProjectId(moveToProjectId) {
this.moveToProjectId = moveToProjectId;
}
diff --git a/app/assets/javascripts/test_utils/index.js b/app/assets/javascripts/test_utils/index.js
index 8875590f0f2..a55a338eea8 100644
--- a/app/assets/javascripts/test_utils/index.js
+++ b/app/assets/javascripts/test_utils/index.js
@@ -1,6 +1,8 @@
import 'core-js/es6/map';
import 'core-js/es6/set';
import simulateDrag from './simulate_drag';
+import simulateInput from './simulate_input';
// Export to global space for rspec to use
window.simulateDrag = simulateDrag;
+window.simulateInput = simulateInput;
diff --git a/app/assets/javascripts/test_utils/simulate_input.js b/app/assets/javascripts/test_utils/simulate_input.js
new file mode 100644
index 00000000000..90c1b7cb57e
--- /dev/null
+++ b/app/assets/javascripts/test_utils/simulate_input.js
@@ -0,0 +1,23 @@
+function triggerEvents(input) {
+ input.dispatchEvent(new Event('keydown'));
+ input.dispatchEvent(new Event('keypress'));
+ input.dispatchEvent(new Event('input'));
+ input.dispatchEvent(new Event('keyup'));
+}
+
+export default function simulateInput(target, text) {
+ const input = document.querySelector(target);
+ if (!input || !input.matches('textarea, input')) {
+ return false;
+ }
+
+ if (text.length > 0) {
+ Array.prototype.forEach.call(text, (char) => {
+ input.value += char;
+ triggerEvents(input);
+ });
+ } else {
+ triggerEvents(input);
+ }
+ return true;
+}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js
index c79b5c720eb..029832bdd27 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js
@@ -1,6 +1,6 @@
import PipelineStage from '../../pipelines/components/stage.vue';
import ciIcon from '../../vue_shared/components/ci_icon.vue';
-import { statusIconEntityMap } from '../../vue_shared/ci_status_icons';
+import icon from '../../vue_shared/components/icon.vue';
export default {
name: 'MRWidgetPipeline',
@@ -10,6 +10,7 @@ export default {
components: {
'pipeline-stage': PipelineStage,
ciIcon,
+ icon,
},
computed: {
hasPipeline() {
@@ -20,9 +21,6 @@ export default {
return hasCI && !ciStatus;
},
- svg() {
- return statusIconEntityMap.icon_status_failed;
- },
stageText() {
return this.mr.pipeline.details.stages.length > 1 ? 'stages' : 'stage';
},
@@ -38,8 +36,10 @@ export default {
<template v-if="hasCIError">
<div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-10">
<span
- v-html="svg"
- aria-hidden="true"></span>
+ aria-hidden="true">
+ <icon
+ name="status_failed"/>
+ </span>
</div>
<div class="media-body">
Could not connect to the CI server. Please check your settings and try again
diff --git a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
index 79c3d335679..99f5c305df5 100644
--- a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
+++ b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
@@ -11,7 +11,7 @@ export default class MRWidgetService {
this.removeWIPResource = Vue.resource(endpoints.removeWIPPath);
this.removeSourceBranchResource = Vue.resource(endpoints.sourceBranchPath);
this.deploymentsResource = Vue.resource(endpoints.ciEnvironmentsStatusPath);
- this.pollResource = Vue.resource(`${endpoints.statusPath}?basic=true`);
+ this.pollResource = Vue.resource(`${endpoints.statusPath}?serializer=basic`);
this.mergeActionsContentResource = Vue.resource(endpoints.mergeActionsContentPath);
}
diff --git a/app/assets/javascripts/vue_shared/ci_action_icons.js b/app/assets/javascripts/vue_shared/ci_action_icons.js
deleted file mode 100644
index b21f0ab49fd..00000000000
--- a/app/assets/javascripts/vue_shared/ci_action_icons.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import cancelSVG from 'icons/_icon_action_cancel.svg';
-import retrySVG from 'icons/_icon_action_retry.svg';
-import playSVG from 'icons/_icon_action_play.svg';
-import stopSVG from 'icons/_icon_action_stop.svg';
-
-/**
- * For the provided action returns the respective SVG
- *
- * @param {String} action
- * @return {SVG|String}
- */
-export default function getActionIcon(action) {
- const icons = {
- icon_action_cancel: cancelSVG,
- icon_action_play: playSVG,
- icon_action_retry: retrySVG,
- icon_action_stop: stopSVG,
- };
-
- return icons[action] || '';
-}
diff --git a/app/assets/javascripts/vue_shared/ci_status_icons.js b/app/assets/javascripts/vue_shared/ci_status_icons.js
deleted file mode 100644
index d9d0cad38e4..00000000000
--- a/app/assets/javascripts/vue_shared/ci_status_icons.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import BORDERLESS_CANCELED_SVG from 'icons/_icon_status_canceled_borderless.svg';
-import BORDERLESS_CREATED_SVG from 'icons/_icon_status_created_borderless.svg';
-import BORDERLESS_FAILED_SVG from 'icons/_icon_status_failed_borderless.svg';
-import BORDERLESS_MANUAL_SVG from 'icons/_icon_status_manual_borderless.svg';
-import BORDERLESS_PENDING_SVG from 'icons/_icon_status_pending_borderless.svg';
-import BORDERLESS_RUNNING_SVG from 'icons/_icon_status_running_borderless.svg';
-import BORDERLESS_SKIPPED_SVG from 'icons/_icon_status_skipped_borderless.svg';
-import BORDERLESS_SUCCESS_SVG from 'icons/_icon_status_success_borderless.svg';
-import BORDERLESS_WARNING_SVG from 'icons/_icon_status_warning_borderless.svg';
-
-import CANCELED_SVG from 'icons/_icon_status_canceled.svg';
-import CREATED_SVG from 'icons/_icon_status_created.svg';
-import FAILED_SVG from 'icons/_icon_status_failed.svg';
-import MANUAL_SVG from 'icons/_icon_status_manual.svg';
-import PENDING_SVG from 'icons/_icon_status_pending.svg';
-import RUNNING_SVG from 'icons/_icon_status_running.svg';
-import SKIPPED_SVG from 'icons/_icon_status_skipped.svg';
-import SUCCESS_SVG from 'icons/_icon_status_success.svg';
-import WARNING_SVG from 'icons/_icon_status_warning.svg';
-
-export const borderlessStatusIconEntityMap = {
- icon_status_canceled: BORDERLESS_CANCELED_SVG,
- icon_status_created: BORDERLESS_CREATED_SVG,
- icon_status_failed: BORDERLESS_FAILED_SVG,
- icon_status_manual: BORDERLESS_MANUAL_SVG,
- icon_status_pending: BORDERLESS_PENDING_SVG,
- icon_status_running: BORDERLESS_RUNNING_SVG,
- icon_status_skipped: BORDERLESS_SKIPPED_SVG,
- icon_status_success: BORDERLESS_SUCCESS_SVG,
- icon_status_warning: BORDERLESS_WARNING_SVG,
-};
-
-export const statusIconEntityMap = {
- icon_status_canceled: CANCELED_SVG,
- icon_status_created: CREATED_SVG,
- icon_status_failed: FAILED_SVG,
- icon_status_manual: MANUAL_SVG,
- icon_status_pending: PENDING_SVG,
- icon_status_running: RUNNING_SVG,
- icon_status_skipped: SKIPPED_SVG,
- icon_status_success: SUCCESS_SVG,
- icon_status_warning: WARNING_SVG,
-};
diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
index 5b6c6e8d0b9..fc795936abf 100644
--- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
@@ -43,7 +43,6 @@
computed: {
cssClass() {
const className = this.status.group;
-
return className ? `ci-status ci-${className}` : 'ci-status';
},
},
diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue
index ec88119e16c..2a018f38366 100644
--- a/app/assets/javascripts/vue_shared/components/ci_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue
@@ -1,5 +1,5 @@
<script>
- import { statusIconEntityMap } from '../ci_status_icons';
+ import icon from '../../vue_shared/components/icon.vue';
/**
* Renders CI icon based on API response shared between all places where it is used.
@@ -30,11 +30,11 @@
},
},
- computed: {
- statusIconSvg() {
- return statusIconEntityMap[this.status.icon];
- },
+ components: {
+ icon,
+ },
+ computed: {
cssClass() {
const status = this.status.group;
return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
@@ -44,7 +44,8 @@
</script>
<template>
<span
- :class="cssClass"
- v-html="statusIconSvg">
+ :class="cssClass">
+ <icon
+ :name="status.icon"/>
</span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue
new file mode 100644
index 00000000000..2e5f9f1088f
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/icon.vue
@@ -0,0 +1,52 @@
+<script>
+
+/* This is a re-usable vue component for rendering a svg sprite
+ icon
+
+ Sample configuration:
+
+ <icon
+ :img-src="userAvatarSrc"
+ :img-alt="tooltipText"
+ :tooltip-text="tooltipText"
+ tooltip-placement="top"
+ />
+
+*/
+ export default {
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+
+ size: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+
+ cssClasses: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+
+ computed: {
+ spriteHref() {
+ return `${gon.sprite_icons}#${this.name}`;
+ },
+ iconSizeClass() {
+ return this.size ? `s${this.size}` : '';
+ },
+ },
+ };
+</script>
+<template>
+ <svg
+ :class="[iconSizeClass, cssClasses]">
+ <use
+ v-bind="{'xlink:href':spriteHref}"/>
+ </svg>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
index 95898d54cf7..dc32e783258 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
@@ -12,12 +12,14 @@
:img-alt="tooltipText"
:img-size="20"
:tooltip-text="tooltipText"
- tooltip-placement="top"
+ :tooltip-placement="top"
+ :username="username"
/>
*/
import userAvatarImage from './user_avatar_image.vue';
+import tooltip from '../../directives/tooltip';
export default {
name: 'UserAvatarLink',
@@ -60,6 +62,22 @@ export default {
required: false,
default: 'top',
},
+ username: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ shouldShowUsername() {
+ return this.username.length > 0;
+ },
+ avatarTooltipText() {
+ return this.shouldShowUsername ? '' : this.tooltipText;
+ },
+ },
+ directives: {
+ tooltip,
},
};
</script>
@@ -73,8 +91,13 @@ export default {
:img-alt="imgAlt"
:css-classes="imgCssClasses"
:size="imgSize"
- :tooltip-text="tooltipText"
+ :tooltip-text="avatarTooltipText"
+ :tooltip-placement="tooltipPlacement"
+ /><span
+ v-if="shouldShowUsername"
+ v-tooltip
+ :title="tooltipText"
:tooltip-placement="tooltipPlacement"
- />
+ >{{username}}</span>
</a>
</template>
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 7b1ef003bb2..c334f39f416 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -56,4 +56,4 @@
@import "framework/icons";
@import "framework/snippets";
@import "framework/memory_graph";
-@import "framework/responsive-tables";
+@import "framework/responsive_tables";
diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss
index 81439c0d2fe..1b944831082 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -23,11 +23,16 @@
@include webkit-prefix(animation-duration, 2s);
}
- &.spin {
+ &.spin-cw {
transform-origin: center;
animation: spin 4s linear infinite;
}
+ &.spin-ccw {
+ transform-origin: center;
+ animation: spin 4s linear infinite reverse;
+ }
+
&.flipOutX,
&.flipOutY,
&.bounceIn,
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index dbd990f84c1..def986180fc 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -40,6 +40,10 @@
&.top-block {
border-top: none;
+
+ .container-fluid {
+ background-color: inherit;
+ }
}
&.middle-block {
@@ -98,10 +102,6 @@
background-color: $white-light;
border-top: none;
}
-
- &.top-block .container-fluid {
- background-color: inherit;
- }
}
.sub-header-block {
@@ -209,7 +209,6 @@
padding: 24px 0 0;
.nav-links {
- justify-content: center;
width: 100%;
float: none;
@@ -217,6 +216,14 @@
float: none;
}
}
+
+ li:first-child {
+ margin-left: auto;
+ }
+
+ li:last-child {
+ margin-right: auto;
+ }
}
.group-info {
diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss
index e0e46dd73af..1bd94c0acba 100644
--- a/app/assets/stylesheets/framework/callout.scss
+++ b/app/assets/stylesheets/framework/callout.scss
@@ -12,15 +12,15 @@
border-left: 3px solid $border-color;
color: $text-color;
background: $gray-light;
-}
-.bs-callout h4 {
- margin-top: 0;
- margin-bottom: 5px;
-}
+ h4 {
+ margin-top: 0;
+ margin-bottom: 5px;
+ }
-.bs-callout p:last-child {
- margin-bottom: 0;
+ p:last-child {
+ margin-bottom: 0;
+ }
}
/* Variations */
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 96f9dda26c4..ea3007f5e08 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -5,32 +5,6 @@
.cgreen { color: $common-green; }
.cdark { color: $common-gray-dark; }
-/** COMMON CLASSES **/
-.prepend-top-0 { margin-top: 0; }
-.prepend-top-5 { margin-top: 5px; }
-.prepend-top-10 { margin-top: 10px; }
-.prepend-top-default { margin-top: $gl-padding !important; }
-.prepend-top-20 { margin-top: 20px; }
-.prepend-left-4 { margin-left: 4px; }
-.prepend-left-5 { margin-left: 5px; }
-.prepend-left-10 { margin-left: 10px; }
-.prepend-left-default { margin-left: $gl-padding; }
-.prepend-left-20 { margin-left: 20px; }
-.append-right-5 { margin-right: 5px; }
-.append-right-8 { margin-right: 8px; }
-.append-right-10 { margin-right: 10px; }
-.append-right-default { margin-right: $gl-padding; }
-.append-right-20 { margin-right: 20px; }
-.append-bottom-0 { margin-bottom: 0; }
-.append-bottom-5 { margin-bottom: 5px; }
-.append-bottom-10 { margin-bottom: 10px; }
-.append-bottom-15 { margin-bottom: 15px; }
-.append-bottom-20 { margin-bottom: 20px; }
-.append-bottom-default { margin-bottom: $gl-padding; }
-.inline { display: inline-block; }
-.center { text-align: center; }
-.vertical-align-middle { vertical-align: middle; }
-
.underlined-link { text-decoration: underline; }
.hint { font-style: italic; color: $hint-color; }
.light { color: $common-gray; }
@@ -79,6 +53,14 @@ hr {
.str-truncated {
@include str-truncated;
+
+ &-60 {
+ @include str-truncated(60%);
+ }
+
+ &-100 {
+ @include str-truncated(100%);
+ }
}
.block-truncated {
@@ -104,10 +86,17 @@ hr {
font-size: 14px;
}
-table a code {
- position: relative;
- top: -2px;
- margin-right: 3px;
+table {
+ a code {
+ position: relative;
+ top: -2px;
+ margin-right: 3px;
+ }
+
+ td.permission-x {
+ background: $table-permission-x-bg !important;
+ text-align: center;
+ }
}
.loading {
@@ -292,13 +281,6 @@ img.emoji {
margin-bottom: 10px;
}
-table {
- td.permission-x {
- background: $table-permission-x-bg !important;
- text-align: center;
- }
-}
-
.btn-sign-in {
text-shadow: none;
@@ -364,10 +346,11 @@ table {
.dropzone .dz-preview .dz-progress {
border-color: $border-color !important;
-}
-.dropzone .dz-preview .dz-progress .dz-upload {
- background: $gl-success !important;
+ .dz-upload {
+ background: $gl-success !important;
+ }
+
}
.dz-message {
@@ -428,16 +411,6 @@ table {
border-radius: $border-radius-default;
}
-.str-truncated {
- &-60 {
- @include str-truncated(60%);
- }
-
- &-100 {
- @include str-truncated(100%);
- }
-}
-
.tooltip {
.tooltip-inner {
word-wrap: break-word;
@@ -448,3 +421,30 @@ table {
pointer-events: none;
opacity: .5;
}
+
+/** COMMON CLASSES **/
+.prepend-top-0 { margin-top: 0; }
+.prepend-top-5 { margin-top: 5px; }
+.prepend-top-10 { margin-top: 10px; }
+.prepend-top-15 { margin-top: 15px; }
+.prepend-top-default { margin-top: $gl-padding !important; }
+.prepend-top-20 { margin-top: 20px; }
+.prepend-left-4 { margin-left: 4px; }
+.prepend-left-5 { margin-left: 5px; }
+.prepend-left-10 { margin-left: 10px; }
+.prepend-left-default { margin-left: $gl-padding; }
+.prepend-left-20 { margin-left: 20px; }
+.append-right-5 { margin-right: 5px; }
+.append-right-8 { margin-right: 8px; }
+.append-right-10 { margin-right: 10px; }
+.append-right-default { margin-right: $gl-padding; }
+.append-right-20 { margin-right: 20px; }
+.append-bottom-0 { margin-bottom: 0; }
+.append-bottom-5 { margin-bottom: 5px; }
+.append-bottom-10 { margin-bottom: 10px; }
+.append-bottom-15 { margin-bottom: 15px; }
+.append-bottom-20 { margin-bottom: 20px; }
+.append-bottom-default { margin-bottom: $gl-padding; }
+.inline { display: inline-block; }
+.center { text-align: center; }
+.vertical-align-middle { vertical-align: middle; }
diff --git a/app/assets/stylesheets/framework/contextual-sidebar.scss b/app/assets/stylesheets/framework/contextual-sidebar.scss
index fa5d3833f3e..320f458630a 100644
--- a/app/assets/stylesheets/framework/contextual-sidebar.scss
+++ b/app/assets/stylesheets/framework/contextual-sidebar.scss
@@ -141,15 +141,15 @@
svg {
fill: $gl-text-color-secondary;
}
- }
- .nav-item-name {
- flex: 1;
- }
+ .nav-item-name {
+ flex: 1;
+ }
- li.active {
- > a {
- font-weight: $gl-font-weight-bold;
+ &.active {
+ > a {
+ font-weight: $gl-font-weight-bold;
+ }
}
}
@@ -484,10 +484,7 @@
height: calc(100vh - #{$header-height});
@media (min-width: $screen-sm-min) {
- height: 475px; // Needed for PhantomJS
- // scss-lint:disable DuplicateProperty
height: calc(100vh - 180px);
- // scss-lint:enable DuplicateProperty
}
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index a9d804e735d..08c603edd23 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -727,11 +727,11 @@
.pika-single.animate-picker.is-bound {
@include set-visible;
-}
-.pika-single.animate-picker.is-bound.is-hidden {
- @include set-invisible;
- overflow: hidden;
+ &.is-hidden {
+ @include set-invisible;
+ overflow: hidden;
+ }
}
@mixin dropdown-item-hover {
@@ -776,12 +776,15 @@
a,
button,
.menu-item {
+ margin-bottom: 0;
border-radius: 0;
box-shadow: none;
padding: 8px 16px;
text-align: left;
white-space: normal;
width: 100%;
+ font-weight: $gl-font-weight-normal;
+ line-height: normal;
&.dropdown-menu-user-link {
white-space: nowrap;
@@ -935,9 +938,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
border-right: 0;
}
}
-}
-.projects-dropdown-container {
.projects-list-frequent-container,
.projects-list-search-container, {
padding: 8px 0;
@@ -948,11 +949,6 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
.projects-list-frequent-container li.section-empty,
.projects-list-search-container li.section-empty {
padding: 0 15px;
- }
-
- .section-header,
- .projects-list-frequent-container li.section-empty,
- .projects-list-search-container li.section-empty {
color: $gl-text-color-secondary;
font-size: $gl-font-size;
}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 5833ef939e9..6382551fcc9 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -165,22 +165,36 @@
&:last-child {
border-right: none;
}
- }
- td.blame-commit {
- padding: 5px 10px;
- min-width: 400px;
- max-width: 400px;
- background: $gray-light;
- border-left: 3px solid;
+ &.blame-commit {
+ padding: 5px 10px;
+ min-width: 400px;
+ max-width: 400px;
+ background: $gray-light;
+ border-left: 3px solid;
+
+ .commit-row-title {
+ display: flex;
+ }
+
+ .item-title {
+ flex: 1;
+ margin-right: 0.5em;
+ }
+ }
+
+ &.line-numbers {
+ float: none;
+ border-left: 1px solid $blame-line-numbers-border;
- .commit-row-title {
- display: flex;
+ i {
+ float: none;
+ margin-right: 0;
+ }
}
- .item-title {
- flex: 1;
- margin-right: 0.5em;
+ &.lines {
+ padding: 0;
}
}
@@ -195,20 +209,6 @@
border-left-color: mix($blame-gray, $blame-cyan, $i / 4.0 * 100%);
}
}
-
- td.line-numbers {
- float: none;
- border-left: 1px solid $blame-line-numbers-border;
-
- i {
- float: none;
- margin-right: 0;
- }
- }
-
- td.lines {
- padding: 0;
- }
}
&.logs {
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 0d80a85d521..a7333925f80 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -268,12 +268,6 @@
.filtered-search-box-input-container {
flex: 1;
position: relative;
- // Fix PhantomJS not supporting `flex: 1;` properly.
- // This is important because it can change the expected `e.target` when clicking things in tests.
- // See https://gitlab.com/gitlab-org/gitlab-ce/blob/b54acba8b732688c59fe2f38510c469dc86ee499/spec/features/issues/filtered_search/visual_tokens_spec.rb#L61
- // - With `width: 100%`: `e.target` = `.tokens-container`, https://i.imgur.com/jGq7wbx.png
- // - Without `width: 100%`: `e.target` = `.filtered-search`, https://i.imgur.com/cNI2CyT.png
- width: 100%;
min-width: 0;
}
@@ -469,10 +463,10 @@
word-break: break-all;
}
}
-}
-.filter-dropdown-item.droplab-item-active .btn {
- @extend %filter-dropdown-item-btn-hover;
+ &.droplab-item-active .btn {
+ @extend %filter-dropdown-item-btn-hover;
+ }
}
.filter-dropdown-loading {
diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss
index 52b87de7a3d..dc591c06c88 100644
--- a/app/assets/stylesheets/framework/gitlab-theme.scss
+++ b/app/assets/stylesheets/framework/gitlab-theme.scss
@@ -216,12 +216,9 @@ body {
color: $theme-gray-900;
}
- &.active > a {
+ &.active > a,
+ &.active > a:hover {
color: $white-light;
-
- &:hover {
- color: $white-light;
- }
}
}
}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index d79444fad79..5d777f0d468 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -239,10 +239,8 @@
fill: currentColor;
}
- &.header-user-dropdown-toggle {
- .header-user-avatar {
- border-color: $white-light;
- }
+ &.header-user-dropdown-toggle .header-user-avatar {
+ border-color: $white-light;
}
}
}
@@ -354,7 +352,77 @@
.header-user .dropdown-menu-nav,
.header-new .dropdown-menu-nav {
- margin-top: $dropdown-vertical-offset;
+ margin-top: 4px;
+}
+
+.search {
+ margin: 4px 8px 0;
+
+ form {
+ height: 32px;
+ border: 0;
+ border-radius: $border-radius-default;
+ transition: border-color ease-in-out 0.15s, background-color ease-in-out 0.15s;
+
+ &:hover {
+ box-shadow: none;
+ }
+ }
+
+ .search-input {
+ color: $white-light;
+ background: none;
+ transition: color ease-in-out 0.15s;
+ }
+
+ .search-input::placeholder {
+ transition: color ease-in-out 0.15s;
+ }
+
+ .location-badge {
+ font-size: 12px;
+ margin: -4px 4px -4px -4px;
+ line-height: 25px;
+ padding: 4px 8px;
+ border-radius: 2px 0 0 2px;
+ height: 32px;
+ transition: border-color ease-in-out 0.15s;
+ }
+
+ &.search-active {
+ form {
+ background-color: rgba($indigo-200, .3);
+ box-shadow: none;
+
+ .search-input {
+ color: $gl-text-color;
+ transition: color ease-in-out 0.15s;
+ }
+
+ .search-input::placeholder {
+ color: $gl-text-color-tertiary;
+ }
+
+ .search-input-wrap {
+ .search-icon,
+ .clear-icon {
+ color: $gl-text-color-tertiary;
+ transition: color ease-in-out 0.15s;
+ }
+ }
+ }
+
+ .location-badge {
+ background-color: $nav-badge-bg;
+ border-color: $border-color;
+ }
+
+ .search-input-wrap {
+ .clear-icon {
+ color: $white-light;
+ }
+ }
+ }
}
.breadcrumbs {
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index 69d19ea2962..cb324ccc440 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -30,10 +30,10 @@ body {
.container {
padding-top: 0;
z-index: 5;
-}
-.container .content {
- margin: 0;
+ .content {
+ margin: 0;
+ }
}
.navless-container {
@@ -82,26 +82,26 @@ body {
transition: background-color 0.15s, border-color 0.15s;
background-color: $orange-500;
border-color: $orange-500;
- }
- .alert-warning + .alert-warning {
- background-color: $orange-600;
- border-color: $orange-600;
- }
+ &:only-of-type {
+ background-color: $orange-500;
+ border-color: $orange-500;
+ }
- .alert-warning + .alert-warning + .alert-warning {
- background-color: $orange-700;
- border-color: $orange-700;
- }
+ + .alert-warning {
+ background-color: $orange-600;
+ border-color: $orange-600;
- .alert-warning + .alert-warning + .alert-warning + .alert-warning {
- background-color: $orange-800;
- border-color: $orange-800;
- }
+ + .alert-warning {
+ background-color: $orange-700;
+ border-color: $orange-700;
- .alert-warning:only-of-type {
- background-color: $orange-500;
- border-color: $orange-500;
+ + .alert-warning {
+ background-color: $orange-800;
+ border-color: $orange-800;
+ }
+ }
+ }
}
}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index d43f998cb82..511608c618c 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -299,40 +299,40 @@ ul.indent-list {
}
}
-.group-list-tree .avatar-container.content-loading {
- position: relative;
+.group-list-tree {
+ .avatar-container.content-loading {
+ position: relative;
- > a,
- > a .avatar {
- height: 100%;
- border-radius: 50%;
- }
+ > a,
+ > a .avatar {
+ height: 100%;
+ border-radius: 50%;
+ }
- > a {
- padding: 2px;
- }
+ > a {
+ padding: 2px;
- > a .avatar {
- border: 2px solid $white-normal;
+ .avatar {
+ border: 2px solid $white-normal;
- &.identicon {
- line-height: 30px;
+ &.identicon {
+ line-height: 30px;
+ }
+ }
}
- }
- &::after {
- content: "";
- position: absolute;
- height: 100%;
- width: 100%;
- background-color: transparent;
- border: 2px outset $kdb-border;
- border-radius: 50%;
- animation: spin-avatar 3s infinite linear;
+ &::after {
+ content: "";
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ background-color: transparent;
+ border: 2px outset $kdb-border;
+ border-radius: 50%;
+ animation: spin-avatar 3s infinite linear;
+ }
}
-}
-.group-list-tree {
.folder-toggle-wrap {
float: left;
line-height: $list-text-height;
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index e3920b5d3d9..0a5a16c09b0 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -173,21 +173,8 @@
ul > li {
white-space: nowrap;
}
-}
-
-@media(max-width: $screen-xs-max) {
- .atwho-view-ul {
- width: 350px;
- }
-
- .atwho-view ul li {
- overflow: hidden;
- text-overflow: ellipsis;
- }
-}
-// TODO: fallback to global style
-.atwho-view {
+ // TODO: fallback to global style
.atwho-view-ul {
padding: 8px 1px;
@@ -220,3 +207,14 @@
}
}
}
+
+@media(max-width: $screen-xs-max) {
+ .atwho-view-ul {
+ width: 350px;
+ }
+
+ .atwho-view ul li {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+}
diff --git a/app/assets/stylesheets/framework/responsive-tables.scss b/app/assets/stylesheets/framework/responsive_tables.scss
index 8e653c443cf..8b7afdbe1a5 100644
--- a/app/assets/stylesheets/framework/responsive-tables.scss
+++ b/app/assets/stylesheets/framework/responsive_tables.scss
@@ -3,57 +3,77 @@
max-width: #{$max + '%'};
}
+.gl-responsive-table-row-layout {
+ width: 100%;
+
+ @media (min-width: $screen-md-min) {
+ display: flex;
+ align-items: center;
+
+ & > &:not(:first-child) {
+ margin-top: $gl-padding;
+ }
+ }
+}
+
.gl-responsive-table-row {
+ @extend .gl-responsive-table-row-layout;
margin-top: 10px;
border: 1px solid $border-color;
@media (min-width: $screen-md-min) {
- padding: 15px 0;
margin: 0;
- display: flex;
- align-items: center;
+ padding: $gl-padding 0;
border: none;
- border-bottom: 1px solid $white-normal;
+
+ &:not(:last-child) {
+ border-bottom: 1px solid $white-normal;
+ }
}
+}
- .table-section {
- white-space: nowrap;
+.gl-responsive-table-row-col-span {
+ flex-wrap: wrap;
+}
+
+.table-section {
+ white-space: nowrap;
- $section-widths: 10 15 20 25 30 40;
- @each $width in $section-widths {
- &.section-#{$width} {
- flex: 0 0 #{$width + '%'};
+ $section-widths: 10 15 20 25 30 40 100;
+ @each $width in $section-widths {
+ &.section-#{$width} {
+ flex: 0 0 #{$width + '%'};
- @media (min-width: $screen-md-min) {
- max-width: #{$width + '%'};
- }
+ @media (min-width: $screen-md-min) {
+ max-width: #{$width + '%'};
}
}
+ }
- &:not(.table-button-footer) {
- @media (max-width: $screen-sm-max) {
- display: flex;
- align-self: stretch;
- padding: 10px;
- align-items: center;
- min-height: 62px;
+ @media (max-width: $screen-sm-max) {
+ display: flex;
+ align-self: stretch;
+ padding: 10px;
+ align-items: center;
+ min-height: 62px;
- &:not(:first-of-type) {
- border-top: 1px solid $white-normal;
- }
- }
+ &:not(:first-child) {
+ border-top: 1px solid $white-normal;
}
+ }
- &.section-wrap {
- white-space: normal;
+ &.section-wrap {
+ white-space: normal;
- @media (max-width: $screen-sm-max) {
- flex-wrap: wrap;
- }
+ @media (max-width: $screen-sm-max) {
+ flex-wrap: wrap;
}
}
-}
+ &.section-align-top {
+ align-self: flex-start;
+ }
+}
.table-button-footer {
@media (min-width: $screen-md-min) {
@@ -61,12 +81,13 @@
}
@media (max-width: $screen-sm-max) {
- background-color: $gray-normal;
+ display: block;
align-self: stretch;
+ min-height: 0;
+ background-color: $gray-normal;
border-top: 1px solid $border-color;
.table-action-buttons {
- padding: 10px 5px;
display: flex;
.btn {
@@ -77,7 +98,14 @@
> .external-url,
> .btn {
flex: 1 1 28px;
- margin: 0 5px;
+
+ &:not(:first-child) {
+ margin-left: 5px;
+ }
+
+ &:not(:last-child) {
+ margin-right: 5px;
+ }
}
.dropdown-new {
diff --git a/app/assets/stylesheets/framework/secondary-navigation-elements.scss b/app/assets/stylesheets/framework/secondary-navigation-elements.scss
index 3fd2549b143..9e1f77e5726 100644
--- a/app/assets/stylesheets/framework/secondary-navigation-elements.scss
+++ b/app/assets/stylesheets/framework/secondary-navigation-elements.scss
@@ -340,11 +340,64 @@
}
}
-.project-item-select-holder.btn-group {
- display: flex;
- max-width: 350px;
- overflow: hidden;
- float: right;
+.page-with-layout-nav {
+ .right-sidebar {
+ top: ($header-height + 1) * 2;
+ }
+
+ &.page-with-sub-nav {
+ .right-sidebar {
+ top: ($header-height + 1) * 3;
+
+ &.affix {
+ top: $header-height;
+ }
+ }
+ }
+}
+
+.with-performance-bar .page-with-layout-nav {
+ .right-sidebar {
+ top: ($header-height + 1) * 2 + $performance-bar-height;
+ }
+
+ &.page-with-sub-nav {
+ .right-sidebar {
+ top: ($header-height + 1) * 3 + $performance-bar-height;
+
+ &.affix {
+ top: $header-height + $performance-bar-height;
+ }
+ }
+ }
+}
+
+@media (max-width: $screen-xs-max) {
+ .top-area {
+ flex-flow: row wrap;
+
+ .nav-controls {
+ $controls-margin: $btn-xs-side-margin - 2px;
+ flex: 0 0 100%;
+
+ &.controls-flex {
+ display: flex;
+ flex-flow: row wrap;
+ align-items: center;
+ justify-content: center;
+ padding: 0 0 $gl-padding-top;
+ }
+
+ .controls-item,
+ .controls-item-full,
+ .controls-item:last-child {
+ flex: 1 1 35%;
+ display: block;
+ width: 100%;
+ margin: $controls-margin;
+ }
+ }
+ }
.new-project-item-link {
white-space: nowrap;
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index 621eec4f158..aa35cd9bea4 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -60,22 +60,12 @@
border-radius: $border-radius-base;
border: 1px solid $dropdown-border-color;
min-width: 175px;
- color: $gl-text-color;
- z-index: 999;
+ color: $gl-grayish-blue;
}
-.select2-drop-mask {
- z-index: 998;
-}
-
-.select2-drop.select2-drop-above.select2-drop-active {
- border-top: 1px solid $dropdown-border-color;
- margin-top: -6px;
-}
-
-.select2-results li.select2-result-with-children > .select2-result-label {
- font-weight: $gl-font-weight-bold;
- color: $gl-text-color;
+.select2-results .select2-result-label,
+.select2-more-results {
+ padding: 10px 15px;
}
.select2-container-active {
@@ -144,58 +134,46 @@
.select2-drop-auto-width & {
padding: 15px 15px 5px;
}
-}
-.select2-search input {
- padding: 2px 25px 2px 5px;
- background: $white-light image-url('select2.png');
- background-repeat: no-repeat;
- background-position: right 0 bottom 6px;
- border: 1px solid $input-border;
- border-radius: $border-radius-default;
- transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
-
- &:focus {
- border-color: $input-border-focus;
+ input {
+ padding: 2px 25px 2px 5px;
+ background: $white-light image-url('select2.png');
+ background-repeat: no-repeat;
+ background-position: right 0 bottom 6px;
+ border: 1px solid $input-border;
+ border-radius: $border-radius-default;
+ transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
+
+ &:focus {
+ border-color: $input-border-focus;
+ }
+
+ &.select2-active {
+ background-color: $white-light;
+ background-image: image-url('select2-spinner.gif') !important;
+ background-repeat: no-repeat;
+ background-position: right 5px center !important;
+ background-size: 16px 16px !important;
+ }
}
}
-.select2-search input.select2-active {
- background-color: $white-light;
- background-image: image-url('select2-spinner.gif') !important;
- background-repeat: no-repeat;
- background-position: right 5px center !important;
- background-size: 16px 16px !important;
+.select2-results .select2-no-results,
+.select2-results .select2-searching,
+.select2-results .select2-ajax-error,
+.select2-results .select2-selection-limit {
+ background: $gray-light;
+ display: list-item;
+ padding: 10px 15px;
}
.select2-results {
margin: 0;
- padding: #{$gl-padding / 2} 0;
-
- .select2-no-results,
- .select2-searching,
- .select2-ajax-error,
- .select2-selection-limit {
- background: transparent;
- padding: #{$gl-padding / 2} $gl-padding;
- }
-
- .select2-result-label,
- .select2-more-results {
- padding: #{$gl-padding / 2} $gl-padding;
- }
+ padding: 10px 0;
- .select2-highlighted {
- background: transparent;
+ li.select2-result-with-children > .select2-result-label {
+ font-weight: $gl-font-weight-bold;
color: $gl-text-color;
-
- .select2-result-label {
- background: $dropdown-item-hover-bg;
- }
- }
-
- .select2-result {
- padding: 0 1px;
}
}
@@ -212,6 +190,8 @@
}
.select2-highlighted {
+ background: $gl-link-color !important;
+
.group-result {
.group-path {
color: $white-light;
diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss
index 65b140cd7f8..c3d8f0c61a2 100644
--- a/app/assets/stylesheets/highlight/white.scss
+++ b/app/assets/stylesheets/highlight/white.scss
@@ -217,13 +217,31 @@ $white-gc-bg: #eaf2f5;
.cp { color: $white-cp; font-weight: $gl-font-weight-bold; }
.c1 { color: $white-c1; font-style: italic; }
.cs { color: $white-cs; font-weight: $gl-font-weight-bold; font-style: italic; }
- .gd { color: $white-gd; background-color: $white-gd-bg; }
- .gd .x { color: $white-gd-x; background-color: $white-gd-x-bg; }
+
+ .gd {
+ color: $white-gd;
+ background-color: $white-gd-bg;
+
+ .x {
+ color: $white-gd-x;
+ background-color: $white-gd-x-bg;
+ }
+ }
+
.ge { font-style: italic; }
.gr { color: $white-gr; }
.gh { color: $white-gh; }
- .gi { color: $white-gi; background-color: $white-gi-bg; }
- .gi .x { color: $white-gi-x; background-color: $white-gi-x-bg; }
+
+ .gi {
+ color: $white-gi;
+ background-color: $white-gi-bg;
+
+ .x {
+ color: $white-gi-x;
+ background-color: $white-gi-x-bg;
+ }
+ }
+
.go { color: $white-go; }
.gp { color: $white-gp; }
.gs { font-weight: $gl-font-weight-bold; }
diff --git a/app/assets/stylesheets/mailers/highlighted_diff_email.scss b/app/assets/stylesheets/mailers/highlighted_diff_email.scss
index fbe538ad1d7..658ac26fca9 100644
--- a/app/assets/stylesheets/mailers/highlighted_diff_email.scss
+++ b/app/assets/stylesheets/mailers/highlighted_diff_email.scss
@@ -158,13 +158,31 @@ span.highlight_word {
.cp { color: $highlighted-cp; font-weight: $gl-font-weight-bold; }
.c1 { color: $highlighted-c1; font-style: italic; }
.cs { color: $highlighted-cs; font-weight: $gl-font-weight-bold; font-style: italic; }
-.gd { color: $highlighted-gd; background-color: $highlighted-gd-bg; }
-.gd .x { color: $highlighted-gd; background-color: $highlighted-gd-x-bg; }
+
+.gd {
+ color: $highlighted-gd;
+ background-color: $highlighted-gd-bg;
+
+ .x {
+ color: $highlighted-gd;
+ background-color: $highlighted-gd-x-bg;
+ }
+}
+
.ge { font-style: italic; }
.gr { color: $highlighted-gr; }
.gh { color: $highlighted-gh; }
-.gi { color: $highlighted-gi; background-color: $highlighted-gi-bg; }
-.gi .x { color: $highlighted-gi; background-color: $highlighted-gi-x-bg; }
+
+.gi {
+ color: $highlighted-gi;
+ background-color: $highlighted-gi-bg;
+
+ .x {
+ color: $highlighted-gi;
+ background-color: $highlighted-gi-x-bg;
+ }
+}
+
.go { color: $highlighted-go; }
.gp { color: $highlighted-gp; }
.gs { font-weight: $gl-font-weight-bold; }
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 91296b354a7..3683afa07de 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -72,7 +72,7 @@
}
.boards-list {
- height: calc(100vh - 152px);
+ height: calc(100vh - 105px);
width: 100%;
padding-top: 25px;
padding-bottom: 25px;
@@ -81,11 +81,12 @@
overflow-x: scroll;
white-space: nowrap;
- @media (min-width: $screen-sm-min) {
- height: 475px; // Needed for PhantomJS
- // scss-lint:disable DuplicateProperty
- height: calc(100vh - 222px);
- // scss-lint:enable DuplicateProperty
+ @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
+ height: calc(100vh - 90px);
+ }
+
+ @media (min-width: $screen-md-min) {
+ height: calc(100vh - 160px);
min-height: 475px;
}
}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 50ec5110bf1..46978be8ba0 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -68,18 +68,18 @@
&.affix {
top: $header-height;
- }
- // with sidebar
- &.affix.sidebar-expanded {
- right: 306px;
- left: 16px;
- }
+ // with sidebar
+ &.sidebar-expanded {
+ right: 306px;
+ left: 16px;
+ }
- // without sidebar
- &.affix.sidebar-collapsed {
- right: 16px;
- left: 16px;
+ // without sidebar
+ &.sidebar-collapsed {
+ right: 16px;
+ left: 16px;
+ }
}
&.affix-top {
@@ -333,8 +333,10 @@
svg {
position: relative;
- top: 2px;
+ top: 3px;
margin-right: 3px;
+ width: 14px;
+ height: 14px;
}
}
@@ -348,9 +350,10 @@
svg {
position: relative;
- top: 2px;
+ top: 3px;
margin-right: 3px;
- height: 13px;
+ height: 14px;
+ width: 14px;
}
a {
@@ -369,7 +372,7 @@
.build-job {
position: relative;
- .fa-arrow-right {
+ .icon-arrow-right {
position: absolute;
left: 15px;
top: 20px;
@@ -379,7 +382,7 @@
&.active {
font-weight: $gl-font-weight-bold;
- .fa-arrow-right {
+ .icon-arrow-right {
display: block;
}
}
@@ -392,8 +395,7 @@
background-color: $row-hover;
}
- .fa-refresh {
- font-size: 13px;
+ .icon-retry {
margin-left: 3px;
}
}
diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss
index 8d6f30e3b84..5c91579c69c 100644
--- a/app/assets/stylesheets/pages/clusters.scss
+++ b/app/assets/stylesheets/pages/clusters.scss
@@ -2,8 +2,4 @@
.clipboard-addon {
background-color: $white-light;
}
-
- .alert-block {
- margin-bottom: 10px;
- }
}
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
index 2a92673d9fa..82d9be29201 100644
--- a/app/assets/stylesheets/pages/cycle_analytics.scss
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -22,6 +22,11 @@
}
}
}
+
+ svg {
+ width: 136px;
+ height: 136px;
+ }
}
.col-headers {
@@ -155,11 +160,6 @@
}
}
- .landing svg {
- width: 136px;
- height: 136px;
- }
-
.fa-spinner {
font-size: 28px;
position: relative;
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 09f831dcb29..faa3d1fb4d5 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -380,6 +380,10 @@
}
}
}
+
+ .line_content {
+ white-space: pre-wrap;
+ }
}
.file-content .diff-file {
@@ -387,10 +391,6 @@
border: none;
}
-.diff-file .line_content {
- white-space: pre-wrap;
-}
-
.diff-wrap-lines .line_content {
white-space: pre-wrap;
}
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 3b5e411e2c5..b5b0f3d9dfa 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -133,12 +133,11 @@
}
.folder-row {
- padding: 15px 0;
- border-bottom: 1px solid $white-normal;
+ border-left: none;
+ border-right: none;
- @media (max-width: $screen-sm-max) {
- border-top: 1px solid $white-normal;
- margin-top: 10px;
+ @media (min-width: $screen-sm-max) {
+ border-top: none;
}
}
@@ -256,23 +255,6 @@
width: 100%;
padding: 0;
padding-bottom: 100%;
-}
-
-.prometheus-svg-container > svg {
- position: absolute;
- height: 100%;
- width: 100%;
- left: 0;
- top: 0;
-
- text {
- fill: $gl-text-color;
- stroke-width: 0;
- }
-
- .text-metric-bold {
- font-weight: $gl-font-weight-bold;
- }
.label-axis-text {
fill: $black;
@@ -287,42 +269,51 @@
font-size: 12px;
}
- .legend-axis-text {
- fill: $black;
- }
+ > svg {
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ left: 0;
+ top: 0;
- .tick {
- > line {
- stroke: $gray-darker;
+ .label-axis-text,
+ .text-metric-usage {
+ fill: $black;
+ font-weight: $gl-font-weight-normal;
+ font-size: 12px;
}
- > text {
- font-size: 12px;
+ .legend-axis-text {
+ fill: $black;
}
- }
- .text-metric-title {
- font-size: 12px;
- }
+ .tick > text {
+ font-size: 12px;
+ }
- .y-label-text,
- .x-label-text {
- fill: $gray-darkest;
- }
+ .text-metric-title {
+ font-size: 12px;
+ }
- .axis-tick {
- stroke: $gray-darker;
- }
+ .y-label-text,
+ .x-label-text {
+ fill: $gray-darkest;
+ }
- @media (max-width: $screen-sm-max) {
- .label-axis-text,
- .text-metric-usage,
- .legend-axis-text {
- font-size: 8px;
+ .axis-tick {
+ stroke: $gray-darker;
}
- .tick > text {
- font-size: 8px;
+ @media (max-width: $screen-sm-max) {
+ .label-axis-text,
+ .text-metric-usage,
+ .legend-axis-text {
+ font-size: 8px;
+ }
+
+ .tick > text {
+ font-size: 8px;
+ }
}
}
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 48532503263..7059a4cfe85 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -127,7 +127,16 @@
}
.right-sidebar {
- a:not(.btn-retry),
+ position: absolute;
+ top: $header-height;
+ bottom: 0;
+ right: 0;
+ transition: width .3s;
+ background: $gray-light;
+ z-index: 200;
+ overflow: hidden;
+
+ a,
.btn-link {
color: inherit;
}
@@ -228,17 +237,6 @@
.btn-clipboard:hover {
color: $gl-text-color;
}
-}
-
-.right-sidebar {
- position: absolute;
- top: $header-height;
- bottom: 0;
- right: 0;
- transition: width $right-sidebar-transition-duration;
- background: $gray-light;
- z-index: 200;
- overflow: hidden;
.issuable-sidebar {
width: calc(100% + 100px);
@@ -542,7 +540,9 @@
}
.participants-list {
- margin: -5px;
+ display: flex;
+ flex-wrap: wrap;
+ margin: -7px;
}
@@ -553,7 +553,7 @@
.participants-author {
display: inline-block;
- padding: 5px;
+ padding: 7px;
&:nth-of-type(7n) {
padding-right: 0;
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index cf5f933a762..92d49bd864a 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -109,6 +109,30 @@
border-top-right-radius: $border-radius-default;
border-top-left-radius: $border-radius-default;
+ // Ldap configurations may need more tabs & the tab labels are user generated (arbitrarily long).
+ // These styles prevent this from breaking the layout, and only applied when providers are configured.
+ &.custom-provider-tabs {
+ flex-wrap: wrap;
+
+ li {
+ min-width: 85px;
+ flex-basis: auto;
+
+ // This styles tab elements that have wrapped to a second line. We cannot easily predict when this will happen.
+ // We are making somewhat of an assumption about the configuration here: that users do not have more than
+ // 3 LDAP servers configured (in addition to standard login) and they are not using especially long names for any
+ // of them. If either condition is false, this will work as expected. If both are true, there may be a missing border
+ // above one of the bottom row elements. If you know a better way, please implement it!
+ &:nth-child(n+5) {
+ border-top: 1px solid $border-color;
+ }
+ }
+
+ a {
+ font-size: 16px;
+ }
+ }
+
li {
flex: 1;
text-align: center;
@@ -154,32 +178,6 @@
}
}
- // Ldap configurations may need more tabs & the tab labels are user generated (arbitrarily long).
- // These styles prevent this from breaking the layout, and only applied when providers are configured.
-
- .new-session-tabs.custom-provider-tabs {
- flex-wrap: wrap;
-
- li {
- min-width: 85px;
- flex-basis: auto;
-
- // This styles tab elements that have wrapped to a second line. We cannot easily predict when this will happen.
- // We are making somewhat of an assumption about the configuration here: that users do not have more than
- // 3 LDAP servers configured (in addition to standard login) and they are not using especially long names for any
- // of them. If either condition is false, this will work as expected. If both are true, there may be a missing border
- // above one of the bottom row elements. If you know a better way, please implement it!
- &:nth-child(n+5) {
- border-top: 1px solid $border-color;
- }
- }
-
- a {
- font-size: 16px;
- }
- }
-
-
.form-control {
&:active,
&:focus {
@@ -231,35 +229,35 @@
margin: 0;
padding: 0;
height: 100%;
-}
-// Fixes footer container to bottom of viewport
-.devise-layout-html body {
- // offset height of fixed header + 1 to avoid scroll
- height: calc(100% - 51px);
- margin: 0;
- padding: 0;
+ // Fixes footer container to bottom of viewport
+ body {
+ // offset height of fixed header + 1 to avoid scroll
+ height: calc(100% - 51px);
+ margin: 0;
+ padding: 0;
- .page-wrap {
- min-height: 100%;
- position: relative;
- }
+ .page-wrap {
+ min-height: 100%;
+ position: relative;
+ }
- .footer-container,
- hr.footer-fixed {
- position: absolute;
- bottom: 0;
- left: 0;
- right: 0;
- height: 40px;
- background: $white-light;
- }
+ .footer-container,
+ hr.footer-fixed {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 40px;
+ background: $white-light;
+ }
- .navless-container {
- padding: 65px 15px; // height of footer + bottom padding of email confirmation link
+ .navless-container {
+ padding: 65px 15px; // height of footer + bottom padding of email confirmation link
- @media (max-width: $screen-xs-max) {
- padding: 0 15px 65px;
+ @media (max-width: $screen-xs-max) {
+ padding: 0 15px 65px;
+ }
}
}
}
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
index 692acf74a58..18c48405ecd 100644
--- a/app/assets/stylesheets/pages/members.scss
+++ b/app/assets/stylesheets/pages/members.scss
@@ -49,9 +49,17 @@
width: auto;
}
}
+
+ &.existing-title {
+ @media (min-width: $screen-sm-min) {
+ float: left;
+ }
+ }
}
.member-form-control {
+ @include new-style-dropdown;
+
@media (max-width: $screen-xs-max) {
padding-bottom: 5px;
margin-left: 0;
@@ -64,12 +72,6 @@
line-height: 43px;
}
-.member.existing-title {
- @media (min-width: $screen-sm-min) {
- float: left;
- }
-}
-
.member-search-form {
@include new-style-dropdown;
@@ -281,7 +283,3 @@
}
}
}
-
-.member-form-control {
- @include new-style-dropdown;
-}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index d9fb3b44d29..6e485ebad1b 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -156,6 +156,10 @@
&.media > *:first-child {
margin-right: 10px;
}
+
+ .approve-btn {
+ margin-right: 5px;
+ }
}
.mr-widget-pipeline-graph {
@@ -165,8 +169,9 @@
z-index: 300;
}
- .ci-action-icon-wrapper {
- line-height: 16px;
+ .ci-action-icon-wrapper svg {
+ width: 16px;
+ height: 16px;
}
}
@@ -190,6 +195,10 @@
overflow: hidden;
word-break: break-all;
+ &.media > *:first-child {
+ margin-right: 10px;
+ }
+
&.label-truncated {
position: relative;
display: inline-block;
@@ -207,14 +216,7 @@
background-color: $gray-light;
}
}
- }
- .mr-widget-help {
- padding: 10px 16px 10px 48px;
- font-style: italic;
- }
-
- .mr-widget-body {
h4 {
float: left;
font-weight: $gl-font-weight-bold;
@@ -237,6 +239,10 @@
margin-right: 7px;
}
+ .approve-btn {
+ margin-right: 5px;
+ }
+
label {
font-weight: $gl-font-weight-normal;
}
@@ -336,6 +342,22 @@
}
}
+ .mini-pipeline-graph-dropdown-menu .mini-pipeline-graph-dropdown-item {
+ display: flex;
+ align-items: center;
+
+ .ci-status-text,
+ .ci-status-icon {
+ top: 0;
+ margin-right: 10px;
+ }
+ }
+
+ .mr-widget-help {
+ padding: 10px 16px 10px 48px;
+ font-style: italic;
+ }
+
.ci-coverage {
float: right;
}
@@ -350,12 +372,6 @@
}
}
-.mr-state-widget .mr-widget-body {
- .approve-btn {
- margin-right: 5px;
- }
-}
-
.mr-widget-body-controls {
flex-wrap: wrap;
}
@@ -469,16 +485,16 @@
padding-bottom: 0;
}
}
-}
-.mr-info-list.mr-memory-usage {
- p {
- float: left;
- }
+ &.mr-memory-usage {
+ p {
+ float: left;
+ }
- .memory-graph-container {
- float: left;
- margin-left: 5px;
+ .memory-graph-container {
+ float: left;
+ margin-left: 5px;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index 32039936be7..ae8fa45a2d7 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -66,6 +66,15 @@
height: 6px;
margin: 0;
}
+
+ .sidebar-collapsed-icon {
+ clear: both;
+ padding: 15px 5px 5px;
+
+ .progress {
+ margin: 5px 0;
+ }
+ }
}
.collapsed-milestone-date {
@@ -93,17 +102,6 @@
margin-right: 0;
}
- .milestone-progress {
- .sidebar-collapsed-icon {
- clear: both;
- padding: 15px 5px 5px;
-
- .progress {
- margin: 5px 0;
- }
- }
- }
-
.right-sidebar-collapsed & {
.reference {
border-top: 1px solid $border-gray-normal;
@@ -156,18 +154,16 @@
.status-box {
margin-top: 0;
- }
-
- .milestone-buttons {
- margin-left: auto;
- }
-
- .status-box {
order: 1;
}
.milestone-buttons {
+ margin-left: auto;
order: 2;
+
+ .verbose {
+ display: none;
+ }
}
.header-text-content {
@@ -175,10 +171,6 @@
width: 100%;
}
- .milestone-buttons .verbose {
- display: none;
- }
-
@media (min-width: $screen-xs-min) {
.milestone-buttons .verbose {
display: inline;
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index f0cad30f4f3..5127307c5e7 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -111,24 +111,9 @@
margin: auto;
align-items: center;
- .icon {
- margin-right: $issuable-warning-icon-margin;
- }
-}
-
-.disabled-comment .issuable-note-warning {
- border: none;
- border-radius: $label-border-radius;
- padding-top: $gl-vert-padding;
- padding-bottom: $gl-vert-padding;
-
- .icon svg {
- position: relative;
- top: 2px;
- margin-right: $btn-xs-side-margin;
- width: $gl-font-size;
- height: $gl-font-size;
- fill: $orange-600;
+ + .md-area {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
}
}
@@ -155,11 +140,6 @@
}
}
-.issuable-note-warning + .md-area {
- border-top-left-radius: 0;
- border-top-right-radius: 0;
-}
-
.discussion-form {
background-color: $white-light;
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 3bd0e3ad535..ca363c6eac4 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -269,7 +269,7 @@ ul.notes {
display: none;
}
- &.system-note-commit-list {
+ &.system-note-commit-list:not(.hide-shade) {
max-height: 70px;
overflow: hidden;
display: block;
@@ -291,16 +291,6 @@ ul.notes {
bottom: 0;
background: linear-gradient(rgba($white-light, 0.1) -100px, $white-light 100%);
}
-
- &.hide-shade {
- max-height: 100%;
- overflow: auto;
-
- &::after {
- display: none;
- background: transparent;
- }
- }
}
}
}
@@ -322,57 +312,72 @@ ul.notes {
}
}
-.diff-file .notes_holder {
- font-family: $regular_font;
+.diff-file {
+ .is-over {
+ .add-diff-note {
+ display: inline-block;
+ }
+ }
- td {
- border: 1px solid $white-normal;
- border-left: none;
+ // Merge request notes in diffs
+ // Diff is inline
+ .notes_content .note-header .note-headline-light {
+ display: inline-block;
+ position: relative;
+ }
- &.notes_line {
- vertical-align: middle;
- text-align: center;
- padding: 10px 0;
- background: $gray-light;
- color: $text-color;
- }
+ .notes_holder {
+ font-family: $regular_font;
- &.notes_line2 {
- text-align: center;
- padding: 10px 0;
- border-left: 1px solid $note-line2-border !important;
- }
+ td {
+ border: 1px solid $white-normal;
+ border-left: none;
- &.notes_content {
- background-color: $gray-light;
- border-width: 1px 0;
- padding: 0;
- vertical-align: top;
- white-space: normal;
+ &.notes_line {
+ vertical-align: middle;
+ text-align: center;
+ padding: 10px 0;
+ background: $gray-light;
+ color: $text-color;
+ }
- &.parallel {
- border-width: 1px;
+ &.notes_line2 {
+ text-align: center;
+ padding: 10px 0;
+ border-left: 1px solid $note-line2-border !important;
}
- .discussion-notes {
- &:not(:first-child) {
- border-top: 1px solid $white-normal;
- margin-top: 20px;
+ &.notes_content {
+ background-color: $gray-light;
+ border-width: 1px 0;
+ padding: 0;
+ vertical-align: top;
+ white-space: normal;
+
+ &.parallel {
+ border-width: 1px;
}
- &:not(:last-child) {
- border-bottom: 1px solid $white-normal;
- margin-bottom: 20px;
+ .discussion-notes {
+ &:not(:first-child) {
+ border-top: 1px solid $white-normal;
+ margin-top: 20px;
+ }
+
+ &:not(:last-child) {
+ border-bottom: 1px solid $white-normal;
+ margin-bottom: 20px;
+ }
}
- }
- .notes {
- background-color: $white-light;
- }
+ .notes {
+ background-color: $white-light;
+ }
- a code {
- top: 0;
- margin-right: 0;
+ a code {
+ top: 0;
+ margin-right: 0;
+ }
}
}
}
@@ -467,8 +472,9 @@ ul.notes {
margin-left: 10px;
color: $gray-darkest;
- .btn-group > .discussion-next-btn {
- margin-left: -1px;
+ @include notes-media('max', $screen-md-max) {
+ float: none;
+ margin-left: 0;
}
}
@@ -479,8 +485,6 @@ ul.notes {
flex-shrink: 0;
display: inline-flex;
align-items: center;
- // For PhantomJS that does not support flex
- float: right;
margin-left: 10px;
color: $gray-darkest;
@@ -491,7 +495,6 @@ ul.notes {
}
.more-actions {
- float: right; // phantomjs fallback
display: flex;
align-items: flex-end;
@@ -512,13 +515,6 @@ ul.notes {
min-width: 180px;
}
-.discussion-actions {
- @include notes-media('max', $screen-md-max) {
- float: none;
- margin-left: 0;
- }
-}
-
.note-actions-item {
margin-left: 12px;
display: flex;
@@ -675,14 +671,6 @@ ul.notes {
}
}
-.diff-file {
- .is-over {
- .add-diff-note {
- display: inline-block;
- }
- }
-}
-
.disabled-comment {
background-color: $gray-light;
border-radius: $border-radius-base;
@@ -724,20 +712,20 @@ ul.notes {
svg path {
fill: $gray-darkest;
}
- }
- .btn.discussion-create-issue-btn {
- margin-left: -4px;
- border-radius: 0;
- border-right: 0;
+ &.discussion-create-issue-btn {
+ margin-left: -4px;
+ border-radius: 0;
+ border-right: 0;
- a {
- padding: 0;
- line-height: 0;
+ a {
+ padding: 0;
+ line-height: 0;
- &:hover {
- text-decoration: none;
- border: 0;
+ &:hover {
+ text-decoration: none;
+ border: 0;
+ }
}
}
}
@@ -811,12 +799,3 @@ ul.notes {
.line-resolve-text {
vertical-align: middle;
}
-
-// Merge request notes in diffs
-.diff-file {
- // Diff is inline
- .notes_content .note-header .note-headline-light {
- display: inline-block;
- position: relative;
- }
-}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 8fc7a5eec9b..2a8cbc61af7 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -31,7 +31,6 @@
}
.pipeline-actions {
- padding-right: 0;
min-width: 170px; //Guarantees buttons don't break in several lines.
.btn-default {
@@ -176,6 +175,25 @@
}
}
+ /**
+ * Play button with icon in dropdowns
+ */
+ .no-btn {
+ border: none;
+ background: none;
+ outline: none;
+ width: 100%;
+ text-align: left;
+
+ .icon-play {
+ position: relative;
+ top: 2px;
+ margin-right: 5px;
+ height: 13px;
+ width: 12px;
+ }
+ }
+
.duration,
.finished-at {
color: $gl-text-color-secondary;
@@ -451,36 +469,46 @@
@extend .build-content:hover;
}
- // Action Icons in big pipeline-graph nodes
- .ci-action-icon-container .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-secondary;
- position: relative;
- left: -1px;
- top: -1px;
- }
-
- &:hover svg {
- fill: $gl-text-color;
- }
- }
-
.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;
+ left: 5px;
+ top: 2px;
+ width: 18px;
+ height: 18px;
+ }
+
+ &.play {
+ svg {
+ width: #{$ci-action-icon-size - 8};
+ height: #{$ci-action-icon-size - 8};
+ left: 8px;
+ }
+ }
+ }
}
.ci-status-icon svg {
@@ -721,17 +749,50 @@ button.mini-pipeline-graph-dropdown-toggle {
svg {
fill: $gl-text-color-secondary;
- width: $ci-action-icon-size;
- height: $ci-action-icon-size;
- left: -6px;
+ width: #{$ci-action-icon-size - 6};
+ height: #{$ci-action-icon-size - 6};
+ left: -3px;
position: relative;
- top: -3px;
+ top: -2px;
+
+ &.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;
+ }
}
&:hover svg,
&:focus svg {
fill: $gl-text-color;
}
+
+ &.icon-action-retry,
+ &.icon-action-play {
+ svg {
+ width: #{$ci-action-icon-size - 6};
+ height: #{$ci-action-icon-size - 6};
+ left: 8px;
+ }
+ }
+
+
}
// link to the build
@@ -799,13 +860,10 @@ button.mini-pipeline-graph-dropdown-toggle {
left: 100%;
top: -10px;
box-shadow: 0 1px 5px $black-transparent;
-}
-
-/**
- * Top arrow in the dropdown in the big pipeline graph
- */
-.big-pipeline-graph-dropdown-menu {
+ /**
+ * Top arrow in the dropdown in the big pipeline graph
+ */
&::before,
&::after {
content: '';
@@ -867,22 +925,23 @@ button.mini-pipeline-graph-dropdown-toggle {
margin-top: 1px;
border-bottom-color: $white-light;
}
-}
-/**
- * Center dropdown menu in mini graph
- */
-.mini-pipeline-graph-dropdown-menu.dropdown-menu {
- transform: translate(-80%, 0);
- min-width: 150px;
+ /**
+ * Center dropdown menu in mini graph
+ */
+ &.dropdown-menu {
+ transform: translate(-80%, 0);
+ min-width: 150px;
- @media(min-width: $screen-md-min) {
- transform: translate(-50%, 0);
- right: auto;
- left: 50%;
- min-width: 240px;
+ @media(min-width: $screen-md-min) {
+ transform: translate(-50%, 0);
+ right: auto;
+ left: 50%;
+ min-width: 240px;
+ }
}
}
+
/**
* Terminal
*/
@@ -906,25 +965,6 @@ button.mini-pipeline-graph-dropdown-toggle {
}
}
-/**
- * Play button with icon in dropdowns
- */
-.ci-table .no-btn {
- border: none;
- background: none;
- outline: none;
- width: 100%;
- text-align: left;
-
- .icon-play {
- position: relative;
- top: 2px;
- margin-right: 5px;
- height: 13px;
- width: 12px;
- }
-}
-
.ci-header-container {
min-height: 55px;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index bd385db9692..b0c3474e3d5 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -88,7 +88,8 @@
transition: background 2s ease-out;
&:disabled {
- opacity: 0.75;
+ opacity: 0.5;
+ pointer-events: none;
}
.highlight-changes & {
@@ -778,35 +779,35 @@ a.deploy-project-label {
.nav {
padding-top: 12px;
padding-bottom: 12px;
- }
- .nav > li {
- display: inline-block;
+ > li {
+ display: inline-block;
- &:not(:last-child) {
- margin-right: $gl-padding;
- }
+ &:not(:last-child) {
+ margin-right: $gl-padding;
+ }
- &.right {
- vertical-align: top;
- margin-top: 0;
+ &.right {
+ vertical-align: top;
+ margin-top: 0;
- @media (min-width: $screen-lg-min) {
- float: right;
+ @media (min-width: $screen-lg-min) {
+ float: right;
+ }
}
- }
- }
- .nav > li > a {
- padding: 0;
- background-color: transparent;
- font-size: 14px;
- line-height: 29px;
- color: $notes-light-color;
+ > a {
+ padding: 0;
+ background-color: transparent;
+ font-size: 14px;
+ line-height: 29px;
+ color: $notes-light-color;
- &:hover,
- &:focus {
- color: $gl-text-color;
+ &:hover,
+ &:focus {
+ color: $gl-text-color;
+ }
+ }
}
}
@@ -1160,13 +1161,6 @@ pre.light-well {
}
}
-.project-repo-select {
- &.disabled {
- opacity: 0.5;
- pointer-events: none;
- }
-}
-
.variables-table {
table-layout: fixed;
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 6a363b1710e..1bb4e3cc345 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -1,12 +1,3 @@
-.monaco-loader {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: $black-transparent;
-}
-
.modal.popup-dialog {
display: block;
background-color: $black-transparent;
@@ -54,6 +45,7 @@
}
.tree-content-holder {
+ display: -webkit-flex;
display: flex;
min-height: 300px;
}
@@ -63,7 +55,9 @@
}
.panel-right {
+ display: -webkit-flex;
display: flex;
+ -webkit-flex-direction: column;
flex-direction: column;
width: 80%;
height: 100%;
@@ -81,10 +75,6 @@
text-decoration: underline;
}
}
-
- .cursor {
- display: none !important;
- }
}
.blob-no-preview {
@@ -94,21 +84,12 @@
}
}
- &.edit-mode {
- .blob-viewer-container {
- overflow: hidden;
- }
-
- .monaco-editor.vs {
- .cursor {
- background: $black;
- border-color: $black;
- display: block !important;
- }
- }
+ &.blob-editor-container {
+ overflow: hidden;
}
.blob-viewer-container {
+ -webkit-flex: 1;
flex: 1;
overflow: auto;
@@ -138,6 +119,7 @@
}
#tabs {
+ position: relative;
flex-shrink: 0;
display: flex;
width: 100%;
@@ -166,6 +148,10 @@
vertical-align: middle;
text-decoration: none;
margin-right: 12px;
+
+ &:focus {
+ outline: none;
+ }
}
.close-btn {
@@ -312,23 +298,3 @@
width: 100%;
}
}
-
-@keyframes swipeRightAppear {
- 0% {
- transform: scaleX(0.00);
- }
-
- 100% {
- transform: scaleX(1.00);
- }
-}
-
-@keyframes swipeRightDissapear {
- 0% {
- transform: scaleX(1.00);
- }
-
- 100% {
- transform: scaleX(0.00);
- }
-}
diff --git a/app/assets/stylesheets/pages/runners.scss b/app/assets/stylesheets/pages/runners.scss
index 6cac37a4e28..5fb97b13470 100644
--- a/app/assets/stylesheets/pages/runners.scss
+++ b/app/assets/stylesheets/pages/runners.scss
@@ -50,3 +50,10 @@
font-size: 11px;
}
}
+
+@media (max-width: $screen-md-max) {
+ .runners-content {
+ width: 100%;
+ overflow: auto;
+ }
+}
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index db0a04a5eb3..eed711b1b66 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -78,6 +78,10 @@ input[type="checkbox"]:hover {
}
.search-input-wrap {
+ // Fallback if flexbox is not supported
+ display: inline-block;
+ width: 100%;
+
.search-icon,
.clear-icon {
position: absolute;
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index 41a6ba2023a..8b9b47a41bc 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -23,15 +23,14 @@
}
.settings {
- overflow: hidden;
border-bottom: 1px solid $gray-darker;
&:first-of-type {
margin-top: 10px;
}
- &.expanded {
- overflow: visible;
+ &.animating {
+ overflow: hidden;
}
}
@@ -56,14 +55,18 @@
overflow-y: scroll;
padding-right: 110px;
animation: collapseMaxHeight 300ms ease-out;
+ // Keep the section from expanding when we scroll over it
+ pointer-events: none;
- &.expanded {
+ .settings.expanded & {
max-height: none;
overflow-y: visible;
animation: expandMaxHeight 300ms ease-in;
+ // Reset and allow clicks again when expanded
+ pointer-events: auto;
}
- &.no-animate {
+ .settings.no-animate & {
animation: none;
}
@@ -238,11 +241,11 @@
margin-left: 5px;
background: $badge-bg;
}
- }
- /* Ensure we don't add border if there's only single li */
- li + li {
- border-top: 1px solid $border-color;
+ /* Ensure we don't add border if there's only single li */
+ + li {
+ border-top: 1px solid $border-color;
+ }
}
}
}
diff --git a/app/assets/stylesheets/pages/sherlock.scss b/app/assets/stylesheets/pages/sherlock.scss
index bfe065dbbaf..2bf0bedb1f5 100644
--- a/app/assets/stylesheets/pages/sherlock.scss
+++ b/app/assets/stylesheets/pages/sherlock.scss
@@ -5,10 +5,10 @@ table .sherlock-code {
.sherlock-code {
pre {
word-wrap: normal;
- }
- pre code {
- white-space: pre;
+ code {
+ white-space: pre;
+ }
}
}
@@ -21,13 +21,13 @@ table .sherlock-code {
text-align: right;
padding: 0 10px !important;
}
+
+ .slow {
+ color: $red-500;
+ font-weight: $gl-font-weight-bold;
+ }
}
.sherlock-file-sample pre {
padding-top: 28px !important;
}
-
-.sherlock-line-samples-table .slow {
- color: $red-500;
- font-weight: $gl-font-weight-bold;
-}
diff --git a/app/assets/stylesheets/pages/stat_graph.scss b/app/assets/stylesheets/pages/stat_graph.scss
index dfa4d033fb8..cede147d559 100644
--- a/app/assets/stylesheets/pages/stat_graph.scss
+++ b/app/assets/stylesheets/pages/stat_graph.scss
@@ -40,16 +40,16 @@
@media (max-width: $screen-xs-max) {
width: 100%;
}
- }
- .person .spark {
- display: block;
- background: $stat-graph-common-bg;
- width: 100%;
- }
+ .spark {
+ display: block;
+ background: $stat-graph-common-bg;
+ width: 100%;
+ }
- .person .area-contributor {
- fill: $stat-graph-orange-fill;
+ .area-contributor {
+ fill: $stat-graph-orange-fill;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss
index b7d4e7bf582..e150f96f3fa 100644
--- a/app/assets/stylesheets/pages/wiki.scss
+++ b/app/assets/stylesheets/pages/wiki.scss
@@ -161,10 +161,10 @@ ul.wiki-pages-list.content-list {
list-style: none;
margin-left: 0;
padding-left: 15px;
- }
- ul li {
- padding: 5px 0;
+ li {
+ padding: 5px 0;
+ }
}
}
diff --git a/app/assets/stylesheets/test.scss b/app/assets/stylesheets/test.scss
index 06733b7f1a9..e65b49c36f3 100644
--- a/app/assets/stylesheets/test.scss
+++ b/app/assets/stylesheets/test.scss
@@ -4,11 +4,6 @@
-ms-transition: none !important;
-webkit-transition: none !important;
transition: none !important;
- -o-transform: none !important;
- -moz-transform: none !important;
- -ms-transform: none !important;
- -webkit-transform: none !important;
- transform: none !important;
-webkit-animation: none !important;
-moz-animation: none !important;
-o-animation: none !important;
diff --git a/app/controllers/admin/applications_controller.rb b/app/controllers/admin/applications_controller.rb
index fb6d8c0bb81..5be23c76a95 100644
--- a/app/controllers/admin/applications_controller.rb
+++ b/app/controllers/admin/applications_controller.rb
@@ -19,10 +19,12 @@ class Admin::ApplicationsController < Admin::ApplicationController
end
def create
- @application = Doorkeeper::Application.new(application_params)
+ @application = Applications::CreateService.new(current_user, application_params).execute(request)
- if @application.save
- redirect_to_admin_page
+ if @application.persisted?
+ flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create])
+
+ redirect_to admin_application_url(@application)
else
render :new
end
@@ -41,13 +43,6 @@ class Admin::ApplicationsController < Admin::ApplicationController
redirect_to admin_applications_url, status: 302, notice: 'Application was successfully destroyed.'
end
- protected
-
- def redirect_to_admin_page
- flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create])
- redirect_to admin_application_url(@application)
- end
-
private
def set_application
diff --git a/app/controllers/admin/impersonation_tokens_controller.rb b/app/controllers/admin/impersonation_tokens_controller.rb
index 07c8bf714fc..7a2c7234a1e 100644
--- a/app/controllers/admin/impersonation_tokens_controller.rb
+++ b/app/controllers/admin/impersonation_tokens_controller.rb
@@ -44,7 +44,7 @@ class Admin::ImpersonationTokensController < Admin::ApplicationController
end
def set_index_vars
- @scopes = Gitlab::Auth::API_SCOPES
+ @scopes = Gitlab::Auth.available_scopes(current_user)
@impersonation_token ||= finder.build
@inactive_impersonation_tokens = finder(state: 'inactive').execute
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 391a0519195..3be7aee69bc 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -11,7 +11,7 @@ class ApplicationController < ActionController::Base
include EnforcesTwoFactorAuthentication
include WithPerformanceBar
- before_action :authenticate_user_from_private_token!
+ before_action :authenticate_user_from_personal_access_token!
before_action :authenticate_user_from_rss_token!
before_action :authenticate_user!
before_action :validate_user_service_ticket!
@@ -100,13 +100,12 @@ class ApplicationController < ActionController::Base
return try(:authenticated_user)
end
- # This filter handles both private tokens and personal access tokens
- def authenticate_user_from_private_token!
+ def authenticate_user_from_personal_access_token!
token = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
return unless token.present?
- user = User.find_by_authentication_token(token) || User.find_by_personal_access_token(token)
+ user = User.find_by_personal_access_token(token)
sessionless_sign_in(user)
end
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 4079072a930..b1ed973d178 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -7,6 +7,54 @@ module IssuableActions
before_action :authorize_admin_issuable!, only: :bulk_update
end
+ def show
+ respond_to do |format|
+ format.html do
+ render show_view
+ end
+ format.json do
+ render json: serializer.represent(issuable, serializer: params[:serializer])
+ end
+ end
+ end
+
+ def update
+ @issuable = update_service.execute(issuable)
+
+ respond_to do |format|
+ format.html do
+ recaptcha_check_with_fallback { render :edit }
+ end
+
+ format.json do
+ render_entity_json
+ end
+ end
+
+ rescue ActiveRecord::StaleObjectError
+ render_conflict_response
+ end
+
+ def realtime_changes
+ Gitlab::PollingInterval.set_header(response, interval: 3_000)
+
+ response = {
+ title: view_context.markdown_field(issuable, :title),
+ title_text: issuable.title,
+ description: view_context.markdown_field(issuable, :description),
+ description_text: issuable.description,
+ task_status: issuable.task_status
+ }
+
+ if issuable.edited?
+ response[:updated_at] = issuable.updated_at
+ response[:updated_by_name] = issuable.last_edited_by.name
+ response[:updated_by_path] = user_path(issuable.last_edited_by)
+ end
+
+ render json: response
+ end
+
def destroy
issuable.destroy
destroy_method = "destroy_#{issuable.class.name.underscore}".to_sym
@@ -68,6 +116,10 @@ module IssuableActions
end
end
+ def authorize_update_issuable!
+ render_404 unless can?(current_user, :"update_#{resource_name}", issuable)
+ end
+
def bulk_update_params
permitted_keys = [
:issuable_ids,
@@ -92,4 +144,24 @@ module IssuableActions
def resource_name
@resource_name ||= controller_name.singularize
end
+
+ def render_entity_json
+ if @issuable.valid?
+ render json: serializer.represent(@issuable)
+ else
+ render json: { errors: @issuable.errors.full_messages }, status: :unprocessable_entity
+ end
+ end
+
+ def show_view
+ 'show'
+ end
+
+ def serializer
+ raise NotImplementedError
+ end
+
+ def update_service
+ raise NotImplementedError
+ end
end
diff --git a/app/controllers/concerns/lfs_request.rb b/app/controllers/concerns/lfs_request.rb
index 2b6afaa6233..738afd612f0 100644
--- a/app/controllers/concerns/lfs_request.rb
+++ b/app/controllers/concerns/lfs_request.rb
@@ -94,10 +94,9 @@ module LfsRequest
@storage_project ||= begin
result = project
- loop do
- break unless result.forked?
- result = result.forked_from_project
- end
+ # TODO: Make this go to the fork_network root immeadiatly
+ # dependant on the discussion in: https://gitlab.com/gitlab-org/gitlab-ce/issues/39769
+ result = result.fork_source while result.forked?
result
end
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index 1126f706393..57b45f335fa 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -4,6 +4,7 @@ module NotesActions
included do
before_action :set_polling_interval_header, only: [:index]
+ before_action :noteable, only: :index
before_action :authorize_admin_note!, only: [:update, :destroy]
before_action :note_project, only: [:create]
end
@@ -108,6 +109,8 @@ module NotesActions
diff_discussion_html: diff_discussion_html(discussion),
discussion_html: discussion_html(discussion)
)
+
+ attrs[:discussion_line_code] = discussion.line_code if discussion.diff_discussion?
end
end
else
@@ -188,7 +191,7 @@ module NotesActions
end
def noteable
- @noteable ||= notes_finder.target
+ @noteable ||= notes_finder.target || render_404
end
def last_fetched_at
diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb
index 80ab681ed87..bc0948cd3fb 100644
--- a/app/controllers/confirmations_controller.rb
+++ b/app/controllers/confirmations_controller.rb
@@ -10,7 +10,7 @@ class ConfirmationsController < Devise::ConfirmationsController
users_almost_there_path
end
- def after_confirmation_path_for(_resource_name, resource)
+ def after_confirmation_path_for(resource_name, resource)
# incoming resource can either be a :user or an :email
if signed_in?(:user)
after_sign_in(resource)
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index 4bceb1d67a3..7d6fe6a0232 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -30,11 +30,11 @@ class JwtController < ApplicationController
render_unauthorized
end
end
- rescue Gitlab::Auth::MissingPersonalTokenError
- render_missing_personal_token
+ rescue Gitlab::Auth::MissingPersonalAccessTokenError
+ render_missing_personal_access_token
end
- def render_missing_personal_token
+ def render_missing_personal_access_token
render json: {
errors: [
{ code: 'UNAUTHORIZED',
diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb
index b02e64a132b..2443f529c7b 100644
--- a/app/controllers/oauth/applications_controller.rb
+++ b/app/controllers/oauth/applications_controller.rb
@@ -16,25 +16,18 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
end
def create
- @application = Doorkeeper::Application.new(application_params)
+ @application = Applications::CreateService.new(current_user, create_application_params).execute(request)
- @application.owner = current_user
+ if @application.persisted?
+ flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create])
- if @application.save
- redirect_to_oauth_application_page
+ redirect_to oauth_application_url(@application)
else
set_index_vars
render :index
end
end
- protected
-
- def redirect_to_oauth_application_page
- flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create])
- redirect_to oauth_application_url(@application)
- end
-
private
def verify_user_oauth_applications_enabled
@@ -61,4 +54,10 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
rescue_from ActiveRecord::RecordNotFound do |exception|
render "errors/not_found", layout: "errors", status: 404
end
+
+ def create_application_params
+ application_params.tap do |params|
+ params[:owner] = current_user
+ end
+ end
end
diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb
index 069e6a810f2..f0e5d2aa94e 100644
--- a/app/controllers/profiles/keys_controller.rb
+++ b/app/controllers/profiles/keys_controller.rb
@@ -11,10 +11,10 @@ class Profiles::KeysController < Profiles::ApplicationController
end
def create
- @key = Keys::CreateService.new(current_user, key_params).execute
+ @key = Keys::CreateService.new(current_user, key_params.merge(ip_address: request.remote_ip)).execute
if @key.persisted?
- redirect_to_profile_key_path
+ redirect_to profile_key_path(@key)
else
@keys = current_user.keys.select(&:persisted?)
render :index
@@ -50,12 +50,6 @@ class Profiles::KeysController < Profiles::ApplicationController
end
end
- protected
-
- def redirect_to_profile_key_path
- redirect_to profile_key_path(@key)
- end
-
private
def key_params
diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb
index 4146deefa89..6d9873e38df 100644
--- a/app/controllers/profiles/personal_access_tokens_controller.rb
+++ b/app/controllers/profiles/personal_access_tokens_controller.rb
@@ -39,7 +39,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
end
def set_index_vars
- @scopes = Gitlab::Auth.available_scopes
+ @scopes = Gitlab::Auth.available_scopes(current_user)
@inactive_personal_access_tokens = finder(state: 'inactive').execute
@active_personal_access_tokens = finder(state: 'active').execute.order(:expires_at)
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index 5d87037f012..dbf61a17724 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -24,16 +24,6 @@ class ProfilesController < Profiles::ApplicationController
end
end
- def reset_private_token
- Users::UpdateService.new(current_user, user: @user).execute! do |user|
- user.reset_authentication_token!
- end
-
- flash[:notice] = "Private token was successfully reset"
-
- redirect_to profile_account_path
- end
-
def reset_incoming_email_token
Users::UpdateService.new(current_user, user: @user).execute! do |user|
user.reset_incoming_email_token!
@@ -41,7 +31,7 @@ class ProfilesController < Profiles::ApplicationController
flash[:notice] = "Incoming email token was successfully reset"
- redirect_to profile_account_path
+ redirect_to profile_personal_access_tokens_path
end
def reset_rss_token
@@ -51,7 +41,7 @@ class ProfilesController < Profiles::ApplicationController
flash[:notice] = "RSS token was successfully reset"
- redirect_to profile_account_path
+ redirect_to profile_personal_access_tokens_path
end
def audit_log
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 7f03ce07dec..f28df83d5a5 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -15,6 +15,8 @@ class Projects::BranchesController < Projects::ApplicationController
respond_to do |format|
format.html do
@refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name))
+ @merged_branch_names =
+ repository.merged_branch_names(@branches.map(&:name))
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37429
Gitlab::GitalyClient.allow_n_plus_1_calls do
@max_commits = @branches.reduce(0) do |memo, branch|
diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb
index 0fd5635523f..9a56c9de858 100644
--- a/app/controllers/projects/clusters_controller.rb
+++ b/app/controllers/projects/clusters_controller.rb
@@ -30,11 +30,13 @@ class Projects::ClustersController < Projects::ApplicationController
end
def new_gcp
- @cluster = project.build_cluster
+ @cluster = Clusters::Cluster.new.tap do |cluster|
+ cluster.build_provider_gcp
+ end
end
def create
- @cluster = Ci::CreateClusterService
+ @cluster = Clusters::CreateService
.new(project, current_user, create_params)
.execute(token_in_session)
@@ -61,7 +63,7 @@ class Projects::ClustersController < Projects::ApplicationController
end
def update
- Ci::UpdateClusterService
+ Clusters::UpdateService
.new(project, current_user, update_params)
.execute(cluster)
@@ -91,19 +93,19 @@ class Projects::ClustersController < Projects::ApplicationController
def create_params
params.require(:cluster).permit(
- :gcp_project_id,
- :gcp_cluster_zone,
- :gcp_cluster_name,
- :gcp_cluster_size,
- :gcp_machine_type,
- :project_namespace,
- :enabled)
+ :enabled,
+ :name,
+ :provider_type,
+ provider_gcp_attributes: [
+ :gcp_project_id,
+ :zone,
+ :num_nodes,
+ :machine_type
+ ])
end
def update_params
- params.require(:cluster).permit(
- :project_namespace,
- :enabled)
+ params.require(:cluster).permit(:enabled)
end
def authorize_google_api
diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb
index 95d7a02e9e9..dd5e66f60e3 100644
--- a/app/controllers/projects/git_http_client_controller.rb
+++ b/app/controllers/projects/git_http_client_controller.rb
@@ -53,8 +53,8 @@ class Projects::GitHttpClientController < Projects::ApplicationController
send_challenges
render plain: "HTTP Basic: Access denied\n", status: 401
- rescue Gitlab::Auth::MissingPersonalTokenError
- render_missing_personal_token
+ rescue Gitlab::Auth::MissingPersonalAccessTokenError
+ render_missing_personal_access_token
end
def basic_auth_provided?
@@ -78,7 +78,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController
@project, @wiki, @redirected_path = Gitlab::RepoPath.parse("#{params[:namespace_id]}/#{params[:project_id]}")
end
- def render_missing_personal_token
+ def render_missing_personal_access_token
render plain: "HTTP Basic: Access denied\n" \
"You must use a personal access token with 'api' scope for Git over HTTP.\n" \
"You can generate one at #{profile_personal_access_tokens_url}",
diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb
index f59200d3b1f..dbc1c8bcc28 100644
--- a/app/controllers/projects/group_links_controller.rb
+++ b/app/controllers/projects/group_links_controller.rb
@@ -12,12 +12,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
if group
return render_404 unless can?(current_user, :read_group, group)
-
- project.project_group_links.create(
- group: group,
- group_access: params[:link_group_access],
- expires_at: params[:expires_at]
- )
+ Projects::GroupLinks::CreateService.new(project, current_user, group_link_create_params).execute(group)
else
flash[:alert] = 'Please select a group.'
end
@@ -32,7 +27,9 @@ class Projects::GroupLinksController < Projects::ApplicationController
end
def destroy
- project.project_group_links.find(params[:id]).destroy
+ group_link = project.project_group_links.find(params[:id])
+
+ ::Projects::GroupLinks::DestroyService.new(project, current_user).execute(group_link)
respond_to do |format|
format.html do
@@ -47,4 +44,8 @@ class Projects::GroupLinksController < Projects::ApplicationController
def group_link_params
params.require(:group_link).permit(:group_access, :expires_at)
end
+
+ def group_link_create_params
+ params.permit(:link_group_access, :expires_at)
+ end
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index b7a108a0ebd..d4e763aa5b8 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -16,7 +16,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :authorize_create_issue!, only: [:new, :create]
# Allow modify issue
- before_action :authorize_update_issue!, only: [:update, :move]
+ before_action :authorize_update_issuable!, only: [:edit, :update, :move]
# Allow create a new branch and empty WIP merge request from current issue
before_action :authorize_create_merge_request!, only: [:create_merge_request]
@@ -63,16 +63,8 @@ class Projects::IssuesController < Projects::ApplicationController
respond_with(@issue)
end
- def show
- @noteable = @issue
- @note = @project.notes.new(noteable: @issue)
-
- respond_to do |format|
- format.html
- format.json do
- render json: serializer.represent(@issue)
- end
- end
+ def edit
+ respond_with(@issue)
end
def discussions
@@ -116,21 +108,6 @@ class Projects::IssuesController < Projects::ApplicationController
end
end
- def update
- update_params = issue_params.merge(spammable_params)
-
- @issue = Issues::UpdateService.new(project, current_user, update_params).execute(issue)
-
- respond_to do |format|
- format.json do
- render_issue_json
- end
- end
-
- rescue ActiveRecord::StaleObjectError
- render_conflict_response
- end
-
def move
params.require(:move_to_project_id)
@@ -188,26 +165,6 @@ class Projects::IssuesController < Projects::ApplicationController
end
end
- def realtime_changes
- Gitlab::PollingInterval.set_header(response, interval: 3_000)
-
- response = {
- title: view_context.markdown_field(@issue, :title),
- title_text: @issue.title,
- description: view_context.markdown_field(@issue, :description),
- description_text: @issue.description,
- task_status: @issue.task_status
- }
-
- if @issue.edited?
- response[:updated_at] = @issue.updated_at
- response[:updated_by_name] = @issue.last_edited_by.name
- response[:updated_by_path] = user_path(@issue.last_edited_by)
- end
-
- render json: response
- end
-
def create_merge_request
result = ::MergeRequests::CreateFromIssueService.new(project, current_user, issue_iid: issue.iid).execute
@@ -223,7 +180,8 @@ class Projects::IssuesController < Projects::ApplicationController
def issue
return @issue if defined?(@issue)
# The Sortable default scope causes performance issues when used with find_by
- @noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take!
+ @issuable = @noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take!
+ @note = @project.notes.new(noteable: @issuable)
return render_404 unless can?(current_user, :read_issue, @issue)
@@ -238,14 +196,6 @@ class Projects::IssuesController < Projects::ApplicationController
project_issue_path(@project, @issue)
end
- def authorize_update_issue!
- render_404 unless can?(current_user, :update_issue, @issue)
- end
-
- def authorize_admin_issues!
- render_404 unless can?(current_user, :admin_issue, @project)
- end
-
def authorize_create_merge_request!
render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user)
end
@@ -297,4 +247,9 @@ class Projects::IssuesController < Projects::ApplicationController
def serializer
IssueSerializer.new(current_user: current_user, project: issue.project)
end
+
+ def update_service
+ update_params = issue_params.merge(spammable_params)
+ Issues::UpdateService.new(project, current_user, update_params)
+ end
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index c5204080333..17cac69e588 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -9,7 +9,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
skip_before_action :merge_request, only: [:index, :bulk_update]
skip_before_action :ensure_ref_fetched, only: [:index, :bulk_update]
- before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :remove_wip, :sort]
+ before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort]
before_action :authenticate_user!, only: [:assign_related_issues]
@@ -83,7 +83,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
format.json do
Gitlab::PollingInterval.set_header(response, interval: 10_000)
- render json: serializer.represent(@merge_request, basic: params[:basic])
+ render json: serializer.represent(@merge_request, serializer: params[:serializer])
end
format.patch do
@@ -256,14 +256,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
alias_method :issuable, :merge_request
alias_method :awardable, :merge_request
- def authorize_update_merge_request!
- return render_404 unless can?(current_user, :update_merge_request, @merge_request)
- end
-
- def authorize_admin_merge_request!
- return render_404 unless can?(current_user, :admin_merge_request, @merge_request)
- end
-
def validates_merge_request
# Show git not found page
# if there is no saved commits between source & target branch
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index c94384d2a1a..980bbf699b6 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -2,13 +2,13 @@ class Projects::MilestonesController < Projects::ApplicationController
include MilestoneActions
before_action :check_issuables_available!
- before_action :milestone, only: [:edit, :update, :destroy, :show, :merge_requests, :participants, :labels]
+ before_action :milestone, only: [:edit, :update, :destroy, :show, :merge_requests, :participants, :labels, :promote]
# Allow read any milestone
before_action :authorize_read_milestone!
# Allow admin milestone
- before_action :authorize_admin_milestone!, except: [:index, :show, :merge_requests, :participants, :labels]
+ before_action :authorize_admin_milestone!, except: [:index, :show, :merge_requests, :participants, :labels, :promote]
respond_to :html
@@ -69,6 +69,14 @@ class Projects::MilestonesController < Projects::ApplicationController
end
end
+ def promote
+ promoted_milestone = Milestones::PromoteService.new(project, current_user).execute(milestone)
+ flash[:notice] = "Milestone has been promoted to group milestone."
+ redirect_to group_milestone_path(project.group, promoted_milestone.iid)
+ rescue Milestones::PromoteService::PromoteMilestoneError => error
+ redirect_to milestone, alert: error.message
+ end
+
def destroy
return access_denied! unless can?(current_user, :admin_milestone, @project)
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index 8022547a6ad..4dd573c61f1 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -63,34 +63,34 @@ module CiStatusHelper
def ci_icon_for_status(status)
if detailed_status?(status)
- return custom_icon(status.icon)
+ return sprite_icon(status.icon)
end
icon_name =
case status
when 'success'
- 'icon_status_success'
+ 'status_success'
when 'success_with_warnings'
- 'icon_status_warning'
+ 'status_warning'
when 'failed'
- 'icon_status_failed'
+ 'status_failed'
when 'pending'
- 'icon_status_pending'
+ 'status_pending'
when 'running'
- 'icon_status_running'
+ 'status_running'
when 'play'
- 'icon_play'
+ 'play'
when 'created'
- 'icon_status_created'
+ 'status_created'
when 'skipped'
- 'icon_status_skipped'
+ 'status_skipped'
when 'manual'
- 'icon_status_manual'
+ 'status_manual'
else
- 'icon_status_canceled'
+ 'status_canceled'
end
- custom_icon(icon_name)
+ sprite_icon(icon_name, size: 16)
end
def pipeline_status_cache_key(pipeline_status)
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index d4a91e533c1..a77aa0ad2cc 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -71,11 +71,13 @@ module GitlabRoutingHelper
project_commit_url(entity.project, entity.sha, *args)
end
- def preview_markdown_path(project, *args)
+ def preview_markdown_path(parent, *args)
+ return group_preview_markdown_path(parent) if parent.is_a?(Group)
+
if @snippet.is_a?(PersonalSnippet)
preview_markdown_snippets_path
else
- preview_markdown_project_path(project, *args)
+ preview_markdown_project_path(parent, *args)
end
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index baa2d6e375e..85407e38532 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -33,15 +33,17 @@ module IssuablesHelper
end
def serialize_issuable(issuable)
- case issuable
- when Issue
- IssueSerializer.new(current_user: current_user, project: issuable.project).represent(issuable).to_json
- when MergeRequest
- MergeRequestSerializer
- .new(current_user: current_user, project: issuable.project)
- .represent(issuable)
- .to_json
- end
+ serializer_klass = case issuable
+ when Issue
+ IssueSerializer
+ when MergeRequest
+ MergeRequestSerializer
+ end
+
+ serializer_klass
+ .new(current_user: current_user, project: issuable.project)
+ .represent(issuable)
+ .to_json
end
def template_dropdown_tag(issuable, &block)
@@ -209,15 +211,13 @@ module IssuablesHelper
def issuable_initial_data(issuable)
data = {
- endpoint: project_issue_path(@project, issuable),
- canUpdate: can?(current_user, :update_issue, issuable),
- canDestroy: can?(current_user, :destroy_issue, issuable),
+ endpoint: issuable_path(issuable),
+ canUpdate: can?(current_user, :"update_#{issuable.to_ability_name}", issuable),
+ canDestroy: can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable),
issuableRef: issuable.to_reference,
- markdownPreviewPath: preview_markdown_path(@project),
+ markdownPreviewPath: preview_markdown_path(parent),
markdownDocsPath: help_page_path('user/markdown'),
issuableTemplates: issuable_templates(issuable),
- projectPath: ref_project.path,
- projectNamespace: ref_project.namespace.full_path,
initialTitleHtml: markdown_field(issuable, :title),
initialTitleText: issuable.title,
initialDescriptionHtml: markdown_field(issuable, :description),
@@ -225,6 +225,12 @@ module IssuablesHelper
initialTaskStatus: issuable.task_status
}
+ if parent.is_a?(Group)
+ data[:groupPath] = parent.path
+ else
+ data.merge!(projectPath: ref_project.path, projectNamespace: ref_project.namespace.full_path)
+ end
+
data.merge!(updated_at_by(issuable))
data.to_json
@@ -261,12 +267,7 @@ module IssuablesHelper
end
def issuable_path(issuable, *options)
- case issuable
- when Issue
- issue_path(issuable, *options)
- when MergeRequest
- merge_request_path(issuable, *options)
- end
+ polymorphic_path(issuable, *options)
end
def issuable_url(issuable, *options)
@@ -357,7 +358,8 @@ module IssuablesHelper
def issuable_sidebar_options(issuable, can_edit_issuable)
{
- endpoint: "#{issuable_json_path(issuable)}?basic=true",
+ endpoint: "#{issuable_json_path(issuable)}?serializer=sidebar",
+ toggleSubscriptionEndpoint: toggle_subscription_path(issuable),
moveIssueEndpoint: move_namespace_project_issue_path(namespace_id: issuable.project.namespace.to_param, project_id: issuable.project, id: issuable),
projectsAutocompleteEndpoint: autocomplete_projects_path(project_id: @project.id),
editable: can_edit_issuable,
@@ -366,4 +368,8 @@ module IssuablesHelper
fullPath: @project.full_path
}
end
+
+ def parent
+ @project || @group
+ end
end
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 5a74511afa7..8ada746b244 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -19,11 +19,7 @@ module NavHelper
end
elsif current_path?('jobs#show')
%w[page-gutter build-sidebar right-sidebar-expanded]
- elsif current_path?('wikis#show') ||
- current_path?('wikis#edit') ||
- current_path?('wikis#update') ||
- current_path?('wikis#history') ||
- current_path?('wikis#git_access')
+ elsif current_controller?('wikis') && current_action?('show', 'create', 'edit', 'update', 'history', 'git_access')
%w[page-gutter wiki-sidebar right-sidebar-expanded]
else
[]
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index d085c1a0e57..f48d47953e4 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -110,7 +110,15 @@ module ProjectsHelper
def remove_fork_project_message(project)
_("You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?") %
- { forked_from_project: @project.forked_from_project.name_with_namespace }
+ { forked_from_project: fork_source_name(project) }
+ end
+
+ def fork_source_name(project)
+ if @project.fork_source
+ @project.fork_source.full_name
+ else
+ @project.fork_network&.deleted_root_project_name
+ end
end
def project_nav_tabs
@@ -140,8 +148,8 @@ module ProjectsHelper
def can_change_visibility_level?(project, current_user)
return false unless can?(current_user, :change_visibility_level, project)
- if project.forked?
- project.forked_from_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE
+ if project.fork_source
+ project.fork_source.visibility_level > Gitlab::VisibilityLevel::PRIVATE
else
true
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index f266e7db6da..5e16badabec 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -420,7 +420,7 @@ class ApplicationSetting < ActiveRecord::Base
# the enabling/disabling is `performance_bar_allowed_group_id`
# - If `enable` is false, we set `performance_bar_allowed_group_id` to `nil`
def performance_bar_enabled=(enable)
- return if enable
+ return if Gitlab::Utils.to_boolean(enable)
self.performance_bar_allowed_group_id = nil
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index cf3ce3c9e54..ca65e81f27a 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -249,9 +249,7 @@ module Ci
end
def commit
- @commit ||= project.commit(sha)
- rescue
- nil
+ @commit ||= project.commit_by(oid: sha)
end
def branch?
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
new file mode 100644
index 00000000000..955dba51745
--- /dev/null
+++ b/app/models/clusters/cluster.rb
@@ -0,0 +1,73 @@
+module Clusters
+ class Cluster < ActiveRecord::Base
+ include Presentable
+
+ self.table_name = 'clusters'
+
+ belongs_to :user
+
+ has_many :cluster_projects, class_name: 'Clusters::Project'
+ has_many :projects, through: :cluster_projects, class_name: '::Project'
+
+ # we force autosave to happen when we save `Cluster` model
+ has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp', autosave: true
+
+ # We have to ":destroy" it today to ensure that we clean also the Kubernetes Integration
+ has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes', autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+
+ accepts_nested_attributes_for :provider_gcp, update_only: true
+ accepts_nested_attributes_for :platform_kubernetes, update_only: true
+
+ validates :name, cluster_name: true
+ validate :restrict_modification, on: :update
+
+ # TODO: Move back this into Clusters::Platforms::Kubernetes in 10.3
+ # We need callback here because `enabled` belongs to Clusters::Cluster
+ # Callbacks in Clusters::Platforms::Kubernetes will not be called after update
+ after_save :update_kubernetes_integration!
+
+ delegate :status, to: :provider, allow_nil: true
+ delegate :status_reason, to: :provider, allow_nil: true
+ delegate :status_name, to: :provider, allow_nil: true
+ delegate :on_creation?, to: :provider, allow_nil: true
+ delegate :update_kubernetes_integration!, to: :platform, allow_nil: true
+
+ enum platform_type: {
+ kubernetes: 1
+ }
+
+ enum provider_type: {
+ user: 0,
+ gcp: 1
+ }
+
+ scope :enabled, -> { where(enabled: true) }
+ scope :disabled, -> { where(enabled: false) }
+
+ def provider
+ return provider_gcp if gcp?
+ end
+
+ def platform
+ return platform_kubernetes if kubernetes?
+ end
+
+ def first_project
+ return @first_project if defined?(@first_project)
+
+ @first_project = projects.first
+ end
+ alias_method :project, :first_project
+
+ private
+
+ def restrict_modification
+ if provider&.on_creation?
+ errors.add(:base, "cannot modify during creation")
+ return false
+ end
+
+ true
+ end
+ end
+end
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
new file mode 100644
index 00000000000..b11701797c2
--- /dev/null
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -0,0 +1,101 @@
+module Clusters
+ module Platforms
+ class Kubernetes < ActiveRecord::Base
+ self.table_name = 'cluster_platforms_kubernetes'
+
+ belongs_to :cluster, inverse_of: :platform_kubernetes, class_name: 'Clusters::Cluster'
+
+ attr_encrypted :password,
+ mode: :per_attribute_iv,
+ key: Gitlab::Application.secrets.db_key_base,
+ algorithm: 'aes-256-cbc'
+
+ attr_encrypted :token,
+ mode: :per_attribute_iv,
+ key: Gitlab::Application.secrets.db_key_base,
+ algorithm: 'aes-256-cbc'
+
+ before_validation :enforce_namespace_to_lower_case
+
+ validates :namespace,
+ allow_blank: true,
+ length: 1..63,
+ format: {
+ with: Gitlab::Regex.kubernetes_namespace_regex,
+ message: Gitlab::Regex.kubernetes_namespace_regex_message
+ }
+
+ # We expect to be `active?` only when enabled and cluster is created (the api_url is assigned)
+ validates :api_url, url: true, presence: true
+ validates :token, presence: true
+
+ # TODO: Glue code till we migrate Kubernetes Integration into Platforms::Kubernetes
+ after_destroy :destroy_kubernetes_integration!
+
+ alias_attribute :ca_pem, :ca_cert
+
+ delegate :project, to: :cluster, allow_nil: true
+ delegate :enabled?, to: :cluster, allow_nil: true
+
+ class << self
+ def namespace_for_project(project)
+ "#{project.path}-#{project.id}"
+ end
+ end
+
+ def actual_namespace
+ if namespace.present?
+ namespace
+ else
+ default_namespace
+ end
+ end
+
+ def default_namespace
+ self.class.namespace_for_project(project) if project
+ end
+
+ def update_kubernetes_integration!
+ raise 'Kubernetes service already configured' unless manages_kubernetes_service?
+
+ # This is neccesary, otheriwse enabled? returns true even though cluster updated with enabled: false
+ cluster.reload
+
+ ensure_kubernetes_service&.update!(
+ active: enabled?,
+ api_url: api_url,
+ namespace: namespace,
+ token: token,
+ ca_pem: ca_cert
+ )
+ end
+
+ private
+
+ def enforce_namespace_to_lower_case
+ self.namespace = self.namespace&.downcase
+ end
+
+ # TODO: glue code till we migrate Kubernetes Service into Platforms::Kubernetes class
+ def manages_kubernetes_service?
+ return true unless kubernetes_service&.active?
+
+ kubernetes_service.api_url == api_url
+ end
+
+ def destroy_kubernetes_integration!
+ return unless manages_kubernetes_service?
+
+ kubernetes_service&.destroy!
+ end
+
+ def kubernetes_service
+ @kubernetes_service ||= project&.kubernetes_service
+ end
+
+ def ensure_kubernetes_service
+ @kubernetes_service ||= kubernetes_service || project&.build_kubernetes_service
+ end
+ end
+ end
+end
diff --git a/app/models/clusters/project.rb b/app/models/clusters/project.rb
new file mode 100644
index 00000000000..eeb734b20b8
--- /dev/null
+++ b/app/models/clusters/project.rb
@@ -0,0 +1,8 @@
+module Clusters
+ class Project < ActiveRecord::Base
+ self.table_name = 'cluster_projects'
+
+ belongs_to :cluster, class_name: 'Clusters::Cluster'
+ belongs_to :project, class_name: '::Project'
+ end
+end
diff --git a/app/models/clusters/providers/gcp.rb b/app/models/clusters/providers/gcp.rb
new file mode 100644
index 00000000000..7700ba86f1a
--- /dev/null
+++ b/app/models/clusters/providers/gcp.rb
@@ -0,0 +1,79 @@
+module Clusters
+ module Providers
+ class Gcp < ActiveRecord::Base
+ self.table_name = 'cluster_providers_gcp'
+
+ belongs_to :cluster, inverse_of: :provider_gcp, class_name: 'Clusters::Cluster'
+
+ default_value_for :zone, 'us-central1-a'
+ default_value_for :num_nodes, 3
+ default_value_for :machine_type, 'n1-standard-4'
+
+ attr_encrypted :access_token,
+ mode: :per_attribute_iv,
+ key: Gitlab::Application.secrets.db_key_base,
+ algorithm: 'aes-256-cbc'
+
+ validates :gcp_project_id,
+ length: 1..63,
+ format: {
+ with: Gitlab::Regex.kubernetes_namespace_regex,
+ message: Gitlab::Regex.kubernetes_namespace_regex_message
+ }
+
+ validates :zone, presence: true
+
+ validates :num_nodes,
+ presence: true,
+ numericality: {
+ only_integer: true,
+ greater_than: 0
+ }
+
+ state_machine :status, initial: :scheduled do
+ state :scheduled, value: 1
+ state :creating, value: 2
+ state :created, value: 3
+ state :errored, value: 4
+
+ event :make_creating do
+ transition any - [:creating] => :creating
+ end
+
+ event :make_created do
+ transition any - [:created] => :created
+ end
+
+ event :make_errored do
+ transition any - [:errored] => :errored
+ end
+
+ before_transition any => [:errored, :created] do |provider|
+ provider.access_token = nil
+ provider.operation_id = nil
+ end
+
+ before_transition any => [:creating] do |provider, transition|
+ operation_id = transition.args.first
+ raise 'operation_id is required' unless operation_id
+ provider.operation_id = operation_id
+ end
+
+ before_transition any => [:errored] do |provider, transition|
+ status_reason = transition.args.first
+ provider.status_reason = status_reason if status_reason
+ end
+ end
+
+ def on_creation?
+ scheduled? || creating?
+ end
+
+ def api_client
+ return unless access_token
+
+ @api_client ||= GoogleApi::CloudPlatform::Client.new(access_token, nil)
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 9417033d1f6..98776eab424 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -49,7 +49,8 @@ module CacheMarkdownField
# Always include a project key, or Banzai complains
project = self.project if self.respond_to?(:project)
- context = cached_markdown_fields[field].merge(project: project)
+ group = self.group if self.respond_to?(:group)
+ context = cached_markdown_fields[field].merge(project: project, group: group)
# Banzai is less strict about authors, so don't always have an author key
context[:author] = self.author if self.respond_to?(:author)
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 27f4dedffd3..a928b9d6367 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -14,7 +14,6 @@ module Issuable
include StripAttribute
include Awardable
include Taskable
- include TimeTrackable
include Importable
include Editable
include AfterCommitQueue
@@ -95,8 +94,6 @@ module Issuable
strip_attributes :title
- acts_as_paranoid
-
after_save :record_metrics, unless: :imported?
# We want to use optimistic lock for cases when only title or description are involved
diff --git a/app/models/concerns/repository_mirroring.rb b/app/models/concerns/repository_mirroring.rb
deleted file mode 100644
index f6aba91bc4c..00000000000
--- a/app/models/concerns/repository_mirroring.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-module RepositoryMirroring
- IMPORT_HEAD_REFS = '+refs/heads/*:refs/heads/*'.freeze
- IMPORT_TAG_REFS = '+refs/tags/*:refs/tags/*'.freeze
-
- def set_remote_as_mirror(name)
- # This is used to define repository as equivalent as "git clone --mirror"
- raw_repository.rugged.config["remote.#{name}.fetch"] = 'refs/*:refs/*'
- raw_repository.rugged.config["remote.#{name}.mirror"] = true
- raw_repository.rugged.config["remote.#{name}.prune"] = true
- end
-
- def set_import_remote_as_mirror(remote_name)
- # Add first fetch with Rugged so it does not create its own.
- raw_repository.rugged.config["remote.#{remote_name}.fetch"] = IMPORT_HEAD_REFS
-
- add_remote_fetch_config(remote_name, IMPORT_TAG_REFS)
-
- raw_repository.rugged.config["remote.#{remote_name}.mirror"] = true
- raw_repository.rugged.config["remote.#{remote_name}.prune"] = true
- end
-
- def add_remote_fetch_config(remote_name, refspec)
- run_git(%W[config --add remote.#{remote_name}.fetch #{refspec}])
- end
-
- def fetch_mirror(remote, url)
- add_remote(remote, url)
- set_remote_as_mirror(remote)
- fetch_remote(remote, forced: true)
- remove_remote(remote)
- end
-end
diff --git a/app/models/concerns/subscribable.rb b/app/models/concerns/subscribable.rb
index 274b38a7708..f478c8ede18 100644
--- a/app/models/concerns/subscribable.rb
+++ b/app/models/concerns/subscribable.rb
@@ -13,6 +13,8 @@ module Subscribable
end
def subscribed?(user, project = nil)
+ return false unless user
+
if subscription = subscriptions.find_by(user: user, project: project)
subscription.subscribed
else
diff --git a/app/models/email.rb b/app/models/email.rb
index 384f38f2db7..2da8b050149 100644
--- a/app/models/email.rb
+++ b/app/models/email.rb
@@ -14,6 +14,8 @@ class Email < ActiveRecord::Base
devise :confirmable
self.reconfirmable = false # currently email can't be changed, no need to reconfirm
+ delegate :username, to: :user
+
def email=(value)
write_attribute(:email, value.downcase.strip)
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index e613d21add6..21a028e351c 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -30,7 +30,6 @@ class Environment < ActiveRecord::Base
message: Gitlab::Regex.environment_slug_regex_message }
validates :external_url,
- uniqueness: { scope: :project_id },
length: { maximum: 255 },
allow_nil: true,
addressable_url: true
@@ -110,7 +109,7 @@ class Environment < ActiveRecord::Base
end
def ref_path
- "refs/#{Repository::REF_ENVIRONMENTS}/#{generate_slug}"
+ "refs/#{Repository::REF_ENVIRONMENTS}/#{slug}"
end
def formatted_external_url
@@ -164,6 +163,10 @@ class Environment < ActiveRecord::Base
end
end
+ def slug
+ super.presence || generate_slug
+ end
+
# An environment name is not necessarily suitable for use in URLs, DNS
# or other third-party contexts, so provide a slugified version. A slug has
# the following properties:
diff --git a/app/models/epic.rb b/app/models/epic.rb
new file mode 100644
index 00000000000..62898a02e2d
--- /dev/null
+++ b/app/models/epic.rb
@@ -0,0 +1,7 @@
+# Placeholder class for model that is implemented in EE
+# It will reserve (ee#3853) '&' as a reference prefix, but the table does not exists in CE
+class Epic < ActiveRecord::Base
+ # TODO: this will be implemented as part of #3853
+ def to_reference
+ end
+end
diff --git a/app/models/fork_network.rb b/app/models/fork_network.rb
index 218e37a5312..7f1728e8c77 100644
--- a/app/models/fork_network.rb
+++ b/app/models/fork_network.rb
@@ -12,4 +12,8 @@ class ForkNetwork < ActiveRecord::Base
def find_forks_in(other_projects)
projects.where(id: other_projects)
end
+
+ def merge_requests
+ MergeRequest.where(target_project: projects)
+ end
end
diff --git a/app/models/gcp/cluster.rb b/app/models/gcp/cluster.rb
deleted file mode 100644
index 162a690c0e3..00000000000
--- a/app/models/gcp/cluster.rb
+++ /dev/null
@@ -1,116 +0,0 @@
-module Gcp
- class Cluster < ActiveRecord::Base
- extend Gitlab::Gcp::Model
- include Presentable
-
- belongs_to :project, inverse_of: :cluster
- belongs_to :user
- belongs_to :service
-
- scope :enabled, -> { where(enabled: true) }
- scope :disabled, -> { where(enabled: false) }
-
- default_value_for :gcp_cluster_zone, 'us-central1-a'
- default_value_for :gcp_cluster_size, 3
- default_value_for :gcp_machine_type, 'n1-standard-4'
-
- attr_encrypted :password,
- mode: :per_attribute_iv,
- key: Gitlab::Application.secrets.db_key_base,
- algorithm: 'aes-256-cbc'
-
- attr_encrypted :kubernetes_token,
- mode: :per_attribute_iv,
- key: Gitlab::Application.secrets.db_key_base,
- algorithm: 'aes-256-cbc'
-
- attr_encrypted :gcp_token,
- mode: :per_attribute_iv,
- key: Gitlab::Application.secrets.db_key_base,
- algorithm: 'aes-256-cbc'
-
- validates :gcp_project_id,
- length: 1..63,
- format: {
- with: Gitlab::Regex.kubernetes_namespace_regex,
- message: Gitlab::Regex.kubernetes_namespace_regex_message
- }
-
- validates :gcp_cluster_name,
- length: 1..63,
- format: {
- with: Gitlab::Regex.kubernetes_namespace_regex,
- message: Gitlab::Regex.kubernetes_namespace_regex_message
- }
-
- validates :gcp_cluster_zone, presence: true
-
- validates :gcp_cluster_size,
- presence: true,
- numericality: {
- only_integer: true,
- greater_than: 0
- }
-
- validates :project_namespace,
- allow_blank: true,
- length: 1..63,
- format: {
- with: Gitlab::Regex.kubernetes_namespace_regex,
- message: Gitlab::Regex.kubernetes_namespace_regex_message
- }
-
- # if we do not do status transition we prevent change
- validate :restrict_modification, on: :update, unless: :status_changed?
-
- state_machine :status, initial: :scheduled do
- state :scheduled, value: 1
- state :creating, value: 2
- state :created, value: 3
- state :errored, value: 4
-
- event :make_creating do
- transition any - [:creating] => :creating
- end
-
- event :make_created do
- transition any - [:created] => :created
- end
-
- event :make_errored do
- transition any - [:errored] => :errored
- end
-
- before_transition any => [:errored, :created] do |cluster|
- cluster.gcp_token = nil
- cluster.gcp_operation_id = nil
- end
-
- before_transition any => [:errored] do |cluster, transition|
- status_reason = transition.args.first
- cluster.status_reason = status_reason if status_reason
- end
- end
-
- def project_namespace_placeholder
- "#{project.path}-#{project.id}"
- end
-
- def on_creation?
- scheduled? || creating?
- end
-
- def api_url
- 'https://' + endpoint if endpoint
- end
-
- def restrict_modification
- if on_creation?
- errors.add(:base, "cannot modify during creation")
- return false
- end
-
- true
- end
- end
-end
diff --git a/app/models/group.rb b/app/models/group.rb
index 07fb62bb249..c660de7fcb6 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -42,6 +42,7 @@ class Group < Namespace
after_create :post_create_hook
after_destroy :post_destroy_hook
after_save :update_two_factor_requirement
+ after_update :path_changed_hook, if: :path_changed?
class << self
def supports_nested_groups?
@@ -180,6 +181,12 @@ class Group < Namespace
add_user(user, :owner, current_user: current_user)
end
+ def member?(user, min_access_level = Gitlab::Access::GUEST)
+ return false unless user
+
+ max_member_access_for_user(user) >= min_access_level
+ end
+
def has_owner?(user)
return false unless user
@@ -289,6 +296,12 @@ class Group < Namespace
list_of_ids.reverse.map { |group| variables[group.id] }.compact.flatten
end
+ def full_path_was
+ return path_was unless has_parent?
+
+ "#{parent.full_path}/#{path_was}"
+ end
+
private
def update_two_factor_requirement
@@ -297,6 +310,10 @@ class Group < Namespace
users.find_each(&:update_two_factor_requirement)
end
+ def path_changed_hook
+ system_hook_service.execute_hooks_for(self, :rename)
+ end
+
def visibility_level_allowed_by_parent
return if visibility_level_allowed_by_parent?
diff --git a/app/models/identity.rb b/app/models/identity.rb
index 920a25932b4..ac8094b610e 100644
--- a/app/models/identity.rb
+++ b/app/models/identity.rb
@@ -7,7 +7,10 @@ class Identity < ActiveRecord::Base
validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider }
validates :user_id, uniqueness: { scope: :provider }
- scope :with_extern_uid, ->(provider, extern_uid) { where(extern_uid: extern_uid, provider: provider) }
+ scope :with_extern_uid, ->(provider, extern_uid) do
+ extern_uid = Gitlab::LDAP::Person.normalize_dn(extern_uid) if provider.starts_with?('ldap')
+ where(extern_uid: extern_uid, provider: provider)
+ end
def ldap?
provider.starts_with?('ldap')
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 36e4108b9d6..fc590f9257e 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -10,6 +10,7 @@ class Issue < ActiveRecord::Base
include FasterCacheKeys
include RelativePositioning
include CreatedAtFilterable
+ include TimeTrackable
DueDateStruct = Struct.new(:title, :name).freeze
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
@@ -74,6 +75,8 @@ class Issue < ActiveRecord::Base
end
end
+ acts_as_paranoid
+
def self.reference_prefix
'#'
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index c3fae16d109..3133dc9e7eb 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -6,6 +6,7 @@ class MergeRequest < ActiveRecord::Base
include Sortable
include IgnorableColumn
include CreatedAtFilterable
+ include TimeTrackable
ignore_column :locked_at
@@ -119,6 +120,8 @@ class MergeRequest < ActiveRecord::Base
after_save :keep_around_commit
+ acts_as_paranoid
+
def self.reference_prefix
'!'
end
@@ -396,6 +399,10 @@ class MergeRequest < ActiveRecord::Base
end
def merge_ongoing?
+ # While the MergeRequest is locked, it should present itself as 'merge ongoing'.
+ # The unlocking process is handled by StuckMergeJobsWorker scheduled in Cron.
+ return true if locked?
+
!!merge_jid && !merged? && Gitlab::SidekiqStatus.running?(merge_jid)
end
@@ -874,7 +881,7 @@ class MergeRequest < ActiveRecord::Base
#
def all_commit_shas
if persisted?
- column_shas = MergeRequestDiffCommit.where(merge_request_diff: merge_request_diffs).pluck('DISTINCT(sha)')
+ column_shas = MergeRequestDiffCommit.where(merge_request_diff: merge_request_diffs).limit(10_000).pluck('sha')
serialised_shas = merge_request_diffs.where.not(st_commits: nil).flat_map(&:commit_shas)
(column_shas + serialised_shas).uniq
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index faf0b95f842..1eda0f9cbbd 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -48,6 +48,10 @@ class MergeRequestDiff < ActiveRecord::Base
# Collect information about commits and diff from repository
# and save it to the database as serialized data
def save_git_content
+ MergeRequest
+ .where('id = ? AND COALESCE(latest_merge_request_diff_id, 0) < ?', self.merge_request_id, self.id)
+ .update_all(latest_merge_request_diff_id: self.id)
+
ensure_commit_shas
save_commits
save_diffs
diff --git a/app/models/merge_request_diff_commit.rb b/app/models/merge_request_diff_commit.rb
index 670b26d4ca3..b75387e236e 100644
--- a/app/models/merge_request_diff_commit.rb
+++ b/app/models/merge_request_diff_commit.rb
@@ -17,7 +17,9 @@ class MergeRequestDiffCommit < ActiveRecord::Base
commit_hash.merge(
merge_request_diff_id: merge_request_diff_id,
relative_order: index,
- sha: sha_attribute.type_cast_for_database(sha)
+ sha: sha_attribute.type_cast_for_database(sha),
+ authored_date: Gitlab::Database.sanitize_timestamp(commit_hash[:authored_date]),
+ committed_date: Gitlab::Database.sanitize_timestamp(commit_hash[:committed_date])
)
end
diff --git a/app/models/note.rb b/app/models/note.rb
index 8939e590ef1..f9676361072 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -69,7 +69,7 @@ class Note < ActiveRecord::Base
delegate :title, to: :noteable, allow_nil: true
validates :note, presence: true
- validates :project, presence: true, unless: :for_personal_snippet?
+ validates :project, presence: true, if: :for_project_noteable?
# Attachments are deprecated and are handled by Markdown uploader
validates :attachment, file_size: { maximum: :max_attachment_size }
@@ -114,7 +114,7 @@ class Note < ActiveRecord::Base
after_initialize :ensure_discussion_id
before_validation :nullify_blank_type, :nullify_blank_line_code
before_validation :set_discussion_id, on: :create
- after_save :keep_around_commit, unless: :for_personal_snippet?
+ after_save :keep_around_commit, if: :for_project_noteable?
after_save :expire_etag_cache
after_destroy :expire_etag_cache
@@ -208,6 +208,10 @@ class Note < ActiveRecord::Base
noteable.is_a?(PersonalSnippet)
end
+ def for_project_noteable?
+ !for_personal_snippet?
+ end
+
def skip_project_check?
for_personal_snippet?
end
diff --git a/app/models/oauth_access_token.rb b/app/models/oauth_access_token.rb
index f89e60ad9f4..e8595b13d6d 100644
--- a/app/models/oauth_access_token.rb
+++ b/app/models/oauth_access_token.rb
@@ -2,5 +2,13 @@ class OauthAccessToken < Doorkeeper::AccessToken
belongs_to :resource_owner, class_name: 'User'
belongs_to :application, class_name: 'Doorkeeper::Application'
- alias_method :user, :resource_owner
+ alias_attribute :user, :resource_owner
+
+ def scopes=(value)
+ if value.is_a?(Array)
+ super(Doorkeeper::OAuth::Scopes.from_array(value).to_s)
+ else
+ super
+ end
+ end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 4689b588906..b91b6a2220f 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -26,7 +26,15 @@ class Project < ActiveRecord::Base
NUMBER_OF_PERMITTED_BOARDS = 1
UNKNOWN_IMPORT_URL = 'http://unknown.git'.freeze
- LATEST_STORAGE_VERSION = 1
+ # Hashed Storage versions handle rolling out new storage to project and dependents models:
+ # nil: legacy
+ # 1: repository
+ # 2: attachments
+ LATEST_STORAGE_VERSION = 2
+ HASHED_STORAGE_FEATURES = {
+ repository: 1,
+ attachments: 2
+ }.freeze
cache_markdown_field :description, pipeline: :description
@@ -120,6 +128,7 @@ class Project < ActiveRecord::Base
has_one :mock_deployment_service
has_one :mock_monitoring_service
has_one :microsoft_teams_service
+ has_one :packagist_service
# TODO: replace these relations with the fork network versions
has_one :forked_project_link, foreign_key: "forked_to_project_id"
@@ -177,7 +186,9 @@ class Project < ActiveRecord::Base
has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true
has_one :project_feature, inverse_of: :project
has_one :statistics, class_name: 'ProjectStatistics'
- has_one :cluster, class_name: 'Gcp::Cluster', inverse_of: :project
+
+ has_one :cluster_project, class_name: 'Clusters::Project'
+ has_one :cluster, through: :cluster_project, class_name: 'Clusters::Cluster'
# Container repositories need to remove data from the container registry,
# which is not managed by the DB. Hence we're still using dependent: :destroy
@@ -540,6 +551,10 @@ class Project < ActiveRecord::Base
repository.commit(ref)
end
+ def commit_by(oid:)
+ repository.commit_by(oid: oid)
+ end
+
# ref can't be HEAD, can only be branch/tag name or SHA
def latest_successful_builds_for(ref = default_branch)
latest_pipeline = pipelines.latest_successful_for(ref)
@@ -553,7 +568,7 @@ class Project < ActiveRecord::Base
def merge_base_commit(first_commit_id, second_commit_id)
sha = repository.merge_base(first_commit_id, second_commit_id)
- repository.commit(sha) if sha
+ commit_by(oid: sha) if sha
end
def saved?
@@ -1027,6 +1042,10 @@ class Project < ActiveRecord::Base
!(forked_project_link.nil? || forked_project_link.forked_from_project.nil?)
end
+ def fork_source
+ forked_from_project || fork_network&.root_project
+ end
+
def personal?
!group
end
@@ -1079,6 +1098,7 @@ class Project < ActiveRecord::Base
def hook_attrs(backward: true)
attrs = {
+ id: id,
name: name,
description: description,
web_url: web_url,
@@ -1390,6 +1410,19 @@ class Project < ActiveRecord::Base
end
end
+ def after_rename_repo
+ path_before_change = previous_changes['path'].first
+
+ # We need to check if project had been rolled out to move resource to hashed storage or not and decide
+ # if we need execute any take action or no-op.
+
+ unless hashed_storage?(:attachments)
+ Gitlab::UploadsTransfer.new.rename_project(path_before_change, self.path, namespace.full_path)
+ end
+
+ Gitlab::PagesTransfer.new.rename_project(path_before_change, self.path, namespace.full_path)
+ end
+
def rename_repo_notify!
send_move_instructions(full_path_was)
expires_full_path_cache
@@ -1400,13 +1433,6 @@ class Project < ActiveRecord::Base
reload_repository!
end
- def after_rename_repo
- path_before_change = previous_changes['path'].first
-
- Gitlab::UploadsTransfer.new.rename_project(path_before_change, self.path, namespace.full_path)
- Gitlab::PagesTransfer.new.rename_project(path_before_change, self.path, namespace.full_path)
- end
-
def running_or_pending_build_count(force: false)
Rails.cache.fetch(['projects', id, 'running_or_pending_build_count'], force: force) do
builds.running_or_pending.count(:all)
@@ -1468,7 +1494,8 @@ class Project < ActiveRecord::Base
{ key: 'CI_PROJECT_PATH', value: full_path, public: true },
{ key: 'CI_PROJECT_PATH_SLUG', value: full_path_slug, public: true },
{ key: 'CI_PROJECT_NAMESPACE', value: namespace.full_path, public: true },
- { key: 'CI_PROJECT_URL', value: web_url, public: true }
+ { key: 'CI_PROJECT_URL', value: web_url, public: true },
+ { key: 'CI_PROJECT_VISIBILITY', value: Gitlab::VisibilityLevel.string_level(visibility_level), public: true }
]
end
@@ -1596,8 +1623,13 @@ class Project < ActiveRecord::Base
[nil, 0].include?(self.storage_version)
end
- def hashed_storage?
- self.storage_version && self.storage_version >= 1
+ # Check if Hashed Storage is enabled for the project with at least informed feature rolled out
+ #
+ # @param [Symbol] feature that needs to be rolled out for the project (:repository, :attachments)
+ def hashed_storage?(feature)
+ raise ArgumentError, "Invalid feature" unless HASHED_STORAGE_FEATURES.include?(feature)
+
+ self.storage_version && self.storage_version >= HASHED_STORAGE_FEATURES[feature]
end
def renamed?
@@ -1633,7 +1665,7 @@ class Project < ActiveRecord::Base
end
def migrate_to_hashed_storage!
- return if hashed_storage?
+ return if hashed_storage?(:repository)
update!(repository_read_only: true)
@@ -1654,11 +1686,15 @@ class Project < ActiveRecord::Base
Gitlab::GlRepository.gl_repository(self, is_wiki)
end
+ def reference_counter(wiki: false)
+ Gitlab::ReferenceCounter.new(gl_repository(is_wiki: wiki))
+ end
+
private
def storage
@storage ||=
- if hashed_storage?
+ if hashed_storage?(:repository)
Storage::HashedProject.new(self)
else
Storage::LegacyProject.new(self)
@@ -1672,11 +1708,11 @@ class Project < ActiveRecord::Base
end
def repo_reference_count
- Gitlab::ReferenceCounter.new(gl_repository(is_wiki: false)).value
+ reference_counter.value
end
def wiki_reference_count
- Gitlab::ReferenceCounter.new(gl_repository(is_wiki: true)).value
+ reference_counter(wiki: true).value
end
def check_repository_absence!
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
index 8ba07173c74..5c0b3338a62 100644
--- a/app/models/project_services/kubernetes_service.rb
+++ b/app/models/project_services/kubernetes_service.rb
@@ -153,7 +153,10 @@ class KubernetesService < DeploymentService
end
def default_namespace
- "#{project.path}-#{project.id}" if project.present?
+ return unless project
+
+ slug = "#{project.path}-#{project.id}".downcase
+ slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '')
end
def build_kubeclient!(api_path: 'api', api_version: 'v1')
diff --git a/app/models/project_services/packagist_service.rb b/app/models/project_services/packagist_service.rb
new file mode 100644
index 00000000000..f68a0c1a3c3
--- /dev/null
+++ b/app/models/project_services/packagist_service.rb
@@ -0,0 +1,65 @@
+class PackagistService < Service
+ include HTTParty
+
+ prop_accessor :username, :token, :server
+
+ validates :username, presence: true, if: :activated?
+ validates :token, presence: true, if: :activated?
+
+ default_value_for :push_events, true
+ default_value_for :tag_push_events, true
+
+ after_save :compose_service_hook, if: :activated?
+
+ def title
+ 'Packagist'
+ end
+
+ def description
+ 'Update your project on Packagist, the main Composer repository'
+ end
+
+ def self.to_param
+ 'packagist'
+ end
+
+ def fields
+ [
+ { type: 'text', name: 'username', placeholder: '', required: true },
+ { type: 'text', name: 'token', placeholder: '', required: true },
+ { type: 'text', name: 'server', placeholder: 'https://packagist.org', required: false }
+ ]
+ end
+
+ def self.supported_events
+ %w(push merge_request tag_push)
+ end
+
+ def execute(data)
+ return unless supported_events.include?(data[:object_kind])
+
+ service_hook.execute(data)
+ end
+
+ def test(data)
+ begin
+ result = execute(data)
+ return { success: false, result: result[:message] } if result[:http_status] != 202
+ rescue StandardError => error
+ return { success: false, result: error }
+ end
+
+ { success: true, result: result[:message] }
+ end
+
+ def compose_service_hook
+ hook = service_hook || build_service_hook
+ hook.url = hook_url
+ hook.save
+ end
+
+ def hook_url
+ base_url = server.present? ? server : 'https://packagist.org'
+ "#{base_url}/api/update-package?username=#{username}&apiToken=#{token}"
+ end
+end
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index bb7be29ef66..43de6809178 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -135,7 +135,7 @@ class ProjectWiki
end
def repository
- @repository ||= Repository.new(full_path, @project, disk_path: disk_path)
+ @repository ||= Repository.new(full_path, @project, disk_path: disk_path, is_wiki: true)
end
def default_branch
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 7497b69f674..69cddb36b2e 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -15,9 +15,8 @@ class Repository
].freeze
include Gitlab::ShellAdapter
- include RepositoryMirroring
- attr_accessor :full_path, :disk_path, :project
+ attr_accessor :full_path, :disk_path, :project, :is_wiki
delegate :ref_name_for_sha, to: :raw_repository
@@ -72,10 +71,12 @@ class Repository
end
end
- def initialize(full_path, project, disk_path: nil)
+ def initialize(full_path, project, disk_path: nil, is_wiki: false)
@full_path = full_path
@disk_path = disk_path || full_path
@project = project
+ @commit_cache = {}
+ @is_wiki = is_wiki
end
def ==(other)
@@ -103,18 +104,17 @@ class Repository
def commit(ref = 'HEAD')
return nil unless exists?
+ return ref if ref.is_a?(::Commit)
- commit =
- if ref.is_a?(Gitlab::Git::Commit)
- ref
- else
- Gitlab::Git::Commit.find(raw_repository, ref)
- end
+ find_commit(ref)
+ end
- commit = ::Commit.new(commit, @project) if commit
- commit
- rescue Rugged::OdbError, Rugged::TreeError
- nil
+ # Finding a commit by the passed SHA
+ # Also takes care of caching, based on the SHA
+ def commit_by(oid:)
+ return @commit_cache[oid] if @commit_cache.key?(oid)
+
+ @commit_cache[oid] = find_commit(oid)
end
def commits(ref, path: nil, limit: nil, offset: nil, skip_merges: false, after: nil, before: nil)
@@ -231,7 +231,7 @@ class Repository
# branches or tags, but we want to keep some of these commits around, for
# example if they have comments or CI builds.
def keep_around(sha)
- return unless sha && commit(sha)
+ return unless sha && commit_by(oid: sha)
return if kept_around?(sha)
@@ -902,18 +902,27 @@ class Repository
end
end
- def merged_to_root_ref?(branch_name)
- branch_commit = commit(branch_name)
- root_ref_commit = commit(root_ref)
+ def merged_to_root_ref?(branch_or_name, pre_loaded_merged_branches = nil)
+ branch = Gitlab::Git::Branch.find(self, branch_or_name)
- if branch_commit
- same_head = branch_commit.id == root_ref_commit.id
- !same_head && ancestor?(branch_commit.id, root_ref_commit.id)
+ if branch
+ root_ref_sha = commit(root_ref).sha
+ same_head = branch.target == root_ref_sha
+ merged =
+ if pre_loaded_merged_branches
+ pre_loaded_merged_branches.include?(branch.name)
+ else
+ ancestor?(branch.target, root_ref_sha)
+ end
+
+ !same_head && merged
else
nil
end
end
+ delegate :merged_branch_names, to: :raw_repository
+
def merge_base(first_commit_id, second_commit_id)
first_commit_id = commit(first_commit_id).try(:id) || first_commit_id
second_commit_id = commit(second_commit_id).try(:id) || second_commit_id
@@ -956,21 +965,8 @@ class Repository
run_git(args).first.lines.map(&:strip)
end
- def add_remote(name, url)
- raw_repository.remote_add(name, url)
- rescue Rugged::ConfigError
- raw_repository.remote_update(name, url: url)
- end
-
- def remove_remote(name)
- raw_repository.remote_delete(name)
- true
- rescue Rugged::ConfigError
- false
- end
-
- def fetch_remote(remote, forced: false, no_tags: false)
- gitlab_shell.fetch_remote(raw_repository, remote, forced: forced, no_tags: no_tags)
+ def fetch_remote(remote, forced: false, ssh_auth: nil, no_tags: false)
+ gitlab_shell.fetch_remote(raw_repository, remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags)
end
def fetch_source_branch(source_repository, source_branch, local_ref)
@@ -1064,6 +1060,18 @@ class Repository
private
+ # TODO Generice finder, later split this on finders by Ref or Oid
+ # gitlab-org/gitlab-ce#39239
+ def find_commit(oid_or_ref)
+ commit = if oid_or_ref.is_a?(Gitlab::Git::Commit)
+ oid_or_ref
+ else
+ Gitlab::Git::Commit.find(raw_repository, oid_or_ref)
+ end
+
+ ::Commit.new(commit, @project) if commit
+ end
+
def blob_data_at(sha, path)
blob = blob_at(sha, path)
return unless blob
@@ -1102,12 +1110,12 @@ class Repository
def last_commit_for_path_by_gitaly(sha, path)
c = raw_repository.gitaly_commit_client.last_commit_for_path(sha, path)
- commit(c)
+ commit_by(oid: c)
end
def last_commit_for_path_by_rugged(sha, path)
sha = last_commit_id_for_path_by_shelling_out(sha, path)
- commit(sha)
+ commit_by(oid: sha)
end
def last_commit_id_for_path_by_shelling_out(sha, path)
@@ -1120,7 +1128,7 @@ class Repository
end
def initialize_raw_repository
- Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', Gitlab::GlRepository.gl_repository(project, false))
+ Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', Gitlab::GlRepository.gl_repository(project, is_wiki))
end
def find_commits_by_message_by_shelling_out(query, ref, path, limit, offset)
diff --git a/app/models/service.rb b/app/models/service.rb
index 6b64079215f..fdd2605e3e3 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -238,6 +238,7 @@ class Service < ActiveRecord::Base
kubernetes
mattermost_slash_commands
mattermost
+ packagist
pipelines_email
pivotaltracker
prometheus
diff --git a/app/models/user.rb b/app/models/user.rb
index 9459b6d4fa4..bcda4564595 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -21,8 +21,8 @@ class User < ActiveRecord::Base
ignore_column :external_email
ignore_column :email_provider
+ ignore_column :authentication_token
- add_authentication_token_field :authentication_token
add_authentication_token_field :incoming_email_token
add_authentication_token_field :rss_token
@@ -163,11 +163,12 @@ class User < ActiveRecord::Base
before_validation :sanitize_attrs
before_validation :set_notification_email, if: :email_changed?
before_validation :set_public_email, if: :public_email_changed?
- before_save :ensure_authentication_token, :ensure_incoming_email_token
+ before_save :ensure_incoming_email_token
before_save :ensure_user_rights_and_limits, if: :external_changed?
before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) }
before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? }
after_save :ensure_namespace_correct
+ after_update :username_changed_hook, if: :username_changed?
after_destroy :post_destroy_hook
after_commit :update_emails_with_primary_email, on: :update, if: -> { previous_changes.key?('email') }
after_commit :update_invalid_gpg_signatures, on: :update, if: -> { previous_changes.key?('email') }
@@ -185,8 +186,6 @@ class User < ActiveRecord::Base
# Note: When adding an option, it MUST go on the end of the array.
enum project_view: [:readme, :activity, :files]
- alias_attribute :private_token, :authentication_token
-
delegate :path, to: :namespace, allow_nil: true, prefix: true
state_machine :state, initial: :active do
@@ -873,6 +872,10 @@ class User < ActiveRecord::Base
end
end
+ def username_changed_hook
+ system_hook_service.execute_hooks_for(self, :rename)
+ end
+
def post_destroy_hook
log_info("User \"#{name}\" (#{email}) was removed")
system_hook_service.execute_hooks_for(self, :destroy)
diff --git a/app/policies/gcp/cluster_policy.rb b/app/policies/clusters/cluster_policy.rb
index e77173ea6e1..1f7c13072b9 100644
--- a/app/policies/gcp/cluster_policy.rb
+++ b/app/policies/clusters/cluster_policy.rb
@@ -1,8 +1,8 @@
-module Gcp
+module Clusters
class ClusterPolicy < BasePolicy
alias_method :cluster, :subject
- delegate { @subject.project }
+ delegate { cluster.project }
rule { can?(:master_access) }.policy do
enable :update_cluster
diff --git a/app/presenters/gcp/cluster_presenter.rb b/app/presenters/clusters/cluster_presenter.rb
index f7908f92a37..01cb59d0d44 100644
--- a/app/presenters/gcp/cluster_presenter.rb
+++ b/app/presenters/clusters/cluster_presenter.rb
@@ -1,9 +1,9 @@
-module Gcp
+module Clusters
class ClusterPresenter < Gitlab::View::Presenter::Delegated
presents :cluster
def gke_cluster_url
- "https://console.cloud.google.com/kubernetes/clusters/details/#{gcp_cluster_zone}/#{gcp_cluster_name}"
+ "https://console.cloud.google.com/kubernetes/clusters/details/#{provider.zone}/#{name}" if gcp?
end
end
end
diff --git a/app/serializers/issuable_entity.rb b/app/serializers/issuable_entity.rb
index 61c7a428745..3b5a4fd4f79 100644
--- a/app/serializers/issuable_entity.rb
+++ b/app/serializers/issuable_entity.rb
@@ -1,20 +1,16 @@
class IssuableEntity < Grape::Entity
+ include RequestAwareEntity
+
expose :id
expose :iid
expose :author_id
expose :description
expose :lock_version
expose :milestone_id
- expose :state
expose :title
expose :updated_by_id
expose :created_at
expose :updated_at
- expose :deleted_at
- expose :time_estimate
- expose :total_time_spent
- expose :human_time_estimate
- expose :human_total_time_spent
expose :milestone, using: API::Entities::Milestone
expose :labels, using: LabelEntity
end
diff --git a/app/serializers/issuable_sidebar_entity.rb b/app/serializers/issuable_sidebar_entity.rb
new file mode 100644
index 00000000000..ff23d8bf0c7
--- /dev/null
+++ b/app/serializers/issuable_sidebar_entity.rb
@@ -0,0 +1,16 @@
+class IssuableSidebarEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :participants, using: ::API::Entities::UserBasic do |issuable|
+ issuable.participants(request.current_user)
+ end
+
+ expose :subscribed do |issuable|
+ issuable.subscribed?(request.current_user, issuable.project)
+ end
+
+ expose :time_estimate
+ expose :total_time_spent
+ expose :human_time_estimate
+ expose :human_total_time_spent
+end
diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb
index 10d3ad0214b..5f47592e4ad 100644
--- a/app/serializers/issue_entity.rb
+++ b/app/serializers/issue_entity.rb
@@ -1,6 +1,8 @@
class IssueEntity < IssuableEntity
- include RequestAwareEntity
+ include TimeTrackableEntity
+ expose :state
+ expose :deleted_at
expose :branch_name
expose :confidential
expose :discussion_locked
diff --git a/app/serializers/issue_serializer.rb b/app/serializers/issue_serializer.rb
index 4fff54a9126..2555595379b 100644
--- a/app/serializers/issue_serializer.rb
+++ b/app/serializers/issue_serializer.rb
@@ -1,3 +1,16 @@
class IssueSerializer < BaseSerializer
- entity IssueEntity
+ # This overrided method takes care of which entity should be used
+ # to serialize the `issue` based on `basic` key in `opts` param.
+ # Hence, `entity` doesn't need to be declared on the class scope.
+ def represent(merge_request, opts = {})
+ entity =
+ case opts[:serializer]
+ when 'sidebar'
+ IssueSidebarEntity
+ else
+ IssueEntity
+ end
+
+ super(merge_request, opts, entity)
+ end
end
diff --git a/app/serializers/issue_sidebar_entity.rb b/app/serializers/issue_sidebar_entity.rb
new file mode 100644
index 00000000000..6c823dbfe95
--- /dev/null
+++ b/app/serializers/issue_sidebar_entity.rb
@@ -0,0 +1,3 @@
+class IssueSidebarEntity < IssuableSidebarEntity
+ expose :assignees, using: API::Entities::UserBasic
+end
diff --git a/app/serializers/merge_request_basic_entity.rb b/app/serializers/merge_request_basic_entity.rb
index 8461f158bb5..d54a6516aed 100644
--- a/app/serializers/merge_request_basic_entity.rb
+++ b/app/serializers/merge_request_basic_entity.rb
@@ -1,11 +1,7 @@
-class MergeRequestBasicEntity < Grape::Entity
+class MergeRequestBasicEntity < IssuableSidebarEntity
expose :assignee_id
expose :merge_status
expose :merge_error
expose :state
expose :source_branch_exists?, as: :source_branch_exists
- expose :time_estimate
- expose :total_time_spent
- expose :human_time_estimate
- expose :human_total_time_spent
end
diff --git a/app/serializers/merge_request_entity.rb b/app/serializers/merge_request_entity.rb
index 297a459e394..b53a49fe59e 100644
--- a/app/serializers/merge_request_entity.rb
+++ b/app/serializers/merge_request_entity.rb
@@ -1,6 +1,8 @@
class MergeRequestEntity < IssuableEntity
- include RequestAwareEntity
+ include TimeTrackableEntity
+ expose :state
+ expose :deleted_at
expose :in_progress_merge_commit_sha
expose :merge_commit_sha
expose :merge_error
diff --git a/app/serializers/merge_request_serializer.rb b/app/serializers/merge_request_serializer.rb
index f67034ce47a..e9d98d8baca 100644
--- a/app/serializers/merge_request_serializer.rb
+++ b/app/serializers/merge_request_serializer.rb
@@ -3,7 +3,14 @@ class MergeRequestSerializer < BaseSerializer
# to serialize the `merge_request` based on `basic` key in `opts` param.
# Hence, `entity` doesn't need to be declared on the class scope.
def represent(merge_request, opts = {})
- entity = opts[:basic] ? MergeRequestBasicEntity : MergeRequestEntity
+ entity =
+ case opts[:serializer]
+ when 'basic', 'sidebar'
+ MergeRequestBasicEntity
+ else
+ MergeRequestEntity
+ end
+
super(merge_request, opts, entity)
end
end
diff --git a/app/serializers/time_trackable_entity.rb b/app/serializers/time_trackable_entity.rb
new file mode 100644
index 00000000000..e81cd7bec72
--- /dev/null
+++ b/app/serializers/time_trackable_entity.rb
@@ -0,0 +1,11 @@
+module TimeTrackableEntity
+ extend ActiveSupport::Concern
+ extend Grape
+
+ included do
+ expose :time_estimate
+ expose :total_time_spent
+ expose :human_time_estimate
+ expose :human_total_time_spent
+ end
+end
diff --git a/app/services/access_token_validation_service.rb b/app/services/access_token_validation_service.rb
index 9c00ea789ec..46e19230328 100644
--- a/app/services/access_token_validation_service.rb
+++ b/app/services/access_token_validation_service.rb
@@ -39,11 +39,8 @@ class AccessTokenValidationService
token_scopes = token.scopes.map(&:to_sym)
required_scopes.any? do |scope|
- if scope.respond_to?(:sufficient?)
- scope.sufficient?(token_scopes, request)
- else
- API::Scope.new(scope).sufficient?(token_scopes, request)
- end
+ scope = API::Scope.new(scope) unless scope.is_a?(API::Scope)
+ scope.sufficient?(token_scopes, request)
end
end
end
diff --git a/app/services/applications/create_service.rb b/app/services/applications/create_service.rb
new file mode 100644
index 00000000000..35d45f25a71
--- /dev/null
+++ b/app/services/applications/create_service.rb
@@ -0,0 +1,13 @@
+module Applications
+ class CreateService
+ def initialize(current_user, params)
+ @current_user = current_user
+ @params = params
+ @ip_address = @params.delete(:ip_address)
+ end
+
+ def execute(request = nil)
+ Doorkeeper::Application.create(@params)
+ end
+ end
+end
diff --git a/app/services/ci/create_cluster_service.rb b/app/services/ci/create_cluster_service.rb
deleted file mode 100644
index f7ee0e468e2..00000000000
--- a/app/services/ci/create_cluster_service.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-module Ci
- class CreateClusterService < BaseService
- def execute(access_token)
- params['gcp_machine_type'] ||= GoogleApi::CloudPlatform::Client::DEFAULT_MACHINE_TYPE
-
- cluster_params =
- params.merge(user: current_user,
- gcp_token: access_token)
-
- project.create_cluster(cluster_params).tap do |cluster|
- ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted?
- end
- end
- end
-end
diff --git a/app/services/ci/fetch_gcp_operation_service.rb b/app/services/ci/fetch_gcp_operation_service.rb
deleted file mode 100644
index 0b68e4d6ea9..00000000000
--- a/app/services/ci/fetch_gcp_operation_service.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-module Ci
- class FetchGcpOperationService
- def execute(cluster)
- api_client =
- GoogleApi::CloudPlatform::Client.new(cluster.gcp_token, nil)
-
- operation = api_client.projects_zones_operations(
- cluster.gcp_project_id,
- cluster.gcp_cluster_zone,
- cluster.gcp_operation_id)
-
- yield(operation) if block_given?
- rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
- return cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}")
- end
- end
-end
diff --git a/app/services/ci/finalize_cluster_creation_service.rb b/app/services/ci/finalize_cluster_creation_service.rb
deleted file mode 100644
index 347875c5697..00000000000
--- a/app/services/ci/finalize_cluster_creation_service.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-module Ci
- class FinalizeClusterCreationService
- def execute(cluster)
- api_client =
- GoogleApi::CloudPlatform::Client.new(cluster.gcp_token, nil)
-
- begin
- gke_cluster = api_client.projects_zones_clusters_get(
- cluster.gcp_project_id,
- cluster.gcp_cluster_zone,
- cluster.gcp_cluster_name)
- rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
- return cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}")
- end
-
- endpoint = gke_cluster.endpoint
- api_url = 'https://' + endpoint
- ca_cert = Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate)
- username = gke_cluster.master_auth.username
- password = gke_cluster.master_auth.password
-
- kubernetes_token = Ci::FetchKubernetesTokenService.new(
- api_url, ca_cert, username, password).execute
-
- unless kubernetes_token
- return cluster.make_errored!('Failed to get a default token of kubernetes')
- end
-
- Ci::IntegrateClusterService.new.execute(
- cluster, endpoint, ca_cert, kubernetes_token, username, password)
- end
- end
-end
diff --git a/app/services/ci/integrate_cluster_service.rb b/app/services/ci/integrate_cluster_service.rb
deleted file mode 100644
index d123ce8d26b..00000000000
--- a/app/services/ci/integrate_cluster_service.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-module Ci
- class IntegrateClusterService
- def execute(cluster, endpoint, ca_cert, token, username, password)
- Gcp::Cluster.transaction do
- cluster.update!(
- enabled: true,
- endpoint: endpoint,
- ca_cert: ca_cert,
- kubernetes_token: token,
- username: username,
- password: password,
- service: cluster.project.find_or_initialize_service('kubernetes'),
- status_event: :make_created)
-
- cluster.service.update!(
- active: true,
- api_url: cluster.api_url,
- ca_pem: ca_cert,
- namespace: cluster.project_namespace,
- token: token)
- end
- rescue ActiveRecord::RecordInvalid => e
- cluster.make_errored!("Failed to integrate cluster into kubernetes_service: #{e.message}")
- end
- end
-end
diff --git a/app/services/ci/provision_cluster_service.rb b/app/services/ci/provision_cluster_service.rb
deleted file mode 100644
index 52d80b01813..00000000000
--- a/app/services/ci/provision_cluster_service.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-module Ci
- class ProvisionClusterService
- def execute(cluster)
- api_client =
- GoogleApi::CloudPlatform::Client.new(cluster.gcp_token, nil)
-
- begin
- operation = api_client.projects_zones_clusters_create(
- cluster.gcp_project_id,
- cluster.gcp_cluster_zone,
- cluster.gcp_cluster_name,
- cluster.gcp_cluster_size,
- machine_type: cluster.gcp_machine_type)
- rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
- return cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}")
- end
-
- unless operation.status == 'RUNNING' || operation.status == 'PENDING'
- return cluster.make_errored!("Operation status is unexpected; #{operation.status_message}")
- end
-
- cluster.gcp_operation_id = api_client.parse_operation_id(operation.self_link)
-
- unless cluster.gcp_operation_id
- return cluster.make_errored!('Can not find operation_id from self_link')
- end
-
- if cluster.make_creating
- WaitForClusterCreationWorker.perform_in(
- WaitForClusterCreationWorker::INITIAL_INTERVAL, cluster.id)
- else
- return cluster.make_errored!("Failed to update cluster record; #{cluster.errors}")
- end
- end
- end
-end
diff --git a/app/services/ci/update_cluster_service.rb b/app/services/ci/update_cluster_service.rb
deleted file mode 100644
index 70d88fca660..00000000000
--- a/app/services/ci/update_cluster_service.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-module Ci
- class UpdateClusterService < BaseService
- def execute(cluster)
- Gcp::Cluster.transaction do
- cluster.update!(params)
-
- if params['enabled'] == 'true'
- cluster.service.update!(
- active: true,
- api_url: cluster.api_url,
- ca_pem: cluster.ca_cert,
- namespace: cluster.project_namespace,
- token: cluster.kubernetes_token)
- else
- cluster.service.update!(active: false)
- end
- end
- rescue ActiveRecord::RecordInvalid => e
- cluster.errors.add(:base, e.message)
- end
- end
-end
diff --git a/app/services/clusters/create_service.rb b/app/services/clusters/create_service.rb
new file mode 100644
index 00000000000..1d407739b21
--- /dev/null
+++ b/app/services/clusters/create_service.rb
@@ -0,0 +1,29 @@
+module Clusters
+ class CreateService < BaseService
+ attr_reader :access_token
+
+ def execute(access_token)
+ @access_token = access_token
+
+ create_cluster.tap do |cluster|
+ ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted?
+ end
+ end
+
+ private
+
+ def create_cluster
+ Clusters::Cluster.create(cluster_params)
+ end
+
+ def cluster_params
+ return @cluster_params if defined?(@cluster_params)
+
+ params[:provider_gcp_attributes].try do |provider|
+ provider[:access_token] = access_token
+ end
+
+ @cluster_params = params.merge(user: current_user, projects: [project])
+ end
+ end
+end
diff --git a/app/services/clusters/gcp/fetch_operation_service.rb b/app/services/clusters/gcp/fetch_operation_service.rb
new file mode 100644
index 00000000000..a4cd3ca5c11
--- /dev/null
+++ b/app/services/clusters/gcp/fetch_operation_service.rb
@@ -0,0 +1,16 @@
+module Clusters
+ module Gcp
+ class FetchOperationService
+ def execute(provider)
+ operation = provider.api_client.projects_zones_operations(
+ provider.gcp_project_id,
+ provider.zone,
+ provider.operation_id)
+
+ yield(operation) if block_given?
+ rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
+ provider.make_errored!("Failed to request to CloudPlatform; #{e.message}")
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb
new file mode 100644
index 00000000000..cea56f4e849
--- /dev/null
+++ b/app/services/clusters/gcp/finalize_creation_service.rb
@@ -0,0 +1,56 @@
+module Clusters
+ module Gcp
+ class FinalizeCreationService
+ attr_reader :provider
+
+ def execute(provider)
+ @provider = provider
+
+ configure_provider
+ configure_kubernetes
+
+ cluster.save!
+ rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
+ provider.make_errored!("Failed to request to CloudPlatform; #{e.message}")
+ rescue ActiveRecord::RecordInvalid => e
+ provider.make_errored!("Failed to configure GKE Cluster: #{e.message}")
+ end
+
+ private
+
+ def configure_provider
+ provider.endpoint = gke_cluster.endpoint
+ provider.status_event = :make_created
+ end
+
+ def configure_kubernetes
+ cluster.platform_type = :kubernetes
+ cluster.build_platform_kubernetes(
+ api_url: 'https://' + gke_cluster.endpoint,
+ ca_cert: Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate),
+ username: gke_cluster.master_auth.username,
+ password: gke_cluster.master_auth.password,
+ token: request_kuberenetes_token)
+ end
+
+ def request_kuberenetes_token
+ Ci::FetchKubernetesTokenService.new(
+ 'https://' + gke_cluster.endpoint,
+ Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate),
+ gke_cluster.master_auth.username,
+ gke_cluster.master_auth.password).execute
+ end
+
+ def gke_cluster
+ @gke_cluster ||= provider.api_client.projects_zones_clusters_get(
+ provider.gcp_project_id,
+ provider.zone,
+ cluster.name)
+ end
+
+ def cluster
+ @cluster ||= provider.cluster
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/gcp/provision_service.rb b/app/services/clusters/gcp/provision_service.rb
new file mode 100644
index 00000000000..8beea5a8cfb
--- /dev/null
+++ b/app/services/clusters/gcp/provision_service.rb
@@ -0,0 +1,47 @@
+module Clusters
+ module Gcp
+ class ProvisionService
+ attr_reader :provider
+
+ def execute(provider)
+ @provider = provider
+
+ get_operation_id do |operation_id|
+ if provider.make_creating(operation_id)
+ WaitForClusterCreationWorker.perform_in(
+ Clusters::Gcp::VerifyProvisionStatusService::INITIAL_INTERVAL,
+ provider.cluster_id)
+ else
+ provider.make_errored!("Failed to update provider record; #{provider.errors}")
+ end
+ end
+ end
+
+ private
+
+ def get_operation_id
+ operation = provider.api_client.projects_zones_clusters_create(
+ provider.gcp_project_id,
+ provider.zone,
+ provider.cluster.name,
+ provider.num_nodes,
+ machine_type: provider.machine_type)
+
+ unless operation.status == 'PENDING' || operation.status == 'RUNNING'
+ return provider.make_errored!("Operation status is unexpected; #{operation.status_message}")
+ end
+
+ operation_id = provider.api_client.parse_operation_id(operation.self_link)
+
+ unless operation_id
+ return provider.make_errored!('Can not find operation_id from self_link')
+ end
+
+ yield(operation_id)
+
+ rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
+ provider.make_errored!("Failed to request to CloudPlatform; #{e.message}")
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/gcp/verify_provision_status_service.rb b/app/services/clusters/gcp/verify_provision_status_service.rb
new file mode 100644
index 00000000000..bc33756f27c
--- /dev/null
+++ b/app/services/clusters/gcp/verify_provision_status_service.rb
@@ -0,0 +1,48 @@
+module Clusters
+ module Gcp
+ class VerifyProvisionStatusService
+ attr_reader :provider
+
+ INITIAL_INTERVAL = 2.minutes
+ EAGER_INTERVAL = 10.seconds
+ TIMEOUT = 20.minutes
+
+ def execute(provider)
+ @provider = provider
+
+ request_operation do |operation|
+ case operation.status
+ when 'PENDING', 'RUNNING'
+ continue_creation(operation)
+ when 'DONE'
+ finalize_creation
+ else
+ return provider.make_errored!("Unexpected operation status; #{operation.status} #{operation.status_message}")
+ end
+ end
+ end
+
+ private
+
+ def continue_creation(operation)
+ if elapsed_time_from_creation(operation) < TIMEOUT
+ WaitForClusterCreationWorker.perform_in(EAGER_INTERVAL, provider.cluster_id)
+ else
+ provider.make_errored!("Cluster creation time exceeds timeout; #{TIMEOUT}")
+ end
+ end
+
+ def elapsed_time_from_creation(operation)
+ Time.now.utc - operation.start_time.to_time.utc
+ end
+
+ def finalize_creation
+ Clusters::Gcp::FinalizeCreationService.new.execute(provider)
+ end
+
+ def request_operation(&blk)
+ Clusters::Gcp::FetchOperationService.new.execute(provider, &blk)
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/update_service.rb b/app/services/clusters/update_service.rb
new file mode 100644
index 00000000000..989218e32a2
--- /dev/null
+++ b/app/services/clusters/update_service.rb
@@ -0,0 +1,7 @@
+module Clusters
+ class UpdateService < BaseService
+ def execute(cluster)
+ cluster.update(params)
+ end
+ end
+end
diff --git a/app/services/issuable/common_system_notes_service.rb b/app/services/issuable/common_system_notes_service.rb
new file mode 100644
index 00000000000..92eaa5d5115
--- /dev/null
+++ b/app/services/issuable/common_system_notes_service.rb
@@ -0,0 +1,81 @@
+module Issuable
+ class CommonSystemNotesService < ::BaseService
+ attr_reader :issuable
+
+ def execute(issuable, old_labels)
+ @issuable = issuable
+
+ if issuable.previous_changes.include?('title')
+ create_title_change_note(issuable.previous_changes['title'].first)
+ end
+
+ handle_description_change_note
+
+ handle_time_tracking_note if issuable.is_a?(TimeTrackable)
+ create_labels_note(old_labels) if issuable.labels != old_labels
+ create_discussion_lock_note if issuable.previous_changes.include?('discussion_locked')
+ create_milestone_note if issuable.previous_changes.include?('milestone_id')
+ end
+
+ private
+
+ def handle_time_tracking_note
+ if issuable.previous_changes.include?('time_estimate')
+ create_time_estimate_note
+ end
+
+ if issuable.time_spent?
+ create_time_spent_note
+ end
+ end
+
+ def handle_description_change_note
+ if issuable.previous_changes.include?('description')
+ if issuable.tasks? && issuable.updated_tasks.any?
+ create_task_status_note
+ else
+ # TODO: Show this note if non-task content was modified.
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/33577
+ create_description_change_note
+ end
+ end
+ end
+
+ def create_labels_note(old_labels)
+ added_labels = issuable.labels - old_labels
+ removed_labels = old_labels - issuable.labels
+
+ SystemNoteService.change_label(issuable, issuable.project, current_user, added_labels, removed_labels)
+ end
+
+ def create_title_change_note(old_title)
+ SystemNoteService.change_title(issuable, issuable.project, current_user, old_title)
+ end
+
+ def create_description_change_note
+ SystemNoteService.change_description(issuable, issuable.project, current_user)
+ end
+
+ def create_task_status_note
+ issuable.updated_tasks.each do |task|
+ SystemNoteService.change_task_status(issuable, issuable.project, current_user, task)
+ end
+ end
+
+ def create_time_estimate_note
+ SystemNoteService.change_time_estimate(issuable, issuable.project, current_user)
+ end
+
+ def create_time_spent_note
+ SystemNoteService.change_time_spent(issuable, issuable.project, issuable.time_spent_user)
+ end
+
+ def create_milestone_note
+ SystemNoteService.change_milestone(issuable, issuable.project, current_user, issuable.milestone)
+ end
+
+ def create_discussion_lock_note
+ SystemNoteService.discussion_lock(issuable, current_user)
+ end
+ end
+end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index d61a342ebad..68b49d880f7 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -1,56 +1,10 @@
class IssuableBaseService < BaseService
private
- def create_milestone_note(issuable)
- SystemNoteService.change_milestone(
- issuable, issuable.project, current_user, issuable.milestone)
- end
-
- def create_labels_note(issuable, old_labels)
- added_labels = issuable.labels - old_labels
- removed_labels = old_labels - issuable.labels
-
- SystemNoteService.change_label(
- issuable, issuable.project, current_user, added_labels, removed_labels)
- end
-
- def create_title_change_note(issuable, old_title)
- SystemNoteService.change_title(
- issuable, issuable.project, current_user, old_title)
- end
-
- def create_description_change_note(issuable)
- SystemNoteService.change_description(issuable, issuable.project, current_user)
- end
-
- def create_branch_change_note(issuable, branch_type, old_branch, new_branch)
- SystemNoteService.change_branch(
- issuable, issuable.project, current_user, branch_type,
- old_branch, new_branch)
- end
-
- def create_task_status_note(issuable)
- issuable.updated_tasks.each do |task|
- SystemNoteService.change_task_status(issuable, issuable.project, current_user, task)
- end
- end
-
- def create_time_estimate_note(issuable)
- SystemNoteService.change_time_estimate(issuable, issuable.project, current_user)
- end
-
- def create_time_spent_note(issuable)
- SystemNoteService.change_time_spent(issuable, issuable.project, current_user)
- end
-
- def create_discussion_lock_note(issuable)
- SystemNoteService.discussion_lock(issuable, current_user)
- end
-
def filter_params(issuable)
ability_name = :"admin_#{issuable.to_ability_name}"
- unless can?(current_user, ability_name, project)
+ unless can?(current_user, ability_name, issuable)
params.delete(:milestone_id)
params.delete(:labels)
params.delete(:add_label_ids)
@@ -233,15 +187,14 @@ class IssuableBaseService < BaseService
# We have to perform this check before saving the issuable as Rails resets
# the changed fields upon calling #save.
- update_project_counters = issuable.update_project_counter_caches?
+ update_project_counters = issuable.project && issuable.update_project_counter_caches?
if issuable.with_transaction_returning_status { issuable.save }
# We do not touch as it will affect a update on updated_at field
ActiveRecord::Base.no_touching do
- handle_common_system_notes(issuable, old_labels: old_labels)
+ Issuable::CommonSystemNotesService.new(project, current_user).execute(issuable, old_labels)
end
- change_discussion_lock(issuable)
handle_changes(
issuable,
old_labels: old_labels,
@@ -300,12 +253,6 @@ class IssuableBaseService < BaseService
end
end
- def change_discussion_lock(issuable)
- if issuable.previous_changes.include?('discussion_locked')
- create_discussion_lock_note(issuable)
- end
- end
-
def toggle_award(issuable)
award = params.delete(:emoji_award)
if award
@@ -328,35 +275,17 @@ class IssuableBaseService < BaseService
attrs_changed || labels_changed || assignees_changed
end
- def handle_common_system_notes(issuable, old_labels: [])
- if issuable.previous_changes.include?('title')
- create_title_change_note(issuable, issuable.previous_changes['title'].first)
- end
-
- if issuable.previous_changes.include?('description')
- if issuable.tasks? && issuable.updated_tasks.any?
- create_task_status_note(issuable)
- else
- # TODO: Show this note if non-task content was modified.
- # https://gitlab.com/gitlab-org/gitlab-ce/issues/33577
- create_description_change_note(issuable)
- end
- end
-
- if issuable.previous_changes.include?('time_estimate')
- create_time_estimate_note(issuable)
- end
-
- if issuable.time_spent?
- create_time_spent_note(issuable)
- end
-
- create_labels_note(issuable, old_labels) if issuable.labels != old_labels
- end
-
def invalidate_cache_counts(issuable, users: [])
users.each do |user|
user.public_send("invalidate_#{issuable.model_name.singular}_cache_counts") # rubocop:disable GitlabSecurity/PublicSend
end
end
+
+ # override if needed
+ def handle_changes(issuable, options)
+ end
+
+ # override if needed
+ def execute_hooks(issuable, action = 'open', params = {})
+ end
end
diff --git a/app/services/issues/reopen_service.rb b/app/services/issues/reopen_service.rb
index 35de4337b15..62b4b4b6a1e 100644
--- a/app/services/issues/reopen_service.rb
+++ b/app/services/issues/reopen_service.rb
@@ -9,6 +9,7 @@ module Issues
notification_service.reopen_issue(issue, current_user)
execute_hooks(issue, 'reopen')
invalidate_cache_counts(issue, users: issue.assignees)
+ issue.update_project_counter_caches
end
issue
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index e0339ddf9bb..1b7b5927c5a 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -27,10 +27,6 @@ module Issues
todo_service.update_issue(issue, current_user, old_mentioned_users)
end
- if issue.previous_changes.include?('milestone_id')
- create_milestone_note(issue)
- end
-
if issue.assignees != old_assignees
create_assignee_note(issue, old_assignees)
notification_service.reassigned_issue(issue, current_user, old_assignees)
diff --git a/app/services/keys/base_service.rb b/app/services/keys/base_service.rb
index 545832d0bd4..f78791932a7 100644
--- a/app/services/keys/base_service.rb
+++ b/app/services/keys/base_service.rb
@@ -4,6 +4,7 @@ module Keys
def initialize(user, params)
@user, @params = user, params
+ @ip_address = @params.delete(:ip_address)
end
def notification_service
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index 8c5821aa870..156e7b2f078 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -82,16 +82,9 @@ module MergeRequests
@merge_request.can_remove_source_branch?(branch_deletion_user)
end
- # Logs merge error message and cleans `MergeRequest#merge_jid`.
- #
def handle_merge_error(log_message:, save_message_on_model: false)
Rails.logger.error("MergeService ERROR: #{merge_request_info} - #{log_message}")
-
- if save_message_on_model
- @merge_request.update(merge_error: log_message, merge_jid: nil)
- else
- clean_merge_jid
- end
+ @merge_request.update(merge_error: log_message) if save_message_on_model
end
def merge_request_info
diff --git a/app/services/merge_requests/reopen_service.rb b/app/services/merge_requests/reopen_service.rb
index b9c65be36ec..c599a90f9fe 100644
--- a/app/services/merge_requests/reopen_service.rb
+++ b/app/services/merge_requests/reopen_service.rb
@@ -11,6 +11,7 @@ module MergeRequests
merge_request.reload_diff(current_user)
merge_request.mark_as_unchecked
invalidate_cache_counts(merge_request, users: merge_request.assignees)
+ merge_request.update_project_counter_caches
end
merge_request
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 2832d893e95..1f394cacc64 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -40,10 +40,6 @@ module MergeRequests
merge_request.target_branch)
end
- if merge_request.previous_changes.include?('milestone_id')
- create_milestone_note(merge_request)
- end
-
if merge_request.previous_changes.include?('assignee_id')
create_assignee_note(merge_request)
notification_service.reassigned_merge_request(merge_request, current_user)
@@ -111,5 +107,11 @@ module MergeRequests
end
end
end
+
+ def create_branch_change_note(issuable, branch_type, old_branch, new_branch)
+ SystemNoteService.change_branch(
+ issuable, issuable.project, current_user, branch_type,
+ old_branch, new_branch)
+ end
end
end
diff --git a/app/services/metrics_service.rb b/app/services/metrics_service.rb
index a02eee4961b..6b3939aeba5 100644
--- a/app/services/metrics_service.rb
+++ b/app/services/metrics_service.rb
@@ -6,8 +6,7 @@ class MetricsService
Gitlab::HealthChecks::Redis::RedisCheck,
Gitlab::HealthChecks::Redis::CacheCheck,
Gitlab::HealthChecks::Redis::QueuesCheck,
- Gitlab::HealthChecks::Redis::SharedStateCheck,
- Gitlab::HealthChecks::FsShardsCheck
+ Gitlab::HealthChecks::Redis::SharedStateCheck
].freeze
def prometheus_metrics_text
diff --git a/app/services/milestones/promote_service.rb b/app/services/milestones/promote_service.rb
new file mode 100644
index 00000000000..bd9cfd4e0ea
--- /dev/null
+++ b/app/services/milestones/promote_service.rb
@@ -0,0 +1,80 @@
+module Milestones
+ class PromoteService < Milestones::BaseService
+ PromoteMilestoneError = Class.new(StandardError)
+
+ def execute(milestone)
+ check_project_milestone!(milestone)
+
+ Milestone.transaction do
+ # Destroy all milestones with same title across projects
+ destroy_old_milestones(milestone)
+
+ group_milestone = clone_project_milestone(milestone)
+
+ move_children_to_group_milestone(group_milestone)
+
+ # Just to be safe
+ unless group_milestone.valid?
+ raise_error(group_milestone.errors.full_messages.to_sentence)
+ end
+
+ group_milestone
+ end
+ end
+
+ private
+
+ def milestone_ids_for_merge(group_milestone)
+ # Pluck need to be used here instead of select so the array of ids
+ # is persistent after old milestones gets deleted.
+ @milestone_ids_for_merge ||= begin
+ search_params = { title: group_milestone.title, project_ids: group_project_ids, state: 'all' }
+ milestones = MilestonesFinder.new(search_params).execute
+ milestones.pluck(:id)
+ end
+ end
+
+ def move_children_to_group_milestone(group_milestone)
+ milestone_ids_for_merge(group_milestone).in_groups_of(100) do |milestone_ids|
+ update_children(group_milestone, milestone_ids)
+ end
+ end
+
+ def check_project_milestone!(milestone)
+ raise_error('Only project milestones can be promoted.') unless milestone.project_milestone?
+ end
+
+ def clone_project_milestone(milestone)
+ params = milestone.slice(:title, :description, :start_date, :due_date, :state_event)
+
+ create_service = CreateService.new(group, current_user, params)
+
+ create_service.execute
+ end
+
+ def update_children(group_milestone, milestone_ids)
+ issues = Issue.where(project_id: group_project_ids, milestone_id: milestone_ids)
+ merge_requests = MergeRequest.where(source_project_id: group_project_ids, milestone_id: milestone_ids)
+
+ [issues, merge_requests].each do |issuable_collection|
+ issuable_collection.update_all(milestone_id: group_milestone.id)
+ end
+ end
+
+ def group
+ @group ||= parent.group || raise_error('Project does not belong to a group.')
+ end
+
+ def destroy_old_milestones(group_milestone)
+ Milestone.where(id: milestone_ids_for_merge(group_milestone)).destroy_all
+ end
+
+ def group_project_ids
+ @group_project_ids ||= group.projects.map(&:id)
+ end
+
+ def raise_error(message)
+ raise PromoteMilestoneError, "Promotion failed - #{message}"
+ end
+ end
+end
diff --git a/app/services/projects/group_links/create_service.rb b/app/services/projects/group_links/create_service.rb
new file mode 100644
index 00000000000..35624577024
--- /dev/null
+++ b/app/services/projects/group_links/create_service.rb
@@ -0,0 +1,15 @@
+module Projects
+ module GroupLinks
+ class CreateService < BaseService
+ def execute(group)
+ return false unless group
+
+ project.project_group_links.create(
+ group: group,
+ group_access: params[:link_group_access],
+ expires_at: params[:expires_at]
+ )
+ end
+ end
+ end
+end
diff --git a/app/services/projects/group_links/destroy_service.rb b/app/services/projects/group_links/destroy_service.rb
new file mode 100644
index 00000000000..fbf31214c28
--- /dev/null
+++ b/app/services/projects/group_links/destroy_service.rb
@@ -0,0 +1,10 @@
+module Projects
+ module GroupLinks
+ class DestroyService < BaseService
+ def execute(group_link)
+ return false unless group_link
+ group_link.destroy
+ end
+ end
+ end
+end
diff --git a/app/services/projects/hashed_storage_migration_service.rb b/app/services/projects/hashed_storage_migration_service.rb
index 41259de3a16..f5945f3b87f 100644
--- a/app/services/projects/hashed_storage_migration_service.rb
+++ b/app/services/projects/hashed_storage_migration_service.rb
@@ -10,7 +10,7 @@ module Projects
end
def execute
- return if project.hashed_storage?
+ return if project.hashed_storage?(:repository)
@old_disk_path = project.disk_path
has_wiki = project.wiki.repository_exists?
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index c3bf0031409..455b302d819 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -44,7 +44,7 @@ module Projects
else
clone_repository
end
- rescue Gitlab::Shell::Error => e
+ rescue Gitlab::Shell::Error, Gitlab::Git::RepositoryMirroring::RemoteError => e
# Expire cache to prevent scenarios such as:
# 1. First import failed, but the repo was imported successfully, so +exists?+ returns true
# 2. Retried import, repo is broken or not imported but +exists?+ still returns true
diff --git a/app/services/projects/unlink_fork_service.rb b/app/services/projects/unlink_fork_service.rb
index 2b82e5732e4..c499f384426 100644
--- a/app/services/projects/unlink_fork_service.rb
+++ b/app/services/projects/unlink_fork_service.rb
@@ -3,18 +3,24 @@ module Projects
def execute
return unless @project.forked?
- @project.forked_from_project.lfs_objects.find_each do |lfs_object|
- lfs_object.projects << @project
+ if fork_source = @project.fork_source
+ fork_source.lfs_objects.find_each do |lfs_object|
+ lfs_object.projects << @project
+ end
+
+ refresh_forks_count(fork_source)
end
- merge_requests = @project.forked_from_project.merge_requests.opened.from_project(@project)
+ merge_requests = @project.fork_network
+ .merge_requests
+ .opened
+ .where.not(target_project: @project)
+ .from_project(@project)
merge_requests.each do |mr|
::MergeRequests::CloseService.new(@project, @current_user).execute(mr)
end
- refresh_forks_count(@project.forked_from_project)
-
@project.fork_network_member.destroy
@project.forked_project_link.destroy
end
diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb
index a1c2f8d0180..911cc919bb8 100644
--- a/app/services/system_hooks_service.rb
+++ b/app/services/system_hooks_service.rb
@@ -35,24 +35,22 @@ class SystemHooksService
data[:old_path_with_namespace] = model.old_path_with_namespace
end
when User
- data.merge!({
- name: model.name,
- email: model.email,
- user_id: model.id,
- username: model.username
- })
+ data.merge!(user_data(model))
+
+ if event == :rename
+ data[:old_username] = model.username_was
+ end
when ProjectMember
data.merge!(project_member_data(model))
when Group
- owner = model.owner
+ data.merge!(group_data(model))
- data.merge!(
- name: model.name,
- path: model.path,
- group_id: model.id,
- owner_name: owner.respond_to?(:name) ? owner.name : nil,
- owner_email: owner.respond_to?(:email) ? owner.email : nil
- )
+ if event == :rename
+ data.merge!(
+ old_path: model.path_was,
+ old_full_path: model.full_path_was
+ )
+ end
when GroupMember
data.merge!(group_member_data(model))
end
@@ -83,7 +81,7 @@ class SystemHooksService
project_id: model.id,
owner_name: owner.name,
owner_email: owner.respond_to?(:email) ? owner.email : "",
- project_visibility: Project.visibility_levels.key(model.visibility_level_value).downcase
+ project_visibility: model.visibility.downcase
}
end
@@ -104,6 +102,19 @@ class SystemHooksService
}
end
+ def group_data(model)
+ owner = model.owner
+
+ {
+ name: model.name,
+ path: model.path,
+ full_path: model.full_path,
+ group_id: model.id,
+ owner_name: owner.try(:name),
+ owner_email: owner.try(:email)
+ }
+ end
+
def group_member_data(model)
{
group_name: model.group.name,
@@ -116,4 +127,13 @@ class SystemHooksService
group_access: model.human_access
}
end
+
+ def user_data(model)
+ {
+ name: model.name,
+ email: model.email,
+ user_id: model.id,
+ username: model.username
+ }
+ end
end
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index 7027ac4b5db..d4ba3a028be 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -30,7 +30,7 @@ class FileUploader < GitlabUploader
#
# Returns a String without a trailing slash
def self.dynamic_path_segment(model)
- File.join(CarrierWave.root, base_dir, model.full_path)
+ File.join(CarrierWave.root, base_dir, model.disk_path)
end
attr_accessor :model
diff --git a/app/validators/cluster_name_validator.rb b/app/validators/cluster_name_validator.rb
new file mode 100644
index 00000000000..13ec342f399
--- /dev/null
+++ b/app/validators/cluster_name_validator.rb
@@ -0,0 +1,24 @@
+# ClusterNameValidator
+#
+# Custom validator for ClusterName.
+class ClusterNameValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ if record.user?
+ unless value.present?
+ record.errors.add(attribute, " has to be present")
+ end
+ elsif record.gcp?
+ if record.persisted? && record.name_changed?
+ record.errors.add(attribute, " can not be changed because it's synchronized with provider")
+ end
+
+ unless value.length >= 1 && value.length <= 63
+ record.errors.add(attribute, " is invalid syntax")
+ end
+
+ unless value =~ Gitlab::Regex.kubernetes_namespace_regex
+ record.errors.add(attribute, Gitlab::Regex.kubernetes_namespace_regex_message)
+ end
+ end
+ end
+end
diff --git a/app/views/admin/hook_logs/_index.html.haml b/app/views/admin/hook_logs/_index.html.haml
index 7dd9943190f..91a8c0c62fe 100644
--- a/app/views/admin/hook_logs/_index.html.haml
+++ b/app/views/admin/hook_logs/_index.html.haml
@@ -24,7 +24,7 @@
%td
= truncate(hook_log.url, length: 50)
%td.light
- #{number_with_precision(hook_log.execution_duration, precision: 2)} ms
+ #{number_with_precision(hook_log.execution_duration, precision: 2)} sec
%td.light
= time_ago_with_tooltip(hook_log.created_at)
%td
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index 4d8754afdd2..c37d8ac45b9 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -14,7 +14,7 @@
= hidden_field_tag :namespace_id, params[:namespace_id]
- namespace = Namespace.find(params[:namespace_id])
- toggle_text = "#{namespace.kind}: #{namespace.full_path}"
- = dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { toggle_class: 'js-namespace-select large' })
+ = dropdown_toggle(toggle_text, { toggle: 'dropdown', is_filter: 'true' }, { toggle_class: 'js-namespace-select large' })
.dropdown-menu.dropdown-select.dropdown-menu-align-right
= dropdown_title('Namespaces')
= dropdown_filter("Search for Namespace")
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index ab4165c0bf2..42f92079d85 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -115,7 +115,7 @@
= f.label :new_namespace_id, "Namespace", class: 'control-label'
.col-sm-10
.dropdown
- = dropdown_toggle('Search for Namespace', { toggle: 'dropdown', field_name: 'new_namespace_id', show_any: 'false' }, { toggle_class: 'js-namespace-select large' })
+ = dropdown_toggle('Search for Namespace', { toggle: 'dropdown', field_name: 'new_namespace_id' }, { toggle_class: 'js-namespace-select large' })
.dropdown-menu.dropdown-select
= dropdown_title('Namespaces')
= dropdown_filter("Search for Namespace")
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index 76f4a817744..4965dffab9d 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -52,22 +52,23 @@
%br
- if @runners.any?
- .table-holder
- %table.table
- %thead
- %tr
- %th Type
- %th Runner token
- %th Description
- %th Version
- %th Projects
- %th Jobs
- %th Tags
- %th= link_to 'Last contact', admin_runners_path(params.slice(:search).merge(sort: 'contacted_asc'))
- %th
+ .runners-content
+ .table-holder
+ %table.table
+ %thead
+ %tr
+ %th Type
+ %th Runner token
+ %th Description
+ %th Version
+ %th Projects
+ %th Jobs
+ %th Tags
+ %th Last contact
+ %th
- - @runners.each do |runner|
- = render "admin/runners/runner", runner: runner
- = paginate @runners, theme: "gitlab"
+ - @runners.each do |runner|
+ = render "admin/runners/runner", runner: runner
+ = paginate @runners, theme: "gitlab"
- else
.nothing-here-block No runners found
diff --git a/app/views/ci/status/_badge.html.haml b/app/views/ci/status/_badge.html.haml
index 39c7fb0eba2..35a3563dff1 100644
--- a/app/views/ci/status/_badge.html.haml
+++ b/app/views/ci/status/_badge.html.haml
@@ -5,9 +5,9 @@
- if link && status.has_details?
= link_to status.details_path, class: css_classes, title: title do
- = custom_icon(status.icon)
+ = sprite_icon(status.icon)
= status.text
- else
%span{ class: css_classes, title: title }
- = custom_icon(status.icon)
+ = sprite_icon(status.icon)
= status.text
diff --git a/app/views/ci/status/_dropdown_graph_badge.html.haml b/app/views/ci/status/_dropdown_graph_badge.html.haml
index dcfb7f0c32d..c5b4439e273 100644
--- a/app/views/ci/status/_dropdown_graph_badge.html.haml
+++ b/app/views/ci/status/_dropdown_graph_badge.html.haml
@@ -7,13 +7,13 @@
- if status.has_details?
= link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip, container: 'body' } do
- %span{ class: klass }= custom_icon(status.icon)
+ %span{ class: klass }= sprite_icon(status.icon)
%span.ci-build-text= subject.name
- else
.menu-item.mini-pipeline-graph-dropdown-item{ data: { toggle: 'tooltip', title: tooltip, container: 'body' } }
- %span{ class: klass }= custom_icon(status.icon)
+ %span{ class: klass }= sprite_icon(status.icon)
%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
- = custom_icon(status.action_icon)
+ = 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
+ = sprite_icon(status.action_icon, css_class: "icon-action-#{status.action_icon}")
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index f62a0cd681e..a5686002328 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -8,7 +8,7 @@
%li.todos-pending{ class: active_when(params[:state].blank? || params[:state] == 'pending') }>
= link_to todos_filter_path(state: 'pending') do
%span
- To do
+ Todos
%span.badge
= number_with_delimiter(todos_pending_count)
%li.todos-done{ class: active_when(params[:state] == 'done') }>
diff --git a/app/views/groups/milestones/_form.html.haml b/app/views/groups/milestones/_form.html.haml
index cc879e5a308..a1be0d3220a 100644
--- a/app/views/groups/milestones/_form.html.haml
+++ b/app/views/groups/milestones/_form.html.haml
@@ -11,7 +11,7 @@
= f.label :description, "Description", class: "control-label"
.col-sm-10
= render layout: 'projects/md_preview', locals: { url: group_preview_markdown_path } do
- = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...'
+ = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...', supports_autocomplete: false
.clearfix
.error-alert
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index f82207559a3..66146e61263 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -274,7 +274,7 @@
Members
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(path: %w[members#show], html_options: { class: "fly-out-top-item" } ) do
- = link_to project_settings_members_path(@project) do
+ = link_to project_project_members_path(@project) do
%strong.fly-out-top-item-name
#{ _('Members') }
diff --git a/app/views/profiles/accounts/_reset_token.html.haml b/app/views/profiles/accounts/_reset_token.html.haml
deleted file mode 100644
index c31a4a8ecd4..00000000000
--- a/app/views/profiles/accounts/_reset_token.html.haml
+++ /dev/null
@@ -1,11 +0,0 @@
-- name = label.parameterize
-- attribute = name.underscore
-
-.reset-action
- %p.cgray
- = label_tag name, label, class: "label-light"
- = text_field_tag name, current_user.send(attribute), class: 'form-control', readonly: true, onclick: 'this.select()'
- %p.help-block
- = help_text
- .prepend-top-default
- = link_to button_label, [:reset, attribute, :profile], method: :put, data: { confirm: 'Are you sure?' }, class: 'btn btn-default private-token'
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index 7f79168dfb3..ced58dffcdc 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -9,22 +9,6 @@
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
- Private Tokens
- %p
- Keep these tokens secret, anyone with access to them can interact with
- GitLab as if they were you.
- .col-lg-8.private-tokens-reset
- = render partial: 'reset_token', locals: { label: 'Private token', button_label: 'Reset private token', help_text: 'Your private token is used to access the API and Atom feeds without username/password authentication.' }
-
- = render partial: 'reset_token', locals: { label: 'RSS token', button_label: 'Reset RSS token', help_text: 'Your RSS token is used to create urls for personalized RSS feeds.' }
-
- - if incoming_email_token_enabled?
- = render partial: 'reset_token', locals: { label: 'Incoming email token', button_label: 'Reset incoming email token', help_text: 'Your incoming email token is used to create new issues by email, and is included in your project-specific email addresses.' }
-
-%hr
-.row.prepend-top-default
- .col-lg-4.profile-settings-sidebar
- %h4.prepend-top-0
Two-Factor Authentication
%p
Increase your account's security by enabling Two-Factor Authentication (2FA).
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index 06bb72b9f0d..26c2e4c5936 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -30,3 +30,40 @@
= render "shared/personal_access_tokens_form", path: profile_personal_access_tokens_path, impersonation: false, token: @personal_access_token, scopes: @scopes
= render "shared/personal_access_tokens_table", impersonation: false, active_tokens: @active_personal_access_tokens, inactive_tokens: @inactive_personal_access_tokens
+
+%hr
+.row.prepend-top-default
+ .col-lg-4.profile-settings-sidebar
+ %h4.prepend-top-0
+ RSS token
+ %p
+ Your RSS token is used to authenticate you when your RSS reader loads a personalized RSS feed, and is included in your personal RSS feed URLs.
+ %p
+ It cannot be used to access any other data.
+ .col-lg-8.rss-token-reset
+ = label_tag :rss_token, 'RSS token', class: "label-light"
+ = text_field_tag :rss_token, current_user.rss_token, class: 'form-control', readonly: true, onclick: 'this.select()'
+ %p.help-block
+ Keep this token secret. Anyone who gets ahold of it can read activity and issue RSS feeds as if they were you.
+ You should
+ = link_to 'reset it', [:reset, :rss_token, :profile], method: :put, data: { confirm: 'Are you sure? Any RSS URLs currently in use will stop working.' }
+ if that ever happens.
+
+- if incoming_email_token_enabled?
+ %hr
+ .row.prepend-top-default
+ .col-lg-4.profile-settings-sidebar
+ %h4.prepend-top-0
+ Incoming email token
+ %p
+ Your incoming email token is used to authenticate you when you create a new issue by email, and is included in your personal project-specific email addresses.
+ %p
+ It cannot be used to access any other data.
+ .col-lg-8.incoming-email-token-reset
+ = label_tag :incoming_email_token, 'Incoming email token', class: "label-light"
+ = text_field_tag :incoming_email_token, current_user.incoming_email_token, class: 'form-control', readonly: true, onclick: 'this.select()'
+ %p.help-block
+ Keep this token secret. Anyone who gets ahold of it can create issues as if they were you.
+ You should
+ = link_to 'reset it', [:reset, :incoming_email_token, :profile], method: :put, data: { confirm: 'Are you sure? Any issue email addresses currently in use will stop working.' }
+ if that ever happens.
diff --git a/app/views/projects/_export.html.haml b/app/views/projects/_export.html.haml
index 623d3bc91c6..c5b1897c492 100644
--- a/app/views/projects/_export.html.haml
+++ b/app/views/projects/_export.html.haml
@@ -3,7 +3,7 @@
- project = local_assigns.fetch(:project)
- expanded = Rails.env.test?
-%section.settings
+%section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
Export project
@@ -11,7 +11,7 @@
= expanded ? 'Collapse' : 'Expand'
%p
Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page.
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
.bs-callout.bs-callout-info
%p.append-bottom-0
%p
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 619b632918e..1d644dda177 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -1,6 +1,5 @@
- empty_repo = @project.empty_repo?
- fork_network = @project.fork_network
-- forked_from_project = @project.forked_from_project || fork_network&.root_project
.project-home-panel.text-center{ class: ("empty-project" if empty_repo) }
.limit-container-width{ class: container_class }
.avatar-container.s70.project-avatar
@@ -16,13 +15,13 @@
- if @project.forked?
%p
- - if forked_from_project
+ - if @project.fork_source
#{ s_('ForkedFromProjectPath|Forked from') }
- = link_to project_path(forked_from_project) do
- = forked_from_project.full_name
+ = link_to project_path(@project.fork_source) do
+ = fork_source_name(@project)
- else
- deleted_message = s_('ForkedFromProjectPath|Forked from %{project_name} (deleted)')
- = deleted_message % { project_name: fork_network.deleted_root_project_name }
+ = deleted_message % { project_name: fork_source_name(@project) }
.project-repo-buttons
.count-buttons
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 49101d1efa4..6e02ae6c9cc 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -1,3 +1,4 @@
+- merged = local_assigns.fetch(:merged, false)
- commit = @repository.commit(branch.dereferenced_target)
- bar_graph_width_factor = @max_commits > 0 ? 100.0/@max_commits : 0
- diverging_commit_counts = @repository.diverging_commit_counts(branch)
@@ -12,7 +13,7 @@
&nbsp;
- if branch.name == @repository.root_ref
%span.label.label-primary default
- - elsif @repository.merged_to_root_ref? branch.name
+ - elsif merged
%span.label.label-info.has-tooltip{ title: s_('Branches|Merged into %{default_branch}') % { default_branch: @repository.root_ref } }
= s_('Branches|merged')
@@ -47,7 +48,7 @@
target: "#modal-delete-branch",
delete_path: project_branch_path(@project, branch.name),
branch_name: branch.name,
- is_merged: ("true" if @repository.merged_to_root_ref?(branch.name)) } }
+ is_merged: ("true" if merged) } }
= icon("trash-o")
- else
%button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled",
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index 7d9645d79e6..aade310236e 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -38,7 +38,7 @@
- if @branches.any?
%ul.content-list.all-branches
- @branches.each do |branch|
- = render "projects/branches/branch", branch: branch
+ = render "projects/branches/branch", branch: branch, merged: @repository.merged_to_root_ref?(branch, @merged_branch_names)
= paginate @branches, theme: 'gitlab'
- else
.nothing-here-block
diff --git a/app/views/projects/clusters/_form.html.haml b/app/views/projects/clusters/_form.html.haml
index 371cdb1e403..d8e5b55bb88 100644
--- a/app/views/projects/clusters/_form.html.haml
+++ b/app/views/projects/clusters/_form.html.haml
@@ -4,34 +4,32 @@
- link_to_help_page = link_to(s_('ClusterIntegration|help page'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Read our %{link_to_help_page} on cluster integration.').html_safe % { link_to_help_page: link_to_help_page}
- = form_for [@project.namespace.becomes(Namespace), @project, @cluster] do |field|
+ = form_for @cluster, url: namespace_project_clusters_path(@project.namespace, @project, @cluster), as: :cluster do |field|
+ = field.hidden_field :provider_type, value: :gcp
= form_errors(@cluster)
.form-group
- = field.label :gcp_cluster_name, s_('ClusterIntegration|Cluster name')
- = field.text_field :gcp_cluster_name, class: 'form-control'
+ = field.label :name, s_('ClusterIntegration|Cluster name')
+ = field.text_field :name, class: 'form-control'
- .form-group
- = field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project ID')
- = link_to(s_('ClusterIntegration|See your projects'), 'https://console.cloud.google.com/home/dashboard', target: '_blank', rel: 'noopener noreferrer')
- = field.text_field :gcp_project_id, class: 'form-control'
-
- .form-group
- = field.label :gcp_cluster_zone, s_('ClusterIntegration|Zone')
- = link_to(s_('ClusterIntegration|See zones'), 'https://cloud.google.com/compute/docs/regions-zones/regions-zones', target: '_blank', rel: 'noopener noreferrer')
- = field.text_field :gcp_cluster_zone, class: 'form-control', placeholder: 'us-central1-a'
+ = field.fields_for :provider_gcp, @cluster.provider_gcp do |provider_gcp_field|
+ .form-group
+ = provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project ID')
+ = link_to(s_('ClusterIntegration|See your projects'), 'https://console.cloud.google.com/home/dashboard', target: '_blank', rel: 'noopener noreferrer')
+ = provider_gcp_field.text_field :gcp_project_id, class: 'form-control'
- .form-group
- = field.label :gcp_cluster_size, s_('ClusterIntegration|Number of nodes')
- = field.text_field :gcp_cluster_size, class: 'form-control', placeholder: '3'
+ .form-group
+ = provider_gcp_field.label :zone, s_('ClusterIntegration|Zone')
+ = link_to(s_('ClusterIntegration|See zones'), 'https://cloud.google.com/compute/docs/regions-zones/regions-zones', target: '_blank', rel: 'noopener noreferrer')
+ = provider_gcp_field.text_field :zone, class: 'form-control', placeholder: 'us-central1-a'
- .form-group
- = field.label :gcp_machine_type, s_('ClusterIntegration|Machine type')
- = link_to(s_('ClusterIntegration|See machine types'), 'https://cloud.google.com/compute/docs/machine-types', target: '_blank', rel: 'noopener noreferrer')
- = field.text_field :gcp_machine_type, class: 'form-control', placeholder: 'n1-standard-4'
+ .form-group
+ = provider_gcp_field.label :num_nodes, s_('ClusterIntegration|Number of nodes')
+ = provider_gcp_field.text_field :num_nodes, class: 'form-control', placeholder: '3'
.form-group
- = field.label :project_namespace, s_('ClusterIntegration|Project namespace (optional, unique)')
- = field.text_field :project_namespace, class: 'form-control', placeholder: @cluster.project_namespace_placeholder
+ = provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type')
+ = link_to(s_('ClusterIntegration|See machine types'), 'https://cloud.google.com/compute/docs/machine-types', target: '_blank', rel: 'noopener noreferrer')
+ = provider_gcp_field.text_field :machine_type, class: 'form-control', placeholder: 'n1-standard-4'
.form-group
= field.submit s_('ClusterIntegration|Create cluster'), class: 'btn btn-save'
diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml
index b127e06030e..ebb9383ca12 100644
--- a/app/views/projects/clusters/show.html.haml
+++ b/app/views/projects/clusters/show.html.haml
@@ -10,9 +10,9 @@
cluster_status: @cluster.status_name,
cluster_status_reason: @cluster.status_reason } }
- %section.settings
+ %section.settings.no-animate.expanded
%h4= s_('ClusterIntegration|Enable cluster integration')
- .settings-content.expanded
+ .settings-content
.hidden.js-cluster-error.alert.alert-danger.alert-block.append-bottom-10{ role: 'alert' }
= s_('ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine')
@@ -33,7 +33,7 @@
- else
= s_('ClusterIntegration|Cluster integration is disabled for this project.')
- = form_for [@project.namespace.becomes(Namespace), @project, @cluster] do |field|
+ = form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
= form_errors(@cluster)
.form-group.append-bottom-20
%label.append-bottom-10
@@ -49,28 +49,28 @@
.form-group
= field.submit _('Save'), class: 'btn btn-success'
- %section.settings#js-cluster-details
+ %section.settings.no-animate#js-cluster-details{ class: ('expanded' if expanded) }
.settings-header
%h4= s_('ClusterIntegration|Cluster details')
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
%p= s_('ClusterIntegration|See and edit the details for your cluster')
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
.form_group.append-bottom-20
%label.append-bottom-10{ for: 'cluter-name' }
= s_('ClusterIntegration|Cluster name')
.input-group
- %input.form-control.cluster-name{ value: @cluster.gcp_cluster_name, disabled: true }
+ %input.form-control.cluster-name{ value: @cluster.name, disabled: true }
%span.input-group-addon.clipboard-addon
- = clipboard_button(text: @cluster.gcp_cluster_name, title: s_('ClusterIntegration|Copy cluster name'))
+ = clipboard_button(text: @cluster.name, title: s_('ClusterIntegration|Copy cluster name'))
- %section.settings#js-cluster-advanced-settings
+ %section.settings.no-animate#js-cluster-advanced-settings{ class: ('expanded' if expanded) }
.settings-header
%h4= _('Advanced settings')
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
%p= s_('ClusterIntegration|Manage Cluster integration on your GitLab project')
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
= render 'advanced_settings'
diff --git a/app/views/projects/deploy_keys/_index.html.haml b/app/views/projects/deploy_keys/_index.html.haml
index 45985a5ecef..e75ae87e771 100644
--- a/app/views/projects/deploy_keys/_index.html.haml
+++ b/app/views/projects/deploy_keys/_index.html.haml
@@ -1,5 +1,5 @@
- expanded = Rails.env.test?
-%section.settings
+%section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
Deploy Keys
@@ -7,7 +7,7 @@
= expanded ? 'Collapse' : 'Expand'
%p
Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one.
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
%h5.prepend-top-0
Create a new deploy key for this project
= render @deploy_keys.form_partial_path
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 893e536e289..5ebeae5c35f 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -4,7 +4,7 @@
- expanded = Rails.env.test?
.project-edit-container
- %section.settings.general-settings
+ %section.settings.general-settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
General project settings
@@ -12,7 +12,7 @@
= expanded ? 'Collapse' : 'Expand'
%p
Update your project name, description, avatar, and other general settings.
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
.project-edit-errors
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit-project" }, authenticity_token: true do |f|
%fieldset
@@ -61,7 +61,7 @@
= link_to 'Remove avatar', project_avatar_path(@project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar"
= f.submit 'Save changes', class: "btn btn-save"
- %section.settings.sharing-permissions
+ %section.settings.sharing-permissions.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
Permissions
@@ -69,13 +69,13 @@
= expanded ? 'Collapse' : 'Expand'
%p
Enable or disable certain project features and choose access levels.
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f|
%script.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project)
.js-project-permissions-form
= f.submit 'Save changes', class: "btn btn-save"
- %section.settings.merge-requests-feature{ class: ("hidden" if @project.project_feature.send(:merge_requests_access_level) == 0) }
+ %section.settings.merge-requests-feature.no-animate{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)] }
.settings-header
%h4
Merge request settings
@@ -83,14 +83,14 @@
= expanded ? 'Collapse' : 'Expand'
%p
Customize your merge request restrictions.
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "merge-request-settings-form" }, authenticity_token: true do |f|
= render 'merge_request_settings', form: f
= f.submit 'Save changes', class: "btn btn-save"
= render 'export', project: @project
- %section.settings.advanced-settings
+ %section.settings.advanced-settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
Advanced settings
@@ -98,7 +98,7 @@
= expanded ? 'Collapse' : 'Expand'
%p
Perform advanced options such as housekeeping, archiving, renaming, transferring, or removing your project.
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
.sub-section
%h4 Housekeeping
%p
@@ -173,7 +173,10 @@
%p
This will remove the fork relationship to source project
= succeed "." do
- = link_to @project.forked_from_project.name_with_namespace, project_path(@project.forked_from_project)
+ - if @project.fork_source
+ = link_to(fork_source_name(@project), project_path(@project.fork_source))
+ - else
+ = fork_source_name(@project)
= form_for([@project.namespace.becomes(Namespace), @project], url: remove_fork_project_path(@project), method: :delete, remote: true, html: { class: 'transfer-project' }) do |f|
%p
%strong Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source.
diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml
index 70156c03e3c..cce16bc58b3 100644
--- a/app/views/projects/graphs/show.html.haml
+++ b/app/views/projects/graphs/show.html.haml
@@ -1,5 +1,5 @@
- @no_container = true
-- page_title "Contributors"
+- page_title _('Contributors')
- content_for :page_specific_javascripts do
= webpack_bundle_tag('common_d3')
= webpack_bundle_tag('graphs')
@@ -7,23 +7,23 @@
.js-graphs-show{ class: container_class, 'data-project-graph-path': project_graph_path(@project, current_ref, format: :json) }
.sub-header-block
- .tree-ref-holder
+ .tree-ref-holder.inline.vertical-align-middle
= render 'shared/ref_switcher', destination: 'graphs'
- %ul.breadcrumb.repo-breadcrumb
- = commits_breadcrumbs
+ = link_to s_('Commits|History'), project_commits_path(@project, current_ref), class: 'btn'
.loading-graph
.center
%h3.page-title
%i.fa.fa-spinner.fa-spin
- Building repository graph.
- %p.slead Please wait a moment, this page will automatically refresh when ready.
+ = s_('ContributorsPage|Building repository graph.')
+ %p.slead
+ = s_('ContributorsPage|Please wait a moment, this page will automatically refresh when ready.')
.stat-graph.hide
.header.clearfix
%h3#date_header.page-title
%p.light
- Commits to #{@ref}, excluding merge commits. Limited to 6,000 commits.
+ = s_('ContributorsPage|Commits to %{branch_name}, excluding merge commits. Limited to 6,000 commits.') % { branch_name: @ref }
%input#brush_change{ :type => "hidden" }
.graphs.row
#contributors-master
diff --git a/app/views/projects/hook_logs/_index.html.haml b/app/views/projects/hook_logs/_index.html.haml
index 05b06cfc8b2..8096d9530c3 100644
--- a/app/views/projects/hook_logs/_index.html.haml
+++ b/app/views/projects/hook_logs/_index.html.haml
@@ -24,7 +24,7 @@
%td
= truncate(hook_log.url, length: 50)
%td.light
- #{number_with_precision(hook_log.execution_duration, precision: 2)} ms
+ #{number_with_precision(hook_log.execution_duration, precision: 2)} sec
%td.light
= time_ago_with_tooltip(hook_log.created_at)
%td
diff --git a/app/views/projects/issues/edit.html.haml b/app/views/projects/issues/edit.html.haml
new file mode 100644
index 00000000000..1b7d878c38c
--- /dev/null
+++ b/app/views/projects/issues/edit.html.haml
@@ -0,0 +1,7 @@
+- page_title "Edit", "#{@issue.title} (#{@issue.to_reference})", "Issues"
+
+%h3.page-title
+ Edit Issue ##{@issue.iid}
+%hr
+
+= render "form"
diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml
index 7da4ffd5e43..b5067367802 100644
--- a/app/views/projects/jobs/_sidebar.html.haml
+++ b/app/views/projects/jobs/_sidebar.html.haml
@@ -91,7 +91,7 @@
- builds.select{|build| build.status == build_status}.each do |build|
.build-job{ class: sidebar_build_class(build, @build), data: { stage: build.stage } }
= link_to project_job_path(@project, build) do
- = icon('arrow-right')
+ = sprite_icon('arrow-right', size:16, css_class: 'icon-arrow-right')
%span{ class: "ci-status-icon-#{build.status}" }
= ci_icon_for_status(build.status)
%span
@@ -100,4 +100,5 @@
- else
= build.id
- if build.retried?
- %i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' }
+ %span.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' }
+ = sprite_icon('retry', size:16, css_class: 'icon-retry')
diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml
index cb723fe6a18..72d5c4961ec 100644
--- a/app/views/projects/merge_requests/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/_mr_title.html.haml
@@ -34,7 +34,7 @@
%li{ class: [merge_request_button_visibility(@merge_request, true), 'js-close-item'] }
= link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request'
%li{ class: merge_request_button_visibility(@merge_request, false) }
- = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request'
+ = link_to 'Reopen', merge_request_path(@merge_request, merge_request: { state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request'
- if can_update_merge_request
= link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "hidden-xs hidden-sm btn btn-grouped issuable-edit"
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index a5153df1159..9fc297ab7f6 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -23,14 +23,18 @@
= milestone_date_range(@milestone)
.milestone-buttons
- if can?(current_user, :admin_milestone, @project)
+ = link_to edit_project_milestone_path(@project, @milestone), class: "btn btn-grouped btn-nr" do
+ Edit
+
+ - if @project.group
+ = link_to promote_project_milestone_path(@milestone.project, @milestone), title: "Promote to Group Milestone", class: 'btn btn-grouped', data: { confirm: "Promoting this milestone will make it available for all projects inside the group. Existing project milestones with the same name will be merged. Are you sure?", toggle: "tooltip" }, method: :post do
+ Promote
+
- if @milestone.active?
= link_to 'Close milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped"
- else
= link_to 'Reopen milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped"
- = link_to edit_project_milestone_path(@project, @milestone), class: "btn btn-grouped btn-nr" do
- Edit
-
= link_to project_milestone_path(@project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-danger" do
Delete
@@ -40,6 +44,7 @@
.detail-page-description.milestone-detail
%h2.title
= markdown_field(@milestone, :title)
+
%div
- if @milestone.description.present?
.description
diff --git a/app/views/projects/protected_branches/shared/_index.html.haml b/app/views/projects/protected_branches/shared/_index.html.haml
index 6a47cbdf724..ba7d98228c3 100644
--- a/app/views/projects/protected_branches/shared/_index.html.haml
+++ b/app/views/projects/protected_branches/shared/_index.html.haml
@@ -1,6 +1,6 @@
- expanded = Rails.env.test?
-%section.settings
+%section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
Protected Branches
@@ -8,7 +8,7 @@
= expanded ? 'Collapse' : 'Expand'
%p
Keep stable branches secure and force developers to use merge requests.
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
%p
By default, protected branches are designed to:
%ul
diff --git a/app/views/projects/protected_tags/shared/_index.html.haml b/app/views/projects/protected_tags/shared/_index.html.haml
index c07bd454ff6..e764a37bbd7 100644
--- a/app/views/projects/protected_tags/shared/_index.html.haml
+++ b/app/views/projects/protected_tags/shared/_index.html.haml
@@ -1,6 +1,6 @@
- expanded = Rails.env.test?
-%section.settings
+%section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
Protected Tags
@@ -8,7 +8,7 @@
= expanded ? 'Collapse' : 'Expand'
%p
Limit access to creating and updating tags.
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
%p
By default, protected tags are designed to:
%ul
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 62455d0d40d..664a4554692 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -4,7 +4,7 @@
- expanded = Rails.env.test?
-%section.settings#js-general-pipeline-settings
+%section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
General pipelines settings
@@ -12,10 +12,10 @@
= expanded ? 'Collapse' : 'Expand'
%p
Update your CI/CD configuration, like job timeout or Auto DevOps.
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
= render 'projects/pipelines_settings/show'
-%section.settings
+%section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
Runners settings
@@ -23,10 +23,10 @@
= expanded ? 'Collapse' : 'Expand'
%p
Register and see your runners for this project.
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
= render 'projects/runners/index'
-%section.settings
+%section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
Secret variables
@@ -35,10 +35,10 @@
= expanded ? 'Collapse' : 'Expand'
%p
= render "ci/variables/content"
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
= render 'ci/variables/index'
-%section.settings
+%section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
Pipeline triggers
@@ -48,5 +48,5 @@
Triggers can force a specific branch or tag to get rebuilt with an API call. These tokens will
impersonate their associated user including their access to projects and their project
permissions.
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
= render 'projects/triggers/index'
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index 7ea19e6c828..c02f7ee37ed 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -2,14 +2,14 @@
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'tree', path: @path, show_create: true
- - if show_new_repo?
+ - if show_new_repo? && can_push_branch?(@project, @ref)
.js-new-dropdown
- else
= render 'projects/tree/old_tree_header'
.tree-controls
- if show_new_repo?
- = render 'shared/repo/editable_mode'
+ .editable-mode
- else
= link_to s_('Commits|History'), project_commits_path(@project, @id), class: 'btn'
diff --git a/app/views/shared/_mini_pipeline_graph.html.haml b/app/views/shared/_mini_pipeline_graph.html.haml
index dff847159d3..901a177323b 100644
--- a/app/views/shared/_mini_pipeline_graph.html.haml
+++ b/app/views/shared/_mini_pipeline_graph.html.haml
@@ -7,7 +7,7 @@
.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) } }
- = custom_icon(icon_status)
+ = sprite_icon(icon_status)
= icon('caret-down')
%ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container
diff --git a/app/views/shared/hook_logs/_content.html.haml b/app/views/shared/hook_logs/_content.html.haml
index af6a499fadb..c80b179d525 100644
--- a/app/views/shared/hook_logs/_content.html.haml
+++ b/app/views/shared/hook_logs/_content.html.haml
@@ -11,7 +11,7 @@
= hook_log.trigger.singularize.titleize
%p
%strong Elapsed time:
- #{number_with_precision(hook_log.execution_duration, precision: 2)} ms
+ #{number_with_precision(hook_log.execution_duration, precision: 2)} sec
%p
%strong Request time:
= time_ago_with_tooltip(hook_log.created_at)
diff --git a/app/views/shared/icons/_icon_autodevops.svg b/app/views/shared/icons/_icon_autodevops.svg
index dde84e14048..423ca6d760d 100644
--- a/app/views/shared/icons/_icon_autodevops.svg
+++ b/app/views/shared/icons/_icon_autodevops.svg
@@ -29,7 +29,7 @@
</g>
<g fill-rule="nonzero" transform="rotate(15 -315.035 277.714)">
<path fill="#FFFFFF" d="M12.275,10.57 C13.986216,9.15630755 15.921048,8.03765363 18,7.26 L18,5.5 C18,2.463 20.47,0 23.493,0 L26.507,0 C27.9648848,0.000530018716 29.3628038,0.580386367 30.3930274,1.61192286 C31.4232511,2.64345935 32.0013267,4.04211574 32,5.5 L32,7.26 C34.098,8.043 36.03,9.17 37.725,10.57 L39.253,9.688 C41.8816141,8.17268496 45.2407537,9.07039379 46.763,11.695 L48.27,14.305 C48.9984289,15.5678669 49.1951495,17.0684426 48.8168566,18.4763972 C48.4385638,19.8843518 47.5162683,21.0842673 46.253,21.812 L44.728,22.693 C44.907,23.769 45,24.873 45,26 C45,27.127 44.907,28.231 44.728,29.307 L46.253,30.187 C48.8800379,31.705769 49.7822744,35.0642181 48.27,37.695 L46.763,40.305 C46.0335844,41.5673849 44.8323832,42.4881439 43.4238487,42.8645658 C42.0153143,43.2409877 40.5149245,43.0422119 39.253,42.312 L37.725,41.43 C36.013784,42.8436924 34.078952,43.9623464 32,44.74 L32,46.5 C32,49.537 29.53,52 26.507,52 L23.493,52 C22.0351152,51.99947 20.6371962,51.4196136 19.6069726,50.3880771 C18.5767489,49.3565406 17.9986733,47.9578843 18,46.5 L18,44.74 C15.921048,43.9623464 13.986216,42.8436924 12.275,41.43 L10.747,42.312 C8.11838594,43.827315 4.75924629,42.9296062 3.237,40.305 L1.73,37.695 C1.00157113,36.4321331 0.804850523,34.9315574 1.18314337,33.5236028 C1.56143621,32.1156482 2.48373172,30.9157327 3.747,30.188 L5.272,29.307 C5.09051204,28.2140265 4.9995366,27.107939 5,26 C5,24.873 5.093,23.769 5.272,22.693 L3.747,21.813 C1.11996213,20.294231 0.217725591,16.9357819 1.73,14.305 L3.237,11.695 C3.96641559,10.4326151 5.16761682,9.51185609 6.57615125,9.13543417 C7.98468568,8.75901226 9.48507553,8.95778814 10.747,9.688 L12.275,10.57 Z"/>
- <path class="animated spin infinite" fill="#E1DBF1" d="M17.9996486,7.25963195 L18.0000013,5.49772675 C18.0034459,2.46713881 20.4561478,0.00952173148 23.493,0 L26.507,0 C29.542757,0 32,2.46161709 32,5.5 L32,7.25850184 C34.0799663,8.03664754 36.0149544,9.15559094 37.7260175,10.5694605 L39.2547869,9.68691874 C41.8812087,8.17416302 45.2363972,9.06948854 46.7630175,11.6949424 L48.270687,14.3061027 C48.9989901,15.569417 49.1952874,17.0704122 48.816349,18.4785295 C48.4374106,19.8866468 47.5143145,21.0864021 46.2530682,21.8120114 L44.7278655,22.6926677 C44.9091017,23.7802451 45,24.8850821 45,26 C45,27.1144218 44.9091826,28.218078 44.7278653,29.3073326 L46.2547984,30.1889888 C48.8778516,31.7070439 49.7801588,35.0599752 48.2700175,37.6950576 L46.7625317,40.3058986 C46.0327098,41.5684739 44.8309328,42.4891542 43.4219037,42.8651509 C42.0128746,43.2411475 40.512172,43.0416186 39.2533538,42.312255 L37.7244858,41.4299789 C36.013753,42.8435912 34.0794396,43.9622923 32.0003514,44.7403681 L31.9999987,46.5022733 C31.9965541,49.5328612 29.5438522,51.9904783 26.507,52 L23.493,52 C20.457243,52 18,49.5383829 18,46.5 L18,44.7414988 C15.9200337,43.9633525 13.9850456,42.8444091 12.2739825,41.4305395 L10.7452131,42.3130813 C8.11879127,43.825837 4.76360277,42.9305115 3.23698247,40.3050576 L1.72931303,37.6938973 C1.0010099,36.430583 0.804712603,34.9295878 1.18365098,33.5214705 C1.56258936,32.1133532 2.48568546,30.9135979 3.74693178,30.1879886 L5.27213454,29.3073323 C5.09089825,28.2197549 5,27.114918 5.00000019,26.0008761 C4.99951488,24.8930059 5.0904571,23.7869854 5.27213502,22.6926675 L3.74520157,21.8110112 C1.12214836,20.2929561 0.219841192,16.9400248 1.72998247,14.3049424 L3.23746831,11.6941014 C3.96729024,10.4315261 5.16906725,9.51084579 6.5780963,9.13484913 C7.98712536,8.75885247 9.48782803,8.95838137 10.7466462,9.687745 L12.2748018,10.56961 C14.0209791,9.13635584 15.9392199,8.03072455 17.9996486,7.25963195 Z M13.7518374,14.537862 C13.108069,15.069723 12.2016163,15.1456339 11.4783538,14.728255 L8.74433999,13.1505123 C8.40103903,12.9516035 7.99274958,12.8973186 7.60940137,12.9996143 C7.22605315,13.10191 6.89909107,13.3523954 6.70101753,13.6950576 L5.19724591,16.2994454 C4.78547321,17.0179634 5.03203388,17.9341714 5.74706822,18.3479886 L8.47306822,19.9219886 C9.19530115,20.3390079 9.58295216,21.1604138 9.44574883,21.983032 L9.21798321,23.3486236 C9.07251948,24.2246212 8.99961081,25.111131 9,26 C9,26.8953847 9.0728258,27.7804297 9.21774883,28.649968 L9.44574883,30.016968 C9.58295216,30.8395862 9.19530115,31.6609921 8.47306822,32.0780114 L5.74435077,33.6535776 C5.40046982,33.851417 5.14932721,34.1778291 5.04623114,34.5609292 C4.94313508,34.9440294 4.9965408,35.3523984 5.19401753,35.6949424 L6.69795587,38.2996585 C7.11427713,39.0156351 8.03110189,39.260288 8.7470791,38.8479035 L11.4770791,37.2719035 C12.200376,36.8543519 13.1069795,36.9302031 13.7508374,37.462138 L14.8210499,38.3463136 C16.1898549,39.4774943 17.737648,40.3725891 19.3990866,40.9941596 L20.6990866,41.4791596 C21.4813437,41.7710017 22,42.5180761 22,43.353 L22,46.5 C22,47.3308348 22.6679761,48 23.493,48 L26.5007228,48.0000099 C27.328845,47.9974107 27.99906,47.3258525 28,46.5 L28,43.353 C28,42.5185702 28.5180515,41.771829 29.2996486,41.4796319 L30.599003,40.9938734 C32.261836,40.3715765 33.8093225,39.4764853 35.1790197,38.3444304 L36.2490197,37.4614304 C36.8927697,36.9301861 37.798736,36.8545694 38.5216462,37.271745 L41.25566,38.8494877 C41.598961,39.0483965 42.0072504,39.1026814 42.3905986,39.0003857 C42.7739468,38.89809 43.1009089,38.6476046 43.2989825,38.3049424 L44.8027541,35.7005546 C45.2145268,34.9820366 44.9679661,34.0658286 44.2529318,33.6520114 L41.5269318,32.0780114 C40.8046988,31.6609921 40.4170478,30.8395862 40.5542512,30.016968 L40.7821577,28.6505288 C40.9272286,27.7792134 41,26.8950523 41,26 C41,25.1046153 40.9271742,24.2195703 40.7822512,23.350032 L40.5542512,21.983032 C40.4170478,21.1604138 40.8046988,20.3390079 41.5269318,19.9219886 L44.2556492,18.3464224 C44.5995302,18.148583 44.8506728,17.8221709 44.9537689,17.4390708 C45.0568649,17.0559706 45.0034592,16.6476016 44.8059825,16.3050576 L43.3020441,13.7003415 C42.8857229,12.9843649 41.9688981,12.739712 41.2529209,13.1520965 L38.5229209,14.7280965 C37.799624,15.1456481 36.8930205,15.0697969 36.2491626,14.537862 L35.1789501,13.6536864 C33.8101451,12.5225057 32.262352,11.6274109 30.6021792,11.0063122 L29.3021792,10.5223122 C28.5192618,10.230826 28,9.48341836 28,8.648 L28,5.5 C28,4.66916515 27.3320239,4 26.507,4 L23.4992772,3.99999015 C22.671155,4.00258933 22.00094,4.67414748 22,5.5 L22,8.647 C22,9.48142977 21.4819485,10.228171 20.7003514,10.5203681 L19.400997,11.0061266 C17.738164,11.6284235 16.1906775,12.5235147 14.822142,13.6546103 C14.8121128,13.6628994 14.4553446,13.9573166 13.7518374,14.537862 Z"/>
+ <path class="animated spin-cw infinite" fill="#E1DBF1" d="M17.9996486,7.25963195 L18.0000013,5.49772675 C18.0034459,2.46713881 20.4561478,0.00952173148 23.493,0 L26.507,0 C29.542757,0 32,2.46161709 32,5.5 L32,7.25850184 C34.0799663,8.03664754 36.0149544,9.15559094 37.7260175,10.5694605 L39.2547869,9.68691874 C41.8812087,8.17416302 45.2363972,9.06948854 46.7630175,11.6949424 L48.270687,14.3061027 C48.9989901,15.569417 49.1952874,17.0704122 48.816349,18.4785295 C48.4374106,19.8866468 47.5143145,21.0864021 46.2530682,21.8120114 L44.7278655,22.6926677 C44.9091017,23.7802451 45,24.8850821 45,26 C45,27.1144218 44.9091826,28.218078 44.7278653,29.3073326 L46.2547984,30.1889888 C48.8778516,31.7070439 49.7801588,35.0599752 48.2700175,37.6950576 L46.7625317,40.3058986 C46.0327098,41.5684739 44.8309328,42.4891542 43.4219037,42.8651509 C42.0128746,43.2411475 40.512172,43.0416186 39.2533538,42.312255 L37.7244858,41.4299789 C36.013753,42.8435912 34.0794396,43.9622923 32.0003514,44.7403681 L31.9999987,46.5022733 C31.9965541,49.5328612 29.5438522,51.9904783 26.507,52 L23.493,52 C20.457243,52 18,49.5383829 18,46.5 L18,44.7414988 C15.9200337,43.9633525 13.9850456,42.8444091 12.2739825,41.4305395 L10.7452131,42.3130813 C8.11879127,43.825837 4.76360277,42.9305115 3.23698247,40.3050576 L1.72931303,37.6938973 C1.0010099,36.430583 0.804712603,34.9295878 1.18365098,33.5214705 C1.56258936,32.1133532 2.48568546,30.9135979 3.74693178,30.1879886 L5.27213454,29.3073323 C5.09089825,28.2197549 5,27.114918 5.00000019,26.0008761 C4.99951488,24.8930059 5.0904571,23.7869854 5.27213502,22.6926675 L3.74520157,21.8110112 C1.12214836,20.2929561 0.219841192,16.9400248 1.72998247,14.3049424 L3.23746831,11.6941014 C3.96729024,10.4315261 5.16906725,9.51084579 6.5780963,9.13484913 C7.98712536,8.75885247 9.48782803,8.95838137 10.7466462,9.687745 L12.2748018,10.56961 C14.0209791,9.13635584 15.9392199,8.03072455 17.9996486,7.25963195 Z M13.7518374,14.537862 C13.108069,15.069723 12.2016163,15.1456339 11.4783538,14.728255 L8.74433999,13.1505123 C8.40103903,12.9516035 7.99274958,12.8973186 7.60940137,12.9996143 C7.22605315,13.10191 6.89909107,13.3523954 6.70101753,13.6950576 L5.19724591,16.2994454 C4.78547321,17.0179634 5.03203388,17.9341714 5.74706822,18.3479886 L8.47306822,19.9219886 C9.19530115,20.3390079 9.58295216,21.1604138 9.44574883,21.983032 L9.21798321,23.3486236 C9.07251948,24.2246212 8.99961081,25.111131 9,26 C9,26.8953847 9.0728258,27.7804297 9.21774883,28.649968 L9.44574883,30.016968 C9.58295216,30.8395862 9.19530115,31.6609921 8.47306822,32.0780114 L5.74435077,33.6535776 C5.40046982,33.851417 5.14932721,34.1778291 5.04623114,34.5609292 C4.94313508,34.9440294 4.9965408,35.3523984 5.19401753,35.6949424 L6.69795587,38.2996585 C7.11427713,39.0156351 8.03110189,39.260288 8.7470791,38.8479035 L11.4770791,37.2719035 C12.200376,36.8543519 13.1069795,36.9302031 13.7508374,37.462138 L14.8210499,38.3463136 C16.1898549,39.4774943 17.737648,40.3725891 19.3990866,40.9941596 L20.6990866,41.4791596 C21.4813437,41.7710017 22,42.5180761 22,43.353 L22,46.5 C22,47.3308348 22.6679761,48 23.493,48 L26.5007228,48.0000099 C27.328845,47.9974107 27.99906,47.3258525 28,46.5 L28,43.353 C28,42.5185702 28.5180515,41.771829 29.2996486,41.4796319 L30.599003,40.9938734 C32.261836,40.3715765 33.8093225,39.4764853 35.1790197,38.3444304 L36.2490197,37.4614304 C36.8927697,36.9301861 37.798736,36.8545694 38.5216462,37.271745 L41.25566,38.8494877 C41.598961,39.0483965 42.0072504,39.1026814 42.3905986,39.0003857 C42.7739468,38.89809 43.1009089,38.6476046 43.2989825,38.3049424 L44.8027541,35.7005546 C45.2145268,34.9820366 44.9679661,34.0658286 44.2529318,33.6520114 L41.5269318,32.0780114 C40.8046988,31.6609921 40.4170478,30.8395862 40.5542512,30.016968 L40.7821577,28.6505288 C40.9272286,27.7792134 41,26.8950523 41,26 C41,25.1046153 40.9271742,24.2195703 40.7822512,23.350032 L40.5542512,21.983032 C40.4170478,21.1604138 40.8046988,20.3390079 41.5269318,19.9219886 L44.2556492,18.3464224 C44.5995302,18.148583 44.8506728,17.8221709 44.9537689,17.4390708 C45.0568649,17.0559706 45.0034592,16.6476016 44.8059825,16.3050576 L43.3020441,13.7003415 C42.8857229,12.9843649 41.9688981,12.739712 41.2529209,13.1520965 L38.5229209,14.7280965 C37.799624,15.1456481 36.8930205,15.0697969 36.2491626,14.537862 L35.1789501,13.6536864 C33.8101451,12.5225057 32.262352,11.6274109 30.6021792,11.0063122 L29.3021792,10.5223122 C28.5192618,10.230826 28,9.48341836 28,8.648 L28,5.5 C28,4.66916515 27.3320239,4 26.507,4 L23.4992772,3.99999015 C22.671155,4.00258933 22.00094,4.67414748 22,5.5 L22,8.647 C22,9.48142977 21.4819485,10.228171 20.7003514,10.5203681 L19.400997,11.0061266 C17.738164,11.6284235 16.1906775,12.5235147 14.822142,13.6546103 C14.8121128,13.6628994 14.4553446,13.9573166 13.7518374,14.537862 Z"/>
<g transform="rotate(15 -59.137 82.348)">
<circle cx="8" cy="8" r="8" fill="#FFFFFF" transform="translate(.035 6.008)"/>
<path fill="#6B4FBB" d="M7.40192379,14.7679492 C2.98364579,14.7679492 -0.598076211,11.1862272 -0.598076211,6.76794919 C-0.598076211,2.34967119 2.98364579,-1.23205081 7.40192379,-1.23205081 C11.8202018,-1.23205081 15.4019238,2.34967119 15.4019238,6.76794919 C15.4019238,11.1862272 11.8202018,14.7679492 7.40192379,14.7679492 Z M7.40192379,10.7679492 C9.61106279,10.7679492 11.4019238,8.97708819 11.4019238,6.76794919 C11.4019238,4.55881019 9.61106279,2.76794919 7.40192379,2.76794919 C5.19278479,2.76794919 3.40192379,4.55881019 3.40192379,6.76794919 C3.40192379,8.97708819 5.19278479,10.7679492 7.40192379,10.7679492 Z"/>
@@ -37,7 +37,7 @@
</g>
<g fill-rule="nonzero" transform="rotate(15 -402.968 460.884)">
<path fill="#FFFFFF" d="M9.82,8.53730769 C11.1889728,7.39547918 12.7368384,6.49195101 14.4,5.86384615 L14.4,4.44230769 C14.4,1.98934615 16.376,0 18.7944,0 L21.2056,0 C22.3719078,0.00042809204 23.4902431,0.468773604 24.314422,1.30193769 C25.1386009,2.13510179 25.6010613,3.26478579 25.6,4.44230769 L25.6,5.86384615 C27.2784,6.49626923 28.824,7.40653846 30.18,8.53730769 L31.4024,7.82492308 C33.5052912,6.60101478 36.192603,7.32608729 37.4104,9.44596154 L38.616,11.5540385 C39.1987431,12.5740464 39.3561196,13.7860498 39.0534853,14.9232439 C38.750851,16.060438 38.0130146,17.0296006 37.0024,17.6173846 L35.7824,18.3289615 C35.9256,19.1980385 36,20.0897308 36,21 C36,21.9102692 35.9256,22.8019615 35.7824,23.6710385 L37.0024,24.3818077 C39.1040303,25.6085057 39.8258195,28.3210992 38.616,30.4459615 L37.4104,32.5540385 C36.8268675,33.573657 35.8659065,34.317347 34.739079,34.6213801 C33.6122515,34.9254132 32.4119396,34.7648634 31.4024,34.1750769 L30.18,33.4626923 C28.8110272,34.6045208 27.2631616,35.508049 25.6,36.1361538 L25.6,37.5576923 C25.6,40.0106538 23.624,42 21.2056,42 L18.7944,42 C17.6280922,41.9995719 16.5097569,41.5312264 15.685578,40.6980623 C14.8613991,39.8648982 14.3989387,38.7352142 14.4,37.5576923 L14.4,36.1361538 C12.7368384,35.508049 11.1889728,34.6045208 9.82,33.4626923 L8.5976,34.1750769 C6.49470875,35.3989852 3.80739703,34.6739127 2.5896,32.5540385 L1.384,30.4459615 C0.8012569,29.4259536 0.643880418,28.2139502 0.946514692,27.0767561 C1.24914897,25.939562 1.98698538,24.9703994 2.9976,24.3826154 L4.2176,23.6710385 C4.07240963,22.7882521 3.99962928,21.8948738 4,21 C4,20.0897308 4.0744,19.1980385 4.2176,18.3289615 L2.9976,17.6181923 C0.895969702,16.3914943 0.174180473,13.6789008 1.384,11.5540385 L2.5896,9.44596154 C3.17313247,8.42634297 4.13409345,7.682653 5.260921,7.37861991 C6.38774855,7.07458682 7.58806043,7.23513658 8.5976,7.82492308 L9.82,8.53730769 Z"/>
- <path class="animated spin infinite" fill="#FEE1D3" d="M14.0000007,5.6038043 L14.0000013,4.44005609 C14.0029906,1.78475013 16.1390906,-0.376211234 18.7944,-0.384615385 L21.2056,-0.384615385 C23.8595941,-0.384615385 26,1.78021801 26,4.44230769 L26,5.60295806 C27.5208716,6.20655954 28.9434678,7.03621848 30.2204219,8.06411282 L31.1970056,7.49492104 C33.4941909,6.15907529 36.4301298,6.95005805 37.7609369,9.26076474 L38.9671983,11.3699991 C39.5988409,12.4761812 39.768854,13.7886936 39.4405746,15.0202941 C39.1116282,16.2543969 38.308799,17.3078735 37.2096539,17.946304 L36.2175721,18.5246428 C36.3390841,19.3401617 36.4,20.1667594 36.4,21 C36.4,21.8329668 36.339124,22.6588262 36.2175401,23.4753391 L37.2113882,24.0547082 C39.4944154,25.3886826 40.276605,28.3232105 38.9665369,30.6311583 L37.7604568,32.7400742 C37.1252608,33.8495148 36.0768547,34.6604208 34.8452776,34.9922248 C33.6111681,35.324711 32.2964469,35.1482289 31.195569,34.5042428 L30.2192355,33.9354047 C28.9426535,34.9630196 27.5206806,35.7924453 25.9999993,36.3961957 L25.9999987,37.5599439 C25.9970094,40.2152499 23.8609094,42.3762112 21.2056,42.3846154 L18.7944,42.3846154 C16.1404059,42.3846154 14,40.219782 14,37.5576923 L14,36.3970419 C12.4791284,35.7934405 11.0565322,34.9637815 9.77957815,33.9358872 L8.80299442,34.505079 C6.50580915,35.8409247 3.56987021,35.049942 2.23906313,32.7392353 L1.03280169,30.6300009 C0.401159146,29.5238188 0.231145999,28.2113064 0.559425405,26.9797059 C0.888371786,25.7456031 1.69120101,24.6921265 2.79034606,24.053696 L3.78242779,23.4753573 C3.66091587,22.6598457 3.60000002,21.8333228 3.60000019,21.0008678 C3.59964068,20.1722851 3.66061719,19.3449468 3.78254167,18.5247085 L2.78861183,17.9452918 C0.505584602,16.6113174 -0.276605002,13.6767895 1.03346313,11.3688417 L2.23954317,9.25992583 C2.87473915,8.15048519 3.92314533,7.33957919 5.15472238,7.00777521 C6.38883187,6.67528896 7.70355311,6.85177112 8.80443097,7.49575721 L9.78076186,8.06459377 C11.0573465,7.03698045 12.4793194,6.20755475 14.0000007,5.6038043 Z M11.2634746,12.0326234 C10.617233,12.5716613 9.7026973,12.6485026 8.97556903,12.2248582 L6.78774825,10.9501716 C6.60754053,10.8447551 6.39506809,10.8162338 6.19527576,10.8700606 C5.99295099,10.9245697 5.8183659,11.0596053 5.71133687,11.246543 L4.50892658,13.3490215 C4.28085652,13.7508163 4.41776119,14.2644394 4.80485394,14.4906191 L6.98565394,15.7619268 C7.70254629,16.1798426 8.08690703,16.9970357 7.95165511,17.8157512 L7.76948523,18.9184706 C7.65638664,19.6061109 7.59969735,20.3020342 7.6,21 C7.6,21.7031066 7.65662064,22.3978283 7.76925511,23.0801334 L7.95165511,24.1842488 C8.08690703,25.0029643 7.70254629,25.8201574 6.98565394,26.2380732 L4.80213007,27.5109659 C4.61772321,27.6180778 4.48116147,27.7972748 4.42448029,28.0099246 C4.36713215,28.2250767 4.39688141,28.454743 4.50573687,28.6453801 L5.70831165,30.7481858 C5.93243371,31.1373303 6.41410538,31.2670993 6.79049373,31.0482253 L8.97449373,29.7753023 C9.7016554,29.3514832 10.6163433,29.4282639 11.2626746,29.9673766 L12.1188867,30.6815536 C13.1796505,31.566598 14.3786867,32.2666727 15.6649769,32.7525215 L16.7049769,33.1442523 C17.4841581,33.4377419 18,34.1832625 18,35.0158846 L18,37.5576923 C18,38.02074 18.3597694,38.3846154 18.7944,38.3846154 L21.1992624,38.3846254 C21.6372484,38.3832375 21.9994819,38.0167881 22,37.5576923 L22,35.0158846 C22,34.18376 22.5152346,33.4385758 23.2937506,33.1447321 L24.3331012,32.7524389 C25.620867,32.2658727 26.8196661,31.5658006 27.8813806,30.679856 L28.7373806,29.9666637 C29.3836087,29.4282468 30.2976553,29.3517028 31.024431,29.7751418 L33.2122517,31.0498284 C33.3924595,31.1552449 33.6049319,31.1837662 33.8047242,31.1299394 C34.007049,31.0754303 34.1816341,30.9403947 34.2886631,30.753457 L35.4910734,28.6509785 C35.7191435,28.2491837 35.5822388,27.7355606 35.1951461,27.5093809 L33.0143461,26.2380732 C32.2974537,25.8201574 31.913093,25.0029643 32.0483449,24.1842488 L32.2306531,23.0806893 C32.3434217,22.3968737 32.4,21.7028459 32.4,21 C32.4,20.2968934 32.3433794,19.6021717 32.2307449,18.9198666 L32.0483449,17.8157512 C31.913093,16.9970357 32.2974537,16.1798426 33.0143461,15.7619268 L35.1978699,14.4890341 C35.3822768,14.3819222 35.5188385,14.2027252 35.5755197,13.9900754 C35.6328679,13.7749233 35.6031186,13.545257 35.4942631,13.3546199 L34.2916883,11.2518142 C34.0675663,10.8626697 33.5858946,10.7329007 33.2095063,10.9517747 L31.0255063,12.2246977 C30.2983446,12.6485168 29.3836567,12.5717361 28.7373254,12.0326234 L27.8811133,11.3184464 C26.8203495,10.433402 25.6213133,9.73332732 24.3362966,9.24795765 L23.2962966,8.85703457 C22.5164499,8.56389992 22,7.81804293 22,6.98492308 L22,4.44230769 C22,3.97925995 21.6402306,3.61538462 21.2056,3.61538462 L18.8007376,3.61537457 C18.3627516,3.61676247 18.0005181,3.98321188 18,4.44230769 L18,6.98411538 C18,7.81623999 17.4847654,8.56142419 16.7062494,8.85526793 L15.6668988,9.24756113 C14.379133,9.73412728 13.1803339,10.4341994 12.1197785,11.3191775 C12.1108094,11.3266617 11.8253748,11.564477 11.2634746,12.0326234 Z"/>
+ <path class="animated spin-ccw infinite" fill="#FEE1D3" d="M14.0000007,5.6038043 L14.0000013,4.44005609 C14.0029906,1.78475013 16.1390906,-0.376211234 18.7944,-0.384615385 L21.2056,-0.384615385 C23.8595941,-0.384615385 26,1.78021801 26,4.44230769 L26,5.60295806 C27.5208716,6.20655954 28.9434678,7.03621848 30.2204219,8.06411282 L31.1970056,7.49492104 C33.4941909,6.15907529 36.4301298,6.95005805 37.7609369,9.26076474 L38.9671983,11.3699991 C39.5988409,12.4761812 39.768854,13.7886936 39.4405746,15.0202941 C39.1116282,16.2543969 38.308799,17.3078735 37.2096539,17.946304 L36.2175721,18.5246428 C36.3390841,19.3401617 36.4,20.1667594 36.4,21 C36.4,21.8329668 36.339124,22.6588262 36.2175401,23.4753391 L37.2113882,24.0547082 C39.4944154,25.3886826 40.276605,28.3232105 38.9665369,30.6311583 L37.7604568,32.7400742 C37.1252608,33.8495148 36.0768547,34.6604208 34.8452776,34.9922248 C33.6111681,35.324711 32.2964469,35.1482289 31.195569,34.5042428 L30.2192355,33.9354047 C28.9426535,34.9630196 27.5206806,35.7924453 25.9999993,36.3961957 L25.9999987,37.5599439 C25.9970094,40.2152499 23.8609094,42.3762112 21.2056,42.3846154 L18.7944,42.3846154 C16.1404059,42.3846154 14,40.219782 14,37.5576923 L14,36.3970419 C12.4791284,35.7934405 11.0565322,34.9637815 9.77957815,33.9358872 L8.80299442,34.505079 C6.50580915,35.8409247 3.56987021,35.049942 2.23906313,32.7392353 L1.03280169,30.6300009 C0.401159146,29.5238188 0.231145999,28.2113064 0.559425405,26.9797059 C0.888371786,25.7456031 1.69120101,24.6921265 2.79034606,24.053696 L3.78242779,23.4753573 C3.66091587,22.6598457 3.60000002,21.8333228 3.60000019,21.0008678 C3.59964068,20.1722851 3.66061719,19.3449468 3.78254167,18.5247085 L2.78861183,17.9452918 C0.505584602,16.6113174 -0.276605002,13.6767895 1.03346313,11.3688417 L2.23954317,9.25992583 C2.87473915,8.15048519 3.92314533,7.33957919 5.15472238,7.00777521 C6.38883187,6.67528896 7.70355311,6.85177112 8.80443097,7.49575721 L9.78076186,8.06459377 C11.0573465,7.03698045 12.4793194,6.20755475 14.0000007,5.6038043 Z M11.2634746,12.0326234 C10.617233,12.5716613 9.7026973,12.6485026 8.97556903,12.2248582 L6.78774825,10.9501716 C6.60754053,10.8447551 6.39506809,10.8162338 6.19527576,10.8700606 C5.99295099,10.9245697 5.8183659,11.0596053 5.71133687,11.246543 L4.50892658,13.3490215 C4.28085652,13.7508163 4.41776119,14.2644394 4.80485394,14.4906191 L6.98565394,15.7619268 C7.70254629,16.1798426 8.08690703,16.9970357 7.95165511,17.8157512 L7.76948523,18.9184706 C7.65638664,19.6061109 7.59969735,20.3020342 7.6,21 C7.6,21.7031066 7.65662064,22.3978283 7.76925511,23.0801334 L7.95165511,24.1842488 C8.08690703,25.0029643 7.70254629,25.8201574 6.98565394,26.2380732 L4.80213007,27.5109659 C4.61772321,27.6180778 4.48116147,27.7972748 4.42448029,28.0099246 C4.36713215,28.2250767 4.39688141,28.454743 4.50573687,28.6453801 L5.70831165,30.7481858 C5.93243371,31.1373303 6.41410538,31.2670993 6.79049373,31.0482253 L8.97449373,29.7753023 C9.7016554,29.3514832 10.6163433,29.4282639 11.2626746,29.9673766 L12.1188867,30.6815536 C13.1796505,31.566598 14.3786867,32.2666727 15.6649769,32.7525215 L16.7049769,33.1442523 C17.4841581,33.4377419 18,34.1832625 18,35.0158846 L18,37.5576923 C18,38.02074 18.3597694,38.3846154 18.7944,38.3846154 L21.1992624,38.3846254 C21.6372484,38.3832375 21.9994819,38.0167881 22,37.5576923 L22,35.0158846 C22,34.18376 22.5152346,33.4385758 23.2937506,33.1447321 L24.3331012,32.7524389 C25.620867,32.2658727 26.8196661,31.5658006 27.8813806,30.679856 L28.7373806,29.9666637 C29.3836087,29.4282468 30.2976553,29.3517028 31.024431,29.7751418 L33.2122517,31.0498284 C33.3924595,31.1552449 33.6049319,31.1837662 33.8047242,31.1299394 C34.007049,31.0754303 34.1816341,30.9403947 34.2886631,30.753457 L35.4910734,28.6509785 C35.7191435,28.2491837 35.5822388,27.7355606 35.1951461,27.5093809 L33.0143461,26.2380732 C32.2974537,25.8201574 31.913093,25.0029643 32.0483449,24.1842488 L32.2306531,23.0806893 C32.3434217,22.3968737 32.4,21.7028459 32.4,21 C32.4,20.2968934 32.3433794,19.6021717 32.2307449,18.9198666 L32.0483449,17.8157512 C31.913093,16.9970357 32.2974537,16.1798426 33.0143461,15.7619268 L35.1978699,14.4890341 C35.3822768,14.3819222 35.5188385,14.2027252 35.5755197,13.9900754 C35.6328679,13.7749233 35.6031186,13.545257 35.4942631,13.3546199 L34.2916883,11.2518142 C34.0675663,10.8626697 33.5858946,10.7329007 33.2095063,10.9517747 L31.0255063,12.2246977 C30.2983446,12.6485168 29.3836567,12.5717361 28.7373254,12.0326234 L27.8811133,11.3184464 C26.8203495,10.433402 25.6213133,9.73332732 24.3362966,9.24795765 L23.2962966,8.85703457 C22.5164499,8.56389992 22,7.81804293 22,6.98492308 L22,4.44230769 C22,3.97925995 21.6402306,3.61538462 21.2056,3.61538462 L18.8007376,3.61537457 C18.3627516,3.61676247 18.0005181,3.98321188 18,4.44230769 L18,6.98411538 C18,7.81623999 17.4847654,8.56142419 16.7062494,8.85526793 L15.6668988,9.24756113 C14.379133,9.73412728 13.1803339,10.4341994 12.1197785,11.3191775 C12.1108094,11.3266617 11.8253748,11.564477 11.2634746,12.0326234 Z"/>
<g transform="rotate(15 -47.892 66.043)">
<ellipse cx="6.4" cy="6.462" fill="#FFFFFF" rx="6.4" ry="6.462" transform="translate(.028 4.853)"/>
<path fill="#FC6D26" d="M5.92153903,11.9125743 C2.3834711,11.9125743 -0.478460969,9.0231237 -0.478460969,5.4664205 C-0.478460969,1.9097173 2.3834711,-0.979733345 5.92153903,-0.979733345 C9.45960696,-0.979733345 12.321539,1.9097173 12.321539,5.4664205 C12.321539,9.0231237 9.45960696,11.9125743 5.92153903,11.9125743 Z M5.92153903,8.71257435 C7.6854047,8.71257435 9.12153903,7.26263103 9.12153903,5.4664205 C9.12153903,3.67020997 7.6854047,2.22026666 5.92153903,2.22026666 C4.15767337,2.22026666 2.72153903,3.67020997 2.72153903,5.4664205 C2.72153903,7.26263103 4.15767337,8.71257435 5.92153903,8.71257435 Z"/>
diff --git a/app/views/shared/issuable/_participants.html.haml b/app/views/shared/issuable/_participants.html.haml
deleted file mode 100644
index 3f553c9fede..00000000000
--- a/app/views/shared/issuable/_participants.html.haml
+++ /dev/null
@@ -1,18 +0,0 @@
-- participants_row = 7
-- participants_size = participants.size
-- participants_extra = participants_size - participants_row
-.block.participants
- .sidebar-collapsed-icon
- = icon('users')
- %span
- = participants.count
- .title.hide-collapsed
- = pluralize participants.count, "participant"
- .hide-collapsed.participants-list
- - participants.each do |participant|
- .participants-author.js-participants-author
- = link_to_member(@project, participant, name: false, size: 24, lazy_load: true)
- - if participants_extra > 0
- .hide-collapsed.participants-more
- %button.btn-transparent.btn-blank.js-participants-more{ type: 'button', data: { original_text: "+ #{participants_size - 7} more", less_text: "- show less" } }
- + #{participants_extra} more
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 7b7411b1e23..e0009a35b9f 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -123,17 +123,10 @@
%script#js-lock-issue-data{ type: "application/json" }= { is_locked: issuable.discussion_locked?, is_editable: can_edit_issuable }.to_json.html_safe
#js-lock-entry-point
- = render "shared/issuable/participants", participants: issuable.participants(current_user)
+ .js-sidebar-participants-entry-point
+
- if current_user
- - subscribed = issuable.subscribed?(current_user, @project)
- .block.light.subscription{ data: { url: toggle_subscription_path(issuable) } }
- .sidebar-collapsed-icon
- = icon('rss', 'aria-hidden': 'true')
- %span.issuable-header-text.hide-collapsed.pull-left
- Notifications
- - subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed'
- %button.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
- %span= subscribed ? 'Unsubscribe' : 'Subscribe'
+ .js-sidebar-subscriptions-entry-point
- project_ref = cross_project_reference(@project, issuable)
.block.project-reference
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index 305e2542281..7ba8f9d4313 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -49,6 +49,13 @@
= link_to edit_project_milestone_path(milestone.project, milestone), class: "btn btn-xs btn-grouped" do
Edit
\
+
+ - if @project.group
+ = link_to promote_project_milestone_path(milestone.project, milestone), title: "Promote to Group Milestone", class: 'btn btn-xs btn-grouped', data: { confirm: "Promoting this milestone will make it available for all projects inside the group. Existing project milestones with the same name will be merged. Are you sure?", toggle: "tooltip" }, method: :post do
+ Promote
+
= link_to 'Close Milestone', project_milestone_path(@project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close btn-grouped"
+
= link_to project_milestone_path(milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-xs btn-remove btn-grouped" do
Delete
+
diff --git a/app/views/shared/repo/_editable_mode.html.haml b/app/views/shared/repo/_editable_mode.html.haml
deleted file mode 100644
index 73fdb8b523f..00000000000
--- a/app/views/shared/repo/_editable_mode.html.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-.editable-mode
- %repo-edit-button
diff --git a/app/views/shared/repo/_repo.html.haml b/app/views/shared/repo/_repo.html.haml
index 7861f92b33f..5867ea58378 100644
--- a/app/views/shared/repo/_repo.html.haml
+++ b/app/views/shared/repo/_repo.html.haml
@@ -1,11 +1,12 @@
#repo{ data: { root: @path.empty?.to_s,
+ root_url: project_tree_path(project),
url: content_url,
+ current_branch: @ref,
+ ref: @commit.id,
project_name: project.name,
- refs_url: refs_project_path(project, format: :json),
project_url: project_path(project),
project_id: project.id,
- blob_url: namespace_project_blob_path(project.namespace, project, '{{branch}}'),
- new_mr_template_url: namespace_project_new_merge_request_path(project.namespace, project, merge_request: { source_branch: '{{source_branch}}' }),
+ new_merge_request_url: namespace_project_new_merge_request_path(project.namespace, project, merge_request: { source_branch: '' }),
can_commit: (!!can_push_branch?(project, @ref)).to_s,
on_top_of_branch: (!!on_top_of_branch?(project, @ref)).to_s,
current_path: @path } }
diff --git a/app/workers/cluster_provision_worker.rb b/app/workers/cluster_provision_worker.rb
index 63300b58a25..b01f9708424 100644
--- a/app/workers/cluster_provision_worker.rb
+++ b/app/workers/cluster_provision_worker.rb
@@ -3,8 +3,10 @@ class ClusterProvisionWorker
include ClusterQueue
def perform(cluster_id)
- Gcp::Cluster.find_by_id(cluster_id).try do |cluster|
- Ci::ProvisionClusterService.new.execute(cluster)
+ Clusters::Cluster.find_by_id(cluster_id).try do |cluster|
+ cluster.provider.try do |provider|
+ Clusters::Gcp::ProvisionService.new.execute(provider) if cluster.gcp?
+ end
end
end
end
diff --git a/app/workers/stuck_merge_jobs_worker.rb b/app/workers/stuck_merge_jobs_worker.rb
index 7843179d77c..a396c0f27b2 100644
--- a/app/workers/stuck_merge_jobs_worker.rb
+++ b/app/workers/stuck_merge_jobs_worker.rb
@@ -23,7 +23,7 @@ class StuckMergeJobsWorker
merge_requests = MergeRequest.where(id: completed_ids)
merge_requests.where.not(merge_commit_sha: nil).update_all(state: :merged)
- merge_requests.where(merge_commit_sha: nil).update_all(state: :opened)
+ merge_requests.where(merge_commit_sha: nil).update_all(state: :opened, merge_jid: nil)
Rails.logger.info("Updated state of locked merge jobs. JIDs: #{completed_jids.join(', ')}")
end
diff --git a/app/workers/update_merge_requests_worker.rb b/app/workers/update_merge_requests_worker.rb
index 89ae17cef37..150788ca611 100644
--- a/app/workers/update_merge_requests_worker.rb
+++ b/app/workers/update_merge_requests_worker.rb
@@ -2,6 +2,10 @@ class UpdateMergeRequestsWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
+ def metrics_tags
+ @metrics_tags || {}
+ end
+
def perform(project_id, user_id, oldrev, newrev, ref)
project = Project.find_by(id: project_id)
return unless project
@@ -9,6 +13,11 @@ class UpdateMergeRequestsWorker
user = User.find_by(id: user_id)
return unless user
+ @metrics_tags = {
+ project_id: project_id,
+ user_id: user_id
+ }
+
MergeRequests::RefreshService.new(project, user).execute(oldrev, newrev, ref)
end
end
diff --git a/app/workers/wait_for_cluster_creation_worker.rb b/app/workers/wait_for_cluster_creation_worker.rb
index 5aa3bbdaa9d..241ed3901dc 100644
--- a/app/workers/wait_for_cluster_creation_worker.rb
+++ b/app/workers/wait_for_cluster_creation_worker.rb
@@ -2,25 +2,10 @@ class WaitForClusterCreationWorker
include Sidekiq::Worker
include ClusterQueue
- INITIAL_INTERVAL = 2.minutes
- EAGER_INTERVAL = 10.seconds
- TIMEOUT = 20.minutes
-
def perform(cluster_id)
- Gcp::Cluster.find_by_id(cluster_id).try do |cluster|
- Ci::FetchGcpOperationService.new.execute(cluster) do |operation|
- case operation.status
- when 'RUNNING'
- if TIMEOUT < Time.now.utc - operation.start_time.to_time.utc
- return cluster.make_errored!("Cluster creation time exceeds timeout; #{TIMEOUT}")
- end
-
- WaitForClusterCreationWorker.perform_in(EAGER_INTERVAL, cluster.id)
- when 'DONE'
- Ci::FinalizeClusterCreationService.new.execute(cluster)
- else
- return cluster.make_errored!("Unexpected operation status; #{operation.status} #{operation.status_message}")
- end
+ Clusters::Cluster.find_by_id(cluster_id).try do |cluster|
+ cluster.provider.try do |provider|
+ Clusters::Gcp::VerifyProvisionStatusService.new.execute(provider) if cluster.gcp?
end
end
end
diff --git a/changelogs/unreleased/23206-load-participants-async.yml b/changelogs/unreleased/23206-load-participants-async.yml
new file mode 100644
index 00000000000..12ab43fb88f
--- /dev/null
+++ b/changelogs/unreleased/23206-load-participants-async.yml
@@ -0,0 +1,5 @@
+---
+title: Update participants and subscriptions button in issuable sidebar to be async
+merge_request: 14836
+author:
+type: changed
diff --git a/changelogs/unreleased/31454-missing-project-id-pipeline-hook-data.yml b/changelogs/unreleased/31454-missing-project-id-pipeline-hook-data.yml
new file mode 100644
index 00000000000..daf7ac715bd
--- /dev/null
+++ b/changelogs/unreleased/31454-missing-project-id-pipeline-hook-data.yml
@@ -0,0 +1,5 @@
+---
+title: Adds project_id to pipeline hook data
+merge_request: 15044
+author: Jacopo Beschi @jacopo-beschi
+type: added
diff --git a/changelogs/unreleased/3274-geo-route-whitelisting.yml b/changelogs/unreleased/3274-geo-route-whitelisting.yml
new file mode 100644
index 00000000000..43a5af80497
--- /dev/null
+++ b/changelogs/unreleased/3274-geo-route-whitelisting.yml
@@ -0,0 +1,5 @@
+---
+title: Tighten up whitelisting of certain Geo routes
+merge_request: 15082
+author:
+type: fixed
diff --git a/changelogs/unreleased/35914-merge-request-update-worker-is-slow.yml b/changelogs/unreleased/35914-merge-request-update-worker-is-slow.yml
new file mode 100644
index 00000000000..34bb76195af
--- /dev/null
+++ b/changelogs/unreleased/35914-merge-request-update-worker-is-slow.yml
@@ -0,0 +1,5 @@
+---
+title: Add metric tagging for sidekiq workers
+merge_request: 15111
+author:
+type: added
diff --git a/changelogs/unreleased/3674-hashed-storage-attachments.yml b/changelogs/unreleased/3674-hashed-storage-attachments.yml
new file mode 100644
index 00000000000..41bdb5fa568
--- /dev/null
+++ b/changelogs/unreleased/3674-hashed-storage-attachments.yml
@@ -0,0 +1,5 @@
+---
+title: Hashed Storage support for Attachments
+merge_request: 15068
+author:
+type: added
diff --git a/changelogs/unreleased/37473-expose-project-visibility-as-ci-variable.yml b/changelogs/unreleased/37473-expose-project-visibility-as-ci-variable.yml
new file mode 100644
index 00000000000..f6906a3b0e0
--- /dev/null
+++ b/changelogs/unreleased/37473-expose-project-visibility-as-ci-variable.yml
@@ -0,0 +1,5 @@
+---
+title: Expose project visibility as CI variable - CI_PROJECT_VISIBILITY
+merge_request: 15193
+author:
+type: added
diff --git a/changelogs/unreleased/37631-add-a-merge_request_diff_id-column-to-merge_requests.yml b/changelogs/unreleased/37631-add-a-merge_request_diff_id-column-to-merge_requests.yml
new file mode 100644
index 00000000000..a7127f49c16
--- /dev/null
+++ b/changelogs/unreleased/37631-add-a-merge_request_diff_id-column-to-merge_requests.yml
@@ -0,0 +1,5 @@
+---
+title: Add a latest_merge_request_diff_id column to merge_requests
+merge_request: 15035
+author:
+type: performance
diff --git a/changelogs/unreleased/38677-render-new-discussions-on-diff-tab.yml b/changelogs/unreleased/38677-render-new-discussions-on-diff-tab.yml
new file mode 100644
index 00000000000..9de6e54e3af
--- /dev/null
+++ b/changelogs/unreleased/38677-render-new-discussions-on-diff-tab.yml
@@ -0,0 +1,5 @@
+---
+title: Add new diff discussions on MR diffs tab in "realtime"
+merge_request: 14981
+author:
+type: fixed
diff --git a/changelogs/unreleased/39188-change-default-disabled-merge-message.yml b/changelogs/unreleased/39188-change-default-disabled-merge-message.yml
deleted file mode 100644
index 7de65f5c3f6..00000000000
--- a/changelogs/unreleased/39188-change-default-disabled-merge-message.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update default disabled merge request widget message to reflect a general failure
-merge_request: 14960
-author:
-type: changed
diff --git a/changelogs/unreleased/39417-todos-spelled-correctly-on-todos-list-page.yml b/changelogs/unreleased/39417-todos-spelled-correctly-on-todos-list-page.yml
new file mode 100644
index 00000000000..edf142f0311
--- /dev/null
+++ b/changelogs/unreleased/39417-todos-spelled-correctly-on-todos-list-page.yml
@@ -0,0 +1,5 @@
+---
+title: Todos spelled correctly on Todos list page
+merge_request: 15015
+author:
+type: changed
diff --git a/changelogs/unreleased/39495-fix-bitbucket-login.yml b/changelogs/unreleased/39495-fix-bitbucket-login.yml
deleted file mode 100644
index b48d557108b..00000000000
--- a/changelogs/unreleased/39495-fix-bitbucket-login.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix bitbucket login
-merge_request: 15051
-author:
-type: fixed
diff --git a/changelogs/unreleased/39509-fix-wiki-create-sidebar-overlap.yml b/changelogs/unreleased/39509-fix-wiki-create-sidebar-overlap.yml
new file mode 100644
index 00000000000..aebf6363d97
--- /dev/null
+++ b/changelogs/unreleased/39509-fix-wiki-create-sidebar-overlap.yml
@@ -0,0 +1,5 @@
+---
+title: Fix overlap of right-sidebar and main content when creating a Wiki page
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/39570-performance-bar-appears-enabled-even-though-it-won-t-show-up.yml b/changelogs/unreleased/39570-performance-bar-appears-enabled-even-though-it-won-t-show-up.yml
new file mode 100644
index 00000000000..66939d89d69
--- /dev/null
+++ b/changelogs/unreleased/39570-performance-bar-appears-enabled-even-though-it-won-t-show-up.yml
@@ -0,0 +1,5 @@
+---
+title: Allow to disable the Performance Bar
+merge_request: 15084
+author:
+type: fixed
diff --git a/changelogs/unreleased/39580-bump-carrierwave-to-1-2-1.yml b/changelogs/unreleased/39580-bump-carrierwave-to-1-2-1.yml
new file mode 100644
index 00000000000..bda85ac24e0
--- /dev/null
+++ b/changelogs/unreleased/39580-bump-carrierwave-to-1-2-1.yml
@@ -0,0 +1,5 @@
+---
+title: Bump carrierwave to 1.2.1
+merge_request: 15072
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/39582-nestingdepth-6.yml b/changelogs/unreleased/39582-nestingdepth-6.yml
new file mode 100644
index 00000000000..efe15f0a5f3
--- /dev/null
+++ b/changelogs/unreleased/39582-nestingdepth-6.yml
@@ -0,0 +1,5 @@
+---
+title: Enable NestingDepth (level 6) on scss-lint
+merge_request: 15073
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/39583-reopen-issue-count-cache.yml b/changelogs/unreleased/39583-reopen-issue-count-cache.yml
new file mode 100644
index 00000000000..ee35bcbcdae
--- /dev/null
+++ b/changelogs/unreleased/39583-reopen-issue-count-cache.yml
@@ -0,0 +1,5 @@
+---
+title: Refresh open Issue and Merge Request project counter caches when re-opening.
+merge_request: 15085
+author: Rob Ede @robjtede
+type: fixed
diff --git a/changelogs/unreleased/39593-emails-on-push-are-sent-to-only-the-first-recipient-when-using-aws-ses.yml b/changelogs/unreleased/39593-emails-on-push-are-sent-to-only-the-first-recipient-when-using-aws-ses.yml
new file mode 100644
index 00000000000..9a7109d054e
--- /dev/null
+++ b/changelogs/unreleased/39593-emails-on-push-are-sent-to-only-the-first-recipient-when-using-aws-ses.yml
@@ -0,0 +1,5 @@
+---
+title: Only set Auto-Submitted header once for emails on push
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/39619-cancel-merge-when-pipeline-succeeds-from-the-api-fails.yml b/changelogs/unreleased/39619-cancel-merge-when-pipeline-succeeds-from-the-api-fails.yml
new file mode 100644
index 00000000000..95251b46ecc
--- /dev/null
+++ b/changelogs/unreleased/39619-cancel-merge-when-pipeline-succeeds-from-the-api-fails.yml
@@ -0,0 +1,5 @@
+---
+title: Fix namespacing for MergeWhenPipelineSucceedsService in MR API
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/39704_fix_webhooks_log_time.yml b/changelogs/unreleased/39704_fix_webhooks_log_time.yml
new file mode 100644
index 00000000000..1234663e66b
--- /dev/null
+++ b/changelogs/unreleased/39704_fix_webhooks_log_time.yml
@@ -0,0 +1,5 @@
+---
+title: Fix webhooks recent deliveries
+merge_request: 15146
+author: Alexander Randa (@randaalex)
+type: fixed
diff --git a/changelogs/unreleased/39776-remove-responsive-table-bottom-border.yml b/changelogs/unreleased/39776-remove-responsive-table-bottom-border.yml
new file mode 100644
index 00000000000..52b6a267ced
--- /dev/null
+++ b/changelogs/unreleased/39776-remove-responsive-table-bottom-border.yml
@@ -0,0 +1,5 @@
+---
+title: Fix double border UI bug on pipelines/environments table and pagination
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/add-packagist-project-service.yml b/changelogs/unreleased/add-packagist-project-service.yml
new file mode 100644
index 00000000000..a13d00e91f7
--- /dev/null
+++ b/changelogs/unreleased/add-packagist-project-service.yml
@@ -0,0 +1,5 @@
+---
+title: Add Packagist project service
+merge_request: 14493
+author: Matt Coleman
+type: added
diff --git a/changelogs/unreleased/backport-workhorse-show-all-refs.yml b/changelogs/unreleased/backport-workhorse-show-all-refs.yml
new file mode 100644
index 00000000000..36dd2115152
--- /dev/null
+++ b/changelogs/unreleased/backport-workhorse-show-all-refs.yml
@@ -0,0 +1,5 @@
+---
+title: Support show-all-refs for git over HTTP
+merge_request: 14834
+author:
+type: added
diff --git a/changelogs/unreleased/bvl-circuitbreaker-backoff.yml b/changelogs/unreleased/bvl-circuitbreaker-backoff.yml
deleted file mode 100644
index 5cb90e7c085..00000000000
--- a/changelogs/unreleased/bvl-circuitbreaker-backoff.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Make the circuitbreaker more robust by adding higher thresholds, and multiple
- access attempts.
-merge_request: 14933
-author:
-type: fixed
diff --git a/changelogs/unreleased/bvl-circuitbreaker-improvements.yml b/changelogs/unreleased/bvl-circuitbreaker-improvements.yml
deleted file mode 100644
index 15cbd5592e9..00000000000
--- a/changelogs/unreleased/bvl-circuitbreaker-improvements.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Store circuitbreaker settings in the database instead of config
-merge_request: 14842
-author:
-type: changed
diff --git a/changelogs/unreleased/bvl-do-not-use-redis-keys.yml b/changelogs/unreleased/bvl-do-not-use-redis-keys.yml
deleted file mode 100644
index f703aad2065..00000000000
--- a/changelogs/unreleased/bvl-do-not-use-redis-keys.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Forbid the usage of `Redis#keys`
-merge_request: 14889
-author:
-type: fixed
diff --git a/changelogs/unreleased/bvl-dont-rename-free-names.yml b/changelogs/unreleased/bvl-dont-rename-free-names.yml
deleted file mode 100644
index 60a4ec8afbe..00000000000
--- a/changelogs/unreleased/bvl-dont-rename-free-names.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Don't rename paths that were freed up when upgrading
-merge_request: 15029
-author:
-type: fixed
diff --git a/changelogs/unreleased/bvl-fix-push-event-service-for-forks.yml b/changelogs/unreleased/bvl-fix-push-event-service-for-forks.yml
deleted file mode 100644
index 2a7d80270ac..00000000000
--- a/changelogs/unreleased/bvl-fix-push-event-service-for-forks.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Only cache last push event for existing projects when pushing to a fork
-merge_request: 14989
-author:
-type: fixed
diff --git a/changelogs/unreleased/bvl-unlink-fixes.yml b/changelogs/unreleased/bvl-unlink-fixes.yml
new file mode 100644
index 00000000000..685d78f479d
--- /dev/null
+++ b/changelogs/unreleased/bvl-unlink-fixes.yml
@@ -0,0 +1,5 @@
+---
+title: Fix issues with forked projects of which the source was deleted
+merge_request: 15150
+author:
+type: fixed
diff --git a/changelogs/unreleased/dm-add-sudo-scope.yml b/changelogs/unreleased/dm-add-sudo-scope.yml
new file mode 100644
index 00000000000..a0c173ce781
--- /dev/null
+++ b/changelogs/unreleased/dm-add-sudo-scope.yml
@@ -0,0 +1,6 @@
+---
+title: Add sudo scope for OAuth and Personal Access Tokens to be used by admins to
+ impersonate other users on the API
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/dm-convert-private-tokens.yml b/changelogs/unreleased/dm-convert-private-tokens.yml
new file mode 100644
index 00000000000..8f5145c897b
--- /dev/null
+++ b/changelogs/unreleased/dm-convert-private-tokens.yml
@@ -0,0 +1,5 @@
+---
+title: Convert private tokens to Personal Access Tokens with sudo scope
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/dm-remove-private-token-from-interface.yml b/changelogs/unreleased/dm-remove-private-token-from-interface.yml
new file mode 100644
index 00000000000..1b8996b08c3
--- /dev/null
+++ b/changelogs/unreleased/dm-remove-private-token-from-interface.yml
@@ -0,0 +1,5 @@
+---
+title: Remove private tokens from web interface and API
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/dm-remove-private-token.yml b/changelogs/unreleased/dm-remove-private-token.yml
new file mode 100644
index 00000000000..d721495721a
--- /dev/null
+++ b/changelogs/unreleased/dm-remove-private-token.yml
@@ -0,0 +1,5 @@
+---
+title: Remove Session API now that private tokens are removed from user API endpoints
+merge_request:
+author:
+type: removed
diff --git a/changelogs/unreleased/enable-scss-lint-mergeable-selector.yml b/changelogs/unreleased/enable-scss-lint-mergeable-selector.yml
new file mode 100644
index 00000000000..5f6e0cafe88
--- /dev/null
+++ b/changelogs/unreleased/enable-scss-lint-mergeable-selector.yml
@@ -0,0 +1,4 @@
+---
+title: Enable MergeableSelector in scss-lint
+merge_request: 12810
+author: Takuya Noguchi
diff --git a/changelogs/unreleased/feature-plantuml-restructured-text-captions.yml b/changelogs/unreleased/feature-plantuml-restructured-text-captions.yml
new file mode 100644
index 00000000000..3d8d0f4fcd1
--- /dev/null
+++ b/changelogs/unreleased/feature-plantuml-restructured-text-captions.yml
@@ -0,0 +1,5 @@
+---
+title: 'Support uml:: and captions in reStructuredText'
+merge_request: 15120
+author: Markus Koller
+type: changed
diff --git a/changelogs/unreleased/fix-500-on-old-merge-requests.yml b/changelogs/unreleased/fix-500-on-old-merge-requests.yml
new file mode 100644
index 00000000000..765d7466819
--- /dev/null
+++ b/changelogs/unreleased/fix-500-on-old-merge-requests.yml
@@ -0,0 +1,5 @@
+---
+title: Fix 500 errors caused by empty diffs in some discussions
+merge_request: 14945
+author: Alexander Popov
+type: fixed
diff --git a/changelogs/unreleased/fix-add-path-attr-to-wiki-file.yml b/changelogs/unreleased/fix-add-path-attr-to-wiki-file.yml
deleted file mode 100644
index 0847b5f6733..00000000000
--- a/changelogs/unreleased/fix-add-path-attr-to-wiki-file.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix broken wiki pages that link to a wiki file
-merge_request: 15019
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-project-select-js-without-button.yml b/changelogs/unreleased/fix-project-select-js-without-button.yml
new file mode 100644
index 00000000000..389ca2394f0
--- /dev/null
+++ b/changelogs/unreleased/fix-project-select-js-without-button.yml
@@ -0,0 +1,5 @@
+---
+title: Use project select dropdown not only as a combobutton
+merge_request: 15043
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-user-tab-activity-mobile.yml b/changelogs/unreleased/fix-user-tab-activity-mobile.yml
new file mode 100644
index 00000000000..a7e4fcb4355
--- /dev/null
+++ b/changelogs/unreleased/fix-user-tab-activity-mobile.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed user profile activity tab being off-screen on mobile
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix_global_board_routes_39073.yml b/changelogs/unreleased/fix_global_board_routes_39073.yml
deleted file mode 100644
index cc9ae8592db..00000000000
--- a/changelogs/unreleased/fix_global_board_routes_39073.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow boards as top level route
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix_migration_that_adds_ff_merge_field.yml b/changelogs/unreleased/fix_migration_that_adds_ff_merge_field.yml
new file mode 100644
index 00000000000..a1685497331
--- /dev/null
+++ b/changelogs/unreleased/fix_migration_that_adds_ff_merge_field.yml
@@ -0,0 +1,5 @@
+---
+title: Fix a migration that adds merge_requests_ff_only_enabled column to MR table
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/go-get-ssh.yml b/changelogs/unreleased/go-get-ssh.yml
new file mode 100644
index 00000000000..e485a94c6db
--- /dev/null
+++ b/changelogs/unreleased/go-get-ssh.yml
@@ -0,0 +1,5 @@
+---
+title: Returns a ssh url for go-get=1
+merge_request: 14990
+author: gvieira37
+type: fixed
diff --git a/changelogs/unreleased/issue_38777.yml b/changelogs/unreleased/issue_38777.yml
new file mode 100644
index 00000000000..5c49b2f7879
--- /dev/null
+++ b/changelogs/unreleased/issue_38777.yml
@@ -0,0 +1,5 @@
+---
+title: Allow promoting project milestones to group milestones
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/issue_39176.yml b/changelogs/unreleased/issue_39176.yml
new file mode 100644
index 00000000000..6255b51c094
--- /dev/null
+++ b/changelogs/unreleased/issue_39176.yml
@@ -0,0 +1,5 @@
+---
+title: Render 404 when polling commit notes without having permissions
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/jivl-fix-cancel-button-file-upload-new-issue.yml b/changelogs/unreleased/jivl-fix-cancel-button-file-upload-new-issue.yml
new file mode 100644
index 00000000000..0205d9626b1
--- /dev/null
+++ b/changelogs/unreleased/jivl-fix-cancel-button-file-upload-new-issue.yml
@@ -0,0 +1,5 @@
+---
+title: Fix cancel button not working while uploading on the new issue page
+merge_request: 15137
+author:
+type: fixed
diff --git a/changelogs/unreleased/jivl-mobile-friendly-table-runners.yml b/changelogs/unreleased/jivl-mobile-friendly-table-runners.yml
new file mode 100644
index 00000000000..3448b003ee0
--- /dev/null
+++ b/changelogs/unreleased/jivl-mobile-friendly-table-runners.yml
@@ -0,0 +1,5 @@
+---
+title: Mobile-friendly table on Admin Runners
+merge_request:
+author: Takuya Noguchi
+type: fixed
diff --git a/changelogs/unreleased/pawel-disable_nfs_metrics_checks_39730.yml b/changelogs/unreleased/pawel-disable_nfs_metrics_checks_39730.yml
new file mode 100644
index 00000000000..556d7d069d3
--- /dev/null
+++ b/changelogs/unreleased/pawel-disable_nfs_metrics_checks_39730.yml
@@ -0,0 +1,5 @@
+---
+title: Remove Filesystem check metrics that use too much CPU to handle requests
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/ph-multi-file-upload-file.yml b/changelogs/unreleased/ph-multi-file-upload-file.yml
new file mode 100644
index 00000000000..a2bd3cfe459
--- /dev/null
+++ b/changelogs/unreleased/ph-multi-file-upload-file.yml
@@ -0,0 +1,5 @@
+---
+title: Allow files to uploaded in the multi-file editor
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/refactor-group_links_controller.yml b/changelogs/unreleased/refactor-group_links_controller.yml
new file mode 100644
index 00000000000..af3d22c34cb
--- /dev/null
+++ b/changelogs/unreleased/refactor-group_links_controller.yml
@@ -0,0 +1,5 @@
+---
+title: Refactor GroupLinksController
+merge_request:
+author: 15121
+type: other
diff --git a/changelogs/unreleased/sh-disable-unicorn-sampling-sidekiq.yml b/changelogs/unreleased/sh-disable-unicorn-sampling-sidekiq.yml
new file mode 100644
index 00000000000..c4ed017dacd
--- /dev/null
+++ b/changelogs/unreleased/sh-disable-unicorn-sampling-sidekiq.yml
@@ -0,0 +1,5 @@
+---
+title: Disable Unicorn sampling in Sidekiq since there are no Unicorn sockets to monitor
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-fix-broken-redirection-relative-url-root.yml b/changelogs/unreleased/sh-fix-broken-redirection-relative-url-root.yml
new file mode 100644
index 00000000000..96e5195d247
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-broken-redirection-relative-url-root.yml
@@ -0,0 +1,5 @@
+---
+title: Fix broken Members link when relative URL root paths are used
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-container-registry-destroy.yml b/changelogs/unreleased/sh-fix-container-registry-destroy.yml
deleted file mode 100644
index 21a463da62a..00000000000
--- a/changelogs/unreleased/sh-fix-container-registry-destroy.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix deletion of container registry or images returning an error
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-fix-environment-slug-generation.yml b/changelogs/unreleased/sh-fix-environment-slug-generation.yml
new file mode 100644
index 00000000000..8a9c670c52c
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-environment-slug-generation.yml
@@ -0,0 +1,5 @@
+---
+title: Avoid regenerating the ref path for the environment
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-environment-write-ref.yml b/changelogs/unreleased/sh-fix-environment-write-ref.yml
deleted file mode 100644
index 8f291843ebe..00000000000
--- a/changelogs/unreleased/sh-fix-environment-write-ref.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix the writing of invalid environment refs
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/update-fe-i18n-guide.yml b/changelogs/unreleased/update-fe-i18n-guide.yml
new file mode 100644
index 00000000000..10bcf7836c6
--- /dev/null
+++ b/changelogs/unreleased/update-fe-i18n-guide.yml
@@ -0,0 +1,5 @@
+---
+title: Update i18n section in FE docs for marking and interpolation
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/use-git-branch-merged.yml b/changelogs/unreleased/use-git-branch-merged.yml
new file mode 100644
index 00000000000..24ec226250c
--- /dev/null
+++ b/changelogs/unreleased/use-git-branch-merged.yml
@@ -0,0 +1,5 @@
+---
+title: Improve branch listing page performance
+merge_request: 14729
+author:
+type: performance
diff --git a/changelogs/unreleased/winh-admin-projects-namespace-filter.yml b/changelogs/unreleased/winh-admin-projects-namespace-filter.yml
new file mode 100644
index 00000000000..7e906f446b0
--- /dev/null
+++ b/changelogs/unreleased/winh-admin-projects-namespace-filter.yml
@@ -0,0 +1,5 @@
+---
+title: Make NamespaceSelect change URL when filtering
+merge_request: 14888
+author:
+type: fixed
diff --git a/changelogs/unreleased/winh-i18n-contributors-page.yml b/changelogs/unreleased/winh-i18n-contributors-page.yml
new file mode 100644
index 00000000000..9b2611fc4fa
--- /dev/null
+++ b/changelogs/unreleased/winh-i18n-contributors-page.yml
@@ -0,0 +1,5 @@
+---
+title: Make contributors page translatable
+merge_request: 14915
+author:
+type: other
diff --git a/changelogs/unreleased/winh-namespace-rename-hooks.yml b/changelogs/unreleased/winh-namespace-rename-hooks.yml
new file mode 100644
index 00000000000..f5090b03b74
--- /dev/null
+++ b/changelogs/unreleased/winh-namespace-rename-hooks.yml
@@ -0,0 +1,5 @@
+---
+title: Add system hooks user_rename and group_rename
+merge_request: 15123
+author:
+type: changed
diff --git a/changelogs/unreleased/zj-commit-cache.yml b/changelogs/unreleased/zj-commit-cache.yml
new file mode 100644
index 00000000000..e3afe0ea7ef
--- /dev/null
+++ b/changelogs/unreleased/zj-commit-cache.yml
@@ -0,0 +1,5 @@
+---
+title: Cache commits fetched from the repository
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/zj-ruby-2-3-5.yml b/changelogs/unreleased/zj-ruby-2-3-5.yml
new file mode 100644
index 00000000000..09ec02417aa
--- /dev/null
+++ b/changelogs/unreleased/zj-ruby-2-3-5.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade Ruby to 2.3.5 to include security patches
+merge_request: 15099
+author:
+type: security
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 1edb6fd39b8..d09e51e766a 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -1,6 +1,7 @@
Rails.application.configure do
# Make sure the middleware is inserted first in middleware chain
config.middleware.insert_before('ActionDispatch::Static', 'Gitlab::Testing::RequestBlockerMiddleware')
+ config.middleware.insert_before('ActionDispatch::Static', 'Gitlab::Testing::RequestInspectorMiddleware')
# Settings specified here will take precedence over those in config/application.rb
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 4bfa5be0136..7547ba4a8fa 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -501,7 +501,7 @@ production: &base
# Gitaly settings
gitaly:
# Path to the directory containing Gitaly client executables.
- client_path: /home/git/gitaly
+ client_path: /home/git/gitaly/bin
# Default Gitaly authentication token. Can be overriden per storage. Can
# be left blank when Gitaly is running locally on a Unix socket, which
# is the normal way to deploy Gitaly.
diff --git a/config/initializers/8_metrics.rb b/config/initializers/8_metrics.rb
index e1a59d8c152..2d8704622b6 100644
--- a/config/initializers/8_metrics.rb
+++ b/config/initializers/8_metrics.rb
@@ -123,7 +123,9 @@ def instrument_classes(instrumentation)
end
# rubocop:enable Metrics/AbcSize
-Gitlab::Metrics::UnicornSampler.initialize_instance(Settings.monitoring.unicorn_sampler_interval).start
+unless Sidekiq.server?
+ Gitlab::Metrics::UnicornSampler.initialize_instance(Settings.monitoring.unicorn_sampler_interval).start
+end
Gitlab::Application.configure do |config|
# 0 should be Sentry to catch errors in this middleware
diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml
index 14d49885fb3..0da6b14c29e 100644
--- a/config/locales/doorkeeper.en.yml
+++ b/config/locales/doorkeeper.en.yml
@@ -58,9 +58,10 @@ en:
expired: "The access token expired"
unknown: "The access token is invalid"
scopes:
- api: Access your API
- read_user: Read user information
+ api: Access the authenticated user's API
+ read_user: Read the authenticated user's personal information
openid: Authenticate using OpenID Connect
+ sudo: Perform API actions as any user in the system (if the authenticated user is an admin)
flash:
applications:
diff --git a/config/routes/ci.rb b/config/routes/ci.rb
index cbd4c2db852..60c1724bc05 100644
--- a/config/routes/ci.rb
+++ b/config/routes/ci.rb
@@ -1,5 +1,5 @@
namespace :ci do
resource :lint, only: [:show, :create]
- root to: redirect('/')
+ root to: redirect('')
end
diff --git a/config/routes/profile.rb b/config/routes/profile.rb
index ddc852f0132..bcfc17a5f66 100644
--- a/config/routes/profile.rb
+++ b/config/routes/profile.rb
@@ -6,7 +6,6 @@ resource :profile, only: [:show, :update] do
get :audit_log
get :applications, to: 'oauth/applications#index'
- put :reset_private_token
put :reset_incoming_email_token
put :reset_rss_token
put :update_username
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 4b703ecb193..c52dd542c90 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -294,6 +294,7 @@ constraints(ProjectUrlConstrainer.new) do
resources :milestones, constraints: { id: /\d+/ } do
member do
+ post :promote
put :sort_issues
put :sort_merge_requests
get :merge_requests
@@ -394,7 +395,7 @@ constraints(ProjectUrlConstrainer.new) do
end
end
namespace :settings do
- get :members, to: redirect('/%{namespace_id}/%{project_id}/project_members')
+ get :members, to: redirect("%{namespace_id}/%{project_id}/project_members")
resource :ci_cd, only: [:show], controller: 'ci_cd'
resource :integrations, only: [:show]
resource :repository, only: [:show], controller: :repository
diff --git a/config/routes/snippets.rb b/config/routes/snippets.rb
index 0a4ebac3ca3..81bc890d86b 100644
--- a/config/routes/snippets.rb
+++ b/config/routes/snippets.rb
@@ -17,5 +17,5 @@ resources :snippets, concerns: :awardable do
end
end
-get '/s/:username', to: redirect('/u/%{username}/snippets'),
+get '/s/:username', to: redirect('u/%{username}/snippets'),
constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }
diff --git a/config/routes/user.rb b/config/routes/user.rb
index e682dcd6663..733a3f6ce9a 100644
--- a/config/routes/user.rb
+++ b/config/routes/user.rb
@@ -22,17 +22,17 @@ scope(constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }) d
get :contributed, as: :contributed_projects
get :snippets
get :exists
- get '/', to: redirect('/%{username}'), as: nil
+ get '/', to: redirect('%{username}'), as: nil
end
# Compatibility with old routing
# TODO (dzaporozhets): remove in 10.0
- get '/u/:username', to: redirect('/%{username}')
+ get '/u/:username', to: redirect('%{username}')
# TODO (dzaporozhets): remove in 9.0
- get '/u/:username/groups', to: redirect('/users/%{username}/groups')
- get '/u/:username/projects', to: redirect('/users/%{username}/projects')
- get '/u/:username/snippets', to: redirect('/users/%{username}/snippets')
- get '/u/:username/contributed', to: redirect('/users/%{username}/contributed')
+ get '/u/:username/groups', to: redirect('users/%{username}/groups')
+ get '/u/:username/projects', to: redirect('users/%{username}/projects')
+ get '/u/:username/snippets', to: redirect('users/%{username}/snippets')
+ get '/u/:username/contributed', to: redirect('users/%{username}/contributed')
end
constraints(UserUrlConstrainer.new) do
diff --git a/db/migrate/20150827121444_add_fast_forward_option_to_project.rb b/db/migrate/20150827121444_add_fast_forward_option_to_project.rb
index 6f22641077d..35df121519e 100644
--- a/db/migrate/20150827121444_add_fast_forward_option_to_project.rb
+++ b/db/migrate/20150827121444_add_fast_forward_option_to_project.rb
@@ -8,7 +8,11 @@ class AddFastForwardOptionToProject < ActiveRecord::Migration
disable_ddl_transaction!
def up
- add_column_with_default(:projects, :merge_requests_ff_only_enabled, :boolean, default: false)
+ # We put condition here because of a mistake we made a couple of years ago
+ # see https://gitlab.com/gitlab-org/gitlab-ce/issues/39382#note_45716103
+ unless column_exists?(:projects, :merge_requests_ff_only_enabled)
+ add_column_with_default(:projects, :merge_requests_ff_only_enabled, :boolean, default: false)
+ end
end
def down
diff --git a/db/migrate/20171012125712_migrate_user_authentication_token_to_personal_access_token.rb b/db/migrate/20171012125712_migrate_user_authentication_token_to_personal_access_token.rb
new file mode 100644
index 00000000000..9a909644a44
--- /dev/null
+++ b/db/migrate/20171012125712_migrate_user_authentication_token_to_personal_access_token.rb
@@ -0,0 +1,78 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MigrateUserAuthenticationTokenToPersonalAccessToken < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # disable_ddl_transaction!
+
+ TOKEN_NAME = 'Private Token'.freeze
+
+ def up
+ execute <<~SQL
+ INSERT INTO personal_access_tokens (user_id, token, name, created_at, updated_at, scopes)
+ SELECT id, authentication_token, '#{TOKEN_NAME}', NOW(), NOW(), '#{%w[api].to_yaml}'
+ FROM users
+ WHERE authentication_token IS NOT NULL
+ AND admin = FALSE
+ AND NOT EXISTS (
+ SELECT true
+ FROM personal_access_tokens
+ WHERE user_id = users.id
+ AND token = users.authentication_token
+ )
+ SQL
+
+ # Admins also need the `sudo` scope
+ execute <<~SQL
+ INSERT INTO personal_access_tokens (user_id, token, name, created_at, updated_at, scopes)
+ SELECT id, authentication_token, '#{TOKEN_NAME}', NOW(), NOW(), '#{%w[api sudo].to_yaml}'
+ FROM users
+ WHERE authentication_token IS NOT NULL
+ AND admin = TRUE
+ AND NOT EXISTS (
+ SELECT true
+ FROM personal_access_tokens
+ WHERE user_id = users.id
+ AND token = users.authentication_token
+ )
+ SQL
+ end
+
+ def down
+ if Gitlab::Database.postgresql?
+ execute <<~SQL
+ UPDATE users
+ SET authentication_token = pats.token
+ FROM (
+ SELECT user_id, token
+ FROM personal_access_tokens
+ WHERE name = '#{TOKEN_NAME}'
+ ) AS pats
+ WHERE id = pats.user_id
+ SQL
+ else
+ execute <<~SQL
+ UPDATE users
+ INNER JOIN personal_access_tokens AS pats
+ ON users.id = pats.user_id
+ SET authentication_token = pats.token
+ WHERE pats.name = '#{TOKEN_NAME}'
+ SQL
+ end
+
+ execute <<~SQL
+ DELETE FROM personal_access_tokens
+ WHERE name = '#{TOKEN_NAME}'
+ AND EXISTS (
+ SELECT true
+ FROM users
+ WHERE id = personal_access_tokens.user_id
+ AND authentication_token = personal_access_tokens.token
+ )
+ SQL
+ end
+end
diff --git a/db/migrate/20171013094327_create_new_clusters_architectures.rb b/db/migrate/20171013094327_create_new_clusters_architectures.rb
new file mode 100644
index 00000000000..dabb3e25e48
--- /dev/null
+++ b/db/migrate/20171013094327_create_new_clusters_architectures.rb
@@ -0,0 +1,68 @@
+class CreateNewClustersArchitectures < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ create_table :clusters do |t|
+ t.references :user, index: true, foreign_key: { on_delete: :nullify }
+
+ t.integer :provider_type
+ t.integer :platform_type
+
+ t.datetime_with_timezone :created_at, null: false
+ t.datetime_with_timezone :updated_at, null: false
+
+ t.boolean :enabled, index: true, default: true
+
+ t.string :name, null: false # If manual, read-write. If gcp, read-only.
+ end
+
+ create_table :cluster_projects do |t|
+ t.references :project, null: false, index: true, foreign_key: { on_delete: :cascade }
+ t.references :cluster, null: false, index: true, foreign_key: { on_delete: :cascade }
+
+ t.datetime_with_timezone :created_at, null: false
+ t.datetime_with_timezone :updated_at, null: false
+ end
+
+ create_table :cluster_platforms_kubernetes do |t|
+ t.references :cluster, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade }
+
+ t.datetime_with_timezone :created_at, null: false
+ t.datetime_with_timezone :updated_at, null: false
+
+ t.text :api_url
+ t.text :ca_cert
+
+ t.string :namespace
+
+ t.string :username
+ t.text :encrypted_password
+ t.string :encrypted_password_iv
+
+ t.text :encrypted_token
+ t.string :encrypted_token_iv
+ end
+
+ create_table :cluster_providers_gcp do |t|
+ t.references :cluster, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade }
+
+ t.integer :status
+ t.integer :num_nodes, null: false
+
+ t.datetime_with_timezone :created_at, null: false
+ t.datetime_with_timezone :updated_at, null: false
+
+ t.text :status_reason
+
+ t.string :gcp_project_id, null: false
+ t.string :zone, null: false
+ t.string :machine_type
+ t.string :operation_id
+
+ t.string :endpoint
+
+ t.text :encrypted_access_token
+ t.string :encrypted_access_token_iv
+ end
+ end
+end
diff --git a/db/migrate/20171025110159_add_latest_merge_request_diff_id_to_merge_requests.rb b/db/migrate/20171025110159_add_latest_merge_request_diff_id_to_merge_requests.rb
new file mode 100644
index 00000000000..74a2badc130
--- /dev/null
+++ b/db/migrate/20171025110159_add_latest_merge_request_diff_id_to_merge_requests.rb
@@ -0,0 +1,26 @@
+class AddLatestMergeRequestDiffIdToMergeRequests < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column :merge_requests, :latest_merge_request_diff_id, :integer
+ add_concurrent_index :merge_requests, :latest_merge_request_diff_id
+
+ add_concurrent_foreign_key :merge_requests, :merge_request_diffs,
+ column: :latest_merge_request_diff_id,
+ on_delete: :nullify
+ end
+
+ def down
+ remove_foreign_key :merge_requests, column: :latest_merge_request_diff_id
+
+ if index_exists?(:merge_requests, :latest_merge_request_diff_id)
+ remove_concurrent_index :merge_requests, :latest_merge_request_diff_id
+ end
+
+ remove_column :merge_requests, :latest_merge_request_diff_id
+ end
+end
diff --git a/db/post_migrate/20171012150314_remove_user_authentication_token.rb b/db/post_migrate/20171012150314_remove_user_authentication_token.rb
new file mode 100644
index 00000000000..d0f3aa06e98
--- /dev/null
+++ b/db/post_migrate/20171012150314_remove_user_authentication_token.rb
@@ -0,0 +1,20 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveUserAuthenticationToken < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ remove_column :users, :authentication_token
+ end
+
+ def down
+ add_column :users, :authentication_token, :string
+
+ add_concurrent_index :users, :authentication_token, unique: true
+ end
+end
diff --git a/db/post_migrate/20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb b/db/post_migrate/20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb
new file mode 100644
index 00000000000..4758c694563
--- /dev/null
+++ b/db/post_migrate/20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb
@@ -0,0 +1,99 @@
+class MigrateGcpClustersToNewClustersArchitectures < ActiveRecord::Migration
+ DOWNTIME = false
+
+ class GcpCluster < ActiveRecord::Base
+ self.table_name = 'gcp_clusters'
+
+ belongs_to :project, class_name: 'Project'
+
+ include EachBatch
+ end
+
+ class Cluster < ActiveRecord::Base
+ self.table_name = 'clusters'
+
+ has_many :cluster_projects, class_name: 'ClustersProject'
+ has_many :projects, through: :cluster_projects, class_name: 'Project'
+ has_one :provider_gcp, class_name: 'ProvidersGcp'
+ has_one :platform_kubernetes, class_name: 'PlatformsKubernetes'
+
+ accepts_nested_attributes_for :provider_gcp
+ accepts_nested_attributes_for :platform_kubernetes
+
+ enum platform_type: {
+ kubernetes: 1
+ }
+
+ enum provider_type: {
+ user: 0,
+ gcp: 1
+ }
+ end
+
+ class Project < ActiveRecord::Base
+ self.table_name = 'projects'
+
+ has_one :cluster_project, class_name: 'ClustersProject'
+ has_one :cluster, through: :cluster_project, class_name: 'Cluster'
+ end
+
+ class ClustersProject < ActiveRecord::Base
+ self.table_name = 'cluster_projects'
+
+ belongs_to :cluster, class_name: 'Cluster'
+ belongs_to :project, class_name: 'Project'
+ end
+
+ class ProvidersGcp < ActiveRecord::Base
+ self.table_name = 'cluster_providers_gcp'
+ end
+
+ class PlatformsKubernetes < ActiveRecord::Base
+ self.table_name = 'cluster_platforms_kubernetes'
+ end
+
+ def up
+ GcpCluster.all.find_each(batch_size: 1) do |gcp_cluster|
+ Cluster.create(
+ enabled: gcp_cluster.enabled,
+ user_id: gcp_cluster.user_id,
+ name: gcp_cluster.gcp_cluster_name,
+ provider_type: Cluster.provider_types[:gcp],
+ platform_type: Cluster.platform_types[:kubernetes],
+ projects: [gcp_cluster.project],
+ provider_gcp_attributes: {
+ status: gcp_cluster.status,
+ status_reason: gcp_cluster.status_reason,
+ gcp_project_id: gcp_cluster.gcp_project_id,
+ zone: gcp_cluster.gcp_cluster_zone,
+ num_nodes: gcp_cluster.gcp_cluster_size,
+ machine_type: gcp_cluster.gcp_machine_type,
+ operation_id: gcp_cluster.gcp_operation_id,
+ endpoint: gcp_cluster.endpoint,
+ encrypted_access_token: gcp_cluster.encrypted_gcp_token,
+ encrypted_access_token_iv: gcp_cluster.encrypted_gcp_token_iv
+ },
+ platform_kubernetes_attributes: {
+ cluster_id: gcp_cluster.id,
+ api_url: api_url(gcp_cluster.endpoint),
+ ca_cert: gcp_cluster.ca_cert,
+ namespace: gcp_cluster.project_namespace,
+ username: gcp_cluster.username,
+ encrypted_password: gcp_cluster.encrypted_password,
+ encrypted_password_iv: gcp_cluster.encrypted_password_iv,
+ encrypted_token: gcp_cluster.encrypted_kubernetes_token,
+ encrypted_token_iv: gcp_cluster.encrypted_kubernetes_token_iv
+ } )
+ end
+ end
+
+ def down
+ execute('DELETE FROM clusters')
+ end
+
+ private
+
+ def api_url(endpoint)
+ endpoint ? 'https://' + endpoint : nil
+ end
+end
diff --git a/db/post_migrate/20171026082505_populate_merge_requests_latest_merge_request_diff_id.rb b/db/post_migrate/20171026082505_populate_merge_requests_latest_merge_request_diff_id.rb
new file mode 100644
index 00000000000..a7ebbbf34c0
--- /dev/null
+++ b/db/post_migrate/20171026082505_populate_merge_requests_latest_merge_request_diff_id.rb
@@ -0,0 +1,27 @@
+class PopulateMergeRequestsLatestMergeRequestDiffId < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ BATCH_SIZE = 1_000
+
+ class MergeRequest < ActiveRecord::Base
+ self.table_name = 'merge_requests'
+
+ include ::EachBatch
+ end
+
+ disable_ddl_transaction!
+
+ def up
+ update = '
+ latest_merge_request_diff_id = (
+ SELECT MAX(id)
+ FROM merge_request_diffs
+ WHERE merge_requests.id = merge_request_diffs.merge_request_id
+ )'.squish
+
+ MergeRequest.where(latest_merge_request_diff_id: nil).each_batch(of: BATCH_SIZE) do |relation|
+ relation.update_all(update)
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 530f08022be..f88abaab86e 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: 20171017145932) do
+ActiveRecord::Schema.define(version: 20171026082505) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -462,6 +462,63 @@ ActiveRecord::Schema.define(version: 20171017145932) do
add_index "ci_variables", ["project_id", "key", "environment_scope"], name: "index_ci_variables_on_project_id_and_key_and_environment_scope", unique: true, using: :btree
+ create_table "cluster_platforms_kubernetes", force: :cascade do |t|
+ t.integer "cluster_id", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.text "api_url"
+ t.text "ca_cert"
+ t.string "namespace"
+ t.string "username"
+ t.text "encrypted_password"
+ t.string "encrypted_password_iv"
+ t.text "encrypted_token"
+ t.string "encrypted_token_iv"
+ end
+
+ add_index "cluster_platforms_kubernetes", ["cluster_id"], name: "index_cluster_platforms_kubernetes_on_cluster_id", unique: true, using: :btree
+
+ create_table "cluster_projects", force: :cascade do |t|
+ t.integer "project_id", null: false
+ t.integer "cluster_id", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ end
+
+ add_index "cluster_projects", ["cluster_id"], name: "index_cluster_projects_on_cluster_id", using: :btree
+ add_index "cluster_projects", ["project_id"], name: "index_cluster_projects_on_project_id", using: :btree
+
+ create_table "cluster_providers_gcp", force: :cascade do |t|
+ t.integer "cluster_id", null: false
+ t.integer "status"
+ t.integer "num_nodes", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.text "status_reason"
+ t.string "gcp_project_id", null: false
+ t.string "zone", null: false
+ t.string "machine_type"
+ t.string "operation_id"
+ t.string "endpoint"
+ t.text "encrypted_access_token"
+ t.string "encrypted_access_token_iv"
+ end
+
+ add_index "cluster_providers_gcp", ["cluster_id"], name: "index_cluster_providers_gcp_on_cluster_id", unique: true, using: :btree
+
+ create_table "clusters", force: :cascade do |t|
+ t.integer "user_id"
+ t.integer "provider_type"
+ t.integer "platform_type"
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.boolean "enabled", default: true
+ t.string "name", null: false
+ end
+
+ add_index "clusters", ["enabled"], name: "index_clusters_on_enabled", using: :btree
+ add_index "clusters", ["user_id"], name: "index_clusters_on_user_id", using: :btree
+
create_table "container_repositories", force: :cascade do |t|
t.integer "project_id", null: false
t.string "name", null: false
@@ -973,6 +1030,7 @@ ActiveRecord::Schema.define(version: 20171017145932) do
t.boolean "ref_fetched"
t.string "merge_jid"
t.boolean "discussion_locked"
+ t.integer "latest_merge_request_diff_id"
end
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
@@ -981,6 +1039,7 @@ ActiveRecord::Schema.define(version: 20171017145932) do
add_index "merge_requests", ["deleted_at"], name: "index_merge_requests_on_deleted_at", using: :btree
add_index "merge_requests", ["description"], name: "index_merge_requests_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
add_index "merge_requests", ["head_pipeline_id"], name: "index_merge_requests_on_head_pipeline_id", using: :btree
+ add_index "merge_requests", ["latest_merge_request_diff_id"], name: "index_merge_requests_on_latest_merge_request_diff_id", 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", "source_branch"], name: "index_merge_requests_on_source_project_id_and_source_branch", using: :btree
@@ -1670,7 +1729,6 @@ ActiveRecord::Schema.define(version: 20171017145932) do
t.string "skype", default: "", null: false
t.string "linkedin", default: "", null: false
t.string "twitter", default: "", null: false
- t.string "authentication_token"
t.string "bio"
t.integer "failed_attempts", default: 0
t.datetime "locked_at"
@@ -1720,7 +1778,6 @@ ActiveRecord::Schema.define(version: 20171017145932) do
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", ["created_at"], name: "index_users_on_created_at", using: :btree
add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
@@ -1810,6 +1867,11 @@ ActiveRecord::Schema.define(version: 20171017145932) do
add_foreign_key "ci_triggers", "projects", name: "fk_e3e63f966e", on_delete: :cascade
add_foreign_key "ci_triggers", "users", column: "owner_id", name: "fk_e8e10d1964", on_delete: :cascade
add_foreign_key "ci_variables", "projects", name: "fk_ada5eb64b3", on_delete: :cascade
+ add_foreign_key "cluster_platforms_kubernetes", "clusters", on_delete: :cascade
+ add_foreign_key "cluster_projects", "clusters", on_delete: :cascade
+ add_foreign_key "cluster_projects", "projects", on_delete: :cascade
+ add_foreign_key "cluster_providers_gcp", "clusters", on_delete: :cascade
+ add_foreign_key "clusters", "users", on_delete: :nullify
add_foreign_key "container_repositories", "projects"
add_foreign_key "deploy_keys_projects", "projects", name: "fk_58a901ca7e", on_delete: :cascade
add_foreign_key "deployments", "projects", name: "fk_b9a3851b82", on_delete: :cascade
@@ -1846,6 +1908,7 @@ ActiveRecord::Schema.define(version: 20171017145932) do
add_foreign_key "merge_request_metrics", "ci_pipelines", column: "pipeline_id", on_delete: :cascade
add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade
add_foreign_key "merge_requests", "ci_pipelines", column: "head_pipeline_id", name: "fk_fd82eae0b9", on_delete: :nullify
+ add_foreign_key "merge_requests", "merge_request_diffs", column: "latest_merge_request_diff_id", name: "fk_06067f5644", on_delete: :nullify
add_foreign_key "merge_requests", "projects", column: "target_project_id", name: "fk_a6963e8447", on_delete: :cascade
add_foreign_key "merge_requests_closing_issues", "issues", on_delete: :cascade
add_foreign_key "merge_requests_closing_issues", "merge_requests", on_delete: :cascade
diff --git a/doc/README.md b/doc/README.md
index 5eabc126b95..7c33a708dc7 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -1,5 +1,6 @@
---
toc: false
+comments: false
---
# GitLab Documentation
diff --git a/doc/administration/auth/README.md b/doc/administration/auth/README.md
index 13bd501e397..ee9b9a9466a 100644
--- a/doc/administration/auth/README.md
+++ b/doc/administration/auth/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Authentication and Authorization
GitLab integrates with the following external authentication and authorization
diff --git a/doc/administration/integration/plantuml.md b/doc/administration/integration/plantuml.md
index 652ca9cf454..93c3642a1f1 100644
--- a/doc/administration/integration/plantuml.md
+++ b/doc/administration/integration/plantuml.md
@@ -56,29 +56,34 @@ that, login with an Admin account and do following:
With PlantUML integration enabled and configured, we can start adding diagrams to
our AsciiDoc snippets, wikis and repos using delimited blocks:
-```
-[plantuml, format="png", id="myDiagram", width="200px"]
---
-Bob->Alice : hello
-Alice -> Bob : Go Away
---
-```
+- **Markdown**
+
+ ```plantuml
+ Bob -> Alice : hello
+ Alice -> Bob : Go Away
+ ```
-And in Markdown using fenced code blocks:
+- **AsciiDoc**
- ```plantuml
- Bob -> Alice : hello
+ ```
+ [plantuml, format="png", id="myDiagram", width="200px"]
+ --
+ Bob->Alice : hello
Alice -> Bob : Go Away
+ --
```
-And in reStructuredText using a directive:
+- **reStructuredText**
-```
-.. plantuml::
+ ```
+ .. plantuml::
+ :caption: Caption with **bold** and *italic*
- Bob -> Alice: hello
- Alice -> Bob: Go Away
-```
+ Bob -> Alice: hello
+ Alice -> Bob: Go Away
+ ```
+
+ You can also use the `uml::` directive for compatibility with [sphinxcontrib-plantuml](https://pypi.python.org/pypi/sphinxcontrib-plantuml), but please note that we currently only support the `caption` option.
The above blocks will be converted to an HTML img tag with source pointing to the
PlantUML instance. If the PlantUML server is correctly configured, this should
diff --git a/doc/administration/monitoring/performance/performance_bar.md b/doc/administration/monitoring/performance/performance_bar.md
index 68efe0aae5c..b9464945cea 100644
--- a/doc/administration/monitoring/performance/performance_bar.md
+++ b/doc/administration/monitoring/performance/performance_bar.md
@@ -28,6 +28,12 @@ will be allowed to display the Performance Bar.
Make sure _Enable the Performance Bar_ is checked and hit
**Save** to save the changes.
+Once the Performance Bar is enabled, you will need to press the [<kbd>p</kbd> +
+<kbd>b</kbd> keyboard shortcut](../../../workflow/shortcuts.md) to actually
+display it.
+
+You can toggle the Bar using the same shortcut.
+
---
![GitLab Performance Bar Admin Settings](img/performance_bar_configuration_settings.png)
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index 6baae20d16a..11d5e077a36 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -20,7 +20,7 @@ it, the client IP needs to be [included in a whitelist][whitelist].
Currently the embedded Prometheus server is not automatically configured to
collect metrics from this endpoint. We recommend setting up another Prometheus
server, because the embedded server configuration is overwritten once every
-[reconfigure of GitLab][reconfigure]. In the future this will not be required.
+[reconfigure of GitLab][reconfigure]. In the future this will not be required.
## Metrics available
@@ -45,6 +45,8 @@ In this experimental phase, only a few metrics are available:
| redis_ping_success | Gauge | 9.4 | Whether or not the last redis ping succeeded |
| redis_ping_latency_seconds | Gauge | 9.4 | Round trip time of the redis ping |
| user_session_logins_total | Counter | 9.4 | Counter of how many users have logged in |
+| filesystem_circuitbreaker_latency_seconds | Histogram | 9.5 | Latency of the stat check the circuitbreaker uses to probe a shard |
+| filesystem_circuitbreaker | Gauge | 9.5 | Wether or not the circuit for a certain shard is broken or not |
## Metrics shared directory
diff --git a/doc/administration/operations/sidekiq_memory_killer.md b/doc/administration/operations/sidekiq_memory_killer.md
index b5e78348989..cbffd883774 100644
--- a/doc/administration/operations/sidekiq_memory_killer.md
+++ b/doc/administration/operations/sidekiq_memory_killer.md
@@ -28,7 +28,7 @@ The MemoryKiller is controlled using environment variables.
delayed shutdown is triggered. The default value for Omnibus packages is set
[in the omnibus-gitlab
repository](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-cookbooks/gitlab/attributes/default.rb).
-- `SIDEKIQ_MEMORY_KILLER_GRACE_TIME`: defaults 900 seconds (15 minutes). When
+- `SIDEKIQ_MEMORY_KILLER_GRACE_TIME`: defaults to 900 seconds (15 minutes). When
a shutdown is triggered, the Sidekiq process will keep working normally for
another 15 minutes.
- `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT`: defaults to 30 seconds. When the grace
@@ -36,5 +36,3 @@ The MemoryKiller is controlled using environment variables.
Existing jobs get 30 seconds to finish. After that, the MemoryKiller tells
Sidekiq to shut down, and an external supervision mechanism (e.g. Runit) must
restart Sidekiq.
-- `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_SIGNAL`: defaults to `SIGKILL`. The name of
- the final signal sent to the Sidekiq process when we want it to shut down.
diff --git a/doc/administration/repository_storage_types.md b/doc/administration/repository_storage_types.md
index fa882bbe28a..bc9b6253f1a 100644
--- a/doc/administration/repository_storage_types.md
+++ b/doc/administration/repository_storage_types.md
@@ -25,7 +25,10 @@ Any change in the URL will need to be reflected on disk (when groups / users or
of load in big installations, and can be even worst if they are using any type of network based filesystem.
Last, for GitLab Geo, this storage type means we have to synchronize the disk state, replicate renames in the correct
-order or we may end-up with wrong repository or missing data temporarily.
+order or we may end-up with wrong repository or missing data temporarily.
+
+This pattern also exists in other objects stored in GitLab, like issue Attachments, GitLab Pages artifacts,
+Docker Containers for the integrated Registry, etc.
## Hashed Storage
@@ -67,3 +70,23 @@ To migrate your existing projects to the new storage type, check the specific [r
[ce-28283]: https://gitlab.com/gitlab-org/gitlab-ce/issues/28283
[rake tasks]: raketasks/storage.md#migrate-existing-projects-to-hashed-storage
[storage-paths]: repository_storage_types.md
+
+### Hashed Storage coverage
+
+We are incrementally moving every storable object in GitLab to the Hashed Storage pattern. You can check the current
+coverage status below.
+
+Note that things stored in an S3 compatible endpoint will not have the downsides mentioned earlier, if they are not
+prefixed with `#{namespace}/#{project_name}`, which is true for CI Cache and LFS Objects.
+
+| Storable Object | Legacy Storage | Hashed Storage | S3 Compatible | GitLab Version |
+| ----------------| -------------- | -------------- | ------------- | -------------- |
+| Repository | Yes | Yes | - | 10.0 |
+| Attachments | Yes | Yes | - | 10.2 |
+| Avatars | Yes | No | - | - |
+| Pages | Yes | No | - | - |
+| Docker Registry | Yes | No | - | - |
+| CI Build Logs | No | No | - | - |
+| CI Artifacts | No | No | - | - |
+| CI Cache | No | No | Yes | - |
+| LFS Objects | Yes | No | Yes (EEP) | - |
diff --git a/doc/administration/troubleshooting/debug.md b/doc/administration/troubleshooting/debug.md
index 6f1356ddf8f..be538ea250a 100644
--- a/doc/administration/troubleshooting/debug.md
+++ b/doc/administration/troubleshooting/debug.md
@@ -141,7 +141,7 @@ separate Rails process to debug the issue:
1. Log in to your GitLab account.
1. Copy the URL that is causing problems (e.g. https://gitlab.com/ABC).
-1. Obtain the private token for your user (Profile Settings -> Account).
+1. Create a Personal Access Token for your user (Profile Settings -> Access Tokens).
1. Bring up the GitLab Rails console. For omnibus users, run:
```
diff --git a/doc/api/README.md b/doc/api/README.md
index 89ffe9d7868..f226716c3b5 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -50,7 +50,6 @@ following locations:
- [Repository Files](repository_files.md)
- [Runners](runners.md)
- [Services](services.md)
-- [Session](session.md)
- [Settings](settings.md)
- [Sidekiq metrics](sidekiq_metrics.md)
- [System Hooks](system_hooks.md)
@@ -86,27 +85,10 @@ API requests should be prefixed with `api` and the API version. The API version
is defined in [`lib/api.rb`][lib-api-url]. For example, the root of the v4 API
is at `/api/v4`.
-For endpoints that require [authentication](#authentication), you need to pass
-a `private_token` parameter via query string or header. If passed as a header,
-the header name must be `PRIVATE-TOKEN` (uppercase and with a dash instead of
-an underscore).
-
-Example of a valid API request:
-
-```
-GET /projects?private_token=9koXpg98eAheJpvBs5tK
-```
-
-Example of a valid API request using cURL and authentication via header:
+Example of a valid API request using cURL:
```shell
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects"
-```
-
-Example of a valid API request using cURL and authentication via a query string:
-
-```shell
-curl "https://gitlab.example.com/api/v4/projects?private_token=9koXpg98eAheJpvBs5tK"
+curl "https://gitlab.example.com/api/v4/projects"
```
The API uses JSON to serialize data. You don't need to specify `.json` at the
@@ -114,15 +96,20 @@ end of an API URL.
## Authentication
-Most API requests require authentication via a session cookie or token. For
+Most API requests require authentication, or will only return public data when
+authentication is not provided. For
those cases where it is not required, this will be mentioned in the documentation
for each individual endpoint. For example, the [`/projects/:id` endpoint](projects.md).
-There are three types of access tokens available:
+There are three ways to authenticate with the GitLab API:
1. [OAuth2 tokens](#oauth2-tokens)
-1. [Private tokens](#private-tokens)
1. [Personal access tokens](#personal-access-tokens)
+1. [Session cookie](#session-cookie)
+
+For admins who want to authenticate with the API as a specific user, or who want to build applications or scripts that do so, two options are available:
+1. [Impersonation tokens](#impersonation-tokens)
+2. [Sudo](#sudo)
If authentication information is invalid or omitted, an error message will be
returned with status code `401`:
@@ -133,74 +120,84 @@ returned with status code `401`:
}
```
-### Session cookie
+### OAuth2 tokens
-When signing in to GitLab as an ordinary user, a `_gitlab_session` cookie is
-set. The API will use this cookie for authentication if it is present, but using
-the API to generate a new session cookie is currently not supported.
+You can use an [OAuth2 token](oauth2.md) to authenticate with the API by passing it in either the
+`access_token` parameter or the `Authorization` header.
-### OAuth2 tokens
+Example of using the OAuth2 token in a parameter:
-You can use an OAuth 2 token to authenticate with the API by passing it either in the
-`access_token` parameter or in the `Authorization` header.
+```shell
+curl https://gitlab.example.com/api/v4/projects?access_token=OAUTH-TOKEN
+```
-Example of using the OAuth2 token in the header:
+Example of using the OAuth2 token in a header:
```shell
curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v4/projects
```
-Read more about [GitLab as an OAuth2 client](oauth2.md).
+Read more about [GitLab as an OAuth2 provider](oauth2.md).
-### Private tokens
+### Personal access tokens
-Private tokens provide full access to the GitLab API. Anyone with access to
-them can interact with GitLab as if they were you. You can find or reset your
-private token in your account page (`/profile/account`).
+You can use a [personal access token][pat] to authenticate with the API by passing it in either the
+`private_token` parameter or the `Private-Token` header.
-For examples of usage, [read the basic usage section](#basic-usage).
+Example of using the personal access token in a parameter:
-### Personal access tokens
+```shell
+curl https://gitlab.example.com/api/v4/projects?private_token=9koXpg98eAheJpvBs5tK
+```
+
+Example of using the personal access token in a header:
-Instead of using your private token which grants full access to your account,
-personal access tokens could be a better fit because of their granular
-permissions.
+```shell
+curl --header "Private-Token: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects
+```
-Once you have your token, pass it to the API using either the `private_token`
-parameter or the `PRIVATE-TOKEN` header. For examples of usage,
-[read the basic usage section](#basic-usage).
+Read more about [personal access tokens][pat].
+
+### Session cookie
+
+When signing in to the main GitLab application, a `_gitlab_session` cookie is
+set. The API will use this cookie for authentication if it is present, but using
+the API to generate a new session cookie is currently not supported.
-[Read more about personal access tokens.][pat]
+The primary user of this authentication method is the web frontend of GitLab itself,
+which can use the API as the authenticated user to get a list of their projects,
+for example, without needing to explicitly pass an access token.
### Impersonation tokens
> [Introduced][ce-9099] in GitLab 9.0. Needs admin permissions.
Impersonation tokens are a type of [personal access token][pat]
-that can only be created by an admin for a specific user.
+that can only be created by an admin for a specific user. They are a great fit
+if you want to build applications or scripts that authenticate with the API as a specific user.
-They are a better alternative to using the user's password/private token
-or using the [Sudo](#sudo) feature which also requires the admin's password
-or private token, since the password/token can change over time. Impersonation
-tokens are a great fit if you want to build applications or tools which
-authenticate with the API as a specific user.
+They are an alternative to directly using the user's password or one of their
+personal access tokens, and to using the [Sudo](#sudo) feature, since the user's (or admin's, in the case of Sudo)
+password/token may not be known or may change over time.
For more information, refer to the
[users API](users.md#retrieve-user-impersonation-tokens) docs.
-For examples of usage, [read the basic usage section](#basic-usage).
+Impersonation tokens are used exactly like regular personal access tokens, and can be passed in either the
+`private_token` parameter or the `Private-Token` header.
### Sudo
> Needs admin permissions.
All API requests support performing an API call as if you were another user,
-provided your private token is from an administrator account. You need to pass
-the `sudo` parameter either via query string or a header with an ID/username of
+provided you are authenticated as an administrator with an OAuth or Personal Access Token that has the `sudo` scope.
+
+You need to pass the `sudo` parameter either via query string or a header with an ID/username of
the user you want to perform the operation as. If passed as a header, the
-header name must be `SUDO` (uppercase).
+header name must be `Sudo`.
-If a non administrative `private_token` is provided, then an error message will
+If a non administrative access token is provided, an error message will
be returned with status code `403`:
```json
@@ -209,12 +206,23 @@ be returned with status code `403`:
}
```
+If an access token without the `sudo` scope is provided, an error message will
+be returned with status code `403`:
+
+```json
+{
+ "error": "insufficient_scope",
+ "error_description": "The request requires higher privileges than provided by the access token.",
+ "scope": "sudo"
+}
+```
+
If the sudo user ID or username cannot be found, an error message will be
returned with status code `404`:
```json
{
- "message": "404 Not Found: No user id or username for: <id/username>"
+ "message": "404 User with ID or username '123' Not Found"
}
```
@@ -228,7 +236,7 @@ GET /projects?private_token=9koXpg98eAheJpvBs5tK&sudo=username
```
```shell
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: username" "https://gitlab.example.com/api/v4/projects"
+curl --header "Private-Token: 9koXpg98eAheJpvBs5tK" --header "Sudo: username" "https://gitlab.example.com/api/v4/projects"
```
Example of a valid API call and a request using cURL with sudo request,
@@ -239,7 +247,7 @@ GET /projects?private_token=9koXpg98eAheJpvBs5tK&sudo=23
```
```shell
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: 23" "https://gitlab.example.com/api/v4/projects"
+curl --header "Private-Token: 9koXpg98eAheJpvBs5tK" --header "Sudo: 23" "https://gitlab.example.com/api/v4/projects"
```
## Status codes
diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md
index 890945cfc7e..a6631cab8c3 100644
--- a/doc/api/pipelines.md
+++ b/doc/api/pipelines.md
@@ -57,7 +57,7 @@ GET /projects/:id/pipelines/:pipeline_id
| `pipeline_id` | integer | yes | The ID of a pipeline |
```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/pipeline/46"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/pipelines/46"
```
Example of response
diff --git a/doc/api/services.md b/doc/api/services.md
index 6c8f196fd5c..e642ec964de 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -582,6 +582,40 @@ Delete Mattermost slash command service for a project.
DELETE /projects/:id/services/mattermost-slash-commands
```
+## Packagist
+
+Update your project on Packagist, the main Composer repository, when commits or tags are pushed to GitLab.
+
+### Create/Edit Packagist service
+
+Set Packagist service for a project.
+
+```
+PUT /projects/:id/services/packagist
+```
+
+Parameters:
+
+- `username` (**required**)
+- `token` (**required**)
+- `server` (optional)
+
+### Delete Packagist service
+
+Delete Packagist service for a project.
+
+```
+DELETE /projects/:id/services/packagist
+```
+
+### Get Packagist service settings
+
+Get Packagist service settings for a project.
+
+```
+GET /projects/:id/services/packagist
+```
+
## Pipeline-Emails
Get emails for GitLab CI pipelines.
diff --git a/doc/api/session.md b/doc/api/session.md
deleted file mode 100644
index b97e26f34a2..00000000000
--- a/doc/api/session.md
+++ /dev/null
@@ -1,55 +0,0 @@
-# Session API
-
->**Deprecation notice:**
-Starting in GitLab 8.11, this feature has been **disabled** for users with
-[two-factor authentication][2fa] turned on. These users can access the API
-using [personal access tokens] instead.
-
-You can login with both GitLab and LDAP credentials in order to obtain the
-private token.
-
-```
-POST /session
-```
-
-| Attribute | Type | Required | Description |
-| ---------- | ------- | -------- | -------- |
-| `login` | string | yes | The username of the user|
-| `email` | string | yes if login is not provided | The email of the user |
-| `password` | string | yes | The password of the user |
-
-```bash
-curl --request POST "https://gitlab.example.com/api/v4/session?login=john_smith&password=strongpassw0rd"
-```
-
-Example response:
-
-```json
-{
- "name": "John Smith",
- "username": "john_smith",
- "id": 32,
- "state": "active",
- "avatar_url": null,
- "created_at": "2015-01-29T21:07:19.440Z",
- "is_admin": true,
- "bio": null,
- "skype": "",
- "linkedin": "",
- "twitter": "",
- "website_url": "",
- "email": "john@example.com",
- "theme_id": 1,
- "color_scheme_id": 1,
- "projects_limit": 10,
- "current_sign_in_at": "2015-07-07T07:10:58.392Z",
- "identities": [],
- "can_create_group": true,
- "can_create_project": true,
- "two_factor_enabled": false,
- "private_token": "9koXpg98eAheJpvBs5tK"
-}
-```
-
-[2fa]: ../user/profile/account/two_factor_authentication.md
-[personal access tokens]: ../user/profile/personal_access_tokens.md
diff --git a/doc/api/users.md b/doc/api/users.md
index 1643c584244..aa711090af1 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -410,8 +410,7 @@ GET /user
"can_create_group": true,
"can_create_project": true,
"two_factor_enabled": true,
- "external": false,
- "private_token": "dd34asd13as"
+ "external": false
}
```
diff --git a/doc/ci/README.md b/doc/ci/README.md
index ec0ddfbea75..12404eddbe2 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# GitLab Continuous Integration (GitLab CI)
![Pipeline graph](img/cicd_pipeline_infograph.png)
diff --git a/doc/ci/docker/README.md b/doc/ci/docker/README.md
index 99669a9272a..b0e01d74f7e 100644
--- a/doc/ci/docker/README.md
+++ b/doc/ci/docker/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Docker integration
- [Using Docker Images](using_docker_images.md)
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index 4586caa457d..0a2419b7ed2 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -31,12 +31,12 @@ There are three methods to enable the use of `docker build` and `docker run` dur
The simplest approach is to install GitLab Runner in `shell` execution mode.
GitLab Runner then executes job scripts as the `gitlab-runner` user.
-1. Install [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/#installation).
+1. Install [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-runner/#installation).
1. During GitLab Runner installation select `shell` as method of executing job scripts or use command:
```bash
- sudo gitlab-ci-multi-runner register -n \
+ sudo gitlab-runner register -n \
--url https://gitlab.com/ \
--registration-token REGISTRATION_TOKEN \
--executor shell \
@@ -93,7 +93,7 @@ In order to do that, follow the steps:
mode:
```bash
- sudo gitlab-ci-multi-runner register -n \
+ sudo gitlab-runner register -n \
--url https://gitlab.com/ \
--registration-token REGISTRATION_TOKEN \
--executor docker \
@@ -178,7 +178,7 @@ In order to do that, follow the steps:
1. Register GitLab Runner from the command line to use `docker` and share `/var/run/docker.sock`:
```bash
- sudo gitlab-ci-multi-runner register -n \
+ sudo gitlab-runner register -n \
--url https://gitlab.com/ \
--registration-token REGISTRATION_TOKEN \
--executor docker \
diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
index f7493794b6a..ecb8f15c851 100644
--- a/doc/ci/docker/using_docker_images.md
+++ b/doc/ci/docker/using_docker_images.md
@@ -501,8 +501,8 @@ First start with creating a file named `build_script`:
```bash
cat <<EOF > build_script
-git clone https://gitlab.com/gitlab-org/gitlab-ci-multi-runner.git /builds/gitlab-org/gitlab-ci-multi-runner
-cd /builds/gitlab-org/gitlab-ci-multi-runner
+git clone https://gitlab.com/gitlab-org/gitlab-runner.git /builds/gitlab-org/gitlab-runner
+cd /builds/gitlab-org/gitlab-runner
make
EOF
```
diff --git a/doc/ci/enable_or_disable_ci.md b/doc/ci/enable_or_disable_ci.md
index b8f9988e3ef..7aa7de97c43 100644
--- a/doc/ci/enable_or_disable_ci.md
+++ b/doc/ci/enable_or_disable_ci.md
@@ -1,4 +1,4 @@
-## Enable or disable GitLab CI/CD
+# How to enable or disable GitLab CI/CD
To effectively use GitLab CI/CD, you need a valid [`.gitlab-ci.yml`](yaml/README.md)
file present at the root directory of your project and a
@@ -21,7 +21,7 @@ individually under each project's settings, or site-wide by modifying the
settings in `gitlab.yml` and `gitlab.rb` for source and Omnibus installations
respectively.
-### Per-project user setting
+## Per-project user setting
The setting to enable or disable GitLab CI/CD can be found under your project's
**Settings > General > Permissions**. Choose one of "Disabled", "Only team members"
@@ -29,7 +29,7 @@ or "Everyone with access" and hit **Save changes** for the settings to take effe
![Sharing & Permissions settings](../user/project/settings/img/sharing_and_permissions_settings.png)
-### Site-wide admin setting
+## Site-wide admin setting
You can disable GitLab CI/CD site-wide, by modifying the settings in `gitlab.yml`
and `gitlab.rb` for source and Omnibus installations respectively.
diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md
index f094546c3bd..d05b4db953a 100644
--- a/doc/ci/examples/README.md
+++ b/doc/ci/examples/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# GitLab CI Examples
A collection of `.gitlab-ci.yml` files is maintained at the [GitLab CI Yml project][gitlab-ci-templates].
diff --git a/doc/ci/examples/deployment/composer-npm-deploy.md b/doc/ci/examples/deployment/composer-npm-deploy.md
index b9f0485290e..bed379b0254 100644
--- a/doc/ci/examples/deployment/composer-npm-deploy.md
+++ b/doc/ci/examples/deployment/composer-npm-deploy.md
@@ -1,4 +1,4 @@
-## Running Composer and NPM scripts with deployment via SCP
+# Running Composer and NPM scripts with deployment via SCP in GitLab CI/CD
This guide covers the building dependencies of a PHP project while compiling assets via an NPM script.
@@ -39,13 +39,13 @@ In this particular case, the `npm deploy` script is a Gulp script that does the
All these operations will put all files into a `build` folder, which is ready to be deployed to a live server.
-### How to transfer files to a live server?
+## How to transfer files to a live server
You have multiple options: rsync, scp, sftp and so on. For now, we will use scp.
To make this work, you need to add a GitLab Secret Variable (accessible on _gitlab.example/your-project-name/variables_). That variable will be called `STAGING_PRIVATE_KEY` and it's the **private** ssh key of your server.
-#### Security tip
+### Security tip
Create a user that has access **only** to the folder that needs to be updated!
@@ -69,7 +69,7 @@ In order, this means that:
And this is basically all you need in the `before_script` section.
-## How to deploy things?
+## How to deploy things
As we stated above, we need to deploy the `build` folder from the docker image to our server. To do so, we create a new job:
@@ -88,7 +88,7 @@ stage_deploy:
- ssh -p22 server_user@server_host "rm -rf htdocs/wp-content/themes/_old"
```
-### What's going on here?
+Here's the breakdown:
1. `only:dev` means that this build will run only when something is pushed to the `dev` branch. You can remove this block completely and have everything be ran on every push (but probably this is something you don't want)
2. `ssh-add ...` we will add that private key you added on the web UI to the docker container
@@ -99,7 +99,7 @@ stage_deploy:
What's the deal with the artifacts? We just tell GitLab CI to keep the `build` directory (later on, you can download that as needed).
-#### Why we do it this way?
+### Why we do it this way
If you're using this only for stage server, you could do this in two steps:
@@ -112,7 +112,7 @@ The problem is that there will be a small period of time when you won't have the
So we use so many steps because we want to make sure that at any given time we have a functional app in place.
-## Where to go next?
+## Where to go next
Since this was a WordPress project, I gave real life code snippets. Some ideas you can pursuit:
diff --git a/doc/ci/examples/php.md b/doc/ci/examples/php.md
index f2dd12b67d3..6768a2e012f 100644
--- a/doc/ci/examples/php.md
+++ b/doc/ci/examples/php.md
@@ -267,10 +267,10 @@ terminal execute:
```bash
# Check using docker executor
-gitlab-ci-multi-runner exec docker test:app
+gitlab-runner exec docker test:app
# Check using shell executor
-gitlab-ci-multi-runner exec shell test:app
+gitlab-runner exec shell test:app
```
## Example project
diff --git a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
index 73aebaf6d7f..a6ed1c54e16 100644
--- a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
+++ b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
@@ -1,10 +1,13 @@
-## Test and Deploy a python application
+# Test and Deploy a python application with GitLab CI/CD
+
This example will guide you how to run tests in your Python application and deploy it automatically as Heroku application.
-You can checkout the example [source](https://gitlab.com/ayufan/python-getting-started) and check [CI status](https://gitlab.com/ayufan/python-getting-started/builds?scope=all).
+You can checkout the [example source](https://gitlab.com/ayufan/python-getting-started).
+
+## Configure project
-### Configure project
This is what the `.gitlab-ci.yml` file looks like for this project:
+
```yaml
test:
script:
@@ -41,23 +44,27 @@ This project has three jobs:
2. `staging` - used to automatically deploy staging environment every push to `master` branch
3. `production` - used to automatically deploy production environmnet for every created tag
-### Store API keys
+## Store API keys
+
You'll need to create two variables in `Project > Variables`:
1. `HEROKU_STAGING_API_KEY` - Heroku API key used to deploy staging app,
2. `HEROKU_PRODUCTION_API_KEY` - Heroku API key used to deploy production app.
Find your Heroku API key in [Manage Account](https://dashboard.heroku.com/account).
-### Create Heroku application
+## Create Heroku application
+
For each of your environments, you'll need to create a new Heroku application.
You can do this through the [Dashboard](https://dashboard.heroku.com/).
-### Create runner
+## Create Runner
+
First install [Docker Engine](https://docs.docker.com/installation/).
-To build this project you also need to have [GitLab Runner](https://about.gitlab.com/gitlab-ci/#gitlab-runner).
+To build this project you also need to have [GitLab Runner](https://docs.gitlab.com/runner).
You can use public runners available on `gitlab.com`, but you can register your own:
+
```
-gitlab-ci-multi-runner register \
+gitlab-runner register \
--non-interactive \
--url "https://gitlab.com/" \
--registration-token "PROJECT_REGISTRATION_TOKEN" \
diff --git a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
index 6fa64a67e82..10fd2616fab 100644
--- a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
+++ b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
@@ -1,10 +1,13 @@
-## Test and Deploy a ruby application
+# Test and Deploy a ruby application with GitLab CI/CD
+
This example will guide you how to run tests in your Ruby on Rails application and deploy it automatically as Heroku application.
You can checkout the example [source](https://gitlab.com/ayufan/ruby-getting-started) and check [CI status](https://gitlab.com/ayufan/ruby-getting-started/builds?scope=all).
-### Configure project
+## Configure the project
+
This is what the `.gitlab-ci.yml` file looks like for this project:
+
```yaml
test:
script:
@@ -36,23 +39,28 @@ This project has three jobs:
2. `staging` - used to automatically deploy staging environment every push to `master` branch
3. `production` - used to automatically deploy production environment for every created tag
-### Store API keys
-You'll need to create two variables in `Project > Variables`:
+## Store API keys
+
+You'll need to create two variables in your project's **Settings > CI/CD > Variables**:
+
1. `HEROKU_STAGING_API_KEY` - Heroku API key used to deploy staging app,
2. `HEROKU_PRODUCTION_API_KEY` - Heroku API key used to deploy production app.
Find your Heroku API key in [Manage Account](https://dashboard.heroku.com/account).
-### Create Heroku application
+## Create Heroku application
+
For each of your environments, you'll need to create a new Heroku application.
You can do this through the [Dashboard](https://dashboard.heroku.com/).
-### Create runner
+## Create Runner
+
First install [Docker Engine](https://docs.docker.com/installation/).
To build this project you also need to have [GitLab Runner](https://about.gitlab.com/gitlab-ci/#gitlab-runner).
You can use public runners available on `gitlab.com`, but you can register your own:
+
```
-gitlab-ci-multi-runner register \
+gitlab-runner register \
--non-interactive \
--url "https://gitlab.com/" \
--registration-token "PROJECT_REGISTRATION_TOKEN" \
@@ -62,6 +70,6 @@ gitlab-ci-multi-runner register \
--docker-postgres latest
```
-With the command above, you create a runner that uses [ruby:2.2](https://hub.docker.com/r/_/ruby/) image and uses [postgres](https://hub.docker.com/r/_/postgres/) database.
+With the command above, you create a Runner that uses [ruby:2.2](https://hub.docker.com/r/_/ruby/) image and uses [postgres](https://hub.docker.com/r/_/postgres/) database.
To access PostgreSQL database you need to connect to `host: postgres` as user `postgres` without password.
diff --git a/doc/ci/examples/test-clojure-application.md b/doc/ci/examples/test-clojure-application.md
index 56b746ce025..3b1026d174f 100644
--- a/doc/ci/examples/test-clojure-application.md
+++ b/doc/ci/examples/test-clojure-application.md
@@ -1,10 +1,10 @@
-## Test a Clojure application
+# Test a Clojure application with GitLab CI/CD
This example will guide you how to run tests in your Clojure application.
You can checkout the example [source](https://gitlab.com/dzaporozhets/clojure-web-application) and check [CI status](https://gitlab.com/dzaporozhets/clojure-web-application/builds?scope=all).
-### Configure project
+## Configure the project
This is what the `.gitlab-ci.yml` file looks like for this project:
@@ -23,13 +23,13 @@ before_script:
- lein deps
- lein migratus migrate
-test:
- script:
+test:
+ script:
- lein test
```
-In before script we install JRE and [Leiningen](http://leiningen.org/).
-Sample project uses [migratus](https://github.com/yogthos/migratus) library to manage database migrations.
+In before script we install JRE and [Leiningen](http://leiningen.org/).
+Sample project uses [migratus](https://github.com/yogthos/migratus) library to manage database migrations.
So we added database migration as last step of `before_script` section
You can use public runners available on `gitlab.com` for testing your application with such configuration.
diff --git a/doc/ci/examples/test-phoenix-application.md b/doc/ci/examples/test-phoenix-application.md
index 150698ca04b..f6c81b076bc 100644
--- a/doc/ci/examples/test-phoenix-application.md
+++ b/doc/ci/examples/test-phoenix-application.md
@@ -1,9 +1,9 @@
-## Test a Phoenix application
+# Test a Phoenix application with GitLab CI/CD
This example demonstrates the integration of Gitlab CI with Phoenix, Elixir and
Postgres.
-### Add `.gitlab-ci.yml` file to project
+## Add `.gitlab-ci.yml` to project
The following `.gitlab-ci.yml` should be added in the root of your
repository to trigger CI:
@@ -36,7 +36,7 @@ run your migrations.
Finally, the test `script` will run your tests.
-### Update the Config Settings
+## Update the Config Settings
In `config/test.exs`, update the database hostname:
@@ -45,12 +45,12 @@ config :my_app, MyApp.Repo,
hostname: if(System.get_env("CI"), do: "postgres", else: "localhost"),
```
-### Add the Migrations Folder
+## Add the Migrations Folder
If you do not have any migrations yet, you will need to create an empty
`.gitkeep` file in `priv/repo/migrations`.
-### Sources
+## Sources
- https://medium.com/@nahtnam/using-phoenix-on-gitlab-ci-5a51eec81142
- https://davejlong.com/ci-with-phoenix-and-gitlab/
diff --git a/doc/ci/git_submodules.md b/doc/ci/git_submodules.md
index 36c6e153d95..c83d3f6f248 100644
--- a/doc/ci/git_submodules.md
+++ b/doc/ci/git_submodules.md
@@ -61,7 +61,7 @@ correctly with your CI jobs:
1. First, make sure you have used [relative URLs](#configuring-the-gitmodules-file)
for the submodules located in the same GitLab server.
-1. Next, if you are using `gitlab-ci-multi-runner` v1.10+, you can set the
+1. Next, if you are using `gitlab-runner` v1.10+, you can set the
`GIT_SUBMODULE_STRATEGY` variable to either `normal` or `recursive` to tell
the runner to fetch your submodules before the job:
```yaml
@@ -71,7 +71,7 @@ correctly with your CI jobs:
See the [`.gitlab-ci.yml` reference](yaml/README.md#git-submodule-strategy)
for more details about `GIT_SUBMODULE_STRATEGY`.
-1. If you are using an older version of `gitlab-ci-multi-runner`, then use
+1. If you are using an older version of `gitlab-runner`, then use
`git submodule sync/update` in `before_script`:
```yaml
diff --git a/doc/ci/permissions/README.md b/doc/ci/permissions/README.md
index 42eb59f84c8..80d8e46f29c 100644
--- a/doc/ci/permissions/README.md
+++ b/doc/ci/permissions/README.md
@@ -1,3 +1 @@
-# Users Permissions
-
This document was moved to [user/permissions.md](../../user/permissions.md#gitlab-ci).
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index 2d56b2540ef..f621bf07251 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -1,4 +1,4 @@
-# Getting started with GitLab CI
+# Getting started with GitLab CI/CD
>**Note:** Starting from version 8.0, GitLab [Continuous Integration][ci] (CI)
is fully integrated into GitLab itself and is [enabled] by default on all
diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md
index 8b51d112a2c..df66810a838 100644
--- a/doc/ci/runners/README.md
+++ b/doc/ci/runners/README.md
@@ -1,4 +1,4 @@
-# Runners
+# Configuring GitLab Runners
In GitLab CI, Runners run the code defined in [`.gitlab-ci.yml`](../yaml/README.md).
They are isolated (virtual) machines that pick up jobs through the coordinator
diff --git a/doc/ci/services/README.md b/doc/ci/services/README.md
index 4b79461d55c..d94b472b768 100644
--- a/doc/ci/services/README.md
+++ b/doc/ci/services/README.md
@@ -1,4 +1,8 @@
-## GitLab CI Services
+---
+comments: false
+---
+
+# GitLab CI Services
GitLab CI uses the `services` keyword to define what docker containers should
be linked with your base image. Below is a list of examples you may use.
diff --git a/doc/ci/services/docker-services.md b/doc/ci/services/docker-services.md
index df36ebaf7d4..787c5e462e4 100644
--- a/doc/ci/services/docker-services.md
+++ b/doc/ci/services/docker-services.md
@@ -1,5 +1,9 @@
-## GitLab CI Services
+---
+comments: false
+---
-+ [Using MySQL](mysql.md)
-+ [Using PostgreSQL](postgres.md)
-+ [Using Redis](redis.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 73568757aaa..a9e6bda9916 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -1,4 +1,4 @@
-# Variables
+# GitLab CI/CD Variables
When receiving a job from GitLab CI, the [Runner] prepares the build environment.
It starts by setting a list of **predefined variables** (environment variables)
@@ -43,7 +43,7 @@ future GitLab releases.**
| **CI_COMMIT_TAG** | 9.0 | 0.5 | The commit tag name. Present only when building tags. |
| **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 | Mark that 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. |
+| **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. |
| **CI_ENVIRONMENT_NAME** | 8.15 | all | The name of the environment for this job |
| **CI_ENVIRONMENT_SLUG** | 8.15 | all | A simplified version of the environment name, suitable for inclusion in DNS, URLs, Kubernetes labels, etc. |
| **CI_ENVIRONMENT_URL** | 9.3 | all | The URL of the environment for this job |
@@ -66,6 +66,7 @@ future GitLab releases.**
| **CI_PROJECT_PATH** | 8.10 | 0.5 | The namespace with project name |
| **CI_PROJECT_PATH_SLUG** | 9.3 | all | `$CI_PROJECT_PATH` lowercased and with everything except `0-9` and `a-z` replaced with `-`. Use in URLs and domain names. |
| **CI_PROJECT_URL** | 8.10 | 0.5 | The HTTP address to access project |
+| **CI_PROJECT_VISIBILITY** | 10.3 | all | The project visibility (internal, private, public) |
| **CI_REGISTRY** | 8.10 | 0.5 | If the Container Registry is enabled it returns the address of GitLab's Container Registry |
| **CI_REGISTRY_IMAGE** | 8.10 | 0.5 | If the Container Registry is enabled for the project it returns the address of the registry tied to the specific project |
| **CI_REGISTRY_PASSWORD** | 9.0 | all | The password to use to push containers to the GitLab Container Registry |
@@ -74,7 +75,7 @@ future GitLab releases.**
| **CI_SERVER_NAME** | all | all | The name of CI server that is used to coordinate jobs |
| **CI_SERVER_REVISION** | all | all | GitLab revision that is used to schedule jobs |
| **CI_SERVER_VERSION** | all | all | GitLab version that is used to schedule jobs |
-| **CI_SHARED_ENVIRONMENT** | all | 10.1 | Mark that job is executed in a shared environment (something that is persisted across CI invocations like `shell` or `ssh` executor). If the environment is shared, it is set to true, otherwise it is not defined at all. |
+| **CI_SHARED_ENVIRONMENT** | all | 10.1 | Marks that the job is executed in a shared environment (something that is persisted across CI invocations like `shell` or `ssh` executor). If the environment is shared, it is set to true, otherwise it is not defined at all. |
| **ARTIFACT_DOWNLOAD_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to download artifacts running a job |
| **GET_SOURCES_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to fetch sources running a job |
| **GITLAB_CI** | all | all | Mark that job is executed in GitLab CI environment |
diff --git a/doc/development/README.md b/doc/development/README.md
index 36096842344..0cafc112b6b 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# GitLab development guides
## Get started!
diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md
index 798f40eef3d..0e4ffbd7910 100644
--- a/doc/development/doc_styleguide.md
+++ b/doc/development/doc_styleguide.md
@@ -459,11 +459,11 @@ Rendered example:
### cURL commands
- Use `https://gitlab.example.com/api/v4/` as an endpoint.
-- Wherever needed use this private token: `9koXpg98eAheJpvBs5tK`.
+- Wherever needed use this personal access token: `9koXpg98eAheJpvBs5tK`.
- Always put the request first. `GET` is the default so you don't have to
include it.
- Use double quotes to the URL when it includes additional parameters.
-- Prefer to use examples using the private token and don't pass data of
+- Prefer to use examples using the personal access token and don't pass data of
username and password.
| Methods | Description |
diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md
new file mode 100644
index 00000000000..932a44f65e4
--- /dev/null
+++ b/doc/development/ee_features.md
@@ -0,0 +1,382 @@
+# Guidelines for implementing Enterprise Edition feature
+
+- **Write the code and the tests.**: As with any code, EE features should have
+ good test coverage to prevent regressions.
+- **Write documentation.**: Add documentation to the `doc/` directory. Describe
+ the feature and include screenshots, if applicable.
+- **Submit a MR to the `www-gitlab-com` project.**: Add the new feature to the
+ [EE features list][ee-features-list].
+
+## Act as CE when unlicensed
+
+Since the implementation of [GitLab CE features to work with unlicensed EE instance][ee-as-ce]
+GitLab Enterprise Edition should work like GitLab Community Edition
+when no license is active. So EE features always should be guarded by
+`project.feature_available?` or `group.feature_available?` (or
+`License.feature_available?` if it is a system-wide feature).
+
+CE specs should remain untouched as much as possible and extra specs
+should be added for EE. Licensed features can be stubbed using the
+spec helper `stub_licensed_features` in `EE::LicenseHelpers`.
+
+[ee-as-ce]: https://gitlab.com/gitlab-org/gitlab-ee/issues/2500
+
+## Separation of EE code
+
+We want a [single code base][] eventually, but before we reach the goal,
+we still need to merge changes from GitLab CE to EE. To help us get there,
+we should make sure that we no longer edit CE files in place in order to
+implement EE features.
+
+Instead, all EE codes should be put inside the `ee/` top-level directory, and
+tests should be put inside `spec/ee/`. We don't use `ee/spec` for now due to
+technical limitation. The rest of codes should be as close as to the CE files.
+
+[single code base]: https://gitlab.com/gitlab-org/gitlab-ee/issues/2952#note_41016454
+
+### EE-only features
+
+If the feature being developed is not present in any form in CE, we don't
+need to put the codes under `EE` namespace. For example, an EE model could
+go into: `ee/app/models/awesome.rb` using `Awesome` as the class name. This
+is applied not only to models. Here's a list of other examples:
+
+- `ee/app/controllers/foos_controller.rb`
+- `ee/app/finders/foos_finder.rb`
+- `ee/app/helpers/foos_helper.rb`
+- `ee/app/mailers/foos_mailer.rb`
+- `ee/app/models/foo.rb`
+- `ee/app/policies/foo_policy.rb`
+- `ee/app/serializers/foo_entity.rb`
+- `ee/app/serializers/foo_serializer.rb`
+- `ee/app/services/foo/create_service.rb`
+- `ee/app/validators/foo_attr_validator.rb`
+- `ee/app/workers/foo_worker.rb`
+
+### EE features based on CE features
+
+For features that build on existing CE features, write a module in the
+`EE` namespace and `prepend` it in the CE class. This makes conflicts
+less likely to happen during CE to EE merges because only one line is
+added to the CE class - the `prepend` line.
+
+Since the module would require an `EE` namespace, the file should also be
+put in an `ee/` sub-directory. For example, we want to extend the user model
+in EE, so we have a module called `::EE::User` put inside
+`ee/app/models/ee/user.rb`.
+
+This is also not just applied to models. Here's a list of other examples:
+
+- `ee/app/controllers/ee/foos_controller.rb`
+- `ee/app/finders/ee/foos_finder.rb`
+- `ee/app/helpers/ee/foos_helper.rb`
+- `ee/app/mailers/ee/foos_mailer.rb`
+- `ee/app/models/ee/foo.rb`
+- `ee/app/policies/ee/foo_policy.rb`
+- `ee/app/serializers/ee/foo_entity.rb`
+- `ee/app/serializers/ee/foo_serializer.rb`
+- `ee/app/services/ee/foo/create_service.rb`
+- `ee/app/validators/ee/foo_attr_validator.rb`
+- `ee/app/workers/ee/foo_worker.rb`
+
+#### Overriding CE methods
+
+To override a method present in the CE codebase, use `prepend`. It
+lets you override a method in a class with a method from a module, while
+still having access the class's implementation with `super`.
+
+There are a few gotchas with it:
+
+- you should always add a `raise NotImplementedError unless defined?(super)`
+ guard clause in the "overrider" method to ensure that if the method gets
+ renamed in CE, the EE override won't be silently forgotten.
+- when the "overrider" would add a line in the middle of the CE
+ implementation, you should refactor the CE method and split it in
+ smaller methods. Or create a "hook" method that is empty in CE,
+ and with the EE-specific implementation in EE.
+- when the original implementation contains a guard clause (e.g.
+ `return unless condition`), we cannot easily extend the behaviour by
+ overriding the method, because we can't know when the overridden method
+ (i.e. calling `super` in the overriding method) would want to stop early.
+ In this case, we shouldn't just override it, but update the original method
+ to make it call the other method we want to extend, like a [template method
+ pattern](https://en.wikipedia.org/wiki/Template_method_pattern).
+ For example, given this base:
+ ``` ruby
+ class Base
+ def execute
+ return unless enabled?
+
+ # ...
+ # ...
+ end
+ end
+ ```
+ Instead of just overriding `Base#execute`, we should update it and extract
+ the behaviour into another method:
+ ``` ruby
+ class Base
+ def execute
+ return unless enabled?
+
+ do_something
+ end
+
+ private
+
+ def do_something
+ # ...
+ # ...
+ end
+ end
+ ```
+ Then we're free to override that `do_something` without worrying about the
+ guards:
+ ``` ruby
+ module EE::Base
+ def do_something
+ # Follow the above pattern to call super and extend it
+ end
+ end
+ ```
+ This would require updating CE first, or make sure this is back ported to CE.
+
+When prepending, place them in the `ee/` specific sub-directory, and
+wrap class or module in `module EE` to avoid naming conflicts.
+
+For example to override the CE implementation of
+`ApplicationController#after_sign_out_path_for`:
+
+```ruby
+def after_sign_out_path_for(resource)
+ current_application_settings.after_sign_out_path.presence || new_user_session_path
+end
+```
+
+Instead of modifying the method in place, you should add `prepend` to
+the existing file:
+
+```ruby
+class ApplicationController < ActionController::Base
+ prepend EE::ApplicationController
+ # ...
+
+ def after_sign_out_path_for(resource)
+ current_application_settings.after_sign_out_path.presence || new_user_session_path
+ end
+
+ # ...
+end
+```
+
+And create a new file in the `ee/` sub-directory with the altered
+implementation:
+
+```ruby
+module EE
+ class ApplicationController
+ def after_sign_out_path_for(resource)
+ raise NotImplementedError unless defined?(super)
+
+ if Gitlab::Geo.secondary?
+ Gitlab::Geo.primary_node.oauth_logout_url(@geo_logout_state)
+ else
+ super
+ end
+ end
+ end
+end
+```
+
+#### Use self-descriptive wrapper methods
+
+When it's not possible/logical to modify the implementation of a
+method. Wrap it in a self-descriptive method and use that method.
+
+For example, in CE only an `admin` is allowed to access all private
+projects/groups, but in EE also an `auditor` has full private
+access. It would be incorrect to override the implementation of
+`User#admin?`, so instead add a method `full_private_access?` to
+`app/models/users.rb`. The implementation in CE will be:
+
+```ruby
+def full_private_access?
+ admin?
+end
+```
+
+In EE, the implementation `ee/app/models/ee/users.rb` would be:
+
+```ruby
+def full_private_access?
+ raise NotImplementedError unless defined?(super)
+ super || auditor?
+end
+```
+
+In `lib/gitlab/visibility_level.rb` this method is used to return the
+allowed visibilty levels:
+
+```ruby
+def levels_for_user(user = nil)
+ if user.full_private_access?
+ [PRIVATE, INTERNAL, PUBLIC]
+ elsif # ...
+end
+```
+
+See [CE MR][ce-mr-full-private] and [EE MR][ee-mr-full-private] for
+full implementation details.
+
+[ce-mr-full-private]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12373
+[ee-mr-full-private]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2199
+
+### Code in `app/controllers/`
+
+In controllers, the most common type of conflict is with `before_action` that
+has a list of actions in CE but EE adds some actions to that list.
+
+The same problem often occurs for `params.require` / `params.permit` calls.
+
+**Mitigations**
+
+Separate CE and EE actions/keywords. For instance for `params.require` in
+`ProjectsController`:
+
+```ruby
+def project_params
+ params.require(:project).permit(project_params_attributes)
+end
+
+# Always returns an array of symbols, created however best fits the use case.
+# It _should_ be sorted alphabetically.
+def project_params_attributes
+ %i[
+ description
+ name
+ path
+ ]
+end
+
+```
+
+In the `EE::ProjectsController` module:
+
+```ruby
+def project_params_attributes
+ super + project_params_attributes_ee
+end
+
+def project_params_attributes_ee
+ %i[
+ approvals_before_merge
+ approver_group_ids
+ approver_ids
+ ...
+ ]
+end
+```
+
+### Code in `app/models/`
+
+EE-specific models should `extend EE::Model`.
+
+For example, if EE has a specific `Tanuki` model, you would
+place it in `ee/app/models/ee/tanuki.rb`.
+
+### Code in `app/views/`
+
+It's a very frequent problem that EE is adding some specific view code in a CE
+view. For instance the approval code in the project's settings page.
+
+**Mitigations**
+
+Blocks of code that are EE-specific should be moved to partials. This
+avoids conflicts with big chunks of HAML code that that are not fun to
+resolve when you add the indentation to the equation.
+
+EE-specific views should be placed in `ee/app/views/ee/`, using extra
+sub-directories if appropriate.
+
+### Code in `lib/`
+
+Place EE-specific logic in the top-level `EE` module namespace. Namespace the
+class beneath the `EE` module just as you would normally.
+
+For example, if CE has LDAP classes in `lib/gitlab/ldap/` then you would place
+EE-specific LDAP classes in `ee/lib/ee/gitlab/ldap`.
+
+### Code in `spec/`
+
+When you're testing EE-only features, avoid adding examples to the
+existing CE specs. Also do no change existing CE examples, since they
+should remain working as-is when EE is running without a license.
+
+Instead place EE specs in the `spec/ee/spec` folder.
+
+## JavaScript code in `assets/javascripts/`
+
+To separate EE-specific JS-files we can also move the files into an `ee` folder.
+
+For example there can be an
+`app/assets/javascripts/protected_branches/protected_branches_bundle.js` and an
+EE counterpart
+`ee/app/assets/javascripts/protected_branches/protected_branches_bundle.js`.
+
+That way we can create a separate webpack bundle in `webpack.config.js`:
+
+```javascript
+ protected_branches: '~/protected_branches',
+ ee_protected_branches: 'ee/protected_branches/protected_branches_bundle.js',
+```
+
+With the separate bundle in place, we can decide which bundle to load inside the
+view, using the `page_specific_javascript_bundle_tag` helper.
+
+```haml
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('protected_branches')
+```
+
+## SCSS code in `assets/stylesheets`
+
+To separate EE-specific styles in SCSS files, if a component you're adding styles for
+is limited to only EE, it is better to have a separate SCSS file in appropriate directory
+within `app/assets/stylesheets`.
+
+In some cases, this is not entirely possible or creating dedicated SCSS file is an overkill,
+e.g. a text style of some component is different for EE. In such cases,
+styles are usually kept in stylesheet that is common for both CE and EE, and it is wise
+to isolate such ruleset from rest of CE rules (along with adding comment describing the same)
+to avoid conflicts during CE to EE merge.
+
+#### Bad
+```scss
+.section-body {
+ .section-title {
+ background: $gl-header-color;
+ }
+
+ &.ee-section-body {
+ .section-title {
+ background: $gl-header-color-cyan;
+ }
+ }
+}
+```
+
+#### Good
+```scss
+.section-body {
+ .section-title {
+ background: $gl-header-color;
+ }
+}
+
+/* EE-specific styles */
+.section-body.ee-section-body {
+ .section-title {
+ background: $gl-header-color-cyan;
+ }
+}
+```
diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md
index 73366eb9f3f..8f956681693 100644
--- a/doc/development/fe_guide/index.md
+++ b/doc/development/fe_guide/index.md
@@ -106,6 +106,10 @@ Frontend security practices.
## [Accessibility](accessibility.md)
Our accessibility standards and resources.
+## [Internationalization (i18n) and Translations](../i18n/externalization.md)
+Frontend internationalization support is described in [this document](../i18n/).
+The [externalization part of the guide](../i18n/externalization.md) explains the helpers/methods available.
+
[rails]: http://rubyonrails.org/
[haml]: http://haml.info/
diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md
index 167260b6e0e..7c38260406d 100644
--- a/doc/development/i18n/externalization.md
+++ b/doc/development/i18n/externalization.md
@@ -180,15 +180,43 @@ aren't in the message with id `1 pipeline`.
## Working with special content
+
+### Just marking content for parsing
+
+- In Ruby/HAML:
+
+ ```ruby
+ _('Subscribe')
+ ```
+
+- In JavaScript:
+
+ ```js
+ import { __ } from '../../../locale';
+ const label = __('Subscribe');
+ ```
+
+
+Sometimes there are some dynamic translations that can't be found by the
+parser when running `bundle exec rake gettext:find`. For these scenarios you can
+use the [`_N` method](https://github.com/grosser/gettext_i18n_rails/blob/c09e38d481e0899ca7d3fc01786834fa8e7aab97/Readme.md#unfound-translations-with-rake-gettextfind).
+
+There is also and alternative method to [translate messages from validation errors](https://github.com/grosser/gettext_i18n_rails/blob/c09e38d481e0899ca7d3fc01786834fa8e7aab97/Readme.md#option-a).
+
### Interpolation
- In Ruby/HAML:
```ruby
- _("Hello %{name}") % { name: 'Joe' }
+ _("Hello %{name}") % { name: 'Joe' } => 'Hello Joe'
```
-- In JavaScript: Not supported at this moment.
+- In JavaScript:
+
+ ```js
+ import { __, sprintf } from '../../../locale';
+ sprintf(__('Hello %{username}'), { username: 'Joe' }) => 'Hello Joe'
+ ```
### Plurals
@@ -234,14 +262,6 @@ Sometimes you need to add some context to the text that you want to translate
s__('OpenedNDaysAgo|Opened')
```
-### Just marking content for parsing
-
-Sometimes there are some dynamic translations that can't be found by the
-parser when running `bundle exec rake gettext:find`. For these scenarios you can
-use the [`_N` method](https://github.com/grosser/gettext_i18n_rails/blob/c09e38d481e0899ca7d3fc01786834fa8e7aab97/Readme.md#unfound-translations-with-rake-gettextfind).
-
-There is also and alternative method to [translate messages from validation errors](https://github.com/grosser/gettext_i18n_rails/blob/c09e38d481e0899ca7d3fc01786834fa8e7aab97/Readme.md#option-a).
-
## Adding a new language
Let's suppose you want to add translations for a new language, let's say French.
diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md
index 7ddd02e6c73..8b7b015427f 100644
--- a/doc/development/testing_guide/best_practices.md
+++ b/doc/development/testing_guide/best_practices.md
@@ -60,6 +60,35 @@ writing one](testing_levels.md#consider-not-writing-a-system-test)!
- It's ok to look for DOM elements but don't abuse it since it makes the tests
more brittle
+#### Debugging Capybara
+
+Sometimes you may need to debug Capybara tests by observing browser behavior.
+
+You can pause Capybara and view the website on the browser by using the
+`live_debug` method in your spec. The current page will be automatically opened
+in your default browser.
+You may need to sign in first (the current user's credentials are displayed in
+the terminal).
+
+To resume the test run, press any key.
+
+For example:
+
+```
+$ bin/rspec spec/features/auto_deploy_spec.rb:34
+Running via Spring preloader in process 8999
+Run options: include {:locations=>{"./spec/features/auto_deploy_spec.rb"=>[34]}}
+
+Current example is paused for live debugging
+The current user credentials are: user2 / 12345678
+Press any key to resume the execution of the example!
+Back to the example!
+.
+
+Finished in 34.51 seconds (files took 0.76702 seconds to load)
+1 example, 0 failures
+```
+
### `let` variables
GitLab's RSpec suite has made extensive use of `let` variables to reduce
diff --git a/doc/development/testing_guide/testing_levels.md b/doc/development/testing_guide/testing_levels.md
index 9b9ba0baa71..1cbd4350284 100644
--- a/doc/development/testing_guide/testing_levels.md
+++ b/doc/development/testing_guide/testing_levels.md
@@ -126,7 +126,7 @@ always in-sync with the codebase.
[GitLab Workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse
[Gitaly]: https://gitlab.com/gitlab-org/gitaly
[GitLab Pages]: https://gitlab.com/gitlab-org/gitlab-pages
-[GitLab Runner]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner
+[GitLab Runner]: https://gitlab.com/gitlab-org/gitlab-runner
[GitLab Omnibus]: https://gitlab.com/gitlab-org/omnibus-gitlab
[GitLab QA]: https://gitlab.com/gitlab-org/gitlab-qa
[part of GitLab Rails]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa
diff --git a/doc/development/testing_guide/testing_rake_tasks.md b/doc/development/testing_guide/testing_rake_tasks.md
index 5bf185dd7b5..60163f1a230 100644
--- a/doc/development/testing_guide/testing_rake_tasks.md
+++ b/doc/development/testing_guide/testing_rake_tasks.md
@@ -1,4 +1,4 @@
-## Testing Rake tasks
+# Testing Rake tasks
To make testing Rake tasks a little easier, there is a helper that can be included
in lieu of the standard Spec helper. Instead of `require 'spec_helper'`, use
diff --git a/doc/development/ux_guide/users.md b/doc/development/ux_guide/users.md
index cbd7c17de41..fce882a45f1 100644
--- a/doc/development/ux_guide/users.md
+++ b/doc/development/ux_guide/users.md
@@ -1,4 +1,5 @@
-## UX Personas
+# UX Personas
+
* [Nazim Ramesh](#nazim-ramesh)
- Small to medium size organisations using GitLab CE
* [James Mackey](#james-mackey)
@@ -7,16 +8,16 @@
* [Karolina Plaskaty](#karolina-plaskaty)
- Using GitLab.com for personal/hobby projects
- Would like to use GitLab at work
- - Working for a medium to large size organisation
+ - Working for a medium to large size organisation
-<hr>
+---
-### Nazim Ramesh
+## Nazim Ramesh
- Small to medium size organisations using GitLab CE
<img src="img/nazim-ramesh.png" width="300px">
-#### Demographics
+### Demographics
- **Age**<br>32 years old
- **Location**<br>Germany
@@ -26,7 +27,7 @@
- **Frequently used programming languages**<br>JavaScript, SQL, PHP
- **Hobbies / interests**<br>Functional programming, open source, gaming, web development and web security.
-#### Motivations
+### Motivations
Nazim works for a software development company which currently hires around 80 people. When Nazim first joined the company, the engineering team were using Subversion (SVN) as their primary form of source control. However, Nazim felt SVN was not flexible enough to work with many feature branches and noticed that developers with less experience of source control struggled with the central-repository nature of SVN. Armed with a wishlist of features, Nazim began comparing source control tools. A search for “self-hosted Git server repository management†returned GitLab. In his own words, Nazim explains why he wanted the engineering team to start using GitLab:
>
@@ -39,48 +40,48 @@ In his role as a full-stack web developer, Nazim could recommend products that h
“The biggest challenge...why should we change anything at all from the status quo? We needed to switch from SVN to Git. They knew they needed to learn Git and a Git workflow...using Git was scary to my colleagues...they thought it was more complex than SVN to use.â€
>
-Undeterred, Nazim decided to migrate a couple of projects across to GitLab.
+Undeterred, Nazim decided to migrate a couple of projects across to GitLab.
>
“Old SVN users couldn’t see the benefits of Git at first. It took a month or two to convince them.â€
>
-Slowly, by showing his colleagues how easy it was to use Git, the majority of the team’s projects were migrated to GitLab.
+Slowly, by showing his colleagues how easy it was to use Git, the majority of the team’s projects were migrated to GitLab.
-The engineering team have been using GitLab CE for around 2 years now. Nazim credits himself as being entirely responsible for his company’s decision to move to GitLab.
+The engineering team have been using GitLab CE for around 2 years now. Nazim credits himself as being entirely responsible for his company’s decision to move to GitLab.
-#### Frustrations
-##### Adoption to GitLab has been slow
+### Frustrations
+#### Adoption to GitLab has been slow
Not only has the engineering team had to get to grips with Git, they’ve also had to adapt to using GitLab. Due to lack of training and existing skills in other tools, the full feature set of GitLab CE is not being utilised. Nazim sold GitLab to his manager as an ‘all in one’ tool which would replace multiple tools used within the company, thus saving costs. Nazim hasn’t had the time to integrate the legacy tools to GitLab and he’s struggling to convince his peers to change their habits.
-##### Missing Features
+#### Missing Features
Nazim’s company want GitLab to be able to do everything. There isn’t a large budget for software, so they’re selective about what tools are implemented. It needs to add real value to the company. In order for GitLab to be widely adopted and to meet the requirements of different roles within the company, it needs a host of features. When an individual within Nazim’s company wants to know if GitLab has a specific feature or does a particular thing, Nazim is the person to ask. He becomes the point of contact to investigate, build or sometimes just raise the feature request. Nazim gets frustrated when GitLab isn’t able to do what he or his colleagues need it to do.
-##### Regressions and bugs
+#### Regressions and bugs
Nazim often has to calm down his colleagues, when a release contains regressions or new bugs. As he puts it “every new version adds something awesome, but breaks somethingâ€. He feels that “old issues for "minor" annoyances get quickly buried in the mass of open issues and linger for a very long time. More generally, I have the feeling that GitLab focus on adding new functionalities, but overlook a bunch of annoying minor regressions or introduced bugs.†Due to limited resource and expertise within the team, not only is it difficult to remain up-to-date with the frequent release cycle, it’s also counterproductive to fix workflows every month.
-##### Uses too much RAM and CPU
+#### Uses too much RAM and CPU
>
“Memory usages mean that if we host it from a cloud based host like AWS, we spend almost as much on the instance as what we would pay GitHubâ€
>
-##### UI/UX
+#### UI/UX
GitLab’s interface initially attracted Nazim when he was comparing version control software. He thought it would help his less technical colleagues to adapt to using Git and perhaps, GitLab could be rolled out to other areas of the business, beyond engineering. However, using GitLab’s interface daily has left him frustrated at the lack of personalisation / control over his user experience. He’s also regularly lost in a maze of navigation. Whilst he acknowledges that GitLab listens to its users and that the interface is improving, he becomes annoyed when the changes are too progressive. “Too frequent UI changes. Most of them tend to turn out great after a few cycles of fixes, but the frequency is still far too high for me to feel comfortable to always stay on the current release.â€
-#### Goals
+### Goals
* To convince his colleagues to fully adopt GitLab CE, thus improving workflow and collaboration.
* To use a feature rich version control platform that covers all stages of the development lifecycle, in order to reduce dependencies on other tools.
* To use an intuitive and stable product, so he can spend more time on his core job responsibilities and less time bug-fixing, guiding colleagues, etc.
-<hr>
+---
-### James Mackey
+## James Mackey
- Medium to large size organisations using CE or EE
- Small organisations using EE
<img src="img/james-mackey.png" width="300px">
-#### Demographics
+### Demographics
- **Age**<br>36 years old
- **Location**<br>US
@@ -90,7 +91,7 @@ GitLab’s interface initially attracted Nazim when he was comparing version con
- **Frequently used programming languages**<br>JavaScript, SQL, Node.js, Java, PHP, Python
- **Hobbies / interests**<br>DevOps, open source, web development, science, automation and electronics.
-#### Motivations
+### Motivations
James works for a research company which currently hires around 800 staff. He began using GitLab.com back in 2013 for his own open source, hobby projects and loved “the simplicity of installation, administration and useâ€. After using GitLab for over a year, he began to wonder about using it at work. James explains:
>
@@ -99,7 +100,7 @@ James works for a research company which currently hires around 800 staff. He be
James and his colleagues also reviewed competitor products including GitHub Enterprise, but they found it “less innovative and with considerable costs...GitLab had the features we wanted at a much lower cost per head than GitHubâ€.
-The company James works for provides employees with a discretionary budget to spend how they want on software, so James and his team decided to upgrade to EE.
+The company James works for provides employees with a discretionary budget to spend how they want on software, so James and his team decided to upgrade to EE.
James feels partially responsible for his organisation’s decision to start using GitLab.
@@ -107,33 +108,33 @@ James feels partially responsible for his organisation’s decision to start usi
“It's still up to the teams themselves [to decide] which tools to use. We just had a great experience moving our daily development to GitLab, so other teams have followed the path or are thinking about switching.â€
>
-#### Frustrations
-##### Third Party Integration
+### Frustrations
+#### Third Party Integration
Some of GitLab EE’s features are too basic, in particular, issues boards which do not have the level of reporting that James and his team need. Subsequently, they still need to use GitLab EE in conjunction with other tools, such as JIRA. Whilst James feels it isn’t essential for GitLab to meet all his needs (his company are happy for him to use, and pay for, multiple tools), he sometimes isn’t sure what is/isn’t possible with plugins and what level of custom development he and his team will need to do.
-##### UX/UI
+#### UX/UI
James and his team use CI quite heavily for several projects. Whilst they’ve welcomed improvements to the builds and pipelines interface, they still have some difficulty following build process on the different tabs under Pipelines. Some confusion has arisen from not knowing where to find different pieces of information or how to get to the next stages logs from the current stage’s log output screen. They feel more intuitive linking and flow may alleviate the problem. Generally, they feel GitLab’s navigation needs to reviewed and optimised.
-##### Permissions
+#### Permissions
>
“There is no granular control over user or group permissions. The permissions for a project are too tightly coupled to the permissions for Gitlab CI/build pipelines.â€
>
-#### Goals
+### Goals
* To be able to integrate third party tools easily with GitLab EE and to create custom integrations and patches where needed.
* To use GitLab EE primarily for code hosting, merge requests, continuous integration and issue management. James and his team want to be able to understand and use these particular features easily.
* To able to share one instance of GitLab EE with multiple teams across the business. Advanced user management, the ability to separate permissions on different parts of the source code, etc are important to James.
-<hr>
+---
-### Karolina Plaskaty
+## Karolina Plaskaty
- Using GitLab.com for personal/hobby projects
- Would like to use GitLab at work
-- Working for a medium to large size organisation
+- Working for a medium to large size organisation
<img src="img/karolina-plaskaty.png" width="300px">
-#### Demographics
+### Demographics
- **Age**<br>26 years old
- **Location**<br>UK
@@ -143,22 +144,22 @@ James and his team use CI quite heavily for several projects. Whilst they’ve w
- **Frequently used programming languages**<br>JavaScript and SQL
- **Hobbies / interests**<br>Web development, mobile development, UX, open source, gaming and travel.
-#### Motivations
+### Motivations
Karolina has been using GitLab.com for around a year. She roughly spends 8 hours every week programming, of that, 2 hours is spent contributing to open source projects. Karolina contributes to open source projects to gain programming experience and to give back to the community. She likes GitLab.com for its free private repositories and range of features which provide her with everything she needs for her personal projects. Karolina is also a massive fan of GitLab’s values and the fact that it isn’t a “behemoth of a companyâ€. She explains that “displaying every single thing (doc, culture, assumptions, development...) in the open gives me greater confidence to choose Gitlab personally and to recommend it at work.†She’s also an avid reader of GitLab’s blog.
Karolina works for a software development company which currently hires around 500 people. Karolina would love to use GitLab at work but the company has used GitHub Enterprise for a number of years. She describes management at her company as “old fashioned†and explains that it’s “less of a technical issue and more of a cultural issue†to convince upper management to move to GitLab. Karolina is also relatively new to the company so she’s apprehensive about pushing too hard to change version control platforms.
-#### Frustrations
-##### Unable to use GitLab at work
+### Frustrations
+#### Unable to use GitLab at work
Karolina wants to use GitLab at work but isn’t sure how to approach the subject with management. In her current role, she doesn’t feel that she has the authority to request GitLab.
-##### Performance
+#### Performance
GitLab.com is frequently slow and unavailable. Karolina has also heard that GitLab is a “memory hog†which has deterred her from running GitLab on her own machine for just hobby / personal projects.
-##### UX/UI
+#### UX/UI
Karolina has an interest in UX and therefore has strong opinions about how GitLab should look and feel. She feels the interface is cluttered, “it has too many links/buttons†and the navigation “feels a bit weird sometimes. I get lost if I don’t pay attention.†As Karolina also enjoys contributing to open-source projects, it’s important to her that GitLab is well designed for public repositories, she doesn’t feel that GitLab currently achieves this.
-#### Goals
+### Goals
* To develop her programming experience and to learn from other developers.
* To contribute to both her own and other open source projects.
-* To use a fast and intuitive version control platform. \ No newline at end of file
+* To use a fast and intuitive version control platform.
diff --git a/doc/gitlab-basics/README.md b/doc/gitlab-basics/README.md
index 3d893ba53dd..4e15f7cfd49 100644
--- a/doc/gitlab-basics/README.md
+++ b/doc/gitlab-basics/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# GitLab basics
Step-by-step guides on the basics of working with Git and GitLab.
diff --git a/doc/gitlab-basics/add-merge-request.md b/doc/gitlab-basics/add-merge-request.md
index bf01fe51dc3..5cc014419ad 100644
--- a/doc/gitlab-basics/add-merge-request.md
+++ b/doc/gitlab-basics/add-merge-request.md
@@ -3,31 +3,28 @@
Merge requests are useful to integrate separate changes that you've made to a
project, on different branches. This is a brief guide on how to create a merge
request. For more information, check the
-[merge requests documentation](../user/project/merge_requests.md).
+[merge requests documentation](../user/project/merge_requests/index.md).
---
1. Before you start, you should have already [created a branch](create-branch.md)
and [pushed your changes](basic-git-commands.md) to GitLab.
-
-1. You can then go to the project where you'd like to merge your changes and
- click on the **Merge requests** tab.
-
- ![Merge requests](img/project_navbar.png)
-
+1. Go to the project where you'd like to merge your changes and click on the
+ **Merge requests** tab.
1. Click on **New merge request** on the right side of the screen.
-
- ![New Merge Request](img/merge_request_new.png)
-
-1. Select a source branch and click on the **Compare branches and continue** button.
+1. From there on, you have the option to select the source branch and the target
+ branch you'd like to compare to. The default target project is the upstream
+ repository, but you can choose to compare across any of its forks.
![Select a branch](img/merge_request_select_branch.png)
+1. When ready, click on the **Compare branches and continue** button.
1. At a minimum, add a title and a description to your merge request. Optionally,
select a user to review your merge request and to accept or close it. You may
also select a milestone and labels.
![New merge request page](img/merge_request_page.png)
-1. When ready, click on the **Submit merge request** button. Your merge request
- will be ready to be approved and published.
+1. When ready, click on the **Submit merge request** button.
+
+Your merge request will be ready to be approved and merged.
diff --git a/doc/gitlab-basics/img/merge_request_new.png b/doc/gitlab-basics/img/merge_request_new.png
deleted file mode 100644
index 6fcd7bebada..00000000000
--- a/doc/gitlab-basics/img/merge_request_new.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/img/merge_request_select_branch.png b/doc/gitlab-basics/img/merge_request_select_branch.png
index 9f6b93943a9..57ea0e65f34 100644
--- a/doc/gitlab-basics/img/merge_request_select_branch.png
+++ b/doc/gitlab-basics/img/merge_request_select_branch.png
Binary files differ
diff --git a/doc/gitlab-basics/img/project_navbar.png b/doc/gitlab-basics/img/project_navbar.png
deleted file mode 100644
index be6f38ede32..00000000000
--- a/doc/gitlab-basics/img/project_navbar.png
+++ /dev/null
Binary files differ
diff --git a/doc/install/README.md b/doc/install/README.md
index 656f8720361..540cb0d3f38 100644
--- a/doc/install/README.md
+++ b/doc/install/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Installation
GitLab can be installed via various ways. Check the [installation methods][methods]
diff --git a/doc/install/relative_url.md b/doc/install/relative_url.md
index 713d11b75e4..2f5d4142d04 100644
--- a/doc/install/relative_url.md
+++ b/doc/install/relative_url.md
@@ -1,11 +1,11 @@
-## Install GitLab under a relative URL
+# Install GitLab under a relative URL
-_**Note:**
+NOTE: **Note:**
This document describes how to run GitLab under a relative URL for installations
from source. If you are using an Omnibus package,
[the steps are different][omnibus-rel]. Use this guide along with the
[installation guide](installation.md) if you are installing GitLab for the
-first time._
+first time.
---
@@ -33,7 +33,7 @@ serve GitLab under a relative URL is:
After all the changes you need to recompile the assets and [restart GitLab].
-### Relative URL requirements
+## Relative URL requirements
If you configure GitLab with a relative URL, the assets (JavaScript, CSS, fonts,
images, etc.) will need to be recompiled, which is a task which consumes a lot
@@ -43,11 +43,11 @@ least 2GB of RAM available on your system, while we recommend 4GB RAM, and 4 or
See the [requirements](requirements.md) document for more information.
-### Enable relative URL in GitLab
+## Enable relative URL in GitLab
-_**Note:**
+NOTE: **Note:**
Do not make any changes to your web server configuration file regarding
-relative URL. The relative URL support is implemented by GitLab Workhorse._
+relative URL. The relative URL support is implemented by GitLab Workhorse.
---
@@ -115,7 +115,7 @@ Make sure to follow all steps below:
1. [Restart GitLab][] for the changes to take effect.
-### Disable relative URL in GitLab
+## Disable relative URL in GitLab
To disable the relative URL:
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index 3d7becd18fc..7bf126eec5d 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -121,7 +121,7 @@ Existing users using GitLab with MySQL/MariaDB are advised to
### PostgreSQL Requirements
-As of GitLab 10.0, PostgreSQL 9.6 or newer is required, and earlier versions are
+As of GitLab 10.0, PostgreSQL 9.6 or newer (but less than 10) is required, and earlier versions are
not supported. We highly recommend users to use PostgreSQL 9.6 as this
is the PostgreSQL version used for development and testing.
@@ -184,7 +184,7 @@ Runner.
We recommend using a separate machine for each GitLab Runner, if you plan to
use the CI features.
-[security reasons]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/security/index.md
+[security reasons]: https://gitlab.com/gitlab-org/gitlab-runner/blob/master/docs/security/index.md
## Supported web browsers
diff --git a/doc/integration/README.md b/doc/integration/README.md
index 09d96bdd338..54e78bdef54 100644
--- a/doc/integration/README.md
+++ b/doc/integration/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# GitLab Integration
GitLab integrates with multiple third-party services to allow external issue
diff --git a/doc/intro/README.md b/doc/intro/README.md
index 7485912d1a2..d9acc5bdeac 100644
--- a/doc/intro/README.md
+++ b/doc/intro/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Get started with GitLab
## Organize
diff --git a/doc/legal/README.md b/doc/legal/README.md
index 56d72ae3859..6413f1d645f 100644
--- a/doc/legal/README.md
+++ b/doc/legal/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Legal
- [Corporate contributor license agreement](corporate_contributor_license_agreement.md)
diff --git a/doc/legal/corporate_contributor_license_agreement.md b/doc/legal/corporate_contributor_license_agreement.md
index 7f08188bd65..ebb24ba0a7f 100644
--- a/doc/legal/corporate_contributor_license_agreement.md
+++ b/doc/legal/corporate_contributor_license_agreement.md
@@ -1,29 +1,2 @@
-# Corporate contributor license agreement
-
-You accept and agree to the following terms and conditions for Your present and future Contributions submitted to GitLab B.V.. Except for the license granted herein to GitLab B.V. and recipients of software distributed by GitLab B.V., You reserve all right, title, and interest in and to Your Contributions.
-
-1. Definitions.
-
- "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with GitLab B.V.. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
-
- "Contribution" shall mean the code, documentation or other original works of authorship, including any modifications or additions to an existing work, that is submitted by You to GitLab B.V. for inclusion in, or documentation of, any of the products owned or managed by GitLab B.V. (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to GitLab B.V. or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, GitLab B.V. for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution."
-
-2. Grant of Copyright License.
-
-Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works.
-
-3. Grant of Patent License.
-
-Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed.
-
-4. You represent that You are legally entitled to grant the above license. You represent further that each of Your employees is authorized to submit Contributions on Your behalf, but excluding employees that are designated in writing by You as "Not authorized to submit Contributions on behalf of [name of Your corporation here]." Such designations of exclusion for unauthorized employees are to be submitted via email to legal@gitlab.com.
-
-5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others).
-
-6. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
-
-7. Should You wish to submit work that is not Your original creation, You may submit it to GitLab B.V. separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]".
-
-8. It is Your responsibility to notify GitLab.com when any change is required to the list of designated employees excluded from submitting Contributions on Your behalf per Section 4. Such notification should be sent via email to legal@gitlab.com.
-
-This text is licensed under the [Creative Commons Attribution 3.0 License](https://creativecommons.org/licenses/by/3.0/) and the original source is the Google Open Source Programs Office.
+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
diff --git a/doc/legal/individual_contributor_license_agreement.md b/doc/legal/individual_contributor_license_agreement.md
index 59803aea080..ebb24ba0a7f 100644
--- a/doc/legal/individual_contributor_license_agreement.md
+++ b/doc/legal/individual_contributor_license_agreement.md
@@ -1,25 +1,2 @@
-# Individual contributor license agreement
-
-You accept and agree to the following terms and conditions for Your present and future Contributions submitted to GitLab B.V.. Except for the license granted herein to GitLab B.V. and recipients of software distributed by GitLab B.V., You reserve all right, title, and interest in and to Your Contributions.
-
-1. Definitions.
-
- "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with GitLab B.V.. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
-
- "Contribution" shall mean any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to GitLab B.V. for inclusion in, or documentation of, any of the products owned or managed by GitLab B.V. (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to GitLab B.V. or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, GitLab B.V. for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution."
-
-2. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works.
-
-3. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed.
-
-4. You represent that you are legally entitled to grant the above license. If your employer(s) has rights to intellectual property that you create that includes your Contributions, you represent that you have received permission to make Contributions on behalf of that employer, that your employer has waived such rights for your Contributions to GitLab B.V., or that your employer has executed a separate Corporate CLA with GitLab B.V..
-
-5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others). You represent that Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of Your Contributions.
-
-6. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON- INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
-
-7. Should You wish to submit work that is not Your original creation, You may submit it to GitLab B.V. separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [insert_name_here]".
-
-8. You agree to notify GitLab B.V. of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect.
-
-This text is licensed under the [Creative Commons Attribution 3.0 License](https://creativecommons.org/licenses/by/3.0/) and the original source is the Google Open Source Programs Office.
+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
diff --git a/doc/migrate_ci_to_ce/README.md b/doc/migrate_ci_to_ce/README.md
index 2e7782736ff..9347a834510 100644
--- a/doc/migrate_ci_to_ce/README.md
+++ b/doc/migrate_ci_to_ce/README.md
@@ -372,8 +372,10 @@ CREATE TABLE
```
To fix that you need to apply this SQL statement before doing final backup:
-```
-# Omnibus
+
+```sql
+## Omnibus GitLab
+
gitlab-ci-rails dbconsole <<EOF
-- ALTER TABLES - DROP DEFAULTS
ALTER TABLE ONLY ci_application_settings ALTER COLUMN id DROP DEFAULT;
@@ -427,7 +429,8 @@ ALTER TABLE ONLY ci_variables ALTER COLUMN id SET DEFAULT nextval('ci_variables_
ALTER TABLE ONLY ci_web_hooks ALTER COLUMN id SET DEFAULT nextval('ci_web_hooks_id_seq'::regclass);
EOF
-# Source
+## Source installations
+
cd /home/gitlab_ci/gitlab-ci
sudo -u gitlab_ci -H bundle exec rails dbconsole production <<EOF
... COPY SQL STATEMENTS FROM ABOVE ...
diff --git a/doc/policy/maintenance.md b/doc/policy/maintenance.md
index 7ab56c89014..8d0afa9e692 100644
--- a/doc/policy/maintenance.md
+++ b/doc/policy/maintenance.md
@@ -19,24 +19,30 @@ For example, for GitLab version 10.5.7:
* `5` represents minor version
* `7` represents patch number
-## Security releases
+## Patch releases
-The current stable release will receive security patches and bug fixes
-(eg. `8.9.0` -> `8.9.1`).
+Patch releases usually only include bug fixes and are only done for the current
+stable release. That said, in some cases, we may backport it to previous stable
+release, depending on the severity of the bug.
-Feature releases will mark the next supported stable
-release where the minor version is increased numerically by increments of one
-(eg. `8.9 -> 8.10`).
+For instance, if we release `10.1.1` with a fix for a severe bug introduced in
+`10.0.0`, we could backport the fix to a new `10.0.x` patch release.
-Our current policy is to support one stable release at any given time.
-For medium-level security issues, we may consider backporting to the previous two
+### Security releases
+
+Security releases are a special kind of patch release that only include security
+fixes and patches (see below).
+
+Our current policy is to support one stable release at any given time, but for
+medium-level security issues, we may backport security fixes to the previous two
monthly releases.
-For very serious security issues, there is [precedent](https://about.gitlab.com/2016/05/02/cve-2016-4340-patches/)
-to backport security fixes to even more monthly releases of GitLab. This decision
-is made on a case-by-case basis.
+For very serious security issues, there is
+[precedent](https://about.gitlab.com/2016/05/02/cve-2016-4340-patches/)
+to backport security fixes to even more monthly releases of GitLab.
+This decision is made on a case-by-case basis.
-## Version support
+## Upgrade recommendations
We encourage everyone to run the latest stable release to ensure that you can
easily upgrade to the most secure and feature-rich GitLab experience. In order
@@ -70,7 +76,6 @@ Please see the table below for some examples:
| -------------- | ------------ | ------------------------ | ---------------- |
| 9.4.5 | 8.13.4 | `8.13.4` -> `8.17.7` -> `9.4.5` | `8.17.7` is the last version in version `8` |
| 10.1.4 | 8.13.4 | `8.13.4` -> `8.17.7` -> `9.5.8` -> `10.1.4` | `8.17.7` is the last version in version `8`, `9.5.8` is the last version in version `9` |
-|
More information about the release procedures can be found in our
[release-tools documentation][rel]. You may also want to read our
diff --git a/doc/raketasks/README.md b/doc/raketasks/README.md
index 2b81ebc9c59..2f916f5dea7 100644
--- a/doc/raketasks/README.md
+++ b/doc/raketasks/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Rake tasks
- [Backup restore](backup_restore.md)
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index e4c09b2b507..54c3e20d61d 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -136,44 +136,54 @@ In the example below we use Amazon S3 for storage, but Fog also lets you use
for AWS, Google, OpenStack Swift, Rackspace and Aliyun as well. A local driver is
[also available](#uploading-to-locally-mounted-shares).
-For omnibus packages, add the following to `/etc/gitlab/gitlab.rb`:
+#### Using Amazon S3
-```ruby
-gitlab_rails['backup_upload_connection'] = {
- 'provider' => 'AWS',
- 'region' => 'eu-west-1',
- 'aws_access_key_id' => 'AKIAKIAKI',
- 'aws_secret_access_key' => 'secret123'
- # If using an IAM Profile, don't configure aws_access_key_id & aws_secret_access_key
- # 'use_iam_profile' => true
-}
-gitlab_rails['backup_upload_remote_directory'] = 'my.s3.bucket'
-```
+For Omnibus GitLab packages:
+
+1. Add the following to `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_rails['backup_upload_connection'] = {
+ 'provider' => 'AWS',
+ 'region' => 'eu-west-1',
+ 'aws_access_key_id' => 'AKIAKIAKI',
+ 'aws_secret_access_key' => 'secret123'
+ # If using an IAM Profile, don't configure aws_access_key_id & aws_secret_access_key
+ # 'use_iam_profile' => true
+ }
+ gitlab_rails['backup_upload_remote_directory'] = 'my.s3.bucket'
+ ```
+
+1. [Reconfigure GitLab] for the changes to take effect
-Make sure to run `sudo gitlab-ctl reconfigure` after editing `/etc/gitlab/gitlab.rb` to reflect the changes.
+---
For installations from source:
-```yaml
- backup:
- # snip
- upload:
- # Fog storage connection settings, see http://fog.io/storage/ .
- connection:
- provider: AWS
- region: eu-west-1
- aws_access_key_id: AKIAKIAKI
- aws_secret_access_key: 'secret123'
- # If using an IAM Profile, leave aws_access_key_id & aws_secret_access_key empty
- # ie. aws_access_key_id: ''
- # use_iam_profile: 'true'
- # The remote 'directory' to store your backups. For S3, this would be the bucket name.
- remote_directory: 'my.s3.bucket'
- # Turns on AWS Server-Side Encryption with Amazon S3-Managed Keys for backups, this is optional
- # encryption: 'AES256'
- # Specifies Amazon S3 storage class to use for backups, this is optional
- # storage_class: 'STANDARD'
-```
+1. Edit `home/git/gitlab/config/gitlab.yml`:
+
+ ```yaml
+ backup:
+ # snip
+ upload:
+ # Fog storage connection settings, see http://fog.io/storage/ .
+ connection:
+ provider: AWS
+ region: eu-west-1
+ aws_access_key_id: AKIAKIAKI
+ aws_secret_access_key: 'secret123'
+ # If using an IAM Profile, leave aws_access_key_id & aws_secret_access_key empty
+ # ie. aws_access_key_id: ''
+ # use_iam_profile: 'true'
+ # The remote 'directory' to store your backups. For S3, this would be the bucket name.
+ remote_directory: 'my.s3.bucket'
+ # Turns on AWS Server-Side Encryption with Amazon S3-Managed Keys for backups, this is optional
+ # encryption: 'AES256'
+ # Specifies Amazon S3 storage class to use for backups, this is optional
+ # storage_class: 'STANDARD'
+ ```
+
+1. [Restart GitLab] for the changes to take effect
If you are uploading your backups to S3 you will probably want to create a new
IAM user with restricted access rights. To give the upload user access only for
@@ -226,6 +236,50 @@ with the name of your bucket:
}
```
+#### Using Google Cloud Storage
+
+If you want to use Google Cloud Storage to save backups, you'll have to create
+an access key from the Google console first:
+
+1. Go to the storage settings page https://console.cloud.google.com/storage/settings
+1. Select "Interoperability" and create an access key
+1. Make note of the "Access Key" and "Secret" and replace them in the
+ configurations below
+1. Make sure you already have a bucket created
+
+For Omnibus GitLab packages:
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_rails['backup_upload_connection'] = {
+ 'provider' => 'Google',
+ 'google_storage_access_key_id' => 'Access Key',
+ 'google_storage_secret_access_key' => 'Secret'
+ }
+ gitlab_rails['backup_upload_remote_directory'] = 'my.google.bucket'
+ ```
+
+1. [Reconfigure GitLab] for the changes to take effect
+
+---
+
+For installations from source:
+
+1. Edit `home/git/gitlab/config/gitlab.yml`:
+
+ ```yaml
+ backup:
+ upload:
+ connection:
+ provider: 'Google'
+ google_storage_access_key_id: 'Access Key'
+ google_storage_secret_access_key: 'Secret'
+ remote_directory: 'my.google.bucket'
+ ```
+
+1. [Restart GitLab] for the changes to take effect
+
### Uploading to locally mounted shares
You may also send backups to a mounted share (`NFS` / `CIFS` / `SMB` / etc.) by
@@ -554,3 +608,6 @@ The rake task runs this as the `gitlab` user which does not have the superuser a
Those objects have no influence on the database backup/restore but they give this annoying warning.
For more information see similar questions on postgresql issue tracker[here](http://www.postgresql.org/message-id/201110220712.30886.adrian.klaver@gmail.com) and [here](http://www.postgresql.org/message-id/2039.1177339749@sss.pgh.pa.us) as well as [stack overflow](http://stackoverflow.com/questions/4368789/error-must-be-owner-of-language-plpgsql).
+
+[reconfigure GitLab]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
diff --git a/doc/raketasks/user_management.md b/doc/raketasks/user_management.md
index 3ae46019daf..5554a0c8b78 100644
--- a/doc/raketasks/user_management.md
+++ b/doc/raketasks/user_management.md
@@ -149,18 +149,3 @@ cp config/secrets.yml.bak config/secrets.yml
sudo /etc/init.d/gitlab start
```
-
-## Clear authentication tokens for all users. Important! Data loss!
-
-Clear authentication tokens for all users in the GitLab database. This
-task is useful if your users' authentication tokens might have been exposed in
-any way. All the existing tokens will become invalid, and new tokens are
-automatically generated upon sign-in or user modification.
-
-```
-# omnibus-gitlab
-sudo gitlab-rake gitlab:users:clear_all_authentication_tokens
-
-# installation from source
-bundle exec rake gitlab:users:clear_all_authentication_tokens RAILS_ENV=production
-```
diff --git a/doc/security/README.md b/doc/security/README.md
index 0fea6be8b55..d397ff104ab 100644
--- a/doc/security/README.md
+++ b/doc/security/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Security
- [Password length limits](password_length_limits.md)
diff --git a/doc/ssh/README.md b/doc/ssh/README.md
index 793de9d777c..33a2d7a88a7 100644
--- a/doc/ssh/README.md
+++ b/doc/ssh/README.md
@@ -1,4 +1,4 @@
-# SSH
+# GitLab and SSH keys
Git is a distributed version control system, which means you can work locally
but you can also share or "push" your changes to other servers.
@@ -114,7 +114,7 @@ custom name continue onto the next step.
If you manually copied your public SSH key make sure you copied the entire
key starting with `ssh-rsa` and ending with your email.
-
+
1. Optionally you can test your setup by running `ssh -T git@example.com`
(replacing `example.com` with your GitLab domain) and verifying that you
receive a `Welcome to GitLab` message.
@@ -172,7 +172,7 @@ dummy user account.
If you are a project master or owner, you can add a deploy key in the
project settings under the section 'Repository'. Specify a title for the new
deploy key and paste a public SSH key. After this, the machine that uses
-the corresponding private SSH key has read-only or read-write (if enabled)
+the corresponding private SSH key has read-only or read-write (if enabled)
access to the project.
You can't add the same deploy key twice using the form.
@@ -232,7 +232,7 @@ something is wrong with your SSH setup.
- Ensure that you generated your SSH key pair correctly and added the public SSH
key to your GitLab profile
-- Try manually registering your private SSH key using `ssh-agent` as documented
+- Try manually registering your private SSH key using `ssh-agent` as documented
earlier in this document
- Try to debug the connection by running `ssh -Tv git@example.com`
(replacing `example.com` with your GitLab domain)
diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md
index a45a4eb9e49..f2a9b1d769b 100644
--- a/doc/system_hooks/system_hooks.md
+++ b/doc/system_hooks/system_hooks.md
@@ -1,6 +1,24 @@
# System hooks
-Your GitLab instance can perform HTTP POST requests on the following events: `project_create`, `project_destroy`, `project_rename`, `project_transfer`, `project_update`, `user_add_to_team`, `user_remove_from_team`, `user_create`, `user_destroy`, `key_create`, `key_destroy`, `group_create`, `group_destroy`, `user_add_to_group` and `user_remove_from_group`.
+Your GitLab instance can perform HTTP POST requests on the following events:
+
+- `project_create`
+- `project_destroy`
+- `project_rename`
+- `project_transfer`
+- `project_update`
+- `user_add_to_team`
+- `user_remove_from_team`
+- `user_create`
+- `user_destroy`
+- `user_rename`
+- `key_create`
+- `key_destroy`
+- `group_create`
+- `group_destroy`
+- `group_rename`
+- `user_add_to_group`
+- `user_remove_from_group`
The triggers for most of these are self-explanatory, but `project_update` and `project_rename` deserve some clarification: `project_update` is fired any time an attribute of a project is changed (name, description, tags, etc.) *unless* the `path` attribute is also changed. In that case, a `project_rename` is triggered instead (so that, for instance, if all you care about is the repo URL, you can just listen for `project_rename`).
@@ -72,6 +90,9 @@ X-Gitlab-Event: System Hook
}
```
+Note that `project_rename` is not triggered if the namespace changes.
+Please refer to `group_rename` and `user_rename` for that case.
+
**Project transferred:**
```json
@@ -175,6 +196,21 @@ X-Gitlab-Event: System Hook
}
```
+**User renamed:**
+
+```json
+{
+ "event_name": "user_rename",
+ "created_at": "2017-11-01T11:21:04Z",
+ "updated_at": "2017-11-01T14:04:47Z",
+ "name": "new-name",
+ "email": "best-email@example.tld",
+ "user_id": 58,
+ "username": "new-exciting-name",
+ "old_username": "old-boring-name"
+}
+```
+
**Key added**
```json
@@ -209,13 +245,15 @@ X-Gitlab-Event: System Hook
"updated_at": "2012-07-21T07:38:22Z",
"event_name": "group_create",
"name": "StoreCloud",
- "owner_email": "johnsmith@gmail.com",
- "owner_name": "John Smith",
+ "owner_email": null,
+ "owner_name": null,
"path": "storecloud",
"group_id": 78
}
```
+`owner_name` and `owner_email` are always `null`. Please see https://gitlab.com/gitlab-org/gitlab-ce/issues/39675.
+
**Group removed:**
```json
@@ -224,13 +262,35 @@ X-Gitlab-Event: System Hook
"updated_at": "2012-07-21T07:38:22Z",
"event_name": "group_destroy",
"name": "StoreCloud",
- "owner_email": "johnsmith@gmail.com",
- "owner_name": "John Smith",
+ "owner_email": null,
+ "owner_name": null,
"path": "storecloud",
"group_id": 78
}
```
+`owner_name` and `owner_email` are always `null`. Please see https://gitlab.com/gitlab-org/gitlab-ce/issues/39675.
+
+**Group renamed:**
+
+```json
+{
+ "event_name": "group_rename",
+ "created_at": "2017-10-30T15:09:00Z",
+ "updated_at": "2017-11-01T10:23:52Z",
+ "name": "Better Name",
+ "path": "better-name",
+ "full_path": "parent-group/better-name",
+ "group_id": 64,
+ "owner_name": null,
+ "owner_email": null,
+ "old_path": "old-name",
+ "old_full_path": "parent-group/old-name"
+}
+```
+
+`owner_name` and `owner_email` are always `null`. Please see https://gitlab.com/gitlab-org/gitlab-ce/issues/39675.
+
**New Group Member:**
```json
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 5561784ed0b..1cfdabac248 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -144,6 +144,12 @@ has a `.gitlab-ci.yml` or not:
All you need to do is remove your existing `.gitlab-ci.yml`, and you can even
do that in a branch to test Auto DevOps before committing to `master`.
+NOTE: **Note:**
+If you are a GitLab Administrator, you can enable Auto DevOps instance wide
+in **Admin Area > Settings > Continuous Integration and Deployment**. Doing that,
+all the projects that haven't explicitly set an option will have Auto DevOps
+enabled by default.
+
## Stages of Auto DevOps
The following sections describe the stages of Auto DevOps. Read them carefully
@@ -517,7 +523,7 @@ Feature.get(:auto_devops_banner_disabled).enable
Or through the HTTP API with an admin access token:
```sh
-curl --data "value=true" --header "PRIVATE-TOKEN: private_token" https://gitlab.example.com/api/v4/features/auto_devops_banner_disabled
+curl --data "value=true" --header "PRIVATE-TOKEN: personal_access_token" https://gitlab.example.com/api/v4/features/auto_devops_banner_disabled
```
[ce-37115]: https://gitlab.com/gitlab-org/gitlab-ce/issues/37115
diff --git a/doc/university/README.md b/doc/university/README.md
index 170582bcd0c..55865ac23e8 100644
--- a/doc/university/README.md
+++ b/doc/university/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# GitLab University
GitLab University is the best place to learn about **Version Control with Git and GitLab**.
@@ -51,10 +55,10 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
#### 1.5. Migrating from other Source Control
-1. [Migrating from BitBucket/Stash](https://docs.gitlab.com/ee/workflow/importing/import_projects_from_bitbucket.html)
-1. [Migrating from GitHub](https://docs.gitlab.com/ee/workflow/importing/import_projects_from_github.html)
-1. [Migrating from SVN](https://docs.gitlab.com/ee/workflow/importing/migrating_from_svn.html)
-1. [Migrating from Fogbugz](https://docs.gitlab.com/ee/workflow/importing/import_projects_from_fogbugz.html)
+1. [Migrating from BitBucket/Stash](https://docs.gitlab.com/ee/user/project/import/bitbucket.html)
+1. [Migrating from GitHub](https://docs.gitlab.com/ee/user/project/import/github.html)
+1. [Migrating from SVN](https://docs.gitlab.com/ee/user/project/import/svn.html)
+1. [Migrating from Fogbugz](https://docs.gitlab.com/ee/user/project/import/fogbugz.html)
#### 1.6. GitLab Inc.
@@ -76,13 +80,13 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
- Being part of our Great Community and Contributing to GitLab
1. [Getting Started with the GitLab Development Kit (GDK)](https://about.gitlab.com/2016/06/08/getting-started-with-gitlab-development-kit/)
1. [Contributing Technical Articles to the GitLab Blog](https://about.gitlab.com/2016/01/26/call-for-writers/)
-1. [GitLab Training Workshops](https://about.gitlab.com/training)
+1. [GitLab Training Workshops](https://docs.gitlab.com/ce/university/training/end-user/)
+1. [GitLab Professional Services](https://about.gitlab.com/services/)
#### 1.8 GitLab Training Material
1. [Git and GitLab Terminology](glossary/README.md)
1. [Git and GitLab Workshop - Slides](https://docs.google.com/presentation/d/1JzTYD8ij9slejV2-TO-NzjCvlvj6mVn9BORePXNJoMI/edit?usp=drive_web)
-1. [Git and GitLab Revision](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/university/training/end-user)
---
diff --git a/doc/university/bookclub/booklist.md b/doc/university/bookclub/booklist.md
index c4229832e9f..26c3851276b 100644
--- a/doc/university/bookclub/booklist.md
+++ b/doc/university/bookclub/booklist.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Books
List of books and resources, that may be worth reading.
diff --git a/doc/university/bookclub/index.md b/doc/university/bookclub/index.md
index 022a61f4429..63238685b2b 100644
--- a/doc/university/bookclub/index.md
+++ b/doc/university/bookclub/index.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# The GitLab Book Club
The Book Club is a casual meet-up to read and discuss books we like.
diff --git a/doc/university/glossary/README.md b/doc/university/glossary/README.md
index 9544de41b9a..c6a91c8d5c2 100644
--- a/doc/university/glossary/README.md
+++ b/doc/university/glossary/README.md
@@ -1,4 +1,8 @@
-## What is the Glossary
+---
+comments: false
+---
+
+# What is the Glossary
This contains a simplified list and definitions of some of the terms that you will encounter in your day to day activities when working with GitLab.
Please add any terms that you discover that you think would be useful for others.
@@ -456,7 +460,7 @@ A route table contains rules (called routes) that determine where network traffi
### Runners
-Actual build machines/containers that [run and execute tests](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner) you have specified to be run on GitLab CI.
+Actual build machines/containers that [run and execute tests](https://gitlab.com/gitlab-org/gitlab-runner) you have specified to be run on GitLab CI.
### Sidekiq
diff --git a/doc/university/high-availability/aws/README.md b/doc/university/high-availability/aws/README.md
index 6b8f3cd3d1d..54625996dff 100644
--- a/doc/university/high-availability/aws/README.md
+++ b/doc/university/high-availability/aws/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# High Availability on AWS
diff --git a/doc/university/process/README.md b/doc/university/process/README.md
index 04f2d52514f..fdf6224f7f6 100644
--- a/doc/university/process/README.md
+++ b/doc/university/process/README.md
@@ -1,8 +1,12 @@
---
+comments: false
+---
+
+---
title: University | Process
---
-## Suggesting improvements
+# Suggesting improvements
If you would like to teach a class or participate or help in any way please
submit a merge request and assign it to [Job](https://gitlab.com/u/JobV).
diff --git a/doc/university/support/README.md b/doc/university/support/README.md
index 567dadb3b47..25d5fe351ca 100644
--- a/doc/university/support/README.md
+++ b/doc/university/support/README.md
@@ -1,5 +1,9 @@
+---
+comments: false
+---
-## Support Boot Camp
+
+# Support Boot Camp
**Goal:** Prepare new Service Engineers at GitLab
diff --git a/doc/university/training/end-user/README.md b/doc/university/training/end-user/README.md
index 03c62a81b10..a882bf0eb48 100644
--- a/doc/university/training/end-user/README.md
+++ b/doc/university/training/end-user/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Training
diff --git a/doc/university/training/gitlab_flow.md b/doc/university/training/gitlab_flow.md
index a7db1f2e069..02a6ad48a38 100644
--- a/doc/university/training/gitlab_flow.md
+++ b/doc/university/training/gitlab_flow.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# GitLab Flow
- A simplified branching strategy
diff --git a/doc/university/training/index.md b/doc/university/training/index.md
index 03179ff5a77..14f096b130f 100644
--- a/doc/university/training/index.md
+++ b/doc/university/training/index.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# GitLab Training Material
All GitLab training material is stored in markdown format. Slides are
diff --git a/doc/university/training/topics/additional_resources.md b/doc/university/training/topics/additional_resources.md
index 3ed601625cf..d01634df744 100644
--- a/doc/university/training/topics/additional_resources.md
+++ b/doc/university/training/topics/additional_resources.md
@@ -1,4 +1,8 @@
-## Additional Resources
+---
+comments: false
+---
+
+# Additional Resources
1. GitLab Documentation [http://docs.gitlab.com](http://docs.gitlab.com/)
2. GUI Clients [http://git-scm.com/downloads/guis](http://git-scm.com/downloads/guis)
diff --git a/doc/university/training/topics/agile_git.md b/doc/university/training/topics/agile_git.md
index e6e4fea9b51..251af99bed7 100644
--- a/doc/university/training/topics/agile_git.md
+++ b/doc/university/training/topics/agile_git.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Agile and Git
----------
diff --git a/doc/university/training/topics/bisect.md b/doc/university/training/topics/bisect.md
index a60c4365e0c..2d5ab107fe6 100644
--- a/doc/university/training/topics/bisect.md
+++ b/doc/university/training/topics/bisect.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Bisect
----------
diff --git a/doc/university/training/topics/cherry_picking.md b/doc/university/training/topics/cherry_picking.md
index af7a70a2818..df23024b6ee 100644
--- a/doc/university/training/topics/cherry_picking.md
+++ b/doc/university/training/topics/cherry_picking.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Cherry Pick
----------
diff --git a/doc/university/training/topics/env_setup.md b/doc/university/training/topics/env_setup.md
index 8149379b36f..b7bec83ed8a 100644
--- a/doc/university/training/topics/env_setup.md
+++ b/doc/university/training/topics/env_setup.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Configure your environment
----------
diff --git a/doc/university/training/topics/explore_gitlab.md b/doc/university/training/topics/explore_gitlab.md
index b65457728c0..84a1879cd92 100644
--- a/doc/university/training/topics/explore_gitlab.md
+++ b/doc/university/training/topics/explore_gitlab.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Explore GitLab projects
----------
diff --git a/doc/university/training/topics/feature_branching.md b/doc/university/training/topics/feature_branching.md
index 4b34406ea75..0df5f26dbea 100644
--- a/doc/university/training/topics/feature_branching.md
+++ b/doc/university/training/topics/feature_branching.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Feature branching
----------
diff --git a/doc/university/training/topics/getting_started.md b/doc/university/training/topics/getting_started.md
index ec7bb2631aa..153b45fb4da 100644
--- a/doc/university/training/topics/getting_started.md
+++ b/doc/university/training/topics/getting_started.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Getting Started
----------
diff --git a/doc/university/training/topics/git_add.md b/doc/university/training/topics/git_add.md
index 9ffb4b9c859..651366e0d49 100644
--- a/doc/university/training/topics/git_add.md
+++ b/doc/university/training/topics/git_add.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Git Add
----------
diff --git a/doc/university/training/topics/git_intro.md b/doc/university/training/topics/git_intro.md
index ca1ff29d93b..7e502d6dad4 100644
--- a/doc/university/training/topics/git_intro.md
+++ b/doc/university/training/topics/git_intro.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Git introduction
----------
diff --git a/doc/university/training/topics/git_log.md b/doc/university/training/topics/git_log.md
index 32ebceff491..f2709ae3890 100644
--- a/doc/university/training/topics/git_log.md
+++ b/doc/university/training/topics/git_log.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Git Log
----------
@@ -49,8 +53,8 @@ git log --since=1.month.ago --until=3.weeks.ago
```
cd ~/workspace
-git clone git@gitlab.com:gitlab-org/gitlab-ci-multi-runner.git
-cd gitlab-ci-multi-runner
+git clone git@gitlab.com:gitlab-org/gitlab-runner.git
+cd gitlab-runner
git log --author="Travis"
git log --since=1.month.ago --until=3.weeks.ago
git log --since=1.month.ago --until=1.day.ago --author="Travis"
diff --git a/doc/university/training/topics/gitlab_flow.md b/doc/university/training/topics/gitlab_flow.md
index 8e5d3baf959..b8049b5c80e 100644
--- a/doc/university/training/topics/gitlab_flow.md
+++ b/doc/university/training/topics/gitlab_flow.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# GitLab Flow
----------
diff --git a/doc/university/training/topics/merge_conflicts.md b/doc/university/training/topics/merge_conflicts.md
index 77807b3e7ef..9a1ce550868 100644
--- a/doc/university/training/topics/merge_conflicts.md
+++ b/doc/university/training/topics/merge_conflicts.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Merge conflicts
----------
diff --git a/doc/university/training/topics/merge_requests.md b/doc/university/training/topics/merge_requests.md
index 5b446f02f63..4e8c9de85a1 100644
--- a/doc/university/training/topics/merge_requests.md
+++ b/doc/university/training/topics/merge_requests.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Merge requests
----------
diff --git a/doc/university/training/topics/rollback_commits.md b/doc/university/training/topics/rollback_commits.md
index cf647284604..0db1d93d1dc 100644
--- a/doc/university/training/topics/rollback_commits.md
+++ b/doc/university/training/topics/rollback_commits.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Rollback Commits
----------
diff --git a/doc/university/training/topics/stash.md b/doc/university/training/topics/stash.md
index c1bdda32645..5b27ac12f77 100644
--- a/doc/university/training/topics/stash.md
+++ b/doc/university/training/topics/stash.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Git Stash
----------
diff --git a/doc/university/training/topics/subtree.md b/doc/university/training/topics/subtree.md
index 5d869af64c1..b5a892dc17b 100644
--- a/doc/university/training/topics/subtree.md
+++ b/doc/university/training/topics/subtree.md
@@ -1,8 +1,8 @@
-## Subtree
+---
+comments: false
+---
-----------
-
-## Subtree
+# Subtree
* Used when there are nested repositories.
* Not recommended when the amount of dependencies is too large
diff --git a/doc/university/training/topics/tags.md b/doc/university/training/topics/tags.md
index e9607b5a875..ab48d52d3c3 100644
--- a/doc/university/training/topics/tags.md
+++ b/doc/university/training/topics/tags.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Tags
----------
diff --git a/doc/university/training/topics/unstage.md b/doc/university/training/topics/unstage.md
index 17dbb64b9e6..fc72949ade9 100644
--- a/doc/university/training/topics/unstage.md
+++ b/doc/university/training/topics/unstage.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Unstage
----------
diff --git a/doc/university/training/user_training.md b/doc/university/training/user_training.md
index 9e38df26b6a..90e1d2ba5e8 100644
--- a/doc/university/training/user_training.md
+++ b/doc/university/training/user_training.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# GitLab Git Workshop
---
diff --git a/doc/update/10.0-to-10.1.md b/doc/update/10.0-to-10.1.md
index dc14c779026..af815d26a74 100644
--- a/doc/update/10.0-to-10.1.md
+++ b/doc/update/10.0-to-10.1.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 10.0 to 10.1
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/2.6-to-3.0.md b/doc/update/2.6-to-3.0.md
index 97cd277b424..8f18bd93cea 100644
--- a/doc/update/2.6-to-3.0.md
+++ b/doc/update/2.6-to-3.0.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 2.6 to 3.0
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/2.6-to-3.0.md) for the most up to date instructions.*
diff --git a/doc/update/2.9-to-3.0.md b/doc/update/2.9-to-3.0.md
index a890aa885d5..6a3c2387683 100644
--- a/doc/update/2.9-to-3.0.md
+++ b/doc/update/2.9-to-3.0.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 2.9 to 3.0
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/2.9-to-3.0.md) for the most up to date instructions.*
diff --git a/doc/update/3.0-to-3.1.md b/doc/update/3.0-to-3.1.md
index e32508745a2..1f25b8265c9 100644
--- a/doc/update/3.0-to-3.1.md
+++ b/doc/update/3.0-to-3.1.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 3.0 to 3.1
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/3.0-to-3.1.md) for the most up to date instructions.*
diff --git a/doc/update/3.1-to-4.0.md b/doc/update/3.1-to-4.0.md
index b370464390e..1a53ddeb4bd 100644
--- a/doc/update/3.1-to-4.0.md
+++ b/doc/update/3.1-to-4.0.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 3.1 to 4.0
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/3.1-to-4.0.md) for the most up to date instructions.*
diff --git a/doc/update/4.0-to-4.1.md b/doc/update/4.0-to-4.1.md
index 7124424bb60..40a133e796e 100644
--- a/doc/update/4.0-to-4.1.md
+++ b/doc/update/4.0-to-4.1.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 4.0 to 4.1
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/4.0-to-4.1.md) for the most up to date instructions.*
diff --git a/doc/update/4.1-to-4.2.md b/doc/update/4.1-to-4.2.md
index 8ed5b333a2e..1fd6c58bda7 100644
--- a/doc/update/4.1-to-4.2.md
+++ b/doc/update/4.1-to-4.2.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 4.1 to 4.2
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/4.1-to-4.2.md) for the most up to date instructions.*
diff --git a/doc/update/4.2-to-5.0.md b/doc/update/4.2-to-5.0.md
index 1ec39218ba8..311664b2bc1 100644
--- a/doc/update/4.2-to-5.0.md
+++ b/doc/update/4.2-to-5.0.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 4.2 to 5.0
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/4.2-to-5.0.md) for the most up to date instructions.*
diff --git a/doc/update/5.0-to-5.1.md b/doc/update/5.0-to-5.1.md
index 9c9950fb2c6..7067ea4c40c 100644
--- a/doc/update/5.0-to-5.1.md
+++ b/doc/update/5.0-to-5.1.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 5.0 to 5.1
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.0-to-5.1.md) for the most up to date instructions.*
diff --git a/doc/update/5.1-to-5.2.md b/doc/update/5.1-to-5.2.md
index 2aab47d2d7c..4faf5fa549e 100644
--- a/doc/update/5.1-to-5.2.md
+++ b/doc/update/5.1-to-5.2.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 5.1 to 5.2
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.1-to-5.2.md) for the most up to date instructions.*
diff --git a/doc/update/5.1-to-5.4.md b/doc/update/5.1-to-5.4.md
index e80f1b89c63..212343bac3f 100644
--- a/doc/update/5.1-to-5.4.md
+++ b/doc/update/5.1-to-5.4.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 5.1 to 5.4
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.1-to-5.4.md) for the most up to date instructions.*
diff --git a/doc/update/5.1-to-6.0.md b/doc/update/5.1-to-6.0.md
index 1ee175383da..865d38e0ca4 100644
--- a/doc/update/5.1-to-6.0.md
+++ b/doc/update/5.1-to-6.0.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 5.1 to 6.0
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.1-to-6.0.md) for the most up to date instructions.*
diff --git a/doc/update/5.2-to-5.3.md b/doc/update/5.2-to-5.3.md
index 2ae50510f63..ed4f3ebdd53 100644
--- a/doc/update/5.2-to-5.3.md
+++ b/doc/update/5.2-to-5.3.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 5.2 to 5.3
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.2-to-5.3.md) for the most up to date instructions.*
diff --git a/doc/update/5.3-to-5.4.md b/doc/update/5.3-to-5.4.md
index 842e3bb6791..7277250eb32 100644
--- a/doc/update/5.3-to-5.4.md
+++ b/doc/update/5.3-to-5.4.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 5.3 to 5.4
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.3-to-5.4.md) for the most up to date instructions.*
diff --git a/doc/update/5.4-to-6.0.md b/doc/update/5.4-to-6.0.md
index 44715984f0c..dacdf05cc9c 100644
--- a/doc/update/5.4-to-6.0.md
+++ b/doc/update/5.4-to-6.0.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 5.4 to 6.0
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.4-to-6.0.md) for the most up to date instructions.*
diff --git a/doc/update/6.0-to-6.1.md b/doc/update/6.0-to-6.1.md
index 0c672abeb05..a3c52a1cfb4 100644
--- a/doc/update/6.0-to-6.1.md
+++ b/doc/update/6.0-to-6.1.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 6.0 to 6.1
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.0-to-6.1.md) for the most up to date instructions.*
diff --git a/doc/update/6.1-to-6.2.md b/doc/update/6.1-to-6.2.md
index d3760cf0619..36a395bf01e 100644
--- a/doc/update/6.1-to-6.2.md
+++ b/doc/update/6.1-to-6.2.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 6.1 to 6.2
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.1-to-6.2.md) for the most up to date instructions.*
diff --git a/doc/update/6.2-to-6.3.md b/doc/update/6.2-to-6.3.md
index 91105de2e29..02e87a08b8f 100644
--- a/doc/update/6.2-to-6.3.md
+++ b/doc/update/6.2-to-6.3.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 6.2 to 6.3
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.2-to-6.3.md) for the most up to date instructions.*
diff --git a/doc/update/6.3-to-6.4.md b/doc/update/6.3-to-6.4.md
index 20b58ed8b25..285ed06bdad 100644
--- a/doc/update/6.3-to-6.4.md
+++ b/doc/update/6.3-to-6.4.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 6.3 to 6.4
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.3-to-6.4.md) for the most up to date instructions.*
diff --git a/doc/update/6.4-to-6.5.md b/doc/update/6.4-to-6.5.md
index 5ee0f040b5d..e07c98a5ad4 100644
--- a/doc/update/6.4-to-6.5.md
+++ b/doc/update/6.4-to-6.5.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 6.4 to 6.5
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.4-to-6.5.md) for the most up to date instructions.*
diff --git a/doc/update/6.5-to-6.6.md b/doc/update/6.5-to-6.6.md
index fa3712f83ad..3f79b19644e 100644
--- a/doc/update/6.5-to-6.6.md
+++ b/doc/update/6.5-to-6.6.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 6.5 to 6.6
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.5-to-6.6.md) for the most up to date instructions.*
diff --git a/doc/update/6.6-to-6.7.md b/doc/update/6.6-to-6.7.md
index 9c85ed091c5..a0542d20d49 100644
--- a/doc/update/6.6-to-6.7.md
+++ b/doc/update/6.6-to-6.7.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 6.6 to 6.7
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.6-to-6.7.md) for the most up to date instructions.*
diff --git a/doc/update/6.7-to-6.8.md b/doc/update/6.7-to-6.8.md
index 687c1265d9b..acf004577f1 100644
--- a/doc/update/6.7-to-6.8.md
+++ b/doc/update/6.7-to-6.8.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 6.7 to 6.8
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.7-to-6.8.md) for the most up to date instructions.*
diff --git a/doc/update/6.8-to-6.9.md b/doc/update/6.8-to-6.9.md
index 0205b0c896a..3d7b1e5346b 100644
--- a/doc/update/6.8-to-6.9.md
+++ b/doc/update/6.8-to-6.9.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 6.8 to 6.9
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.8-to-6.9.md) for the most up to date instructions.*
diff --git a/doc/update/6.9-to-7.0.md b/doc/update/6.9-to-7.0.md
index 4b6e3989893..27063948028 100644
--- a/doc/update/6.9-to-7.0.md
+++ b/doc/update/6.9-to-7.0.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 6.9 to 7.0
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.9-to-7.0.md) for the most up to date instructions.*
diff --git a/doc/update/6.x-or-7.x-to-7.14.md b/doc/update/6.x-or-7.x-to-7.14.md
index 1e39fe47ef9..41d0e78b7d8 100644
--- a/doc/update/6.x-or-7.x-to-7.14.md
+++ b/doc/update/6.x-or-7.x-to-7.14.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 6.x or 7.x to 7.14
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.x-or-7.x-to-7.14.md) for the most up to date instructions.*
diff --git a/doc/update/7.0-to-7.1.md b/doc/update/7.0-to-7.1.md
index 2e9457aa142..308e8aeb985 100644
--- a/doc/update/7.0-to-7.1.md
+++ b/doc/update/7.0-to-7.1.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.0 to 7.1
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/7.0-to-7.1.md) for the most up to date instructions.*
diff --git a/doc/update/7.1-to-7.2.md b/doc/update/7.1-to-7.2.md
index e5045b5570f..07f92ac3af6 100644
--- a/doc/update/7.1-to-7.2.md
+++ b/doc/update/7.1-to-7.2.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.1 to 7.2
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/7.1-to-7.2.md) for the most up to date instructions.*
diff --git a/doc/update/7.10-to-7.11.md b/doc/update/7.10-to-7.11.md
index 89213ba7178..39eeefc0e32 100644
--- a/doc/update/7.10-to-7.11.md
+++ b/doc/update/7.10-to-7.11.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.10 to 7.11
### 0. Stop server
diff --git a/doc/update/7.11-to-7.12.md b/doc/update/7.11-to-7.12.md
index 3865186918c..530066e5fdb 100644
--- a/doc/update/7.11-to-7.12.md
+++ b/doc/update/7.11-to-7.12.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.11 to 7.12
### 0. Double-check your Git version
diff --git a/doc/update/7.12-to-7.13.md b/doc/update/7.12-to-7.13.md
index 4c8d8f1f741..8f413a2079a 100644
--- a/doc/update/7.12-to-7.13.md
+++ b/doc/update/7.12-to-7.13.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.12 to 7.13
### 0. Double-check your Git version
diff --git a/doc/update/7.13-to-7.14.md b/doc/update/7.13-to-7.14.md
index 934898da5a1..a8980662855 100644
--- a/doc/update/7.13-to-7.14.md
+++ b/doc/update/7.13-to-7.14.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.13 to 7.14
### 0. Double-check your Git version
diff --git a/doc/update/7.14-to-8.0.md b/doc/update/7.14-to-8.0.md
index 25fa6d93f06..513afccff50 100644
--- a/doc/update/7.14-to-8.0.md
+++ b/doc/update/7.14-to-8.0.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.14 to 8.0
### 0. Double-check your Git version
diff --git a/doc/update/7.2-to-7.3.md b/doc/update/7.2-to-7.3.md
index d3391ddd225..a16f9de54e4 100644
--- a/doc/update/7.2-to-7.3.md
+++ b/doc/update/7.2-to-7.3.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.2 to 7.3
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/7.2-to-7.3.md) for the most up to date instructions.*
diff --git a/doc/update/7.3-to-7.4.md b/doc/update/7.3-to-7.4.md
index 6d632dc3c8e..734c655f1d1 100644
--- a/doc/update/7.3-to-7.4.md
+++ b/doc/update/7.3-to-7.4.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.3 to 7.4
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/7.3-to-7.4.md) for the most up to date instructions.*
diff --git a/doc/update/7.4-to-7.5.md b/doc/update/7.4-to-7.5.md
index ec50706d421..7a3a49ff948 100644
--- a/doc/update/7.4-to-7.5.md
+++ b/doc/update/7.4-to-7.5.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.4 to 7.5
### 0. Stop server
diff --git a/doc/update/7.5-to-7.6.md b/doc/update/7.5-to-7.6.md
index 331f5de080e..f0dfb177b79 100644
--- a/doc/update/7.5-to-7.6.md
+++ b/doc/update/7.5-to-7.6.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.5 to 7.6
### 0. Stop server
diff --git a/doc/update/7.6-to-7.7.md b/doc/update/7.6-to-7.7.md
index 918b10fbd95..85de6b0c546 100644
--- a/doc/update/7.6-to-7.7.md
+++ b/doc/update/7.6-to-7.7.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.6 to 7.7
### 0. Stop server
diff --git a/doc/update/7.7-to-7.8.md b/doc/update/7.7-to-7.8.md
index 84e0464a824..7cee5f79a13 100644
--- a/doc/update/7.7-to-7.8.md
+++ b/doc/update/7.7-to-7.8.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.7 to 7.8
### 0. Stop server
diff --git a/doc/update/7.8-to-7.9.md b/doc/update/7.8-to-7.9.md
index b0dc2ba1dbb..5a8b689dbc1 100644
--- a/doc/update/7.8-to-7.9.md
+++ b/doc/update/7.8-to-7.9.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.8 to 7.9
### 0. Stop server
diff --git a/doc/update/7.9-to-7.10.md b/doc/update/7.9-to-7.10.md
index 8f7f84b41ba..99df51dbb99 100644
--- a/doc/update/7.9-to-7.10.md
+++ b/doc/update/7.9-to-7.10.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.9 to 7.10
### 0. Stop server
diff --git a/doc/update/8.0-to-8.1.md b/doc/update/8.0-to-8.1.md
index 6ee0c0656ee..f612606af68 100644
--- a/doc/update/8.0-to-8.1.md
+++ b/doc/update/8.0-to-8.1.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.0 to 8.1
**NOTE:** GitLab 8.0 introduced several significant changes related to
diff --git a/doc/update/8.1-to-8.2.md b/doc/update/8.1-to-8.2.md
index 4c9ff5c5c0a..2d0b19abd74 100644
--- a/doc/update/8.1-to-8.2.md
+++ b/doc/update/8.1-to-8.2.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.1 to 8.2
**NOTE:** GitLab 8.0 introduced several significant changes related to
diff --git a/doc/update/8.10-to-8.11.md b/doc/update/8.10-to-8.11.md
index e538983e603..df3e34f5cc6 100644
--- a/doc/update/8.10-to-8.11.md
+++ b/doc/update/8.10-to-8.11.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.10 to 8.11
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.11-to-8.12.md b/doc/update/8.11-to-8.12.md
index 604166beb56..9d6a1f42375 100644
--- a/doc/update/8.11-to-8.12.md
+++ b/doc/update/8.11-to-8.12.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.11 to 8.12
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.12-to-8.13.md b/doc/update/8.12-to-8.13.md
index d83965131f5..6225dee9802 100644
--- a/doc/update/8.12-to-8.13.md
+++ b/doc/update/8.12-to-8.13.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.12 to 8.13
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.13-to-8.14.md b/doc/update/8.13-to-8.14.md
index aaadcec8ac0..d2508e3f980 100644
--- a/doc/update/8.13-to-8.14.md
+++ b/doc/update/8.13-to-8.14.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.13 to 8.14
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.14-to-8.15.md b/doc/update/8.14-to-8.15.md
index a68fe3bb605..daf8d0f2ca6 100644
--- a/doc/update/8.14-to-8.15.md
+++ b/doc/update/8.14-to-8.15.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.14 to 8.15
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.15-to-8.16.md b/doc/update/8.15-to-8.16.md
index 9f8f0f714d4..3668142edd2 100644
--- a/doc/update/8.15-to-8.16.md
+++ b/doc/update/8.15-to-8.16.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.15 to 8.16
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.16-to-8.17.md b/doc/update/8.16-to-8.17.md
index 74ffe0bc846..ee2e31c2aec 100644
--- a/doc/update/8.16-to-8.17.md
+++ b/doc/update/8.16-to-8.17.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.16 to 8.17
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.17-to-9.0.md b/doc/update/8.17-to-9.0.md
index baab217b6b7..2e0c26a9092 100644
--- a/doc/update/8.17-to-9.0.md
+++ b/doc/update/8.17-to-9.0.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.17 to 9.0
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.2-to-8.3.md b/doc/update/8.2-to-8.3.md
index 4b3c5bf6d64..3a0d647cbfe 100644
--- a/doc/update/8.2-to-8.3.md
+++ b/doc/update/8.2-to-8.3.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.2 to 8.3
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.3-to-8.4.md b/doc/update/8.3-to-8.4.md
index 8b89455ca87..f5162dd5ff5 100644
--- a/doc/update/8.3-to-8.4.md
+++ b/doc/update/8.3-to-8.4.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.3 to 8.4
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.4-to-8.5.md b/doc/update/8.4-to-8.5.md
index 0eedfaee2db..9e2f98add8d 100644
--- a/doc/update/8.4-to-8.5.md
+++ b/doc/update/8.4-to-8.5.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.4 to 8.5
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.5-to-8.6.md b/doc/update/8.5-to-8.6.md
index 851056161bb..55d8178c407 100644
--- a/doc/update/8.5-to-8.6.md
+++ b/doc/update/8.5-to-8.6.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.5 to 8.6
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.6-to-8.7.md b/doc/update/8.6-to-8.7.md
index 34c727260aa..49db6f2967c 100644
--- a/doc/update/8.6-to-8.7.md
+++ b/doc/update/8.6-to-8.7.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.6 to 8.7
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.7-to-8.8.md b/doc/update/8.7-to-8.8.md
index 6feeb1919de..ee7ec6f7614 100644
--- a/doc/update/8.7-to-8.8.md
+++ b/doc/update/8.7-to-8.8.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.7 to 8.8
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.8-to-8.9.md b/doc/update/8.8-to-8.9.md
index 61cdf8854d4..7508443c30a 100644
--- a/doc/update/8.8-to-8.9.md
+++ b/doc/update/8.8-to-8.9.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.8 to 8.9
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.9-to-8.10.md b/doc/update/8.9-to-8.10.md
index 42132f690d8..915e7db819a 100644
--- a/doc/update/8.9-to-8.10.md
+++ b/doc/update/8.9-to-8.10.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.9 to 8.10
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/9.0-to-9.1.md b/doc/update/9.0-to-9.1.md
index 6f1870a1366..f60bd92e236 100644
--- a/doc/update/9.0-to-9.1.md
+++ b/doc/update/9.0-to-9.1.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 9.0 to 9.1
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/9.1-to-9.2.md b/doc/update/9.1-to-9.2.md
index ce72b313031..2fff6544797 100644
--- a/doc/update/9.1-to-9.2.md
+++ b/doc/update/9.1-to-9.2.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 9.1 to 9.2
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/9.2-to-9.3.md b/doc/update/9.2-to-9.3.md
index 779ced0cf75..1b36cf53f4c 100644
--- a/doc/update/9.2-to-9.3.md
+++ b/doc/update/9.2-to-9.3.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 9.2 to 9.3
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/9.3-to-9.4.md b/doc/update/9.3-to-9.4.md
index 78d8a6c7de5..210b6eb607d 100644
--- a/doc/update/9.3-to-9.4.md
+++ b/doc/update/9.3-to-9.4.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 9.3 to 9.4
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/9.4-to-9.5.md b/doc/update/9.4-to-9.5.md
index a7255142ef5..1bfc1167c36 100644
--- a/doc/update/9.4-to-9.5.md
+++ b/doc/update/9.4-to-9.5.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 9.4 to 9.5
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/9.5-to-10.0.md b/doc/update/9.5-to-10.0.md
index 8581e6511f2..8d1cf0f737b 100644
--- a/doc/update/9.5-to-10.0.md
+++ b/doc/update/9.5-to-10.0.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 9.5 to 10.0
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md
index b2679d1ff22..e1857ce99c6 100644
--- a/doc/update/patch_versions.md
+++ b/doc/update/patch_versions.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Universal update guide for patch versions
## Select Version to Install
diff --git a/doc/update/upgrader.md b/doc/update/upgrader.md
index eb7f14a96d5..746d6bf93e7 100644
--- a/doc/update/upgrader.md
+++ b/doc/update/upgrader.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# GitLab Upgrader (deprecated)
*DEPRECATED* We recommend to [switch to the Omnibus package and repository server](https://about.gitlab.com/update/) instead of using this script.
diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md
index 5ebb88bf324..5fcc0501dc1 100644
--- a/doc/user/profile/index.md
+++ b/doc/user/profile/index.md
@@ -52,7 +52,7 @@ You can edit your account settings by navigating from the up-right corner menu b
From there, you can:
- Update your personal information
-- Manage [private tokens](../../api/README.md#private-tokens), email tokens, [2FA](account/two_factor_authentication.md)
+- Manage [2FA](account/two_factor_authentication.md)
- Change your username and [delete your account](account/delete_account.md)
- Manage applications that can
[use GitLab as an OAuth provider](../../integration/oauth_provider.md#introduction-to-oauth)
diff --git a/doc/user/profile/personal_access_tokens.md b/doc/user/profile/personal_access_tokens.md
index f28c034e74c..9b4fdd65e2f 100644
--- a/doc/user/profile/personal_access_tokens.md
+++ b/doc/user/profile/personal_access_tokens.md
@@ -2,17 +2,15 @@
> [Introduced][ce-3749] in GitLab 8.8.
-Personal access tokens are useful if you need access to the [GitLab API][api].
-Instead of using your private token which grants full access to your account,
-personal access tokens could be a better fit because of their
-[granular permissions](#limiting-scopes-of-a-personal-access-token).
+Personal access tokens are the preferred way for third party applications and scripts to
+authenticate with the [GitLab API][api], if using [OAuth2](../../api/oauth2.md) is not practical.
You can also use them to authenticate against Git over HTTP. They are the only
accepted method of authentication when you have
[Two-Factor Authentication (2FA)][2fa] enabled.
Once you have your token, [pass it to the API][usage] using either the
-`private_token` parameter or the `PRIVATE-TOKEN` header.
+`private_token` parameter or the `Private-Token` header.
The expiration of personal access tokens happens on the date you define,
at midnight UTC.
@@ -49,12 +47,14 @@ the following table.
|`read_user` | Allows access to the read-only endpoints under `/users`. Essentially, any of the `GET` requests in the [Users API][users] are allowed ([introduced][ce-5951] in GitLab 8.15). |
| `api` | Grants complete access to the API (read/write) ([introduced][ce-5951] in GitLab 8.15). Required for accessing Git repositories over HTTP when 2FA is enabled. |
| `read_registry` | Allows to read [container registry] images if a project is private and authorization is required ([introduced][ce-11845] in GitLab 9.3). |
+| `sudo` | Allows performing API actions as any user in the system (if the authenticated user is an admin) ([introduced][ce-14838] in GitLab 10.2). |
[2fa]: ../account/two_factor_authentication.md
[api]: ../../api/README.md
[ce-3749]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3749
[ce-5951]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951
[ce-11845]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11845
+[ce-14838]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14838
[container registry]: ../project/container_registry.md
[users]: ../../api/users.md
-[usage]: ../../api/README.md#basic-usage
+[usage]: ../../api/README.md#personal-access-tokens
diff --git a/doc/user/project/integrations/img/webhook_logs.png b/doc/user/project/integrations/img/webhook_logs.png
index 917068d9398..803678db6b6 100644
--- a/doc/user/project/integrations/img/webhook_logs.png
+++ b/doc/user/project/integrations/img/webhook_logs.png
Binary files differ
diff --git a/doc/user/project/integrations/project_services.md b/doc/user/project/integrations/project_services.md
index 51989ccaaea..a0405161495 100644
--- a/doc/user/project/integrations/project_services.md
+++ b/doc/user/project/integrations/project_services.md
@@ -43,6 +43,7 @@ Click on the service links to see further configuration instructions and details
| [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands |
| [Mattermost Notifications](mattermost.md) | Receive event notifications in Mattermost |
| [Microsoft teams](microsoft_teams.md) | Receive notifications for actions that happen on GitLab into a room on Microsoft Teams using Office 365 Connectors |
+| Packagist | Update your project on Packagist, the main Composer repository |
| Pipelines emails | Email the pipeline status to a list of recipients |
| [Slack Notifications](slack.md) | Send GitLab events (e.g. issue created) to Slack as notifications |
| [Slack slash commands](slack_slash_commands.md) | Use slash commands in Slack to control GitLab |
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index 7abc600a680..5896f8f72a0 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -76,6 +76,7 @@ X-Gitlab-Event: Push Hook
"user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
"project_id": 15,
"project":{
+ "id": 15,
"name":"Diaspora",
"description":"",
"web_url":"http://example.com/mike/diaspora",
@@ -156,6 +157,7 @@ X-Gitlab-Event: Tag Push Hook
"user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
"project_id": 1,
"project":{
+ "id": 1,
"name":"Example",
"description":"",
"web_url":"http://example.com/jsmith/example",
@@ -206,6 +208,7 @@ X-Gitlab-Event: Issue Hook
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
},
"project": {
+ "id": 1,
"name":"Gitlab Test",
"description":"Aut reprehenderit ut est.",
"web_url":"http://example.com/gitlabhq/gitlab-test",
@@ -335,6 +338,7 @@ X-Gitlab-Event: Note Hook
},
"project_id": 5,
"project":{
+ "id": 5,
"name":"Gitlab Test",
"description":"Aut reprehenderit ut est.",
"web_url":"http://example.com/gitlabhq/gitlab-test",
@@ -414,6 +418,7 @@ X-Gitlab-Event: Note Hook
},
"project_id": 5,
"project":{
+ "id": 5,
"name":"Gitlab Test",
"description":"Aut reprehenderit ut est.",
"web_url":"http://example.com/gitlab-org/gitlab-test",
@@ -540,6 +545,7 @@ X-Gitlab-Event: Note Hook
},
"project_id": 5,
"project":{
+ "id": 5,
"name":"Gitlab Test",
"description":"Aut reprehenderit ut est.",
"web_url":"http://example.com/gitlab-org/gitlab-test",
@@ -618,6 +624,7 @@ X-Gitlab-Event: Note Hook
},
"project_id": 5,
"project":{
+ "id": 5,
"name":"Gitlab Test",
"description":"Aut reprehenderit ut est.",
"web_url":"http://example.com/gitlab-org/gitlab-test",
@@ -692,6 +699,7 @@ X-Gitlab-Event: Merge Request Hook
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
},
"project": {
+ "id": 1,
"name":"Gitlab Test",
"description":"Aut reprehenderit ut est.",
"web_url":"http://example.com/gitlabhq/gitlab-test",
@@ -848,6 +856,7 @@ X-Gitlab-Event: Wiki Page Hook
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon"
},
"project": {
+ "id": 1,
"name": "awesome-project",
"description": "This is awesome",
"web_url": "http://example.com/root/awesome-project",
@@ -919,6 +928,7 @@ X-Gitlab-Event: Pipeline Hook
"avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
},
"project":{
+ "id": 1,
"name": "Gitlab Test",
"description": "Atque in sunt eos similique dolores voluptatem.",
"web_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test",
@@ -1130,6 +1140,18 @@ From this page, you can repeat delivery with the same data by clicking `Resend R
>**Note:** If URL or secret token of the webhook were updated, data will be delivered to the new address.
+### Receiving duplicate or multiple web hook requests triggered by one event
+
+When GitLab sends a webhook it expects a response in 10 seconds (set default value). If it does not receive one, it'll retry the webhook.
+If the endpoint doesn't send its HTTP response within those 10 seconds, GitLab may decide the hook failed and retry it.
+
+If you are receiving multiple requests, you can try increasing the default value to wait for the HTTP response after sending the webhook
+by uncommenting or adding the following setting to your `/etc/gitlab/gitlab.rb`:
+
+```
+gitlab_rails['webhook_timeout'] = 10
+```
+
## Example webhook receiver
If you want to see GitLab's webhooks in action for testing purposes you can use
diff --git a/doc/user/project/milestones/index.md b/doc/user/project/milestones/index.md
index 876b98a4dc5..83adbd8cce2 100644
--- a/doc/user/project/milestones/index.md
+++ b/doc/user/project/milestones/index.md
@@ -29,7 +29,8 @@ In addition to that you will be able to filter issues or merge requests by group
## Milestone promotion
-You will be able to promote a project milestone to a group milestone [in the future](https://gitlab.com/gitlab-org/gitlab-ce/issues/35833).
+Project milestones can be promoted to group milestones if its project belongs to a group. When a milestone is promoted all other milestones across the group projects with the same title will be merged into it, which means all milestone's children like issues, merge requests and boards will be moved into the new promoted milestone.
+The promote button can be found in the milestone view or milestones list.
## Special milestone filters
diff --git a/doc/user/project/pipelines/job_artifacts.md b/doc/user/project/pipelines/job_artifacts.md
index 9ef6f9185c9..f9a268fb789 100644
--- a/doc/user/project/pipelines/job_artifacts.md
+++ b/doc/user/project/pipelines/job_artifacts.md
@@ -52,7 +52,8 @@ directly in the job artifacts browser without the need to download them.
>**Note:**
With [GitLab 10.1][ce-14399], HTML files in a public project can be previewed
-directly in a new tab without the need to download them.
+directly in a new tab without the need to download them when
+[GitLab Pages](../../../administration/pages/index.md) is enabled
After a job finishes, if you visit the job's specific page, there are three
buttons. You can download the artifacts archive or browse its contents, whereas
@@ -69,7 +70,8 @@ browse inside them.
Below you can see how browsing looks like. In this case we have browsed inside
the archive and at this point there is one directory, a couple files, and
-one HTML file that you can view directly online (opens in a new tab).
+one HTML file that you can view directly online when
+[GitLab Pages](../../../administration/pages/index.md) is enabled (opens in a new tab).
![Job artifacts browser](img/job_artifacts_browser.png)
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 6b2aba47f54..272f7807ac0 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Workflow
- [Automatic issue closing](../user/project/issues/automatic_issue_closing.md)
diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md
index 9d466ae1971..23b67310d25 100644
--- a/doc/workflow/gitlab_flow.md
+++ b/doc/workflow/gitlab_flow.md
@@ -1,6 +1,6 @@
![GitLab Flow](gitlab_flow.png)
-## Introduction
+# Introduction to GitLab Flow
Version management with git makes branching and merging much easier than older versioning systems such as SVN.
This allows a wide variety of branching strategies and workflows.
diff --git a/doc/workflow/shortcuts.md b/doc/workflow/shortcuts.md
index 87416008e98..2e1bd6bfe5c 100644
--- a/doc/workflow/shortcuts.md
+++ b/doc/workflow/shortcuts.md
@@ -9,7 +9,7 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?'
| <kbd>n</kbd> | Main navigation |
| <kbd>s</kbd> | Focus search |
| <kbd>f</kbd> | Focus filter |
-| <kbd>p b</kbd> | Show/hide the Performance Bar |
+| <kbd>p</kbd> + <kbd>b</kbd> | Show/hide the Performance Bar |
| <kbd>?</kbd> | Show/hide this dialog |
| <kbd>⌘</kbd> + <kbd>shift</kbd> + <kbd>p</kbd> | Toggle markdown preview |
| <kbd>↑</kbd> | Edit last comment (when focused on an empty textarea) |
diff --git a/features/steps/profile/notifications.rb b/features/steps/profile/notifications.rb
index 7e339443b75..f8eb0f01de8 100644
--- a/features/steps/profile/notifications.rb
+++ b/features/steps/profile/notifications.rb
@@ -11,7 +11,7 @@ class Spinach::Features::ProfileNotifications < Spinach::FeatureSteps
end
step 'I select Mention setting from dropdown' do
- first(:link, "On mention").trigger('click')
+ first(:link, "On mention").click
end
step 'I should see Notification saved message' do
diff --git a/features/steps/project/commits/branches.rb b/features/steps/project/commits/branches.rb
index ccaf3237815..c3ae33d2aa9 100644
--- a/features/steps/project/commits/branches.rb
+++ b/features/steps/project/commits/branches.rb
@@ -40,6 +40,7 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps
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
@@ -70,17 +71,16 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps
step "I click branch 'improve/awesome' delete link" do
page.within '.js-branch-improve\/awesome' do
- find('.btn-remove').click
- sleep 0.05
+ accept_alert { find('.btn-remove').click }
end
end
step "I should not see branch 'improve/awesome'" do
- expect(page.all(visible: true)).not_to have_content 'improve/awesome'
+ expect(page).to have_css('.js-branch-improve\\/awesome', visible: :hidden)
end
def select_branch(branch_name)
- click_button 'master'
+ find('.git-revision-dropdown-toggle').click
page.within '#new-branch-form .dropdown-menu' do
click_link branch_name
diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb
index 2c3ef2efd52..3843374678c 100644
--- a/features/steps/project/issues/issues.rb
+++ b/features/steps/project/issues/issues.rb
@@ -20,11 +20,13 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end
step 'I should see that I am subscribed' do
- expect(find('.issuable-subscribe-button span')).to have_content 'Unsubscribe'
+ wait_for_requests
+ expect(find('.js-issuable-subscribe-button span')).to have_content 'Unsubscribe'
end
step 'I should see that I am unsubscribed' do
- expect(find('.issuable-subscribe-button span')).to have_content 'Subscribe'
+ wait_for_requests
+ expect(find('.js-issuable-subscribe-button span')).to have_content 'Subscribe'
end
step 'I click link "Closed"' do
diff --git a/features/steps/project/issues/labels.rb b/features/steps/project/issues/labels.rb
index dac18c537ac..196e0fff63a 100644
--- a/features/steps/project/issues/labels.rb
+++ b/features/steps/project/issues/labels.rb
@@ -16,7 +16,7 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps
step 'I delete all labels' do
page.within '.labels' do
page.all('.remove-row').each do
- first('.remove-row').click
+ accept_confirm { first('.remove-row').click }
end
end
end
diff --git a/features/steps/project/issues/milestones.rb b/features/steps/project/issues/milestones.rb
index 16a2d4a6f93..33a24e8913a 100644
--- a/features/steps/project/issues/milestones.rb
+++ b/features/steps/project/issues/milestones.rb
@@ -3,6 +3,7 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps
include SharedProject
include SharedPaths
include SharedMarkdown
+ include CapybaraHelpers
step 'I should see milestone "v2.2"' do
milestone = @project.milestones.find_by(title: "v2.2")
@@ -65,7 +66,7 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps
end
step 'I click link to remove milestone' do
- click_link 'Delete'
+ confirm_modal_if_present { click_link 'Delete' }
end
step 'I should see no milestones' do
diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb
index c872bd6f861..aa32528a7ca 100644
--- a/features/steps/shared/diff_note.rb
+++ b/features/steps/shared/diff_note.rb
@@ -215,7 +215,7 @@ module SharedDiffNote
end
step 'I click side-by-side diff button' do
- find('#parallel-diff-btn').trigger('click')
+ find('#parallel-diff-btn').click
end
step 'I see side-by-side diff button' do
@@ -227,12 +227,11 @@ module SharedDiffNote
end
def click_diff_line(code)
- find(".line_holder[id='#{code}'] td:nth-of-type(1)").trigger 'mouseover'
- find(".line_holder[id='#{code}'] button").trigger 'click'
+ 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]').trigger 'mouseover'
- find(".line_holder.parallel button[data-line-code='#{code}']").trigger 'click'
+ 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/note.rb b/features/steps/shared/note.rb
index 0cd7b506a95..95f0cd2156e 100644
--- a/features/steps/shared/note.rb
+++ b/features/steps/shared/note.rb
@@ -14,7 +14,7 @@ module SharedNote
find('.more-actions').click
find('.more-actions .dropdown-menu li', match: :first)
- find(".js-note-delete").click
+ accept_confirm { find(".js-note-delete").click }
end
end
diff --git a/features/support/capybara.rb b/features/support/capybara.rb
index f4691647d4b..3c4db8b9601 100644
--- a/features/support/capybara.rb
+++ b/features/support/capybara.rb
@@ -1,22 +1,21 @@
-require 'capybara/poltergeist'
require 'capybara-screenshot/spinach'
# Give CI some extra time
timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 60 : 30
-Capybara.javascript_driver = :poltergeist
-Capybara.register_driver :poltergeist do |app|
- Capybara::Poltergeist::Driver.new(
- app,
- js_errors: true,
- timeout: timeout,
- window_size: [1366, 768],
- url_whitelist: %w[localhost 127.0.0.1],
- url_blacklist: %w[.mp4 .png .gif .avi .bmp .jpg .jpeg],
- phantomjs_options: [
- '--load-images=yes'
- ]
+Capybara.javascript_driver = :chrome
+Capybara.register_driver :chrome do |app|
+ extra_args = []
+ extra_args << 'headless' unless ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i
+
+ capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
+ chromeOptions: {
+ 'args' => %w[no-sandbox disable-gpu --window-size=1240,1400] + extra_args
+ }
)
+
+ Capybara::Selenium::Driver
+ .new(app, browser: :chrome, desired_capabilities: capabilities)
end
Capybara.default_max_wait_time = timeout
@@ -24,6 +23,10 @@ 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
diff --git a/features/support/capybara_helpers.rb b/features/support/capybara_helpers.rb
new file mode 100644
index 00000000000..647f8d087c3
--- /dev/null
+++ b/features/support/capybara_helpers.rb
@@ -0,0 +1,10 @@
+module CapybaraHelpers
+ def confirm_modal_if_present
+ if Capybara.current_driver == Capybara.javascript_driver
+ accept_confirm { yield }
+ return
+ end
+
+ yield
+ end
+end
diff --git a/lib/additional_email_headers_interceptor.rb b/lib/additional_email_headers_interceptor.rb
index 2358fa6bbfd..3cb1694b9f1 100644
--- a/lib/additional_email_headers_interceptor.rb
+++ b/lib/additional_email_headers_interceptor.rb
@@ -1,8 +1,6 @@
class AdditionalEmailHeadersInterceptor
def self.delivering_email(message)
- message.headers(
- 'Auto-Submitted' => 'auto-generated',
- 'X-Auto-Response-Suppress' => 'All'
- )
+ message.header['Auto-Submitted'] ||= 'auto-generated'
+ message.header['X-Auto-Response-Suppress'] ||= 'All'
end
end
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 7db18e25a5f..c37e596eb9d 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -142,7 +142,6 @@ module API
mount ::API::Runner
mount ::API::Runners
mount ::API::Services
- mount ::API::Session
mount ::API::Settings
mount ::API::SidekiqMetrics
mount ::API::Snippets
diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb
index 87b9db66efd..b9c7d443f6c 100644
--- a/lib/api/api_guard.rb
+++ b/lib/api/api_guard.rb
@@ -42,72 +42,42 @@ module API
# Helper Methods for Grape Endpoint
module HelperMethods
- def find_current_user
- user =
- find_user_from_private_token ||
- find_user_from_oauth_token ||
- find_user_from_warden
+ def find_current_user!
+ user = find_user_from_access_token || find_user_from_warden
+ return unless user
- return nil unless user
-
- raise UnauthorizedError unless Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api)
+ forbidden!('User is blocked') unless Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api)
user
end
- def private_token
- params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]
- end
-
- private
-
- def find_user_from_private_token
- token_string = private_token.to_s
- return nil unless token_string.present?
+ def access_token
+ return @access_token if defined?(@access_token)
- user =
- find_user_by_authentication_token(token_string) ||
- find_user_by_personal_access_token(token_string)
-
- raise UnauthorizedError unless user
-
- user
+ @access_token = find_oauth_access_token || find_personal_access_token
end
- # Invokes the doorkeeper guard.
- #
- # If token is presented and valid, then it sets @current_user.
- #
- # If the token does not have sufficient scopes to cover the requred scopes,
- # then it raises InsufficientScopeError.
- #
- # If the token is expired, then it raises ExpiredError.
- #
- # If the token is revoked, then it raises RevokedError.
- #
- # If the token is not found (nil), then it returns nil
- #
- # Arguments:
- #
- # scopes: (optional) scopes required for this guard.
- # Defaults to empty array.
- #
- def find_user_from_oauth_token
- access_token = find_oauth_access_token
+ def validate_access_token!(scopes: [])
return unless access_token
- find_user_by_access_token(access_token)
+ case AccessTokenValidationService.new(access_token, request: request).validate(scopes: scopes)
+ when AccessTokenValidationService::INSUFFICIENT_SCOPE
+ raise InsufficientScopeError.new(scopes)
+ when AccessTokenValidationService::EXPIRED
+ raise ExpiredError
+ when AccessTokenValidationService::REVOKED
+ raise RevokedError
+ end
end
- def find_user_by_authentication_token(token_string)
- User.find_by_authentication_token(token_string)
- end
+ private
- def find_user_by_personal_access_token(token_string)
- access_token = PersonalAccessToken.find_by_token(token_string)
+ def find_user_from_access_token
return unless access_token
- find_user_by_access_token(access_token)
+ validate_access_token!
+
+ access_token.user || raise(UnauthorizedError)
end
# Check the Rails session for valid authentication details
@@ -125,34 +95,26 @@ module API
end
def find_oauth_access_token
- return @oauth_access_token if defined?(@oauth_access_token)
-
token = Doorkeeper::OAuth::Token.from_request(doorkeeper_request, *Doorkeeper.configuration.access_token_methods)
- return @oauth_access_token = nil unless token
+ return unless token
- @oauth_access_token = OauthAccessToken.by_token(token)
- raise UnauthorizedError unless @oauth_access_token
+ # Expiration, revocation and scopes are verified in `find_user_by_access_token`
+ access_token = OauthAccessToken.by_token(token)
+ raise UnauthorizedError unless access_token
- @oauth_access_token.revoke_previous_refresh_token!
- @oauth_access_token
+ access_token.revoke_previous_refresh_token!
+ access_token
end
- def find_user_by_access_token(access_token)
- scopes = scopes_registered_for_endpoint
+ def find_personal_access_token
+ token = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s
+ return unless token.present?
- case AccessTokenValidationService.new(access_token, request: request).validate(scopes: scopes)
- when AccessTokenValidationService::INSUFFICIENT_SCOPE
- raise InsufficientScopeError.new(scopes)
-
- when AccessTokenValidationService::EXPIRED
- raise ExpiredError
+ # Expiration, revocation and scopes are verified in `find_user_by_access_token`
+ access_token = PersonalAccessToken.find_by(token: token)
+ raise UnauthorizedError unless access_token
- when AccessTokenValidationService::REVOKED
- raise RevokedError
-
- when AccessTokenValidationService::VALID
- access_token.user
- end
+ access_token
end
def doorkeeper_request
@@ -236,7 +198,7 @@ module API
class InsufficientScopeError < StandardError
attr_reader :scopes
def initialize(scopes)
- @scopes = scopes
+ @scopes = scopes.map { |s| s.try(:name) || s }
end
end
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index efe874b2e6b..67cecb6a7ad 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -57,10 +57,6 @@ module API
expose :admin?, as: :is_admin
end
- class UserWithPrivateDetails < UserWithAdmin
- expose :private_token
- end
-
class Email < Grape::Entity
expose :id, :email
end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 7a2ec865860..1c12166e434 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -41,6 +41,8 @@ module API
sudo!
+ validate_access_token!(scopes: scopes_registered_for_endpoint) unless sudo?
+
@current_user
end
@@ -385,7 +387,7 @@ module API
return @initial_current_user if defined?(@initial_current_user)
begin
- @initial_current_user = Gitlab::Auth::UniqueIpsLimiter.limit_user! { find_current_user }
+ @initial_current_user = Gitlab::Auth::UniqueIpsLimiter.limit_user! { find_current_user! }
rescue APIGuard::UnauthorizedError
unauthorized!
end
@@ -393,24 +395,23 @@ module API
def sudo!
return unless sudo_identifier
- return unless initial_current_user
+
+ unauthorized! unless initial_current_user
unless initial_current_user.admin?
forbidden!('Must be admin to use sudo')
end
- # Only private tokens should be used for the SUDO feature
- unless private_token == initial_current_user.private_token
- forbidden!('Private token must be specified in order to use sudo')
+ unless access_token
+ forbidden!('Must be authenticated using an OAuth or Personal Access Token to use sudo')
end
+ validate_access_token!(scopes: [:sudo])
+
sudoed_user = find_user(sudo_identifier)
+ not_found!("User with ID or username '#{sudo_identifier}'") unless sudoed_user
- if sudoed_user
- @current_user = sudoed_user
- else
- not_found!("No user id or username for: #{sudo_identifier}")
- end
+ @current_user = sudoed_user
end
def sudo_identifier
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index be843ec8251..726f09e3669 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -295,7 +295,7 @@ module API
unauthorized! unless merge_request.can_cancel_merge_when_pipeline_succeeds?(current_user)
- ::MergeRequest::MergeWhenPipelineSucceedsService
+ ::MergeRequests::MergeWhenPipelineSucceedsService
.new(merge_request.target_project, current_user)
.cancel(merge_request)
end
diff --git a/lib/api/services.rb b/lib/api/services.rb
index 1e4f7c29633..6454e475036 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -374,6 +374,26 @@ module API
desc: 'The Slack token'
}
],
+ 'packagist' => [
+ {
+ required: true,
+ name: :username,
+ type: String,
+ desc: 'The username'
+ },
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The Packagist API token'
+ },
+ {
+ required: false,
+ name: :server,
+ type: String,
+ desc: 'The server'
+ }
+ ],
'pipelines-email' => [
{
required: true,
@@ -551,6 +571,7 @@ module API
KubernetesService,
MattermostSlashCommandsService,
SlackSlashCommandsService,
+ PackagistService,
PipelinesEmailService,
PivotaltrackerService,
PrometheusService,
diff --git a/lib/api/session.rb b/lib/api/session.rb
deleted file mode 100644
index 016415c3023..00000000000
--- a/lib/api/session.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-module API
- class Session < Grape::API
- desc 'Login to get token' do
- success Entities::UserWithPrivateDetails
- end
- params do
- optional :login, type: String, desc: 'The username'
- optional :email, type: String, desc: 'The email of the user'
- requires :password, type: String, desc: 'The password of the user'
- at_least_one_of :login, :email
- end
- post "/session" do
- user = Gitlab::Auth.find_with_user_password(params[:email] || params[:login], params[:password])
-
- return unauthorized! unless user
- return render_api_error!('401 Unauthorized. You have 2FA enabled. Please use a personal access token to access the API', 401) if user.two_factor_enabled?
- present user, with: Entities::UserWithPrivateDetails
- end
- end
-end
diff --git a/lib/api/users.rb b/lib/api/users.rb
index b6f97a1eac2..d80b364bd09 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -507,9 +507,7 @@ module API
end
get do
entity =
- if sudo?
- Entities::UserWithPrivateDetails
- elsif current_user.admin?
+ if current_user.admin?
Entities::UserWithAdmin
else
Entities::UserPublic
diff --git a/lib/api/v3/services.rb b/lib/api/v3/services.rb
index 2d13d6fabfd..44ed94d2869 100644
--- a/lib/api/v3/services.rb
+++ b/lib/api/v3/services.rb
@@ -395,6 +395,26 @@ module API
desc: 'The Slack token'
}
],
+ 'packagist' => [
+ {
+ required: true,
+ name: :username,
+ type: String,
+ desc: 'The username'
+ },
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The Packagist API token'
+ },
+ {
+ required: false,
+ name: :server,
+ type: String,
+ desc: 'The server'
+ }
+ ],
'pipelines-email' => [
{
required: true,
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index ef4578aabd6..a0f7e4e5ad5 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -95,7 +95,7 @@ module Banzai
end
def call
- return doc if project.nil?
+ return doc unless project || group
ref_pattern = object_class.reference_pattern
link_pattern = object_class.link_reference_pattern
@@ -288,10 +288,14 @@ module Banzai
end
def current_project_path
+ return unless project
+
@current_project_path ||= project.full_path
end
def current_project_namespace_path
+ return unless project
+
@current_project_namespace_path ||= project.namespace.full_path
end
diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb
index a6f8650ed3d..c6ae28adf87 100644
--- a/lib/banzai/filter/reference_filter.rb
+++ b/lib/banzai/filter/reference_filter.rb
@@ -55,6 +55,10 @@ module Banzai
context[:project]
end
+ def group
+ context[:group]
+ end
+
def skip_project_check?
context[:skip_project_check]
end
diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb
index f3356d6c51e..afb6e25963c 100644
--- a/lib/banzai/filter/user_reference_filter.rb
+++ b/lib/banzai/filter/user_reference_filter.rb
@@ -24,7 +24,7 @@ module Banzai
end
def call
- return doc if project.nil? && !skip_project_check?
+ return doc if project.nil? && group.nil? && !skip_project_check?
ref_pattern = User.reference_pattern
ref_pattern_start = /\A#{ref_pattern}\z/
@@ -101,19 +101,12 @@ module Banzai
end
def link_to_all(link_content: nil)
- project = context[:project]
author = context[:author]
- if author && !project.team.member?(author)
+ if author && !team_member?(author)
link_content
else
- url = urls.project_url(project,
- only_path: context[:only_path])
-
- data = data_attribute(project: project.id, author: author.try(:id))
- content = link_content || User.reference_prefix + 'all'
-
- link_tag(url, data, content, 'All Project and Group Members')
+ parent_url(link_content, author)
end
end
@@ -144,6 +137,35 @@ module Banzai
def link_tag(url, data, link_content, title)
%(<a href="#{url}" #{data} class="#{link_class}" title="#{escape_once(title)}">#{link_content}</a>)
end
+
+ def parent
+ context[:project] || context[:group]
+ end
+
+ def parent_group?
+ parent.is_a?(Group)
+ end
+
+ def team_member?(user)
+ if parent_group?
+ parent.member?(user)
+ else
+ parent.team.member?(user)
+ end
+ end
+
+ def parent_url(link_content, author)
+ if parent_group?
+ url = urls.group_url(parent, only_path: context[:only_path])
+ data = data_attribute(group: group.id, author: author.try(:id))
+ else
+ url = urls.project_url(parent, only_path: context[:only_path])
+ data = data_attribute(project: project.id, author: author.try(:id))
+ end
+
+ content = link_content || User.reference_prefix + 'all'
+ link_tag(url, data, content, 'All Project and Group Members')
+ end
end
end
end
diff --git a/lib/github/import.rb b/lib/github/import.rb
index 76612799412..8cabbdec940 100644
--- a/lib/github/import.rb
+++ b/lib/github/import.rb
@@ -60,7 +60,9 @@ module Github
project.repository.set_import_remote_as_mirror('github')
project.repository.add_remote_fetch_config('github', '+refs/pull/*/head:refs/merge-requests/*/head')
fetch_remote(forced: true)
- rescue Gitlab::Git::Repository::NoRepository, Gitlab::Shell::Error => e
+ rescue Gitlab::Git::Repository::NoRepository,
+ Gitlab::Git::RepositoryMirroring::RemoteError,
+ Gitlab::Shell::Error => e
error(:project, repo_url, e.message)
raise Github::RepositoryFetchError
end
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 87aeb76b66a..0ad9285c0ea 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -1,11 +1,11 @@
module Gitlab
module Auth
- MissingPersonalTokenError = Class.new(StandardError)
+ MissingPersonalAccessTokenError = Class.new(StandardError)
REGISTRY_SCOPES = [:read_registry].freeze
# Scopes used for GitLab API access
- API_SCOPES = [:api, :read_user].freeze
+ API_SCOPES = [:api, :read_user, :sudo].freeze
# Scopes used for OpenID Connect
OPENID_SCOPES = [:openid].freeze
@@ -38,7 +38,7 @@ module Gitlab
# If sign-in is disabled and LDAP is not configured, recommend a
# personal access token on failed auth attempts
- raise Gitlab::Auth::MissingPersonalTokenError
+ raise Gitlab::Auth::MissingPersonalAccessTokenError
end
def find_with_user_password(login, password)
@@ -106,7 +106,7 @@ module Gitlab
user = find_with_user_password(login, password)
return unless user
- raise Gitlab::Auth::MissingPersonalTokenError if user.two_factor_enabled?
+ raise Gitlab::Auth::MissingPersonalAccessTokenError if user.two_factor_enabled?
Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)
end
@@ -128,7 +128,7 @@ module Gitlab
token = PersonalAccessTokensFinder.new(state: 'active').find_by(token: password)
if token && valid_scoped_token?(token, available_scopes)
- Gitlab::Auth::Result.new(token.user, nil, :personal_token, abilities_for_scope(token.scopes))
+ Gitlab::Auth::Result.new(token.user, nil, :personal_access_token, abilities_for_scope(token.scopes))
end
end
@@ -226,8 +226,10 @@ module Gitlab
[]
end
- def available_scopes
- API_SCOPES + registry_scopes
+ def available_scopes(current_user = nil)
+ scopes = API_SCOPES + registry_scopes
+ scopes.delete(:sudo) if current_user && !current_user.admin?
+ scopes
end
# Other available scopes
diff --git a/lib/gitlab/ci/status/build/cancelable.rb b/lib/gitlab/ci/status/build/cancelable.rb
index 8ad3e57e59d..2d9166d6bdd 100644
--- a/lib/gitlab/ci/status/build/cancelable.rb
+++ b/lib/gitlab/ci/status/build/cancelable.rb
@@ -8,7 +8,7 @@ module Gitlab
end
def action_icon
- 'icon_action_cancel'
+ 'cancel'
end
def action_path
diff --git a/lib/gitlab/ci/status/build/failed_allowed.rb b/lib/gitlab/ci/status/build/failed_allowed.rb
index e42d3574357..d71e63e73eb 100644
--- a/lib/gitlab/ci/status/build/failed_allowed.rb
+++ b/lib/gitlab/ci/status/build/failed_allowed.rb
@@ -8,7 +8,7 @@ module Gitlab
end
def icon
- 'icon_status_warning'
+ 'warning'
end
def group
diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb
index c7726543599..b7b45466d3b 100644
--- a/lib/gitlab/ci/status/build/play.rb
+++ b/lib/gitlab/ci/status/build/play.rb
@@ -12,7 +12,7 @@ module Gitlab
end
def action_icon
- 'icon_action_play'
+ 'play'
end
def action_title
diff --git a/lib/gitlab/ci/status/build/retryable.rb b/lib/gitlab/ci/status/build/retryable.rb
index 8c8fdc56d75..44ffe783e50 100644
--- a/lib/gitlab/ci/status/build/retryable.rb
+++ b/lib/gitlab/ci/status/build/retryable.rb
@@ -8,7 +8,7 @@ module Gitlab
end
def action_icon
- 'icon_action_retry'
+ 'retry'
end
def action_title
diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb
index d464738deaf..46e730797e4 100644
--- a/lib/gitlab/ci/status/build/stop.rb
+++ b/lib/gitlab/ci/status/build/stop.rb
@@ -12,7 +12,7 @@ module Gitlab
end
def action_icon
- 'icon_action_stop'
+ 'stop'
end
def action_title
diff --git a/lib/gitlab/ci/status/canceled.rb b/lib/gitlab/ci/status/canceled.rb
index e5fdc1f8136..e6195a60d4f 100644
--- a/lib/gitlab/ci/status/canceled.rb
+++ b/lib/gitlab/ci/status/canceled.rb
@@ -11,7 +11,7 @@ module Gitlab
end
def icon
- 'icon_status_canceled'
+ 'status_canceled'
end
def favicon
diff --git a/lib/gitlab/ci/status/created.rb b/lib/gitlab/ci/status/created.rb
index d188bd286a6..846f00b83dd 100644
--- a/lib/gitlab/ci/status/created.rb
+++ b/lib/gitlab/ci/status/created.rb
@@ -11,7 +11,7 @@ module Gitlab
end
def icon
- 'icon_status_created'
+ 'status_created'
end
def favicon
diff --git a/lib/gitlab/ci/status/failed.rb b/lib/gitlab/ci/status/failed.rb
index 38e45714c22..27ce85bd3ed 100644
--- a/lib/gitlab/ci/status/failed.rb
+++ b/lib/gitlab/ci/status/failed.rb
@@ -11,7 +11,7 @@ module Gitlab
end
def icon
- 'icon_status_failed'
+ 'status_failed'
end
def favicon
diff --git a/lib/gitlab/ci/status/manual.rb b/lib/gitlab/ci/status/manual.rb
index a4a7edadac9..fc387e2fd25 100644
--- a/lib/gitlab/ci/status/manual.rb
+++ b/lib/gitlab/ci/status/manual.rb
@@ -11,7 +11,7 @@ module Gitlab
end
def icon
- 'icon_status_manual'
+ 'status_manual'
end
def favicon
diff --git a/lib/gitlab/ci/status/pending.rb b/lib/gitlab/ci/status/pending.rb
index 5164260b861..6780780db32 100644
--- a/lib/gitlab/ci/status/pending.rb
+++ b/lib/gitlab/ci/status/pending.rb
@@ -11,7 +11,7 @@ module Gitlab
end
def icon
- 'icon_status_pending'
+ 'status_pending'
end
def favicon
diff --git a/lib/gitlab/ci/status/running.rb b/lib/gitlab/ci/status/running.rb
index 993937e98ca..ee13905e46d 100644
--- a/lib/gitlab/ci/status/running.rb
+++ b/lib/gitlab/ci/status/running.rb
@@ -11,7 +11,7 @@ module Gitlab
end
def icon
- 'icon_status_running'
+ 'status_running'
end
def favicon
diff --git a/lib/gitlab/ci/status/skipped.rb b/lib/gitlab/ci/status/skipped.rb
index 0c942920b02..0dbdc4de426 100644
--- a/lib/gitlab/ci/status/skipped.rb
+++ b/lib/gitlab/ci/status/skipped.rb
@@ -11,7 +11,7 @@ module Gitlab
end
def icon
- 'icon_status_skipped'
+ 'status_skipped'
end
def favicon
diff --git a/lib/gitlab/ci/status/success.rb b/lib/gitlab/ci/status/success.rb
index d7af98857b0..731013ec017 100644
--- a/lib/gitlab/ci/status/success.rb
+++ b/lib/gitlab/ci/status/success.rb
@@ -11,7 +11,7 @@ module Gitlab
end
def icon
- 'icon_status_success'
+ 'status_success'
end
def favicon
diff --git a/lib/gitlab/ci/status/success_warning.rb b/lib/gitlab/ci/status/success_warning.rb
index 4d7d82e04cf..32b4cf43e48 100644
--- a/lib/gitlab/ci/status/success_warning.rb
+++ b/lib/gitlab/ci/status/success_warning.rb
@@ -15,7 +15,7 @@ module Gitlab
end
def icon
- 'icon_status_warning'
+ 'status_warning'
end
def group
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index 357f16936c6..43a00d6cedb 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -4,6 +4,10 @@ module Gitlab
# https://www.postgresql.org/docs/9.2/static/datatype-numeric.html
# http://dev.mysql.com/doc/refman/5.7/en/integer-types.html
MAX_INT_VALUE = 2147483647
+ # The max value between MySQL's TIMESTAMP and PostgreSQL's timestampz:
+ # https://www.postgresql.org/docs/9.1/static/datatype-datetime.html
+ # https://dev.mysql.com/doc/refman/5.7/en/datetime.html
+ MAX_TIMESTAMP_VALUE = Time.at((1 << 31) - 1).freeze
def self.config
ActiveRecord::Base.configurations[Rails.env]
@@ -120,6 +124,10 @@ module Gitlab
EOF
end
+ def self.sanitize_timestamp(timestamp)
+ MAX_TIMESTAMP_VALUE > timestamp ? timestamp : MAX_TIMESTAMP_VALUE.dup
+ end
+
# pool_size - The size of the DB pool.
# host - An optional host name to use instead of the default one.
def self.create_connection_pool(pool_size, host = nil)
diff --git a/lib/gitlab/diff/position.rb b/lib/gitlab/diff/position.rb
index bd0a9502a5e..ccfb908bcca 100644
--- a/lib/gitlab/diff/position.rb
+++ b/lib/gitlab/diff/position.rb
@@ -94,7 +94,9 @@ module Gitlab
end
def diff_file(repository)
- @diff_file ||= begin
+ return @diff_file if defined?(@diff_file)
+
+ @diff_file = begin
if RequestStore.active?
key = {
project_id: repository.project.id,
@@ -122,8 +124,8 @@ module Gitlab
def find_diff_file(repository)
return unless diff_refs.complete?
-
- diff_refs.compare_in(repository.project).diffs(paths: paths, expanded: true).diff_files.first
+ return unless comparison = diff_refs.compare_in(repository.project)
+ comparison.diffs(paths: paths, expanded: true).diff_files.first
end
def get_formatter_class(type)
diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb
index c4c60d1dfee..0ea534a5fd0 100644
--- a/lib/gitlab/ee_compat_check.rb
+++ b/lib/gitlab/ee_compat_check.rb
@@ -2,8 +2,8 @@
module Gitlab
# Checks if a set of migrations requires downtime or not.
class EeCompatCheck
- DEFAULT_CE_REPO = 'https://gitlab.com/gitlab-org/gitlab-ce.git'.freeze
- EE_REPO = 'https://gitlab.com/gitlab-org/gitlab-ee.git'.freeze
+ DEFAULT_CE_PROJECT_URL = 'https://gitlab.com/gitlab-org/gitlab-ce'.freeze
+ EE_REPO_URL = 'https://gitlab.com/gitlab-org/gitlab-ee.git'.freeze
CHECK_DIR = Rails.root.join('ee_compat_check')
IGNORED_FILES_REGEX = /(VERSION|CHANGELOG\.md:\d+)/.freeze
PLEASE_READ_THIS_BANNER = %Q{
@@ -17,14 +17,16 @@ module Gitlab
============================================================\n
}.freeze
- attr_reader :ee_repo_dir, :patches_dir, :ce_repo, :ce_branch, :ee_branch_found
- attr_reader :failed_files
+ attr_reader :ee_repo_dir, :patches_dir, :ce_project_url, :ce_repo_url, :ce_branch, :ee_branch_found
+ attr_reader :job_id, :failed_files
- def initialize(branch:, ce_repo: DEFAULT_CE_REPO)
+ def initialize(branch:, ce_project_url: DEFAULT_CE_PROJECT_URL, job_id: nil)
@ee_repo_dir = CHECK_DIR.join('ee-repo')
@patches_dir = CHECK_DIR.join('patches')
@ce_branch = branch
- @ce_repo = ce_repo
+ @ce_project_url = ce_project_url
+ @ce_repo_url = "#{ce_project_url}.git"
+ @job_id = job_id
end
def check
@@ -59,8 +61,8 @@ module Gitlab
step("#{ee_repo_dir} already exists")
else
step(
- "Cloning #{EE_REPO} into #{ee_repo_dir}",
- %W[git clone --branch master --single-branch --depth=200 #{EE_REPO} #{ee_repo_dir}]
+ "Cloning #{EE_REPO_URL} into #{ee_repo_dir}",
+ %W[git clone --branch master --single-branch --depth=200 #{EE_REPO_URL} #{ee_repo_dir}]
)
end
end
@@ -132,7 +134,7 @@ module Gitlab
def check_patch(patch_path)
step("Checking out master", %w[git checkout master])
step("Resetting to latest master", %w[git reset --hard origin/master])
- step("Fetching CE/#{ce_branch}", %W[git fetch #{ce_repo} #{ce_branch}])
+ step("Fetching CE/#{ce_branch}", %W[git fetch #{ce_repo_url} #{ce_branch}])
step(
"Checking if #{patch_path} applies cleanly to EE/master",
# Don't use --check here because it can result in a 0-exit status even
@@ -237,7 +239,7 @@ module Gitlab
end
def patch_url
- "https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/#{ENV['CI_JOB_ID']}/artifacts/raw/ee_compat_check/patches/#{ce_patch_name}"
+ "#{ce_project_url}/-/jobs/#{job_id}/artifacts/raw/ee_compat_check/patches/#{ce_patch_name}"
end
def step(desc, cmd = nil)
@@ -304,7 +306,7 @@ module Gitlab
# In the EE repo
$ git fetch origin
$ git checkout -b #{ee_branch_prefix} origin/master
- $ git fetch #{ce_repo} #{ce_branch}
+ $ git fetch #{ce_repo_url} #{ce_branch}
$ git cherry-pick SHA # Repeat for all the commits you want to pick
You can squash the `#{ce_branch}` commits into a single "Port of #{ce_branch} to EE" commit.
diff --git a/lib/gitlab/gcp/model.rb b/lib/gitlab/gcp/model.rb
deleted file mode 100644
index 195391f0e3c..00000000000
--- a/lib/gitlab/gcp/model.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-module Gitlab
- module Gcp
- module Model
- def table_name_prefix
- "gcp_"
- end
-
- def model_name
- @model_name ||= ActiveModel::Name.new(self, nil, self.name.split("::").last)
- end
- end
- end
-end
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index a4336facee5..cc6c7609ec7 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -12,6 +12,12 @@ module Gitlab
# blob data should use load_all_data!.
MAX_DATA_DISPLAY_SIZE = 10.megabytes
+ # These limits are used as a heuristic to ignore files which can't be LFS
+ # pointers. The format of these is described in
+ # https://github.com/git-lfs/git-lfs/blob/master/docs/spec.md#the-pointer
+ LFS_POINTER_MIN_SIZE = 120.bytes
+ LFS_POINTER_MAX_SIZE = 200.bytes
+
attr_accessor :name, :path, :size, :data, :mode, :id, :commit_id, :loaded_size, :binary
class << self
@@ -30,16 +36,7 @@ module Gitlab
if is_enabled
Gitlab::GitalyClient::BlobService.new(repository).get_blob(oid: sha, limit: MAX_DATA_DISPLAY_SIZE)
else
- blob = repository.lookup(sha)
-
- next unless blob.is_a?(Rugged::Blob)
-
- new(
- id: blob.oid,
- size: blob.size,
- data: blob.content(MAX_DATA_DISPLAY_SIZE),
- binary: blob.binary?
- )
+ rugged_raw(repository, sha, limit: MAX_DATA_DISPLAY_SIZE)
end
end
end
@@ -59,10 +56,25 @@ module Gitlab
end
end
+ # Find LFS blobs given an array of sha ids
+ # Returns array of Gitlab::Git::Blob
+ # Does not guarantee blob data will be set
+ def batch_lfs_pointers(repository, blob_ids)
+ blob_ids.lazy
+ .select { |sha| possible_lfs_blob?(repository, sha) }
+ .map { |sha| rugged_raw(repository, sha, limit: LFS_POINTER_MAX_SIZE) }
+ .select(&:lfs_pointer?)
+ .force
+ end
+
def binary?(data)
EncodingHelper.detect_libgit2_binary?(data)
end
+ def size_could_be_lfs?(size)
+ size.between?(LFS_POINTER_MIN_SIZE, LFS_POINTER_MAX_SIZE)
+ end
+
private
# Recursive search of blob id by path
@@ -167,6 +179,29 @@ module Gitlab
end
end
end
+
+ def rugged_raw(repository, sha, limit:)
+ blob = repository.lookup(sha)
+
+ return unless blob.is_a?(Rugged::Blob)
+
+ new(
+ id: blob.oid,
+ size: blob.size,
+ data: blob.content(limit),
+ binary: blob.binary?
+ )
+ end
+
+ # Efficient lookup to determine if object size
+ # and type make it a possible LFS blob without loading
+ # blob content into memory with repository.lookup(sha)
+ def possible_lfs_blob?(repository, sha)
+ object_header = repository.rugged.read_header(sha)
+
+ object_header[:type] == :blob &&
+ size_could_be_lfs?(object_header[:len])
+ end
end
def initialize(options)
@@ -226,7 +261,7 @@ module Gitlab
# size
# see https://github.com/github/git-lfs/blob/v1.1.0/docs/spec.md#the-pointer
def lfs_pointer?
- has_lfs_version_key? && lfs_oid.present? && lfs_size.present?
+ self.class.size_could_be_lfs?(size) && has_lfs_version_key? && lfs_oid.present? && lfs_size.present?
end
def lfs_oid
diff --git a/lib/gitlab/git/branch.rb b/lib/gitlab/git/branch.rb
index c53882787f1..3487e099381 100644
--- a/lib/gitlab/git/branch.rb
+++ b/lib/gitlab/git/branch.rb
@@ -3,6 +3,14 @@
module Gitlab
module Git
class Branch < Ref
+ def self.find(repo, branch_name)
+ if branch_name.is_a?(Gitlab::Git::Branch)
+ branch_name
+ else
+ repo.find_branch(branch_name)
+ end
+ end
+
def initialize(repository, name, target, target_commit)
super(repository, name, target, target_commit)
end
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index 1957c254c28..d5518814483 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -72,7 +72,8 @@ module Gitlab
decorate(repo, commit) if commit
rescue Rugged::ReferenceError, Rugged::InvalidError, Rugged::ObjectError,
- Gitlab::Git::CommandError, Gitlab::Git::Repository::NoRepository
+ Gitlab::Git::CommandError, Gitlab::Git::Repository::NoRepository,
+ Rugged::OdbError, Rugged::TreeError, ArgumentError
nil
end
diff --git a/lib/gitlab/git/lfs_changes.rb b/lib/gitlab/git/lfs_changes.rb
new file mode 100644
index 00000000000..2749e2e69e2
--- /dev/null
+++ b/lib/gitlab/git/lfs_changes.rb
@@ -0,0 +1,36 @@
+module Gitlab
+ module Git
+ class LfsChanges
+ def initialize(repository, newrev)
+ @repository = repository
+ @newrev = newrev
+ end
+
+ def new_pointers(object_limit: nil, not_in: nil)
+ @new_pointers ||= begin
+ object_ids = new_objects(not_in: not_in)
+ object_ids = object_ids.take(object_limit) if object_limit
+
+ Gitlab::Git::Blob.batch_lfs_pointers(@repository, object_ids)
+ end
+ end
+
+ def all_pointers
+ object_ids = rev_list.all_objects(require_path: true)
+
+ Gitlab::Git::Blob.batch_lfs_pointers(@repository, object_ids)
+ end
+
+ private
+
+ def new_objects(not_in:)
+ rev_list.new_objects(require_path: true, lazy: true, not_in: not_in)
+ end
+
+ def rev_list
+ ::Gitlab::Git::RevList.new(path_to_repo: @repository.path_to_repo,
+ newrev: @newrev)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 408616d174b..182ffc96ef9 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -6,6 +6,7 @@ require "rubygems/package"
module Gitlab
module Git
class Repository
+ include Gitlab::Git::RepositoryMirroring
include Gitlab::Git::Popen
ALLOWED_OBJECT_DIRECTORIES_VARIABLES = %w[
@@ -290,6 +291,14 @@ module Gitlab
end
end
+ def batch_existence(object_ids, existing: true)
+ filter_method = existing ? :select : :reject
+
+ object_ids.public_send(filter_method) do |oid| # rubocop:disable GitlabSecurity/PublicSend
+ rugged.exists?(oid)
+ end
+ end
+
# Returns an Array of branch and tag names
def ref_names
branch_names + tag_names
@@ -511,6 +520,10 @@ module Gitlab
gitaly_commit_client.ancestor?(from, to)
end
+ def merged_branch_names(branch_names = [])
+ Set.new(git_merged_branch_names(branch_names))
+ end
+
# Return an array of Diff objects that represent the diff
# between +from+ and +to+. See Diff::filter_diff_options for the allowed
# diff options. The +options+ hash can also include :break_rewrites to
@@ -746,13 +759,13 @@ module Gitlab
end
def ff_merge(user, source_sha, target_branch)
- OperationService.new(user, self).with_branch(target_branch) do |our_commit|
- raise ArgumentError, 'Invalid merge target' unless our_commit
-
- source_sha
+ gitaly_migrate(:operation_user_ff_branch) do |is_enabled|
+ if is_enabled
+ gitaly_ff_merge(user, source_sha, target_branch)
+ else
+ rugged_ff_merge(user, source_sha, target_branch)
+ end
end
- rescue Rugged::ReferenceError
- raise ArgumentError, 'Invalid merge source'
end
def revert(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
@@ -886,16 +899,25 @@ module Gitlab
end
end
- # Delete the specified remote from this repository.
- def remote_delete(remote_name)
- rugged.remotes.delete(remote_name)
- nil
+ def add_remote(remote_name, url)
+ rugged.remotes.create(remote_name, url)
+ rescue Rugged::ConfigError
+ remote_update(remote_name, url: url)
end
- # Add a new remote to this repository.
- def remote_add(remote_name, url)
- rugged.remotes.create(remote_name, url)
- nil
+ def remove_remote(remote_name)
+ # When a remote is deleted all its remote refs are deleted too, but in
+ # the case of mirrors we map its refs (that would usualy go under
+ # [remote_name]/) to the top level namespace. We clean the mapping so
+ # those don't get deleted.
+ if rugged.config["remote.#{remote_name}.mirror"]
+ rugged.config.delete("remote.#{remote_name}.fetch")
+ end
+
+ rugged.remotes.delete(remote_name)
+ true
+ rescue Rugged::ConfigError
+ false
end
# Update the specified remote using the values in the +options+ hash
@@ -1165,10 +1187,10 @@ module Gitlab
Gitlab::GitalyClient.migrate(method, status: status, &block)
rescue GRPC::NotFound => e
raise NoRepository.new(e)
- rescue GRPC::BadStatus => e
- raise CommandError.new(e)
rescue GRPC::InvalidArgument => e
raise ArgumentError.new(e)
+ rescue GRPC::BadStatus => e
+ raise CommandError.new(e)
end
private
@@ -1190,6 +1212,13 @@ module Gitlab
sort_branches(branches, sort_by)
end
+ def git_merged_branch_names(branch_names = [])
+ lines = run_git(['branch', '--merged', root_ref] + branch_names)
+ .first.lines
+
+ lines.map(&:strip)
+ end
+
def log_using_shell?(options)
options[:path].present? ||
options[:disable_walk] ||
@@ -1603,6 +1632,22 @@ module Gitlab
run_git(args, env: env)
end
+
+ def gitaly_ff_merge(user, source_sha, target_branch)
+ gitaly_operations_client.user_ff_branch(user, source_sha, target_branch)
+ rescue GRPC::FailedPrecondition => e
+ raise CommitError, e
+ end
+
+ def rugged_ff_merge(user, source_sha, target_branch)
+ OperationService.new(user, self).with_branch(target_branch) do |our_commit|
+ raise ArgumentError, 'Invalid merge target' unless our_commit
+
+ source_sha
+ end
+ rescue Rugged::ReferenceError
+ raise ArgumentError, 'Invalid merge source'
+ end
end
end
end
diff --git a/lib/gitlab/git/repository_mirroring.rb b/lib/gitlab/git/repository_mirroring.rb
new file mode 100644
index 00000000000..637e7a0659c
--- /dev/null
+++ b/lib/gitlab/git/repository_mirroring.rb
@@ -0,0 +1,95 @@
+module Gitlab
+ module Git
+ module RepositoryMirroring
+ IMPORT_HEAD_REFS = '+refs/heads/*:refs/heads/*'.freeze
+ IMPORT_TAG_REFS = '+refs/tags/*:refs/tags/*'.freeze
+ MIRROR_REMOTE = 'mirror'.freeze
+
+ RemoteError = Class.new(StandardError)
+
+ def set_remote_as_mirror(remote_name)
+ # This is used to define repository as equivalent as "git clone --mirror"
+ rugged.config["remote.#{remote_name}.fetch"] = 'refs/*:refs/*'
+ rugged.config["remote.#{remote_name}.mirror"] = true
+ rugged.config["remote.#{remote_name}.prune"] = true
+ end
+
+ def set_import_remote_as_mirror(remote_name)
+ # Add first fetch with Rugged so it does not create its own.
+ rugged.config["remote.#{remote_name}.fetch"] = IMPORT_HEAD_REFS
+
+ add_remote_fetch_config(remote_name, IMPORT_TAG_REFS)
+
+ rugged.config["remote.#{remote_name}.mirror"] = true
+ rugged.config["remote.#{remote_name}.prune"] = true
+ end
+
+ def add_remote_fetch_config(remote_name, refspec)
+ run_git(%W[config --add remote.#{remote_name}.fetch #{refspec}])
+ end
+
+ def fetch_mirror(url)
+ add_remote(MIRROR_REMOTE, url)
+ set_remote_as_mirror(MIRROR_REMOTE)
+ fetch(MIRROR_REMOTE)
+ remove_remote(MIRROR_REMOTE)
+ end
+
+ def remote_tags(remote)
+ # Each line has this format: "dc872e9fa6963f8f03da6c8f6f264d0845d6b092\trefs/tags/v1.10.0\n"
+ # We want to convert it to: [{ 'v1.10.0' => 'dc872e9fa6963f8f03da6c8f6f264d0845d6b092' }, ...]
+ list_remote_tags(remote).map do |line|
+ target, path = line.strip.split("\t")
+
+ # When the remote repo does not have tags.
+ if target.nil? || path.nil?
+ Rails.logger.info "Empty or invalid list of tags for remote: #{remote}. Output: #{output}"
+ return []
+ end
+
+ name = path.split('/', 3).last
+ # We're only interested in tag references
+ # See: http://stackoverflow.com/questions/15472107/when-listing-git-ls-remote-why-theres-after-the-tag-name
+ next if name =~ /\^\{\}\Z/
+
+ target_commit = Gitlab::Git::Commit.find(self, target)
+ Gitlab::Git::Tag.new(self, name, target, target_commit)
+ end.compact
+ end
+
+ def remote_branches(remote_name)
+ branches = []
+
+ rugged.references.each("refs/remotes/#{remote_name}/*").map do |ref|
+ name = ref.name.sub(/\Arefs\/remotes\/#{remote_name}\//, '')
+
+ begin
+ target_commit = Gitlab::Git::Commit.find(self, ref.target)
+ branches << Gitlab::Git::Branch.new(self, name, ref.target, target_commit)
+ rescue Rugged::ReferenceError
+ # Omit invalid branch
+ end
+ end
+
+ branches
+ end
+
+ private
+
+ def list_remote_tags(remote)
+ tag_list, exit_code, error = nil
+ cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{full_path} ls-remote --tags #{remote})
+
+ Open3.popen3(*cmd) do |stdin, stdout, stderr, wait_thr|
+ tag_list = stdout.read
+ error = stderr.read
+ exit_code = wait_thr.value.exitstatus
+ end
+
+ raise RemoteError, error unless exit_code.zero?
+
+ tag_list.split('\n')
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/rev_list.rb b/lib/gitlab/git/rev_list.rb
index 60b2a4ec411..e0c884aceaa 100644
--- a/lib/gitlab/git/rev_list.rb
+++ b/lib/gitlab/git/rev_list.rb
@@ -13,11 +13,31 @@ module Gitlab
@path_to_repo = path_to_repo
end
- # This method returns an array of new references
+ # This method returns an array of new commit references
def new_refs
execute([*base_args, newrev, '--not', '--all'])
end
+ # Finds newly added objects
+ # Returns an array of shas
+ #
+ # Can skip objects which do not have a path using required_path: true
+ # This skips commit objects and root trees, which might not be needed when
+ # looking for blobs
+ #
+ # Can return a lazy enumerator to limit work done on megabytes of data
+ def new_objects(require_path: nil, lazy: false, not_in: nil)
+ object_output = execute([*base_args, newrev, *not_in_refs(not_in), '--objects'])
+
+ objects_from_output(object_output, require_path: require_path, lazy: lazy)
+ end
+
+ def all_objects(require_path: nil)
+ object_output = execute([*base_args, '--all', '--objects'])
+
+ objects_from_output(object_output, require_path: require_path, lazy: true)
+ end
+
# This methods returns an array of missed references
#
# Should become obsolete after https://gitlab.com/gitlab-org/gitaly/issues/348.
@@ -27,6 +47,13 @@ module Gitlab
private
+ def not_in_refs(references)
+ return ['--not', '--all'] unless references
+ return [] if references.empty?
+
+ references.prepend('--not')
+ end
+
def execute(args)
output, status = popen(args, nil, Gitlab::Git::Env.to_env_hash)
@@ -44,6 +71,22 @@ module Gitlab
'rev-list'
]
end
+
+ def objects_from_output(object_output, require_path: nil, lazy: nil)
+ objects = object_output.lazy.map do |output_line|
+ sha, path = output_line.split(' ', 2)
+
+ next if require_path && path.blank?
+
+ sha
+ end.reject(&:nil?)
+
+ if lazy
+ objects
+ else
+ objects.force
+ end
+ end
end
end
end
diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb
index e7b2f52a552..fe901d049d4 100644
--- a/lib/gitlab/git/wiki.rb
+++ b/lib/gitlab/git/wiki.rb
@@ -8,6 +8,9 @@ module Gitlab
{ name: name, email: email, message: message }
end
end
+ PageBlob = Struct.new(:name)
+
+ attr_reader :repository
def self.default_ref
'master'
@@ -34,10 +37,14 @@ module Gitlab
end
def delete_page(page_path, commit_details)
- assert_type!(commit_details, CommitDetails)
-
- gollum_wiki.delete_page(gollum_page_by_path(page_path), commit_details.to_h)
- nil
+ @repository.gitaly_migrate(:wiki_delete_page) do |is_enabled|
+ if is_enabled
+ gitaly_delete_page(page_path, commit_details)
+ gollum_wiki.clear_cache
+ else
+ gollum_delete_page(page_path, commit_details)
+ end
+ end
end
def update_page(page_path, title, format, content, commit_details)
@@ -53,22 +60,23 @@ module Gitlab
end
def page(title:, version: nil, dir: nil)
- if version
- version = Gitlab::Git::Commit.find(@repository, version).id
+ @repository.gitaly_migrate(:wiki_find_page) do |is_enabled|
+ if is_enabled
+ gitaly_find_page(title: title, version: version, dir: dir)
+ else
+ gollum_find_page(title: title, version: version, dir: dir)
+ end
end
-
- gollum_page = gollum_wiki.page(title, version, dir)
- return unless gollum_page
-
- new_page(gollum_page)
end
def file(name, version)
- version ||= self.class.default_ref
- gollum_file = gollum_wiki.file(name, version)
- return unless gollum_file
-
- Gitlab::Git::WikiFile.new(gollum_file)
+ @repository.gitaly_migrate(:wiki_find_file) do |is_enabled|
+ if is_enabled
+ gitaly_find_file(name, version)
+ else
+ gollum_find_file(name, version)
+ end
+ end
end
def page_versions(page_path)
@@ -80,7 +88,15 @@ module Gitlab
end
def preview_slug(title, format)
- gollum_wiki.preview_page(title, '', format).url_path
+ # Adapted from gollum gem (Gollum::Wiki#preview_page) to avoid
+ # using Rugged through a Gollum::Wiki instance
+ page_class = Gollum::Page
+ page = page_class.new(nil)
+ ext = page_class.format_to_ext(format.to_sym)
+ name = page_class.cname(title) + '.' + ext
+ blob = PageBlob.new(name)
+ page.populate(blob)
+ page.url_path
end
private
@@ -126,9 +142,53 @@ module Gitlab
raise Gitlab::Git::Wiki::DuplicatePageError, e.message
end
+ def gollum_delete_page(page_path, commit_details)
+ assert_type!(commit_details, CommitDetails)
+
+ gollum_wiki.delete_page(gollum_page_by_path(page_path), commit_details.to_h)
+ nil
+ end
+
+ def gollum_find_page(title:, version: nil, dir: nil)
+ if version
+ version = Gitlab::Git::Commit.find(@repository, version).id
+ end
+
+ gollum_page = gollum_wiki.page(title, version, dir)
+ return unless gollum_page
+
+ new_page(gollum_page)
+ end
+
+ def gollum_find_file(name, version)
+ version ||= self.class.default_ref
+ gollum_file = gollum_wiki.file(name, version)
+ return unless gollum_file
+
+ Gitlab::Git::WikiFile.new(gollum_file)
+ end
+
def gitaly_write_page(name, format, content, commit_details)
gitaly_wiki_client.write_page(name, format, content, commit_details)
end
+
+ def gitaly_delete_page(page_path, commit_details)
+ gitaly_wiki_client.delete_page(page_path, commit_details)
+ end
+
+ def gitaly_find_page(title:, version: nil, dir: nil)
+ wiki_page, version = gitaly_wiki_client.find_page(title: title, version: version, dir: dir)
+ return unless wiki_page
+
+ Gitlab::Git::WikiPage.new(wiki_page, version)
+ end
+
+ def gitaly_find_file(name, version)
+ wiki_file = gitaly_wiki_client.find_file(name, version)
+ return unless wiki_file
+
+ Gitlab::Git::WikiFile.new(wiki_file)
+ end
end
end
end
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index 6868be26758..0b35a787e07 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -34,10 +34,11 @@ module Gitlab
private_constant :MUTEX
class << self
- attr_accessor :query_time
+ attr_accessor :query_time, :migrate_histogram
end
self.query_time = 0
+ self.migrate_histogram = Gitlab::Metrics.histogram(:gitaly_migrate_call_duration, "Gitaly migration call execution timings")
def self.stub(name, storage)
MUTEX.synchronize do
@@ -171,8 +172,11 @@ module Gitlab
feature_stack = Thread.current[:gitaly_feature_stack] ||= []
feature_stack.unshift(feature)
begin
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
yield is_enabled
ensure
+ total_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
+ migrate_histogram.observe({ gitaly_enabled: is_enabled, feature: feature }, total_time)
feature_stack.shift
Thread.current[:gitaly_feature_stack] = nil if feature_stack.empty?
end
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index a2b50f2507e..da5505cb2fe 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -18,7 +18,7 @@ module Gitlab
response = GitalyClient.call(@repository.storage, :commit_service, :list_files, request)
response.flat_map do |msg|
- msg.paths.map { |d| d.dup.force_encoding(Encoding::UTF_8) }
+ msg.paths.map { |d| EncodingHelper.encode!(d.dup) }
end
end
diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb
index adaf255f24b..526d44a8b77 100644
--- a/lib/gitlab/gitaly_client/operation_service.rb
+++ b/lib/gitlab/gitaly_client/operation_service.rb
@@ -105,6 +105,23 @@ module Gitlab
ensure
request_enum.close
end
+
+ def user_ff_branch(user, source_sha, target_branch)
+ request = Gitaly::UserFFBranchRequest.new(
+ repository: @gitaly_repo,
+ user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
+ commit_id: source_sha,
+ branch: GitalyClient.encode(target_branch)
+ )
+
+ branch_update = GitalyClient.call(
+ @repository.storage,
+ :operation_service,
+ :user_ff_branch,
+ request
+ ).branch_update
+ Gitlab::Git::OperationService::BranchUpdate.from_gitaly(branch_update)
+ end
end
end
end
diff --git a/lib/gitlab/gitaly_client/wiki_file.rb b/lib/gitlab/gitaly_client/wiki_file.rb
new file mode 100644
index 00000000000..a2e415864e6
--- /dev/null
+++ b/lib/gitlab/gitaly_client/wiki_file.rb
@@ -0,0 +1,17 @@
+module Gitlab
+ module GitalyClient
+ class WikiFile
+ FIELDS = %i(name mime_type path raw_data).freeze
+
+ attr_accessor(*FIELDS)
+
+ def initialize(params)
+ params = params.with_indifferent_access
+
+ FIELDS.each do |field|
+ instance_variable_set("@#{field}", params[field])
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gitaly_client/wiki_page.rb b/lib/gitlab/gitaly_client/wiki_page.rb
new file mode 100644
index 00000000000..8226278d5f6
--- /dev/null
+++ b/lib/gitlab/gitaly_client/wiki_page.rb
@@ -0,0 +1,25 @@
+module Gitlab
+ module GitalyClient
+ class WikiPage
+ FIELDS = %i(title format url_path path name historical raw_data).freeze
+
+ attr_accessor(*FIELDS)
+
+ def initialize(params)
+ params = params.with_indifferent_access
+
+ FIELDS.each do |field|
+ instance_variable_set("@#{field}", params[field])
+ end
+ end
+
+ def historical?
+ @historical
+ end
+
+ def format
+ @format.to_sym
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gitaly_client/wiki_service.rb b/lib/gitlab/gitaly_client/wiki_service.rb
index 03afcce81f0..15f0f30d303 100644
--- a/lib/gitlab/gitaly_client/wiki_service.rb
+++ b/lib/gitlab/gitaly_client/wiki_service.rb
@@ -15,11 +15,7 @@ module Gitlab
repository: @gitaly_repo,
name: GitalyClient.encode(name),
format: format.to_s,
- commit_details: Gitaly::WikiCommitDetails.new(
- name: GitalyClient.encode(commit_details.name),
- email: GitalyClient.encode(commit_details.email),
- message: GitalyClient.encode(commit_details.message)
- )
+ commit_details: gitaly_commit_details(commit_details)
)
strio = StringIO.new(content)
@@ -40,6 +36,85 @@ module Gitlab
raise Gitlab::Git::Wiki::DuplicatePageError, error
end
end
+
+ def delete_page(page_path, commit_details)
+ request = Gitaly::WikiDeletePageRequest.new(
+ repository: @gitaly_repo,
+ page_path: GitalyClient.encode(page_path),
+ commit_details: gitaly_commit_details(commit_details)
+ )
+
+ GitalyClient.call(@repository.storage, :wiki_service, :wiki_delete_page, request)
+ end
+
+ def find_page(title:, version: nil, dir: nil)
+ request = Gitaly::WikiFindPageRequest.new(
+ repository: @gitaly_repo,
+ title: GitalyClient.encode(title),
+ revision: GitalyClient.encode(version),
+ directory: GitalyClient.encode(dir)
+ )
+
+ response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_find_page, request)
+ wiki_page = version = nil
+
+ response.each do |message|
+ page = message.page
+ next unless page
+
+ if wiki_page
+ wiki_page.raw_data << page.raw_data
+ else
+ wiki_page = GitalyClient::WikiPage.new(page.to_h)
+ # All gRPC strings in a response are frozen, so we get
+ # an unfrozen version here so appending in the else clause below doesn't blow up.
+ wiki_page.raw_data = wiki_page.raw_data.dup
+
+ version = Gitlab::Git::WikiPageVersion.new(
+ Gitlab::Git::Commit.decorate(@repository, page.version.commit),
+ page.version.format
+ )
+ end
+ end
+
+ [wiki_page, version]
+ end
+
+ def find_file(name, revision)
+ request = Gitaly::WikiFindFileRequest.new(
+ repository: @gitaly_repo,
+ name: GitalyClient.encode(name),
+ revision: GitalyClient.encode(revision)
+ )
+
+ response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_find_file, request)
+ wiki_file = nil
+
+ response.each do |message|
+ next unless message.name.present?
+
+ if wiki_file
+ wiki_file.raw_data << message.raw_data
+ else
+ wiki_file = GitalyClient::WikiFile.new(message.to_h)
+ # All gRPC strings in a response are frozen, so we get
+ # an unfrozen version here so appending in the else clause below doesn't blow up.
+ wiki_file.raw_data = wiki_file.raw_data.dup
+ end
+ end
+
+ wiki_file
+ end
+
+ private
+
+ def gitaly_commit_details(commit_details)
+ Gitaly::WikiCommitDetails.new(
+ name: GitalyClient.encode(commit_details.name),
+ email: GitalyClient.encode(commit_details.email),
+ message: GitalyClient.encode(commit_details.message)
+ )
+ end
end
end
end
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index dec8b4c5acd..561779182bc 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -19,6 +19,7 @@ project_tree:
- milestone:
- events:
- :push_event_payload
+ - :issue_assignees
- snippets:
- :award_emoji
- notes:
@@ -113,6 +114,7 @@ excluded_attributes:
- :milestone_id
- :ref_fetched
- :merge_jid
+ - :latest_merge_request_diff_id
award_emoji:
- :awardable_id
statuses:
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 469b230377d..a790dcfe8a6 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -8,8 +8,8 @@ module Gitlab
triggers: 'Ci::Trigger',
pipeline_schedules: 'Ci::PipelineSchedule',
builds: 'Ci::Build',
- cluster: 'Gcp::Cluster',
- clusters: 'Gcp::Cluster',
+ cluster: 'Clusters::Cluster',
+ clusters: 'Clusters::Cluster',
hooks: 'ProjectHook',
merge_access_levels: 'ProtectedBranch::MergeAccessLevel',
push_access_levels: 'ProtectedBranch::PushAccessLevel',
diff --git a/lib/gitlab/ldap/auth_hash.rb b/lib/gitlab/ldap/auth_hash.rb
index 3123da17fd9..1bd0965679a 100644
--- a/lib/gitlab/ldap/auth_hash.rb
+++ b/lib/gitlab/ldap/auth_hash.rb
@@ -4,7 +4,7 @@ module Gitlab
module LDAP
class AuthHash < Gitlab::OAuth::AuthHash
def uid
- Gitlab::LDAP::Person.normalize_dn(super)
+ @uid ||= Gitlab::LDAP::Person.normalize_dn(super)
end
private
diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb
index 1793097363e..4d5c67ed892 100644
--- a/lib/gitlab/ldap/user.rb
+++ b/lib/gitlab/ldap/user.rb
@@ -9,10 +9,11 @@ module Gitlab
class User < Gitlab::OAuth::User
class << self
def find_by_uid_and_provider(uid, provider)
- # LDAP distinguished name is case-insensitive
+ uid = Gitlab::LDAP::Person.normalize_dn(uid)
+
identity = ::Identity
.where(provider: provider)
- .iwhere(extern_uid: uid).last
+ .where(extern_uid: uid).last
identity && identity.user
end
end
diff --git a/lib/gitlab/metrics/sidekiq_middleware.rb b/lib/gitlab/metrics/sidekiq_middleware.rb
index f9dd8e41912..b983a40611f 100644
--- a/lib/gitlab/metrics/sidekiq_middleware.rb
+++ b/lib/gitlab/metrics/sidekiq_middleware.rb
@@ -11,6 +11,8 @@ module Gitlab
# Old gitlad-shell messages don't provide enqueued_at/created_at attributes
trans.set(:sidekiq_queue_duration, Time.now.to_f - (message['enqueued_at'] || message['created_at'] || 0))
trans.run { yield }
+
+ worker.metrics_tags.each { |tag, value| trans.add_tag(tag, value) } if worker.respond_to?(:metrics_tags)
rescue Exception => error # rubocop: disable Lint/RescueException
trans.add_event(:sidekiq_exception)
diff --git a/lib/gitlab/metrics/system.rb b/lib/gitlab/metrics/system.rb
index aba3e0df382..c2cbd3c16a1 100644
--- a/lib/gitlab/metrics/system.rb
+++ b/lib/gitlab/metrics/system.rb
@@ -46,14 +46,14 @@ module Gitlab
# Returns the current real time in a given precision.
#
- # Returns the time as a Float.
+ # Returns the time as a Fixnum.
def self.real_time(precision = :millisecond)
Process.clock_gettime(Process::CLOCK_REALTIME, precision)
end
# Returns the current monotonic clock time in a given precision.
#
- # Returns the time as a Float.
+ # Returns the time as a Fixnum.
def self.monotonic_time(precision = :millisecond)
Process.clock_gettime(Process::CLOCK_MONOTONIC, precision)
end
diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb
index f42168c720e..cfc6b2a2029 100644
--- a/lib/gitlab/middleware/go.rb
+++ b/lib/gitlab/middleware/go.rb
@@ -4,6 +4,7 @@ module Gitlab
module Middleware
class Go
include ActionView::Helpers::TagHelper
+ include Gitlab::CurrentSettings
PROJECT_PATH_REGEX = %r{\A(#{Gitlab::PathRegex.full_namespace_route_regex}/#{Gitlab::PathRegex.project_route_regex})/}.freeze
@@ -37,10 +38,20 @@ module Gitlab
end
def go_body(path)
- project_url = URI.join(Gitlab.config.gitlab.url, path)
+ config = Gitlab.config
+ project_url = URI.join(config.gitlab.url, path)
import_prefix = strip_url(project_url.to_s)
- meta_tag = tag :meta, name: 'go-import', content: "#{import_prefix} git #{project_url}.git"
+ repository_url = case current_application_settings.enabled_git_access_protocol
+ when 'ssh'
+ shell = config.gitlab_shell
+ port = ":#{shell.ssh_port}" unless shell.ssh_port == 22
+ "ssh://#{shell.ssh_user}@#{shell.ssh_host}#{port}/#{path}.git"
+ when 'http', nil
+ "#{project_url}.git"
+ end
+
+ meta_tag = tag :meta, name: 'go-import', content: "#{import_prefix} git #{repository_url}"
head_tag = content_tag :head, meta_tag
content_tag :html, head_tag
end
diff --git a/lib/gitlab/middleware/read_only.rb b/lib/gitlab/middleware/read_only.rb
index 0de0cddcce4..8853dfa3d2d 100644
--- a/lib/gitlab/middleware/read_only.rb
+++ b/lib/gitlab/middleware/read_only.rb
@@ -12,6 +12,7 @@ module Gitlab
def call(env)
@env = env
+ @route_hash = nil
if disallowed_request? && Gitlab::Database.read_only?
Rails.logger.debug('GitLab ReadOnly: preventing possible non read-only operation')
@@ -77,11 +78,11 @@ module Gitlab
end
def grack_route
- request.path.end_with?('.git/git-upload-pack')
+ route_hash[:controller] == 'projects/git_http' && route_hash[:action] == 'git_upload_pack'
end
def lfs_route
- request.path.end_with?('/info/lfs/objects/batch')
+ route_hash[:controller] == 'projects/lfs_api' && route_hash[:action] == 'batch'
end
end
end
diff --git a/lib/gitlab/performance_bar/peek_query_tracker.rb b/lib/gitlab/performance_bar/peek_query_tracker.rb
index 69e117f1da9..f2825db59ae 100644
--- a/lib/gitlab/performance_bar/peek_query_tracker.rb
+++ b/lib/gitlab/performance_bar/peek_query_tracker.rb
@@ -36,7 +36,7 @@ module Gitlab
end
def track_query(raw_query, bindings, start, finish)
- duration = finish - start
+ duration = (finish - start) * 1000.0
query_info = { duration: duration.round(3), sql: raw_query }
PEEK_DB_CLIENT.query_details << query_info
diff --git a/lib/gitlab/sherlock/transaction.rb b/lib/gitlab/sherlock/transaction.rb
index 3489fb251b6..400a552bf99 100644
--- a/lib/gitlab/sherlock/transaction.rb
+++ b/lib/gitlab/sherlock/transaction.rb
@@ -89,7 +89,9 @@ module Gitlab
ActiveSupport::Notifications.subscribe('sql.active_record') do |_, start, finish, _, data|
next unless same_thread?
- track_query(data[:sql].strip, data[:binds], start, finish)
+ unless data.fetch(:cached, data[:name] == 'CACHE')
+ track_query(data[:sql].strip, data[:binds], start, finish)
+ end
end
end
diff --git a/lib/gitlab/sidekiq_middleware/memory_killer.rb b/lib/gitlab/sidekiq_middleware/memory_killer.rb
index d7d24eeb37b..2bfb7caefd9 100644
--- a/lib/gitlab/sidekiq_middleware/memory_killer.rb
+++ b/lib/gitlab/sidekiq_middleware/memory_killer.rb
@@ -7,7 +7,6 @@ module Gitlab
GRACE_TIME = (ENV['SIDEKIQ_MEMORY_KILLER_GRACE_TIME'] || 15 * 60).to_s.to_i
# Wait 30 seconds for running jobs to finish during graceful shutdown
SHUTDOWN_WAIT = (ENV['SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT'] || 30).to_s.to_i
- SHUTDOWN_SIGNAL = (ENV['SIDEKIQ_MEMORY_KILLER_SHUTDOWN_SIGNAL'] || 'SIGKILL').to_s
# Create a mutex used to ensure there will be only one thread waiting to
# shut Sidekiq down
@@ -15,6 +14,7 @@ module Gitlab
def call(worker, job, queue)
yield
+
current_rss = get_rss
return unless MAX_RSS > 0 && current_rss > MAX_RSS
@@ -23,32 +23,45 @@ module Gitlab
# Return if another thread is already waiting to shut Sidekiq down
return unless MUTEX.try_lock
- Sidekiq.logger.warn "current RSS #{current_rss} exceeds maximum RSS "\
- "#{MAX_RSS}"
- Sidekiq.logger.warn "this thread will shut down PID #{Process.pid} - Worker #{worker.class} - JID-#{job['jid']} "\
- "in #{GRACE_TIME} seconds"
- sleep(GRACE_TIME)
+ Sidekiq.logger.warn "Sidekiq worker PID-#{pid} current RSS #{current_rss}"\
+ " exceeds maximum RSS #{MAX_RSS} after finishing job #{worker.class} JID-#{job['jid']}"
+ Sidekiq.logger.warn "Sidekiq worker PID-#{pid} will stop fetching new jobs in #{GRACE_TIME} seconds, and will be shut down #{SHUTDOWN_WAIT} seconds later"
- Sidekiq.logger.warn "sending SIGTERM to PID #{Process.pid} - Worker #{worker.class} - JID-#{job['jid']}"
- Process.kill('SIGTERM', Process.pid)
+ # Wait `GRACE_TIME` to give the memory intensive job time to finish.
+ # Then, tell Sidekiq to stop fetching new jobs.
+ wait_and_signal(GRACE_TIME, 'SIGSTP', 'stop fetching new jobs')
- Sidekiq.logger.warn "waiting #{SHUTDOWN_WAIT} seconds before sending "\
- "#{SHUTDOWN_SIGNAL} to PID #{Process.pid} - Worker #{worker.class} - JID-#{job['jid']}"
- sleep(SHUTDOWN_WAIT)
+ # Wait `SHUTDOWN_WAIT` to give already fetched jobs time to finish.
+ # Then, tell Sidekiq to gracefully shut down by giving jobs a few more
+ # moments to finish, killing and requeuing them if they didn't, and
+ # then terminating itself.
+ wait_and_signal(SHUTDOWN_WAIT, 'SIGTERM', 'gracefully shut down')
- Sidekiq.logger.warn "sending #{SHUTDOWN_SIGNAL} to PID #{Process.pid} - Worker #{worker.class} - JID-#{job['jid']}"
- Process.kill(SHUTDOWN_SIGNAL, Process.pid)
+ # Wait for Sidekiq to shutdown gracefully, and kill it if it didn't.
+ wait_and_signal(Sidekiq.options[:timeout] + 2, 'SIGKILL', 'die')
end
end
private
def get_rss
- output, status = Gitlab::Popen.popen(%W(ps -o rss= -p #{Process.pid}))
+ output, status = Gitlab::Popen.popen(%W(ps -o rss= -p #{pid}))
return 0 unless status.zero?
output.to_i
end
+
+ def wait_and_signal(time, signal, explanation)
+ Sidekiq.logger.warn "waiting #{time} seconds before sending Sidekiq worker PID-#{pid} #{signal} (#{explanation})"
+ sleep(time)
+
+ Sidekiq.logger.warn "sending Sidekiq worker PID-#{pid} #{signal} (#{explanation})"
+ Process.kill(signal, pid)
+ end
+
+ def pid
+ Process.pid
+ end
end
end
end
diff --git a/lib/gitlab/testing/request_blocker_middleware.rb b/lib/gitlab/testing/request_blocker_middleware.rb
index aa67fa08577..4a8e3c2eee0 100644
--- a/lib/gitlab/testing/request_blocker_middleware.rb
+++ b/lib/gitlab/testing/request_blocker_middleware.rb
@@ -7,6 +7,7 @@ module Gitlab
class RequestBlockerMiddleware
@@num_active_requests = Concurrent::AtomicFixnum.new(0)
@@block_requests = Concurrent::AtomicBoolean.new(false)
+ @@slow_requests = Concurrent::AtomicBoolean.new(false)
# Returns the number of requests the server is currently processing.
def self.num_active_requests
@@ -19,9 +20,15 @@ module Gitlab
@@block_requests.value = true
end
+ # Slows down incoming requests (useful for race conditions).
+ def self.slow_requests!
+ @@slow_requests.value = true
+ end
+
# Allows the server to accept requests again.
def self.allow_requests!
@@block_requests.value = false
+ @@slow_requests.value = false
end
def initialize(app)
@@ -33,6 +40,7 @@ module Gitlab
if block_requests?
block_request(env)
else
+ sleep 0.2 if slow_requests?
@app.call(env)
end
ensure
@@ -45,6 +53,10 @@ module Gitlab
@@block_requests.true?
end
+ def slow_requests?
+ @@slow_requests.true?
+ end
+
def block_request(env)
[503, {}, []]
end
diff --git a/lib/gitlab/testing/request_inspector_middleware.rb b/lib/gitlab/testing/request_inspector_middleware.rb
new file mode 100644
index 00000000000..e387667480d
--- /dev/null
+++ b/lib/gitlab/testing/request_inspector_middleware.rb
@@ -0,0 +1,71 @@
+# rubocop:disable Style/ClassVars
+
+module Gitlab
+ module Testing
+ class RequestInspectorMiddleware
+ @@log_requests = Concurrent::AtomicBoolean.new(false)
+ @@logged_requests = Concurrent::Array.new
+ @@inject_headers = Concurrent::Hash.new
+
+ # Resets the current request log and starts logging requests
+ def self.log_requests!(headers = {})
+ @@inject_headers.replace(headers)
+ @@logged_requests.replace([])
+ @@log_requests.value = true
+ end
+
+ # Stops logging requests
+ def self.stop_logging!
+ @@log_requests.value = false
+ end
+
+ def self.requests
+ @@logged_requests
+ end
+
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ return @app.call(env) unless @@log_requests.true?
+
+ url = env['REQUEST_URI']
+ env.merge! http_headers_env(@@inject_headers) if @@inject_headers.any?
+ request_headers = env_http_headers(env)
+ status, headers, body = @app.call(env)
+
+ request = OpenStruct.new(
+ url: url,
+ status_code: status,
+ request_headers: request_headers,
+ response_headers: headers
+ )
+ log_request request
+
+ [status, headers, body]
+ end
+
+ private
+
+ def env_http_headers(env)
+ Hash[*env.select { |k, v| k.start_with? 'HTTP_' }
+ .collect { |k, v| [k.sub(/^HTTP_/, ''), v] }
+ .collect { |k, v| [k.split('_').collect(&:capitalize).join('-'), v] }
+ .sort
+ .flatten]
+ end
+
+ def http_headers_env(headers)
+ Hash[*headers
+ .collect { |k, v| [k.split('-').collect(&:upcase).join('_'), v] }
+ .collect { |k, v| [k.prepend('HTTP_'), v] }
+ .flatten]
+ end
+
+ def log_request(response)
+ @@logged_requests.push(response)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 70a403652e7..112d4939582 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -48,9 +48,9 @@ module Gitlab
deploy_keys: DeployKey.count,
deployments: Deployment.count,
environments: ::Environment.count,
- gcp_clusters: ::Gcp::Cluster.count,
- gcp_clusters_enabled: ::Gcp::Cluster.enabled.count,
- gcp_clusters_disabled: ::Gcp::Cluster.disabled.count,
+ clusters: ::Clusters::Cluster.count,
+ clusters_enabled: ::Clusters::Cluster.enabled.count,
+ clusters_disabled: ::Clusters::Cluster.disabled.count,
in_review_folder: ::Environment.in_review_folder.count,
groups: Group.count,
issues: Issue.count,
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index 58d5b0da1c4..e1219df1b25 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -16,14 +16,15 @@ module Gitlab
SECRET_LENGTH = 32
class << self
- def git_http_ok(repository, is_wiki, user, action)
+ def git_http_ok(repository, is_wiki, user, action, show_all_refs: false)
project = repository.project
repo_path = repository.path_to_repo
params = {
GL_ID: Gitlab::GlId.gl_id(user),
GL_REPOSITORY: Gitlab::GlRepository.gl_repository(project, is_wiki),
GL_USERNAME: user&.username,
- RepoPath: repo_path
+ RepoPath: repo_path,
+ ShowAllRefs: show_all_refs
}
server = {
address: Gitlab::GitalyClient.address(project.repository_storage),
diff --git a/lib/google_api/cloud_platform/client.rb b/lib/google_api/cloud_platform/client.rb
index a440a3e3562..9242cbe840c 100644
--- a/lib/google_api/cloud_platform/client.rb
+++ b/lib/google_api/cloud_platform/client.rb
@@ -3,7 +3,6 @@ require 'google/apis/container_v1'
module GoogleApi
module CloudPlatform
class Client < GoogleApi::Auth
- DEFAULT_MACHINE_TYPE = 'n1-standard-1'.freeze
SCOPE = 'https://www.googleapis.com/auth/cloud-platform'.freeze
LEAST_TOKEN_LIFE_TIME = 10.minutes
diff --git a/lib/system_check/app/git_user_default_ssh_config_check.rb b/lib/system_check/app/git_user_default_ssh_config_check.rb
index 9af21078403..ad41760dff2 100644
--- a/lib/system_check/app/git_user_default_ssh_config_check.rb
+++ b/lib/system_check/app/git_user_default_ssh_config_check.rb
@@ -11,10 +11,10 @@ module SystemCheck
].freeze
set_name 'Git user has default SSH configuration?'
- set_skip_reason 'skipped (GitLab read-only, or git user is not present / configured)'
+ set_skip_reason 'skipped (git user is not present / configured)'
def skip?
- Gitlab::Database.read_only? || !home_dir || !File.directory?(home_dir)
+ !home_dir || !File.directory?(home_dir)
end
def check?
diff --git a/lib/system_check/app/ruby_version_check.rb b/lib/system_check/app/ruby_version_check.rb
index 08a2c495bd4..57bbabece1f 100644
--- a/lib/system_check/app/ruby_version_check.rb
+++ b/lib/system_check/app/ruby_version_check.rb
@@ -5,7 +5,7 @@ module SystemCheck
set_check_pass -> { "yes (#{self.current_version})" }
def self.required_version
- @required_version ||= Gitlab::VersionInfo.new(2, 3, 3)
+ @required_version ||= Gitlab::VersionInfo.new(2, 3, 5)
end
def self.current_version
diff --git a/lib/tasks/gitlab/dev.rake b/lib/tasks/gitlab/dev.rake
index 930b4bc13e2..ba221e44e5d 100644
--- a/lib/tasks/gitlab/dev.rake
+++ b/lib/tasks/gitlab/dev.rake
@@ -5,10 +5,9 @@ namespace :gitlab do
opts =
if ENV['CI']
{
- # We don't use CI_REPOSITORY_URL since it includes `gitlab-ci-token:xxxxxxxxxxxxxxxxxxxx@`
- # which is confusing in the steps suggested in the job's output.
- ce_repo: "#{ENV['CI_PROJECT_URL']}.git",
- branch: ENV['CI_COMMIT_REF_NAME']
+ ce_project_url: ENV['CI_PROJECT_URL'],
+ branch: ENV['CI_COMMIT_REF_NAME'],
+ job_id: ENV['CI_JOB_ID']
}
else
unless args[:branch]
diff --git a/lib/tasks/gitlab/users.rake b/lib/tasks/gitlab/users.rake
deleted file mode 100644
index 3a16ace60bd..00000000000
--- a/lib/tasks/gitlab/users.rake
+++ /dev/null
@@ -1,11 +0,0 @@
-namespace :gitlab do
- namespace :users do
- desc "GitLab | Clear the authentication token for all users"
- task clear_all_authentication_tokens: :environment do |t, args|
- # Do small batched updates because these updates will be slow and locking
- User.select(:id).find_in_batches(batch_size: 100) do |batch|
- User.where(id: batch.map(&:id)).update_all(authentication_token: nil)
- end
- end
- end
-end
diff --git a/lib/tasks/tokens.rake b/lib/tasks/tokens.rake
index ad1818ff1fa..693597afdf8 100644
--- a/lib/tasks/tokens.rake
+++ b/lib/tasks/tokens.rake
@@ -1,12 +1,7 @@
require_relative '../../app/models/concerns/token_authenticatable.rb'
namespace :tokens do
- desc "Reset all GitLab user auth tokens"
- task reset_all_auth: :environment do
- reset_all_users_token(:reset_authentication_token!)
- end
-
- desc "Reset all GitLab email tokens"
+ desc "Reset all GitLab incoming email tokens"
task reset_all_email: :environment do
reset_all_users_token(:reset_incoming_email_token!)
end
@@ -31,11 +26,6 @@ class TmpUser < ActiveRecord::Base
self.table_name = 'users'
- def reset_authentication_token!
- write_new_token(:authentication_token)
- save!(validate: false)
- end
-
def reset_incoming_email_token!
write_new_token(:incoming_email_token)
save!(validate: false)
diff --git a/package.json b/package.json
index 057cd8f7bc7..e607981143d 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
"webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js"
},
"dependencies": {
+ "autosize": "^4.0.0",
"axios": "^0.16.2",
"babel-core": "^6.22.1",
"babel-eslint": "^7.2.1",
@@ -35,6 +36,7 @@
"eslint-plugin-html": "^2.0.1",
"exports-loader": "^0.6.4",
"file-loader": "^0.11.1",
+ "fuzzaldrin-plus": "^0.5.0",
"imports-loader": "^0.7.1",
"jed": "^1.1.1",
"jquery": "^2.2.1",
@@ -62,10 +64,10 @@
"underscore": "^1.8.3",
"url-loader": "^0.5.8",
"visibilityjs": "^1.2.4",
- "vue": "^2.2.6",
+ "vue": "^2.5.2",
"vue-loader": "^11.3.4",
"vue-resource": "^1.3.4",
- "vue-template-compiler": "^2.2.6",
+ "vue-template-compiler": "^2.5.2",
"vuex": "^3.0.0",
"webpack": "^3.5.5",
"webpack-bundle-analyzer": "^2.8.2",
diff --git a/qa/qa.rb b/qa/qa.rb
index 59d9dd131c2..e8689a44f4d 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -8,6 +8,7 @@ module QA
autoload :Release, 'qa/runtime/release'
autoload :User, 'qa/runtime/user'
autoload :Namespace, 'qa/runtime/namespace'
+ autoload :Scenario, 'qa/runtime/scenario'
end
##
@@ -80,6 +81,11 @@ module QA
module Admin
autoload :Menu, 'qa/page/admin/menu'
end
+
+ module Mattermost
+ autoload :Main, 'qa/page/mattermost/main'
+ autoload :Login, 'qa/page/mattermost/login'
+ end
end
##
diff --git a/qa/qa/page/mattermost/login.rb b/qa/qa/page/mattermost/login.rb
new file mode 100644
index 00000000000..2001dc5b230
--- /dev/null
+++ b/qa/qa/page/mattermost/login.rb
@@ -0,0 +1,19 @@
+module QA
+ module Page
+ module Mattermost
+ class Login < Page::Base
+ def initialize
+ visit(Runtime::Scenario.mattermost + '/login')
+ end
+
+ def sign_in_using_oauth
+ click_link class: 'btn btn-custom-login gitlab'
+
+ if page.has_content?('Authorize GitLab Mattermost to use your account?')
+ click_button 'Authorize'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/mattermost/main.rb b/qa/qa/page/mattermost/main.rb
new file mode 100644
index 00000000000..e636d7676f4
--- /dev/null
+++ b/qa/qa/page/mattermost/main.rb
@@ -0,0 +1,11 @@
+module QA
+ module Page
+ module Mattermost
+ class Main < Page::Base
+ def initialize
+ visit(Runtime::Scenario.mattermost)
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/runtime/scenario.rb b/qa/qa/runtime/scenario.rb
new file mode 100644
index 00000000000..0c5e9787e17
--- /dev/null
+++ b/qa/qa/runtime/scenario.rb
@@ -0,0 +1,8 @@
+module QA
+ module Runtime
+ module Scenario
+ extend self
+ attr_accessor :mattermost
+ end
+ end
+end
diff --git a/qa/qa/scenario/test/integration/mattermost.rb b/qa/qa/scenario/test/integration/mattermost.rb
index 4732f2b635b..9a84e5c8fd8 100644
--- a/qa/qa/scenario/test/integration/mattermost.rb
+++ b/qa/qa/scenario/test/integration/mattermost.rb
@@ -8,6 +8,11 @@ module QA
#
class Mattermost < Scenario::Entrypoint
tags :core, :mattermost
+
+ def perform(address, mattermost, *files)
+ Runtime::Scenario.mattermost = mattermost
+ super(address, files)
+ end
end
end
end
diff --git a/qa/qa/specs/features/mattermost/login_spec.rb b/qa/qa/specs/features/mattermost/login_spec.rb
new file mode 100644
index 00000000000..a89a6a3d1cf
--- /dev/null
+++ b/qa/qa/specs/features/mattermost/login_spec.rb
@@ -0,0 +1,12 @@
+module QA
+ feature 'logging in to Mattermost', :mattermost do
+ scenario 'can use gitlab oauth' do
+ Page::Main::Entry.act { sign_in_using_credentials }
+ Page::Mattermost::Login.act { sign_in_using_oauth }
+
+ Page::Mattermost::Main.perform do |page|
+ expect(page).to have_content(/(Welcome to: Mattermost|Logout GitLab Mattermost)/)
+ end
+ end
+ end
+end
diff --git a/qa/spec/scenario/entrypoint_spec.rb b/qa/spec/scenario/entrypoint_spec.rb
new file mode 100644
index 00000000000..3fd068b641c
--- /dev/null
+++ b/qa/spec/scenario/entrypoint_spec.rb
@@ -0,0 +1,46 @@
+describe QA::Scenario::Entrypoint do
+ subject do
+ Class.new(QA::Scenario::Entrypoint) do
+ tags :rspec
+ end
+ end
+
+ context '#perform' do
+ let(:config) { spy('Specs::Config') }
+ let(:release) { spy('Runtime::Release') }
+ let(:runner) { spy('Specs::Runner') }
+
+ before do
+ allow(config).to receive(:perform) { |&block| block.call config }
+ allow(runner).to receive(:perform) { |&block| block.call runner }
+
+ stub_const('QA::Specs::Config', config)
+ stub_const('QA::Runtime::Release', release)
+ stub_const('QA::Specs::Runner', runner)
+ end
+
+ it 'should set address' do
+ subject.perform("hello")
+
+ expect(config).to have_received(:address=).with("hello")
+ end
+
+ context 'no paths' do
+ it 'should call runner with default arguments' do
+ subject.perform("test")
+
+ expect(runner).to have_received(:rspec)
+ .with(hash_including(files: 'qa/specs/features'))
+ end
+ end
+
+ context 'specifying paths' do
+ it 'should call runner with paths' do
+ subject.perform('test', 'path1', 'path2')
+
+ expect(runner).to have_received(:rspec)
+ .with(hash_including(files: %w(path1 path2)))
+ end
+ end
+ end
+end
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index 6802b839eaa..b73ca0c2346 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -50,70 +50,36 @@ describe ApplicationController do
end
end
- describe "#authenticate_user_from_token!" do
- describe "authenticating a user from a private token" do
- controller(described_class) do
- def index
- render text: "authenticated"
- end
- end
-
- context "when the 'private_token' param is populated with the private token" do
- it "logs the user in" do
- get :index, private_token: user.private_token
- expect(response).to have_gitlab_http_status(200)
- expect(response.body).to eq("authenticated")
- end
- end
-
- context "when the 'PRIVATE-TOKEN' header is populated with the private token" do
- it "logs the user in" do
- @request.headers['PRIVATE-TOKEN'] = user.private_token
- get :index
- expect(response).to have_gitlab_http_status(200)
- expect(response.body).to eq("authenticated")
- end
- end
-
- it "doesn't log the user in otherwise" do
- @request.headers['PRIVATE-TOKEN'] = "token"
- get :index, private_token: "token", authenticity_token: "token"
- expect(response.status).not_to eq(200)
- expect(response.body).not_to eq("authenticated")
+ describe "#authenticate_user_from_personal_access_token!" do
+ controller(described_class) do
+ def index
+ render text: 'authenticated'
end
end
- describe "authenticating a user from a personal access token" do
- controller(described_class) do
- def index
- render text: 'authenticated'
- end
- end
-
- let(:personal_access_token) { create(:personal_access_token, user: user) }
+ let(:personal_access_token) { create(:personal_access_token, user: user) }
- context "when the 'personal_access_token' param is populated with the personal access token" do
- it "logs the user in" do
- get :index, private_token: personal_access_token.token
- expect(response).to have_gitlab_http_status(200)
- expect(response.body).to eq('authenticated')
- end
+ context "when the 'personal_access_token' param is populated with the personal access token" do
+ it "logs the user in" do
+ get :index, private_token: personal_access_token.token
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.body).to eq('authenticated')
end
+ end
- context "when the 'PERSONAL_ACCESS_TOKEN' header is populated with the personal access token" do
- it "logs the user in" do
- @request.headers["PRIVATE-TOKEN"] = personal_access_token.token
- get :index
- expect(response).to have_gitlab_http_status(200)
- expect(response.body).to eq('authenticated')
- end
+ context "when the 'PERSONAL_ACCESS_TOKEN' header is populated with the personal access token" do
+ it "logs the user in" do
+ @request.headers["PRIVATE-TOKEN"] = personal_access_token.token
+ get :index
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.body).to eq('authenticated')
end
+ end
- it "doesn't log the user in otherwise" do
- get :index, private_token: "token"
- expect(response.status).not_to eq(200)
- expect(response.body).not_to eq('authenticated')
- end
+ it "doesn't log the user in otherwise" do
+ get :index, private_token: "token"
+ expect(response.status).not_to eq(200)
+ expect(response.body).not_to eq('authenticated')
end
end
@@ -152,11 +118,15 @@ describe ApplicationController do
end
end
+ before do
+ sign_in user
+ end
+
context 'when format is handled' do
let(:requested_format) { :json }
it 'returns 200 response' do
- get :index, private_token: user.private_token, format: requested_format
+ get :index, format: requested_format
expect(response).to have_gitlab_http_status 200
end
@@ -164,7 +134,7 @@ describe ApplicationController do
context 'when format is not handled' do
it 'returns 404 response' do
- get :index, private_token: user.private_token
+ get :index
expect(response).to have_gitlab_http_status 404
end
diff --git a/spec/controllers/concerns/lfs_request_spec.rb b/spec/controllers/concerns/lfs_request_spec.rb
new file mode 100644
index 00000000000..33b23db302a
--- /dev/null
+++ b/spec/controllers/concerns/lfs_request_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+describe LfsRequest do
+ include ProjectForksHelper
+
+ controller(Projects::GitHttpClientController) do
+ # `described_class` is not available in this context
+ include LfsRequest # rubocop:disable RSpec/DescribedClass
+
+ def show
+ storage_project
+
+ render nothing: true
+ end
+
+ def project
+ @project ||= Project.find(params[:id])
+ end
+
+ def download_request?
+ true
+ end
+
+ def ci?
+ false
+ end
+ end
+
+ let(:project) { create(:project, :public) }
+
+ before do
+ stub_lfs_setting(enabled: true)
+ end
+
+ describe '#storage_project' do
+ it 'assigns the project as storage project' do
+ get :show, id: project.id
+
+ expect(assigns(:storage_project)).to eq(project)
+ end
+
+ it 'assigns the source of a forked project' do
+ forked_project = fork_project(project)
+
+ get :show, id: forked_project.id
+
+ expect(assigns(:storage_project)).to eq(project)
+ end
+ end
+end
diff --git a/spec/controllers/metrics_controller_spec.rb b/spec/controllers/metrics_controller_spec.rb
index 7b0976e3e67..4aed2a25baa 100644
--- a/spec/controllers/metrics_controller_spec.rb
+++ b/spec/controllers/metrics_controller_spec.rb
@@ -59,17 +59,6 @@ describe MetricsController do
expect(response.body).to match(/^redis_shared_state_ping_latency_seconds [0-9\.]+$/)
end
- it 'returns file system check metrics' do
- get :index
-
- expect(response.body).to match(/^filesystem_access_latency_seconds{shard="default"} [0-9\.]+$/)
- expect(response.body).to match(/^filesystem_accessible{shard="default"} 1$/)
- expect(response.body).to match(/^filesystem_write_latency_seconds{shard="default"} [0-9\.]+$/)
- expect(response.body).to match(/^filesystem_writable{shard="default"} 1$/)
- expect(response.body).to match(/^filesystem_read_latency_seconds{shard="default"} [0-9\.]+$/)
- expect(response.body).to match(/^filesystem_readable{shard="default"} 1$/)
- end
-
context 'prometheus metrics are disabled' do
before do
allow(Gitlab::Metrics).to receive(:prometheus_metrics_enabled?).and_return(false)
diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb
index 5da4be2d319..f64b493c283 100644
--- a/spec/controllers/projects/clusters_controller_spec.rb
+++ b/spec/controllers/projects/clusters_controller_spec.rb
@@ -1,68 +1,108 @@
require 'spec_helper'
describe Projects::ClustersController do
- set(:user) { create(:user) }
- set(:project) { create(:project) }
- let(:role) { :master }
+ include AccessMatchersForController
+ include GoogleApi::CloudPlatformHelpers
- before do
- project.team << [user, role]
+ describe 'GET index' do
+ describe 'functionality' do
+ let(:user) { create(:user) }
- sign_in(user)
- end
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
- describe 'GET index' do
- subject do
- get :index, namespace_id: project.namespace,
- project_id: project
- end
+ context 'when project has a cluster' do
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
- context 'when cluster is already created' do
- let!(:cluster) { create(:gcp_cluster, :created_on_gke, project: project) }
+ it { expect(go).to redirect_to(project_cluster_path(project, project.cluster)) }
+ end
- it 'redirects to show a cluster' do
- subject
+ context 'when project does not have a cluster' do
+ let(:project) { create(:project) }
- expect(response).to redirect_to(project_cluster_path(project, cluster))
+ it { expect(go).to redirect_to(new_project_cluster_path(project)) }
end
end
- context 'when we do not have cluster' do
- it 'redirects to create a cluster' do
- subject
+ describe 'security' do
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
- expect(response).to redirect_to(new_project_cluster_path(project))
- end
+ def go
+ get :index, namespace_id: project.namespace.to_param, project_id: project
end
end
describe 'GET login' do
- render_views
+ let(:project) { create(:project) }
- subject do
- get :login, namespace_id: project.namespace,
- project_id: project
- end
-
- context 'when we do have omniauth configured' do
- it 'shows login button' do
- subject
+ describe 'functionality' do
+ let(:user) { create(:user) }
- expect(response.body).to include('auth_buttons/signin_with_google')
+ before do
+ project.add_master(user)
+ sign_in(user)
end
- end
- context 'when we do not have omniauth configured' do
- before do
- stub_omniauth_setting(providers: [])
+ context 'when omniauth has been configured' do
+ let(:key) { 'secere-key' }
+
+ let(:session_key_for_redirect_uri) do
+ GoogleApi::CloudPlatform::Client.session_key_for_redirect_uri(key)
+ end
+
+ before do
+ allow(SecureRandom).to receive(:hex).and_return(key)
+ end
+
+ it 'has authorize_url' do
+ go
+
+ expect(assigns(:authorize_url)).to include(key)
+ expect(session[session_key_for_redirect_uri]).to eq(providers_gcp_new_project_clusters_url(project))
+ end
end
- it 'shows notice message' do
- subject
+ context 'when omniauth has not configured' do
+ before do
+ stub_omniauth_setting(providers: [])
+ end
- expect(response.body).to include('Ask your GitLab administrator if you want to use this service.')
+ it 'does not have authorize_url' do
+ go
+
+ expect(assigns(:authorize_url)).to be_nil
+ end
end
end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+
+ def go
+ get :login, namespace_id: project.namespace, project_id: project
+ end
end
shared_examples 'requires to login' do
@@ -74,235 +114,416 @@ describe Projects::ClustersController do
end
describe 'GET new_gcp' do
- render_views
+ let(:project) { create(:project) }
- subject do
- get :new_gcp, namespace_id: project.namespace,
- project_id: project
- end
+ describe 'functionality' do
+ let(:user) { create(:user) }
- context 'when logged' do
before do
- make_logged_in
+ project.add_master(user)
+ sign_in(user)
end
- it 'shows a creation form' do
- subject
+ context 'when access token is valid' do
+ before do
+ stub_google_api_validate_token
+ end
+
+ it 'has new object' do
+ go
- expect(response.body).to include('Create cluster')
+ expect(assigns(:cluster)).to be_an_instance_of(Clusters::Cluster)
+ end
+ end
+
+ context 'when access token is expired' do
+ before do
+ stub_google_api_expired_token
+ end
+
+ it { expect(go).to redirect_to(login_project_clusters_path(project)) }
+ end
+
+ context 'when access token is not stored in session' do
+ it { expect(go).to redirect_to(login_project_clusters_path(project)) }
end
end
- context 'when not logged' do
- it_behaves_like 'requires to login'
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+
+ def go
+ get :new_gcp, namespace_id: project.namespace, project_id: project
end
end
describe 'POST create' do
- subject do
- post :create, params.merge(namespace_id: project.namespace,
- project_id: project)
+ let(:project) { create(:project) }
+
+ let(:params) do
+ {
+ cluster: {
+ name: 'new-cluster',
+ provider_type: :gcp,
+ provider_gcp_attributes: {
+ gcp_project_id: '111'
+ }
+ }
+ }
end
- context 'when not logged' do
- let(:params) { {} }
-
- it_behaves_like 'requires to login'
- end
+ describe 'functionality' do
+ let(:user) { create(:user) }
- context 'when logged in' do
before do
- make_logged_in
+ project.add_master(user)
+ sign_in(user)
end
- context 'when all required parameters are set' do
- let(:params) do
- {
- cluster: {
- gcp_cluster_name: 'new-cluster',
- gcp_project_id: '111'
- }
- }
- end
-
+ context 'when access token is valid' do
before do
- expect(ClusterProvisionWorker).to receive(:perform_async) { }
+ stub_google_api_validate_token
end
- it 'creates a new cluster' do
- expect { subject }.to change { Gcp::Cluster.count }
-
- expect(response).to redirect_to(project_cluster_path(project, project.cluster))
+ context 'when creates a cluster on gke' do
+ it 'creates a new cluster' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+ expect { go }.to change { Clusters::Cluster.count }
+ expect(response).to redirect_to(project_cluster_path(project, project.cluster))
+ end
end
- end
- context 'when not all required parameters are set' do
- render_views
+ # TODO: Activate in 10.3
+ # context 'when adds a cluster manually' do
+ # let(:params) do
+ # {
+ # cluster: {
+ # name: 'new-cluster',
+ # platform_type: :kubernetes,
+ # provider_type: :user,
+ # platform_kubernetes_attributes: {
+ # namespace: 'custom-namespace',
+ # api_url: 'https://111.111.111.111',
+ # token: 'token'
+ # }
+ # }
+ # }
+ # end
+
+ # it 'creates a new cluster' do
+ # expect(ClusterProvisionWorker).to receive(:perform_async)
+ # expect { go }.to change { Clusters::Cluster.count }
+ # expect(response).to redirect_to(project_cluster_path(project, project.cluster))
+ # end
+ # end
+
+ # TODO: We should fix this in 10.2
+ # Maybe
+ # - validates :provider_gcp, presence: true, if: :gcp?
+ # - validates :provider_type, presence: true
+ # are required in Clusters::Cluster
+ # context 'when not all required parameters are set' do
+ # let(:params) do
+ # {
+ # cluster: {
+ # name: 'new-cluster'
+ # }
+ # }
+ # end
+
+ # it 'shows an error message' do
+ # expect { go }.not_to change { Clusters::Cluster.count }
+ # expect(assigns(:cluster).errors).not_to be_empty
+ # expect(response).to render_template(:new)
+ # end
+ # end
+ end
- let(:params) do
- {
- cluster: {
- project_namespace: 'some namespace'
- }
- }
+ context 'when access token is expired' do
+ before do
+ stub_google_api_expired_token
end
- it 'shows an error message' do
- expect { subject }.not_to change { Gcp::Cluster.count }
+ it 'redirects to login page' do
+ expect(go).to redirect_to(login_project_clusters_path(project))
+ end
+ end
- expect(response).to render_template(:new_gcp)
+ context 'when access token is not stored in session' do
+ it 'redirects to login page' do
+ expect(go).to redirect_to(login_project_clusters_path(project))
end
end
end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+
+ def go
+ post :create, params.merge(namespace_id: project.namespace, project_id: project)
+ end
end
describe 'GET status' do
- let(:cluster) { create(:gcp_cluster, :created_on_gke, project: project) }
+ let(:cluster) { create(:cluster, :project, :providing_by_gcp) }
+ let(:project) { cluster.project }
+
+ describe 'functionality' do
+ let(:user) { create(:user) }
- subject do
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ it "responds with matching schema" do
+ go
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('cluster_status')
+ end
+ end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+
+ def go
get :status, namespace_id: project.namespace,
project_id: project,
id: cluster,
format: :json
end
-
- it "responds with matching schema" do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('cluster_status')
- end
end
describe 'GET show' do
- render_views
-
- let(:cluster) { create(:gcp_cluster, :created_on_gke, project: project) }
-
- subject do
- get :show, namespace_id: project.namespace,
- project_id: project,
- id: cluster
- end
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
- context 'when logged as master' do
- it "allows to update cluster" do
- subject
+ describe 'functionality' do
+ let(:user) { create(:user) }
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.body).to include("Save")
+ before do
+ project.add_master(user)
+ sign_in(user)
end
- it "allows remove integration" do
- subject
+ it "renders view" do
+ go
expect(response).to have_gitlab_http_status(:ok)
- expect(response.body).to include("Remove integration")
+ expect(assigns(:cluster)).to eq(cluster)
end
end
- context 'when logged as developer' do
- let(:role) { :developer }
-
- it "does not allow to access page" do
- subject
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
- expect(response).to have_gitlab_http_status(:not_found)
- end
+ def go
+ get :show, namespace_id: project.namespace,
+ project_id: project,
+ id: cluster
end
end
describe 'PUT update' do
- render_views
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
- let(:service) { project.build_kubernetes_service }
- let(:cluster) { create(:gcp_cluster, :created_on_gke, project: project, service: service) }
- let(:params) { {} }
+ describe 'functionality' do
+ let(:user) { create(:user) }
- subject do
- put :update, params.merge(namespace_id: project.namespace,
- project_id: project,
- id: cluster)
- end
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
- context 'when logged as master' do
- context 'when valid params are used' do
+ context 'when update enabled' do
let(:params) do
{
cluster: { enabled: false }
}
end
- it "redirects back to show page" do
- subject
+ it "updates and redirects back to show page" do
+ go
+ cluster.reload
expect(response).to redirect_to(project_cluster_path(project, project.cluster))
expect(flash[:notice]).to eq('Cluster was successfully updated.')
+ expect(cluster.enabled).to be_falsey
end
- end
- context 'when invalid params are used' do
- let(:params) do
- {
- cluster: { project_namespace: 'my Namespace 321321321 #' }
- }
- end
+ context 'when cluster is being created' do
+ let(:cluster) { create(:cluster, :project, :providing_by_gcp) }
- it "rejects changes" do
- subject
+ it "rejects changes" do
+ go
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to render_template(:show)
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:show)
+ expect(cluster.enabled).to be_truthy
+ end
end
end
+
+ # TODO: Activate in 10.3
+ # context 'when update namespace' do
+ # let(:namespace) { 'namespace-123' }
+
+ # let(:params) do
+ # {
+ # cluster: {
+ # platform_kubernetes_attributes: {
+ # namespace: namespace
+ # }
+ # }
+ # }
+ # end
+
+ # it "updates and redirects back to show page" do
+ # go
+
+ # cluster.reload
+ # expect(response).to redirect_to(project_cluster_path(project, project.cluster))
+ # expect(flash[:notice]).to eq('Cluster was successfully updated.')
+ # expect(cluster.platform.namespace).to eq(namespace)
+ # end
+
+ # context 'when namespace is invalid' do
+ # let(:namespace) { 'my Namespace 321321321 #' }
+
+ # it "rejects changes" do
+ # go
+
+ # expect(response).to have_gitlab_http_status(:ok)
+ # expect(response).to render_template(:show)
+ # expect(cluster.platform.namespace).not_to eq(namespace)
+ # end
+ # end
+ # end
end
- context 'when logged as developer' do
- let(:role) { :developer }
+ describe 'security' do
+ let(:params) do
+ {
+ cluster: { enabled: false }
+ }
+ end
- it "does not allow to update cluster" do
- subject
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
- expect(response).to have_gitlab_http_status(:not_found)
- end
+ def go
+ put :update, params.merge(namespace_id: project.namespace,
+ project_id: project,
+ id: cluster)
end
end
describe 'delete update' do
- let(:cluster) { create(:gcp_cluster, :created_on_gke, project: project) }
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
- subject do
- delete :destroy, namespace_id: project.namespace,
- project_id: project,
- id: cluster
- end
+ describe 'functionality' do
+ let(:user) { create(:user) }
- context 'when logged as master' do
- it "redirects back to clusters list" do
- subject
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ it "destroys and redirects back to clusters list" do
+ expect { go }
+ .to change { Clusters::Cluster.count }.by(-1)
+ .and change { Clusters::Platforms::Kubernetes.count }.by(-1)
+ .and change { Clusters::Providers::Gcp.count }.by(-1)
expect(response).to redirect_to(project_clusters_path(project))
expect(flash[:notice]).to eq('Cluster integration was successfully removed.')
end
- end
- context 'when logged as developer' do
- let(:role) { :developer }
+ context 'when cluster is being created' do
+ let(:cluster) { create(:cluster, :project, :providing_by_gcp) }
+
+ it "destroys and redirects back to clusters list" do
+ expect { go }
+ .to change { Clusters::Cluster.count }.by(-1)
+ .and change { Clusters::Providers::Gcp.count }.by(-1)
+
+ expect(response).to redirect_to(project_clusters_path(project))
+ expect(flash[:notice]).to eq('Cluster integration was successfully removed.')
+ end
+ end
+
+ context 'when provider is user' do
+ let(:cluster) { create(:cluster, :project, :provided_by_user) }
- it "does not allow to destroy cluster" do
- subject
+ it "destroys and redirects back to clusters list" do
+ expect { go }
+ .to change { Clusters::Cluster.count }.by(-1)
+ .and change { Clusters::Platforms::Kubernetes.count }.by(-1)
+ .and change { Clusters::Providers::Gcp.count }.by(0)
- expect(response).to have_gitlab_http_status(:not_found)
+ expect(response).to redirect_to(project_clusters_path(project))
+ expect(flash[:notice]).to eq('Cluster integration was successfully removed.')
+ end
end
end
- end
- def make_logged_in
- session[GoogleApi::CloudPlatform::Client.session_key_for_token] = '1234'
- session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] = in_hour.to_i.to_s
- end
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
- def in_hour
- Time.now + 1.hour
+ def go
+ delete :destroy, namespace_id: project.namespace,
+ project_id: project,
+ id: cluster
+ end
end
end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 6f48f091a20..8016176110e 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -248,6 +248,45 @@ describe Projects::IssuesController do
end
end
+ describe 'PUT #update' do
+ subject do
+ put :update,
+ namespace_id: project.namespace,
+ project_id: project,
+ id: issue.to_param,
+ issue: { title: 'New title' }, format: :json
+ end
+
+ before do
+ sign_in(user)
+ end
+
+ context 'when user has access to update issue' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'updates the issue' do
+ subject
+
+ expect(response).to have_http_status(:ok)
+ expect(issue.reload.title).to eq('New title')
+ end
+ end
+
+ context 'when user does not have access to update issue' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'responds with 404' do
+ subject
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+ end
+
describe 'Confidential Issues' do
let(:project) { create(:project_empty_repo, :public) }
let(:assignee) { create(:assignee) }
@@ -557,6 +596,29 @@ describe Projects::IssuesController do
end
end
end
+
+ describe 'GET #edit' do
+ it_behaves_like 'restricted action', success: 200
+
+ def go(id:)
+ get :edit,
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: id
+ end
+ end
+
+ describe 'PUT #update' do
+ it_behaves_like 'restricted action', success: 302
+
+ def go(id:)
+ put :update,
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: id,
+ issue: { title: 'New title' }
+ end
+ end
end
describe 'POST #create' do
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index b7cccddefdd..14021b8ca50 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -83,15 +83,15 @@ describe Projects::MergeRequestsController do
end
describe 'as json' do
- context 'with basic param' do
+ context 'with basic serializer param' do
it 'renders basic MR entity as json' do
- go(basic: true, format: :json)
+ go(serializer: 'basic', format: :json)
expect(response).to match_response_schema('entities/merge_request_basic')
end
end
- context 'without basic param' do
+ context 'without basic serializer param' do
it 'renders the merge request in the json format' do
go(format: :json)
@@ -186,17 +186,23 @@ describe Projects::MergeRequestsController do
end
describe 'PUT update' do
+ def update_merge_request(mr_params, additional_params = {})
+ params = {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: merge_request.iid,
+ merge_request: mr_params
+ }.merge(additional_params)
+
+ put :update, params
+ end
+
context 'changing the assignee' do
it 'limits the attributes exposed on the assignee' do
assignee = create(:user)
project.add_developer(assignee)
- put :update,
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: merge_request.iid,
- merge_request: { assignee_id: assignee.id },
- format: :json
+ update_merge_request({ assignee_id: assignee.id }, format: :json)
body = JSON.parse(response.body)
expect(body['assignee'].keys)
@@ -204,6 +210,20 @@ describe Projects::MergeRequestsController do
end
end
+ context 'when user does not have access to update issue' do
+ before do
+ reporter = create(:user)
+ project.add_reporter(reporter)
+ sign_in(reporter)
+ end
+
+ it 'responds with 404' do
+ update_merge_request(title: 'New title')
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
context 'there is no source project' do
let(:project) { create(:project, :repository) }
let(:forked_project) { fork_project_with_submodules(project) }
@@ -214,13 +234,7 @@ describe Projects::MergeRequestsController do
end
it 'closes MR without errors' do
- post :update,
- namespace_id: project.namespace,
- project_id: project,
- id: merge_request.iid,
- merge_request: {
- state_event: 'close'
- }
+ update_merge_request(state_event: 'close')
expect(response).to redirect_to([merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request])
expect(merge_request.reload.closed?).to be_truthy
@@ -229,13 +243,7 @@ describe Projects::MergeRequestsController do
it 'allows editing of a closed merge request' do
merge_request.close!
- put :update,
- namespace_id: project.namespace,
- project_id: project,
- id: merge_request.iid,
- merge_request: {
- title: 'New title'
- }
+ update_merge_request(title: 'New title')
expect(response).to redirect_to([merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request])
expect(merge_request.reload.title).to eq 'New title'
@@ -244,13 +252,7 @@ describe Projects::MergeRequestsController do
it 'does not allow to update target branch closed merge request' do
merge_request.close!
- put :update,
- namespace_id: project.namespace,
- project_id: project,
- id: merge_request.iid,
- merge_request: {
- target_branch: 'new_branch'
- }
+ update_merge_request(target_branch: 'new_branch')
expect { merge_request.reload.target_branch }.not_to change { merge_request.target_branch }
end
diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb
index 573530d0db0..209979e642d 100644
--- a/spec/controllers/projects/milestones_controller_spec.rb
+++ b/spec/controllers/projects/milestones_controller_spec.rb
@@ -86,4 +86,32 @@ describe Projects::MilestonesController do
expect(last_note).to eq('removed milestone')
end
end
+
+ describe '#promote' do
+ context 'promotion succeeds' do
+ before do
+ group = create(:group)
+ group.add_developer(user)
+ milestone.project.update(namespace: group)
+ end
+
+ it 'shows group milestone' do
+ post :promote, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid
+
+ group_milestone = assigns(:milestone)
+
+ expect(response).to redirect_to(group_milestone_path(project.group, group_milestone.iid))
+ expect(flash[:notice]).to eq('Milestone has been promoted to group milestone.')
+ end
+ end
+
+ context 'promotion fails' do
+ it 'shows project milestone' do
+ post :promote, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid
+
+ expect(response).to redirect_to(project_milestone_path(project, milestone))
+ expect(flash[:alert]).to eq('Promotion failed - Project does not belong to a group.')
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index 1184c55e540..5f5a789d5cc 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -59,6 +59,7 @@ describe Projects::NotesController do
expect(note_json[:id]).to eq(note.id)
expect(note_json[:discussion_html]).not_to be_nil
expect(note_json[:diff_discussion_html]).to be_nil
+ expect(note_json[:discussion_line_code]).to be_nil
end
end
@@ -74,6 +75,7 @@ describe Projects::NotesController do
expect(note_json[:id]).to eq(note.id)
expect(note_json[:discussion_html]).not_to be_nil
expect(note_json[:diff_discussion_html]).not_to be_nil
+ expect(note_json[:discussion_line_code]).not_to be_nil
end
end
@@ -92,6 +94,7 @@ describe Projects::NotesController do
expect(note_json[:id]).to eq(note.id)
expect(note_json[:discussion_html]).not_to be_nil
expect(note_json[:diff_discussion_html]).to be_nil
+ expect(note_json[:discussion_line_code]).to be_nil
end
end
@@ -104,6 +107,20 @@ describe Projects::NotesController do
expect(note_json[:id]).to eq(note.id)
expect(note_json[:discussion_html]).to be_nil
expect(note_json[:diff_discussion_html]).to be_nil
+ expect(note_json[:discussion_line_code]).to be_nil
+ end
+
+ context 'when user cannot read commit' do
+ before do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?).with(user, :download_code, project).and_return(false)
+ end
+
+ it 'renders 404' do
+ get :index, params
+
+ expect(response).to have_gitlab_http_status(404)
+ end
end
end
end
@@ -120,6 +137,7 @@ describe Projects::NotesController do
expect(note_json[:html]).not_to be_nil
expect(note_json[:discussion_html]).to be_nil
expect(note_json[:diff_discussion_html]).to be_nil
+ expect(note_json[:discussion_line_code]).to be_nil
end
end
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index f1ad5dd84ff..1604a2da485 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -46,7 +46,7 @@ describe Projects::PipelinesController do
context 'when performing gitaly calls', :request_store do
it 'limits the Gitaly requests' do
- expect { subject }.to change { Gitlab::GitalyClient.get_request_count }.by(10)
+ expect { subject }.to change { Gitlab::GitalyClient.get_request_count }.by(8)
end
end
end
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index c2b59239af9..cf38066dedc 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -119,7 +119,7 @@ FactoryGirl.define do
finished_at nil
end
- factory :ci_build_tag do
+ trait :tag do
tag true
end
diff --git a/spec/factories/clusters/cluster.rb b/spec/factories/clusters/cluster.rb
new file mode 100644
index 00000000000..802981d47a0
--- /dev/null
+++ b/spec/factories/clusters/cluster.rb
@@ -0,0 +1,39 @@
+FactoryGirl.define do
+ factory :cluster, class: Clusters::Cluster do
+ user
+ name 'test-cluster'
+
+ trait :project do
+ after(:create) do |cluster, evaluator|
+ cluster.projects << create(:project)
+ end
+ end
+
+ trait :provided_by_user do
+ provider_type :user
+ platform_type :kubernetes
+
+ platform_kubernetes do
+ create(:platform_kubernetes, :configured)
+ end
+ end
+
+ trait :provided_by_gcp do
+ provider_type :gcp
+ platform_type :kubernetes
+
+ before(:create) do |cluster, evaluator|
+ cluster.platform_kubernetes = build(:platform_kubernetes, :configured)
+ cluster.provider_gcp = build(:provider_gcp, :created)
+ end
+ end
+
+ trait :providing_by_gcp do
+ provider_type :gcp
+
+ provider_gcp do
+ create(:provider_gcp, :creating)
+ end
+ end
+ end
+end
diff --git a/spec/factories/clusters/platforms/kubernetes.rb b/spec/factories/clusters/platforms/kubernetes.rb
new file mode 100644
index 00000000000..5d4e3e5cf5d
--- /dev/null
+++ b/spec/factories/clusters/platforms/kubernetes.rb
@@ -0,0 +1,20 @@
+FactoryGirl.define do
+ factory :platform_kubernetes, class: Clusters::Platforms::Kubernetes do
+ cluster
+ namespace nil
+ api_url 'https://kubernetes.example.com'
+ token 'a' * 40
+
+ trait :configured do
+ api_url 'https://kubernetes.example.com'
+ token 'a' * 40
+ username 'xxxxxx'
+ password 'xxxxxx'
+
+ after(:create) do |platform_kubernetes, evaluator|
+ pem_file = File.expand_path(Rails.root.join('spec/fixtures/clusters/sample_cert.pem'))
+ platform_kubernetes.ca_cert = File.read(pem_file)
+ end
+ end
+ end
+end
diff --git a/spec/factories/clusters/providers/gcp.rb b/spec/factories/clusters/providers/gcp.rb
new file mode 100644
index 00000000000..13bf50d7b7f
--- /dev/null
+++ b/spec/factories/clusters/providers/gcp.rb
@@ -0,0 +1,32 @@
+FactoryGirl.define do
+ factory :provider_gcp, class: Clusters::Providers::Gcp do
+ cluster
+ gcp_project_id 'test-gcp-project'
+
+ trait :scheduled do
+ access_token 'access_token_123'
+ end
+
+ trait :creating do
+ access_token 'access_token_123'
+
+ after(:build) do |gcp, evaluator|
+ gcp.make_creating('operation-123')
+ end
+ end
+
+ trait :created do
+ endpoint '111.111.111.111'
+
+ after(:build) do |gcp, evaluator|
+ gcp.make_created
+ end
+ end
+
+ trait :errored do
+ after(:build) do |gcp, evaluator|
+ gcp.make_errored('Something wrong')
+ end
+ end
+ end
+end
diff --git a/spec/factories/gcp/cluster.rb b/spec/factories/gcp/cluster.rb
deleted file mode 100644
index 630e40da888..00000000000
--- a/spec/factories/gcp/cluster.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-FactoryGirl.define do
- factory :gcp_cluster, class: Gcp::Cluster do
- project
- user
- enabled true
- gcp_project_id 'gcp-project-12345'
- gcp_cluster_name 'test-cluster'
- gcp_cluster_zone 'us-central1-a'
- gcp_cluster_size 1
- gcp_machine_type 'n1-standard-4'
-
- trait :with_kubernetes_service do
- after(:create) do |cluster, evaluator|
- create(:kubernetes_service, project: cluster.project).tap do |service|
- cluster.update(service: service)
- end
- end
- end
-
- trait :custom_project_namespace do
- project_namespace 'sample-app'
- end
-
- trait :created_on_gke do
- status_event :make_created
- endpoint '111.111.111.111'
- ca_cert 'xxxxxx'
- kubernetes_token 'xxxxxx'
- username 'xxxxxx'
- password 'xxxxxx'
- end
-
- trait :errored do
- status_event :make_errored
- status_reason 'general error'
- end
- end
-end
diff --git a/spec/features/admin/admin_disables_two_factor_spec.rb b/spec/features/admin/admin_disables_two_factor_spec.rb
index 6a97378391b..2abdd3c9ef2 100644
--- a/spec/features/admin/admin_disables_two_factor_spec.rb
+++ b/spec/features/admin/admin_disables_two_factor_spec.rb
@@ -7,7 +7,7 @@ feature 'Admin disables 2FA for a user' do
edit_user(user)
page.within('.two-factor-status') do
- click_link 'Disable'
+ accept_confirm { click_link 'Disable' }
end
page.within('.two-factor-status') do
diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb
index 771fb5253da..a5f22848031 100644
--- a/spec/features/admin/admin_groups_spec.rb
+++ b/spec/features/admin/admin_groups_spec.rb
@@ -152,7 +152,7 @@ feature 'Admin Groups' do
expect(page).to have_content('Developer')
end
- find(:css, 'li', text: current_user.name).find(:css, 'a.btn-remove').click
+ accept_confirm { find(:css, 'li', text: current_user.name).find(:css, 'a.btn-remove').click }
visit group_group_members_path(group)
diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb
index 2e65fcc5231..eec44549a03 100644
--- a/spec/features/admin/admin_hooks_spec.rb
+++ b/spec/features/admin/admin_hooks_spec.rb
@@ -62,14 +62,14 @@ describe 'Admin::Hooks', :js do
it 'from hooks list page' do
visit admin_hooks_path
- expect { click_link 'Remove' }.to change(SystemHook, :count).by(-1)
+ expect { accept_confirm { find(:link, 'Remove').send_keys(:return) } }.to change(SystemHook, :count).by(-1)
end
it 'from hook edit page' do
visit admin_hooks_path
click_link 'Edit'
- expect { click_link 'Remove' }.to change(SystemHook, :count).by(-1)
+ expect { accept_confirm { find(:link, 'Remove').send_keys(:return) } }.to change(SystemHook, :count).by(-1)
end
end
end
diff --git a/spec/features/admin/admin_labels_spec.rb b/spec/features/admin/admin_labels_spec.rb
index a5834056a1d..de406d7d966 100644
--- a/spec/features/admin/admin_labels_spec.rb
+++ b/spec/features/admin/admin_labels_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe 'admin issues labels' do
it 'deletes all labels', :js do
page.within '.labels' do
page.all('.btn-remove').each do |remove|
- remove.click
+ accept_confirm { remove.click }
wait_for_requests
end
end
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index c490dce7ab0..1218ea52227 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -73,7 +73,7 @@ feature 'Admin updates settings' do
context 'sign-in restrictions', :js do
it 'de-activates oauth sign-in source' do
- find('.btn', text: 'GitLab.com').click
+ find('input#application_setting_enabled_oauth_sign_in_sources_[value=gitlab]').send_keys(:return)
expect(find('.btn', text: 'GitLab.com')).not_to have_css('.active')
end
@@ -95,6 +95,29 @@ feature 'Admin updates settings' do
expect(find_field('ED25519 SSH keys').value).to eq(forbidden)
end
+ scenario 'Change Performance Bar settings' do
+ group = create(:group)
+
+ check 'Enable the Performance Bar'
+ fill_in 'Allowed group', with: group.path
+
+ click_on 'Save'
+
+ expect(page).to have_content 'Application settings saved successfully'
+
+ expect(find_field('Enable the Performance Bar')).to be_checked
+ expect(find_field('Allowed group').value).to eq group.path
+
+ uncheck 'Enable the Performance Bar'
+
+ click_on 'Save'
+
+ expect(page).to have_content 'Application settings saved successfully'
+
+ expect(find_field('Enable the Performance Bar')).not_to be_checked
+ expect(find_field('Allowed group').value).to be_nil
+ end
+
def check_all_events
page.check('Active')
page.check('Push')
diff --git a/spec/features/admin/admin_users_impersonation_tokens_spec.rb b/spec/features/admin/admin_users_impersonation_tokens_spec.rb
index 388d30828a7..e16eae219a4 100644
--- a/spec/features/admin/admin_users_impersonation_tokens_spec.rb
+++ b/spec/features/admin/admin_users_impersonation_tokens_spec.rb
@@ -24,7 +24,7 @@ describe 'Admin > Users > Impersonation Tokens', :js do
fill_in "Name", with: name
# Set date to 1st of next month
- find_field("Expires at").trigger('focus')
+ find_field("Expires at").click
find(".pika-next").click
click_on "1"
@@ -60,7 +60,7 @@ describe 'Admin > Users > Impersonation Tokens', :js do
it "allows revocation of an active impersonation token" do
visit admin_user_impersonation_tokens_path(user_id: user.username)
- click_on "Revoke"
+ accept_confirm { click_on "Revoke" }
expect(page).to have_selector(".settings-message")
expect(no_personal_access_tokens_message).to have_text("This user has no active Impersonation Tokens.")
diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb
index f9f4bd6f5b9..b47f9055d29 100644
--- a/spec/features/admin/admin_users_spec.rb
+++ b/spec/features/admin/admin_users_spec.rb
@@ -290,7 +290,7 @@ describe "Admin::Users" do
it 'allows group membership to be revoked', :js do
page.within(first('.group_member')) do
- find('.btn-remove').click
+ accept_confirm { find('.btn-remove').click }
end
wait_for_requests
@@ -319,7 +319,7 @@ describe "Admin::Users" do
expect(page).to have_content("Secondary email: #{secondary_email.email}")
- find("#remove_email_#{secondary_email.id}").click
+ accept_confirm { find("#remove_email_#{secondary_email.id}").click }
expect(page).not_to have_content(secondary_email.email)
end
diff --git a/spec/features/admin/admin_uses_repository_checks_spec.rb b/spec/features/admin/admin_uses_repository_checks_spec.rb
index 42f5b5eb8dc..f1ac73ff819 100644
--- a/spec/features/admin/admin_uses_repository_checks_spec.rb
+++ b/spec/features/admin/admin_uses_repository_checks_spec.rb
@@ -37,7 +37,7 @@ feature 'Admin uses repository checks' do
expect(RepositoryCheck::ClearWorker).to receive(:perform_async)
- click_link 'Clear all repository checks'
+ accept_confirm { find(:link, 'Clear all repository checks').send_keys(:return) }
expect(page).to have_content('Started asynchronous removal of all repository check states.')
end
diff --git a/spec/features/atom/dashboard_issues_spec.rb b/spec/features/atom/dashboard_issues_spec.rb
index 5aae2dbaf91..89c9d377003 100644
--- a/spec/features/atom/dashboard_issues_spec.rb
+++ b/spec/features/atom/dashboard_issues_spec.rb
@@ -13,8 +13,10 @@ describe "Dashboard Issues Feed" do
end
describe "atom feed" do
- it "renders atom feed via private token" do
- visit issues_dashboard_path(:atom, private_token: user.private_token)
+ it "renders atom feed via personal access token" do
+ personal_access_token = create(:personal_access_token, user: user)
+
+ visit issues_dashboard_path(:atom, private_token: personal_access_token.token)
expect(response_headers['Content-Type']).to have_content('application/atom+xml')
expect(body).to have_selector('title', text: "#{user.name} issues")
diff --git a/spec/features/atom/dashboard_spec.rb b/spec/features/atom/dashboard_spec.rb
index 321c8a2a670..2c0c331b6db 100644
--- a/spec/features/atom/dashboard_spec.rb
+++ b/spec/features/atom/dashboard_spec.rb
@@ -4,9 +4,11 @@ describe "Dashboard Feed" do
describe "GET /" do
let!(:user) { create(:user, name: "Jonh") }
- context "projects atom feed via private token" do
+ context "projects atom feed via personal access token" do
it "renders projects atom feed" do
- visit dashboard_projects_path(:atom, private_token: user.private_token)
+ personal_access_token = create(:personal_access_token, user: user)
+
+ visit dashboard_projects_path(:atom, private_token: personal_access_token.token)
expect(body).to have_selector('feed title')
end
end
diff --git a/spec/features/atom/issues_spec.rb b/spec/features/atom/issues_spec.rb
index 3eeb4d35131..4102ac0588a 100644
--- a/spec/features/atom/issues_spec.rb
+++ b/spec/features/atom/issues_spec.rb
@@ -28,10 +28,12 @@ describe 'Issues Feed' do
end
end
- context 'when authenticated via private token' do
+ context 'when authenticated via personal access token' do
it 'renders atom feed' do
+ personal_access_token = create(:personal_access_token, user: user)
+
visit project_issues_path(project, :atom,
- private_token: user.private_token)
+ private_token: personal_access_token.token)
expect(response_headers['Content-Type'])
.to have_content('application/atom+xml')
diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb
index 9ce687afb31..2b934d81674 100644
--- a/spec/features/atom/users_spec.rb
+++ b/spec/features/atom/users_spec.rb
@@ -4,9 +4,11 @@ describe "User Feed" do
describe "GET /" do
let!(:user) { create(:user) }
- context 'user atom feed via private token' do
+ context 'user atom feed via personal access token' do
it "renders user atom feed" do
- visit user_path(user, :atom, private_token: user.private_token)
+ personal_access_token = create(:personal_access_token, user: user)
+
+ visit user_path(user, :atom, private_token: personal_access_token.token)
expect(body).to have_selector('feed title')
end
end
diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb
index c480b5b7e34..e4cfcea45a5 100644
--- a/spec/features/boards/add_issues_modal_spec.rb
+++ b/spec/features/boards/add_issues_modal_spec.rb
@@ -101,7 +101,7 @@ describe 'Issue Boards add issue modal', :js do
click_button 'Cancel'
end
- first('.board-delete').click
+ accept_confirm { first('.board-delete').click }
click_button('Add issues')
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 60ed17c0c81..e8d779f5772 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -2,6 +2,7 @@ require 'rails_helper'
describe 'Issue Boards', :js do
include DragTo
+ include MobileHelpers
let(:group) { create(:group, :nested) }
let(:project) { create(:project, :public, namespace: group) }
@@ -13,7 +14,7 @@ describe 'Issue Boards', :js do
project.team << [user, :master]
project.team << [user2, :master]
- page.driver.set_cookie('sidebar_collapsed', 'true')
+ set_cookie('sidebar_collapsed', 'true')
sign_in(user)
end
@@ -135,7 +136,7 @@ describe 'Issue Boards', :js do
it 'allows user to delete board' do
page.within(find('.board:nth-child(2)')) do
- find('.board-delete').click
+ accept_confirm { find('.board-delete').click }
end
wait_for_requests
@@ -150,7 +151,7 @@ describe 'Issue Boards', :js do
find('.dropdown-menu-close').click
page.within(find('.board:nth-child(2)')) do
- find('.board-delete').click
+ accept_confirm { find('.board-delete').click }
end
wait_for_requests
@@ -379,7 +380,7 @@ describe 'Issue Boards', :js do
end
it 'filters by milestone' do
- set_filter("milestone", "\"#{milestone.title}\"")
+ set_filter("milestone", "\"#{milestone.title}")
click_filter_link(milestone.title)
submit_filter
@@ -400,7 +401,7 @@ describe 'Issue Boards', :js do
end
it 'filters by label with space after reload' do
- set_filter("label", "\"#{accepting.title}\"")
+ set_filter("label", "\"#{accepting.title}")
click_filter_link(accepting.title)
submit_filter
@@ -521,7 +522,7 @@ describe 'Issue Boards', :js do
end
it 'allows user to use keyboard shortcuts' do
- find('.boards-list').native.send_keys('i')
+ find('body').native.send_keys('i')
expect(page).to have_content('New Issue')
end
end
@@ -538,7 +539,7 @@ describe 'Issue Boards', :js do
end
it 'does not show create new list' do
- expect(page).not_to have_selector('.js-new-board-list')
+ expect(page).not_to have_button('.js-new-board-list')
end
it 'does not allow dragging' do
@@ -563,6 +564,9 @@ describe 'Issue Boards', :js do
end
def drag(selector: '.board-list', list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0)
+ # ensure there is enough horizontal space for four boards
+ resize_window(2000, 800)
+
drag_to(selector: selector,
scrollable: '#board-app',
list_from_index: list_from_index,
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index 4965f803883..9137ab82ff4 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -51,7 +51,7 @@ describe 'Issue Boards', :js do
expect(page).to have_selector('.issue-boards-sidebar')
- find('.gutter-toggle').trigger('click')
+ find('.gutter-toggle').click
expect(page).not_to have_selector('.issue-boards-sidebar')
end
@@ -171,7 +171,7 @@ describe 'Issue Boards', :js do
end
page.within(find('.board:nth-child(2)')) do
- find('.card:nth-child(2)').trigger('click')
+ find('.card:nth-child(2)').click
end
page.within('.assignee') do
diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb
index 4fc6956d111..a9530becb65 100644
--- a/spec/features/calendar_spec.rb
+++ b/spec/features/calendar_spec.rb
@@ -63,8 +63,8 @@ feature 'Contributions Calendar', :js do
Event.create(note_comment_params)
end
- def selected_day_activities
- find('.user-calendar-activities').text
+ def selected_day_activities(visible: true)
+ find('.user-calendar-activities', visible: visible).text
end
before do
@@ -112,7 +112,7 @@ feature 'Contributions Calendar', :js do
end
it 'hides calendar day activities' do
- expect(selected_day_activities).to be_empty
+ expect(selected_day_activities(visible: false)).to be_empty
end
end
end
diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb
index d5e9de20e59..bef2aa9e0e5 100644
--- a/spec/features/container_registry_spec.rb
+++ b/spec/features/container_registry_spec.rb
@@ -47,7 +47,7 @@ describe "Container Registry", :js do
scenario 'user removes a specific tag from container repository' do
visit_container_registry
- find('.js-toggle-repo').trigger('click')
+ find('.js-toggle-repo').click
wait_for_requests
expect_any_instance_of(ContainerRegistry::Tag)
diff --git a/spec/features/dashboard/group_spec.rb b/spec/features/dashboard/group_spec.rb
index 1213f8c32eb..1c7932e7964 100644
--- a/spec/features/dashboard/group_spec.rb
+++ b/spec/features/dashboard/group_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe 'Dashboard Group' do
it 'creates new group', :js do
visit dashboard_groups_path
- find('.btn-new').trigger('click')
+ find('.btn-new').click
new_path = 'Samurai'
new_description = 'Tokugawa Shogunate'
diff --git a/spec/features/dashboard/groups_list_spec.rb b/spec/features/dashboard/groups_list_spec.rb
index c6873d1923c..d92c002b4e7 100644
--- a/spec/features/dashboard/groups_list_spec.rb
+++ b/spec/features/dashboard/groups_list_spec.rb
@@ -138,7 +138,7 @@ feature 'Dashboard Groups page', :js do
expect(page).not_to have_selector("#group-#{group.id}")
# Go to next page
- find(".gl-pagination .page:not(.active) a").trigger('click')
+ find(".gl-pagination .page:not(.active) a").click
wait_for_requests
diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb
index a8919976c31..5b4c00b3c7e 100644
--- a/spec/features/dashboard/issues_spec.rb
+++ b/spec/features/dashboard/issues_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe 'Dashboard Issues' do
end
it 'shows issues when current user is author', :js do
- find('#assignee_id', visible: false).set('')
+ execute_script("document.querySelector('#assignee_id').value=''")
find('.js-author-search', match: :first).click
expect(find('li[data-user-id="null"] a.is-active')).to be_visible
@@ -71,7 +71,7 @@ RSpec.describe 'Dashboard Issues' do
describe 'new issue dropdown' do
it 'shows projects only with issues feature enabled', :js do
- find('.new-project-item-select-button').trigger('click')
+ find('.new-project-item-select-button').click
page.within('.select2-results') do
expect(page).to have_content(project.name_with_namespace)
@@ -80,7 +80,7 @@ RSpec.describe 'Dashboard Issues' do
end
it 'shows the new issue page', :js do
- find('.new-project-item-select-button').trigger('click')
+ find('.new-project-item-select-button').click
wait_for_requests
@@ -93,7 +93,7 @@ RSpec.describe 'Dashboard Issues' do
find('#select2-drop-mask', visible: false)
execute_script("$('#select2-drop-mask').remove();")
- find('.new-project-item-link').trigger('click')
+ find('.new-project-item-link').click
expect(page).to have_current_path("#{project_path}/issues/new")
diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb
index f01ba442e58..991d360ccaf 100644
--- a/spec/features/dashboard/merge_requests_spec.rb
+++ b/spec/features/dashboard/merge_requests_spec.rb
@@ -25,7 +25,7 @@ feature 'Dashboard Merge Requests' do
end
it 'shows projects only with merge requests feature enabled', :js do
- find('.new-project-item-select-button').trigger('click')
+ find('.new-project-item-select-button').click
page.within('.select2-results') do
expect(page).to have_content(project.name_with_namespace)
diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb
index 01aca443f4a..6f916078b1a 100644
--- a/spec/features/dashboard/todos/todos_spec.rb
+++ b/spec/features/dashboard/todos/todos_spec.rb
@@ -52,7 +52,7 @@ feature 'Dashboard Todos' do
end
it 'updates todo count' do
- expect(page).to have_content 'To do 0'
+ expect(page).to have_content 'Todos 0'
expect(page).to have_content 'Done 1'
end
@@ -81,7 +81,7 @@ feature 'Dashboard Todos' do
end
it 'updates todo count' do
- expect(page).to have_content 'To do 1'
+ expect(page).to have_content 'Todos 1'
expect(page).to have_content 'Done 0'
end
end
@@ -200,7 +200,7 @@ feature 'Dashboard Todos' do
end
it 'updates todo count' do
- expect(page).to have_content 'To do 1'
+ expect(page).to have_content 'Todos 1'
expect(page).to have_content 'Done 0'
end
end
@@ -252,11 +252,11 @@ feature 'Dashboard Todos' do
describe 'mark all as done', :js do
before do
visit dashboard_todos_path
- find('.js-todos-mark-all').trigger('click')
+ find('.js-todos-mark-all').click
end
it 'shows "All done" message!' do
- expect(page).to have_content 'To do 0'
+ expect(page).to have_content 'Todos 0'
expect(page).to have_content "You're all done!"
expect(page).not_to have_selector('.gl-pagination')
end
@@ -283,7 +283,7 @@ feature 'Dashboard Todos' do
it 'updates todo count' do
mark_all_and_undo
- expect(page).to have_content 'To do 2'
+ expect(page).to have_content 'Todos 2'
expect(page).to have_content 'Done 0'
end
@@ -309,9 +309,9 @@ feature 'Dashboard Todos' do
end
def mark_all_and_undo
- find('.js-todos-mark-all').trigger('click')
+ find('.js-todos-mark-all').click
wait_for_requests
- find('.js-todos-undo-all').trigger('click')
+ find('.js-todos-undo-all').click
wait_for_requests
end
end
diff --git a/spec/features/discussion_comments/commit_spec.rb b/spec/features/discussion_comments/commit_spec.rb
index 0375d0bf8ff..69d35cdbc72 100644
--- a/spec/features/discussion_comments/commit_spec.rb
+++ b/spec/features/discussion_comments/commit_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Discussion Comments Merge Request', :js do
+describe 'Discussion Comments Commit', :js do
include RepoHelpers
let(:user) { create(:user) }
diff --git a/spec/features/discussion_comments/snippets_spec.rb b/spec/features/discussion_comments/snippets_spec.rb
index 1e6389d9a13..4a236c4639b 100644
--- a/spec/features/discussion_comments/snippets_spec.rb
+++ b/spec/features/discussion_comments/snippets_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Discussion Comments Issue', :js do
+describe 'Discussion Comments Snippet', :js do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:snippet) { create(:project_snippet, :private, project: project, author: user) }
diff --git a/spec/features/explore/new_menu_spec.rb b/spec/features/explore/new_menu_spec.rb
index c5ec495a418..8d5233d0c0f 100644
--- a/spec/features/explore/new_menu_spec.rb
+++ b/spec/features/explore/new_menu_spec.rb
@@ -65,9 +65,9 @@ feature 'Top Plus Menu', :js do
visit project_path(project)
page.within '.header-content' do
- find('.header-new-dropdown-toggle').trigger('click')
+ find('.header-new-dropdown-toggle').click
expect(page).to have_selector('.header-new.dropdown.open', count: 1)
- find('.header-new-project-snippet a').trigger('click')
+ find('.header-new-project-snippet a').click
end
expect(page).to have_content('New Snippet')
@@ -87,9 +87,9 @@ feature 'Top Plus Menu', :js do
visit group_path(group)
page.within '.header-content' do
- find('.header-new-dropdown-toggle').trigger('click')
+ find('.header-new-dropdown-toggle').click
expect(page).to have_selector('.header-new.dropdown.open', count: 1)
- find('.header-new-group-project a').trigger('click')
+ find('.header-new-group-project a').click
end
expect(page).to have_content('Project path')
@@ -155,7 +155,7 @@ feature 'Top Plus Menu', :js do
def click_topmenuitem(item_name)
page.within '.header-content' do
- find('.header-new-dropdown-toggle').trigger('click')
+ find('.header-new-dropdown-toggle').click
expect(page).to have_selector('.header-new.dropdown.open', count: 1)
click_link item_name
end
diff --git a/spec/features/groups/milestone_spec.rb b/spec/features/groups/milestone_spec.rb
index 12aa54a3da1..1b41b3842c8 100644
--- a/spec/features/groups/milestone_spec.rb
+++ b/spec/features/groups/milestone_spec.rb
@@ -19,9 +19,9 @@ feature 'Group milestones', :js do
end
it 'renders description preview' do
- form = find('.gfm-form')
+ description = find('.note-textarea')
- form.fill_in(:milestone_description, with: '')
+ description.native.send_keys('')
click_link('Preview')
@@ -31,7 +31,7 @@ feature 'Group milestones', :js do
click_link('Write')
- form.fill_in(:milestone_description, with: ':+1: Nice')
+ description.native.send_keys(':+1: Nice')
click_link('Preview')
@@ -51,6 +51,13 @@ feature 'Group milestones', :js do
expect(find('.start_date')).to have_content(Date.today.at_beginning_of_month.strftime('%b %-d, %Y'))
end
+
+ it 'description input does not support autocomplete' do
+ description = find('.note-textarea')
+ description.native.send_keys('!')
+
+ expect(page).not_to have_selector('.atwho-view')
+ end
end
context 'milestones list' do
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index cc8906fa969..c1f3d94bc20 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -65,7 +65,7 @@ feature 'Group' do
end
it 'updates the team URL on graph path update', :js do
- out_span = find('span[data-bind-out="create_chat_team"]')
+ out_span = find('span[data-bind-out="create_chat_team"]', visible: false)
expect(out_span.text).to be_empty
diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb
index 3223eb20b55..fa4d3a55c62 100644
--- a/spec/features/issues/bulk_assignment_labels_spec.rb
+++ b/spec/features/issues/bulk_assignment_labels_spec.rb
@@ -405,7 +405,7 @@ feature 'Issues > Labels bulk assignment' do
end
def update_issues
- find('.update-selected-issues').trigger('click')
+ find('.update-selected-issues').click
wait_for_requests
end
diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
index 1c4649d0ba9..2e4a25ee15d 100644
--- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
@@ -43,15 +43,16 @@ describe 'Dropdown assignee', :js do
end
it 'should show loading indicator when opened' do
- filtered_search.set('assignee:')
+ slow_requests do
+ filtered_search.set('assignee:')
- expect(page).to have_css('#js-dropdown-assignee .filter-dropdown-loading', visible: true)
+ expect(page).to have_css('#js-dropdown-assignee .filter-dropdown-loading', visible: true)
+ end
end
it 'should hide loading indicator when loaded' do
filtered_search.set('assignee:')
- expect(find(js_dropdown_assignee)).to have_css('.filter-dropdown-loading')
expect(find(js_dropdown_assignee)).not_to have_css('.filter-dropdown-loading')
end
diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb
index 5e20fb48768..2fb5e7cdba4 100644
--- a/spec/features/issues/filtered_search/dropdown_author_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb
@@ -51,9 +51,11 @@ describe 'Dropdown author', :js do
end
it 'should show loading indicator when opened' do
- filtered_search.set('author:')
+ slow_requests do
+ filtered_search.set('author:')
- expect(page).to have_css('#js-dropdown-author .filter-dropdown-loading', visible: true)
+ expect(page).to have_css('#js-dropdown-author .filter-dropdown-loading', visible: true)
+ end
end
it 'should hide loading indicator when loaded' do
diff --git a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
index 3012c77f2b9..8db435634fd 100644
--- a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
@@ -70,9 +70,11 @@ describe 'Dropdown emoji', :js do
end
it 'should show loading indicator when opened' do
- filtered_search.set('my-reaction:')
+ slow_requests do
+ filtered_search.set('my-reaction:')
- expect(page).to have_css('#js-dropdown-my-reaction .filter-dropdown-loading', visible: true)
+ expect(page).to have_css('#js-dropdown-my-reaction .filter-dropdown-loading', visible: true)
+ end
end
it 'should hide loading indicator when loaded' do
diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb
index cbc4f8d4c50..18cdb199c70 100644
--- a/spec/features/issues/filtered_search/dropdown_label_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb
@@ -66,9 +66,11 @@ describe 'Dropdown label', :js do
end
it 'shows loading indicator when opened and hides it when loaded' do
- filtered_search.set('label:')
+ slow_requests do
+ filtered_search.set('label:')
- expect(find(js_dropdown_label)).to have_css('.filter-dropdown-loading')
+ expect(page).to have_css("#{js_dropdown_label} .filter-dropdown-loading", visible: true)
+ end
expect(find(js_dropdown_label)).not_to have_css('.filter-dropdown-loading')
end
diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
index f6c2e952bea..031eb06723a 100644
--- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
@@ -50,15 +50,16 @@ describe 'Dropdown milestone', :js do
end
it 'should show loading indicator when opened' do
- filtered_search.set('milestone:')
+ slow_requests do
+ filtered_search.set('milestone:')
- expect(page).to have_css('#js-dropdown-milestone .filter-dropdown-loading', visible: true)
+ expect(page).to have_css('#js-dropdown-milestone .filter-dropdown-loading', visible: true)
+ end
end
it 'should hide loading indicator when loaded' do
filtered_search.set('milestone:')
- expect(find(js_dropdown_milestone)).to have_css('.filter-dropdown-loading')
expect(find(js_dropdown_milestone)).not_to have_css('.filter-dropdown-loading')
end
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index 2974016c6a7..b3c50964810 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -139,7 +139,7 @@ describe 'Filter issues', :js do
input_filtered_search('label:none')
expect_tokens([label_token('none', false)])
- expect_issues_list_count(8)
+ expect_issues_list_count(4)
expect_filtered_search_input_empty
end
diff --git a/spec/features/issues/filtered_search/recent_searches_spec.rb b/spec/features/issues/filtered_search/recent_searches_spec.rb
index eef7988e2bd..f355cec3ba9 100644
--- a/spec/features/issues/filtered_search/recent_searches_spec.rb
+++ b/spec/features/issues/filtered_search/recent_searches_spec.rb
@@ -27,9 +27,8 @@ describe 'Recent searches', :js do
input_filtered_search('foo', submit: true)
input_filtered_search('bar', submit: true)
- items = all('.filtered-search-history-dropdown-item', visible: false)
+ items = all('.filtered-search-history-dropdown-item', visible: false, count: 2)
- expect(items.count).to eq(2)
expect(items[0].text).to eq('bar')
expect(items[1].text).to eq('foo')
end
@@ -38,9 +37,8 @@ describe 'Recent searches', :js do
visit project_issues_path(project_1, label_name: 'foo', search: 'bar')
visit project_issues_path(project_1, label_name: 'qux', search: 'garply')
- items = all('.filtered-search-history-dropdown-item', visible: false)
+ items = all('.filtered-search-history-dropdown-item', visible: false, count: 2)
- expect(items.count).to eq(2)
expect(items[0].text).to eq('label:~qux garply')
expect(items[1].text).to eq('label:~foo bar')
end
@@ -50,9 +48,8 @@ describe 'Recent searches', :js do
visit project_issues_path(project_1, search: 'foo')
- items = all('.filtered-search-history-dropdown-item', visible: false)
+ items = all('.filtered-search-history-dropdown-item', visible: false, count: 3)
- expect(items.count).to eq(3)
expect(items[0].text).to eq('foo')
expect(items[1].text).to eq('saved1')
expect(items[2].text).to eq('saved2')
@@ -69,9 +66,8 @@ describe 'Recent searches', :js do
input_filtered_search('more', submit: true)
input_filtered_search('things', submit: true)
- items = all('.filtered-search-history-dropdown-item', visible: false)
+ items = all('.filtered-search-history-dropdown-item', visible: false, count: 2)
- expect(items.count).to eq(2)
expect(items[0].text).to eq('things')
expect(items[1].text).to eq('more')
end
@@ -80,7 +76,8 @@ describe 'Recent searches', :js do
set_recent_searches(project_1_local_storage_key, '["foo", "bar"]')
visit project_issues_path(project_1)
- all('.filtered-search-history-dropdown-item', visible: false)[0].trigger('click')
+ find('.filtered-search-history-dropdown-toggle-button').click
+ all('.filtered-search-history-dropdown-item', count: 2)[0].click
wait_for_filtered_search('foo')
expect(find('.filtered-search').value.strip).to eq('foo')
@@ -90,12 +87,11 @@ describe 'Recent searches', :js do
set_recent_searches(project_1_local_storage_key, '["foo"]')
visit project_issues_path(project_1)
- items_before = all('.filtered-search-history-dropdown-item', visible: false)
+ find('.filtered-search-history-dropdown-toggle-button').click
+ all('.filtered-search-history-dropdown-item', count: 1)
- expect(items_before.count).to eq(1)
-
- find('.filtered-search-history-clear-button', visible: false).trigger('click')
- items_after = all('.filtered-search-history-dropdown-item', visible: false)
+ find('.filtered-search-history-clear-button').click
+ items_after = all('.filtered-search-history-dropdown-item', count: 0)
expect(items_after.count).to eq(0)
end
diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb
index 920f5546eef..0ae70c855db 100644
--- a/spec/features/issues/filtered_search/visual_tokens_spec.rb
+++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb
@@ -2,7 +2,6 @@ require 'rails_helper'
describe 'Visual tokens', :js do
include FilteredSearchHelpers
- include WaitForRequests
let!(:project) { create(:project) }
let!(:user) { create(:user, name: 'administrator', username: 'root') }
@@ -28,7 +27,7 @@ describe 'Visual tokens', :js do
sign_in(user)
create(:issue, project: project)
- page.driver.set_cookie('sidebar_collapsed', 'true')
+ set_cookie('sidebar_collapsed', 'true')
visit project_issues_path(project)
end
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
index 8ce470fc288..2db6f9a2982 100644
--- a/spec/features/issues/form_spec.rb
+++ b/spec/features/issues/form_spec.rb
@@ -218,15 +218,54 @@ describe 'New/edit issue', :js do
context 'edit issue' do
before do
- visit project_issue_path(project, issue)
- page.within('.content .issuable-actions') do
- click_on 'Edit'
+ visit edit_project_issue_path(project, issue)
+ end
+
+ it 'allows user to update issue' do
+ expect(find('input[name="issue[assignee_ids][]"]', visible: false).value).to match(user.id.to_s)
+ expect(find('input[name="issue[milestone_id]"]', visible: false).value).to match(milestone.id.to_s)
+ expect(find('a', text: 'Assign to me', visible: false)).not_to be_visible
+
+ page.within '.js-user-search' do
+ expect(page).to have_content user.name
+ end
+
+ page.within '.js-milestone-select' do
+ expect(page).to have_content milestone.title
+ end
+
+ click_button 'Labels'
+ page.within '.dropdown-menu-labels' do
+ click_link label.title
+ click_link label2.title
+ end
+ page.within '.js-label-select' do
+ expect(page).to have_content label.title
+ end
+ expect(page.all('input[name="issue[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s)
+ expect(page.all('input[name="issue[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s)
+
+ click_button 'Save changes'
+
+ page.within '.issuable-sidebar' do
+ page.within '.assignee' do
+ expect(page).to have_content user.name
+ end
+
+ page.within '.milestone' do
+ expect(page).to have_content milestone.title
+ end
+
+ page.within '.labels' do
+ expect(page).to have_content label.title
+ expect(page).to have_content label2.title
+ end
end
end
it 'description has autocomplete' do
- find_field('issue-description').native.send_keys('')
- fill_in 'issue-description', with: '@'
+ find('#issue_description').native.send_keys('')
+ fill_in 'issue_description', with: '@'
expect(page).to have_selector('.atwho-view')
end
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index 15041ff04ea..b8a66245153 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -17,9 +17,9 @@ feature 'GFM autocomplete', :js do
it 'updates issue descripton with GFM reference' do
find('.issuable-edit').click
- find('#issue-description').native.send_keys("@#{user.name[0...3]}")
+ simulate_input('#issue-description', "@#{user.name[0...3]}")
- find('.atwho-view .cur').trigger('click')
+ find('.atwho-view .cur').click
click_button 'Save changes'
@@ -28,7 +28,6 @@ feature 'GFM autocomplete', :js do
it 'opens autocomplete menu when field starts with text' do
page.within '.timeline-content-form' do
- find('#note-body').native.send_keys('')
find('#note-body').native.send_keys('@')
end
@@ -46,7 +45,6 @@ feature 'GFM autocomplete', :js do
it 'doesnt select the first item for non-assignee dropdowns' do
page.within '.timeline-content-form' do
- find('#note-body').native.send_keys('')
find('#note-body').native.send_keys(':')
end
@@ -86,7 +84,6 @@ feature 'GFM autocomplete', :js do
it 'selects the first item for assignee dropdowns' do
page.within '.timeline-content-form' do
- find('#note-body').native.send_keys('')
find('#note-body').native.send_keys('@')
end
@@ -100,7 +97,7 @@ feature 'GFM autocomplete', :js do
it 'includes items for assignee dropdowns with non-ASCII characters in name' do
page.within '.timeline-content-form' do
find('#note-body').native.send_keys('')
- find('#note-body').native.send_keys("@#{user.name[0...8]}")
+ simulate_input('#note-body', "@#{user.name[0...8]}")
end
expect(page).to have_selector('.atwho-container')
@@ -112,7 +109,6 @@ feature 'GFM autocomplete', :js do
it 'selects the first item for non-assignee dropdowns if a query is entered' do
page.within '.timeline-content-form' do
- find('#note-body').native.send_keys('')
find('#note-body').native.send_keys(':1')
end
@@ -127,9 +123,8 @@ feature 'GFM autocomplete', :js do
it 'wraps the result in double quotes' do
note = find('#note-body')
page.within '.timeline-content-form' do
- note.native.send_keys('')
- note.native.send_keys("~#{label.title[0]}")
- note.click
+ find('#note-body').native.send_keys('')
+ simulate_input('#note-body', "~#{label.title[0]}")
end
label_item = find('.atwho-view li', text: label.title)
@@ -152,16 +147,13 @@ feature 'GFM autocomplete', :js do
it "does not show dropdown when preceded with a special character" do
note = find('#note-body')
page.within '.timeline-content-form' do
- note.native.send_keys('')
note.native.send_keys("@")
- note.click
end
expect(page).to have_selector('.atwho-container')
page.within '.timeline-content-form' do
note.native.send_keys("@")
- note.click
end
expect(page).to have_selector('.atwho-container', visible: false)
@@ -170,9 +162,7 @@ feature 'GFM autocomplete', :js do
it "does not throw an error if no labels exist" do
note = find('#note-body')
page.within '.timeline-content-form' do
- note.native.send_keys('')
note.native.send_keys('~')
- note.click
end
expect(page).to have_selector('.atwho-container', visible: false)
@@ -181,9 +171,7 @@ feature 'GFM autocomplete', :js do
it 'doesn\'t wrap for assignee values' do
note = find('#note-body')
page.within '.timeline-content-form' do
- note.native.send_keys('')
note.native.send_keys("@#{user.username[0]}")
- note.click
end
user_item = find('.atwho-view li', text: user.username)
@@ -194,9 +182,7 @@ feature 'GFM autocomplete', :js do
it 'doesn\'t wrap for emoji values' do
note = find('#note-body')
page.within '.timeline-content-form' do
- note.native.send_keys('')
- note.native.send_keys(":cartwheel")
- note.click
+ note.native.send_keys(":cartwheel_")
end
emoji_item = find('.atwho-view li', text: 'cartwheel_tone1')
@@ -223,12 +209,11 @@ feature 'GFM autocomplete', :js do
it 'triggers autocomplete after selecting a quick action' do
note = find('#note-body')
page.within '.timeline-content-form' do
- note.native.send_keys('')
note.native.send_keys('/as')
- note.click
end
- find('.atwho-view li', text: '/assign').native.send_keys(:tab)
+ find('.atwho-view li', text: '/assign')
+ note.native.send_keys(:tab)
user_item = find('.atwho-view li', text: user.username)
expect(user_item).to have_content(user.username)
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index bc9c3d825c1..a9de52bd8d5 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -130,8 +130,8 @@ feature 'Issue Sidebar' do
it 'adds new label' do
page.within('.block.labels') do
fill_in 'new_label_name', with: 'wontfix'
- page.find('.suggest-colors a', match: :first).trigger('click')
- page.find('button', text: 'Create').trigger('click')
+ page.find('.suggest-colors a', match: :first).click
+ page.find('button', text: 'Create').click
page.within('.dropdown-page-one') do
expect(page).to have_content 'wontfix'
@@ -142,8 +142,8 @@ feature 'Issue Sidebar' do
it 'shows error message if label title is taken' do
page.within('.block.labels') do
fill_in 'new_label_name', with: label.title
- page.find('.suggest-colors a', match: :first).trigger('click')
- page.find('button', text: 'Create').trigger('click')
+ page.find('.suggest-colors a', match: :first).click
+ page.find('button', text: 'Create').click
page.within('.dropdown-page-two') do
expect(page).to have_content 'Title has already been taken'
@@ -170,7 +170,7 @@ feature 'Issue Sidebar' do
end
def open_issue_sidebar
- find('aside.right-sidebar.right-sidebar-collapsed .js-sidebar-toggle').trigger('click')
+ find('aside.right-sidebar.right-sidebar-collapsed .js-sidebar-toggle').click
find('aside.right-sidebar.right-sidebar-expanded')
end
end
diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb
index 6d7b1b1cd8f..17035b5501c 100644
--- a/spec/features/issues/move_spec.rb
+++ b/spec/features/issues/move_spec.rb
@@ -38,7 +38,7 @@ feature 'issue move to another project' do
end
scenario 'moving issue to another project', :js do
- find('.js-move-issue').trigger('click')
+ find('.js-move-issue').click
wait_for_requests
all('.js-move-issue-dropdown-item')[0].click
find('.js-move-issue-confirmation-button').click
@@ -52,7 +52,7 @@ feature 'issue move to another project' do
scenario 'searching project dropdown', :js do
new_project_search.team << [user, :reporter]
- find('.js-move-issue').trigger('click')
+ find('.js-move-issue').click
wait_for_requests
page.within '.js-sidebar-move-issue-block' do
@@ -69,7 +69,7 @@ feature 'issue move to another project' do
background { another_project.team << [user, :guest] }
scenario 'browsing projects in projects select' do
- find('.js-move-issue').trigger('click')
+ find('.js-move-issue').click
wait_for_requests
page.within '.js-sidebar-move-issue-block' do
diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb
index 1f57c110c11..bcc6e9bab0f 100644
--- a/spec/features/issues/update_issues_spec.rb
+++ b/spec/features/issues/update_issues_spec.rb
@@ -118,7 +118,7 @@ feature 'Multiple issue updating from issues#index', :js do
end
def click_update_issues_button
- find('.update-selected-issues').trigger('click')
+ find('.update-selected-issues').click
wait_for_requests
end
end
diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb
index 9f5e25ff2cb..c4c06ed514b 100644
--- a/spec/features/issues/user_uses_slash_commands_spec.rb
+++ b/spec/features/issues/user_uses_slash_commands_spec.rb
@@ -226,7 +226,7 @@ feature 'Issues > User uses quick actions', :js do
end
it 'applies the commands to both issues and moves the issue' do
- write_note("/label ~#{bug.title} ~#{wontfix.title}\n/milestone %\"#{milestone.title}\"\n/move #{target_project.full_path}")
+ write_note("/label ~#{bug.title} ~#{wontfix.title}\n\n/milestone %\"#{milestone.title}\"\n\n/move #{target_project.full_path}")
expect(page).to have_content 'Commands applied'
expect(issue.reload).to be_closed
@@ -245,7 +245,7 @@ feature 'Issues > User uses quick actions', :js do
end
it 'moves the issue and applies the commands to both issues' do
- write_note("/move #{target_project.full_path}\n/label ~#{bug.title} ~#{wontfix.title}\n/milestone %\"#{milestone.title}\"")
+ write_note("/move #{target_project.full_path}\n\n/label ~#{bug.title} ~#{wontfix.title}\n\n/milestone %\"#{milestone.title}\"")
expect(page).to have_content 'Commands applied'
expect(issue.reload).to be_closed
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index 25e99774575..b9af77f918a 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Issues', :js do
+describe 'Issues' do
include DropzoneHelper
include IssueHelpers
include SortingHelper
@@ -24,15 +24,109 @@ describe 'Issues', :js do
end
before do
- visit project_issue_path(project, issue)
- page.within('.content .issuable-actions') do
- find('.issuable-edit').click
- end
- find('.issue-details .content-block .js-zen-enter').click
+ visit edit_project_issue_path(project, issue)
+ find('.js-zen-enter').click
end
it 'opens new issue popup' do
- expect(page).to have_content(issue.description)
+ expect(page).to have_content("Issue ##{issue.iid}")
+ end
+ end
+
+ describe 'Editing issue assignee' do
+ let!(:issue) do
+ create(:issue,
+ author: user,
+ assignees: [user],
+ project: project)
+ end
+
+ it 'allows user to select unassigned', :js do
+ visit edit_project_issue_path(project, issue)
+
+ expect(page).to have_content "Assignee #{user.name}"
+
+ first('.js-user-search').click
+ click_link 'Unassigned'
+
+ click_button 'Save changes'
+
+ page.within('.assignee') do
+ expect(page).to have_content 'No assignee - assign yourself'
+ end
+
+ expect(issue.reload.assignees).to be_empty
+ end
+ end
+
+ describe 'due date', :js do
+ context 'on new form' do
+ before do
+ visit new_project_issue_path(project)
+ end
+
+ it 'saves with due date' do
+ date = Date.today.at_beginning_of_month
+
+ fill_in 'issue_title', with: 'bug 345'
+ fill_in 'issue_description', with: 'bug description'
+ find('#issuable-due-date').click
+
+ page.within '.pika-single' do
+ click_button date.day
+ end
+
+ expect(find('#issuable-due-date').value).to eq date.to_s
+
+ click_button 'Submit issue'
+
+ page.within '.issuable-sidebar' do
+ expect(page).to have_content date.to_s(:medium)
+ end
+ end
+ end
+
+ context 'on edit form' do
+ let(:issue) { create(:issue, author: user, project: project, due_date: Date.today.at_beginning_of_month.to_s) }
+
+ before do
+ visit edit_project_issue_path(project, issue)
+ end
+
+ it 'saves with due date' do
+ date = Date.today.at_beginning_of_month
+
+ expect(find('#issuable-due-date').value).to eq date.to_s
+
+ date = date.tomorrow
+
+ fill_in 'issue_title', with: 'bug 345'
+ fill_in 'issue_description', with: 'bug description'
+ find('#issuable-due-date').click
+
+ page.within '.pika-single' do
+ click_button date.day
+ end
+
+ expect(find('#issuable-due-date').value).to eq date.to_s
+
+ click_button 'Save changes'
+
+ page.within '.issuable-sidebar' do
+ expect(page).to have_content date.to_s(:medium)
+ end
+ end
+
+ it 'warns about version conflict' do
+ issue.update(title: "New title")
+
+ fill_in 'issue_title', with: 'bug 345'
+ fill_in 'issue_description', with: 'bug description'
+
+ click_button 'Save changes'
+
+ expect(page).to have_content 'Someone edited the issue the same time you did'
+ end
end
end
@@ -273,7 +367,7 @@ describe 'Issues', :js do
it 'changes incoming email address token', :js do
find('.issue-email-modal-btn').click
previous_token = find('input#issue_email').value
- find('.incoming-email-token-reset').trigger('click')
+ find('.incoming-email-token-reset').click
wait_for_requests
@@ -489,6 +583,18 @@ describe 'Issues', :js do
expect(page.find_field("issue_description").value).not_to match /\n\n$/
end
+
+ it "cancels a file upload correctly" do
+ slow_requests do
+ dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
+
+ click_button 'Cancel'
+ end
+
+ expect(page).to have_button('Attach a file')
+ expect(page).not_to have_button('Cancel')
+ expect(page).not_to have_selector('.uploading-progress-container', visible: true)
+ end
end
context 'form filled by URL parameters' do
diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb
index ba976bc7216..4e2963c116d 100644
--- a/spec/features/merge_requests/conflicts_spec.rb
+++ b/spec/features/merge_requests/conflicts_spec.rb
@@ -23,11 +23,11 @@ feature 'Merge request conflict resolution', :js do
within find('.files-wrapper .diff-file', text: 'files/ruby/regex.rb') do
all('button', text: 'Use ours').each do |button|
- button.trigger('click')
+ button.send_keys(:return)
end
end
- click_button 'Commit conflict resolution'
+ find_button('Commit conflict resolution').send_keys(:return)
expect(page).to have_content('All merge conflicts were resolved')
merge_request.reload_diff
@@ -71,7 +71,7 @@ feature 'Merge request conflict resolution', :js do
execute_script('ace.edit($(".files-wrapper .diff-file pre")[1]).setValue("Gregor Samsa woke from troubled dreams");')
end
- click_button 'Commit conflict resolution'
+ find_button('Commit conflict resolution').send_keys(:return)
expect(page).to have_content('All merge conflicts were resolved')
merge_request.reload_diff
diff --git a/spec/features/merge_requests/diff_notes_avatars_spec.rb b/spec/features/merge_requests/diff_notes_avatars_spec.rb
index 9aa0672feae..9e816cf041b 100644
--- a/spec/features/merge_requests/diff_notes_avatars_spec.rb
+++ b/spec/features/merge_requests/diff_notes_avatars_spec.rb
@@ -22,7 +22,7 @@ feature 'Diff note avatars', :js do
project.team << [user, :master]
sign_in user
- page.driver.set_cookie('sidebar_collapsed', 'true')
+ set_cookie('sidebar_collapsed', 'true')
end
context 'discussion tab' do
@@ -56,7 +56,7 @@ feature 'Diff note avatars', :js do
end
it 'does not render avatar after commenting' do
- first('.diff-line-num').trigger('mouseover')
+ first('.diff-line-num').click
find('.js-add-diff-note-button').click
page.within('.js-discussion-note-form') do
@@ -85,7 +85,7 @@ feature 'Diff note avatars', :js do
it 'shows note avatar' do
page.within find_line(position.line_code(project.repository)) do
- find('.diff-notes-collapse').click
+ find('.diff-notes-collapse').send_keys(:return)
expect(page).to have_selector('img.js-diff-comment-avatar', count: 1)
end
@@ -93,7 +93,7 @@ feature 'Diff note avatars', :js do
it 'shows comment on note avatar' do
page.within find_line(position.line_code(project.repository)) do
- find('.diff-notes-collapse').click
+ find('.diff-notes-collapse').send_keys(:return)
expect(first('img.js-diff-comment-avatar')["data-original-title"]).to eq("#{note.author.name}: #{note.note.truncate(17)}")
end
@@ -101,7 +101,7 @@ feature 'Diff note avatars', :js do
it 'toggles comments when clicking avatar' do
page.within find_line(position.line_code(project.repository)) do
- find('.diff-notes-collapse').click
+ find('.diff-notes-collapse').send_keys(:return)
end
expect(page).to have_selector('.notes_holder', visible: false)
@@ -117,7 +117,7 @@ feature 'Diff note avatars', :js do
open_more_actions_dropdown(note)
page.within find(".note-row-#{note.id}") do
- find('.js-note-delete').click
+ accept_confirm { find('.js-note-delete').click }
end
wait_for_requests
@@ -139,7 +139,7 @@ feature 'Diff note avatars', :js do
end
page.within find_line(position.line_code(project.repository)) do
- find('.diff-notes-collapse').trigger('click')
+ find('.diff-notes-collapse').send_keys(:return)
expect(page).to have_selector('img.js-diff-comment-avatar', count: 2)
end
@@ -152,14 +152,14 @@ feature 'Diff note avatars', :js do
page.within '.js-discussion-note-form' do
find('.js-note-text').native.send_keys('Test')
- find('.js-comment-button').trigger('click')
+ find('.js-comment-button').click
wait_for_requests
end
end
page.within find_line(position.line_code(project.repository)) do
- find('.diff-notes-collapse').trigger('click')
+ find('.diff-notes-collapse').send_keys(:return)
expect(page).to have_selector('img.js-diff-comment-avatar', count: 3)
expect(find('.diff-comments-more-count')).to have_content '+1'
@@ -177,7 +177,7 @@ feature 'Diff note avatars', :js do
it 'shows extra comment count' do
page.within find_line(position.line_code(project.repository)) do
- find('.diff-notes-collapse').click
+ find('.diff-notes-collapse').send_keys(:return)
expect(find('.diff-comments-more-count')).to have_content '+1'
end
diff --git a/spec/features/merge_requests/diff_notes_resolve_spec.rb b/spec/features/merge_requests/diff_notes_resolve_spec.rb
index 3db0729cafb..15d380b1bf4 100644
--- a/spec/features/merge_requests/diff_notes_resolve_spec.rb
+++ b/spec/features/merge_requests/diff_notes_resolve_spec.rb
@@ -192,7 +192,7 @@ feature 'Diff notes resolve', :js do
page.find('.discussion-next-btn').click
end
- expect(page.evaluate_script("$('body').scrollTop()")).to be > 0
+ expect(page.evaluate_script("window.pageYOffset")).to be > 0
end
it 'hides jump to next button when all resolved' do
@@ -241,10 +241,8 @@ feature 'Diff notes resolve', :js do
end
it 'resolves discussion' do
- page.all('.note').each do |note|
- note.all('.line-resolve-btn').each do |button|
- button.click
- end
+ page.all('.note .line-resolve-btn').each do |button|
+ button.click
end
expect(page).to have_content('Resolved by')
@@ -305,10 +303,10 @@ feature 'Diff notes resolve', :js do
end
page.within '.line-resolve-all-container' do
- page.find('.discussion-next-btn').trigger('click')
+ page.find('.discussion-next-btn').click
end
- expect(page.evaluate_script("$('body').scrollTop()")).to be > 0
+ expect(page.evaluate_script("window.pageYOffset")).to be > 0
end
it 'updates updated text after resolving note' do
diff --git a/spec/features/merge_requests/diffs_spec.rb b/spec/features/merge_requests/diffs_spec.rb
index 2adca58620f..1bf77296ae6 100644
--- a/spec/features/merge_requests/diffs_spec.rb
+++ b/spec/features/merge_requests/diffs_spec.rb
@@ -7,14 +7,12 @@ feature 'Diffs URL', :js do
let(:merge_request) { create(:merge_request, source_project: project) }
context 'when visit with */* as accept header' do
- before do
- page.driver.add_header('Accept', '*/*')
- end
-
it 'renders the notes' do
create :note_on_merge_request, project: project, noteable: merge_request, note: 'Rebasing with master'
- visit diffs_project_merge_request_path(project, merge_request)
+ inspect_requests(inject_headers: { 'Accept' => '*/*' }) do
+ visit diffs_project_merge_request_path(project, merge_request)
+ end
# Load notes and diff through AJAX
expect(page).to have_css('.note-text', visible: false, text: 'Rebasing with master')
@@ -90,7 +88,7 @@ feature 'Diffs URL', :js do
visit diffs_project_merge_request_path(project, merge_request)
# Throws `Capybara::Poltergeist::InvalidSelector` if we try to use `#hash` syntax
- find("[id=\"#{changelog_id}\"] .js-edit-blob").trigger('click')
+ find("[id=\"#{changelog_id}\"] .js-edit-blob").click
expect(page).to have_selector('.js-fork-suggestion-button', count: 1)
expect(page).to have_selector('.js-cancel-fork-suggestion-button', count: 1)
diff --git a/spec/features/merge_requests/form_spec.rb b/spec/features/merge_requests/form_spec.rb
index 758fc9b139d..1dcc1e139a0 100644
--- a/spec/features/merge_requests/form_spec.rb
+++ b/spec/features/merge_requests/form_spec.rb
@@ -43,7 +43,7 @@ describe 'New/edit merge request', :js do
expect(page).to have_content user2.name
end
- find('a', text: 'Assign to me').trigger('click')
+ find('a', text: 'Assign to me').click
expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s)
page.within '.js-assignee-search' do
expect(page).to have_content user.name
diff --git a/spec/features/merge_requests/mini_pipeline_graph_spec.rb b/spec/features/merge_requests/mini_pipeline_graph_spec.rb
index bf21a719901..bac56270362 100644
--- a/spec/features/merge_requests/mini_pipeline_graph_spec.rb
+++ b/spec/features/merge_requests/mini_pipeline_graph_spec.rb
@@ -92,7 +92,7 @@ feature 'Mini Pipeline Graph', :js do
end
it 'should close when toggle is clicked again' do
- toggle.trigger('click')
+ toggle.click
expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu')
end
diff --git a/spec/features/merge_requests/update_merge_requests_spec.rb b/spec/features/merge_requests/update_merge_requests_spec.rb
index 1a41fd36a4f..c5498563b39 100644
--- a/spec/features/merge_requests/update_merge_requests_spec.rb
+++ b/spec/features/merge_requests/update_merge_requests_spec.rb
@@ -127,7 +127,7 @@ feature 'Multiple merge requests updating from merge_requests#index' do
end
def click_update_merge_requests_button
- find('.update-selected-issues').trigger('click')
+ find('.update-selected-issues').click
wait_for_requests
end
end
diff --git a/spec/features/merge_requests/user_posts_diff_notes_spec.rb b/spec/features/merge_requests/user_posts_diff_notes_spec.rb
index 7a773fb2baa..d44eb23d7f4 100644
--- a/spec/features/merge_requests/user_posts_diff_notes_spec.rb
+++ b/spec/features/merge_requests/user_posts_diff_notes_spec.rb
@@ -8,7 +8,7 @@ feature 'Merge requests > User posts diff notes', :js do
let(:project) { merge_request.source_project }
before do
- page.driver.set_cookie('sidebar_collapsed', 'true')
+ set_cookie('sidebar_collapsed', 'true')
project.add_developer(user)
sign_in(user)
@@ -103,7 +103,10 @@ feature 'Merge requests > User posts diff notes', :js do
it 'allows commenting' do
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
- first('.js-note-delete', visible: false).trigger('click')
+ accept_confirm do
+ first('button.more-actions-toggle').click
+ first('.js-note-delete').click
+ end
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
end
@@ -236,7 +239,7 @@ feature 'Merge requests > User posts diff notes', :js do
def should_allow_dismissing_a_comment(line_holder, diff_side = nil)
write_comment_on_line(line_holder, diff_side)
- find('.js-close-discussion-note-form').trigger('click')
+ find('.js-close-discussion-note-form').click
assert_comment_dismissal(line_holder)
end
diff --git a/spec/features/merge_requests/user_posts_notes_spec.rb b/spec/features/merge_requests/user_posts_notes_spec.rb
index d7cda73ab40..f4c75a2f265 100644
--- a/spec/features/merge_requests/user_posts_notes_spec.rb
+++ b/spec/features/merge_requests/user_posts_notes_spec.rb
@@ -141,7 +141,7 @@ describe 'Merge requests > User posts notes', :js do
end
it 'removes the attachment div and resets the edit form' do
- find('.js-note-attachment-delete').click
+ accept_confirm { find('.js-note-attachment-delete').click }
is_expected.not_to have_css('.note-attachment')
is_expected.not_to have_css('.current-note-edit-form')
wait_for_requests
diff --git a/spec/features/merge_requests/versions_spec.rb b/spec/features/merge_requests/versions_spec.rb
index 50f7d721ff3..29f95039af8 100644
--- a/spec/features/merge_requests/versions_spec.rb
+++ b/spec/features/merge_requests/versions_spec.rb
@@ -67,8 +67,8 @@ feature 'Merge Request versions', :js do
line_code = '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44_2_2'
page.within(diff_file_selector) do
- find(".line_holder[id='#{line_code}'] td:nth-of-type(1)").trigger 'mouseover'
- find(".line_holder[id='#{line_code}'] button").trigger 'click'
+ find(".line_holder[id='#{line_code}'] td:nth-of-type(1)").hover
+ find(".line_holder[id='#{line_code}'] button").click
page.within("form[data-line-code='#{line_code}']") do
fill_in "note[note]", with: "Typo, please fix"
@@ -137,8 +137,8 @@ feature 'Merge Request versions', :js do
line_code = '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44_4_4'
page.within(diff_file_selector) do
- find(".line_holder[id='#{line_code}'] td:nth-of-type(1)").trigger 'mouseover'
- find(".line_holder[id='#{line_code}'] button").trigger 'click'
+ find(".line_holder[id='#{line_code}'] td:nth-of-type(1)").hover
+ find(".line_holder[id='#{line_code}'] button").click
page.within("form[data-line-code='#{line_code}']") do
fill_in "note[note]", with: "Typo, please fix"
diff --git a/spec/features/merge_requests/widget_deployments_spec.rb b/spec/features/merge_requests/widget_deployments_spec.rb
index 5658c2c5122..72a52c979b3 100644
--- a/spec/features/merge_requests/widget_deployments_spec.rb
+++ b/spec/features/merge_requests/widget_deployments_spec.rb
@@ -42,7 +42,7 @@ feature 'Widget Deployments Header', :js do
end
scenario 'does start build when stop button clicked' do
- click_button('Stop environment')
+ accept_confirm { click_button('Stop environment') }
expect(page).to have_content('close_app')
end
diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb
index 1cddd35fd8a..c60883911f7 100644
--- a/spec/features/profile_spec.rb
+++ b/spec/features/profile_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Profile account page' do
+describe 'Profile account page', :js do
let(:user) { create(:user) }
before do
@@ -56,47 +56,38 @@ describe 'Profile account page' do
end
end
- describe 'when I reset private token' do
- before do
- visit profile_account_path
- end
-
- it 'resets private token' do
- previous_token = find("#private-token").value
-
- click_link('Reset private token')
-
- expect(find('#private-token').value).not_to eq(previous_token)
- end
- end
-
describe 'when I reset RSS token' do
before do
- visit profile_account_path
+ visit profile_personal_access_tokens_path
end
it 'resets RSS token' do
- previous_token = find("#rss-token").value
+ within('.rss-token-reset') do
+ previous_token = find("#rss_token").value
- click_link('Reset RSS token')
+ accept_confirm { click_link('reset it') }
+
+ expect(find('#rss_token').value).not_to eq(previous_token)
+ end
expect(page).to have_content 'RSS token was successfully reset'
- expect(find('#rss-token').value).not_to eq(previous_token)
end
end
describe 'when I reset incoming email token' do
before do
allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true)
- visit profile_account_path
+ visit profile_personal_access_tokens_path
end
it 'resets incoming email token' do
- previous_token = find('#incoming-email-token').value
+ within('.incoming-email-token-reset') do
+ previous_token = find('#incoming_email_token').value
- click_link('Reset incoming email token')
+ accept_confirm { click_link('reset it') }
- expect(find('#incoming-email-token').value).not_to eq(previous_token)
+ expect(find('#incoming_email_token').value).not_to eq(previous_token)
+ end
end
end
diff --git a/spec/features/profiles/oauth_applications_spec.rb b/spec/features/profiles/oauth_applications_spec.rb
index 8cb240077eb..d1edeef8da4 100644
--- a/spec/features/profiles/oauth_applications_spec.rb
+++ b/spec/features/profiles/oauth_applications_spec.rb
@@ -14,7 +14,7 @@ describe 'Profile > Applications' do
page.within('.oauth-applications') do
expect(page).to have_content('Your applications (1)')
- click_button 'Destroy'
+ accept_confirm { click_button 'Destroy' }
end
expect(page).to have_content('The application was deleted successfully')
@@ -28,7 +28,7 @@ describe 'Profile > Applications' do
page.within('.oauth-authorized-applications') do
expect(page).to have_content('Authorized applications (1)')
- click_button 'Revoke'
+ accept_confirm { click_button 'Revoke' }
end
expect(page).to have_content('The application was revoked access.')
diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb
index a572160dae9..8461cd0027c 100644
--- a/spec/features/profiles/personal_access_tokens_spec.rb
+++ b/spec/features/profiles/personal_access_tokens_spec.rb
@@ -34,7 +34,7 @@ describe 'Profile > Personal Access Tokens', :js do
fill_in "Name", with: name
# Set date to 1st of next month
- find_field("Expires at").trigger('focus')
+ find_field("Expires at").click
find(".pika-next").click
click_on "1"
@@ -78,7 +78,7 @@ describe 'Profile > Personal Access Tokens', :js do
it "allows revocation of an active token" do
visit profile_personal_access_tokens_path
- click_on "Revoke"
+ accept_confirm { click_on "Revoke" }
expect(page).to have_selector(".settings-message")
expect(no_personal_access_tokens_message).to have_text("This user has no active Personal Access Tokens.")
@@ -100,7 +100,7 @@ describe 'Profile > Personal Access Tokens', :js do
errors = ActiveModel::Errors.new(PersonalAccessToken.new).tap { |e| e.add(:name, "cannot be nil") }
allow_any_instance_of(PersonalAccessToken).to receive(:errors).and_return(errors)
- click_on "Revoke"
+ accept_confirm { click_on "Revoke" }
expect(active_personal_access_tokens).to have_text(personal_access_token.name)
expect(page).to have_content("Could not revoke")
end
diff --git a/spec/features/profiles/user_visits_notifications_tab_spec.rb b/spec/features/profiles/user_visits_notifications_tab_spec.rb
index 923ca8b1c80..df89918f17a 100644
--- a/spec/features/profiles/user_visits_notifications_tab_spec.rb
+++ b/spec/features/profiles/user_visits_notifications_tab_spec.rb
@@ -13,7 +13,7 @@ feature 'User visits the notifications tab', :js do
it 'changes the project notifications setting' do
expect(page).to have_content('Notifications')
- first('#notifications-button').trigger('click')
+ first('#notifications-button').click
click_link('On mention')
expect(page).to have_content('On mention')
diff --git a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
index 924ee0e4174..90d6841af0e 100644
--- a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
+++ b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
@@ -53,7 +53,7 @@ describe 'User visits the profile preferences page' do
expect(page).to have_content("You don't have starred projects yet")
expect(page.current_path).to eq starred_dashboard_projects_path
- find('.shortcuts-activity').trigger('click')
+ find('.shortcuts-activity').click
expect(page).not_to have_content("You don't have starred projects yet")
expect(page.current_path).to eq dashboard_projects_path
diff --git a/spec/features/projects/artifacts/download_spec.rb b/spec/features/projects/artifacts/download_spec.rb
index f1bdb2812c6..6f76c14910b 100644
--- a/spec/features/projects/artifacts/download_spec.rb
+++ b/spec/features/projects/artifacts/download_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Download artifact', :js do
+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) }
diff --git a/spec/features/projects/artifacts/file_spec.rb b/spec/features/projects/artifacts/file_spec.rb
index b2be10a7e0c..df1d17bdcb7 100644
--- a/spec/features/projects/artifacts/file_spec.rb
+++ b/spec/features/projects/artifacts/file_spec.rb
@@ -39,7 +39,6 @@ feature 'Artifact file', :js do
context 'JPG file' do
before do
- page.driver.browser.url_blacklist = []
visit_file('rails_sample.jpg')
wait_for_requests
diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb
index 941d34dd660..7a77df83034 100644
--- a/spec/features/projects/branches_spec.rb
+++ b/spec/features/projects/branches_spec.rb
@@ -67,7 +67,7 @@ describe 'Branches' do
expect(page).to have_content('fix')
expect(find('.all-branches')).to have_selector('li', count: 1)
- find('.js-branch-fix .btn-remove').trigger(:click)
+ accept_confirm { find('.js-branch-fix .btn-remove').click }
expect(page).not_to have_content('fix')
expect(find('.all-branches')).to have_selector('li', count: 0)
diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb
index a644f78a1f9..e16b5e7b5f3 100644
--- a/spec/features/projects/clusters_spec.rb
+++ b/spec/features/projects/clusters_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
feature 'Clusters', :js do
+ include GoogleApi::CloudPlatformHelpers
+
let!(:project) { create(:project, :repository) }
let!(:user) { create(:user) }
@@ -11,8 +13,10 @@ feature 'Clusters', :js do
context 'when user has signed in Google' do
before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:validate_token).and_return(true)
+ allow_any_instance_of(Projects::ClustersController)
+ .to receive(:token_in_session).and_return('token')
+ allow_any_instance_of(Projects::ClustersController)
+ .to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s)
end
context 'when user does not have a cluster and visits cluster index page' do
@@ -38,15 +42,15 @@ feature 'Clusters', :js do
allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil)
- fill_in 'cluster_gcp_project_id', with: 'gcp-project-123'
- fill_in 'cluster_gcp_cluster_name', with: 'dev-cluster'
+ fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123'
+ fill_in 'cluster_name', with: 'dev-cluster'
click_button 'Create cluster'
end
it 'user sees a cluster details page and creation status' do
expect(page).to have_content('Cluster is being created on Google Container Engine...')
- Gcp::Cluster.last.make_created!
+ Clusters::Cluster.last.provider.make_created!
expect(page).to have_content('Cluster was successfully created on Google Container Engine')
end
@@ -64,7 +68,8 @@ feature 'Clusters', :js do
end
context 'when user has a cluster and visits cluster index page' do
- let!(:cluster) { create(:gcp_cluster, :created_on_gke, :with_kubernetes_service, project: project) }
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
before do
visit project_clusters_path(project)
@@ -72,7 +77,7 @@ feature 'Clusters', :js do
it 'user sees an cluster details page' do
expect(page).to have_button('Save')
- expect(page.find(:css, '.cluster-name').value).to eq(cluster.gcp_cluster_name)
+ expect(page.find(:css, '.cluster-name').value).to eq(cluster.name)
end
context 'when user disables the cluster' do
diff --git a/spec/features/projects/commit/diff_notes_spec.rb b/spec/features/projects/commit/diff_notes_spec.rb
index f0fe4e00acc..4dbfc6f6edf 100644
--- a/spec/features/projects/commit/diff_notes_spec.rb
+++ b/spec/features/projects/commit/diff_notes_spec.rb
@@ -20,8 +20,8 @@ feature 'Commit diff', :js do
it "adds comment to diff" do
diff_line_num = first('.diff-line-num.new')
- diff_line_num.trigger('mouseover')
- diff_line_num.find('.js-add-diff-note-button').trigger('click')
+ diff_line_num.hover
+ diff_line_num.find('.js-add-diff-note-button').click
page.within(first('.diff-viewer')) do
find('.js-note-text').set 'test comment'
diff --git a/spec/features/projects/deploy_keys_spec.rb b/spec/features/projects/deploy_keys_spec.rb
index 2d1a9b931b5..e445758cb5e 100644
--- a/spec/features/projects/deploy_keys_spec.rb
+++ b/spec/features/projects/deploy_keys_spec.rb
@@ -20,7 +20,7 @@ describe 'Project deploy keys', :js do
page.within(find('.deploy-keys')) do
expect(page).to have_selector('.deploy-keys li', count: 1)
- click_on 'Remove'
+ accept_confirm { find(:button, text: 'Remove').send_keys(:return) }
expect(page).not_to have_selector('.fa-spinner', count: 0)
expect(page).to have_selector('.deploy-keys li', count: 0)
diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
index 0fe1eb4c293..5fc3ba54f65 100644
--- a/spec/features/projects/environments/environment_spec.rb
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -193,12 +193,14 @@ feature 'Environment' do
create(:environment, project: project,
name: 'staging-1.0/review',
state: :available)
-
- visit folder_project_environments_path(project, id: 'staging-1.0')
end
it 'renders a correct environment folder' do
- expect(page).to have_gitlab_http_status(:ok)
+ reqs = inspect_requests do
+ visit folder_project_environments_path(project, id: 'staging-1.0')
+ end
+
+ expect(reqs.first.status_code).to eq(200)
expect(page).to have_content('Environments / staging-1.0')
end
end
diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
index 610f566c0cf..b4eb5795470 100644
--- a/spec/features/projects/environments/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -151,7 +151,7 @@ feature 'Environments page', :js do
find('.js-dropdown-play-icon-container').click
expect(page).to have_content(action.name.humanize)
- expect { find('.js-manual-action-link').trigger('click') }
+ expect { find('.js-manual-action-link').click }
.not_to change { Ci::Pipeline.count }
end
diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb
index e5282b42a4f..951456763dc 100644
--- a/spec/features/projects/features_visibility_spec.rb
+++ b/spec/features/projects/features_visibility_spec.rb
@@ -22,7 +22,7 @@ describe 'Edit Project Settings' do
# disable by clicking toggle
toggle_feature_off("project[project_feature_attributes][#{tool_name}_access_level]")
page.within('.sharing-permissions') do
- click_button 'Save changes'
+ find('input[value="Save changes"]').click
end
wait_for_requests
expect(page).not_to have_selector(".shortcuts-#{shortcut_name}")
@@ -30,7 +30,7 @@ describe 'Edit Project Settings' do
# re-enable by clicking toggle again
toggle_feature_on("project[project_feature_attributes][#{tool_name}_access_level]")
page.within('.sharing-permissions') do
- click_button 'Save changes'
+ find('input[value="Save changes"]').click
end
wait_for_requests
expect(page).to have_selector(".shortcuts-#{shortcut_name}")
diff --git a/spec/features/projects/files/edit_file_soft_wrap_spec.rb b/spec/features/projects/files/edit_file_soft_wrap_spec.rb
index 25f7e18ac5c..3ab43b3c656 100644
--- a/spec/features/projects/files/edit_file_soft_wrap_spec.rb
+++ b/spec/features/projects/files/edit_file_soft_wrap_spec.rb
@@ -7,18 +7,18 @@ feature 'User uses soft wrap whilst editing file', :js do
project.team << [user, :master]
sign_in user
visit project_new_blob_path(project, 'master', file_name: 'test_file-name')
- editor = find('.file-editor.code')
- editor.click
- editor.send_keys 'Touch water with paw then recoil in horror chase dog then
- run away chase the pig around the house eat owner\'s food, and knock
- dish off table head butt cant eat out of my own dish. Cat is love, cat
- is life rub face on everything poop on grasses so meow. Playing with
- balls of wool flee in terror at cucumber discovered on floor run in
- circles tuxedo cats always looking dapper, but attack dog, run away
- and pretend to be victim so all of a sudden cat goes crazy, yet chase
- laser. Make muffins sit in window and stare ooo, a bird! yum lick yarn
- hanging out of own butt jump off balcony, onto stranger\'s head yet
- chase laser. Purr for no reason stare at ceiling hola te quiero.'.squish
+ page.within('.file-editor.code') do
+ find('.ace_text-input', visible: false).send_keys 'Touch water with paw then recoil in horror chase dog then
+ run away chase the pig around the house eat owner\'s food, and knock
+ dish off table head butt cant eat out of my own dish. Cat is love, cat
+ is life rub face on everything poop on grasses so meow. Playing with
+ balls of wool flee in terror at cucumber discovered on floor run in
+ circles tuxedo cats always looking dapper, but attack dog, run away
+ and pretend to be victim so all of a sudden cat goes crazy, yet chase
+ laser. Make muffins sit in window and stare ooo, a bird! yum lick yarn
+ hanging out of own butt jump off balcony, onto stranger\'s head yet
+ chase laser. Purr for no reason stare at ceiling hola te quiero.'.squish
+ end
end
let(:toggle_button) { find('.soft-wrap-toggle') }
@@ -36,6 +36,6 @@ feature 'User uses soft wrap whilst editing file', :js do
end
def get_content_width
- find('.ace_content')[:style].slice!(/width: \d+/).slice!(/\d+/)
+ find('.ace_content')[:style].slice!(/width: \d+/).slice!(/\d+/).to_i
end
end
diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb
index 05776c50f9d..461aa39d0ad 100644
--- a/spec/features/projects/import_export/export_file_spec.rb
+++ b/spec/features/projects/import_export/export_file_spec.rb
@@ -41,7 +41,7 @@ feature 'Import/Export - project export integration test', :js do
expect(page).to have_content('Export project')
- click_link 'Export project'
+ find(:link, 'Export project').send_keys(:return)
visit edit_project_path(project)
diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
index 026aa03f7cf..af125e1b9d3 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -94,6 +94,6 @@ feature 'Import/Export - project import integration test', :js do
end
def click_import_project_tab
- find('#import-project-tab').trigger('click')
+ find('#import-project-tab').click
end
end
diff --git a/spec/features/projects/import_export/namespace_export_file_spec.rb b/spec/features/projects/import_export/namespace_export_file_spec.rb
index b6a7c3cdcdb..e76bc6f1220 100644
--- a/spec/features/projects/import_export/namespace_export_file_spec.rb
+++ b/spec/features/projects/import_export/namespace_export_file_spec.rb
@@ -52,7 +52,7 @@ feature 'Import/Export - Namespace export file cleanup', :js do
expect(page).to have_content('Export project')
- click_link 'Export project'
+ find(:link, 'Export project').send_keys(:return)
visit edit_project_path(project)
diff --git a/spec/features/projects/jobs/user_browses_job_spec.rb b/spec/features/projects/jobs/user_browses_job_spec.rb
index 21c9acc7ac0..5d9208ebadd 100644
--- a/spec/features/projects/jobs/user_browses_job_spec.rb
+++ b/spec/features/projects/jobs/user_browses_job_spec.rb
@@ -21,12 +21,12 @@ describe 'User browses a job', :js do
expect(page).to have_content("Job ##{build.id}")
expect(page).to have_css('#build-trace')
- click_link('Erase')
+ accept_confirm { click_link('Erase') }
+ expect(page).to have_no_css('.artifacts')
expect(build).not_to have_trace
expect(build.artifacts_file.exists?).to be_falsy
expect(build.artifacts_metadata.exists?).to be_falsy
- expect(page).to have_no_css('.artifacts')
page.within('.erased') do
expect(page).to have_content('Job has been erased')
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index b095c3e6f7b..c2a0d2395a9 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -380,7 +380,6 @@ feature 'Jobs' do
end
it 'loads the page and shows all needed controls' do
- expect(page.status_code).to eq(200)
expect(page).to have_content 'Retry'
end
end
@@ -392,11 +391,10 @@ feature 'Jobs' do
job.run!
visit project_job_path(project, job)
find('.js-cancel-job').click()
- find('.js-retry-button').trigger('click')
+ find('.js-retry-button').click
end
it 'shows the right status and buttons', :js do
- expect(page).to have_gitlab_http_status(200)
page.within('aside.right-sidebar') do
expect(page).to have_content 'Cancel'
end
@@ -443,28 +441,30 @@ feature 'Jobs' do
context 'access source' do
context 'job from project' do
before do
- Capybara.current_session.driver.headers = { 'X-Sendfile-Type' => 'X-Sendfile' }
job.run!
- visit project_job_path(project, job)
- find('.js-raw-link-controller').click()
end
it 'sends the right headers' do
- expect(page.status_code).to eq(200)
- expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8')
- expect(page.response_headers['X-Sendfile']).to eq(job.trace.send(:current_path))
+ requests = inspect_requests(inject_headers: { 'X-Sendfile-Type' => 'X-Sendfile' }) do
+ visit raw_project_job_path(project, job)
+ end
+
+ expect(requests.first.status_code).to eq(200)
+ expect(requests.first.response_headers['Content-Type']).to eq('text/plain; charset=utf-8')
+ expect(requests.first.response_headers['X-Sendfile']).to eq(job.trace.send(:current_path))
end
end
context 'job from other project' do
before do
- Capybara.current_session.driver.headers = { 'X-Sendfile-Type' => 'X-Sendfile' }
job2.run!
- visit raw_project_job_path(project, job2)
end
it 'sends the right headers' do
- expect(page.status_code).to eq(404)
+ requests = inspect_requests(inject_headers: { 'X-Sendfile-Type' => 'X-Sendfile' }) do
+ visit raw_project_job_path(project, job2)
+ end
+ expect(requests.first.status_code).to eq(404)
end
end
end
@@ -473,8 +473,6 @@ feature 'Jobs' do
let(:existing_file) { Tempfile.new('existing-trace-file').path }
before do
- Capybara.current_session.driver.headers = { 'X-Sendfile-Type' => 'X-Sendfile' }
-
job.run!
end
@@ -483,16 +481,14 @@ feature 'Jobs' do
allow_any_instance_of(Gitlab::Ci::Trace)
.to receive(:paths)
.and_return([existing_file])
-
- visit project_job_path(project, job)
-
- find('.js-raw-link-controller').click
end
it 'sends the right headers' do
- expect(page.status_code).to eq(200)
- expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8')
- expect(page.response_headers['X-Sendfile']).to eq(existing_file)
+ 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
diff --git a/spec/features/projects/members/groups_with_access_list_spec.rb b/spec/features/projects/members/groups_with_access_list_spec.rb
index b1053982eee..7f067aadec6 100644
--- a/spec/features/projects/members/groups_with_access_list_spec.rb
+++ b/spec/features/projects/members/groups_with_access_list_spec.rb
@@ -31,6 +31,7 @@ feature 'Projects > Members > Groups with access list', :js do
tomorrow = Date.today + 3
fill_in "member_expires_at_#{group.id}", with: tomorrow.strftime("%F")
+ find('body').click
wait_for_requests
page.within(find('li.group_member')) do
@@ -40,7 +41,7 @@ feature 'Projects > Members > Groups with access list', :js do
scenario 'deletes group link' do
page.within(first('.group_member')) do
- find('.btn-remove').click
+ accept_confirm { find('.btn-remove').click }
end
wait_for_requests
diff --git a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
index 5f7b4ee0e77..0f88f4cb1e8 100644
--- a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
+++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
@@ -20,7 +20,7 @@ feature 'Projects > Members > Master adds member with expiration date', :js do
page.within '.users-project-form' do
select2(new_member.id, from: '#user_ids', multiple: true)
- fill_in 'expires_at', with: date.to_s(:medium)
+ fill_in 'expires_at', with: date.to_s(:medium) + "\n"
click_on 'Add to project'
end
@@ -37,7 +37,7 @@ feature 'Projects > Members > Master adds member with expiration date', :js do
visit project_project_members_path(project)
page.within "#project_member_#{new_member.project_members.first.id}" do
- find('.js-access-expiration-date').set date.to_s(:medium)
+ find('.js-access-expiration-date').set date.to_s(:medium) + "\n"
wait_for_requests
expect(page).to have_content('Expires in 3 days')
end
diff --git a/spec/features/projects/members/share_with_group_spec.rb b/spec/features/projects/members/share_with_group_spec.rb
index 63b5df5a8f5..3198798306c 100644
--- a/spec/features/projects/members/share_with_group_spec.rb
+++ b/spec/features/projects/members/share_with_group_spec.rb
@@ -41,7 +41,7 @@ feature 'Project > Members > Share with Group', :js do
select2 group_to_share_with.id, from: '#link_group_id'
page.find('body').click
- find('.btn-create').trigger('click')
+ find('.btn-create').click
page.within('.project-members-groups') do
expect(page).to have_content(group_to_share_with.name)
@@ -123,7 +123,7 @@ feature 'Project > Members > Share with Group', :js do
fill_in 'expires_at_groups', with: (Time.now + 4.5.days).strftime('%Y-%m-%d')
page.find('body').click
- find('.btn-create').trigger('click')
+ find('.btn-create').click
end
scenario 'the group link shows the expiration time with a warning class' do
diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
index 0fbe1ddb2a5..4eb36156812 100644
--- a/spec/features/projects/members/user_requests_access_spec.rb
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -60,7 +60,7 @@ feature 'Projects > Members > User requests access', :js do
expect(project.requesters.exists?(user_id: user)).to be_truthy
- click_link 'Withdraw Access Request'
+ accept_confirm { click_link 'Withdraw Access Request' }
expect(project.requesters.exists?(user_id: user)).to be_falsey
expect(page).to have_content 'Your access request to the project has been withdrawn.'
diff --git a/spec/features/projects/merge_requests/user_comments_on_diff_spec.rb b/spec/features/projects/merge_requests/user_comments_on_diff_spec.rb
index f34302f25f8..e3f90a78cb5 100644
--- a/spec/features/projects/merge_requests/user_comments_on_diff_spec.rb
+++ b/spec/features/projects/merge_requests/user_comments_on_diff_spec.rb
@@ -31,7 +31,7 @@ describe 'User comments on a diff', :js do
page.within('.files > div:nth-child(3)') do
expect(page).to have_content('Line is wrong')
- find('.js-toggle-diff-comments').trigger('click')
+ find('.js-toggle-diff-comments').click
expect(page).not_to have_content('Line is wrong')
end
@@ -64,7 +64,7 @@ describe 'User comments on a diff', :js do
# Hide the comment.
page.within('.files > div:nth-child(3)') do
- find('.js-toggle-diff-comments').trigger('click')
+ find('.js-toggle-diff-comments').click
expect(page).not_to have_content('Line is wrong')
end
@@ -77,7 +77,7 @@ describe 'User comments on a diff', :js do
# Show the comment.
page.within('.files > div:nth-child(3)') do
- find('.js-toggle-diff-comments').trigger('click')
+ find('.js-toggle-diff-comments').click
end
# Now both the comments should be shown.
@@ -90,6 +90,7 @@ describe 'User comments on a diff', :js do
end
# Check the same comments in the side-by-side view.
+ execute_script("window.scrollTo(0,0);")
click_link('Side-by-side')
wait_for_requests
@@ -153,11 +154,11 @@ describe 'User comments on a diff', :js do
find('.more-actions').click
find('.more-actions .dropdown-menu li', match: :first)
- find('.js-note-delete').click
+ accept_confirm { find('.js-note-delete').click }
end
page.within('.merge-request-tabs') do
- find('.notes-tab').trigger('click')
+ find('.notes-tab').click
end
wait_for_requests
diff --git a/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb b/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb
index f6e3997383f..3d19a2923b9 100644
--- a/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb
+++ b/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe 'User edits a merge request', :js do
+ include Select2Helper
+
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:user) { create(:user) }
@@ -15,8 +17,7 @@ describe 'User edits a merge request', :js do
it 'changes the target branch' do
expect(page).to have_content('Target branch')
- first('.target_branch').click
- select('merge-test', from: 'merge_request_target_branch', visible: false)
+ select2('merge-test', from: '#merge_request_target_branch')
click_button('Save changes')
expect(page).to have_content("Request to merge #{merge_request.source_branch} into merge-test")
diff --git a/spec/features/projects/merge_requests/user_manages_subscription_spec.rb b/spec/features/projects/merge_requests/user_manages_subscription_spec.rb
index 30a80f8e652..4ca435491cb 100644
--- a/spec/features/projects/merge_requests/user_manages_subscription_spec.rb
+++ b/spec/features/projects/merge_requests/user_manages_subscription_spec.rb
@@ -13,7 +13,7 @@ describe 'User manages subscription', :js do
end
it 'toggles subscription' do
- subscribe_button = find('.issuable-subscribe-button span')
+ subscribe_button = find('.js-issuable-subscribe-button')
expect(subscribe_button).to have_content('Subscribe')
diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb
index 8e11cb94350..6f097ad16c7 100644
--- a/spec/features/projects/new_project_spec.rb
+++ b/spec/features/projects/new_project_spec.rb
@@ -15,7 +15,7 @@ feature 'New project' do
expect(page).to have_content('Project path')
expect(page).to have_content('Project name')
- find('#import-project-tab').trigger('click')
+ find('#import-project-tab').click
expect(page).to have_link('GitHub')
expect(page).to have_link('Bitbucket')
@@ -137,7 +137,7 @@ feature 'New project' do
context 'Import project options', :js do
before do
visit new_project_path
- find('#import-project-tab').trigger('click')
+ find('#import-project-tab').click
end
context 'from git repository url' do
diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb
index 24b335a7068..fa2f7a1fd78 100644
--- a/spec/features/projects/pipeline_schedules_spec.rb
+++ b/spec/features/projects/pipeline_schedules_spec.rb
@@ -54,7 +54,7 @@ feature 'Pipeline Schedules', :js do
end
it 'deletes the pipeline' do
- click_link 'Delete'
+ accept_confirm { click_link 'Delete' }
expect(page).not_to have_css(".pipeline-schedule-table-row")
end
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index acbc5b046e6..b8fa1a54c24 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -67,13 +67,13 @@ describe 'Pipeline', :js do
it 'shows a running icon and a cancel action for the running build' do
page.within('#ci-badge-deploy') do
expect(page).to have_selector('.js-ci-status-icon-running')
- expect(page).to have_selector('.js-icon-action-cancel')
+ expect(page).to have_selector('.js-icon-cancel')
expect(page).to have_content('deploy')
end
end
it 'should be possible to cancel the running build' do
- find('#ci-badge-deploy .ci-action-icon-container').trigger('click')
+ find('#ci-badge-deploy .ci-action-icon-container').click
expect(page).not_to have_content('Cancel running')
end
@@ -86,13 +86,13 @@ describe 'Pipeline', :js do
expect(page).to have_content('build')
end
- page.within('#ci-badge-build .ci-action-icon-container') do
- expect(page).to have_selector('.js-icon-action-retry')
+ page.within('#ci-badge-build .ci-action-icon-container.js-icon-retry') do
+ expect(page).to have_selector('svg')
end
end
it 'should be possible to retry the success job' do
- find('#ci-badge-build .ci-action-icon-container').trigger('click')
+ find('#ci-badge-build .ci-action-icon-container').click
expect(page).not_to have_content('Retry job')
end
@@ -105,13 +105,13 @@ describe 'Pipeline', :js do
expect(page).to have_content('test')
end
- page.within('#ci-badge-test .ci-action-icon-container') do
- expect(page).to have_selector('.js-icon-action-retry')
+ page.within('#ci-badge-test .ci-action-icon-container.js-icon-retry') do
+ expect(page).to have_selector('svg')
end
end
it 'should be possible to retry the failed build' do
- find('#ci-badge-test .ci-action-icon-container').trigger('click')
+ find('#ci-badge-test .ci-action-icon-container').click
expect(page).not_to have_content('Retry job')
end
@@ -124,13 +124,13 @@ describe 'Pipeline', :js do
expect(page).to have_content('manual')
end
- page.within('#ci-badge-manual-build .ci-action-icon-container') do
- expect(page).to have_selector('.js-icon-action-play')
+ page.within('#ci-badge-manual-build .ci-action-icon-container.js-icon-play') do
+ expect(page).to have_selector('svg')
end
end
it 'should be possible to play the manual job' do
- find('#ci-badge-manual-build .ci-action-icon-container').trigger('click')
+ find('#ci-badge-manual-build .ci-action-icon-container').click
expect(page).not_to have_content('Play job')
end
@@ -165,7 +165,7 @@ describe 'Pipeline', :js do
context 'when retrying' do
before do
- find('.js-retry-button').trigger('click')
+ find('.js-retry-button').click
end
it { expect(page).not_to have_content('Retry') }
@@ -231,7 +231,7 @@ describe 'Pipeline', :js do
context 'when retrying' do
before do
- find('.js-retry-button').trigger('click')
+ find('.js-retry-button').click
end
it { expect(page).not_to have_content('Retry') }
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index ae888fd4343..fc689bbb486 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -103,7 +103,7 @@ describe 'Pipelines', :js do
context 'when canceling' do
before do
- find('.js-pipelines-cancel-button').click
+ accept_confirm { find('.js-pipelines-cancel-button').click }
wait_for_requests
end
@@ -232,7 +232,7 @@ describe 'Pipelines', :js do
context 'when canceling' do
before do
- find('.js-pipelines-cancel-button').trigger('click')
+ accept_alert { find('.js-pipelines-cancel-button').click }
end
it 'indicates that pipeline was canceled' do
@@ -345,14 +345,14 @@ describe 'Pipelines', :js do
context 'when clicking a stage badge' do
it 'should open a dropdown' do
- find('.js-builds-dropdown-button').trigger('click')
+ find('.js-builds-dropdown-button').click
expect(page).to have_link build.name
end
it 'should be possible to cancel pending build' do
- find('.js-builds-dropdown-button').trigger('click')
- find('a.js-ci-action-icon').trigger('click')
+ find('.js-builds-dropdown-button').click
+ find('a.js-ci-action-icon').click
expect(page).to have_content('canceled')
expect(build.reload).to be_canceled
@@ -361,11 +361,16 @@ describe 'Pipelines', :js do
context 'dropdown jobs list' do
it 'should keep the dropdown open when the user ctr/cmd + clicks in the job name' do
- find('.js-builds-dropdown-button').trigger('click')
-
- execute_script('var e = $.Event("keydown", { keyCode: 64 }); $("body").trigger(e);')
-
- find('.mini-pipeline-graph-dropdown-item').trigger('click')
+ find('.js-builds-dropdown-button').click
+ dropdown_item = find('.mini-pipeline-graph-dropdown-item').native
+
+ %i(alt control).each do |meta_key|
+ page.driver.browser.action
+ .key_down(meta_key)
+ .click(dropdown_item)
+ .key_up(meta_key)
+ .perform
+ end
expect(page).to have_selector('.js-ci-action-icon')
end
@@ -525,7 +530,6 @@ describe 'Pipelines', :js do
let(:project) { create(:project, :public, :repository) }
it { expect(page).to have_content 'Build with confidence' }
- it { expect(page).to have_gitlab_http_status(:success) }
end
context 'when project is private' do
diff --git a/spec/features/projects/ref_switcher_spec.rb b/spec/features/projects/ref_switcher_spec.rb
index 50c0bfd580d..33ccbc1a29f 100644
--- a/spec/features/projects/ref_switcher_spec.rb
+++ b/spec/features/projects/ref_switcher_spec.rb
@@ -6,7 +6,7 @@ feature 'Ref switcher', :js do
before do
project.team << [user, :master]
- page.driver.set_cookie('new_repo', 'true')
+ set_cookie('new_repo', 'true')
sign_in(user)
visit project_tree_path(project, 'master')
end
diff --git a/spec/features/projects/services/user_activates_jira_spec.rb b/spec/features/projects/services/user_activates_jira_spec.rb
index 0a86292ae6c..ac78b1dfb1c 100644
--- a/spec/features/projects/services/user_activates_jira_spec.rb
+++ b/spec/features/projects/services/user_activates_jira_spec.rb
@@ -65,7 +65,7 @@ describe 'User activates Jira', :js do
expect(find('.flash-container-page')).to have_content 'Test failed. message'
expect(find('.flash-container-page')).to have_content 'Save anyway'
- find('.flash-alert .flash-action').trigger('click')
+ find('.flash-alert .flash-action').click
wait_for_requests
expect(page).to have_content('JIRA activated.')
diff --git a/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb b/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb
index 95d5e8b14b9..6f057137867 100644
--- a/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb
+++ b/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb
@@ -76,7 +76,7 @@ feature 'Setup Mattermost slash commands', :js do
select_element = find('#mattermost_team_id')
selected_option = select_element.find('option[selected]')
- expect(select_element['disabled']).to be(true)
+ expect(select_element['disabled']).to eq("true")
expect(selected_option).to have_content(team_name.to_s)
end
@@ -104,7 +104,7 @@ feature 'Setup Mattermost slash commands', :js do
select_element = find('#mattermost_team_id')
- expect(select_element['disabled']).to be(false)
+ expect(select_element['disabled']).to be_falsey
expect(select_element.all('option').count).to eq(3)
end
@@ -122,7 +122,7 @@ feature 'Setup Mattermost slash commands', :js do
click_link 'Add to Mattermost'
- expect(find('input[type="submit"]')['disabled']).not_to be(true)
+ expect(find('input[type="submit"]')['disabled']).not_to eq("true")
end
it 'disables the submit button if the required fields are not provided', :js do
@@ -132,7 +132,7 @@ feature 'Setup Mattermost slash commands', :js do
fill_in('mattermost_trigger', with: '')
- expect(find('input[type="submit"]')['disabled']).to be(true)
+ expect(find('input[type="submit"]')['disabled']).to eq("true")
end
def stub_teams(count: 0)
diff --git a/spec/features/projects/services/user_activates_packagist_spec.rb b/spec/features/projects/services/user_activates_packagist_spec.rb
new file mode 100644
index 00000000000..b0cc818f093
--- /dev/null
+++ b/spec/features/projects/services/user_activates_packagist_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe 'User activates Packagist' do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_settings_integrations_path(project))
+
+ click_link('Packagist')
+ end
+
+ it 'activates service' do
+ check('Active')
+ fill_in('Username', with: 'theUser')
+ fill_in('Token', with: 'verySecret')
+ click_button('Save')
+
+ expect(page).to have_content('Packagist activated.')
+ end
+end
diff --git a/spec/features/projects/services/user_views_services_spec.rb b/spec/features/projects/services/user_views_services_spec.rb
index f86591c2633..5c5e8b66642 100644
--- a/spec/features/projects/services/user_views_services_spec.rb
+++ b/spec/features/projects/services/user_views_services_spec.rb
@@ -21,5 +21,6 @@ describe 'User views services' do
expect(page).to have_content('JetBrains TeamCity')
expect(page).to have_content('Asana')
expect(page).to have_content('Irker (IRC gateway)')
+ expect(page).to have_content('Packagist')
end
end
diff --git a/spec/features/projects/settings/forked_project_settings_spec.rb b/spec/features/projects/settings/forked_project_settings_spec.rb
new file mode 100644
index 00000000000..28954a4fb40
--- /dev/null
+++ b/spec/features/projects/settings/forked_project_settings_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+feature 'Settings for a forked project', :js do
+ include ProjectForksHelper
+ let(:user) { create(:user) }
+ let(:original_project) { create(:project) }
+ let(:forked_project) { fork_project(original_project, user) }
+
+ before do
+ original_project.add_master(user)
+ forked_project.add_master(user)
+ sign_in(user)
+ end
+
+ shared_examples 'project settings for a forked projects' do
+ it 'allows deleting the link to the forked project' do
+ visit edit_project_path(forked_project)
+
+ click_button 'Remove fork relationship'
+
+ wait_for_requests
+
+ fill_in('confirm_name_input', with: forked_project.name)
+ click_button('Confirm')
+
+ expect(page).to have_content('The fork relationship has been removed.')
+ expect(forked_project.reload.forked?).to be_falsy
+ end
+ end
+
+ it_behaves_like 'project settings for a forked projects'
+
+ context 'when the original project is deleted' do
+ before do
+ original_project.destroy!
+ end
+
+ it_behaves_like 'project settings for a forked projects'
+ end
+end
diff --git a/spec/features/projects/settings/merge_requests_settings_spec.rb b/spec/features/projects/settings/merge_requests_settings_spec.rb
index b1ec556bf16..ac76c30cc7c 100644
--- a/spec/features/projects/settings/merge_requests_settings_spec.rb
+++ b/spec/features/projects/settings/merge_requests_settings_spec.rb
@@ -21,7 +21,7 @@ feature 'Project settings > Merge Requests', :js do
within('.sharing-permissions-form') do
find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .project-feature-toggle').click
- click_on('Save changes')
+ find('input[value="Save changes"]').send_keys(:return)
end
expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds')
@@ -41,7 +41,7 @@ feature 'Project settings > Merge Requests', :js do
within('.sharing-permissions-form') do
find('.project-feature-controls[data-for="project[project_feature_attributes][builds_access_level]"] .project-feature-toggle').click
- click_on('Save changes')
+ find('input[value="Save changes"]').send_keys(:return)
end
expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds')
@@ -62,7 +62,7 @@ feature 'Project settings > Merge Requests', :js do
within('.sharing-permissions-form') do
find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .project-feature-toggle').click
- click_on('Save changes')
+ find('input[value="Save changes"]').send_keys(:return)
end
expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds')
diff --git a/spec/features/projects/settings/pipelines_settings_spec.rb b/spec/features/projects/settings/pipelines_settings_spec.rb
index de8fbb15b9c..ea8f997409d 100644
--- a/spec/features/projects/settings/pipelines_settings_spec.rb
+++ b/spec/features/projects/settings/pipelines_settings_spec.rb
@@ -22,7 +22,7 @@ feature "Pipelines settings" do
context 'for master' do
given(:role) { :master }
- scenario 'be allowed to change', :js do
+ scenario 'be allowed to change' do
fill_in('Test coverage parsing', with: 'coverage_regex')
click_on 'Save changes'
diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb
index a4fefb0d0e7..e2a5619c22b 100644
--- a/spec/features/projects/settings/repository_settings_spec.rb
+++ b/spec/features/projects/settings/repository_settings_spec.rb
@@ -34,7 +34,6 @@ feature 'Repository settings' do
visit project_settings_repository_path(project)
- expect(page.status_code).to eq(200)
expect(page).to have_content('private_deploy_key')
expect(page).to have_content('public_deploy_key')
end
@@ -86,7 +85,7 @@ feature 'Repository settings' do
project.deploy_keys << private_deploy_key
visit project_settings_repository_path(project)
- find('li', text: private_deploy_key.title).click_button('Remove')
+ accept_confirm { find('li', text: private_deploy_key.title).click_button('Remove') }
expect(page).not_to have_content(private_deploy_key.title)
end
diff --git a/spec/features/projects/snippets/create_snippet_spec.rb b/spec/features/projects/snippets/create_snippet_spec.rb
index 3e79dba3f19..e4215291f99 100644
--- a/spec/features/projects/snippets/create_snippet_spec.rb
+++ b/spec/features/projects/snippets/create_snippet_spec.rb
@@ -10,7 +10,7 @@ feature 'Create Snippet', :js do
fill_in 'project_snippet_title', with: 'My Snippet Title'
fill_in 'project_snippet_description', with: 'My Snippet **Description**'
page.within('.file-editor') do
- find('.ace_editor').native.send_keys('Hello World!')
+ find('.ace_text-input', visible: false).send_keys('Hello World!')
end
end
@@ -59,7 +59,7 @@ feature 'Create Snippet', :js do
fill_form
dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
- click_button('Create snippet')
+ find("input[value='Create snippet']").send_keys(:return)
wait_for_requests
expect(page).to have_content('My Snippet Title')
diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb
index 4c1fa5a666e..8ee7b9cf015 100644
--- a/spec/features/projects/tree/create_directory_spec.rb
+++ b/spec/features/projects/tree/create_directory_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'Multi-file editor new directory', :js do
- include WaitForRequests
-
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
@@ -10,7 +8,7 @@ feature 'Multi-file editor new directory', :js do
project.add_master(user)
sign_in(user)
- page.driver.set_cookie('new_repo', 'true')
+ set_cookie('new_repo', 'true')
visit project_tree_path(project, :master)
@@ -32,7 +30,6 @@ feature 'Multi-file editor new directory', :js do
click_button('Commit 1 file')
- expect(page).to have_content('Your changes have been committed')
expect(page).to have_selector('td', text: 'commit message')
click_link('foldername')
diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb
index a67ec891e7c..1e2de0711b8 100644
--- a/spec/features/projects/tree/create_file_spec.rb
+++ b/spec/features/projects/tree/create_file_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'Multi-file editor new file', :js do
- include WaitForRequests
-
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
@@ -10,7 +8,7 @@ feature 'Multi-file editor new file', :js do
project.add_master(user)
sign_in(user)
- page.driver.set_cookie('new_repo', 'true')
+ set_cookie('new_repo', 'true')
visit project_tree_path(project, :master)
@@ -28,13 +26,10 @@ feature 'Multi-file editor new file', :js do
click_button('Create file')
end
- find('.inputarea').send_keys('file content')
-
fill_in('commit-message', with: 'commit message')
click_button('Commit 1 file')
- expect(page).to have_content('Your changes have been committed')
expect(page).to have_selector('td', text: 'commit message')
end
end
diff --git a/spec/features/projects/tree/upload_file_spec.rb b/spec/features/projects/tree/upload_file_spec.rb
new file mode 100644
index 00000000000..8439bb5a69e
--- /dev/null
+++ b/spec/features/projects/tree/upload_file_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+feature 'Multi-file editor upload file', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:txt_file) { File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt') }
+ let(:img_file) { File.join(Rails.root, 'spec', 'fixtures', 'dk.png') }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ set_cookie('new_repo', 'true')
+
+ visit project_tree_path(project, :master)
+
+ wait_for_requests
+ end
+
+ it 'uploads text file' do
+ find('.add-to-tree').click
+
+ # make the field visible so capybara can use it
+ execute_script('document.querySelector("#file-upload").classList.remove("hidden")')
+ attach_file('file-upload', txt_file)
+
+ find('.add-to-tree').click
+
+ expect(page).to have_selector('.repo-tab', text: 'doc_sample.txt')
+ expect(find('.blob-editor-container .lines-content')['innerText']).to have_content(File.open(txt_file, &:readline))
+ end
+
+ it 'uploads image file' do
+ find('.add-to-tree').click
+
+ # make the field visible so capybara can use it
+ execute_script('document.querySelector("#file-upload").classList.remove("hidden")')
+ attach_file('file-upload', img_file)
+
+ find('.add-to-tree').click
+
+ expect(page).to have_selector('.repo-tab', text: 'dk.png')
+ expect(page).not_to have_selector('.monaco-editor')
+ expect(page).to have_content('The source could not be displayed for this temporary file.')
+ end
+end
diff --git a/spec/features/projects/user_browses_files_spec.rb b/spec/features/projects/user_browses_files_spec.rb
index f43b11c9485..f5e4d7f5130 100644
--- a/spec/features/projects/user_browses_files_spec.rb
+++ b/spec/features/projects/user_browses_files_spec.rb
@@ -175,10 +175,11 @@ describe 'User browses files' do
page.within('#modal-upload-blob') do
fill_in(:commit_message, with: 'New commit message')
+ fill_in(:branch_name, with: 'new_branch_name', visible: true)
+ click_button('Upload file')
end
- fill_in(:branch_name, with: 'new_branch_name', visible: true)
- click_button('Upload file')
+ wait_for_all_requests
visit(project_blob_path(project, 'new_branch_name/logo_sample.svg'))
diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb
index d63cbe578d8..337baaf4dcd 100644
--- a/spec/features/projects/wiki/markdown_preview_spec.rb
+++ b/spec/features/projects/wiki/markdown_preview_spec.rb
@@ -18,13 +18,13 @@ feature 'Projects > Wiki > User previews markdown changes', :js do
sign_in(user)
visit project_path(project)
- find('.shortcuts-wiki').trigger('click')
+ find('.shortcuts-wiki').click
end
context "while creating a new wiki page" do
context "when there are no spaces or hyphens in the page name" do
it "rewrites relative links as expected" do
- find('.add-new-wiki').trigger('click')
+ find('.add-new-wiki').click
page.within '#modal-new-wiki' do
fill_in :new_wiki_path, with: 'a/b/c/d'
click_button 'Create page'
@@ -91,7 +91,7 @@ feature 'Projects > Wiki > User previews markdown changes', :js do
context "while editing a wiki page" do
def create_wiki_page(path)
- find('.add-new-wiki').trigger('click')
+ find('.add-new-wiki').click
page.within '#modal-new-wiki' do
fill_in :new_wiki_path, with: path
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 e72b7dc0dd5..4a9d1cb87e1 100644
--- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
@@ -77,14 +77,14 @@ describe 'User creates wiki page' do
[stem]
++++
- \sqrt{4} = 2
+ \\sqrt{4} = 2
++++
another part
[latexmath]
++++
- \beta_x \gamma
+ \\beta_x \\gamma
++++
stem:[2+2] is 4
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 470391dc66b..ff325aeadd3 100644
--- a/spec/features/projects/wiki/user_views_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_views_wiki_page_spec.rb
@@ -81,10 +81,15 @@ describe 'User views a wiki page' do
end
it 'shows a file stored in a page' do
- file = Gollum::File.new(project.wiki)
+ gollum_file_double = double('Gollum::File',
+ mime_type: 'image/jpeg',
+ name: 'images/image.jpg',
+ path: 'images/image.jpg',
+ raw_data: '')
+ wiki_file = Gitlab::Git::WikiFile.new(gollum_file_double)
- allow_any_instance_of(Gollum::Wiki).to receive(:file).with('image.jpg', 'master').and_return(file)
- allow_any_instance_of(Gollum::File).to receive(:mime_type).and_return('image/jpeg')
+ allow(wiki_file).to receive(:mime_type).and_return('image/jpeg')
+ allow_any_instance_of(ProjectWiki).to receive(:find_file).with('image.jpg', nil).and_return(wiki_file)
expect(page).to have_xpath('//img[@data-src="image.jpg"]')
expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/image.jpg")
@@ -133,7 +138,7 @@ describe 'User views a wiki page' do
it 'opens a default wiki page', :js do
visit(project_path(project))
- find('.shortcuts-wiki').trigger('click')
+ find('.shortcuts-wiki').click
expect(page).to have_content('Home · Create Page')
end
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 3b01ed442bf..63e6051b571 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -13,8 +13,8 @@ feature 'Project' do
end
it "allows creation from templates", :js do
- find('#create-from-template-tab').trigger('click')
- find("##{template.name}").trigger('click')
+ find('#create-from-template-tab').click
+ find("label[for=#{template.name}]").click
fill_in("project_path", with: template.name)
page.within '#content-body' do
diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb
index 2ab1eda90f1..a4084818284 100644
--- a/spec/features/protected_branches_spec.rb
+++ b/spec/features/protected_branches_spec.rb
@@ -48,7 +48,7 @@ feature 'Protected Branches', :js do
expect(page).to have_content('fix')
expect(find('.all-branches')).to have_selector('li', count: 1)
- page.find('[data-target="#modal-delete-branch"]').trigger(:click)
+ page.find('[data-target="#modal-delete-branch"]').click
expect(page).to have_css('.js-delete-branch[disabled]')
fill_in 'delete_branch_input', with: 'fix'
@@ -67,9 +67,9 @@ feature 'Protected Branches', :js do
form = '.js-new-protected-branch'
within form do
- find(".js-allowed-to-merge").trigger('click')
+ find(".js-allowed-to-merge").click
click_link 'No one'
- find(".js-allowed-to-push").trigger('click')
+ find(".js-allowed-to-push").click
click_link 'Developers + Masters'
end
@@ -171,7 +171,7 @@ feature 'Protected Branches', :js do
end
def set_protected_branch_name(branch_name)
- find(".js-protected-branch-select").trigger('click')
+ find(".js-protected-branch-select").click
find(".dropdown-input-field").set(branch_name)
click_on("Create wildcard #{branch_name}")
end
diff --git a/spec/features/raven_js_spec.rb b/spec/features/raven_js_spec.rb
index b1f51959d54..74890c86047 100644
--- a/spec/features/raven_js_spec.rb
+++ b/spec/features/raven_js_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'RavenJS', :js do
+feature 'RavenJS' do
let(:raven_path) { '/raven.bundle.js' }
it 'should not load raven if sentry is disabled' do
@@ -18,6 +18,8 @@ feature 'RavenJS', :js do
end
def has_requested_raven
- page.driver.network_traffic.one? {|request| request.url.end_with?(raven_path)}
+ page.all('script', visible: false).one? do |elm|
+ elm[:src] =~ /#{raven_path}$/
+ end
end
end
diff --git a/spec/features/search/user_searches_for_code_spec.rb b/spec/features/search/user_searches_for_code_spec.rb
index 0ed797a62ea..77212fb105b 100644
--- a/spec/features/search/user_searches_for_code_spec.rb
+++ b/spec/features/search/user_searches_for_code_spec.rb
@@ -32,14 +32,14 @@ describe 'User searches for code' do
include_examples 'top right search form'
it 'finds code' do
- find('.js-search-project-dropdown').trigger('click')
+ find('.js-search-project-dropdown').click
page.within('.project-filter') do
click_link(project.name_with_namespace)
end
fill_in('dashboard_search', with: 'rspec')
- find('.btn-search').trigger('click')
+ find('.btn-search').click
page.within('.results') do
expect(find(:css, '.search-results')).to have_content('Update capybara, rspec-rails, poltergeist to recent versions')
diff --git a/spec/features/search/user_searches_for_issues_spec.rb b/spec/features/search/user_searches_for_issues_spec.rb
index 630a81b1c5e..ef9553f2a91 100644
--- a/spec/features/search/user_searches_for_issues_spec.rb
+++ b/spec/features/search/user_searches_for_issues_spec.rb
@@ -18,7 +18,7 @@ describe 'User searches for issues', :js do
it 'finds an issue' do
fill_in('dashboard_search', with: issue1.title)
- find('.btn-search').trigger('click')
+ find('.btn-search').click
page.within('.search-filter') do
click_link('Issues')
@@ -31,14 +31,14 @@ describe 'User searches for issues', :js do
context 'when on a project page' do
it 'finds an issue' do
- find('.js-search-project-dropdown').trigger('click')
+ find('.js-search-project-dropdown').click
page.within('.project-filter') do
click_link(project.name_with_namespace)
end
fill_in('dashboard_search', with: issue1.title)
- find('.btn-search').trigger('click')
+ find('.btn-search').click
page.within('.search-filter') do
click_link('Issues')
@@ -62,7 +62,7 @@ describe 'User searches for issues', :js do
it 'finds an issue' do
fill_in('dashboard_search', with: issue1.title)
- find('.btn-search').trigger('click')
+ find('.btn-search').click
page.within('.search-filter') do
click_link('Issues')
diff --git a/spec/features/search/user_searches_for_merge_requests_spec.rb b/spec/features/search/user_searches_for_merge_requests_spec.rb
index 116256682f4..3b6739aecbd 100644
--- a/spec/features/search/user_searches_for_merge_requests_spec.rb
+++ b/spec/features/search/user_searches_for_merge_requests_spec.rb
@@ -17,7 +17,7 @@ describe 'User searches for merge requests', :js do
it 'finds a merge request' do
fill_in('dashboard_search', with: merge_request1.title)
- find('.btn-search').trigger('click')
+ find('.btn-search').click
page.within('.search-filter') do
click_link('Merge requests')
@@ -30,14 +30,14 @@ describe 'User searches for merge requests', :js do
context 'when on a project page' do
it 'finds a merge request' do
- find('.js-search-project-dropdown').trigger('click')
+ find('.js-search-project-dropdown').click
page.within('.project-filter') do
click_link(project.name_with_namespace)
end
fill_in('dashboard_search', with: merge_request1.title)
- find('.btn-search').trigger('click')
+ find('.btn-search').click
page.within('.search-filter') do
click_link('Merge requests')
diff --git a/spec/features/search/user_searches_for_milestones_spec.rb b/spec/features/search/user_searches_for_milestones_spec.rb
index 4fa9fe9ce8c..6e197aee498 100644
--- a/spec/features/search/user_searches_for_milestones_spec.rb
+++ b/spec/features/search/user_searches_for_milestones_spec.rb
@@ -17,7 +17,7 @@ describe 'User searches for milestones', :js do
it 'finds a milestone' do
fill_in('dashboard_search', with: milestone1.title)
- find('.btn-search').trigger('click')
+ find('.btn-search').click
page.within('.search-filter') do
click_link('Milestones')
@@ -30,14 +30,14 @@ describe 'User searches for milestones', :js do
context 'when on a project page' do
it 'finds a milestone' do
- find('.js-search-project-dropdown').trigger('click')
+ find('.js-search-project-dropdown').click
page.within('.project-filter') do
click_link(project.name_with_namespace)
end
fill_in('dashboard_search', with: milestone1.title)
- find('.btn-search').trigger('click')
+ find('.btn-search').click
page.within('.search-filter') do
click_link('Milestones')
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 1ea56479ecc..00af625dc86 100644
--- a/spec/features/search/user_searches_for_wiki_pages_spec.rb
+++ b/spec/features/search/user_searches_for_wiki_pages_spec.rb
@@ -15,14 +15,14 @@ describe 'User searches for wiki pages', :js do
include_examples 'top right search form'
it 'finds a page' do
- find('.js-search-project-dropdown').trigger('click')
+ find('.js-search-project-dropdown').click
page.within('.project-filter') do
click_link(project.name_with_namespace)
end
fill_in('dashboard_search', with: 'content')
- find('.btn-search').trigger('click')
+ find('.btn-search').click
page.within('.search-filter') do
click_link('Wiki')
diff --git a/spec/features/search/user_uses_search_filters_spec.rb b/spec/features/search/user_uses_search_filters_spec.rb
index 95f3eb5e805..aa883c964d2 100644
--- a/spec/features/search/user_uses_search_filters_spec.rb
+++ b/spec/features/search/user_uses_search_filters_spec.rb
@@ -16,7 +16,7 @@ describe 'User uses search filters', :js do
context' when filtering by group' do
it 'shows group projects' do
- find('.js-search-group-dropdown').trigger('click')
+ find('.js-search-group-dropdown').click
wait_for_requests
@@ -27,7 +27,7 @@ describe 'User uses search filters', :js do
expect(find('.js-search-group-dropdown')).to have_content(group.name)
page.within('.project-filter') do
- find('.js-search-project-dropdown').trigger('click')
+ find('.js-search-project-dropdown').click
wait_for_requests
@@ -39,7 +39,7 @@ describe 'User uses search filters', :js do
context' when filtering by project' do
it 'shows a project' do
page.within('.project-filter') do
- find('.js-search-project-dropdown').trigger('click')
+ find('.js-search-project-dropdown').click
wait_for_requests
diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb
index d70cf1527e7..a7928857b7d 100644
--- a/spec/features/security/project/internal_access_spec.rb
+++ b/spec/features/security/project/internal_access_spec.rb
@@ -181,6 +181,21 @@ describe "Internal Project Access" do
it { is_expected.to be_denied_for(:visitor) }
end
+ describe "GET /:project_path/issues/:id/edit" do
+ let(:issue) { create(:issue, project: project) }
+ subject { edit_project_issue_path(project, issue) }
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_for(:reporter).of(project) }
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ it { is_expected.to be_denied_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
+ end
+
describe "GET /:project_path/snippets" do
subject { project_snippets_path(project) }
diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb
index ea130606545..a4396b20afd 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -181,6 +181,21 @@ describe "Private Project Access" do
it { is_expected.to be_denied_for(:visitor) }
end
+ describe "GET /:project_path/issues/:id/edit" do
+ let(:issue) { create(:issue, project: project) }
+ subject { edit_project_issue_path(project, issue) }
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_for(:reporter).of(project) }
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ it { is_expected.to be_denied_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
+ end
+
describe "GET /:project_path/snippets" do
subject { project_snippets_path(project) }
diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb
index d15f5af66c9..fccdeb0e5b7 100644
--- a/spec/features/security/project/public_access_spec.rb
+++ b/spec/features/security/project/public_access_spec.rb
@@ -394,6 +394,21 @@ describe "Public Project Access" do
it { is_expected.to be_allowed_for(:visitor) }
end
+ describe "GET /:project_path/issues/:id/edit" do
+ let(:issue) { create(:issue, project: project) }
+ subject { edit_project_issue_path(project, issue) }
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_for(:reporter).of(project) }
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ it { is_expected.to be_denied_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
+ end
+
describe "GET /:project_path/snippets" do
subject { project_snippets_path(project) }
diff --git a/spec/features/snippets/notes_on_personal_snippets_spec.rb b/spec/features/snippets/notes_on_personal_snippets_spec.rb
index bf79974b8c6..269351e55c9 100644
--- a/spec/features/snippets/notes_on_personal_snippets_spec.rb
+++ b/spec/features/snippets/notes_on_personal_snippets_spec.rb
@@ -74,24 +74,21 @@ describe 'Comments on personal snippets', :js do
it 'should not have autocomplete' do
wait_for_requests
- request_count_before = page.driver.network_traffic.count
find('#note_note').native.send_keys('')
fill_in 'note[note]', with: '@'
wait_for_requests
- request_count_after = page.driver.network_traffic.count
# This selector probably won't be in place even if autocomplete was enabled
# but we want to make sure
expect(page).not_to have_selector('.atwho-view')
- expect(request_count_before).to eq(request_count_after)
end
end
context 'when editing a note' do
it 'changes the text' do
- find('.js-note-edit').trigger('click')
+ find('.js-note-edit').click
page.within('.current-note-edit-form') do
fill_in 'note[note]', with: 'new content'
@@ -113,7 +110,7 @@ describe 'Comments on personal snippets', :js do
open_more_actions_dropdown(snippet_notes[0])
page.within("#notes-list li#note_#{snippet_notes[0].id}") do
- click_on 'Delete comment'
+ accept_confirm { click_on 'Delete comment' }
end
wait_for_requests
diff --git a/spec/features/snippets/user_creates_snippet_spec.rb b/spec/features/snippets/user_creates_snippet_spec.rb
index d732383a1e1..941765b7578 100644
--- a/spec/features/snippets/user_creates_snippet_spec.rb
+++ b/spec/features/snippets/user_creates_snippet_spec.rb
@@ -14,7 +14,7 @@ feature 'User creates snippet', :js do
fill_in 'personal_snippet_title', with: 'My Snippet Title'
fill_in 'personal_snippet_description', with: 'My Snippet **Description**'
page.within('.file-editor') do
- find('.ace_editor').native.send_keys 'Hello World!'
+ find('.ace_text-input', visible: false).send_keys 'Hello World!'
end
end
@@ -43,8 +43,8 @@ feature 'User creates snippet', :js do
link = find('a.no-attachment-icon img[alt="banana_sample"]')['src']
expect(link).to match(%r{/uploads/-/system/temp/\h{32}/banana_sample\.gif\z})
- visit(link)
- expect(page.status_code).to eq(200)
+ reqs = inspect_requests { visit(link) }
+ expect(reqs.first.status_code).to eq(200)
end
end
@@ -61,8 +61,8 @@ feature 'User creates snippet', :js do
link = find('a.no-attachment-icon img[alt="banana_sample"]')['src']
expect(link).to match(%r{/uploads/-/system/personal_snippet/#{Snippet.last.id}/\h{32}/banana_sample\.gif\z})
- visit(link)
- expect(page.status_code).to eq(200)
+ reqs = inspect_requests { visit(link) }
+ expect(reqs.first.status_code).to eq(200)
end
scenario 'validation fails for the first time' do
@@ -86,15 +86,15 @@ feature 'User creates snippet', :js do
link = find('a.no-attachment-icon img[alt="banana_sample"]')['src']
expect(link).to match(%r{/uploads/-/system/personal_snippet/#{Snippet.last.id}/\h{32}/banana_sample\.gif\z})
- visit(link)
- expect(page.status_code).to eq(200)
+ reqs = inspect_requests { visit(link) }
+ expect(reqs.first.status_code).to eq(200)
end
scenario 'Authenticated user creates a snippet with + in filename' do
fill_in 'personal_snippet_title', with: 'My Snippet Title'
page.within('.file-editor') do
find(:xpath, "//input[@id='personal_snippet_file_name']").set 'snippet+file+name'
- find('.ace_editor').native.send_keys 'Hello World!'
+ find('.ace_text-input', visible: false).send_keys 'Hello World!'
end
click_button 'Create snippet'
diff --git a/spec/features/tags/master_creates_tag_spec.rb b/spec/features/tags/master_creates_tag_spec.rb
index 1455345bd56..1f8bd8d681e 100644
--- a/spec/features/tags/master_creates_tag_spec.rb
+++ b/spec/features/tags/master_creates_tag_spec.rb
@@ -63,7 +63,7 @@ feature 'Master creates tag' do
expect(ref_input.value).to eq 'master'
expect(find('.dropdown-toggle-text')).to have_content 'master'
- find('.js-branch-select').trigger('click')
+ find('.js-branch-select').click
expect(find('.dropdown-menu')).to have_content 'empty-branch'
end
diff --git a/spec/features/tags/master_deletes_tag_spec.rb b/spec/features/tags/master_deletes_tag_spec.rb
index f5b3774122b..dfda664d673 100644
--- a/spec/features/tags/master_deletes_tag_spec.rb
+++ b/spec/features/tags/master_deletes_tag_spec.rb
@@ -64,7 +64,7 @@ feature 'Master deletes tag' do
def delete_first_tag
page.within('.content') do
- first('.btn-remove').click
+ accept_confirm { first('.btn-remove').click }
end
end
end
diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb
index 548d8372a07..bc472e74997 100644
--- a/spec/features/triggers_spec.rb
+++ b/spec/features/triggers_spec.rb
@@ -45,7 +45,7 @@ feature 'Triggers', :js do
visit project_settings_ci_cd_path(@project)
# See if edit page has correct descrption
- find('a[title="Edit"]').click
+ find('a[title="Edit"]').send_keys(:return)
expect(page.find('#trigger_description').value).to have_content 'trigger desc'
end
@@ -54,7 +54,7 @@ feature 'Triggers', :js do
visit project_settings_ci_cd_path(@project)
# See if edit page opens, then fill in new description and save
- find('a[title="Edit"]').click
+ find('a[title="Edit"]').send_keys(:return)
fill_in 'trigger_description', with: new_trigger_title
click_button 'Save trigger'
@@ -70,7 +70,7 @@ feature 'Triggers', :js do
visit project_settings_ci_cd_path(@project)
# See if the trigger can be edited and description is blank
- find('a[title="Edit"]').click
+ find('a[title="Edit"]').send_keys(:return)
expect(page.find('#trigger_description').value).to have_content ''
# See if trigger can be updated with description and saved successfully
@@ -94,12 +94,13 @@ feature 'Triggers', :js do
scenario 'take trigger ownership' do
# See if "Take ownership" on trigger works post trigger creation
- find('a.btn-trigger-take-ownership').click
page.accept_confirm do
- expect(page.find('.flash-notice')).to have_content 'Trigger was re-assigned.'
- expect(page.find('.triggers-list')).to have_content trigger_title
- expect(page.find('.triggers-list .trigger-owner')).to have_content user.name
+ first(:link, "Take ownership").send_keys(:return)
end
+
+ expect(page.find('.flash-notice')).to have_content 'Trigger was re-assigned.'
+ expect(page.find('.triggers-list')).to have_content trigger_title
+ expect(page.find('.triggers-list .trigger-owner')).to have_content user.name
end
end
@@ -116,11 +117,12 @@ feature 'Triggers', :js do
scenario 'revoke trigger' do
# See if "Revoke" on trigger works post trigger creation
- find('a.btn-trigger-revoke').click
page.accept_confirm do
- expect(page.find('.flash-notice')).to have_content 'Trigger removed'
- expect(page).to have_selector('p.settings-message.text-center.append-bottom-default')
+ find('a.btn-trigger-revoke').send_keys(:return)
end
+
+ expect(page.find('.flash-notice')).to have_content 'Trigger removed'
+ expect(page).to have_selector('p.settings-message.text-center.append-bottom-default')
end
end
diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb
index f3662cb184f..c9afef2a8de 100644
--- a/spec/features/u2f_spec.rb
+++ b/spec/features/u2f_spec.rb
@@ -79,7 +79,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do
first_u2f_device = register_u2f_device
second_u2f_device = register_u2f_device(name: 'My other device')
- click_on "Delete", match: :first
+ accept_confirm { click_on "Delete", match: :first }
expect(page).to have_content('Successfully deleted')
expect(page.body).not_to match(first_u2f_device.name)
@@ -162,7 +162,6 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do
@u2f_device.respond_to_u2f_authentication
- expect(page).to have_content('We heard back from your U2F device')
expect(page).to have_css('.sign-out-link', visible: false)
end
end
@@ -174,23 +173,10 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do
@u2f_device.respond_to_u2f_authentication
- expect(page).to have_content('We heard back from your U2F device')
expect(page).to have_css('.sign-out-link', visible: false)
end
end
- it 'persists remember_me value via hidden field' do
- gitlab_sign_in(user, remember: true)
-
- @u2f_device.respond_to_u2f_authentication
- expect(page).to have_content('We heard back from your U2F device')
-
- within 'div#js-authenticate-u2f' do
- field = first('input#user_remember_me', visible: false)
- expect(field.value).to eq '1'
- end
- end
-
describe "when a given U2F device has already been registered by another user" do
describe "but not the current user" do
it "does not allow logging in with that particular device" do
@@ -205,7 +191,6 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do
# Try authenticating user with the old U2F device
gitlab_sign_in(current_user)
@u2f_device.respond_to_u2f_authentication
- expect(page).to have_content('We heard back from your U2F device')
expect(page).to have_content('Authentication via U2F device failed')
end
end
@@ -223,7 +208,6 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do
# Try authenticating user with the same U2F device
gitlab_sign_in(current_user)
@u2f_device.respond_to_u2f_authentication
- expect(page).to have_content('We heard back from your U2F device')
expect(page).to have_css('.sign-out-link', visible: false)
end
@@ -235,7 +219,6 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do
unregistered_device = FakeU2fDevice.new(page, 'My device')
gitlab_sign_in(user)
unregistered_device.respond_to_u2f_authentication
- expect(page).to have_content('We heard back from your U2F device')
expect(page).to have_content('Authentication via U2F device failed')
end
@@ -260,7 +243,6 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do
[first_device, second_device].each do |device|
gitlab_sign_in(user)
device.respond_to_u2f_authentication
- expect(page).to have_content('We heard back from your U2F device')
expect(page).to have_css('.sign-out-link', visible: false)
@@ -283,7 +265,9 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do
it "deletes u2f registrations" do
visit profile_account_path
- expect { click_on "Disable" }.to change { U2fRegistration.count }.by(-1)
+ expect do
+ accept_confirm { click_on "Disable" }
+ end.to change { U2fRegistration.count }.by(-1)
end
end
end
diff --git a/spec/features/uploads/user_uploads_file_to_note_spec.rb b/spec/features/uploads/user_uploads_file_to_note_spec.rb
index 1261ffdc2ee..972c10aaf23 100644
--- a/spec/features/uploads/user_uploads_file_to_note_spec.rb
+++ b/spec/features/uploads/user_uploads_file_to_note_spec.rb
@@ -21,16 +21,12 @@ feature 'User uploads file to note' do
end
context 'uploading is in progress' do
- it 'shows "Cancel" button on uploading', :js do
- dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
-
- expect(page).to have_button('Cancel')
- end
-
it 'cancels uploading on clicking to "Cancel" button', :js do
- dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
+ slow_requests do
+ dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
- click_button 'Cancel'
+ click_button 'Cancel'
+ end
expect(page).to have_button('Attach a file')
expect(page).not_to have_button('Cancel')
@@ -38,16 +34,20 @@ feature 'User uploads file to note' do
end
it 'shows "Attaching a file" message on uploading 1 file', :js do
- dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
+ slow_requests do
+ dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
- expect(page).to have_selector('.attaching-file-message', visible: true, text: 'Attaching a file -')
+ expect(page).to have_selector('.attaching-file-message', visible: true, text: 'Attaching a file -')
+ end
end
it 'shows "Attaching 2 files" message on uploading 2 file', :js do
- dropzone_file([Rails.root.join('spec', 'fixtures', 'video_sample.mp4'),
- Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
+ slow_requests do
+ dropzone_file([Rails.root.join('spec', 'fixtures', 'video_sample.mp4'),
+ Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
- expect(page).to have_selector('.attaching-file-message', visible: true, text: 'Attaching 2 files -')
+ expect(page).to have_selector('.attaching-file-message', visible: true, text: 'Attaching 2 files -')
+ end
end
it 'shows error message, "retry" and "attach a new file" link a if file is too big', :js do
diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb
index 0252c957c95..a9973cdf214 100644
--- a/spec/features/users_spec.rb
+++ b/spec/features/users_spec.rb
@@ -24,6 +24,7 @@ feature 'Users', :js do
user.reload
expect(user.reset_password_token).not_to be_nil
+ find('a[href="#login-pane"]').click
gitlab_sign_in(user)
expect(current_path).to eq root_path
diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb
index 5d8e818f7bf..c78f7d0d9be 100644
--- a/spec/features/variables_spec.rb
+++ b/spec/features/variables_spec.rb
@@ -82,7 +82,7 @@ describe 'Project variables', :js do
it 'deletes variable' do
page.within('.variables-table') do
- click_on 'Remove'
+ accept_confirm { click_on 'Remove' }
end
expect(page).not_to have_selector('variables-table')
diff --git a/spec/fixtures/api/schemas/entities/issue.json b/spec/fixtures/api/schemas/entities/issue.json
new file mode 100644
index 00000000000..3d3329a3406
--- /dev/null
+++ b/spec/fixtures/api/schemas/entities/issue.json
@@ -0,0 +1,44 @@
+{
+ "type": "object",
+ "properties" : {
+ "id": { "type": "integer" },
+ "iid": { "type": "integer" },
+ "author_id": { "type": "integer" },
+ "description": { "type": ["string", "null"] },
+ "lock_version": { "type": ["string", "null"] },
+ "milestone_id": { "type": ["string", "null"] },
+ "title": { "type": "string" },
+ "moved_to_id": { "type": ["integer", "null"] },
+ "project_id": { "type": "integer" },
+ "web_url": { "type": "string" },
+ "state": { "type": "string" },
+ "create_note_path": { "type": "string" },
+ "preview_note_path": { "type": "string" },
+ "current_user": {
+ "type": "object",
+ "properties": {
+ "can_create_note": { "type": "boolean" },
+ "can_update": { "type": "boolean" }
+ }
+ },
+ "created_at": { "type": "date-time" },
+ "updated_at": { "type": "date-time" },
+ "branch_name": { "type": ["string", "null"] },
+ "due_date": { "type": "date" },
+ "confidential": { "type": "boolean" },
+ "discussion_locked": { "type": ["boolean", "null"] },
+ "updated_by_id": { "type": ["string", "null"] },
+ "deleted_at": { "type": ["string", "null"] },
+ "time_estimate": { "type": "integer" },
+ "total_time_spent": { "type": "integer" },
+ "human_time_estimate": { "type": ["integer", "null"] },
+ "human_total_time_spent": { "type": ["integer", "null"] },
+ "milestone": { "type": ["object", "null"] },
+ "labels": {
+ "type": "array",
+ "items": { "$ref": "label.json" }
+ },
+ "assignees": { "type": ["array", "null"] }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/entities/issue_sidebar.json b/spec/fixtures/api/schemas/entities/issue_sidebar.json
new file mode 100644
index 00000000000..682e345d5f5
--- /dev/null
+++ b/spec/fixtures/api/schemas/entities/issue_sidebar.json
@@ -0,0 +1,21 @@
+{
+ "type": "object",
+ "properties" : {
+ "id": { "type": "integer" },
+ "iid": { "type": "integer" },
+ "subscribed": { "type": "boolean" },
+ "time_estimate": { "type": "integer" },
+ "total_time_spent": { "type": "integer" },
+ "human_time_estimate": { "type": ["integer", "null"] },
+ "human_total_time_spent": { "type": ["integer", "null"] },
+ "participants": {
+ "type": "array",
+ "items": { "$ref": "../public_api/v4/user/basic.json" }
+ },
+ "assignees": {
+ "type": "array",
+ "items": { "$ref": "../public_api/v4/user/basic.json" }
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/entities/label.json b/spec/fixtures/api/schemas/entities/label.json
new file mode 100644
index 00000000000..40dff764c17
--- /dev/null
+++ b/spec/fixtures/api/schemas/entities/label.json
@@ -0,0 +1,26 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "color",
+ "description",
+ "title",
+ "priority"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "color": {
+ "type": "string",
+ "pattern": "^#[0-9A-Fa-f]{3}{1,2}$"
+ },
+ "description": { "type": ["string", "null"] },
+ "text_color": {
+ "type": "string",
+ "pattern": "^#[0-9A-Fa-f]{3}{1,2}$"
+ },
+ "type": { "type": "string" },
+ "title": { "type": "string" },
+ "priority": { "type": ["integer", "null"] }
+ },
+ "additionalProperties": false
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/entities/merge_request_basic.json b/spec/fixtures/api/schemas/entities/merge_request_basic.json
index 6b14188582a..995f13381ad 100644
--- a/spec/fixtures/api/schemas/entities/merge_request_basic.json
+++ b/spec/fixtures/api/schemas/entities/merge_request_basic.json
@@ -9,7 +9,9 @@
"human_time_estimate": { "type": ["string", "null"] },
"human_total_time_spent": { "type": ["string", "null"] },
"merge_error": { "type": ["string", "null"] },
- "assignee_id": { "type": ["integer", "null"] }
+ "assignee_id": { "type": ["integer", "null"] },
+ "subscribed": { "type": ["boolean", "null"] },
+ "participants": { "type": "array" }
},
"additionalProperties": false
}
diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json
index e1f62508933..a55ecaa5697 100644
--- a/spec/fixtures/api/schemas/issue.json
+++ b/spec/fixtures/api/schemas/issue.json
@@ -19,32 +19,7 @@
},
"labels": {
"type": "array",
- "items": {
- "type": "object",
- "required": [
- "id",
- "color",
- "description",
- "title",
- "priority"
- ],
- "properties": {
- "id": { "type": "integer" },
- "color": {
- "type": "string",
- "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
- },
- "description": { "type": ["string", "null"] },
- "text_color": {
- "type": "string",
- "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
- },
- "type": { "type": "string" },
- "title": { "type": "string" },
- "priority": { "type": ["integer", "null"] }
- },
- "additionalProperties": false
- }
+ "items": { "$ref": "entities/label.json" }
},
"assignee": {
"id": { "type": "integet" },
diff --git a/spec/fixtures/api/schemas/public_api/v4/user/login.json b/spec/fixtures/api/schemas/public_api/v4/user/login.json
index e6c1d9c9d84..aa066883c47 100644
--- a/spec/fixtures/api/schemas/public_api/v4/user/login.json
+++ b/spec/fixtures/api/schemas/public_api/v4/user/login.json
@@ -27,11 +27,9 @@
"can_create_group",
"can_create_project",
"two_factor_enabled",
- "external",
- "private_token"
+ "external"
],
"properties": {
- "$ref": "full.json",
- "private_token": { "type": "string" }
+ "$ref": "full.json"
}
}
diff --git a/spec/fixtures/clusters/sample_cert.pem b/spec/fixtures/clusters/sample_cert.pem
new file mode 100644
index 00000000000..e39a2b34416
--- /dev/null
+++ b/spec/fixtures/clusters/sample_cert.pem
@@ -0,0 +1,33 @@
+-----BEGIN CERTIFICATE-----
+MIIFtTCCA52gAwIBAgIJAOutg3Kf2y5dMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
+aWRnaXRzIFB0eSBMdGQwHhcNMTcxMDI5MTgxOTU3WhcNMTgxMDI5MTgxOTU3WjBF
+MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
+ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
+CgKCAgEAvQysroM3TLxaavadSPnFIltrYnxCnU4PvCR8971HMWXsq7Z4ShU4BbbE
+8yp7oUFjulSwW6DhdIvnQb8ihLKictLmrA0isQqrD/iNpKZ6/lI4DGWw4QzrvMnW
+V4yy2QZNpg9tzQHd4+xkeeIoG23RijDU/sPd5dqxF+rPHBfCVInmYvSzLvMhneNj
+Bt6gV02gU9e9hsnMatsDvEbvWKp7wcbPot0nWrfZulx2QAWyXy+zG9mJQUds6yc0
+4agAeT9JEb/xtRgR/kS0aUHSGnfSnhZiEn17s0PhTmbu7qSHgzgB+7oJrC9jPoUh
+S2Wo3n0xykAjHrA8wC/Ddw3L38S41VQ58GEfNchistPswyMmXo/Oenv9P3s/kCOI
+fndiksFNdqVo51y9Vjngj589hpOseFDyKmWPIEQZ9kxW/crjP6RZWWLHgz26KtxZ
+uJaoYL8VBbYfrk/bucw0Ma2GEOp8rTsBE7SvgejXZa78q+381Kzc/utW6VwSXqzY
+xeIitft0rXi17SZ+XoiTkIXtHn0ZwMtOXNDBADTpFmKa6wVACQilvcpOYD8gUHyH
+pB+EDRdST3M4Fiq1MBAVhk8Lj3tHSJ/1ymeF1PWSu57AnJlzerzq2fcfPotNNd37
+ZPNkPh0kxPLwxbAyrHflzx9qVVdI1irY9055mNSnhzlec4qJ9cECAwEAAaOBpzCB
+pDAdBgNVHQ4EFgQUnVa5dYPoIG/3+qXml0bX8+N16GwwdQYDVR0jBG4wbIAUnVa5
+dYPoIG/3+qXml0bX8+N16GyhSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpT
+b21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQDr
+rYNyn9suXTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQAUg4cyxXi1
+VR8ejTpaAruRyJ1pEG9Kc3kiIRXODy60z3hJXnx9LkScPkWGiuL5XacfZ2rMd4bw
+oVXIyi8U1UHWfAH8EZdrFKkU92jCiL5soHUONxLAvQEJ/FTR/qijrpzLCxXBdVQE
+xFEDWUu6rxLFyjEwzwnRTLgpjR606fdb7qXHkuAMvZ/ezJj8j97hok3Odpn4lr2H
+6hMTpK7HmDBX+kmdJJ+yBrm9hG1Pzpl7QU0dkxZ+qJNFjYMLnziiTwkv0c5ZaA9E
+NykZUcOv3Sjb6spu1A/E2BSq4WTjkIjrogFlfimE1vmUmObTRJOqUB0Vky1kHEwN
+pg7QqIJQmof1EAIaSM/YpUWXyumBwGLDUEud1JUz05In9Q4IZjEwZSJwbQW4fUia
+A93m9rk3Lw3xsFcaUdPMFIXk0rPoF1IgmV/oqb0gK95lOWRLbN+AV8qpKPpcKXOc
+TkIdFE47ZisEDhIdF6wC1izEMLeMEsPAO7/Y6MY4nRxsinSe95lRaw+yQpzx+mvJ
+Q7n1kiHI9Pd5M3+CiQda0d/GO1o5ORJnUGJRvr9HKuNmE7Lif0As/N0AlywjzE7A
+6Z8AEiWyRV1ffshu1k2UKmzvZuZeGGKRtrIjbJIRAtpRVtVZZGzhq5/sojCLoJ+u
+texqFBUo/4mFRZa4pDItUdyOlDy2/LO/ag==
+-----END CERTIFICATE-----
diff --git a/spec/helpers/ci_status_helper_spec.rb b/spec/helpers/ci_status_helper_spec.rb
index 6a3945c0ebc..bc2422aba90 100644
--- a/spec/helpers/ci_status_helper_spec.rb
+++ b/spec/helpers/ci_status_helper_spec.rb
@@ -8,17 +8,13 @@ describe CiStatusHelper do
describe '#ci_icon_for_status' do
it 'renders to correct svg on success' do
- expect(helper).to receive(:render)
- .with('shared/icons/icon_status_success.svg', anything)
-
- helper.ci_icon_for_status(success_commit.status)
+ expect(helper.ci_icon_for_status('success').to_s)
+ .to include 'status_success'
end
it 'renders the correct svg on failure' do
- expect(helper).to receive(:render)
- .with('shared/icons/icon_status_failed.svg', anything)
-
- helper.ci_icon_for_status(failed_commit.status)
+ expect(helper.ci_icon_for_status('failed').to_s)
+ .to include 'status_failed'
end
end
diff --git a/spec/helpers/gitlab_routing_helper_spec.rb b/spec/helpers/gitlab_routing_helper_spec.rb
index a44b200c5da..6c4f7050ee0 100644
--- a/spec/helpers/gitlab_routing_helper_spec.rb
+++ b/spec/helpers/gitlab_routing_helper_spec.rb
@@ -63,4 +63,30 @@ describe GitlabRoutingHelper do
it { expect(resend_invite_group_member_path(group_member)).to eq resend_invite_group_group_member_path(group_member.source, group_member) }
end
end
+
+ describe '#preview_markdown_path' do
+ let(:project) { create(:project) }
+
+ it 'returns group preview markdown path for a group parent' do
+ group = create(:group)
+
+ expect(preview_markdown_path(group)).to eq("/groups/#{group.path}/preview_markdown")
+ end
+
+ it 'returns project preview markdown path for a project parent' do
+ expect(preview_markdown_path(project)).to eq("/#{project.full_path}/preview_markdown")
+ end
+
+ it 'returns snippet preview markdown path for a personal snippet' do
+ @snippet = create(:personal_snippet)
+
+ expect(preview_markdown_path(nil)).to eq("/snippets/preview_markdown")
+ end
+
+ it 'returns project preview markdown path for a project snippet' do
+ @snippet = create(:project_snippet, project: project)
+
+ expect(preview_markdown_path(project)).to eq("/#{project.full_path}/preview_markdown")
+ end
+ end
end
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index ead3e28438e..cb851d828f2 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -159,4 +159,36 @@ describe IssuablesHelper do
end
end
end
+
+ describe '#issuable_initial_data' do
+ let(:user) { create(:user) }
+
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ allow(helper).to receive(:can?).and_return(true)
+ end
+
+ it 'returns the correct json for an issue' do
+ issue = create(:issue, author: user, description: 'issue text')
+ @project = issue.project
+
+ expected_data = {
+ 'endpoint' => "/#{@project.full_path}/issues/#{issue.iid}",
+ 'canUpdate' => true,
+ 'canDestroy' => true,
+ 'issuableRef' => "##{issue.iid}",
+ 'markdownPreviewPath' => "/#{@project.full_path}/preview_markdown",
+ 'markdownDocsPath' => '/help/user/markdown',
+ 'issuableTemplates' => [],
+ 'projectPath' => @project.path,
+ 'projectNamespace' => @project.namespace.path,
+ 'initialTitleHtml' => issue.title,
+ 'initialTitleText' => issue.title,
+ 'initialDescriptionHtml' => '<p dir="auto">issue text</p>',
+ 'initialDescriptionText' => 'issue text',
+ 'initialTaskStatus' => '0 of 0 tasks completed'
+ }
+ expect(JSON.parse(helper.issuable_initial_data(issue))).to eq(expected_data)
+ end
+ end
end
diff --git a/spec/javascripts/fixtures/clusters.rb b/spec/javascripts/fixtures/clusters.rb
index 5774f36f026..8e74c4f859c 100644
--- a/spec/javascripts/fixtures/clusters.rb
+++ b/spec/javascripts/fixtures/clusters.rb
@@ -6,7 +6,7 @@ describe Projects::ClustersController, '(JavaScript fixtures)', type: :controlle
let(:admin) { create(:admin) }
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project, :repository, namespace: namespace) }
- let(:cluster) { project.create_cluster!(gcp_cluster_name: "gke-test-creation-1", gcp_project_id: 'gitlab-internal-153318', gcp_cluster_zone: 'us-central1-a', gcp_cluster_size: '1', project_namespace: 'aaa', gcp_machine_type: 'n1-standard-1')}
+ let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
render_views
diff --git a/spec/javascripts/gl_form_spec.js b/spec/javascripts/gl_form_spec.js
index 124fc030774..5a8009e57fd 100644
--- a/spec/javascripts/gl_form_spec.js
+++ b/spec/javascripts/gl_form_spec.js
@@ -1,9 +1,9 @@
-import autosize from 'vendor/autosize';
+import Autosize from 'autosize';
import GLForm from '~/gl_form';
import '~/lib/utils/text_utility';
import '~/lib/utils/common_utils';
-window.autosize = autosize;
+window.autosize = Autosize;
describe('GLForm', () => {
describe('when instantiated', function () {
diff --git a/spec/javascripts/groups/components/app_spec.js b/spec/javascripts/groups/components/app_spec.js
index cd19a0fae1e..59d4f7c45c6 100644
--- a/spec/javascripts/groups/components/app_spec.js
+++ b/spec/javascripts/groups/components/app_spec.js
@@ -431,9 +431,9 @@ describe('AppComponent', () => {
});
it('should render groups tree', (done) => {
- vm.groups = [mockParentGroupItem];
+ vm.store.state.groups = [mockParentGroupItem];
vm.isLoading = false;
- vm.pageInfo = mockPageInfo;
+ vm.store.state.pageInfo = mockPageInfo;
Vue.nextTick(() => {
expect(vm.$el.querySelector('.groups-list-tree-container')).toBeDefined();
done();
diff --git a/spec/javascripts/header_spec.js b/spec/javascripts/header_spec.js
index 4751eb868a4..2443ffd48f3 100644
--- a/spec/javascripts/header_spec.js
+++ b/spec/javascripts/header_spec.js
@@ -1,4 +1,4 @@
-import '~/header';
+import initTodoToggle from '~/header';
describe('Header', function () {
const todosPendingCount = '.todos-count';
@@ -14,6 +14,7 @@ describe('Header', function () {
preloadFixtures(fixtureTemplate);
beforeEach(() => {
+ initTodoToggle();
loadFixtures(fixtureTemplate);
});
diff --git a/spec/javascripts/helpers/vue_mount_component_helper.js b/spec/javascripts/helpers/vue_mount_component_helper.js
index b71136c4114..34acdfbfba9 100644
--- a/spec/javascripts/helpers/vue_mount_component_helper.js
+++ b/spec/javascripts/helpers/vue_mount_component_helper.js
@@ -1,3 +1,8 @@
+export const createComponentWithStore = (Component, store, propsData = {}) => new Component({
+ store,
+ propsData,
+});
+
export default (Component, props = {}, el = null) => new Component({
propsData: props,
}).$mount(el);
diff --git a/spec/javascripts/issuable_context_spec.js b/spec/javascripts/issuable_context_spec.js
deleted file mode 100644
index bcb2b7b24a0..00000000000
--- a/spec/javascripts/issuable_context_spec.js
+++ /dev/null
@@ -1,34 +0,0 @@
-/* global IssuableContext */
-import '~/issuable_context';
-import $ from 'jquery';
-
-describe('IssuableContext', () => {
- describe('toggleHiddenParticipants', () => {
- const event = jasmine.createSpyObj('event', ['preventDefault']);
-
- beforeEach(() => {
- spyOn($.fn, 'data').and.returnValue('data');
- spyOn($.fn, 'text').and.returnValue('data');
- });
-
- afterEach(() => {
- gl.lazyLoader = undefined;
- });
-
- it('calls loadCheck if lazyLoader is set', () => {
- gl.lazyLoader = jasmine.createSpyObj('lazyLoader', ['loadCheck']);
-
- IssuableContext.prototype.toggleHiddenParticipants(event);
-
- expect(gl.lazyLoader.loadCheck).toHaveBeenCalled();
- });
-
- it('does not throw if lazyLoader is not defined', () => {
- gl.lazyLoader = undefined;
-
- const toggle = IssuableContext.prototype.toggleHiddenParticipants.bind(null, event);
-
- expect(toggle).not.toThrow();
- });
- });
-});
diff --git a/spec/javascripts/issuable_spec.js b/spec/javascripts/issuable_spec.js
index 45f55395d3a..ceee08d47c5 100644
--- a/spec/javascripts/issuable_spec.js
+++ b/spec/javascripts/issuable_spec.js
@@ -1,80 +1,44 @@
-/* global IssuableIndex */
-
-import '~/lib/utils/url_utility';
-import '~/issuable_index';
-
-(() => {
- const BASE_URL = '/user/project/issues?scope=all&state=closed';
- const DEFAULT_PARAMS = '&utf8=%E2%9C%93';
-
- function updateForm(formValues, form) {
- $.each(formValues, (id, value) => {
- $(`#${id}`, form).val(value);
- });
- }
-
- function resetForm(form) {
- $('input[name!="utf8"]', form).each((index, input) => {
- input.setAttribute('value', '');
+import IssuableIndex from '~/issuable_index';
+
+describe('Issuable', () => {
+ let Issuable;
+ describe('initBulkUpdate', () => {
+ it('should not set bulkUpdateSidebar', () => {
+ Issuable = new IssuableIndex('issue_');
+ expect(Issuable.bulkUpdateSidebar).not.toBeDefined();
});
- }
- describe('Issuable', () => {
- preloadFixtures('static/issuable_filter.html.raw');
+ it('should set bulkUpdateSidebar', () => {
+ const element = document.createElement('div');
+ element.classList.add('issues-bulk-update');
+ document.body.appendChild(element);
- beforeEach(() => {
- loadFixtures('static/issuable_filter.html.raw');
- IssuableIndex.init();
- });
-
- it('should be defined', () => {
- expect(window.IssuableIndex).toBeDefined();
+ Issuable = new IssuableIndex('issue_');
+ expect(Issuable.bulkUpdateSidebar).toBeDefined();
});
+ });
- describe('filtering', () => {
- let $filtersForm;
-
- beforeEach(() => {
- $filtersForm = $('.js-filter-form');
- loadFixtures('static/issuable_filter.html.raw');
- resetForm($filtersForm);
- });
-
- it('should contain only the default parameters', () => {
- spyOn(gl.utils, 'visitUrl');
-
- IssuableIndex.filterResults($filtersForm);
-
- expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + DEFAULT_PARAMS);
- });
-
- it('should filter for the phrase "broken"', () => {
- spyOn(gl.utils, 'visitUrl');
-
- updateForm({ search: 'broken' }, $filtersForm);
- IssuableIndex.filterResults($filtersForm);
- const params = `${DEFAULT_PARAMS}&search=broken`;
-
- expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
- });
-
- it('should keep query parameters after modifying filter', () => {
- spyOn(gl.utils, 'visitUrl');
+ describe('resetIncomingEmailToken', () => {
+ beforeEach(() => {
+ const element = document.createElement('a');
+ element.classList.add('incoming-email-token-reset');
+ element.setAttribute('href', 'foo');
+ document.body.appendChild(element);
- // initial filter
- updateForm({ milestone_title: 'v1.0' }, $filtersForm);
+ const input = document.createElement('input');
+ input.setAttribute('id', 'issue_email');
+ document.body.appendChild(input);
- IssuableIndex.filterResults($filtersForm);
- let params = `${DEFAULT_PARAMS}&milestone_title=v1.0`;
- expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
+ Issuable = new IssuableIndex('issue_');
+ });
- // update filter
- updateForm({ label_name: 'Frontend' }, $filtersForm);
+ it('should send request to reset email token', () => {
+ spyOn(jQuery, 'ajax').and.callThrough();
+ document.querySelector('.incoming-email-token-reset').click();
- IssuableIndex.filterResults($filtersForm);
- params = `${DEFAULT_PARAMS}&milestone_title=v1.0&label_name=Frontend`;
- expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
- });
+ expect(jQuery.ajax).toHaveBeenCalled();
+ expect(jQuery.ajax.calls.argsFor(0)[0].url).toEqual('foo');
});
});
-})();
+});
+
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index 60a452f2223..3636aac79a0 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -1,6 +1,5 @@
/* eslint-disable space-before-function-paren, one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle, max-len */
import Issue from '~/issue';
-import CloseReopenReportToggle from '~/close_reopen_report_toggle';
import '~/lib/utils/text_utility';
describe('Issue', function() {
@@ -189,37 +188,4 @@ describe('Issue', function() {
});
});
});
-
- describe('units', () => {
- describe('class constructor', () => {
- it('calls .initCloseReopenReport', () => {
- spyOn(Issue.prototype, 'initCloseReopenReport');
-
- new Issue(); // eslint-disable-line no-new
-
- expect(Issue.prototype.initCloseReopenReport).toHaveBeenCalled();
- });
- });
-
- describe('initCloseReopenReport', () => {
- it('calls .initDroplab', () => {
- const container = jasmine.createSpyObj('container', ['querySelector']);
- const dropdownTrigger = {};
- const dropdownList = {};
- const button = {};
-
- spyOn(document, 'querySelector').and.returnValue(container);
- spyOn(CloseReopenReportToggle.prototype, 'initDroplab');
- container.querySelector.and.returnValues(dropdownTrigger, dropdownList, button);
-
- Issue.prototype.initCloseReopenReport();
-
- expect(document.querySelector).toHaveBeenCalledWith('.js-issuable-close-dropdown');
- expect(container.querySelector).toHaveBeenCalledWith('.js-issuable-close-toggle');
- expect(container.querySelector).toHaveBeenCalledWith('.js-issuable-close-menu');
- expect(container.querySelector).toHaveBeenCalledWith('.js-issuable-close-button');
- expect(CloseReopenReportToggle.prototype.initDroplab).toHaveBeenCalled();
- });
- });
- });
});
diff --git a/spec/javascripts/jobs/mock_data.js b/spec/javascripts/jobs/mock_data.js
index 17e4ef26b2c..43532275121 100644
--- a/spec/javascripts/jobs/mock_data.js
+++ b/spec/javascripts/jobs/mock_data.js
@@ -22,7 +22,7 @@ export default {
details_path: '/root/ci-mock/-/jobs/4757',
favicon: '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
action: {
- icon: 'icon_action_retry',
+ icon: 'retry',
title: 'Retry',
path: '/root/ci-mock/-/jobs/4757/retry',
method: 'post',
diff --git a/spec/javascripts/labels_issue_sidebar_spec.js b/spec/javascripts/labels_issue_sidebar_spec.js
index e47adc49224..a197b35f6fb 100644
--- a/spec/javascripts/labels_issue_sidebar_spec.js
+++ b/spec/javascripts/labels_issue_sidebar_spec.js
@@ -1,14 +1,12 @@
/* eslint-disable no-new */
-/* global IssuableContext */
-/* global LabelsSelect */
+import IssuableContext from '~/issuable_context';
+import LabelsSelect from '~/labels_select';
import '~/gl_dropdown';
import 'select2';
import '~/api';
import '~/create_label';
-import '~/issuable_context';
import '~/users_select';
-import '~/labels_select';
(() => {
let saveLabelCount = 0;
diff --git a/spec/javascripts/merge_request_notes_spec.js b/spec/javascripts/merge_request_notes_spec.js
index ac6ace48108..6054b75d0b8 100644
--- a/spec/javascripts/merge_request_notes_spec.js
+++ b/spec/javascripts/merge_request_notes_spec.js
@@ -1,6 +1,6 @@
/* global Notes */
-import 'vendor/autosize';
+import 'autosize';
import '~/gl_form';
import '~/lib/utils/text_utility';
import '~/render_gfm';
diff --git a/spec/javascripts/namespace_select_spec.js b/spec/javascripts/namespace_select_spec.js
new file mode 100644
index 00000000000..9d7625ca269
--- /dev/null
+++ b/spec/javascripts/namespace_select_spec.js
@@ -0,0 +1,65 @@
+import NamespaceSelect from '~/namespace_select';
+
+describe('NamespaceSelect', () => {
+ beforeEach(() => {
+ spyOn($.fn, 'glDropdown');
+ });
+
+ it('initializes glDropdown', () => {
+ const dropdown = document.createElement('div');
+
+ // eslint-disable-next-line no-new
+ new NamespaceSelect({ dropdown });
+
+ expect($.fn.glDropdown).toHaveBeenCalled();
+ });
+
+ describe('as input', () => {
+ let glDropdownOptions;
+
+ beforeEach(() => {
+ const dropdown = document.createElement('div');
+ // eslint-disable-next-line no-new
+ new NamespaceSelect({ dropdown });
+ glDropdownOptions = $.fn.glDropdown.calls.argsFor(0)[0];
+ });
+
+ it('prevents click events', () => {
+ const dummyEvent = new Event('dummy');
+ spyOn(dummyEvent, 'preventDefault');
+
+ glDropdownOptions.clicked({ e: dummyEvent });
+
+ expect(dummyEvent.preventDefault).toHaveBeenCalled();
+ });
+ });
+
+ describe('as filter', () => {
+ let glDropdownOptions;
+
+ beforeEach(() => {
+ const dropdown = document.createElement('div');
+ dropdown.dataset.isFilter = 'true';
+ // eslint-disable-next-line no-new
+ new NamespaceSelect({ dropdown });
+ glDropdownOptions = $.fn.glDropdown.calls.argsFor(0)[0];
+ });
+
+ it('does not prevent click events', () => {
+ const dummyEvent = new Event('dummy');
+ spyOn(dummyEvent, 'preventDefault');
+
+ glDropdownOptions.clicked({ e: dummyEvent });
+
+ expect(dummyEvent.preventDefault).not.toHaveBeenCalled();
+ });
+
+ it('sets URL of dropdown items', () => {
+ const dummyNamespace = { id: 'eal' };
+
+ const itemUrl = glDropdownOptions.url(dummyNamespace);
+
+ expect(itemUrl).toContain(`namespace_id=${dummyNamespace.id}`);
+ });
+ });
+});
diff --git a/spec/javascripts/notes/components/issue_comment_form_spec.js b/spec/javascripts/notes/components/issue_comment_form_spec.js
index 3f659af5c3b..a26fc8f63cc 100644
--- a/spec/javascripts/notes/components/issue_comment_form_spec.js
+++ b/spec/javascripts/notes/components/issue_comment_form_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import autosize from 'vendor/autosize';
+import Autosize from 'autosize';
import store from '~/notes/stores';
import issueCommentForm from '~/notes/components/issue_comment_form.vue';
import { loggedOutIssueData, notesDataMock, userDataMock, issueDataMock } from '../mock_data';
@@ -97,14 +97,14 @@ describe('issue_comment_form component', () => {
});
it('should resize textarea after note discarded', (done) => {
- spyOn(autosize, 'update');
+ spyOn(Autosize, 'update');
spyOn(vm, 'discard').and.callThrough();
vm.note = 'foo';
vm.discard();
Vue.nextTick(() => {
- expect(autosize.update).toHaveBeenCalled();
+ expect(Autosize.update).toHaveBeenCalled();
done();
});
});
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index 4546b88e44d..928a4b461cc 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -1,7 +1,7 @@
/* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, max-len */
/* global Notes */
-import 'vendor/autosize';
+import 'autosize';
import '~/gl_form';
import '~/lib/utils/text_utility';
import '~/render_gfm';
@@ -343,6 +343,7 @@ import '~/notes';
diff_discussion_html: false,
};
$form = jasmine.createSpyObj('$form', ['closest', 'find']);
+ $form.length = 1;
row = jasmine.createSpyObj('row', ['prevAll', 'first', 'find']);
notes = jasmine.createSpyObj('notes', [
@@ -371,13 +372,29 @@ import '~/notes';
$form.closest.and.returnValues(row, $form);
$form.find.and.returnValues(discussionContainer);
body.attr.and.returnValue('');
-
- Notes.prototype.renderDiscussionNote.call(notes, note, $form);
});
it('should call Notes.animateAppendNote', () => {
+ Notes.prototype.renderDiscussionNote.call(notes, note, $form);
+
expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.discussion_html, $('.main-notes-list'));
});
+
+ it('should append to row selected with line_code', () => {
+ $form.length = 0;
+ note.discussion_line_code = 'line_code';
+ note.diff_discussion_html = '<tr></tr>';
+
+ const line = document.createElement('div');
+ line.id = note.discussion_line_code;
+ document.body.appendChild(line);
+
+ $form.closest.and.returnValues($form);
+
+ Notes.prototype.renderDiscussionNote.call(notes, note, $form);
+
+ expect(line.nextSibling.outerHTML).toEqual(note.diff_discussion_html);
+ });
});
describe('Discussion sub note', () => {
diff --git a/spec/javascripts/pipelines/graph/action_component_spec.js b/spec/javascripts/pipelines/graph/action_component_spec.js
index 85bd87318db..e8fcd4b1a36 100644
--- a/spec/javascripts/pipelines/graph/action_component_spec.js
+++ b/spec/javascripts/pipelines/graph/action_component_spec.js
@@ -11,7 +11,7 @@ describe('pipeline graph action component', () => {
tooltipText: 'bar',
link: 'foo',
actionMethod: 'post',
- actionIcon: 'icon_action_cancel',
+ actionIcon: 'cancel',
},
}).$mount();
diff --git a/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js b/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js
index 25fd18b197e..ba721bc53c6 100644
--- a/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js
+++ b/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js
@@ -11,7 +11,7 @@ describe('action component', () => {
tooltipText: 'bar',
link: 'foo',
actionMethod: 'post',
- actionIcon: 'icon_action_cancel',
+ actionIcon: 'cancel',
},
}).$mount();
diff --git a/spec/javascripts/pipelines/graph/job_component_spec.js b/spec/javascripts/pipelines/graph/job_component_spec.js
index e90593e0f40..342ee6c1242 100644
--- a/spec/javascripts/pipelines/graph/job_component_spec.js
+++ b/spec/javascripts/pipelines/graph/job_component_spec.js
@@ -14,7 +14,7 @@ describe('pipeline graph job component', () => {
group: 'success',
details_path: '/root/ci-mock/builds/4256',
action: {
- icon: 'icon_action_retry',
+ icon: 'retry',
title: 'Retry',
path: '/root/ci-mock/builds/4256/retry',
method: 'post',
diff --git a/spec/javascripts/pipelines/graph/mock_data.js b/spec/javascripts/pipelines/graph/mock_data.js
index 56c522b7f77..b9494f86d74 100644
--- a/spec/javascripts/pipelines/graph/mock_data.js
+++ b/spec/javascripts/pipelines/graph/mock_data.js
@@ -39,7 +39,7 @@ export default {
"details_path": "/root/ci-mock/builds/4153",
"favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico",
"action": {
- "icon": "icon_action_retry",
+ "icon": "retry",
"title": "Retry",
"path": "/root/ci-mock/builds/4153/retry",
"method": "post"
@@ -62,7 +62,7 @@ export default {
"details_path": "/root/ci-mock/builds/4153",
"favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico",
"action": {
- "icon": "icon_action_retry",
+ "icon": "retry",
"title": "Retry",
"path": "/root/ci-mock/builds/4153/retry",
"method": "post"
@@ -96,7 +96,7 @@ export default {
"details_path": "/root/ci-mock/builds/4166",
"favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico",
"action": {
- "icon": "icon_action_retry",
+ "icon": "retry",
"title": "Retry",
"path": "/root/ci-mock/builds/4166/retry",
"method": "post"
@@ -119,7 +119,7 @@ export default {
"details_path": "/root/ci-mock/builds/4166",
"favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico",
"action": {
- "icon": "icon_action_retry",
+ "icon": "retry",
"title": "Retry",
"path": "/root/ci-mock/builds/4166/retry",
"method": "post"
@@ -138,7 +138,7 @@ export default {
"details_path": "/root/ci-mock/builds/4159",
"favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico",
"action": {
- "icon": "icon_action_retry",
+ "icon": "retry",
"title": "Retry",
"path": "/root/ci-mock/builds/4159/retry",
"method": "post"
@@ -161,7 +161,7 @@ export default {
"details_path": "/root/ci-mock/builds/4159",
"favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico",
"action": {
- "icon": "icon_action_retry",
+ "icon": "retry",
"title": "Retry",
"path": "/root/ci-mock/builds/4159/retry",
"method": "post"
diff --git a/spec/javascripts/pipelines/graph/stage_column_component_spec.js b/spec/javascripts/pipelines/graph/stage_column_component_spec.js
index aa4d6eedaf4..063ab53681b 100644
--- a/spec/javascripts/pipelines/graph/stage_column_component_spec.js
+++ b/spec/javascripts/pipelines/graph/stage_column_component_spec.js
@@ -13,7 +13,7 @@ describe('stage column component', () => {
group: 'success',
details_path: '/root/ci-mock/builds/4256',
action: {
- icon: 'icon_action_retry',
+ icon: 'retry',
title: 'Retry',
path: '/root/ci-mock/builds/4256/retry',
method: 'post',
diff --git a/spec/javascripts/repo/components/new_branch_form_spec.js b/spec/javascripts/repo/components/new_branch_form_spec.js
index c9c5ce096fc..9a705a1f0ed 100644
--- a/spec/javascripts/repo/components/new_branch_form_spec.js
+++ b/spec/javascripts/repo/components/new_branch_form_spec.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
+import store from '~/repo/stores';
import newBranchForm from '~/repo/components/new_branch_form.vue';
-import eventHub from '~/repo/event_hub';
-import RepoStore from '~/repo/stores/repo_store';
-import createComponent from '../../helpers/vue_mount_component_helper';
+import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
+import { resetStore } from '../helpers';
describe('Multi-file editor new branch form', () => {
let vm;
@@ -10,17 +10,17 @@ describe('Multi-file editor new branch form', () => {
beforeEach(() => {
const Component = Vue.extend(newBranchForm);
- RepoStore.currentBranch = 'master';
+ vm = createComponentWithStore(Component, store);
- vm = createComponent(Component, {
- currentBranch: RepoStore.currentBranch,
- });
+ vm.$store.state.currentBranch = 'master';
+
+ vm.$mount();
});
afterEach(() => {
vm.$destroy();
- RepoStore.currentBranch = '';
+ resetStore(vm.$store);
});
describe('template', () => {
@@ -48,6 +48,10 @@ describe('Multi-file editor new branch form', () => {
});
describe('submitNewBranch', () => {
+ beforeEach(() => {
+ spyOn(vm, 'createNewBranch').and.returnValue(Promise.resolve());
+ });
+
it('sets to loading', () => {
vm.submitNewBranch();
@@ -66,57 +70,45 @@ describe('Multi-file editor new branch form', () => {
});
});
- it('emits an event with branchName', () => {
- spyOn(eventHub, '$emit');
-
+ it('calls createdNewBranch with branchName', () => {
vm.branchName = 'testing';
vm.submitNewBranch();
- expect(eventHub.$emit).toHaveBeenCalledWith('createNewBranch', 'testing');
+ expect(vm.createNewBranch).toHaveBeenCalledWith('testing');
});
});
- describe('showErrorMessage', () => {
- it('sets loading to false', () => {
- vm.loading = true;
-
- vm.showErrorMessage();
-
- expect(vm.loading).toBeFalsy();
- });
-
- it('creates flash element', () => {
- vm.showErrorMessage('error message');
-
- expect(vm.$el.querySelector('.flash-alert')).not.toBeNull();
- expect(vm.$el.querySelector('.flash-alert').textContent.trim()).toBe('error message');
+ describe('submitNewBranch with error', () => {
+ beforeEach(() => {
+ spyOn(vm, 'createNewBranch').and.returnValue(Promise.reject({
+ json: () => Promise.resolve({
+ message: 'error message',
+ }),
+ }));
});
- });
- describe('createdNewBranch', () => {
- it('set loading to false', () => {
+ it('sets loading to false', (done) => {
vm.loading = true;
- vm.createdNewBranch();
-
- expect(vm.loading).toBeFalsy();
- });
-
- it('resets branch name', () => {
- vm.branchName = 'testing';
+ vm.submitNewBranch();
- vm.createdNewBranch();
+ setTimeout(() => {
+ expect(vm.loading).toBeFalsy();
- expect(vm.branchName).toBe('');
+ done();
+ });
});
- it('sets the dropdown toggle text', () => {
- vm.dropdownText = document.createElement('span');
+ it('creates flash element', (done) => {
+ vm.submitNewBranch();
- vm.createdNewBranch('branch name');
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.flash-alert')).not.toBeNull();
+ expect(vm.$el.querySelector('.flash-alert').textContent.trim()).toBe('error message');
- expect(vm.dropdownText.textContent).toBe('branch name');
+ done();
+ });
});
});
});
diff --git a/spec/javascripts/repo/components/new_dropdown/index_spec.js b/spec/javascripts/repo/components/new_dropdown/index_spec.js
index ddbfdab582d..93b10fc1fee 100644
--- a/spec/javascripts/repo/components/new_dropdown/index_spec.js
+++ b/spec/javascripts/repo/components/new_dropdown/index_spec.js
@@ -1,9 +1,8 @@
import Vue from 'vue';
+import store from '~/repo/stores';
import newDropdown from '~/repo/components/new_dropdown/index.vue';
-import RepoStore from '~/repo/stores/repo_store';
-import RepoHelper from '~/repo/helpers/repo_helper';
-import eventHub from '~/repo/event_hub';
-import createComponent from '../../../helpers/vue_mount_component_helper';
+import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+import { resetStore } from '../../helpers';
describe('new dropdown component', () => {
let vm;
@@ -11,15 +10,17 @@ describe('new dropdown component', () => {
beforeEach(() => {
const component = Vue.extend(newDropdown);
- vm = createComponent(component);
+ vm = createComponentWithStore(component, store);
+
+ vm.$store.state.path = '';
+
+ vm.$mount();
});
afterEach(() => {
vm.$destroy();
- RepoStore.files = [];
- RepoStore.openedFiles = [];
- RepoStore.setViewToPreview();
+ resetStore(vm.$store);
});
it('renders new file and new directory links', () => {
@@ -67,125 +68,4 @@ describe('new dropdown component', () => {
.catch(done.fail);
});
});
-
- describe('createEntryInStore', () => {
- ['tree', 'blob'].forEach((type) => {
- describe(type, () => {
- it('closes modal after creating file', () => {
- vm.openModal = true;
-
- eventHub.$emit('createNewEntry', 'testing', type);
-
- expect(vm.openModal).toBeFalsy();
- });
-
- it('sets editMode to true', () => {
- eventHub.$emit('createNewEntry', 'testing', type);
-
- expect(RepoStore.editMode).toBeTruthy();
- });
-
- it('toggles blob view', () => {
- eventHub.$emit('createNewEntry', 'testing', type);
-
- expect(RepoStore.isPreviewView()).toBeFalsy();
- });
-
- it('adds file into activeFiles', () => {
- eventHub.$emit('createNewEntry', 'testing', type);
-
- expect(RepoStore.openedFiles.length).toBe(1);
- });
-
- it(`creates ${type} in the current stores path`, () => {
- RepoStore.path = 'testing';
-
- eventHub.$emit('createNewEntry', 'testing/app', type);
-
- expect(RepoStore.files[0].path).toBe('testing/app');
- expect(RepoStore.files[0].name).toBe('app');
-
- if (type === 'tree') {
- expect(RepoStore.files[0].files.length).toBe(1);
- }
-
- RepoStore.path = '';
- });
- });
- });
-
- describe('file', () => {
- it('creates new file', () => {
- eventHub.$emit('createNewEntry', 'testing', 'blob');
-
- expect(RepoStore.files.length).toBe(1);
- expect(RepoStore.files[0].name).toBe('testing');
- expect(RepoStore.files[0].type).toBe('blob');
- expect(RepoStore.files[0].tempFile).toBeTruthy();
- });
-
- it('does not create temp file when file already exists', () => {
- RepoStore.files.push(RepoHelper.serializeRepoEntity('blob', {
- name: 'testing',
- }));
-
- eventHub.$emit('createNewEntry', 'testing', 'blob');
-
- expect(RepoStore.files.length).toBe(1);
- expect(RepoStore.files[0].name).toBe('testing');
- expect(RepoStore.files[0].type).toBe('blob');
- expect(RepoStore.files[0].tempFile).toBeUndefined();
- });
- });
-
- describe('tree', () => {
- it('creates new tree', () => {
- eventHub.$emit('createNewEntry', 'testing', 'tree');
-
- expect(RepoStore.files.length).toBe(1);
- expect(RepoStore.files[0].name).toBe('testing');
- expect(RepoStore.files[0].type).toBe('tree');
- expect(RepoStore.files[0].tempFile).toBeTruthy();
- expect(RepoStore.files[0].files.length).toBe(1);
- expect(RepoStore.files[0].files[0].name).toBe('.gitkeep');
- });
-
- it('creates multiple trees when entryName has slashes', () => {
- eventHub.$emit('createNewEntry', 'app/test', 'tree');
-
- expect(RepoStore.files.length).toBe(1);
- expect(RepoStore.files[0].name).toBe('app');
- expect(RepoStore.files[0].files[0].name).toBe('test');
- expect(RepoStore.files[0].files[0].files[0].name).toBe('.gitkeep');
- });
-
- it('creates tree in existing tree', () => {
- RepoStore.files.push(RepoHelper.serializeRepoEntity('tree', {
- name: 'app',
- }));
-
- eventHub.$emit('createNewEntry', 'app/test', 'tree');
-
- expect(RepoStore.files.length).toBe(1);
- expect(RepoStore.files[0].name).toBe('app');
- expect(RepoStore.files[0].tempFile).toBeUndefined();
- expect(RepoStore.files[0].files[0].tempFile).toBeTruthy();
- expect(RepoStore.files[0].files[0].name).toBe('test');
- expect(RepoStore.files[0].files[0].files[0].name).toBe('.gitkeep');
- });
-
- it('does not create new tree when already exists', () => {
- RepoStore.files.push(RepoHelper.serializeRepoEntity('tree', {
- name: 'app',
- }));
-
- eventHub.$emit('createNewEntry', 'app', 'tree');
-
- expect(RepoStore.files.length).toBe(1);
- expect(RepoStore.files[0].name).toBe('app');
- expect(RepoStore.files[0].tempFile).toBeUndefined();
- expect(RepoStore.files[0].files.length).toBe(0);
- });
- });
- });
});
diff --git a/spec/javascripts/repo/components/new_dropdown/modal_spec.js b/spec/javascripts/repo/components/new_dropdown/modal_spec.js
index 4c5cdc47c6e..1ff7590ec79 100644
--- a/spec/javascripts/repo/components/new_dropdown/modal_spec.js
+++ b/spec/javascripts/repo/components/new_dropdown/modal_spec.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
-import RepoStore from '~/repo/stores/repo_store';
+import store from '~/repo/stores';
import modal from '~/repo/components/new_dropdown/modal.vue';
-import eventHub from '~/repo/event_hub';
-import createComponent from '../../../helpers/vue_mount_component_helper';
+import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+import { file, resetStore } from '../../helpers';
describe('new file modal component', () => {
const Component = Vue.extend(modal);
@@ -11,18 +11,18 @@ describe('new file modal component', () => {
afterEach(() => {
vm.$destroy();
- RepoStore.files = [];
- RepoStore.openedFiles = [];
- RepoStore.setViewToPreview();
+ resetStore(vm.$store);
});
['tree', 'blob'].forEach((type) => {
describe(type, () => {
beforeEach(() => {
- vm = createComponent(Component, {
+ vm = createComponentWithStore(Component, store, {
type,
- currentPath: RepoStore.path,
- });
+ path: '',
+ }).$mount();
+
+ vm.entryName = 'testing';
});
it(`sets modal title as ${type}`, () => {
@@ -42,35 +42,157 @@ describe('new file modal component', () => {
expect(vm.$el.querySelector('.label-light').textContent.trim()).toBe(`${title} name`);
});
+
+ describe('createEntryInStore', () => {
+ it('calls createTempEntry', () => {
+ spyOn(vm, 'createTempEntry');
+
+ vm.createEntryInStore();
+
+ expect(vm.createTempEntry).toHaveBeenCalledWith({
+ name: 'testing',
+ type,
+ });
+ });
+
+ it('sets editMode to true', (done) => {
+ vm.createEntryInStore();
+
+ setTimeout(() => {
+ expect(vm.$store.state.editMode).toBeTruthy();
+
+ done();
+ });
+ });
+
+ it('toggles blob view', (done) => {
+ vm.createEntryInStore();
+
+ setTimeout(() => {
+ expect(vm.$store.state.currentBlobView).toBe('repo-editor');
+
+ done();
+ });
+ });
+
+ it('opens newly created file', (done) => {
+ vm.createEntryInStore();
+
+ setTimeout(() => {
+ expect(vm.$store.state.openFiles.length).toBe(1);
+ expect(vm.$store.state.openFiles[0].name).toBe(type === 'blob' ? 'testing' : '.gitkeep');
+
+ done();
+ });
+ });
+
+ it(`creates ${type} in the current stores path`, (done) => {
+ vm.$store.state.path = 'app';
+
+ vm.createEntryInStore();
+
+ setTimeout(() => {
+ expect(vm.$store.state.tree[0].path).toBe('app/testing');
+ expect(vm.$store.state.tree[0].name).toBe('testing');
+
+ if (type === 'tree') {
+ expect(vm.$store.state.tree[0].tree.length).toBe(1);
+ }
+
+ done();
+ });
+ });
+
+ if (type === 'blob') {
+ it('creates new file', (done) => {
+ vm.createEntryInStore();
+
+ setTimeout(() => {
+ expect(vm.$store.state.tree.length).toBe(1);
+ expect(vm.$store.state.tree[0].name).toBe('testing');
+ expect(vm.$store.state.tree[0].type).toBe('blob');
+ expect(vm.$store.state.tree[0].tempFile).toBeTruthy();
+
+ done();
+ });
+ });
+
+ it('does not create temp file when file already exists', (done) => {
+ vm.$store.state.tree.push(file('testing', '1', type));
+
+ vm.createEntryInStore();
+
+ setTimeout(() => {
+ expect(vm.$store.state.tree.length).toBe(1);
+ expect(vm.$store.state.tree[0].name).toBe('testing');
+ expect(vm.$store.state.tree[0].type).toBe('blob');
+ expect(vm.$store.state.tree[0].tempFile).toBeFalsy();
+
+ done();
+ });
+ });
+ } else {
+ it('creates new tree', () => {
+ vm.createEntryInStore();
+
+ expect(vm.$store.state.tree.length).toBe(1);
+ expect(vm.$store.state.tree[0].name).toBe('testing');
+ expect(vm.$store.state.tree[0].type).toBe('tree');
+ expect(vm.$store.state.tree[0].tempFile).toBeTruthy();
+ expect(vm.$store.state.tree[0].tree.length).toBe(1);
+ expect(vm.$store.state.tree[0].tree[0].name).toBe('.gitkeep');
+ });
+
+ it('creates multiple trees when entryName has slashes', () => {
+ vm.entryName = 'app/test';
+ vm.createEntryInStore();
+
+ expect(vm.$store.state.tree.length).toBe(1);
+ expect(vm.$store.state.tree[0].name).toBe('app');
+ expect(vm.$store.state.tree[0].tree[0].name).toBe('test');
+ expect(vm.$store.state.tree[0].tree[0].tree[0].name).toBe('.gitkeep');
+ });
+
+ it('creates tree in existing tree', () => {
+ vm.$store.state.tree.push(file('app', '1', 'tree'));
+
+ vm.entryName = 'app/test';
+ vm.createEntryInStore();
+
+ expect(vm.$store.state.tree.length).toBe(1);
+ expect(vm.$store.state.tree[0].name).toBe('app');
+ expect(vm.$store.state.tree[0].tempFile).toBeFalsy();
+ expect(vm.$store.state.tree[0].tree[0].tempFile).toBeTruthy();
+ expect(vm.$store.state.tree[0].tree[0].name).toBe('test');
+ expect(vm.$store.state.tree[0].tree[0].tree[0].name).toBe('.gitkeep');
+ });
+
+ it('does not create new tree when already exists', () => {
+ vm.$store.state.tree.push(file('app', '1', 'tree'));
+
+ vm.entryName = 'app';
+ vm.createEntryInStore();
+
+ expect(vm.$store.state.tree.length).toBe(1);
+ expect(vm.$store.state.tree[0].name).toBe('app');
+ expect(vm.$store.state.tree[0].tempFile).toBeFalsy();
+ expect(vm.$store.state.tree[0].tree.length).toBe(0);
+ });
+ }
+ });
});
});
it('focuses field on mount', () => {
document.body.innerHTML += '<div class="js-test"></div>';
- vm = createComponent(Component, {
+ vm = createComponentWithStore(Component, store, {
type: 'tree',
- currentPath: RepoStore.path,
- }, '.js-test');
+ path: '',
+ }).$mount('.js-test');
expect(document.activeElement).toBe(vm.$refs.fieldName);
vm.$el.remove();
});
-
- describe('createEntryInStore', () => {
- it('emits createNewEntry event', () => {
- spyOn(eventHub, '$emit');
-
- vm = createComponent(Component, {
- type: 'tree',
- currentPath: RepoStore.path,
- });
- vm.entryName = 'testing';
-
- vm.createEntryInStore();
-
- expect(eventHub.$emit).toHaveBeenCalledWith('createNewEntry', 'testing', 'tree');
- });
- });
});
diff --git a/spec/javascripts/repo/components/new_dropdown/upload_spec.js b/spec/javascripts/repo/components/new_dropdown/upload_spec.js
new file mode 100644
index 00000000000..bf7893029b1
--- /dev/null
+++ b/spec/javascripts/repo/components/new_dropdown/upload_spec.js
@@ -0,0 +1,103 @@
+import Vue from 'vue';
+import upload from '~/repo/components/new_dropdown/upload.vue';
+import store from '~/repo/stores';
+import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+import { resetStore } from '../../helpers';
+
+describe('new dropdown upload', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(upload);
+
+ vm = createComponentWithStore(Component, store, {
+ path: '',
+ });
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ describe('readFile', () => {
+ beforeEach(() => {
+ spyOn(FileReader.prototype, 'readAsText');
+ spyOn(FileReader.prototype, 'readAsDataURL');
+ });
+
+ it('calls readAsText for text files', () => {
+ const file = {
+ type: 'text/html',
+ };
+
+ vm.readFile(file);
+
+ expect(FileReader.prototype.readAsText).toHaveBeenCalledWith(file);
+ });
+
+ it('calls readAsDataURL for non-text files', () => {
+ const file = {
+ type: 'images/png',
+ };
+
+ vm.readFile(file);
+
+ expect(FileReader.prototype.readAsDataURL).toHaveBeenCalledWith(file);
+ });
+ });
+
+ describe('createFile', () => {
+ const target = {
+ result: 'content',
+ };
+ const binaryTarget = {
+ result: 'base64,base64content',
+ };
+ const file = {
+ name: 'file',
+ };
+
+ it('creates new file', (done) => {
+ vm.createFile(target, file, true);
+
+ vm.$nextTick(() => {
+ expect(vm.$store.state.tree.length).toBe(1);
+ expect(vm.$store.state.tree[0].name).toBe(file.name);
+ expect(vm.$store.state.tree[0].content).toBe(target.result);
+
+ done();
+ });
+ });
+
+ it('creates new file in path', (done) => {
+ vm.$store.state.path = 'testing';
+ vm.createFile(target, file, true);
+
+ vm.$nextTick(() => {
+ expect(vm.$store.state.tree.length).toBe(1);
+ expect(vm.$store.state.tree[0].name).toBe(file.name);
+ expect(vm.$store.state.tree[0].content).toBe(target.result);
+ expect(vm.$store.state.tree[0].path).toBe(`testing/${file.name}`);
+
+ done();
+ });
+ });
+
+ it('splits content on base64 if binary', (done) => {
+ vm.createFile(binaryTarget, file, false);
+
+ vm.$nextTick(() => {
+ expect(vm.$store.state.tree.length).toBe(1);
+ expect(vm.$store.state.tree[0].name).toBe(file.name);
+ expect(vm.$store.state.tree[0].content).toBe(binaryTarget.result.split('base64,')[1]);
+ expect(vm.$store.state.tree[0].base64).toBe(true);
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/repo/components/repo_commit_section_spec.js b/spec/javascripts/repo/components/repo_commit_section_spec.js
index e09d593f04c..0f991e1b727 100644
--- a/spec/javascripts/repo/components/repo_commit_section_spec.js
+++ b/spec/javascripts/repo/components/repo_commit_section_spec.js
@@ -1,56 +1,43 @@
import Vue from 'vue';
+import store from '~/repo/stores';
+import service from '~/repo/services';
import repoCommitSection from '~/repo/components/repo_commit_section.vue';
-import RepoStore from '~/repo/stores/repo_store';
-import RepoService from '~/repo/services/repo_service';
import getSetTimeoutPromise from '../../helpers/set_timeout_promise_helper';
+import { file, resetStore } from '../helpers';
describe('RepoCommitSection', () => {
- const branch = 'master';
- const projectUrl = 'projectUrl';
- let changedFiles;
- let openedFiles;
+ let vm;
- RepoStore.projectUrl = projectUrl;
-
- function createComponent(el) {
+ function createComponent() {
const RepoCommitSection = Vue.extend(repoCommitSection);
- return new RepoCommitSection().$mount(el);
+ const comp = new RepoCommitSection({
+ store,
+ }).$mount();
+
+ comp.$store.state.currentBranch = 'master';
+ comp.$store.state.openFiles = [file(), file()];
+ comp.$store.state.openFiles.forEach(f => Object.assign(f, {
+ changed: true,
+ content: 'testing',
+ }));
+
+ return comp.$mount();
}
beforeEach(() => {
- // Create a copy for each test because these can get modified directly
- changedFiles = [{
- id: 0,
- changed: true,
- url: `/namespace/${projectUrl}/blob/${branch}/dir/file0.ext`,
- path: 'dir/file0.ext',
- newContent: 'a',
- }, {
- id: 1,
- changed: true,
- url: `/namespace/${projectUrl}/blob/${branch}/dir/file1.ext`,
- path: 'dir/file1.ext',
- newContent: 'b',
- }];
- openedFiles = changedFiles.concat([{
- id: 2,
- url: `/namespace/${projectUrl}/blob/${branch}/dir/file2.ext`,
- path: 'dir/file2.ext',
- changed: false,
- }]);
+ vm = createComponent();
});
- it('renders a commit section', () => {
- RepoStore.isCommitable = true;
- RepoStore.currentBranch = branch;
- RepoStore.targetBranch = branch;
- RepoStore.openedFiles = openedFiles;
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
- const vm = createComponent();
+ it('renders a commit section', () => {
const changedFileElements = [...vm.$el.querySelectorAll('.changed-files > li')];
- const commitMessage = vm.$el.querySelector('#commit-message');
- const submitCommit = vm.$refs.submitCommit;
+ const submitCommit = vm.$el.querySelector('.btn');
const targetBranch = vm.$el.querySelector('.target-branch');
expect(vm.$el.querySelector(':scope > form')).toBeTruthy();
@@ -58,160 +45,70 @@ describe('RepoCommitSection', () => {
expect(changedFileElements.length).toEqual(2);
changedFileElements.forEach((changedFile, i) => {
- expect(changedFile.textContent.trim()).toEqual(changedFiles[i].path);
+ expect(changedFile.textContent.trim()).toEqual(vm.$store.getters.changedFiles[i].path);
});
- expect(commitMessage.tagName).toEqual('TEXTAREA');
- expect(commitMessage.name).toEqual('commit-message');
- expect(submitCommit.type).toEqual('submit');
expect(submitCommit.disabled).toBeTruthy();
expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeFalsy();
expect(vm.$el.querySelector('.commit-summary').textContent.trim()).toEqual('Commit 2 files');
expect(targetBranch.querySelector(':scope > label').textContent.trim()).toEqual('Target branch');
- expect(targetBranch.querySelector('.help-block').textContent.trim()).toEqual(branch);
- });
-
- it('does not render if not isCommitable', () => {
- RepoStore.isCommitable = false;
- RepoStore.openedFiles = [{
- id: 0,
- changed: true,
- }];
-
- const vm = createComponent();
-
- expect(vm.$el.innerHTML).toBeFalsy();
- });
-
- it('does not render if no changedFiles', () => {
- RepoStore.isCommitable = true;
- RepoStore.openedFiles = [];
-
- const vm = createComponent();
-
- expect(vm.$el.innerHTML).toBeFalsy();
+ expect(targetBranch.querySelector('.help-block').textContent.trim()).toEqual('master');
});
describe('when submitting', () => {
- let el;
- let vm;
- const projectId = 'projectId';
- const commitMessage = 'commitMessage';
-
- beforeEach((done) => {
- RepoStore.isCommitable = true;
- RepoStore.currentBranch = branch;
- RepoStore.targetBranch = branch;
- RepoStore.openedFiles = openedFiles;
- RepoStore.projectId = projectId;
-
- // We need to append to body to get form `submit` events working
- // Otherwise we run into, "Form submission canceled because the form is not connected"
- // See https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm
- el = document.createElement('div');
- document.body.appendChild(el);
-
- vm = createComponent(el);
- vm.commitMessage = commitMessage;
-
- spyOn(vm, 'tryCommit').and.callThrough();
- spyOn(vm, 'redirectToNewMr').and.stub();
- spyOn(vm, 'redirectToBranch').and.stub();
- spyOn(RepoService, 'commitFiles').and.returnValue(Promise.resolve());
- spyOn(RepoService, 'getBranch').and.returnValue(Promise.resolve({
- commit: {
- id: 1,
- short_id: 1,
- },
- }));
-
- // Wait for the vm data to be in place
- Vue.nextTick(() => {
- done();
- });
- });
+ let changedFiles;
- afterEach(() => {
- vm.$destroy();
- el.remove();
- RepoStore.openedFiles = [];
- });
+ beforeEach(() => {
+ vm.commitMessage = 'testing';
+ changedFiles = JSON.parse(JSON.stringify(vm.$store.getters.changedFiles));
- it('shows commit message', () => {
- const commitMessageEl = vm.$el.querySelector('#commit-message');
- expect(commitMessageEl.value).toBe(commitMessage);
+ spyOn(service, 'commit').and.returnValue(Promise.resolve({
+ short_id: '1',
+ stats: {},
+ }));
});
it('allows you to submit', () => {
- const submitCommit = vm.$refs.submitCommit;
- expect(submitCommit.disabled).toBeFalsy();
+ expect(vm.$el.querySelector('.btn').disabled).toBeTruthy();
});
- it('shows commit submit and summary if commitMessage and spinner if submitCommitsLoading', (done) => {
- const submitCommit = vm.$refs.submitCommit;
- submitCommit.click();
+ it('submits commit', (done) => {
+ vm.makeCommit();
// Wait for the branch check to finish
getSetTimeoutPromise()
.then(() => Vue.nextTick())
.then(() => {
- expect(vm.tryCommit).toHaveBeenCalled();
- expect(submitCommit.querySelector('.js-commit-loading-icon')).toBeTruthy();
- expect(vm.redirectToBranch).toHaveBeenCalled();
-
- const args = RepoService.commitFiles.calls.allArgs()[0];
- const { commit_message, actions, branch: payloadBranch } = args[0];
+ const args = service.commit.calls.allArgs()[0];
+ const { commit_message, actions, branch: payloadBranch } = args[1];
- expect(commit_message).toBe(commitMessage);
+ expect(commit_message).toBe('testing');
expect(actions.length).toEqual(2);
- expect(payloadBranch).toEqual(branch);
+ expect(payloadBranch).toEqual('master');
expect(actions[0].action).toEqual('update');
expect(actions[1].action).toEqual('update');
- expect(actions[0].content).toEqual(openedFiles[0].newContent);
- expect(actions[1].content).toEqual(openedFiles[1].newContent);
- expect(actions[0].file_path).toEqual(openedFiles[0].path);
- expect(actions[1].file_path).toEqual(openedFiles[1].path);
+ expect(actions[0].content).toEqual(changedFiles[0].content);
+ expect(actions[1].content).toEqual(changedFiles[1].content);
+ expect(actions[0].file_path).toEqual(changedFiles[0].path);
+ expect(actions[1].file_path).toEqual(changedFiles[1].path);
})
.then(done)
.catch(done.fail);
});
it('redirects to MR creation page if start new MR checkbox checked', (done) => {
+ spyOn(gl.utils, 'visitUrl');
vm.startNewMR = true;
- Vue.nextTick()
- .then(() => {
- const submitCommit = vm.$refs.submitCommit;
- submitCommit.click();
- })
- // Wait for the branch check to finish
- .then(() => getSetTimeoutPromise())
+ vm.makeCommit();
+
+ getSetTimeoutPromise()
+ .then(() => Vue.nextTick())
.then(() => {
- expect(vm.redirectToNewMr).toHaveBeenCalled();
+ expect(gl.utils.visitUrl).toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
});
});
-
- describe('methods', () => {
- describe('resetCommitState', () => {
- it('should reset store vars and scroll to top', () => {
- const vm = {
- submitCommitsLoading: true,
- changedFiles: new Array(10),
- openedFiles: new Array(3),
- commitMessage: 'commitMessage',
- editMode: true,
- };
-
- repoCommitSection.methods.resetCommitState.call(vm);
-
- expect(vm.submitCommitsLoading).toEqual(false);
- expect(vm.changedFiles).toEqual([]);
- expect(vm.commitMessage).toEqual('');
- expect(vm.editMode).toEqual(false);
- });
- });
- });
});
diff --git a/spec/javascripts/repo/components/repo_edit_button_spec.js b/spec/javascripts/repo/components/repo_edit_button_spec.js
index dff2fac191d..44018464b35 100644
--- a/spec/javascripts/repo/components/repo_edit_button_spec.js
+++ b/spec/javascripts/repo/components/repo_edit_button_spec.js
@@ -1,45 +1,83 @@
import Vue from 'vue';
+import store from '~/repo/stores';
import repoEditButton from '~/repo/components/repo_edit_button.vue';
-import RepoStore from '~/repo/stores/repo_store';
+import { file, resetStore } from '../helpers';
describe('RepoEditButton', () => {
- function createComponent() {
+ let vm;
+
+ beforeEach(() => {
+ const f = file();
const RepoEditButton = Vue.extend(repoEditButton);
- return new RepoEditButton().$mount();
- }
+ vm = new RepoEditButton({
+ store,
+ });
+
+ f.active = true;
+ vm.$store.dispatch('setInitialData', {
+ canCommit: true,
+ onTopOfBranch: true,
+ });
+ vm.$store.state.openFiles.push(f);
+ });
afterEach(() => {
- RepoStore.openedFiles = [];
+ vm.$destroy();
+
+ resetStore(vm.$store);
});
- it('renders an edit button that toggles the view state', (done) => {
- RepoStore.isCommitable = true;
- RepoStore.changedFiles = [];
- RepoStore.binary = false;
- RepoStore.openedFiles = [{}, {}];
+ it('renders an edit button', () => {
+ vm.$mount();
+
+ expect(vm.$el.querySelector('.btn')).not.toBeNull();
+ expect(vm.$el.querySelector('.btn').textContent.trim()).toBe('Edit');
+ });
- const vm = createComponent();
+ it('renders edit button with cancel text', () => {
+ vm.$store.state.editMode = true;
- expect(vm.$el.tagName).toEqual('BUTTON');
- expect(vm.$el.textContent).toMatch('Edit');
+ vm.$mount();
- spyOn(vm, 'editCancelClicked').and.callThrough();
+ expect(vm.$el.querySelector('.btn')).not.toBeNull();
+ expect(vm.$el.querySelector('.btn').textContent.trim()).toBe('Cancel edit');
+ });
- vm.$el.click();
+ it('toggles edit mode on click', (done) => {
+ vm.$mount();
+
+ vm.$el.querySelector('.btn').click();
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.btn').textContent.trim()).toBe('Cancel edit');
- Vue.nextTick(() => {
- expect(vm.editCancelClicked).toHaveBeenCalled();
- expect(vm.$el.textContent).toMatch('Cancel edit');
done();
});
});
- it('does not render if not isCommitable', () => {
- RepoStore.isCommitable = false;
+ describe('discardPopupOpen', () => {
+ beforeEach(() => {
+ vm.$store.state.discardPopupOpen = true;
+ vm.$store.state.editMode = true;
+ vm.$store.state.openFiles[0].changed = true;
+
+ vm.$mount();
+ });
+
+ it('renders popup', () => {
+ expect(vm.$el.querySelector('.modal')).not.toBeNull();
+ });
+
+ it('removes all changed files', (done) => {
+ vm.$el.querySelector('.btn-warning').click();
- const vm = createComponent();
+ vm.$nextTick(() => {
+ expect(vm.$store.getters.changedFiles.length).toBe(0);
+ expect(vm.$el.querySelector('.modal')).toBeNull();
- expect(vm.$el.innerHTML).toBeUndefined();
+ done();
+ });
+ });
});
});
diff --git a/spec/javascripts/repo/components/repo_editor_spec.js b/spec/javascripts/repo/components/repo_editor_spec.js
index a25a600b3be..979d2185076 100644
--- a/spec/javascripts/repo/components/repo_editor_spec.js
+++ b/spec/javascripts/repo/components/repo_editor_spec.js
@@ -1,54 +1,56 @@
import Vue from 'vue';
-import RepoStore from '~/repo/stores/repo_store';
+import store from '~/repo/stores';
import repoEditor from '~/repo/components/repo_editor.vue';
+import { file, resetStore } from '../helpers';
describe('RepoEditor', () => {
+ let vm;
+
beforeEach(() => {
+ const f = file();
const RepoEditor = Vue.extend(repoEditor);
- this.vm = new RepoEditor().$mount();
+ vm = new RepoEditor({
+ store,
+ });
+
+ f.active = true;
+ f.tempFile = true;
+ vm.$store.state.openFiles.push(f);
+ vm.$store.getters.activeFile.html = 'testing';
+ vm.monaco = true;
+
+ vm.$mount();
});
afterEach(() => {
- RepoStore.openedFiles = [];
+ vm.$destroy();
+
+ resetStore(vm.$store);
});
it('renders an ide container', (done) => {
- this.vm.openedFiles = ['idiidid'];
- this.vm.binary = false;
-
Vue.nextTick(() => {
- expect(this.vm.shouldHideEditor).toBe(false);
- expect(this.vm.$el.id).toEqual('ide');
- expect(this.vm.$el.tagName).toBe('DIV');
+ expect(vm.shouldHideEditor).toBeFalsy();
+ expect(vm.$el.textContent.trim()).toBe('');
+
done();
});
});
- describe('when there are no open files', () => {
- it('does not render the ide', (done) => {
- this.vm.openedFiles = [];
+ describe('when open file is binary and not raw', () => {
+ beforeEach((done) => {
+ vm.$store.getters.activeFile.binary = true;
- Vue.nextTick(() => {
- expect(this.vm.shouldHideEditor).toBe(true);
- expect(this.vm.$el.tagName).not.toBeDefined();
- done();
- });
+ Vue.nextTick(done);
});
- });
- describe('when open file is binary and not raw', () => {
- it('does not render the IDE', (done) => {
- this.vm.binary = true;
- this.vm.activeFile = {
- raw: false,
- };
-
- Vue.nextTick(() => {
- expect(this.vm.shouldHideEditor).toBe(true);
- expect(this.vm.$el.tagName).not.toBeDefined();
- done();
- });
+ it('does not render the IDE', () => {
+ expect(vm.shouldHideEditor).toBeTruthy();
+ });
+
+ it('shows activeFile html', () => {
+ expect(vm.$el.textContent.trim()).toBe('testing');
});
});
});
diff --git a/spec/javascripts/repo/components/repo_file_buttons_spec.js b/spec/javascripts/repo/components/repo_file_buttons_spec.js
index 111c83ee50d..d6e255e4810 100644
--- a/spec/javascripts/repo/components/repo_file_buttons_spec.js
+++ b/spec/javascripts/repo/components/repo_file_buttons_spec.js
@@ -1,72 +1,49 @@
import Vue from 'vue';
+import store from '~/repo/stores';
import repoFileButtons from '~/repo/components/repo_file_buttons.vue';
-import RepoStore from '~/repo/stores/repo_store';
+import { file, resetStore } from '../helpers';
describe('RepoFileButtons', () => {
- const activeFile = {
- extension: 'md',
- url: 'url',
- raw_path: 'raw_path',
- blame_path: 'blame_path',
- commits_path: 'commits_path',
- permalink: 'permalink',
- };
+ const activeFile = file();
+ let vm;
function createComponent() {
const RepoFileButtons = Vue.extend(repoFileButtons);
- return new RepoFileButtons().$mount();
+ activeFile.rawPath = 'test';
+ activeFile.blamePath = 'test';
+ activeFile.commitsPath = 'test';
+ activeFile.active = true;
+ store.state.openFiles.push(activeFile);
+
+ return new RepoFileButtons({
+ store,
+ }).$mount();
}
afterEach(() => {
- RepoStore.openedFiles = [];
- });
-
- it('renders Raw, Blame, History, Permalink and Preview toggle', () => {
- const activeFileLabel = 'activeFileLabel';
- RepoStore.openedFiles = new Array(1);
- RepoStore.activeFile = activeFile;
- RepoStore.activeFileLabel = activeFileLabel;
- RepoStore.editMode = true;
- RepoStore.binary = false;
+ vm.$destroy();
- const vm = createComponent();
- const raw = vm.$el.querySelector('.raw');
- const blame = vm.$el.querySelector('.blame');
- const history = vm.$el.querySelector('.history');
-
- expect(raw.href).toMatch(`/${activeFile.raw_path}`);
- expect(raw.textContent.trim()).toEqual('Raw');
- expect(blame.href).toMatch(`/${activeFile.blame_path}`);
- expect(blame.textContent.trim()).toEqual('Blame');
- expect(history.href).toMatch(`/${activeFile.commits_path}`);
- expect(history.textContent.trim()).toEqual('History');
- expect(vm.$el.querySelector('.permalink').textContent.trim()).toEqual('Permalink');
- expect(vm.$el.querySelector('.preview').textContent.trim()).toEqual(activeFileLabel);
+ resetStore(vm.$store);
});
- it('triggers rawPreviewToggle on preview click', () => {
- RepoStore.openedFiles = new Array(1);
- RepoStore.activeFile = activeFile;
- RepoStore.editMode = true;
-
- const vm = createComponent();
- const preview = vm.$el.querySelector('.preview');
-
- spyOn(vm, 'rawPreviewToggle');
-
- preview.click();
-
- expect(vm.rawPreviewToggle).toHaveBeenCalled();
- });
+ it('renders Raw, Blame, History, Permalink and Preview toggle', (done) => {
+ vm = createComponent();
- it('does not render preview toggle if not canPreview', () => {
- activeFile.extension = 'js';
- RepoStore.openedFiles = new Array(1);
- RepoStore.activeFile = activeFile;
+ vm.$nextTick(() => {
+ const raw = vm.$el.querySelector('.raw');
+ const blame = vm.$el.querySelector('.blame');
+ const history = vm.$el.querySelector('.history');
- const vm = createComponent();
+ expect(raw.href).toMatch(`/${activeFile.rawPath}`);
+ expect(raw.textContent.trim()).toEqual('Raw');
+ expect(blame.href).toMatch(`/${activeFile.blamePath}`);
+ expect(blame.textContent.trim()).toEqual('Blame');
+ expect(history.href).toMatch(`/${activeFile.commitsPath}`);
+ expect(history.textContent.trim()).toEqual('History');
+ expect(vm.$el.querySelector('.permalink').textContent.trim()).toEqual('Permalink');
- expect(vm.$el.querySelector('.preview')).toBeFalsy();
+ done();
+ });
});
});
diff --git a/spec/javascripts/repo/components/repo_file_spec.js b/spec/javascripts/repo/components/repo_file_spec.js
index 8403df9be64..c45f8a18d1f 100644
--- a/spec/javascripts/repo/components/repo_file_spec.js
+++ b/spec/javascripts/repo/components/repo_file_spec.js
@@ -1,32 +1,29 @@
import Vue from 'vue';
+import store from '~/repo/stores';
import repoFile from '~/repo/components/repo_file.vue';
-import RepoStore from '~/repo/stores/repo_store';
-import eventHub from '~/repo/event_hub';
-import { file } from '../mock_data';
+import { file, resetStore } from '../helpers';
describe('RepoFile', () => {
const updated = 'updated';
- const otherFile = {
- id: 'test',
- html: '<p class="file-content">html</p>',
- pageTitle: 'otherpageTitle',
- };
+ let vm;
function createComponent(propsData) {
const RepoFile = Vue.extend(repoFile);
return new RepoFile({
+ store,
propsData,
}).$mount();
}
- beforeEach(() => {
- RepoStore.openedFiles = [];
+ afterEach(() => {
+ resetStore(vm.$store);
});
it('renders link, icon, name and last commit details', () => {
const RepoFile = Vue.extend(repoFile);
- const vm = new RepoFile({
+ vm = new RepoFile({
+ store,
propsData: {
file: file(),
},
@@ -47,23 +44,17 @@ describe('RepoFile', () => {
});
it('does render if hasFiles is true and is loading tree', () => {
- const vm = createComponent({
+ vm = createComponent({
file: file(),
});
expect(vm.$el.querySelector('.fa-spin.fa-spinner')).toBeFalsy();
});
- it('sets the document title correctly', () => {
- RepoStore.setActiveFiles(otherFile);
-
- expect(document.title.trim()).toEqual(otherFile.pageTitle);
- });
-
it('renders a spinner if the file is loading', () => {
const f = file();
f.loading = true;
- const vm = createComponent({
+ vm = createComponent({
file: f,
});
@@ -71,32 +62,34 @@ describe('RepoFile', () => {
expect(vm.$el.querySelector('.fa-spin.fa-spinner').style.marginLeft).toEqual(`${vm.file.level * 16}px`);
});
- it('does not render commit message and datetime if mini', () => {
- RepoStore.openedFiles.push(file());
-
- const vm = createComponent({
+ it('does not render commit message and datetime if mini', (done) => {
+ vm = createComponent({
file: file(),
});
+ vm.$store.state.openFiles.push(vm.file);
- expect(vm.$el.querySelector('.commit-message')).toBeFalsy();
- expect(vm.$el.querySelector('.commit-update')).toBeFalsy();
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.commit-message')).toBeFalsy();
+ expect(vm.$el.querySelector('.commit-update')).toBeFalsy();
+
+ done();
+ });
});
- it('fires linkClicked when the link is clicked', () => {
- const vm = createComponent({
+ it('fires clickedTreeRow when the link is clicked', () => {
+ vm = createComponent({
file: file(),
});
- spyOn(vm, 'linkClicked');
+ spyOn(vm, 'clickedTreeRow');
vm.$el.click();
- expect(vm.linkClicked).toHaveBeenCalledWith(vm.file);
+ expect(vm.clickedTreeRow).toHaveBeenCalledWith(vm.file);
});
describe('submodule', () => {
let f;
- let vm;
beforeEach(() => {
f = file('submodule name', '123456789');
@@ -119,20 +112,4 @@ describe('RepoFile', () => {
expect(vm.$el.querySelector('td').textContent.replace(/\s+/g, ' ')).toContain('submodule name @ 12345678');
});
});
-
- describe('methods', () => {
- describe('linkClicked', () => {
- it('$emits fileNameClicked with file obj', () => {
- spyOn(eventHub, '$emit');
-
- const vm = createComponent({
- file: file(),
- });
-
- vm.linkClicked(vm.file);
-
- expect(eventHub.$emit).toHaveBeenCalledWith('fileNameClicked', vm.file);
- });
- });
- });
});
diff --git a/spec/javascripts/repo/components/repo_loading_file_spec.js b/spec/javascripts/repo/components/repo_loading_file_spec.js
index e9f95a02028..031f2a9c0b2 100644
--- a/spec/javascripts/repo/components/repo_loading_file_spec.js
+++ b/spec/javascripts/repo/components/repo_loading_file_spec.js
@@ -1,13 +1,16 @@
import Vue from 'vue';
-import RepoStore from '~/repo/stores/repo_store';
+import store from '~/repo/stores';
import repoLoadingFile from '~/repo/components/repo_loading_file.vue';
+import { resetStore } from '../helpers';
describe('RepoLoadingFile', () => {
- function createComponent(propsData) {
+ let vm;
+
+ function createComponent() {
const RepoLoadingFile = Vue.extend(repoLoadingFile);
return new RepoLoadingFile({
- propsData,
+ store,
}).$mount();
}
@@ -30,33 +33,30 @@ describe('RepoLoadingFile', () => {
}
afterEach(() => {
- RepoStore.openedFiles = [];
+ vm.$destroy();
+
+ resetStore(vm.$store);
});
it('renders 3 columns of animated LoC', () => {
- const vm = createComponent({
- loading: {
- tree: true,
- },
- hasFiles: false,
- });
+ vm = createComponent();
const columns = [...vm.$el.querySelectorAll('td')];
expect(columns.length).toEqual(3);
assertColumns(columns);
});
- it('renders 1 column of animated LoC if isMini', () => {
- RepoStore.openedFiles = new Array(1);
- const vm = createComponent({
- loading: {
- tree: true,
- },
- hasFiles: false,
- });
- const columns = [...vm.$el.querySelectorAll('td')];
+ it('renders 1 column of animated LoC if isMini', (done) => {
+ vm = createComponent();
+ vm.$store.state.openFiles.push('test');
- expect(columns.length).toEqual(1);
- assertColumns(columns);
+ vm.$nextTick(() => {
+ const columns = [...vm.$el.querySelectorAll('td')];
+
+ expect(columns.length).toEqual(1);
+ assertColumns(columns);
+
+ done();
+ });
});
});
diff --git a/spec/javascripts/repo/components/repo_prev_directory_spec.js b/spec/javascripts/repo/components/repo_prev_directory_spec.js
index 4c064f21084..7f82ae36a64 100644
--- a/spec/javascripts/repo/components/repo_prev_directory_spec.js
+++ b/spec/javascripts/repo/components/repo_prev_directory_spec.js
@@ -1,47 +1,45 @@
import Vue from 'vue';
+import store from '~/repo/stores';
import repoPrevDirectory from '~/repo/components/repo_prev_directory.vue';
-import eventHub from '~/repo/event_hub';
+import { resetStore } from '../helpers';
describe('RepoPrevDirectory', () => {
- function createComponent(propsData) {
+ let vm;
+ const parentLink = 'parent';
+ function createComponent() {
const RepoPrevDirectory = Vue.extend(repoPrevDirectory);
- return new RepoPrevDirectory({
- propsData,
- }).$mount();
- }
-
- it('renders a prev dir link', () => {
- const prevUrl = 'prevUrl';
- const vm = createComponent({
- prevUrl,
+ const comp = new RepoPrevDirectory({
+ store,
});
- const link = vm.$el.querySelector('a');
- spyOn(vm, 'linkClicked');
+ comp.$store.state.parentTreeUrl = parentLink;
- expect(link.href).toMatch(`/${prevUrl}`);
- expect(link.textContent).toEqual('...');
+ return comp.$mount();
+ }
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
- link.click();
+ afterEach(() => {
+ vm.$destroy();
- expect(vm.linkClicked).toHaveBeenCalledWith(prevUrl);
+ resetStore(vm.$store);
});
- describe('methods', () => {
- describe('linkClicked', () => {
- it('$emits linkclicked with prevUrl', () => {
- const prevUrl = 'prevUrl';
- const vm = createComponent({
- prevUrl,
- });
+ it('renders a prev dir link', () => {
+ const link = vm.$el.querySelector('a');
- spyOn(eventHub, '$emit');
+ expect(link.href).toMatch(`/${parentLink}`);
+ expect(link.textContent).toEqual('...');
+ });
- vm.linkClicked(prevUrl);
+ it('clicking row triggers getTreeData', () => {
+ spyOn(vm, 'getTreeData');
- expect(eventHub.$emit).toHaveBeenCalledWith('goToPreviousDirectoryClicked', prevUrl);
- });
- });
+ vm.$el.querySelector('td').click();
+
+ expect(vm.getTreeData).toHaveBeenCalledWith({ endpoint: parentLink });
});
});
diff --git a/spec/javascripts/repo/components/repo_preview_spec.js b/spec/javascripts/repo/components/repo_preview_spec.js
index 4920cf02083..8d1a87494cf 100644
--- a/spec/javascripts/repo/components/repo_preview_spec.js
+++ b/spec/javascripts/repo/components/repo_preview_spec.js
@@ -1,23 +1,37 @@
import Vue from 'vue';
+import store from '~/repo/stores';
import repoPreview from '~/repo/components/repo_preview.vue';
-import RepoStore from '~/repo/stores/repo_store';
+import { file, resetStore } from '../helpers';
describe('RepoPreview', () => {
+ let vm;
+
function createComponent() {
+ const f = file();
const RepoPreview = Vue.extend(repoPreview);
- return new RepoPreview().$mount();
+ const comp = new RepoPreview({
+ store,
+ });
+
+ f.active = true;
+ f.html = 'test';
+
+ comp.$store.state.openFiles.push(f);
+
+ return comp.$mount();
}
- it('renders a div with the activeFile html', () => {
- const activeFile = {
- html: '<p class="file-content">html</p>',
- };
- RepoStore.activeFile = activeFile;
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
- const vm = createComponent();
+ it('renders a div with the activeFile html', () => {
+ vm = createComponent();
expect(vm.$el.tagName).toEqual('DIV');
- expect(vm.$el.innerHTML).toContain(activeFile.html);
+ expect(vm.$el.innerHTML).toContain('test');
});
});
diff --git a/spec/javascripts/repo/components/repo_sidebar_spec.js b/spec/javascripts/repo/components/repo_sidebar_spec.js
index 148f275e03d..7cb4dace491 100644
--- a/spec/javascripts/repo/components/repo_sidebar_spec.js
+++ b/spec/javascripts/repo/components/repo_sidebar_spec.js
@@ -1,32 +1,31 @@
import Vue from 'vue';
-import Helper from '~/repo/helpers/repo_helper';
-import RepoService from '~/repo/services/repo_service';
-import RepoStore from '~/repo/stores/repo_store';
+import store from '~/repo/stores';
import repoSidebar from '~/repo/components/repo_sidebar.vue';
-import { file } from '../mock_data';
+import { file, resetStore } from '../helpers';
describe('RepoSidebar', () => {
let vm;
- function createComponent() {
+ beforeEach(() => {
const RepoSidebar = Vue.extend(repoSidebar);
- return new RepoSidebar().$mount();
- }
+ vm = new RepoSidebar({
+ store,
+ });
+
+ vm.$store.state.isRoot = true;
+ vm.$store.state.tree.push(file());
+
+ vm.$mount();
+ });
afterEach(() => {
vm.$destroy();
- RepoStore.files = [];
- RepoStore.openedFiles = [];
+ resetStore(vm.$store);
});
it('renders a sidebar', () => {
- RepoStore.files = [file()];
- RepoStore.openedFiles = [];
- RepoStore.isRoot = true;
-
- vm = createComponent();
const thead = vm.$el.querySelector('thead');
const tbody = vm.$el.querySelector('tbody');
@@ -41,139 +40,36 @@ describe('RepoSidebar', () => {
expect(tbody.querySelector('.file')).toBeTruthy();
});
- it('does not render a thead, renders repo-file-options and sets sidebar-mini class if isMini', () => {
- RepoStore.openedFiles = [{
- id: 0,
- }];
- vm = createComponent();
-
- expect(vm.$el.classList.contains('sidebar-mini')).toBeTruthy();
- expect(vm.$el.querySelector('thead')).toBeTruthy();
- expect(vm.$el.querySelector('thead .repo-file-options')).toBeTruthy();
- });
-
- it('renders 5 loading files if tree is loading and not hasFiles', () => {
- RepoStore.loading.tree = true;
- RepoStore.files = [];
- vm = createComponent();
+ it('does not render a thead, renders repo-file-options and sets sidebar-mini class if isMini', (done) => {
+ vm.$store.state.openFiles.push(vm.$store.state.tree[0]);
- expect(vm.$el.querySelectorAll('tbody .loading-file').length).toEqual(5);
- });
-
- it('renders a prev directory if is not root', () => {
- RepoStore.files = [file()];
- RepoStore.isRoot = false;
- RepoStore.loading.tree = false;
- vm = createComponent();
-
- expect(vm.$el.querySelector('tbody .prev-directory')).toBeTruthy();
- });
+ Vue.nextTick(() => {
+ expect(vm.$el.classList.contains('sidebar-mini')).toBeTruthy();
+ expect(vm.$el.querySelector('thead')).toBeTruthy();
+ expect(vm.$el.querySelector('thead .repo-file-options')).toBeTruthy();
- describe('flattendFiles', () => {
- it('returns a flattend array of files', () => {
- const f = file();
- f.files.push(file('testing 123'));
- const files = [f, file()];
- vm = createComponent();
- vm.files = files;
-
- expect(vm.flattendFiles.length).toBe(3);
- expect(vm.flattendFiles[1].name).toBe('testing 123');
+ done();
});
});
- describe('methods', () => {
- describe('fileClicked', () => {
- it('should fetch data for new file', () => {
- spyOn(Helper, 'getContent').and.callThrough();
- RepoStore.files = [file()];
- RepoStore.isRoot = true;
- vm = createComponent();
-
- vm.fileClicked(RepoStore.files[0]);
-
- expect(Helper.getContent).toHaveBeenCalledWith(RepoStore.files[0]);
- });
-
- it('should not fetch data for already opened files', () => {
- const f = file();
- spyOn(Helper, 'getFileFromPath').and.returnValue(f);
- spyOn(RepoStore, 'setActiveFiles');
- vm = createComponent();
- vm.fileClicked(f);
+ it('renders 5 loading files if tree is loading', (done) => {
+ vm.$store.state.tree = [];
+ vm.$store.state.loading = true;
- expect(RepoStore.setActiveFiles).toHaveBeenCalledWith(f);
- });
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelectorAll('tbody .loading-file').length).toEqual(5);
- it('should hide files in directory if already open', () => {
- spyOn(Helper, 'setDirectoryToClosed').and.callThrough();
- const f = file();
- f.opened = true;
- f.type = 'tree';
- RepoStore.files = [f];
- vm = createComponent();
-
- vm.fileClicked(RepoStore.files[0]);
-
- expect(Helper.setDirectoryToClosed).toHaveBeenCalledWith(RepoStore.files[0]);
- });
-
- describe('submodule', () => {
- it('opens submodule project URL', () => {
- spyOn(gl.utils, 'visitUrl');
-
- const f = file();
- f.type = 'submodule';
-
- vm = createComponent();
-
- vm.fileClicked(f);
-
- expect(gl.utils.visitUrl).toHaveBeenCalledWith('url');
- });
- });
- });
-
- describe('goToPreviousDirectoryClicked', () => {
- it('should hide files in directory if already open', () => {
- const prevUrl = 'foo/bar';
- vm = createComponent();
-
- vm.goToPreviousDirectoryClicked(prevUrl);
-
- expect(RepoService.url).toEqual(prevUrl);
- });
+ done();
});
+ });
- describe('back button', () => {
- beforeEach(() => {
- const f = file();
- const file2 = Object.assign({}, file());
- file2.url = 'test';
- RepoStore.files = [f, file2];
- RepoStore.openedFiles = [];
- RepoStore.isRoot = true;
-
- vm = createComponent();
- });
-
- it('render previous file when using back button', () => {
- spyOn(Helper, 'getContent').and.callThrough();
-
- vm.fileClicked(RepoStore.files[1]);
- expect(Helper.getContent).toHaveBeenCalledWith(RepoStore.files[1]);
-
- history.pushState({
- key: Math.random(),
- }, '', RepoStore.files[1].url);
- const popEvent = document.createEvent('Event');
- popEvent.initEvent('popstate', true, true);
- window.dispatchEvent(popEvent);
+ it('renders a prev directory if is not root', (done) => {
+ vm.$store.state.isRoot = false;
- expect(Helper.getContent.calls.mostRecent().args[0].url).toContain(RepoStore.files[1].url);
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('tbody .prev-directory')).toBeTruthy();
- window.history.pushState({}, null, '/');
- });
+ done();
});
});
});
diff --git a/spec/javascripts/repo/components/repo_spec.js b/spec/javascripts/repo/components/repo_spec.js
index 3558a155728..b32d2c13af8 100644
--- a/spec/javascripts/repo/components/repo_spec.js
+++ b/spec/javascripts/repo/components/repo_spec.js
@@ -1,9 +1,8 @@
import Vue from 'vue';
+import store from '~/repo/stores';
import repo from '~/repo/components/repo.vue';
-import RepoStore from '~/repo/stores/repo_store';
-import Service from '~/repo/services/repo_service';
-import eventHub from '~/repo/event_hub';
-import createComponent from '../../helpers/vue_mount_component_helper';
+import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
+import { file, resetStore } from '../helpers';
describe('repo component', () => {
let vm;
@@ -11,86 +10,26 @@ describe('repo component', () => {
beforeEach(() => {
const Component = Vue.extend(repo);
- RepoStore.currentBranch = 'master';
-
- vm = createComponent(Component);
+ vm = createComponentWithStore(Component, store).$mount();
});
afterEach(() => {
vm.$destroy();
- RepoStore.currentBranch = '';
+ resetStore(vm.$store);
});
- describe('createNewBranch', () => {
- beforeEach(() => {
- spyOn(history, 'pushState');
- });
-
- describe('success', () => {
- beforeEach(() => {
- spyOn(Service, 'createBranch').and.returnValue(Promise.resolve({
- data: {
- name: 'test',
- },
- }));
- });
-
- it('calls createBranch with branchName', () => {
- eventHub.$emit('createNewBranch', 'test');
-
- expect(Service.createBranch).toHaveBeenCalledWith({
- branch: 'test',
- ref: RepoStore.currentBranch,
- });
- });
-
- it('pushes new history state', (done) => {
- RepoStore.currentBranch = 'master';
-
- spyOn(vm, 'getCurrentLocation').and.returnValue('http://test.com/master');
-
- eventHub.$emit('createNewBranch', 'test');
-
- setTimeout(() => {
- expect(history.pushState).toHaveBeenCalledWith(jasmine.anything(), '', 'http://test.com/test');
- done();
- });
- });
-
- it('updates stores currentBranch', (done) => {
- eventHub.$emit('createNewBranch', 'test');
-
- setTimeout(() => {
- expect(RepoStore.currentBranch).toBe('test');
-
- done();
- });
- });
- });
-
- describe('failure', () => {
- beforeEach(() => {
- spyOn(Service, 'createBranch').and.returnValue(Promise.reject({
- response: {
- data: {
- message: 'test',
- },
- },
- }));
- });
-
- it('emits createNewBranchError event', (done) => {
- spyOn(eventHub, '$emit').and.callThrough();
+ it('does not render panel right when no files open', () => {
+ expect(vm.$el.querySelector('.panel-right')).toBeNull();
+ });
- eventHub.$emit('createNewBranch', 'test');
+ it('renders panel right when files are open', (done) => {
+ vm.$store.state.tree.push(file());
- setTimeout(() => {
- expect(eventHub.$emit).toHaveBeenCalledWith('createNewBranchError', 'test');
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.panel-right')).toBeNull();
- done();
- });
- });
+ done();
});
});
});
diff --git a/spec/javascripts/repo/components/repo_tab_spec.js b/spec/javascripts/repo/components/repo_tab_spec.js
index 37e297437f0..df0ca55aafc 100644
--- a/spec/javascripts/repo/components/repo_tab_spec.js
+++ b/spec/javascripts/repo/components/repo_tab_spec.js
@@ -1,47 +1,64 @@
import Vue from 'vue';
+import store from '~/repo/stores';
import repoTab from '~/repo/components/repo_tab.vue';
-import RepoStore from '~/repo/stores/repo_store';
+import { file, resetStore } from '../helpers';
describe('RepoTab', () => {
+ let vm;
+
function createComponent(propsData) {
const RepoTab = Vue.extend(repoTab);
return new RepoTab({
+ store,
propsData,
}).$mount();
}
+ afterEach(() => {
+ resetStore(vm.$store);
+ });
+
it('renders a close link and a name link', () => {
- const tab = {
- url: 'url',
- name: 'name',
- };
- const vm = createComponent({
- tab,
+ vm = createComponent({
+ tab: file(),
});
+ vm.$store.state.openFiles.push(vm.tab);
const close = vm.$el.querySelector('.close-btn');
- const name = vm.$el.querySelector(`a[title="${tab.url}"]`);
-
- spyOn(vm, 'closeTab');
- spyOn(vm, 'tabClicked');
+ const name = vm.$el.querySelector(`a[title="${vm.tab.url}"]`);
expect(close.querySelector('.fa-times')).toBeTruthy();
- expect(name.textContent.trim()).toEqual(tab.name);
+ expect(name.textContent.trim()).toEqual(vm.tab.name);
+ });
- close.click();
- name.click();
+ it('calls setFileActive when clicking tab', () => {
+ vm = createComponent({
+ tab: file(),
+ });
+
+ spyOn(vm, 'setFileActive');
+
+ vm.$el.click();
- expect(vm.closeTab).toHaveBeenCalledWith(tab);
- expect(vm.tabClicked).toHaveBeenCalledWith(tab);
+ expect(vm.setFileActive).toHaveBeenCalledWith(vm.tab);
+ });
+
+ it('calls closeFile when clicking close button', () => {
+ vm = createComponent({
+ tab: file(),
+ });
+
+ spyOn(vm, 'closeFile');
+
+ vm.$el.querySelector('.close-btn').click();
+
+ expect(vm.closeFile).toHaveBeenCalledWith({ file: vm.tab });
});
it('renders an fa-circle icon if tab is changed', () => {
- const tab = {
- url: 'url',
- name: 'name',
- changed: true,
- };
- const vm = createComponent({
+ const tab = file();
+ tab.changed = true;
+ vm = createComponent({
tab,
});
@@ -50,38 +67,41 @@ describe('RepoTab', () => {
describe('methods', () => {
describe('closeTab', () => {
- it('returns undefined and does not $emit if file is changed', () => {
- const tab = {
- url: 'url',
- name: 'name',
- changed: true,
- };
- const vm = createComponent({
+ it('does not close tab if is changed', (done) => {
+ const tab = file();
+ tab.changed = true;
+ tab.opened = true;
+ vm = createComponent({
tab,
});
-
- spyOn(RepoStore, 'removeFromOpenedFiles');
+ vm.$store.state.openFiles.push(tab);
+ vm.$store.dispatch('setFileActive', tab);
vm.$el.querySelector('.close-btn').click();
- expect(RepoStore.removeFromOpenedFiles).not.toHaveBeenCalled();
+ vm.$nextTick(() => {
+ expect(tab.opened).toBeTruthy();
+
+ done();
+ });
});
- it('$emits tabclosed event with file obj', () => {
- const tab = {
- url: 'url',
- name: 'name',
- changed: false,
- };
- const vm = createComponent({
+ it('closes tab when clicking close btn', (done) => {
+ const tab = file('lose');
+ tab.opened = true;
+ vm = createComponent({
tab,
});
-
- spyOn(RepoStore, 'removeFromOpenedFiles');
+ vm.$store.state.openFiles.push(tab);
+ vm.$store.dispatch('setFileActive', tab);
vm.$el.querySelector('.close-btn').click();
- expect(RepoStore.removeFromOpenedFiles).toHaveBeenCalledWith(tab);
+ vm.$nextTick(() => {
+ expect(tab.opened).toBeFalsy();
+
+ done();
+ });
});
});
});
diff --git a/spec/javascripts/repo/components/repo_tabs_spec.js b/spec/javascripts/repo/components/repo_tabs_spec.js
index 431129bc866..d0246cc72e6 100644
--- a/spec/javascripts/repo/components/repo_tabs_spec.js
+++ b/spec/javascripts/repo/components/repo_tabs_spec.js
@@ -1,35 +1,38 @@
import Vue from 'vue';
-import RepoStore from '~/repo/stores/repo_store';
+import store from '~/repo/stores';
import repoTabs from '~/repo/components/repo_tabs.vue';
+import { file, resetStore } from '../helpers';
describe('RepoTabs', () => {
- const openedFiles = [{
- id: 0,
- active: true,
- }, {
- id: 1,
- }];
+ const openedFiles = [file(), file()];
+ let vm;
function createComponent() {
const RepoTabs = Vue.extend(repoTabs);
- return new RepoTabs().$mount();
+ return new RepoTabs({
+ store,
+ }).$mount();
}
afterEach(() => {
- RepoStore.openedFiles = [];
+ resetStore(vm.$store);
});
- it('renders a list of tabs', () => {
- RepoStore.openedFiles = openedFiles;
+ it('renders a list of tabs', (done) => {
+ vm = createComponent();
+ openedFiles[0].active = true;
+ vm.$store.state.openFiles = openedFiles;
- const vm = createComponent();
- const tabs = [...vm.$el.querySelectorAll(':scope > li')];
+ vm.$nextTick(() => {
+ const tabs = [...vm.$el.querySelectorAll(':scope > li')];
- expect(vm.$el.id).toEqual('tabs');
- expect(tabs.length).toEqual(3);
- expect(tabs[0].classList.contains('active')).toBeTruthy();
- expect(tabs[1].classList.contains('active')).toBeFalsy();
- expect(tabs[2].classList.contains('tabs-divider')).toBeTruthy();
+ expect(tabs.length).toEqual(3);
+ expect(tabs[0].classList.contains('active')).toBeTruthy();
+ expect(tabs[1].classList.contains('active')).toBeFalsy();
+ expect(tabs[2].classList.contains('tabs-divider')).toBeTruthy();
+
+ done();
+ });
});
});
diff --git a/spec/javascripts/repo/helpers.js b/spec/javascripts/repo/helpers.js
new file mode 100644
index 00000000000..376c291c64b
--- /dev/null
+++ b/spec/javascripts/repo/helpers.js
@@ -0,0 +1,20 @@
+import { decorateData } from '~/repo/stores/utils';
+import state from '~/repo/stores/state';
+
+export const resetStore = (store) => {
+ store.replaceState(state());
+};
+
+export const file = (name = 'name', id = name, type = '') => decorateData({
+ id,
+ type,
+ icon: 'icon',
+ url: 'url',
+ name,
+ path: name,
+ last_commit: {
+ id: '123',
+ message: 'test',
+ committed_date: new Date().toISOString(),
+ },
+});
diff --git a/spec/javascripts/repo/mock_data.js b/spec/javascripts/repo/mock_data.js
deleted file mode 100644
index 71e275caf09..00000000000
--- a/spec/javascripts/repo/mock_data.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import RepoHelper from '~/repo/helpers/repo_helper';
-
-// eslint-disable-next-line import/prefer-default-export
-export const file = (name = 'name', id = name) => RepoHelper.serializeRepoEntity('blob', {
- id,
- icon: 'icon',
- url: 'url',
- name,
- last_commit: {
- id: '123',
- message: 'test',
- committed_date: new Date().toISOString(),
- },
-});
diff --git a/spec/javascripts/repo/services/repo_service_spec.js b/spec/javascripts/repo/services/repo_service_spec.js
deleted file mode 100644
index 6f530770525..00000000000
--- a/spec/javascripts/repo/services/repo_service_spec.js
+++ /dev/null
@@ -1,171 +0,0 @@
-import axios from 'axios';
-import RepoService from '~/repo/services/repo_service';
-import RepoStore from '~/repo/stores/repo_store';
-import Api from '~/api';
-
-describe('RepoService', () => {
- it('has default json format param', () => {
- expect(RepoService.options.params.format).toBe('json');
- });
-
- describe('buildParams', () => {
- let newParams;
- const url = 'url';
-
- beforeEach(() => {
- newParams = {};
-
- spyOn(Object, 'assign').and.returnValue(newParams);
- });
-
- it('clones params', () => {
- const params = RepoService.buildParams(url);
-
- expect(Object.assign).toHaveBeenCalledWith({}, RepoService.options.params);
-
- expect(params).toBe(newParams);
- });
-
- it('sets and returns viewer params to richif urlIsRichBlob is true', () => {
- spyOn(RepoService, 'urlIsRichBlob').and.returnValue(true);
-
- const params = RepoService.buildParams(url);
-
- expect(params.viewer).toEqual('rich');
- });
-
- it('returns params urlIsRichBlob is false', () => {
- spyOn(RepoService, 'urlIsRichBlob').and.returnValue(false);
-
- const params = RepoService.buildParams(url);
-
- expect(params.viewer).toBeUndefined();
- });
-
- it('calls urlIsRichBlob with the objects url prop if no url arg is provided', () => {
- spyOn(RepoService, 'urlIsRichBlob');
- RepoService.url = url;
-
- RepoService.buildParams();
-
- expect(RepoService.urlIsRichBlob).toHaveBeenCalledWith(url);
- });
- });
-
- describe('urlIsRichBlob', () => {
- it('returns true for md extension', () => {
- const isRichBlob = RepoService.urlIsRichBlob('url.md');
-
- expect(isRichBlob).toBeTruthy();
- });
-
- it('returns false for js extension', () => {
- const isRichBlob = RepoService.urlIsRichBlob('url.js');
-
- expect(isRichBlob).toBeFalsy();
- });
- });
-
- describe('getContent', () => {
- const params = {};
- const url = 'url';
- const requestPromise = Promise.resolve();
-
- beforeEach(() => {
- spyOn(RepoService, 'buildParams').and.returnValue(params);
- spyOn(axios, 'get').and.returnValue(requestPromise);
- });
-
- it('calls buildParams and axios.get', () => {
- const request = RepoService.getContent(url);
-
- expect(RepoService.buildParams).toHaveBeenCalledWith(url);
- expect(axios.get).toHaveBeenCalledWith(url, {
- params,
- });
- expect(request).toBe(requestPromise);
- });
-
- it('uses object url prop if no url arg is provided', () => {
- RepoService.url = url;
-
- RepoService.getContent();
-
- expect(axios.get).toHaveBeenCalledWith(url, {
- params,
- });
- });
- });
-
- describe('getBase64Content', () => {
- const url = 'url';
- const response = { data: 'data' };
-
- beforeEach(() => {
- spyOn(RepoService, 'bufferToBase64');
- spyOn(axios, 'get').and.returnValue(Promise.resolve(response));
- });
-
- it('calls axios.get and bufferToBase64 on completion', (done) => {
- const request = RepoService.getBase64Content(url);
-
- expect(axios.get).toHaveBeenCalledWith(url, {
- responseType: 'arraybuffer',
- });
- expect(request).toEqual(jasmine.any(Promise));
-
- request.then(() => {
- expect(RepoService.bufferToBase64).toHaveBeenCalledWith(response.data);
- done();
- }).catch(done.fail);
- });
- });
-
- describe('commitFiles', () => {
- it('calls commitMultiple and .then commitFlash', (done) => {
- const projectId = 'projectId';
- const payload = {};
- RepoStore.projectId = projectId;
-
- spyOn(Api, 'commitMultiple').and.returnValue(Promise.resolve());
- spyOn(RepoService, 'commitFlash');
-
- const apiPromise = RepoService.commitFiles(payload);
-
- expect(Api.commitMultiple).toHaveBeenCalledWith(projectId, payload);
-
- apiPromise.then(() => {
- expect(RepoService.commitFlash).toHaveBeenCalled();
- done();
- }).catch(done.fail);
- });
- });
-
- describe('commitFlash', () => {
- it('calls Flash with data.message', () => {
- const data = {
- message: 'message',
- };
- spyOn(window, 'Flash');
-
- RepoService.commitFlash(data);
-
- expect(window.Flash).toHaveBeenCalledWith(data.message);
- });
-
- it('calls Flash with success string if short_id and stats', () => {
- const data = {
- short_id: 'short_id',
- stats: {
- additions: '4',
- deletions: '5',
- },
- };
- spyOn(window, 'Flash');
-
- RepoService.commitFlash(data);
-
- expect(window.Flash).toHaveBeenCalledWith(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
- });
- });
-});
diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js
index cf811af3d6c..5e55a5d2686 100644
--- a/spec/javascripts/search_autocomplete_spec.js
+++ b/spec/javascripts/search_autocomplete_spec.js
@@ -3,7 +3,6 @@
import '~/gl_dropdown';
import '~/search_autocomplete';
import '~/lib/utils/common_utils';
-import 'vendor/fuzzaldrin-plus';
(function() {
var assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget;
diff --git a/spec/javascripts/sidebar/mock_data.js b/spec/javascripts/sidebar/mock_data.js
index e2b6bcabc98..0682b463043 100644
--- a/spec/javascripts/sidebar/mock_data.js
+++ b/spec/javascripts/sidebar/mock_data.js
@@ -109,12 +109,14 @@ const sidebarMockData = {
labels: [],
web_url: '/root/some-project/issues/5',
},
+ '/gitlab-org/gitlab-shell/issues/5/toggle_subscription': {},
},
};
export default {
mediator: {
endpoint: '/gitlab-org/gitlab-shell/issues/5.json',
+ toggleSubscriptionEndpoint: '/gitlab-org/gitlab-shell/issues/5/toggle_subscription',
moveIssueEndpoint: '/gitlab-org/gitlab-shell/issues/5/move',
projectsAutocompleteEndpoint: '/autocomplete/projects?project_id=15',
editable: true,
diff --git a/spec/javascripts/sidebar/participants_spec.js b/spec/javascripts/sidebar/participants_spec.js
new file mode 100644
index 00000000000..30cc549c7c0
--- /dev/null
+++ b/spec/javascripts/sidebar/participants_spec.js
@@ -0,0 +1,174 @@
+import Vue from 'vue';
+import participants from '~/sidebar/components/participants/participants.vue';
+import mountComponent from '../helpers/vue_mount_component_helper';
+
+const PARTICIPANT = {
+ id: 1,
+ state: 'active',
+ username: 'marcene',
+ name: 'Allie Will',
+ web_url: 'foo.com',
+ avatar_url: 'gravatar.com/avatar/xxx',
+};
+
+const PARTICIPANT_LIST = [
+ PARTICIPANT,
+ { ...PARTICIPANT, id: 2 },
+ { ...PARTICIPANT, id: 3 },
+];
+
+describe('Participants', function () {
+ let vm;
+ let Participants;
+
+ beforeEach(() => {
+ Participants = Vue.extend(participants);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('collapsed sidebar state', () => {
+ it('shows loading spinner when loading', () => {
+ vm = mountComponent(Participants, {
+ loading: true,
+ });
+
+ expect(vm.$el.querySelector('.js-participants-collapsed-loading-icon')).toBeDefined();
+ });
+
+ it('shows participant count when given', () => {
+ vm = mountComponent(Participants, {
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ });
+ const countEl = vm.$el.querySelector('.js-participants-collapsed-count');
+
+ expect(countEl.textContent.trim()).toBe(`${PARTICIPANT_LIST.length}`);
+ });
+
+ it('shows full participant count when there are hidden participants', () => {
+ vm = mountComponent(Participants, {
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants: 1,
+ });
+ const countEl = vm.$el.querySelector('.js-participants-collapsed-count');
+
+ expect(countEl.textContent.trim()).toBe(`${PARTICIPANT_LIST.length}`);
+ });
+ });
+
+ describe('expanded sidebar state', () => {
+ it('shows loading spinner when loading', () => {
+ vm = mountComponent(Participants, {
+ loading: true,
+ });
+
+ expect(vm.$el.querySelector('.js-participants-expanded-loading-icon')).toBeDefined();
+ });
+
+ it('when only showing visible participants, shows an avatar only for each participant under the limit', (done) => {
+ const numberOfLessParticipants = 2;
+ vm = mountComponent(Participants, {
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants,
+ });
+ vm.isShowingMoreParticipants = false;
+
+ Vue.nextTick()
+ .then(() => {
+ const participantEls = vm.$el.querySelectorAll('.js-participants-author');
+
+ expect(participantEls.length).toBe(numberOfLessParticipants);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('when only showing all participants, each has an avatar', (done) => {
+ const numberOfLessParticipants = 2;
+ vm = mountComponent(Participants, {
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants,
+ });
+ vm.isShowingMoreParticipants = true;
+
+ Vue.nextTick()
+ .then(() => {
+ const participantEls = vm.$el.querySelectorAll('.js-participants-author');
+
+ expect(participantEls.length).toBe(PARTICIPANT_LIST.length);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('does not have more participants link when they can all be shown', () => {
+ const numberOfLessParticipants = 100;
+ vm = mountComponent(Participants, {
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants,
+ });
+ const moreParticipantLink = vm.$el.querySelector('.js-toggle-participants-button');
+
+ expect(PARTICIPANT_LIST.length).toBeLessThan(numberOfLessParticipants);
+ expect(moreParticipantLink).toBeNull();
+ });
+
+ it('when too many participants, has more participants link to show more', (done) => {
+ vm = mountComponent(Participants, {
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants: 2,
+ });
+ vm.isShowingMoreParticipants = false;
+
+ Vue.nextTick()
+ .then(() => {
+ const moreParticipantLink = vm.$el.querySelector('.js-toggle-participants-button');
+
+ expect(moreParticipantLink.textContent.trim()).toBe('+ 1 more');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('when too many participants and already showing them, has more participants link to show less', (done) => {
+ vm = mountComponent(Participants, {
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants: 2,
+ });
+ vm.isShowingMoreParticipants = true;
+
+ Vue.nextTick()
+ .then(() => {
+ const moreParticipantLink = vm.$el.querySelector('.js-toggle-participants-button');
+
+ expect(moreParticipantLink.textContent.trim()).toBe('- show less');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('clicking more participants link emits event', () => {
+ vm = mountComponent(Participants, {
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants: 2,
+ });
+ const moreParticipantLink = vm.$el.querySelector('.js-toggle-participants-button');
+
+ expect(vm.isShowingMoreParticipants).toBe(false);
+
+ moreParticipantLink.click();
+
+ expect(vm.isShowingMoreParticipants).toBe(true);
+ });
+ });
+});
diff --git a/spec/javascripts/sidebar/sidebar_mediator_spec.js b/spec/javascripts/sidebar/sidebar_mediator_spec.js
index 3aa8ca5db0d..7deb1fd2118 100644
--- a/spec/javascripts/sidebar/sidebar_mediator_spec.js
+++ b/spec/javascripts/sidebar/sidebar_mediator_spec.js
@@ -57,8 +57,8 @@ describe('Sidebar mediator', () => {
.then(() => {
expect(this.mediator.service.getProjectsAutocomplete).toHaveBeenCalledWith(searchTerm);
expect(this.mediator.store.setAutocompleteProjects).toHaveBeenCalled();
- done();
})
+ .then(done)
.catch(done.fail);
});
@@ -72,8 +72,21 @@ describe('Sidebar mediator', () => {
.then(() => {
expect(this.mediator.service.moveIssue).toHaveBeenCalledWith(moveToProjectId);
expect(gl.utils.visitUrl).toHaveBeenCalledWith('/root/some-project/issues/5');
- done();
})
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('toggle subscription', (done) => {
+ this.mediator.store.setSubscribedState(false);
+ spyOn(this.mediator.service, 'toggleSubscription').and.callThrough();
+
+ this.mediator.toggleSubscription()
+ .then(() => {
+ expect(this.mediator.service.toggleSubscription).toHaveBeenCalled();
+ expect(this.mediator.store.subscribed).toEqual(true);
+ })
+ .then(done)
.catch(done.fail);
});
});
diff --git a/spec/javascripts/sidebar/sidebar_service_spec.js b/spec/javascripts/sidebar/sidebar_service_spec.js
index a4bd8ba8d88..7324d34d84a 100644
--- a/spec/javascripts/sidebar/sidebar_service_spec.js
+++ b/spec/javascripts/sidebar/sidebar_service_spec.js
@@ -7,6 +7,7 @@ describe('Sidebar service', () => {
Vue.http.interceptors.push(Mock.sidebarMockInterceptor);
this.service = new SidebarService({
endpoint: '/gitlab-org/gitlab-shell/issues/5.json',
+ toggleSubscriptionEndpoint: '/gitlab-org/gitlab-shell/issues/5/toggle_subscription',
moveIssueEndpoint: '/gitlab-org/gitlab-shell/issues/5/move',
projectsAutocompleteEndpoint: '/autocomplete/projects?project_id=15',
});
@@ -23,6 +24,7 @@ describe('Sidebar service', () => {
expect(resp).toBeDefined();
done();
})
+ .then(done)
.catch(done.fail);
});
@@ -30,8 +32,8 @@ describe('Sidebar service', () => {
this.service.update('issue[assignee_ids]', [1])
.then((resp) => {
expect(resp).toBeDefined();
- done();
})
+ .then(done)
.catch(done.fail);
});
@@ -39,8 +41,8 @@ describe('Sidebar service', () => {
this.service.getProjectsAutocomplete()
.then((resp) => {
expect(resp).toBeDefined();
- done();
})
+ .then(done)
.catch(done.fail);
});
@@ -48,8 +50,17 @@ describe('Sidebar service', () => {
this.service.moveIssue(123)
.then((resp) => {
expect(resp).toBeDefined();
- done();
})
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('toggles the subscription', (done) => {
+ this.service.toggleSubscription()
+ .then((resp) => {
+ expect(resp).toBeDefined();
+ })
+ .then(done)
.catch(done.fail);
});
});
diff --git a/spec/javascripts/sidebar/sidebar_store_spec.js b/spec/javascripts/sidebar/sidebar_store_spec.js
index 69eb3839d67..51dee64fb93 100644
--- a/spec/javascripts/sidebar/sidebar_store_spec.js
+++ b/spec/javascripts/sidebar/sidebar_store_spec.js
@@ -2,21 +2,36 @@ import SidebarStore from '~/sidebar/stores/sidebar_store';
import Mock from './mock_data';
import UsersMockHelper from '../helpers/user_mock_data_helper';
-describe('Sidebar store', () => {
- const assignee = {
- id: 2,
- name: 'gitlab user 2',
- username: 'gitlab2',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- };
-
- const anotherAssignee = {
- id: 3,
- name: 'gitlab user 3',
- username: 'gitlab3',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- };
+const ASSIGNEE = {
+ id: 2,
+ name: 'gitlab user 2',
+ username: 'gitlab2',
+ avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+};
+
+const ANOTHER_ASSINEE = {
+ id: 3,
+ name: 'gitlab user 3',
+ username: 'gitlab3',
+ avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+};
+
+const PARTICIPANT = {
+ id: 1,
+ state: 'active',
+ username: 'marcene',
+ name: 'Allie Will',
+ web_url: 'foo.com',
+ avatar_url: 'gravatar.com/avatar/xxx',
+};
+
+const PARTICIPANT_LIST = [
+ PARTICIPANT,
+ { ...PARTICIPANT, id: 2 },
+ { ...PARTICIPANT, id: 3 },
+];
+describe('Sidebar store', () => {
beforeEach(() => {
this.store = new SidebarStore({
currentUser: {
@@ -40,23 +55,23 @@ describe('Sidebar store', () => {
});
it('adds a new assignee', () => {
- this.store.addAssignee(assignee);
+ this.store.addAssignee(ASSIGNEE);
expect(this.store.assignees.length).toEqual(1);
});
it('removes an assignee', () => {
- this.store.removeAssignee(assignee);
+ this.store.removeAssignee(ASSIGNEE);
expect(this.store.assignees.length).toEqual(0);
});
it('finds an existent assignee', () => {
let foundAssignee;
- this.store.addAssignee(assignee);
- foundAssignee = this.store.findAssignee(assignee);
+ this.store.addAssignee(ASSIGNEE);
+ foundAssignee = this.store.findAssignee(ASSIGNEE);
expect(foundAssignee).toBeDefined();
- expect(foundAssignee).toEqual(assignee);
- foundAssignee = this.store.findAssignee(anotherAssignee);
+ expect(foundAssignee).toEqual(ASSIGNEE);
+ foundAssignee = this.store.findAssignee(ANOTHER_ASSINEE);
expect(foundAssignee).toBeUndefined();
});
@@ -65,6 +80,28 @@ describe('Sidebar store', () => {
expect(this.store.assignees.length).toEqual(0);
});
+ it('sets participants data', () => {
+ expect(this.store.participants.length).toEqual(0);
+
+ this.store.setParticipantsData({
+ participants: PARTICIPANT_LIST,
+ });
+
+ expect(this.store.isFetching.participants).toEqual(false);
+ expect(this.store.participants.length).toEqual(PARTICIPANT_LIST.length);
+ });
+
+ it('sets subcriptions data', () => {
+ expect(this.store.subscribed).toEqual(null);
+
+ this.store.setSubscriptionsData({
+ subscribed: true,
+ });
+
+ expect(this.store.isFetching.subscriptions).toEqual(false);
+ expect(this.store.subscribed).toEqual(true);
+ });
+
it('set assigned data', () => {
const users = {
assignees: UsersMockHelper.createNumberRandomUsers(3),
@@ -75,6 +112,14 @@ describe('Sidebar store', () => {
expect(this.store.assignees.length).toEqual(3);
});
+ it('sets fetching state', () => {
+ expect(this.store.isFetching.participants).toEqual(true);
+
+ this.store.setFetchingState('participants', false);
+
+ expect(this.store.isFetching.participants).toEqual(false);
+ });
+
it('set time tracking data', () => {
this.store.setTimeTrackingData(Mock.time);
expect(this.store.timeEstimate).toEqual(Mock.time.time_estimate);
@@ -90,6 +135,14 @@ describe('Sidebar store', () => {
expect(this.store.autocompleteProjects).toEqual(projects);
});
+ it('sets subscribed state', () => {
+ expect(this.store.subscribed).toEqual(null);
+
+ this.store.setSubscribedState(true);
+
+ expect(this.store.subscribed).toEqual(true);
+ });
+
it('set move to project ID', () => {
const projectId = 7;
this.store.setMoveToProjectId(projectId);
diff --git a/spec/javascripts/sidebar/sidebar_subscriptions_spec.js b/spec/javascripts/sidebar/sidebar_subscriptions_spec.js
new file mode 100644
index 00000000000..7adf22b0f1f
--- /dev/null
+++ b/spec/javascripts/sidebar/sidebar_subscriptions_spec.js
@@ -0,0 +1,36 @@
+import Vue from 'vue';
+import sidebarSubscriptions from '~/sidebar/components/subscriptions/sidebar_subscriptions.vue';
+import SidebarMediator from '~/sidebar/sidebar_mediator';
+import SidebarService from '~/sidebar/services/sidebar_service';
+import SidebarStore from '~/sidebar/stores/sidebar_store';
+import eventHub from '~/sidebar/event_hub';
+import mountComponent from '../helpers/vue_mount_component_helper';
+import Mock from './mock_data';
+
+describe('Sidebar Subscriptions', function () {
+ let vm;
+ let SidebarSubscriptions;
+
+ beforeEach(() => {
+ SidebarSubscriptions = Vue.extend(sidebarSubscriptions);
+ // Setup the stores, services, etc
+ // eslint-disable-next-line no-new
+ new SidebarMediator(Mock.mediator);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ SidebarService.singleton = null;
+ SidebarStore.singleton = null;
+ SidebarMediator.singleton = null;
+ });
+
+ it('calls the mediator toggleSubscription on event', () => {
+ spyOn(SidebarMediator.prototype, 'toggleSubscription').and.returnValue(Promise.resolve());
+ vm = mountComponent(SidebarSubscriptions, {});
+
+ eventHub.$emit('toggleSubscription');
+
+ expect(SidebarMediator.prototype.toggleSubscription).toHaveBeenCalled();
+ });
+});
diff --git a/spec/javascripts/sidebar/subscriptions_spec.js b/spec/javascripts/sidebar/subscriptions_spec.js
new file mode 100644
index 00000000000..9b33dd02fb9
--- /dev/null
+++ b/spec/javascripts/sidebar/subscriptions_spec.js
@@ -0,0 +1,42 @@
+import Vue from 'vue';
+import subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue';
+import mountComponent from '../helpers/vue_mount_component_helper';
+
+describe('Subscriptions', function () {
+ let vm;
+ let Subscriptions;
+
+ beforeEach(() => {
+ Subscriptions = Vue.extend(subscriptions);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('shows loading spinner when loading', () => {
+ vm = mountComponent(Subscriptions, {
+ loading: true,
+ subscribed: undefined,
+ });
+
+ expect(vm.$refs.loadingButton.loading).toBe(true);
+ expect(vm.$refs.loadingButton.label).toBeUndefined();
+ });
+
+ it('has "Subscribe" text when currently not subscribed', () => {
+ vm = mountComponent(Subscriptions, {
+ subscribed: false,
+ });
+
+ expect(vm.$refs.loadingButton.label).toBe('Subscribe');
+ });
+
+ it('has "Unsubscribe" text when currently not subscribed', () => {
+ vm = mountComponent(Subscriptions, {
+ subscribed: true,
+ });
+
+ expect(vm.$refs.loadingButton.label).toBe('Unsubscribe');
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
index 690665ae12c..33ed0cb4342 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
@@ -1,5 +1,4 @@
import Vue from 'vue';
-import { statusIconEntityMap } from '~/vue_shared/ci_status_icons';
import pipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline';
import mockData from '../mock_data';
@@ -29,14 +28,6 @@ describe('MRWidgetPipeline', () => {
});
describe('computed', () => {
- describe('svg', () => {
- it('should have the proper SVG icon', () => {
- const vm = createComponent({ pipeline: mockData.pipeline });
-
- expect(vm.svg).toEqual(statusIconEntityMap.icon_status_failed);
- });
- });
-
describe('hasPipeline', () => {
it('should return true when there is a pipeline', () => {
expect(Object.keys(mockData.pipeline).length).toBeGreaterThan(0);
@@ -142,6 +133,7 @@ describe('MRWidgetPipeline', () => {
Vue.nextTick(() => {
expect(el.querySelectorAll('.js-ci-error').length).toEqual(1);
expect(el.innerText).toContain('Could not connect to the CI server');
+ expect(el.querySelector('.ci-status-icon svg use').getAttribute('xlink:href')).toContain('status_failed');
done();
});
});
diff --git a/spec/javascripts/vue_shared/ci_action_icons_spec.js b/spec/javascripts/vue_shared/ci_action_icons_spec.js
deleted file mode 100644
index 3d53a5ab24d..00000000000
--- a/spec/javascripts/vue_shared/ci_action_icons_spec.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import getActionIcon from '~/vue_shared/ci_action_icons';
-import cancelSVG from 'icons/_icon_action_cancel.svg';
-import retrySVG from 'icons/_icon_action_retry.svg';
-import playSVG from 'icons/_icon_action_play.svg';
-import stopSVG from 'icons/_icon_action_stop.svg';
-
-describe('getActionIcon', () => {
- it('should return an empty string', () => {
- expect(getActionIcon()).toEqual('');
- });
-
- it('should return cancel svg', () => {
- expect(getActionIcon('icon_action_cancel')).toEqual(cancelSVG);
- });
-
- it('should return retry svg', () => {
- expect(getActionIcon('icon_action_retry')).toEqual(retrySVG);
- });
-
- it('should return play svg', () => {
- expect(getActionIcon('icon_action_play')).toEqual(playSVG);
- });
-
- it('should render stop svg', () => {
- expect(getActionIcon('icon_action_stop')).toEqual(stopSVG);
- });
-});
diff --git a/spec/javascripts/vue_shared/ci_status_icon_spec.js b/spec/javascripts/vue_shared/ci_status_icon_spec.js
deleted file mode 100644
index b6621d6054d..00000000000
--- a/spec/javascripts/vue_shared/ci_status_icon_spec.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import { borderlessStatusIconEntityMap, statusIconEntityMap } from '~/vue_shared/ci_status_icons';
-
-describe('CI status icons', () => {
- const statuses = [
- 'icon_status_canceled',
- 'icon_status_created',
- 'icon_status_failed',
- 'icon_status_manual',
- 'icon_status_pending',
- 'icon_status_running',
- 'icon_status_skipped',
- 'icon_status_success',
- 'icon_status_warning',
- ];
-
- it('should have a dictionary for borderless icons', () => {
- statuses.forEach((status) => {
- expect(borderlessStatusIconEntityMap[status]).toBeDefined();
- });
- });
-
- it('should have a dictionary for icons', () => {
- statuses.forEach((status) => {
- expect(statusIconEntityMap[status]).toBeDefined();
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/ci_badge_link_spec.js b/spec/javascripts/vue_shared/components/ci_badge_link_spec.js
index ba303738f71..8762ce9903b 100644
--- a/spec/javascripts/vue_shared/components/ci_badge_link_spec.js
+++ b/spec/javascripts/vue_shared/components/ci_badge_link_spec.js
@@ -11,63 +11,63 @@ describe('CI Badge Link Component', () => {
text: 'canceled',
label: 'canceled',
group: 'canceled',
- icon: 'icon_status_canceled',
+ icon: 'status_canceled',
details_path: 'status/canceled',
},
created: {
text: 'created',
label: 'created',
group: 'created',
- icon: 'icon_status_created',
+ icon: 'status_created',
details_path: 'status/created',
},
failed: {
text: 'failed',
label: 'failed',
group: 'failed',
- icon: 'icon_status_failed',
+ icon: 'status_failed',
details_path: 'status/failed',
},
manual: {
text: 'manual',
label: 'manual action',
group: 'manual',
- icon: 'icon_status_manual',
+ icon: 'status_manual',
details_path: 'status/manual',
},
pending: {
text: 'pending',
label: 'pending',
group: 'pending',
- icon: 'icon_status_pending',
+ icon: 'status_pending',
details_path: 'status/pending',
},
running: {
text: 'running',
label: 'running',
group: 'running',
- icon: 'icon_status_running',
+ icon: 'status_running',
details_path: 'status/running',
},
skipped: {
text: 'skipped',
label: 'skipped',
group: 'skipped',
- icon: 'icon_status_skipped',
+ icon: 'status_skipped',
details_path: 'status/skipped',
},
success_warining: {
text: 'passed',
label: 'passed',
group: 'success_with_warnings',
- icon: 'icon_status_warning',
+ icon: 'status_warning',
details_path: 'status/warning',
},
success: {
text: 'passed',
label: 'passed',
group: 'passed',
- icon: 'icon_status_success',
+ icon: 'status_success',
details_path: 'status/passed',
},
};
diff --git a/spec/javascripts/vue_shared/components/icon_spec.js b/spec/javascripts/vue_shared/components/icon_spec.js
new file mode 100644
index 00000000000..104da4473ce
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/icon_spec.js
@@ -0,0 +1,48 @@
+import Vue from 'vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('Sprite Icon Component', function () {
+ describe('Initialization', function () {
+ let icon;
+
+ beforeEach(function () {
+ const IconComponent = Vue.extend(Icon);
+
+ icon = mountComponent(IconComponent, {
+ name: 'test',
+ size: 99,
+ cssClasses: 'extraclasses',
+ });
+ });
+
+ afterEach(() => {
+ icon.$destroy();
+ });
+
+ it('should return a defined Vue component', function () {
+ expect(icon).toBeDefined();
+ });
+
+ it('should have <svg> as a child element', function () {
+ expect(icon.$el.tagName).toBe('svg');
+ });
+
+ it('should have <use> as a child element with the correct href', function () {
+ expect(icon.$el.firstChild.tagName).toBe('use');
+ expect(icon.$el.firstChild.getAttribute('xlink:href')).toBe(`${gon.sprite_icons}#test`);
+ });
+
+ it('should properly compute iconSizeClass', function () {
+ expect(icon.iconSizeClass).toBe('s99');
+ });
+
+ it('should properly render img css', function () {
+ const classList = icon.$el.classList;
+ const containsSizeClass = classList.contains('s99');
+ const containsCustomClass = classList.contains('extraclasses');
+ expect(containsSizeClass).toBe(true);
+ expect(containsCustomClass).toBe(true);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/markdown/field_spec.js b/spec/javascripts/vue_shared/components/markdown/field_spec.js
index 60a5c2ae74e..65c49b9f30b 100644
--- a/spec/javascripts/vue_shared/components/markdown/field_spec.js
+++ b/spec/javascripts/vue_shared/components/markdown/field_spec.js
@@ -42,12 +42,14 @@ describe('Markdown field component', () => {
beforeEach(() => {
spyOn(Vue.http, 'post').and.callFake(() => new Promise((resolve) => {
- resolve({
- json() {
- return {
- body: '<p>markdown preview</p>',
- };
- },
+ setTimeout(() => {
+ resolve({
+ json() {
+ return {
+ body: '<p>markdown preview</p>',
+ };
+ },
+ });
});
}));
diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js
index 52e450e9ba5..8450ad9dbcb 100644
--- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js
+++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js
@@ -11,6 +11,7 @@ describe('User Avatar Link Component', function () {
imgCssClasses: 'myextraavatarclass',
tooltipText: 'tooltip text',
tooltipPlacement: 'bottom',
+ username: 'username',
};
const UserAvatarLinkComponent = Vue.extend(UserAvatarLink);
@@ -47,4 +48,42 @@ describe('User Avatar Link Component', function () {
expect(this.userAvatarLink[key]).toBeDefined();
});
});
+
+ describe('no username', function () {
+ beforeEach(function (done) {
+ this.userAvatarLink.username = '';
+
+ Vue.nextTick(done);
+ });
+
+ it('should only render image tag in link', function () {
+ const childElements = this.userAvatarLink.$el.childNodes;
+ expect(childElements[0].tagName).toBe('IMG');
+
+ // Vue will render the hidden component as <!---->
+ expect(childElements[1].tagName).toBeUndefined();
+ });
+
+ it('should render avatar image tooltip', function () {
+ expect(this.userAvatarLink.$el.querySelector('img').dataset.originalTitle).toEqual(this.propsData.tooltipText);
+ });
+ });
+
+ describe('username', function () {
+ it('should not render avatar image tooltip', function () {
+ expect(this.userAvatarLink.$el.querySelector('img').dataset.originalTitle).toEqual('');
+ });
+
+ it('should render username prop in <span>', function () {
+ expect(this.userAvatarLink.$el.querySelector('span').innerText.trim()).toEqual(this.propsData.username);
+ });
+
+ it('should render text tooltip for <span>', function () {
+ expect(this.userAvatarLink.$el.querySelector('span').dataset.originalTitle).toEqual(this.propsData.tooltipText);
+ });
+
+ it('should render text tooltip placement for <span>', function () {
+ expect(this.userAvatarLink.$el.querySelector('span').getAttribute('tooltip-placement')).toEqual(this.propsData.tooltipPlacement);
+ });
+ });
});
diff --git a/spec/lib/additional_email_headers_interceptor_spec.rb b/spec/lib/additional_email_headers_interceptor_spec.rb
index 580450eef1e..b5c1a360ba9 100644
--- a/spec/lib/additional_email_headers_interceptor_spec.rb
+++ b/spec/lib/additional_email_headers_interceptor_spec.rb
@@ -1,12 +1,29 @@
require 'spec_helper'
describe AdditionalEmailHeadersInterceptor do
- it 'adds Auto-Submitted header' do
- mail = ActionMailer::Base.mail(to: 'test@mail.com', from: 'info@mail.com', body: 'hello').deliver
+ let(:mail) do
+ ActionMailer::Base.mail(to: 'test@mail.com', from: 'info@mail.com', body: 'hello')
+ end
+
+ before do
+ mail.deliver_now
+ end
+ it 'adds Auto-Submitted header' do
expect(mail.header['To'].value).to eq('test@mail.com')
expect(mail.header['From'].value).to eq('info@mail.com')
expect(mail.header['Auto-Submitted'].value).to eq('auto-generated')
expect(mail.header['X-Auto-Response-Suppress'].value).to eq('All')
end
+
+ context 'when the same mail object is sent twice' do
+ before do
+ mail.deliver_now
+ end
+
+ it 'does not add the Auto-Submitted header twice' do
+ expect(mail.header['Auto-Submitted'].value).to eq('auto-generated')
+ expect(mail.header['X-Auto-Response-Suppress'].value).to eq('All')
+ end
+ end
end
diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
index 9c74c9b8c99..3c98b18f99b 100644
--- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
@@ -317,6 +317,68 @@ describe Banzai::Filter::IssueReferenceFilter do
end
end
+ context 'group context' do
+ let(:group) { create(:group) }
+ let(:context) { { project: nil, group: group } }
+
+ it 'ignores shorthanded issue reference' do
+ reference = "##{issue.iid}"
+ text = "Fixed #{reference}"
+
+ expect(reference_filter(text, context).to_html).to eq(text)
+ end
+
+ it 'ignores valid references when cross-reference project uses external tracker' do
+ expect_any_instance_of(described_class).to receive(:find_object)
+ .with(project, issue.iid)
+ .and_return(nil)
+
+ reference = "#{project.full_path}##{issue.iid}"
+ text = "Issue #{reference}"
+
+ expect(reference_filter(text, context).to_html).to eq(text)
+ end
+
+ it 'links to a valid reference for complete cross-reference' do
+ reference = "#{project.full_path}##{issue.iid}"
+ doc = reference_filter("See #{reference}", context)
+
+ expect(doc.css('a').first.attr('href')).to eq helper.url_for_issue(issue.iid, project)
+ end
+
+ it 'ignores reference for shorthand cross-reference' do
+ reference = "#{project.path}##{issue.iid}"
+ text = "See #{reference}"
+
+ expect(reference_filter(text, context).to_html).to eq(text)
+ end
+
+ it 'links to a valid reference for url cross-reference' do
+ reference = helper.url_for_issue(issue.iid, project) + "#note_123"
+
+ doc = reference_filter("See #{reference}", context)
+
+ expect(doc.css('a').first.attr('href')).to eq(helper.url_for_issue(issue.iid, project) + "#note_123")
+ end
+
+ it 'links to a valid reference for cross-reference in link href' do
+ reference = "#{helper.url_for_issue(issue.iid, project) + "#note_123"}"
+ reference_link = %{<a href="#{reference}">Reference</a>}
+
+ doc = reference_filter("See #{reference_link}", context)
+
+ expect(doc.css('a').first.attr('href')).to eq helper.url_for_issue(issue.iid, project) + "#note_123"
+ end
+
+ it 'links to a valid reference for issue reference in the link href' do
+ reference = issue.to_reference(group)
+ reference_link = %{<a href="#{reference}">Reference</a>}
+ doc = reference_filter("See #{reference_link}", context)
+
+ expect(doc.css('a').first.attr('href')).to eq helper.url_for_issue(issue.iid, project)
+ end
+ end
+
describe '#issues_per_project' do
context 'using an internal issue tracker' do
it 'returns a Hash containing the issues per project' do
diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb
index 2cd30a5e302..862b1fe3fd3 100644
--- a/spec/lib/banzai/filter/label_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb
@@ -594,4 +594,16 @@ describe Banzai::Filter::LabelReferenceFilter do
expect(reference_filter(act).to_html).to eq exp
end
end
+
+ describe 'group context' do
+ it 'points to referenced project issues page' do
+ project = create(:project)
+ label = create(:label, project: project)
+ reference = "#{project.full_path}~#{label.name}"
+
+ result = reference_filter("See #{reference}", { project: nil, group: create(:group) } )
+
+ expect(result.css('a').first.attr('href')).to eq(urls.project_issues_url(project, label_name: label.name))
+ end
+ end
end
diff --git a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
index ed2788f8a33..158844e25ae 100644
--- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
@@ -214,4 +214,14 @@ describe Banzai::Filter::MergeRequestReferenceFilter do
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(merge.to_reference(project))} \(diffs, comment 123\)<\/a>\.\)/)
end
end
+
+ context 'group context' do
+ it 'links to a valid reference' do
+ reference = "#{project.full_path}!#{merge.iid}"
+
+ result = reference_filter("See #{reference}", { project: nil, group: create(:group) } )
+
+ expect(result.css('a').first.attr('href')).to eq(urls.project_merge_request_url(project, merge))
+ end
+ end
end
diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
index fe7a8c84c9e..84578668133 100644
--- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
@@ -343,4 +343,15 @@ describe Banzai::Filter::MilestoneReferenceFilter do
expect(doc.css('a')).to be_empty
end
end
+
+ context 'group context' do
+ it 'links to a valid reference' do
+ milestone = create(:milestone, project: project)
+ reference = "#{project.full_path}%#{milestone.iid}"
+
+ result = reference_filter("See #{reference}", { project: nil, group: create(:group) } )
+
+ expect(result.css('a').first.attr('href')).to eq(urls.milestone_url(milestone))
+ end
+ end
end
diff --git a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
index 90ac4c7b238..3a07a6dc179 100644
--- a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
@@ -201,4 +201,14 @@ describe Banzai::Filter::SnippetReferenceFilter do
expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/)
end
end
+
+ context 'group context' do
+ it 'links to a valid reference' do
+ reference = "#{project.full_path}$#{snippet.id}"
+
+ result = reference_filter("See #{reference}", { project: nil, group: create(:group) } )
+
+ expect(result.css('a').first.attr('href')).to eq(urls.project_snippet_url(project, snippet))
+ end
+ end
end
diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb
index 34dac1db69a..fc03741976e 100644
--- a/spec/lib/banzai/filter/user_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb
@@ -208,6 +208,39 @@ describe Banzai::Filter::UserReferenceFilter do
end
end
+ context 'in group context' do
+ let(:group) { create(:group) }
+ let(:group_member) { create(:user) }
+
+ before do
+ group.add_developer(group_member)
+ end
+
+ let(:context) { { author: group_member, project: nil, group: group } }
+
+ it 'supports a special @all mention' do
+ reference = User.reference_prefix + 'all'
+ doc = reference_filter("Hey #{reference}", context)
+
+ expect(doc.css('a').length).to eq(1)
+ expect(doc.css('a').first.attr('href')).to eq urls.group_url(group)
+ end
+
+ it 'supports mentioning a single user' do
+ reference = group_member.to_reference
+ doc = reference_filter("Hey #{reference}", context)
+
+ expect(doc.css('a').first.attr('href')).to eq urls.user_url(group_member)
+ end
+
+ it 'supports mentioning a group' do
+ reference = group.to_reference
+ doc = reference_filter("Hey #{reference}", context)
+
+ expect(doc.css('a').first.attr('href')).to eq urls.user_url(group)
+ end
+ end
+
describe '#namespaces' do
it 'returns a Hash containing all Namespaces' do
document = Nokogiri::HTML.fragment("<p>#{user.to_reference}</p>")
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index af1db2c3455..54a853c9ce3 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::Auth do
describe 'constants' do
it 'API_SCOPES contains all scopes for API access' do
- expect(subject::API_SCOPES).to eq [:api, :read_user]
+ expect(subject::API_SCOPES).to eq %i[api read_user sudo]
end
it 'OPENID_SCOPES contains all scopes for OpenID Connect' do
@@ -19,7 +19,7 @@ describe Gitlab::Auth do
it 'optional_scopes contains all non-default scopes' do
stub_container_registry_config(enabled: true)
- expect(subject.optional_scopes).to eq %i[read_user read_registry openid]
+ expect(subject.optional_scopes).to eq %i[read_user sudo read_registry openid]
end
context 'registry_scopes' do
@@ -164,7 +164,7 @@ describe Gitlab::Auth do
personal_access_token = create(:personal_access_token, scopes: ['api'])
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '')
- expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, full_authentication_abilities))
+ expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_access_token, full_authentication_abilities))
end
context 'when registry is enabled' do
@@ -176,7 +176,7 @@ describe Gitlab::Auth do
personal_access_token = create(:personal_access_token, scopes: ['read_registry'])
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '')
- expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, [:read_container_image]))
+ expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_access_token, [:read_container_image]))
end
end
@@ -184,14 +184,14 @@ describe Gitlab::Auth do
impersonation_token = create(:personal_access_token, :impersonation, scopes: ['api'])
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '')
- expect(gl_auth.find_for_git_client('', impersonation_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(impersonation_token.user, nil, :personal_token, full_authentication_abilities))
+ expect(gl_auth.find_for_git_client('', impersonation_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(impersonation_token.user, nil, :personal_access_token, full_authentication_abilities))
end
it 'limits abilities based on scope' do
personal_access_token = create(:personal_access_token, scopes: ['read_user'])
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '')
- expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, []))
+ expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_access_token, []))
end
it 'fails if password is nil' do
@@ -234,7 +234,7 @@ describe Gitlab::Auth do
it 'throws an error suggesting user create a PAT when internal auth is disabled' do
allow_any_instance_of(ApplicationSetting).to receive(:password_authentication_enabled?) { false }
- expect { gl_auth.find_for_git_client('foo', 'bar', project: nil, ip: 'ip') }.to raise_error(Gitlab::Auth::MissingPersonalTokenError)
+ expect { gl_auth.find_for_git_client('foo', 'bar', project: nil, ip: 'ip') }.to raise_error(Gitlab::Auth::MissingPersonalAccessTokenError)
end
end
diff --git a/spec/lib/gitlab/ci/cron_parser_spec.rb b/spec/lib/gitlab/ci/cron_parser_spec.rb
index 809fda11879..2a3f7807fdb 100644
--- a/spec/lib/gitlab/ci/cron_parser_spec.rb
+++ b/spec/lib/gitlab/ci/cron_parser_spec.rb
@@ -77,8 +77,20 @@ describe Gitlab::Ci::CronParser do
it_behaves_like "returns time in the future"
- it 'converts time in server time zone' do
- expect(subject.hour).to eq(hour_in_utc)
+ context 'when PST (Pacific Standard Time)' do
+ it 'converts time in server time zone' do
+ Timecop.freeze(Time.utc(2017, 1, 1)) do
+ expect(subject.hour).to eq(hour_in_utc)
+ end
+ end
+ end
+
+ context 'when PDT (Pacific Daylight Time)' do
+ it 'converts time in server time zone' do
+ Timecop.freeze(Time.utc(2017, 6, 1)) do
+ expect(subject.hour).to eq(hour_in_utc)
+ end
+ end
end
end
end
@@ -100,8 +112,20 @@ describe Gitlab::Ci::CronParser do
it_behaves_like "returns time in the future"
- it 'converts time in server time zone' do
- expect(subject.hour).to eq(hour_in_utc)
+ context 'when CET (Central European Time)' do
+ it 'converts time in server time zone' do
+ Timecop.freeze(Time.utc(2017, 1, 1)) do
+ expect(subject.hour).to eq(hour_in_utc)
+ end
+ end
+ end
+
+ context 'when CEST (Central European Summer Time)' do
+ it 'converts time in server time zone' do
+ Timecop.freeze(Time.utc(2017, 6, 1)) do
+ expect(subject.hour).to eq(hour_in_utc)
+ end
+ end
end
end
@@ -111,8 +135,20 @@ describe Gitlab::Ci::CronParser do
it_behaves_like "returns time in the future"
- it 'converts time in server time zone' do
- expect(subject.hour).to eq(hour_in_utc)
+ context 'when EST (Eastern Standard Time)' do
+ it 'converts time in server time zone' do
+ Timecop.freeze(Time.utc(2017, 1, 1)) do
+ expect(subject.hour).to eq(hour_in_utc)
+ end
+ end
+ end
+
+ context 'when EDT (Eastern Daylight Time)' do
+ it 'converts time in server time zone' do
+ Timecop.freeze(Time.utc(2017, 6, 1)) do
+ expect(subject.hour).to eq(hour_in_utc)
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb
index 5a7a42d84c0..9cdebaa5cf2 100644
--- a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb
@@ -66,7 +66,7 @@ describe Gitlab::Ci::Status::Build::Cancelable do
end
describe '#action_icon' do
- it { expect(subject.action_icon).to eq 'icon_action_cancel' }
+ it { expect(subject.action_icon).to eq 'cancel' }
end
describe '#action_title' do
diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb
index 8768302eda1..2b32e47e9ba 100644
--- a/spec/lib/gitlab/ci/status/build/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb
@@ -30,7 +30,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'passed'
- expect(status.icon).to eq 'icon_status_success'
+ expect(status.icon).to eq 'status_success'
expect(status.favicon).to eq 'favicon_status_success'
expect(status.label).to eq 'passed'
expect(status).to have_details
@@ -57,7 +57,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'failed'
- expect(status.icon).to eq 'icon_status_failed'
+ expect(status.icon).to eq 'status_failed'
expect(status.favicon).to eq 'favicon_status_failed'
expect(status.label).to eq 'failed'
expect(status).to have_details
@@ -84,7 +84,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'failed'
- expect(status.icon).to eq 'icon_status_warning'
+ expect(status.icon).to eq 'warning'
expect(status.favicon).to eq 'favicon_status_failed'
expect(status.label).to eq 'failed (allowed to fail)'
expect(status).to have_details
@@ -113,7 +113,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'canceled'
- expect(status.icon).to eq 'icon_status_canceled'
+ expect(status.icon).to eq 'status_canceled'
expect(status.favicon).to eq 'favicon_status_canceled'
expect(status.label).to eq 'canceled'
expect(status).to have_details
@@ -139,7 +139,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'running'
- expect(status.icon).to eq 'icon_status_running'
+ expect(status.icon).to eq 'status_running'
expect(status.favicon).to eq 'favicon_status_running'
expect(status.label).to eq 'running'
expect(status).to have_details
@@ -165,7 +165,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'pending'
- expect(status.icon).to eq 'icon_status_pending'
+ expect(status.icon).to eq 'status_pending'
expect(status.favicon).to eq 'favicon_status_pending'
expect(status.label).to eq 'pending'
expect(status).to have_details
@@ -190,7 +190,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'skipped'
- expect(status.icon).to eq 'icon_status_skipped'
+ expect(status.icon).to eq 'status_skipped'
expect(status.favicon).to eq 'favicon_status_skipped'
expect(status.label).to eq 'skipped'
expect(status).to have_details
@@ -219,7 +219,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'manual'
expect(status.group).to eq 'manual'
- expect(status.icon).to eq 'icon_status_manual'
+ expect(status.icon).to eq 'status_manual'
expect(status.favicon).to eq 'favicon_status_manual'
expect(status.label).to include 'manual play action'
expect(status).to have_details
@@ -274,7 +274,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'manual'
expect(status.group).to eq 'manual'
- expect(status.icon).to eq 'icon_status_manual'
+ expect(status.icon).to eq 'status_manual'
expect(status.favicon).to eq 'favicon_status_manual'
expect(status.label).to eq 'manual stop action (not allowed)'
expect(status).to have_details
diff --git a/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb b/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb
index 20f71459738..79a65fc67e8 100644
--- a/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb
@@ -18,7 +18,7 @@ describe Gitlab::Ci::Status::Build::FailedAllowed do
describe '#icon' do
it 'returns a warning icon' do
- expect(subject.icon).to eq 'icon_status_warning'
+ expect(subject.icon).to eq 'warning'
end
end
diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb
index 32b2e62e4e0..81d5f553fd1 100644
--- a/spec/lib/gitlab/ci/status/build/play_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/play_spec.rb
@@ -46,7 +46,7 @@ describe Gitlab::Ci::Status::Build::Play do
end
describe '#action_icon' do
- it { expect(subject.action_icon).to eq 'icon_action_play' }
+ it { expect(subject.action_icon).to eq 'play' }
end
describe '#action_title' do
diff --git a/spec/lib/gitlab/ci/status/build/retryable_spec.rb b/spec/lib/gitlab/ci/status/build/retryable_spec.rb
index 21026f2c968..14d42e0d70f 100644
--- a/spec/lib/gitlab/ci/status/build/retryable_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/retryable_spec.rb
@@ -66,7 +66,7 @@ describe Gitlab::Ci::Status::Build::Retryable do
end
describe '#action_icon' do
- it { expect(subject.action_icon).to eq 'icon_action_retry' }
+ it { expect(subject.action_icon).to eq 'retry' }
end
describe '#action_title' do
diff --git a/spec/lib/gitlab/ci/status/build/stop_spec.rb b/spec/lib/gitlab/ci/status/build/stop_spec.rb
index e0425103f41..18e250772f0 100644
--- a/spec/lib/gitlab/ci/status/build/stop_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/stop_spec.rb
@@ -38,7 +38,7 @@ describe Gitlab::Ci::Status::Build::Stop do
end
describe '#action_icon' do
- it { expect(subject.action_icon).to eq 'icon_action_stop' }
+ it { expect(subject.action_icon).to eq 'stop' }
end
describe '#action_title' do
diff --git a/spec/lib/gitlab/ci/status/canceled_spec.rb b/spec/lib/gitlab/ci/status/canceled_spec.rb
index 530639a5897..dc74d7e28c5 100644
--- a/spec/lib/gitlab/ci/status/canceled_spec.rb
+++ b/spec/lib/gitlab/ci/status/canceled_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Canceled do
end
describe '#icon' do
- it { expect(subject.icon).to eq 'icon_status_canceled' }
+ it { expect(subject.icon).to eq 'status_canceled' }
end
describe '#favicon' do
diff --git a/spec/lib/gitlab/ci/status/created_spec.rb b/spec/lib/gitlab/ci/status/created_spec.rb
index aef982e17f1..ce4333f2aca 100644
--- a/spec/lib/gitlab/ci/status/created_spec.rb
+++ b/spec/lib/gitlab/ci/status/created_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Created do
end
describe '#icon' do
- it { expect(subject.icon).to eq 'icon_status_created' }
+ it { expect(subject.icon).to eq 'status_created' }
end
describe '#favicon' do
diff --git a/spec/lib/gitlab/ci/status/failed_spec.rb b/spec/lib/gitlab/ci/status/failed_spec.rb
index 9a25743885c..a4a92117c7f 100644
--- a/spec/lib/gitlab/ci/status/failed_spec.rb
+++ b/spec/lib/gitlab/ci/status/failed_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Failed do
end
describe '#icon' do
- it { expect(subject.icon).to eq 'icon_status_failed' }
+ it { expect(subject.icon).to eq 'status_failed' }
end
describe '#favicon' do
diff --git a/spec/lib/gitlab/ci/status/manual_spec.rb b/spec/lib/gitlab/ci/status/manual_spec.rb
index 6fdc3801d71..0463f2e1aff 100644
--- a/spec/lib/gitlab/ci/status/manual_spec.rb
+++ b/spec/lib/gitlab/ci/status/manual_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Manual do
end
describe '#icon' do
- it { expect(subject.icon).to eq 'icon_status_manual' }
+ it { expect(subject.icon).to eq 'status_manual' }
end
describe '#favicon' do
diff --git a/spec/lib/gitlab/ci/status/pending_spec.rb b/spec/lib/gitlab/ci/status/pending_spec.rb
index ffc53f0506b..0e25358dd8a 100644
--- a/spec/lib/gitlab/ci/status/pending_spec.rb
+++ b/spec/lib/gitlab/ci/status/pending_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Pending do
end
describe '#icon' do
- it { expect(subject.icon).to eq 'icon_status_pending' }
+ it { expect(subject.icon).to eq 'status_pending' }
end
describe '#favicon' do
diff --git a/spec/lib/gitlab/ci/status/running_spec.rb b/spec/lib/gitlab/ci/status/running_spec.rb
index 0babf1fb54e..9c9d431bb5d 100644
--- a/spec/lib/gitlab/ci/status/running_spec.rb
+++ b/spec/lib/gitlab/ci/status/running_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Running do
end
describe '#icon' do
- it { expect(subject.icon).to eq 'icon_status_running' }
+ it { expect(subject.icon).to eq 'status_running' }
end
describe '#favicon' do
diff --git a/spec/lib/gitlab/ci/status/skipped_spec.rb b/spec/lib/gitlab/ci/status/skipped_spec.rb
index 670747c9f0b..63694ca0ea6 100644
--- a/spec/lib/gitlab/ci/status/skipped_spec.rb
+++ b/spec/lib/gitlab/ci/status/skipped_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Skipped do
end
describe '#icon' do
- it { expect(subject.icon).to eq 'icon_status_skipped' }
+ it { expect(subject.icon).to eq 'status_skipped' }
end
describe '#favicon' do
diff --git a/spec/lib/gitlab/ci/status/success_spec.rb b/spec/lib/gitlab/ci/status/success_spec.rb
index ff65b074808..2f67df71c4f 100644
--- a/spec/lib/gitlab/ci/status/success_spec.rb
+++ b/spec/lib/gitlab/ci/status/success_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Success do
end
describe '#icon' do
- it { expect(subject.icon).to eq 'icon_status_success' }
+ it { expect(subject.icon).to eq 'status_success' }
end
describe '#favicon' do
diff --git a/spec/lib/gitlab/ci/status/success_warning_spec.rb b/spec/lib/gitlab/ci/status/success_warning_spec.rb
index 7e2269397c6..4582354e739 100644
--- a/spec/lib/gitlab/ci/status/success_warning_spec.rb
+++ b/spec/lib/gitlab/ci/status/success_warning_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::SuccessWarning do
end
describe '#icon' do
- it { expect(subject.icon).to eq 'icon_status_warning' }
+ it { expect(subject.icon).to eq 'status_warning' }
end
describe '#group' do
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index 5fa94999d25..7aeb85b8f5a 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -256,4 +256,26 @@ describe Gitlab::Database do
expect(described_class.false_value).to eq 0
end
end
+
+ describe '#sanitize_timestamp' do
+ let(:max_timestamp) { Time.at((1 << 31) - 1) }
+
+ subject { described_class.sanitize_timestamp(timestamp) }
+
+ context 'with a timestamp smaller than MAX_TIMESTAMP_VALUE' do
+ let(:timestamp) { max_timestamp - 10.years }
+
+ it 'returns the given timestamp' do
+ expect(subject).to eq(timestamp)
+ end
+ end
+
+ context 'with a timestamp larger than MAX_TIMESTAMP_VALUE' do
+ let(:timestamp) { max_timestamp + 1.second }
+
+ it 'returns MAX_TIMESTAMP_VALUE' do
+ expect(subject).to eq(max_timestamp)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb
index 245f24e96d4..677eb373d22 100644
--- a/spec/lib/gitlab/diff/position_spec.rb
+++ b/spec/lib/gitlab/diff/position_spec.rb
@@ -364,6 +364,43 @@ describe Gitlab::Diff::Position do
end
end
+ describe "position for a missing ref" do
+ let(:diff_refs) do
+ Gitlab::Diff::DiffRefs.new(
+ base_sha: "not_existing_sha",
+ head_sha: "existing_sha"
+ )
+ end
+
+ subject do
+ described_class.new(
+ old_path: "files/ruby/feature.rb",
+ new_path: "files/ruby/feature.rb",
+ old_line: 3,
+ new_line: nil,
+ diff_refs: diff_refs
+ )
+ end
+
+ describe "#diff_file" do
+ it "does not raise exception" do
+ expect { subject.diff_file(project.repository) }.not_to raise_error
+ end
+ end
+
+ describe "#diff_line" do
+ it "does not raise exception" do
+ expect { subject.diff_line(project.repository) }.not_to raise_error
+ end
+ end
+
+ describe "#line_code" do
+ it "does not raise exception" do
+ expect { subject.line_code(project.repository) }.not_to raise_error
+ end
+ end
+ end
+
describe "position for a file in the initial commit" do
let(:commit) { project.commit("1a0b36b3cdad1d2ee32457c102a8c0b7056fa863") }
diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb
index 412a0093d97..c04a9688503 100644
--- a/spec/lib/gitlab/git/blob_spec.rb
+++ b/spec/lib/gitlab/git/blob_spec.rb
@@ -143,6 +143,16 @@ describe Gitlab::Git::Blob, seed_helper: true do
expect(blob.loaded_size).to eq(blob_size)
end
end
+
+ context 'when sha references a tree' do
+ it 'returns nil' do
+ tree = Gitlab::Git::Commit.find(repository, 'master').tree
+
+ blob = Gitlab::Git::Blob.raw(repository, tree.oid)
+
+ expect(blob).to be_nil
+ end
+ end
end
describe '.raw' do
@@ -226,6 +236,51 @@ describe Gitlab::Git::Blob, seed_helper: true do
end
end
+ describe '.batch_lfs_pointers' do
+ let(:tree_object) { Gitlab::Git::Commit.find(repository, 'master').tree }
+
+ let(:non_lfs_blob) do
+ Gitlab::Git::Blob.find(
+ repository,
+ 'master',
+ 'README.md'
+ )
+ end
+
+ let(:lfs_blob) do
+ Gitlab::Git::Blob.find(
+ repository,
+ '33bcff41c232a11727ac6d660bd4b0c2ba86d63d',
+ 'files/lfs/image.jpg'
+ )
+ end
+
+ it 'returns a list of Gitlab::Git::Blob' do
+ blobs = described_class.batch_lfs_pointers(repository, [lfs_blob.id])
+
+ expect(blobs.count).to eq(1)
+ expect(blobs).to all( be_a(Gitlab::Git::Blob) )
+ end
+
+ it 'silently ignores tree objects' do
+ blobs = described_class.batch_lfs_pointers(repository, [tree_object.oid])
+
+ expect(blobs).to eq([])
+ end
+
+ it 'silently ignores non lfs objects' do
+ blobs = described_class.batch_lfs_pointers(repository, [non_lfs_blob.id])
+
+ expect(blobs).to eq([])
+ end
+
+ it 'avoids loading large blobs into memory' do
+ expect(repository).not_to receive(:lookup)
+
+ described_class.batch_lfs_pointers(repository, [non_lfs_blob.id])
+ end
+ end
+
describe 'encoding' do
context 'file with russian text' do
let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "encoding/russian.rb") }
diff --git a/spec/lib/gitlab/git/branch_spec.rb b/spec/lib/gitlab/git/branch_spec.rb
index 318a7b7a332..708870060e7 100644
--- a/spec/lib/gitlab/git/branch_spec.rb
+++ b/spec/lib/gitlab/git/branch_spec.rb
@@ -7,6 +7,38 @@ describe Gitlab::Git::Branch, seed_helper: true do
it { is_expected.to be_kind_of Array }
+ describe '.find' do
+ subject { described_class.find(repository, branch) }
+
+ before do
+ allow(repository).to receive(:find_branch).with(branch)
+ .and_call_original
+ end
+
+ context 'when finding branch via branch name' do
+ let(:branch) { 'master' }
+
+ it 'returns a branch object' do
+ expect(subject).to be_a(described_class)
+ expect(subject.name).to eq(branch)
+
+ expect(repository).to have_received(:find_branch).with(branch)
+ end
+ end
+
+ context 'when the branch is already a branch' do
+ let(:commit) { repository.commit('master') }
+ let(:branch) { described_class.new(repository, 'master', commit.sha, commit) }
+
+ it 'returns a branch object' do
+ expect(subject).to be_a(described_class)
+ expect(subject).to eq(branch)
+
+ expect(repository).not_to have_received(:find_branch).with(branch)
+ end
+ end
+ end
+
describe '#size' do
subject { super().size }
it { is_expected.to eq(SeedRepo::Repo::BRANCHES.size) }
diff --git a/spec/lib/gitlab/git/lfs_changes_spec.rb b/spec/lib/gitlab/git/lfs_changes_spec.rb
new file mode 100644
index 00000000000..c2d2c6e1bc8
--- /dev/null
+++ b/spec/lib/gitlab/git/lfs_changes_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe Gitlab::Git::LfsChanges do
+ let(:project) { create(:project, :repository) }
+ let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' }
+ let(:blob_object_id) { '0c304a93cb8430108629bbbcaa27db3343299bc0' }
+
+ subject { described_class.new(project.repository, newrev) }
+
+ describe 'new_pointers' do
+ before do
+ allow_any_instance_of(Gitlab::Git::RevList).to receive(:new_objects).and_return([blob_object_id])
+ end
+
+ it 'uses rev-list to find new objects' do
+ rev_list = double
+ allow(Gitlab::Git::RevList).to receive(:new).and_return(rev_list)
+
+ expect(rev_list).to receive(:new_objects).and_return([])
+
+ subject.new_pointers
+ end
+
+ it 'filters new objects to find lfs pointers' do
+ expect(Gitlab::Git::Blob).to receive(:batch_lfs_pointers).with(project.repository, [blob_object_id])
+
+ subject.new_pointers(object_limit: 1)
+ end
+
+ it 'limits new_objects using object_limit' do
+ expect(Gitlab::Git::Blob).to receive(:batch_lfs_pointers).with(project.repository, [])
+
+ subject.new_pointers(object_limit: 0)
+ end
+ end
+
+ describe 'all_pointers' do
+ it 'uses rev-list to find all objects' do
+ rev_list = double
+ allow(Gitlab::Git::RevList).to receive(:new).and_return(rev_list)
+ allow(rev_list).to receive(:all_objects).and_return([blob_object_id])
+
+ expect(Gitlab::Git::Blob).to receive(:batch_lfs_pointers).with(project.repository, [blob_object_id])
+
+ subject.all_pointers
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index e3d320631bc..1d4d0c300eb 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -559,10 +559,10 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
- describe "#remote_delete" do
+ describe "#remove_remote" do
before(:all) do
@repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
- @repo.remote_delete("expendable")
+ @repo.remove_remote("expendable")
end
it "should remove the remote" do
@@ -575,14 +575,16 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
- describe "#remote_add" do
+ describe "#remote_update" do
before(:all) do
@repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
- @repo.remote_add("new_remote", SeedHelper::GITLAB_GIT_TEST_REPO_URL)
+ @repo.remote_update("expendable", url: TEST_NORMAL_REPO_PATH)
end
it "should add the remote" do
- expect(@repo.rugged.remotes.each_name.to_a).to include("new_remote")
+ expect(@repo.rugged.remotes["expendable"].url).to(
+ eq(TEST_NORMAL_REPO_PATH)
+ )
end
after(:all) do
@@ -591,21 +593,58 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
- describe "#remote_update" do
- before(:all) do
- @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
- @repo.remote_update("expendable", url: TEST_NORMAL_REPO_PATH)
+ describe '#fetch_mirror' do
+ let(:new_repository) do
+ Gitlab::Git::Repository.new('default', 'my_project.git', '')
end
- it "should add the remote" do
- expect(@repo.rugged.remotes["expendable"].url).to(
- eq(TEST_NORMAL_REPO_PATH)
- )
+ subject { new_repository.fetch_mirror(repository.path) }
+
+ before do
+ Gitlab::Shell.new.add_repository('default', 'my_project')
end
- after(:all) do
- FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
- ensure_seeds
+ after do
+ Gitlab::Shell.new.remove_repository(TestEnv.repos_path, 'my_project')
+ end
+
+ it 'fetches a url as a mirror remote' do
+ subject
+
+ expect(refs(new_repository.path)).to eq(refs(repository.path))
+ end
+
+ context 'with keep-around refs' do
+ let(:sha) { SeedRepo::Commit::ID }
+ let(:keep_around_ref) { "refs/keep-around/#{sha}" }
+ let(:tmp_ref) { "refs/tmp/#{SecureRandom.hex}" }
+
+ before do
+ repository.rugged.references.create(keep_around_ref, sha, force: true)
+ repository.rugged.references.create(tmp_ref, sha, force: true)
+ end
+
+ it 'includes the temporary and keep-around refs' do
+ subject
+
+ expect(refs(new_repository.path)).to include(keep_around_ref)
+ expect(refs(new_repository.path)).to include(tmp_ref)
+ end
+ end
+ end
+
+ describe '#remote_tags' do
+ let(:target_commit_id) { SeedRepo::Commit::ID }
+
+ subject { repository.remote_tags('upstream') }
+
+ it 'gets the remote tags' do
+ expect(repository).to receive(:list_remote_tags).with('upstream')
+ .and_return(["#{target_commit_id}\trefs/tags/v0.0.1\n"])
+
+ expect(subject.first).to be_an_instance_of(Gitlab::Git::Tag)
+ expect(subject.first.name).to eq('v0.0.1')
+ expect(subject.first.dereferenced_target.id).to eq(target_commit_id)
end
end
@@ -1135,8 +1174,35 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
+ describe '#merged_branch_names' do
+ context 'when branch names are passed' do
+ it 'only returns the names we are asking' do
+ names = repository.merged_branch_names(%w[merge-test])
+
+ expect(names).to contain_exactly('merge-test')
+ end
+
+ it 'does not return unmerged branch names' do
+ names = repository.merged_branch_names(%w[feature])
+
+ expect(names).to be_empty
+ end
+ end
+
+ context 'when no branch names are specified' do
+ it 'returns all merged branch names' do
+ names = repository.merged_branch_names
+
+ expect(names).to include('merge-test')
+ expect(names).to include('fix-mode')
+ expect(names).not_to include('feature')
+ end
+ end
+ end
+
describe "#ls_files" do
let(:master_file_paths) { repository.ls_files("master") }
+ let(:utf8_file_paths) { repository.ls_files("ls-files-utf8") }
let(:not_existed_branch) { repository.ls_files("not_existed_branch") }
it "read every file paths of master branch" do
@@ -1158,6 +1224,10 @@ describe Gitlab::Git::Repository, seed_helper: true do
it "returns empty array when not existed branch" do
expect(not_existed_branch.length).to equal(0)
end
+
+ it "returns valid utf-8 data" do
+ expect(utf8_file_paths.map { |file| file.force_encoding('utf-8') }).to all(be_valid_encoding)
+ end
end
describe "#copy_gitattributes" do
@@ -1310,6 +1380,24 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
+ describe '#batch_existence' do
+ let(:refs) { ['deadbeef', SeedRepo::RubyBlob::ID, '909e6157199'] }
+
+ it 'returns existing refs back' do
+ result = repository.batch_existence(refs)
+
+ expect(result).to eq([SeedRepo::RubyBlob::ID])
+ end
+
+ context 'existing: true' do
+ it 'inverts meaning and returns non-existing refs' do
+ result = repository.batch_existence(refs, existing: false)
+
+ expect(result).to eq(%w(deadbeef 909e6157199))
+ end
+ end
+ end
+
describe '#local_branches' do
before(:all) do
@repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
@@ -1583,38 +1671,71 @@ describe Gitlab::Git::Repository, seed_helper: true do
subject { repository.ff_merge(user, source_sha, target_branch) }
- it 'performs a ff_merge' do
- expect(subject.newrev).to eq(source_sha)
- expect(subject.repo_created).to be(false)
- expect(subject.branch_created).to be(false)
+ shared_examples '#ff_merge' do
+ it 'performs a ff_merge' do
+ expect(subject.newrev).to eq(source_sha)
+ expect(subject.repo_created).to be(false)
+ expect(subject.branch_created).to be(false)
- expect(repository.commit(target_branch).id).to eq(source_sha)
- end
+ expect(repository.commit(target_branch).id).to eq(source_sha)
+ end
+
+ context 'with a non-existing target branch' do
+ subject { repository.ff_merge(user, source_sha, 'this-isnt-real') }
+
+ it 'throws an ArgumentError' do
+ expect { subject }.to raise_error(ArgumentError)
+ end
+ end
+
+ context 'with a non-existing source commit' do
+ let(:source_sha) { 'f001' }
+
+ it 'throws an ArgumentError' do
+ expect { subject }.to raise_error(ArgumentError)
+ end
+ end
+
+ context 'when the source sha is not a descendant of the branch head' do
+ let(:source_sha) { '1a0b36b3cdad1d2ee32457c102a8c0b7056fa863' }
- context 'with a non-existing target branch' do
- subject { repository.ff_merge(user, source_sha, 'this-isnt-real') }
+ it "doesn't perform the ff_merge" do
+ expect { subject }.to raise_error(Gitlab::Git::CommitError)
- it 'throws an ArgumentError' do
- expect { subject }.to raise_error(ArgumentError)
+ expect(repository.commit(target_branch).id).to eq(branch_head)
+ end
end
end
- context 'with a non-existing source commit' do
- let(:source_sha) { 'f001' }
+ context 'with gitaly' do
+ it "calls Gitaly's OperationService" do
+ expect_any_instance_of(Gitlab::GitalyClient::OperationService)
+ .to receive(:user_ff_branch).with(user, source_sha, target_branch)
+ .and_return(nil)
- it 'throws an ArgumentError' do
- expect { subject }.to raise_error(ArgumentError)
+ subject
end
+
+ it_behaves_like '#ff_merge'
end
- context 'when the source sha is not a descendant of the branch head' do
- let(:source_sha) { '1a0b36b3cdad1d2ee32457c102a8c0b7056fa863' }
+ context 'without gitaly', :skip_gitaly_mock do
+ it_behaves_like '#ff_merge'
+ end
+ end
- it "doesn't perform the ff_merge" do
- expect { subject }.to raise_error(Gitlab::Git::CommitError)
+ describe '#fetch' do
+ let(:git_path) { Gitlab.config.git.bin_path }
+ let(:remote_name) { 'my_remote' }
- expect(repository.commit(target_branch).id).to eq(branch_head)
- end
+ subject { repository.fetch(remote_name) }
+
+ it 'fetches the remote and returns true if the command was successful' do
+ expect(repository).to receive(:popen)
+ .with(%W(#{git_path} fetch #{remote_name}), repository.path)
+ .and_return(['', 0])
+
+ expect(subject).to be(true)
end
end
@@ -1693,4 +1814,10 @@ describe Gitlab::Git::Repository, seed_helper: true do
sha = Rugged::Commit.create(repo, options)
repo.lookup(sha)
end
+
+ def refs(dir)
+ IO.popen(%W[git -C #{dir} for-each-ref], &:read).split("\n").map do |line|
+ line.split("\t").last
+ end
+ end
end
diff --git a/spec/lib/gitlab/git/rev_list_spec.rb b/spec/lib/gitlab/git/rev_list_spec.rb
index c0eac98d718..643a4b2d03e 100644
--- a/spec/lib/gitlab/git/rev_list_spec.rb
+++ b/spec/lib/gitlab/git/rev_list_spec.rb
@@ -2,53 +2,82 @@ require 'spec_helper'
describe Gitlab::Git::RevList do
let(:project) { create(:project, :repository) }
+ let(:rev_list) { described_class.new(newrev: 'newrev', path_to_repo: project.repository.path_to_repo) }
before do
- expect(Gitlab::Git::Env).to receive(:all).and_return({
+ allow(Gitlab::Git::Env).to receive(:all).and_return({
GIT_OBJECT_DIRECTORY: 'foo',
GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar'
})
end
- context "#new_refs" do
- let(:rev_list) { described_class.new(newrev: 'newrev', path_to_repo: project.repository.path_to_repo) }
+ def stub_popen_rev_list(*additional_args, output:)
+ expect(rev_list).to receive(:popen).with([
+ Gitlab.config.git.bin_path,
+ "--git-dir=#{project.repository.path_to_repo}",
+ 'rev-list',
+ *additional_args
+ ],
+ nil,
+ {
+ 'GIT_OBJECT_DIRECTORY' => 'foo',
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar'
+ }).and_return([output, 0])
+ end
+ context "#new_refs" do
it 'calls out to `popen`' do
- expect(rev_list).to receive(:popen).with([
- Gitlab.config.git.bin_path,
- "--git-dir=#{project.repository.path_to_repo}",
- 'rev-list',
- 'newrev',
- '--not',
- '--all'
- ],
- nil,
- {
- 'GIT_OBJECT_DIRECTORY' => 'foo',
- 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar'
- }).and_return(["sha1\nsha2", 0])
+ stub_popen_rev_list('newrev', '--not', '--all', output: "sha1\nsha2")
expect(rev_list.new_refs).to eq(%w[sha1 sha2])
end
end
+ context '#new_objects' do
+ it 'fetches list of newly pushed objects using rev-list' do
+ stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2")
+
+ expect(rev_list.new_objects).to eq(%w[sha1 sha2])
+ end
+
+ it 'can skip pathless objects' do
+ stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2 path/to/file")
+
+ expect(rev_list.new_objects(require_path: true)).to eq(%w[sha2])
+ end
+
+ it 'can return a lazy enumerator' do
+ stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2")
+
+ expect(rev_list.new_objects(lazy: true)).to be_a Enumerator::Lazy
+ end
+
+ it 'can accept list of references to exclude' do
+ stub_popen_rev_list('newrev', '--not', 'master', '--objects', output: "sha1\nsha2")
+
+ expect(rev_list.new_objects(not_in: ['master'])).to eq(%w[sha1 sha2])
+ end
+
+ it 'handles empty list of references to exclude as listing all known objects' do
+ stub_popen_rev_list('newrev', '--objects', output: "sha1\nsha2")
+
+ expect(rev_list.new_objects(not_in: [])).to eq(%w[sha1 sha2])
+ end
+ end
+
+ context '#all_objects' do
+ it 'fetches list of all pushed objects using rev-list' do
+ stub_popen_rev_list('--all', '--objects', output: "sha1\nsha2")
+
+ expect(rev_list.all_objects.force).to eq(%w[sha1 sha2])
+ end
+ end
+
context "#missed_ref" do
let(:rev_list) { described_class.new(oldrev: 'oldrev', newrev: 'newrev', path_to_repo: project.repository.path_to_repo) }
it 'calls out to `popen`' do
- expect(rev_list).to receive(:popen).with([
- Gitlab.config.git.bin_path,
- "--git-dir=#{project.repository.path_to_repo}",
- 'rev-list',
- '--max-count=1',
- 'oldrev',
- '^newrev'
- ],
- nil,
- {
- 'GIT_OBJECT_DIRECTORY' => 'foo',
- 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar'
- }).and_return(["sha1\nsha2", 0])
+ stub_popen_rev_list('--max-count=1', 'oldrev', '^newrev', output: "sha1\nsha2")
expect(rev_list.missed_ref).to eq(%w[sha1 sha2])
end
diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
index e144e28b5d8..d9ec28ab02e 100644
--- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
@@ -89,4 +89,38 @@ describe Gitlab::GitalyClient::OperationService do
end
end
end
+
+ describe '#user_ff_branch' do
+ let(:target_branch) { 'my-branch' }
+ let(:source_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' }
+ let(:request) do
+ Gitaly::UserFFBranchRequest.new(
+ repository: repository.gitaly_repository,
+ branch: target_branch,
+ commit_id: source_sha,
+ user: gitaly_user
+ )
+ end
+ let(:branch_update) do
+ Gitaly::OperationBranchUpdate.new(
+ commit_id: source_sha,
+ repo_created: false,
+ branch_created: false
+ )
+ end
+ let(:response) { Gitaly::UserFFBranchResponse.new(branch_update: branch_update) }
+
+ subject { client.user_ff_branch(user, source_sha, target_branch) }
+
+ it 'sends a user_ff_branch message and returns a BranchUpdate object' do
+ expect_any_instance_of(Gitaly::OperationService::Stub)
+ .to receive(:user_ff_branch).with(request, kind_of(Hash))
+ .and_return(response)
+
+ expect(subject).to be_a(Gitlab::Git::OperationService::BranchUpdate)
+ expect(subject.newrev).to eq(source_sha)
+ expect(subject.repo_created).to be(false)
+ expect(subject.branch_created).to be(false)
+ end
+ end
end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 29baa70d5ae..96efdd0949b 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -148,9 +148,18 @@ deploy_keys:
- deploy_keys_projects
- projects
cluster:
-- project
+- cluster_projects
+- projects
- user
-- service
+- provider_gcp
+- platform_kubernetes
+cluster_projects:
+- projects
+- clusters
+provider_gcp:
+- cluster
+platform_kubernetes:
+- cluster
services:
- project
- service_hook
@@ -182,6 +191,7 @@ project:
- tags
- chat_services
- cluster
+- cluster_project
- creator
- group
- namespace
@@ -195,6 +205,7 @@ project:
- mattermost_slash_commands_service
- slack_slash_commands_service
- irker_service
+- packagist_service
- pivotaltracker_service
- prometheus_service
- hipchat_service
@@ -286,3 +297,6 @@ timelogs:
- user
push_event_payload:
- event
+issue_assignees:
+- issue
+- assignee \ No newline at end of file
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index 1115fb218d6..9a68bbb379c 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -43,7 +43,7 @@
"issues": [
{
"id": 40,
- "title": "Voluptatem amet doloribus deleniti eos maxime repudiandae molestias.",
+ "title": "Voluptatem",
"assignee_id": 1,
"author_id": 22,
"project_id": 5,
@@ -60,6 +60,12 @@
"due_date": null,
"moved_to_id": null,
"test_ee_field": "test",
+ "issue_assignees": [
+ {
+ "user_id": 1,
+ "issue_id": 1
+ }
+ ],
"milestone": {
"id": 1,
"title": "test milestone",
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index 4301eee17dc..76b01b6a1ec 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -63,6 +63,10 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
expect(issue.reload.updated_at.to_s).to eq('2016-06-14 15:02:47 UTC')
end
+ it 'has issue assignees' do
+ expect(Issue.where(title: 'Voluptatem').first.issue_assignees).not_to be_empty
+ end
+
it 'contains the merge access levels on a protected branch' do
expect(ProtectedBranch.first.merge_access_levels).not_to be_empty
end
diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
index d9b86e1bf34..8da768ebd07 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -77,6 +77,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
expect(saved_project_json['issues'].first['notes']).not_to be_empty
end
+ it 'has issue assignees' do
+ expect(saved_project_json['issues'].first['issue_assignees']).not_to be_empty
+ end
+
it 'has author on issue comments' do
expect(saved_project_json['issues'].first['notes'].first['author']).not_to be_empty
end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 121c0ed04ed..4b79e9f18c6 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -313,30 +313,47 @@ Ci::PipelineSchedule:
- deleted_at
- created_at
- updated_at
-Gcp::Cluster:
+Clusters::Cluster:
- id
-- project_id
- user_id
-- service_id
- enabled
+- name
+- provider_type
+- platform_type
+- created_at
+- updated_at
+Clusters::Project:
+- id
+- project_id
+- cluster_id
+- created_at
+- updated_at
+Clusters::Providers::Gcp:
+- id
+- cluster_id
- status
- status_reason
-- project_namespace
+- gcp_project_id
+- zone
+- num_nodes
+- machine_type
+- operation_id
- endpoint
+- encrypted_access_token
+- encrypted_access_token_iv
+- created_at
+- updated_at
+Clusters::Platforms::Kubernetes:
+- id
+- cluster_id
+- api_url
- ca_cert
-- encrypted_kubernetes_token
-- encrypted_kubernetes_token_iv
+- namespace
- username
- encrypted_password
- encrypted_password_iv
-- gcp_project_id
-- gcp_cluster_zone
-- gcp_cluster_name
-- gcp_cluster_size
-- gcp_machine_type
-- gcp_operation_id
-- encrypted_gcp_token
-- encrypted_gcp_token_iv
+- encrypted_token
+- encrypted_token_iv
- created_at
- updated_at
DeployKey:
@@ -506,3 +523,6 @@ ProjectAutoDevops:
- project_id
- created_at
- updated_at
+IssueAssignee:
+- user_id
+- issue_id \ No newline at end of file
diff --git a/spec/lib/gitlab/ldap/authentication_spec.rb b/spec/lib/gitlab/ldap/authentication_spec.rb
index 01b6282af0c..9d57a46c12b 100644
--- a/spec/lib/gitlab/ldap/authentication_spec.rb
+++ b/spec/lib/gitlab/ldap/authentication_spec.rb
@@ -1,8 +1,8 @@
require 'spec_helper'
describe Gitlab::LDAP::Authentication do
- let(:user) { create(:omniauth_user, extern_uid: dn) }
- let(:dn) { 'uid=john,ou=people,dc=example,dc=com' }
+ let(:dn) { 'uid=John Smith, ou=People, dc=example, dc=com' }
+ let(:user) { create(:omniauth_user, extern_uid: Gitlab::LDAP::Person.normalize_dn(dn)) }
let(:login) { 'john' }
let(:password) { 'password' }
diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb
index 9a4705d1cee..260df6e4dae 100644
--- a/spec/lib/gitlab/ldap/user_spec.rb
+++ b/spec/lib/gitlab/ldap/user_spec.rb
@@ -44,23 +44,25 @@ describe Gitlab::LDAP::User do
end
describe '.find_by_uid_and_provider' do
+ let(:dn) { 'CN=John Åström, CN=Users, DC=Example, DC=com' }
+
it 'retrieves the correct user' do
special_info = {
name: 'John Åström',
email: 'john@example.com',
nickname: 'jastrom'
}
- special_hash = OmniAuth::AuthHash.new(uid: 'CN=John Åström,CN=Users,DC=Example,DC=com', provider: 'ldapmain', info: special_info)
+ special_hash = OmniAuth::AuthHash.new(uid: dn, provider: 'ldapmain', info: special_info)
special_chars_user = described_class.new(special_hash)
user = special_chars_user.save
- expect(described_class.find_by_uid_and_provider(special_hash.uid, special_hash.provider)).to eq user
+ expect(described_class.find_by_uid_and_provider(dn, 'ldapmain')).to eq user
end
end
describe 'find or create' do
it "finds the user if already existing" do
- create(:omniauth_user, extern_uid: 'uid=John Smith,ou=People,dc=example,dc=com', provider: 'ldapmain')
+ create(:omniauth_user, extern_uid: 'uid=john smith,ou=people,dc=example,dc=com', provider: 'ldapmain')
expect { ldap_user.save }.not_to change { User.count }
end
diff --git a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
index b576d7173f5..0803ce42fac 100644
--- a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
@@ -4,35 +4,30 @@ describe Gitlab::Metrics::SidekiqMiddleware do
let(:middleware) { described_class.new }
let(:message) { { 'args' => ['test'], 'enqueued_at' => Time.new(2016, 6, 23, 6, 59).to_f } }
- describe '#call' do
- it 'tracks the transaction' do
- worker = double(:worker, class: double(:class, name: 'TestWorker'))
+ def run(worker, message)
+ expect(Gitlab::Metrics::Transaction).to receive(:new)
+ .with('TestWorker#perform')
+ .and_call_original
+
+ expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set)
+ .with(:sidekiq_queue_duration, instance_of(Float))
- expect(Gitlab::Metrics::Transaction).to receive(:new)
- .with('TestWorker#perform')
- .and_call_original
+ expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:finish)
- expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set)
- .with(:sidekiq_queue_duration, instance_of(Float))
+ middleware.call(worker, message, :test) { nil }
+ end
- expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:finish)
+ describe '#call' do
+ it 'tracks the transaction' do
+ worker = double(:worker, class: double(:class, name: 'TestWorker'))
- middleware.call(worker, message, :test) { nil }
+ run(worker, message)
end
it 'tracks the transaction (for messages without `enqueued_at`)' do
worker = double(:worker, class: double(:class, name: 'TestWorker'))
- expect(Gitlab::Metrics::Transaction).to receive(:new)
- .with('TestWorker#perform')
- .and_call_original
-
- expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set)
- .with(:sidekiq_queue_duration, instance_of(Float))
-
- expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:finish)
-
- middleware.call(worker, {}, :test) { nil }
+ run(worker, {})
end
it 'tracks any raised exceptions' do
@@ -50,5 +45,18 @@ describe Gitlab::Metrics::SidekiqMiddleware do
expect { middleware.call(worker, message, :test) }
.to raise_error(RuntimeError)
end
+
+ it 'tags the metrics accordingly' do
+ tags = { one: 1, two: 2 }
+ worker = double(:worker, class: double(:class, name: 'TestWorker'))
+ allow(worker).to receive(:metrics_tags).and_return(tags)
+
+ tags.each do |tag, value|
+ expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:add_tag)
+ .with(tag, value)
+ end
+
+ run(worker, message)
+ end
end
end
diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb
index cab662819ac..67121937398 100644
--- a/spec/lib/gitlab/middleware/go_spec.rb
+++ b/spec/lib/gitlab/middleware/go_spec.rb
@@ -17,89 +17,115 @@ describe Gitlab::Middleware::Go do
describe 'when go-get=1' do
let(:current_user) { nil }
- context 'with simple 2-segment project path' do
- let!(:project) { create(:project, :private) }
+ shared_examples 'go-get=1' do |enabled_protocol:|
+ context 'with simple 2-segment project path' do
+ let!(:project) { create(:project, :private) }
- context 'with subpackages' do
- let(:path) { "#{project.full_path}/subpackage" }
+ context 'with subpackages' do
+ let(:path) { "#{project.full_path}/subpackage" }
- it 'returns the full project path' do
- expect_response_with_path(go, project.full_path)
- end
- end
-
- context 'without subpackages' do
- let(:path) { project.full_path }
-
- it 'returns the full project path' do
- expect_response_with_path(go, project.full_path)
+ it 'returns the full project path' do
+ expect_response_with_path(go, enabled_protocol, project.full_path)
+ end
end
- end
- end
- context 'with a nested project path' do
- let(:group) { create(:group, :nested) }
- let!(:project) { create(:project, :public, namespace: group) }
+ context 'without subpackages' do
+ let(:path) { project.full_path }
- shared_examples 'a nested project' do
- context 'when the project is public' do
it 'returns the full project path' do
- expect_response_with_path(go, project.full_path)
+ expect_response_with_path(go, enabled_protocol, project.full_path)
end
end
+ end
- context 'when the project is private' do
- before do
- project.update_attribute(:visibility_level, Project::PRIVATE)
- end
+ context 'with a nested project path' do
+ let(:group) { create(:group, :nested) }
+ let!(:project) { create(:project, :public, namespace: group) }
- context 'with access to the project' do
- let(:current_user) { project.creator }
+ shared_examples 'a nested project' do
+ context 'when the project is public' do
+ it 'returns the full project path' do
+ expect_response_with_path(go, enabled_protocol, project.full_path)
+ end
+ end
+ context 'when the project is private' do
before do
- project.team.add_master(current_user)
+ project.update_attribute(:visibility_level, Project::PRIVATE)
end
- it 'returns the full project path' do
- expect_response_with_path(go, project.full_path)
+ context 'with access to the project' do
+ let(:current_user) { project.creator }
+
+ before do
+ project.team.add_master(current_user)
+ end
+
+ it 'returns the full project path' do
+ expect_response_with_path(go, enabled_protocol, project.full_path)
+ end
end
- end
- context 'without access to the project' do
- it 'returns the 2-segment group path' do
- expect_response_with_path(go, group.full_path)
+ context 'without access to the project' do
+ it 'returns the 2-segment group path' do
+ expect_response_with_path(go, enabled_protocol, group.full_path)
+ end
end
end
end
- end
- context 'with subpackages' do
- let(:path) { "#{project.full_path}/subpackage" }
+ context 'with subpackages' do
+ let(:path) { "#{project.full_path}/subpackage" }
- it_behaves_like 'a nested project'
- end
+ it_behaves_like 'a nested project'
+ end
+
+ context 'with a subpackage that is not a valid project path' do
+ let(:path) { "#{project.full_path}/---subpackage" }
- context 'with a subpackage that is not a valid project path' do
- let(:path) { "#{project.full_path}/---subpackage" }
+ it_behaves_like 'a nested project'
+ end
+
+ context 'without subpackages' do
+ let(:path) { project.full_path }
- it_behaves_like 'a nested project'
+ it_behaves_like 'a nested project'
+ end
end
- context 'without subpackages' do
- let(:path) { project.full_path }
+ context 'with a bogus path' do
+ let(:path) { "http:;url=http:&sol;&sol;www.example.com'http-equiv='refresh'x='?go-get=1" }
+
+ it 'skips go-import generation' do
+ expect(app).to receive(:call).and_return('no-go')
- it_behaves_like 'a nested project'
+ go
+ end
+ end
+ end
+
+ context 'with SSH disabled' do
+ before do
+ stub_application_setting(enabled_git_access_protocol: 'http')
end
+
+ include_examples 'go-get=1', enabled_protocol: :http
end
- context 'with a bogus path' do
- let(:path) { "http:;url=http:&sol;&sol;www.example.com'http-equiv='refresh'x='?go-get=1" }
+ context 'with HTTP disabled' do
+ before do
+ stub_application_setting(enabled_git_access_protocol: 'ssh')
+ end
- it 'skips go-import generation' do
- expect(app).to receive(:call).and_return('no-go')
+ include_examples 'go-get=1', enabled_protocol: :ssh
+ end
- go
+ context 'with nothing disabled' do
+ before do
+ stub_application_setting(enabled_git_access_protocol: nil)
end
+
+ include_examples 'go-get=1', enabled_protocol: nil
end
end
@@ -113,10 +139,16 @@ describe Gitlab::Middleware::Go do
middleware.call(env)
end
- def expect_response_with_path(response, path)
+ def expect_response_with_path(response, protocol, path)
+ repository_url = case protocol
+ when :ssh
+ "ssh://git@#{Gitlab.config.gitlab.host}/#{path}.git"
+ when :http, nil
+ "http://#{Gitlab.config.gitlab.host}/#{path}.git"
+ end
expect(response[0]).to eq(200)
expect(response[1]['Content-Type']).to eq('text/html')
- expected_body = %{<html><head><meta name="go-import" content="#{Gitlab.config.gitlab.host}/#{path} git http://#{Gitlab.config.gitlab.host}/#{path}.git" /></head></html>}
+ expected_body = %{<html><head><meta name="go-import" content="#{Gitlab.config.gitlab.host}/#{path} git #{repository_url}" /></head></html>}
expect(response[2].body).to eq([expected_body])
end
end
diff --git a/spec/lib/gitlab/middleware/read_only_spec.rb b/spec/lib/gitlab/middleware/read_only_spec.rb
index 742a792a1af..86be06ff595 100644
--- a/spec/lib/gitlab/middleware/read_only_spec.rb
+++ b/spec/lib/gitlab/middleware/read_only_spec.rb
@@ -83,6 +83,13 @@ describe Gitlab::Middleware::ReadOnly do
expect(subject).to disallow_request
end
+ it 'expects POST of new file that looks like an LFS batch url to be disallowed' do
+ response = request.post('/root/gitlab-ce/new/master/app/info/lfs/objects/batch')
+
+ expect(response).to be_a_redirect
+ expect(subject).to disallow_request
+ end
+
context 'whitelisted requests' do
it 'expects DELETE request to logout to be allowed' do
response = request.delete('/users/sign_out')
@@ -104,6 +111,25 @@ describe Gitlab::Middleware::ReadOnly do
expect(response).not_to be_a_redirect
expect(subject).not_to disallow_request
end
+
+ it 'expects a POST request to git-upload-pack URL to be allowed' do
+ response = request.post('/root/rouge.git/git-upload-pack')
+
+ expect(response).not_to be_a_redirect
+ expect(subject).not_to disallow_request
+ end
+
+ it 'expects requests to sidekiq admin to be allowed' do
+ response = request.post('/admin/sidekiq')
+
+ expect(response).not_to be_a_redirect
+ expect(subject).not_to disallow_request
+
+ response = request.get('/admin/sidekiq')
+
+ expect(response).not_to be_a_redirect
+ expect(subject).not_to disallow_request
+ end
end
end
diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb
index db26e16e3b2..c7471a21fda 100644
--- a/spec/lib/gitlab/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/o_auth/user_spec.rb
@@ -4,7 +4,7 @@ describe Gitlab::OAuth::User do
let(:oauth_user) { described_class.new(auth_hash) }
let(:gl_user) { oauth_user.gl_user }
let(:uid) { 'my-uid' }
- let(:dn) { 'uid=user1,ou=People,dc=example' }
+ let(:dn) { 'uid=user1,ou=people,dc=example' }
let(:provider) { 'my-provider' }
let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: provider, info: info_hash) }
let(:info_hash) do
diff --git a/spec/lib/gitlab/saml/user_spec.rb b/spec/lib/gitlab/saml/user_spec.rb
index 1c23fb5f285..1765980e977 100644
--- a/spec/lib/gitlab/saml/user_spec.rb
+++ b/spec/lib/gitlab/saml/user_spec.rb
@@ -7,7 +7,7 @@ describe Gitlab::Saml::User do
let(:saml_user) { described_class.new(auth_hash) }
let(:gl_user) { saml_user.gl_user }
let(:uid) { 'my-uid' }
- let(:dn) { 'uid=user1,ou=People,dc=example' }
+ let(:dn) { 'uid=user1,ou=people,dc=example' }
let(:provider) { 'saml' }
let(:raw_info_attr) { { 'groups' => %w(Developers Freelancers Designers) } }
let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: provider, info: info_hash, extra: { raw_info: OneLogin::RubySaml::Attributes.new(raw_info_attr) }) }
diff --git a/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb b/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb
new file mode 100644
index 00000000000..8fdbbacd04d
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb
@@ -0,0 +1,63 @@
+require 'spec_helper'
+
+describe Gitlab::SidekiqMiddleware::MemoryKiller do
+ subject { described_class.new }
+ let(:pid) { 999 }
+
+ let(:worker) { double(:worker, class: 'TestWorker') }
+ let(:job) { { 'jid' => 123 } }
+ let(:queue) { 'test_queue' }
+
+ def run
+ thread = subject.call(worker, job, queue) { nil }
+ thread&.join
+ end
+
+ before do
+ allow(subject).to receive(:get_rss).and_return(10.kilobytes)
+ allow(subject).to receive(:pid).and_return(pid)
+ end
+
+ context 'when MAX_RSS is set to 0' do
+ before do
+ stub_const("#{described_class}::MAX_RSS", 0)
+ end
+
+ it 'does nothing' do
+ expect(subject).not_to receive(:sleep)
+
+ run
+ end
+ end
+
+ context 'when MAX_RSS is exceeded' do
+ before do
+ stub_const("#{described_class}::MAX_RSS", 5.kilobytes)
+ end
+
+ it 'sends the STP, TERM and KILL signals at expected times' do
+ expect(subject).to receive(:sleep).with(15 * 60).ordered
+ expect(Process).to receive(:kill).with('SIGSTP', pid).ordered
+
+ expect(subject).to receive(:sleep).with(30).ordered
+ expect(Process).to receive(:kill).with('SIGTERM', pid).ordered
+
+ expect(subject).to receive(:sleep).with(10).ordered
+ expect(Process).to receive(:kill).with('SIGKILL', pid).ordered
+
+ run
+ end
+ end
+
+ context 'when MAX_RSS is not exceeded' do
+ before do
+ stub_const("#{described_class}::MAX_RSS", 15.kilobytes)
+ end
+
+ it 'does nothing' do
+ expect(subject).not_to receive(:sleep)
+
+ run
+ end
+ end
+end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index a7b65e94706..a4c1113ae37 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -60,9 +60,9 @@ describe Gitlab::UsageData do
deploy_keys
deployments
environments
- gcp_clusters
- gcp_clusters_enabled
- gcp_clusters_disabled
+ clusters
+ clusters_enabled
+ clusters_disabled
in_review_folder
groups
issues
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index 80bf7986ee0..249c77dc636 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -268,7 +268,8 @@ describe Gitlab::Workhorse do
GL_ID: "user-#{user.id}",
GL_USERNAME: user.username,
GL_REPOSITORY: "project-#{project.id}",
- RepoPath: repo_path
+ RepoPath: repo_path,
+ ShowAllRefs: false
}
end
@@ -282,7 +283,8 @@ describe Gitlab::Workhorse do
GL_ID: "user-#{user.id}",
GL_USERNAME: user.username,
GL_REPOSITORY: "wiki-#{project.id}",
- RepoPath: repo_path
+ RepoPath: repo_path,
+ ShowAllRefs: false
}
end
@@ -324,6 +326,12 @@ describe Gitlab::Workhorse do
expect(subject).to include(gitaly_params)
end
+
+ context 'show_all_refs enabled' do
+ subject { described_class.git_http_ok(repository, false, user, action, show_all_refs: true) }
+
+ it { is_expected.to include(ShowAllRefs: true) }
+ end
end
context "when git_receive_pack action is passed" do
@@ -336,6 +344,12 @@ describe Gitlab::Workhorse do
let(:action) { 'info_refs' }
it { expect(subject).to include(gitaly_params) }
+
+ context 'show_all_refs enabled' do
+ subject { described_class.git_http_ok(repository, false, user, action, show_all_refs: true) }
+
+ it { is_expected.to include(ShowAllRefs: true) }
+ end
end
context 'when action passed is not supported by Gitaly' do
diff --git a/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb b/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb
index b4b83b70d1c..a0fb86345f3 100644
--- a/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb
+++ b/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb
@@ -39,14 +39,6 @@ describe SystemCheck::App::GitUserDefaultSSHConfigCheck do
it { is_expected.to eq(expected_result) }
end
-
- it 'skips GitLab read-only instances' do
- stub_user
- stub_home_dir
- allow(Gitlab::Database).to receive(:read_only?).and_return(true)
-
- is_expected.to be_truthy
- end
end
describe '#check?' do
diff --git a/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb b/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb
new file mode 100644
index 00000000000..9f41534441b
--- /dev/null
+++ b/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb
@@ -0,0 +1,166 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb')
+
+describe MigrateGcpClustersToNewClustersArchitectures, :migration do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:service) { create(:kubernetes_service, project: project) }
+
+ context 'when cluster is being created' do
+ let(:project_id) { project.id }
+ let(:user_id) { user.id }
+ let(:service_id) { service.id }
+ let(:status) { 2 } # creating
+ let(:gcp_cluster_size) { 1 }
+ let(:created_at) { "'2017-10-17 20:24:02'" }
+ let(:updated_at) { "'2017-10-17 20:28:44'" }
+ let(:enabled) { true }
+ let(:status_reason) { "''" }
+ let(:project_namespace) { "'sample-app'" }
+ let(:endpoint) { 'NULL' }
+ let(:ca_cert) { 'NULL' }
+ let(:encrypted_kubernetes_token) { 'NULL' }
+ let(:encrypted_kubernetes_token_iv) { 'NULL' }
+ let(:username) { 'NULL' }
+ let(:encrypted_password) { 'NULL' }
+ let(:encrypted_password_iv) { 'NULL' }
+ let(:gcp_project_id) { "'gcp_project_id'" }
+ let(:gcp_cluster_zone) { "'gcp_cluster_zone'" }
+ let(:gcp_cluster_name) { "'gcp_cluster_name'" }
+ let(:gcp_machine_type) { "'gcp_machine_type'" }
+ let(:gcp_operation_id) { 'NULL' }
+ let(:encrypted_gcp_token) { "'encrypted_gcp_token'" }
+ let(:encrypted_gcp_token_iv) { "'encrypted_gcp_token_iv'" }
+
+ let(:cluster) { Clusters::Cluster.last }
+ let(:cluster_id) { cluster.id }
+
+ before do
+ ActiveRecord::Base.connection.execute <<-SQL
+ INSERT INTO gcp_clusters (project_id, user_id, service_id, status, gcp_cluster_size, created_at, updated_at, enabled, status_reason, project_namespace, endpoint, ca_cert, encrypted_kubernetes_token, encrypted_kubernetes_token_iv, username, encrypted_password, encrypted_password_iv, gcp_project_id, gcp_cluster_zone, gcp_cluster_name, gcp_machine_type, gcp_operation_id, encrypted_gcp_token, encrypted_gcp_token_iv)
+ VALUES (#{project_id}, #{user_id}, #{service_id}, #{status}, #{gcp_cluster_size}, #{created_at}, #{updated_at}, #{enabled}, #{status_reason}, #{project_namespace}, #{endpoint}, #{ca_cert}, #{encrypted_kubernetes_token}, #{encrypted_kubernetes_token_iv}, #{username}, #{encrypted_password}, #{encrypted_password_iv}, #{gcp_project_id}, #{gcp_cluster_zone}, #{gcp_cluster_name}, #{gcp_machine_type}, #{gcp_operation_id}, #{encrypted_gcp_token}, #{encrypted_gcp_token_iv});
+ SQL
+ end
+
+ it 'correctly migrate to new clusters architectures' do
+ migrate!
+
+ expect(Clusters::Cluster.count).to eq(1)
+ expect(Clusters::Project.count).to eq(1)
+ expect(Clusters::Providers::Gcp.count).to eq(1)
+ expect(Clusters::Platforms::Kubernetes.count).to eq(1)
+
+ expect(cluster.user).to eq(user)
+ expect(cluster.enabled).to be_truthy
+ expect(cluster.name).to eq(gcp_cluster_name.delete!("'"))
+ expect(cluster.provider_type).to eq('gcp')
+ expect(cluster.platform_type).to eq('kubernetes')
+
+ expect(cluster.project).to eq(project)
+ expect(project.cluster).to eq(cluster)
+
+ expect(cluster.provider_gcp.cluster).to eq(cluster)
+ expect(cluster.provider_gcp.status).to eq(status)
+ expect(cluster.provider_gcp.status_reason).to eq(tr(status_reason))
+ expect(cluster.provider_gcp.gcp_project_id).to eq(tr(gcp_project_id))
+ expect(cluster.provider_gcp.zone).to eq(tr(gcp_cluster_zone))
+ expect(cluster.provider_gcp.num_nodes).to eq(gcp_cluster_size)
+ expect(cluster.provider_gcp.machine_type).to eq(tr(gcp_machine_type))
+ expect(cluster.provider_gcp.operation_id).to be_nil
+ expect(cluster.provider_gcp.endpoint).to be_nil
+ expect(cluster.provider_gcp.encrypted_access_token).to eq(tr(encrypted_gcp_token))
+ expect(cluster.provider_gcp.encrypted_access_token_iv).to eq(tr(encrypted_gcp_token_iv))
+
+ expect(cluster.platform_kubernetes.cluster).to eq(cluster)
+ expect(cluster.platform_kubernetes.api_url).to be_nil
+ expect(cluster.platform_kubernetes.ca_cert).to be_nil
+ expect(cluster.platform_kubernetes.namespace).to eq(tr(project_namespace))
+ expect(cluster.platform_kubernetes.username).to be_nil
+ expect(cluster.platform_kubernetes.encrypted_password).to be_nil
+ expect(cluster.platform_kubernetes.encrypted_password_iv).to be_nil
+ expect(cluster.platform_kubernetes.encrypted_token).to be_nil
+ expect(cluster.platform_kubernetes.encrypted_token_iv).to be_nil
+ end
+ end
+
+ context 'when cluster has been created' do
+ let(:project_id) { project.id }
+ let(:user_id) { user.id }
+ let(:service_id) { service.id }
+ let(:status) { 3 } # created
+ let(:gcp_cluster_size) { 1 }
+ let(:created_at) { "'2017-10-17 20:24:02'" }
+ let(:updated_at) { "'2017-10-17 20:28:44'" }
+ let(:enabled) { true }
+ let(:status_reason) { "'general error'" }
+ let(:project_namespace) { "'sample-app'" }
+ let(:endpoint) { "'111.111.111.111'" }
+ let(:ca_cert) { "'ca_cert'" }
+ let(:encrypted_kubernetes_token) { "'encrypted_kubernetes_token'" }
+ let(:encrypted_kubernetes_token_iv) { "'encrypted_kubernetes_token_iv'" }
+ let(:username) { "'username'" }
+ let(:encrypted_password) { "'encrypted_password'" }
+ let(:encrypted_password_iv) { "'encrypted_password_iv'" }
+ let(:gcp_project_id) { "'gcp_project_id'" }
+ let(:gcp_cluster_zone) { "'gcp_cluster_zone'" }
+ let(:gcp_cluster_name) { "'gcp_cluster_name'" }
+ let(:gcp_machine_type) { "'gcp_machine_type'" }
+ let(:gcp_operation_id) { "'gcp_operation_id'" }
+ let(:encrypted_gcp_token) { "'encrypted_gcp_token'" }
+ let(:encrypted_gcp_token_iv) { "'encrypted_gcp_token_iv'" }
+
+ let(:cluster) { Clusters::Cluster.last }
+ let(:cluster_id) { cluster.id }
+
+ before do
+ ActiveRecord::Base.connection.execute <<-SQL
+ INSERT INTO gcp_clusters (project_id, user_id, service_id, status, gcp_cluster_size, created_at, updated_at, enabled, status_reason, project_namespace, endpoint, ca_cert, encrypted_kubernetes_token, encrypted_kubernetes_token_iv, username, encrypted_password, encrypted_password_iv, gcp_project_id, gcp_cluster_zone, gcp_cluster_name, gcp_machine_type, gcp_operation_id, encrypted_gcp_token, encrypted_gcp_token_iv)
+ VALUES (#{project_id}, #{user_id}, #{service_id}, #{status}, #{gcp_cluster_size}, #{created_at}, #{updated_at}, #{enabled}, #{status_reason}, #{project_namespace}, #{endpoint}, #{ca_cert}, #{encrypted_kubernetes_token}, #{encrypted_kubernetes_token_iv}, #{username}, #{encrypted_password}, #{encrypted_password_iv}, #{gcp_project_id}, #{gcp_cluster_zone}, #{gcp_cluster_name}, #{gcp_machine_type}, #{gcp_operation_id}, #{encrypted_gcp_token}, #{encrypted_gcp_token_iv});
+ SQL
+ end
+
+ it 'correctly migrate to new clusters architectures' do
+ migrate!
+
+ expect(Clusters::Cluster.count).to eq(1)
+ expect(Clusters::Project.count).to eq(1)
+ expect(Clusters::Providers::Gcp.count).to eq(1)
+ expect(Clusters::Platforms::Kubernetes.count).to eq(1)
+
+ expect(cluster.user).to eq(user)
+ expect(cluster.enabled).to be_truthy
+ expect(cluster.name).to eq(tr(gcp_cluster_name))
+ expect(cluster.provider_type).to eq('gcp')
+ expect(cluster.platform_type).to eq('kubernetes')
+
+ expect(cluster.project).to eq(project)
+ expect(project.cluster).to eq(cluster)
+
+ expect(cluster.provider_gcp.cluster).to eq(cluster)
+ expect(cluster.provider_gcp.status).to eq(status)
+ expect(cluster.provider_gcp.status_reason).to eq(tr(status_reason))
+ expect(cluster.provider_gcp.gcp_project_id).to eq(tr(gcp_project_id))
+ expect(cluster.provider_gcp.zone).to eq(tr(gcp_cluster_zone))
+ expect(cluster.provider_gcp.num_nodes).to eq(gcp_cluster_size)
+ expect(cluster.provider_gcp.machine_type).to eq(tr(gcp_machine_type))
+ expect(cluster.provider_gcp.operation_id).to eq(tr(gcp_operation_id))
+ expect(cluster.provider_gcp.endpoint).to eq(tr(endpoint))
+ expect(cluster.provider_gcp.encrypted_access_token).to eq(tr(encrypted_gcp_token))
+ expect(cluster.provider_gcp.encrypted_access_token_iv).to eq(tr(encrypted_gcp_token_iv))
+
+ expect(cluster.platform_kubernetes.cluster).to eq(cluster)
+ expect(cluster.platform_kubernetes.api_url).to eq('https://' + tr(endpoint))
+ expect(cluster.platform_kubernetes.ca_cert).to eq(tr(ca_cert))
+ expect(cluster.platform_kubernetes.namespace).to eq(tr(project_namespace))
+ expect(cluster.platform_kubernetes.username).to eq(tr(username))
+ expect(cluster.platform_kubernetes.encrypted_password).to eq(tr(encrypted_password))
+ expect(cluster.platform_kubernetes.encrypted_password_iv).to eq(tr(encrypted_password_iv))
+ expect(cluster.platform_kubernetes.encrypted_token).to eq(tr(encrypted_kubernetes_token))
+ expect(cluster.platform_kubernetes.encrypted_token_iv).to eq(tr(encrypted_kubernetes_token_iv))
+ end
+ end
+
+ def tr(s)
+ s.delete("'")
+ end
+end
diff --git a/spec/migrations/migrate_user_authentication_token_to_personal_access_token_spec.rb b/spec/migrations/migrate_user_authentication_token_to_personal_access_token_spec.rb
new file mode 100644
index 00000000000..b4834705011
--- /dev/null
+++ b/spec/migrations/migrate_user_authentication_token_to_personal_access_token_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20171012125712_migrate_user_authentication_token_to_personal_access_token.rb')
+
+describe MigrateUserAuthenticationTokenToPersonalAccessToken, :migration do
+ let(:users) { table(:users) }
+ let(:personal_access_tokens) { table(:personal_access_tokens) }
+
+ let!(:user) { users.create!(id: 1, email: 'user@example.com', authentication_token: 'user-token', admin: false) }
+ let!(:admin) { users.create!(id: 2, email: 'admin@example.com', authentication_token: 'admin-token', admin: true) }
+
+ it 'migrates private tokens to Personal Access Tokens' do
+ migrate!
+
+ expect(personal_access_tokens.count).to eq(2)
+
+ user_token = personal_access_tokens.find_by(user_id: user.id)
+ admin_token = personal_access_tokens.find_by(user_id: admin.id)
+
+ expect(user_token.token).to eq('user-token')
+ expect(admin_token.token).to eq('admin-token')
+
+ expect(user_token.scopes).to eq(%w[api].to_yaml)
+ expect(admin_token.scopes).to eq(%w[api sudo].to_yaml)
+ end
+end
diff --git a/spec/migrations/populate_merge_requests_latest_merge_request_diff_id_spec.rb b/spec/migrations/populate_merge_requests_latest_merge_request_diff_id_spec.rb
new file mode 100644
index 00000000000..4ea7f441f7c
--- /dev/null
+++ b/spec/migrations/populate_merge_requests_latest_merge_request_diff_id_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20171026082505_populate_merge_requests_latest_merge_request_diff_id')
+
+describe PopulateMergeRequestsLatestMergeRequestDiffId, :migration do
+ let(:projects_table) { table(:projects) }
+ let(:merge_requests_table) { table(:merge_requests) }
+ let(:merge_request_diffs_table) { table(:merge_request_diffs) }
+
+ let(:project) { projects_table.create!(name: 'gitlab', path: 'gitlab-org/gitlab-ce') }
+
+ def create_mr!(name, diffs: 0)
+ merge_request =
+ merge_requests_table.create!(target_project_id: project.id,
+ target_branch: 'master',
+ source_project_id: project.id,
+ source_branch: name,
+ title: name)
+
+ diffs.times do
+ merge_request_diffs_table.create!(merge_request_id: merge_request.id)
+ end
+
+ merge_request
+ end
+
+ def diffs_for(merge_request)
+ merge_request_diffs_table.where(merge_request_id: merge_request.id)
+ end
+
+ describe '#up' do
+ it 'ignores MRs without diffs' do
+ merge_request_without_diff = create_mr!('without_diff')
+
+ expect(merge_request_without_diff.latest_merge_request_diff_id).to be_nil
+
+ expect { migrate! }
+ .not_to change { merge_request_without_diff.reload.latest_merge_request_diff_id }
+ end
+
+ it 'ignores MRs that have a diff ID already set' do
+ merge_request_with_multiple_diffs = create_mr!('with_multiple_diffs', diffs: 3)
+ diff_id = diffs_for(merge_request_with_multiple_diffs).minimum(:id)
+
+ merge_request_with_multiple_diffs.update!(latest_merge_request_diff_id: diff_id)
+
+ expect { migrate! }
+ .not_to change { merge_request_with_multiple_diffs.reload.latest_merge_request_diff_id }
+ end
+
+ it 'migrates multiple MR diffs to the correct values' do
+ merge_requests = Array.new(3).map.with_index { |_, i| create_mr!(i, diffs: 3) }
+
+ migrate!
+
+ merge_requests.each do |merge_request|
+ expect(merge_request.reload.latest_merge_request_diff_id)
+ .to eq(diffs_for(merge_request).maximum(:id))
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 41ecdb604f1..5ed2e1ca99a 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -1271,6 +1271,7 @@ describe Ci::Build do
{ key: 'CI_PROJECT_PATH_SLUG', value: project.full_path_slug, public: true },
{ key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path, public: true },
{ key: 'CI_PROJECT_URL', value: project.web_url, public: true },
+ { key: 'CI_PROJECT_VISIBILITY', value: 'private', public: true },
{ key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true },
{ key: 'CI_CONFIG_PATH', value: pipeline.ci_yaml_file_path, public: true },
{ key: 'CI_REGISTRY_USER', value: 'gitlab-ci-token', public: true },
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
new file mode 100644
index 00000000000..12123a3d753
--- /dev/null
+++ b/spec/models/clusters/cluster_spec.rb
@@ -0,0 +1,181 @@
+require 'spec_helper'
+
+describe Clusters::Cluster do
+ it { is_expected.to belong_to(:user) }
+ it { is_expected.to have_many(:projects) }
+ it { is_expected.to have_one(:provider_gcp) }
+ it { is_expected.to have_one(:platform_kubernetes) }
+ it { is_expected.to delegate_method(:status).to(:provider) }
+ it { is_expected.to delegate_method(:status_reason).to(:provider) }
+ it { is_expected.to delegate_method(:status_name).to(:provider) }
+ it { is_expected.to delegate_method(:on_creation?).to(:provider) }
+ it { is_expected.to delegate_method(:update_kubernetes_integration!).to(:platform) }
+ it { is_expected.to respond_to :project }
+
+ describe '.enabled' do
+ subject { described_class.enabled }
+
+ let!(:cluster) { create(:cluster, enabled: true) }
+
+ before do
+ create(:cluster, enabled: false)
+ end
+
+ it { is_expected.to contain_exactly(cluster) }
+ end
+
+ describe '.disabled' do
+ subject { described_class.disabled }
+
+ let!(:cluster) { create(:cluster, enabled: false) }
+
+ before do
+ create(:cluster, enabled: true)
+ end
+
+ it { is_expected.to contain_exactly(cluster) }
+ end
+
+ describe 'validation' do
+ subject { cluster.valid? }
+
+ context 'when validates name' do
+ context 'when provided by user' do
+ let!(:cluster) { build(:cluster, :provided_by_user, name: name) }
+
+ context 'when name is empty' do
+ let(:name) { '' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when name is nil' do
+ let(:name) { nil }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when name is present' do
+ let(:name) { 'cluster-name-1' }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'when provided by gcp' do
+ let!(:cluster) { build(:cluster, :provided_by_gcp, name: name) }
+
+ context 'when name is shorter than 1' do
+ let(:name) { '' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when name is longer than 63' do
+ let(:name) { 'a' * 64 }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when name includes invalid character' do
+ let(:name) { '!!!!!!' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when name is present' do
+ let(:name) { 'cluster-name-1' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when record is persisted' do
+ let(:name) { 'cluster-name-1' }
+
+ before do
+ cluster.save!
+ end
+
+ context 'when name is changed' do
+ before do
+ cluster.name = 'new-cluster-name'
+ end
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when name is same' do
+ before do
+ cluster.name = name
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+ end
+ end
+
+ context 'when validates restrict_modification' do
+ context 'when creation is on going' do
+ let!(:cluster) { create(:cluster, :providing_by_gcp) }
+
+ it { expect(cluster.update(enabled: false)).to be_falsey }
+ end
+
+ context 'when creation is done' do
+ let!(:cluster) { create(:cluster, :provided_by_gcp) }
+
+ it { expect(cluster.update(enabled: false)).to be_truthy }
+ end
+ end
+ end
+
+ describe '#provider' do
+ subject { cluster.provider }
+
+ context 'when provider is gcp' do
+ let(:cluster) { create(:cluster, :provided_by_gcp) }
+
+ it 'returns a provider' do
+ is_expected.to eq(cluster.provider_gcp)
+ expect(subject.class.name.deconstantize).to eq(Clusters::Providers.to_s)
+ end
+ end
+
+ context 'when provider is user' do
+ let(:cluster) { create(:cluster, :provided_by_user) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#platform' do
+ subject { cluster.platform }
+
+ context 'when platform is kubernetes' do
+ let(:cluster) { create(:cluster, :provided_by_user) }
+
+ it 'returns a platform' do
+ is_expected.to eq(cluster.platform_kubernetes)
+ expect(subject.class.name.deconstantize).to eq(Clusters::Platforms.to_s)
+ end
+ end
+ end
+
+ describe '#first_project' do
+ subject { cluster.first_project }
+
+ context 'when cluster belongs to a project' do
+ let(:cluster) { create(:cluster, :project) }
+ let(:project) { Clusters::Project.find_by_cluster_id(cluster.id).project }
+
+ it { is_expected.to eq(project) }
+ end
+
+ context 'when cluster does not belong to projects' do
+ let(:cluster) { create(:cluster) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+end
diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb
new file mode 100644
index 00000000000..e6ebe079ceb
--- /dev/null
+++ b/spec/models/clusters/platforms/kubernetes_spec.rb
@@ -0,0 +1,188 @@
+require 'spec_helper'
+
+describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching do
+ include KubernetesHelpers
+ include ReactiveCachingHelpers
+
+ it { is_expected.to belong_to(:cluster) }
+ it { is_expected.to respond_to :ca_pem }
+
+ describe 'before_validation' do
+ context 'when namespace includes upper case' do
+ let(:kubernetes) { create(:platform_kubernetes, :configured, namespace: namespace) }
+ let(:namespace) { 'ABC' }
+
+ it 'converts to lower case' do
+ expect(kubernetes.namespace).to eq('abc')
+ end
+ end
+ end
+
+ describe 'validation' do
+ subject { kubernetes.valid? }
+
+ context 'when validates namespace' do
+ let(:kubernetes) { build(:platform_kubernetes, :configured, namespace: namespace) }
+
+ context 'when namespace is blank' do
+ let(:namespace) { '' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when namespace is longer than 63' do
+ let(:namespace) { 'a' * 64 }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when namespace includes invalid character' do
+ let(:namespace) { '!!!!!!' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when namespace is vaild' do
+ let(:namespace) { 'namespace-123' }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'when validates api_url' do
+ let(:kubernetes) { build(:platform_kubernetes, :configured) }
+
+ before do
+ kubernetes.api_url = api_url
+ end
+
+ context 'when api_url is invalid url' do
+ let(:api_url) { '!!!!!!' }
+
+ it { expect(kubernetes.save).to be_falsey }
+ end
+
+ context 'when api_url is nil' do
+ let(:api_url) { nil }
+
+ it { expect(kubernetes.save).to be_falsey }
+ end
+
+ context 'when api_url is valid url' do
+ let(:api_url) { 'https://111.111.111.111' }
+
+ it { expect(kubernetes.save).to be_truthy }
+ end
+ end
+
+ context 'when validates token' do
+ let(:kubernetes) { build(:platform_kubernetes, :configured) }
+
+ before do
+ kubernetes.token = token
+ end
+
+ context 'when token is nil' do
+ let(:token) { nil }
+
+ it { expect(kubernetes.save).to be_falsey }
+ end
+ end
+ end
+
+ describe 'after_save from Clusters::Cluster' do
+ context 'when platform_kubernetes is being cerated' do
+ let(:enabled) { true }
+ let(:project) { create(:project) }
+ let(:cluster) { build(:cluster, provider_type: :gcp, platform_type: :kubernetes, platform_kubernetes: platform, provider_gcp: provider, enabled: enabled, projects: [project]) }
+ let(:platform) { build(:platform_kubernetes, :configured) }
+ let(:provider) { build(:provider_gcp) }
+ let(:kubernetes_service) { project.kubernetes_service }
+
+ it 'updates KubernetesService' do
+ cluster.save!
+
+ expect(kubernetes_service.active).to eq(enabled)
+ expect(kubernetes_service.api_url).to eq(platform.api_url)
+ expect(kubernetes_service.namespace).to eq(platform.namespace)
+ expect(kubernetes_service.ca_pem).to eq(platform.ca_cert)
+ end
+ end
+
+ context 'when platform_kubernetes has been created' do
+ let(:enabled) { false }
+ let!(:project) { create(:project) }
+ let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+ let(:platform) { cluster.platform }
+ let(:kubernetes_service) { project.kubernetes_service }
+
+ it 'updates KubernetesService' do
+ cluster.update(enabled: enabled)
+
+ expect(kubernetes_service.active).to eq(enabled)
+ end
+ end
+
+ context 'when kubernetes_service has been configured without cluster integration' do
+ let!(:project) { create(:project) }
+ let(:cluster) { build(:cluster, provider_type: :gcp, platform_type: :kubernetes, platform_kubernetes: platform, provider_gcp: provider, projects: [project]) }
+ let(:platform) { build(:platform_kubernetes, :configured, api_url: 'https://111.111.111.111') }
+ let(:provider) { build(:provider_gcp) }
+
+ before do
+ create(:kubernetes_service, project: project)
+ end
+
+ it 'raises an error' do
+ expect { cluster.save! }.to raise_error('Kubernetes service already configured')
+ end
+ end
+ end
+
+ describe '#actual_namespace' do
+ subject { kubernetes.actual_namespace }
+
+ let!(:cluster) { create(:cluster, :project, platform_kubernetes: kubernetes) }
+ let(:project) { cluster.project }
+ let(:kubernetes) { create(:platform_kubernetes, :configured, namespace: namespace) }
+
+ context 'when namespace is present' do
+ let(:namespace) { 'namespace-123' }
+
+ it { is_expected.to eq(namespace) }
+ end
+
+ context 'when namespace is not present' do
+ let(:namespace) { nil }
+
+ it { is_expected.to eq("#{project.path}-#{project.id}") }
+ end
+ end
+
+ describe '.namespace_for_project' do
+ subject { described_class.namespace_for_project(project) }
+
+ let(:project) { create(:project) }
+
+ it { is_expected.to eq("#{project.path}-#{project.id}") }
+ end
+
+ describe '#default_namespace' do
+ subject { kubernetes.default_namespace }
+
+ let(:kubernetes) { create(:platform_kubernetes, :configured) }
+
+ context 'when cluster belongs to a project' do
+ let!(:cluster) { create(:cluster, :project, platform_kubernetes: kubernetes) }
+ let(:project) { cluster.project }
+
+ it { is_expected.to eq("#{project.path}-#{project.id}") }
+ end
+
+ context 'when cluster belongs to nothing' do
+ let!(:cluster) { create(:cluster, platform_kubernetes: kubernetes) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+end
diff --git a/spec/models/clusters/project_spec.rb b/spec/models/clusters/project_spec.rb
new file mode 100644
index 00000000000..7d75d6ab345
--- /dev/null
+++ b/spec/models/clusters/project_spec.rb
@@ -0,0 +1,6 @@
+require 'spec_helper'
+
+describe Clusters::Project do
+ it { is_expected.to belong_to(:cluster) }
+ it { is_expected.to belong_to(:project) }
+end
diff --git a/spec/models/clusters/providers/gcp_spec.rb b/spec/models/clusters/providers/gcp_spec.rb
new file mode 100644
index 00000000000..99eb8c46e9a
--- /dev/null
+++ b/spec/models/clusters/providers/gcp_spec.rb
@@ -0,0 +1,183 @@
+require 'spec_helper'
+
+describe Clusters::Providers::Gcp do
+ it { is_expected.to belong_to(:cluster) }
+ it { is_expected.to validate_presence_of(:zone) }
+
+ describe 'default_value_for' do
+ let(:gcp) { build(:provider_gcp) }
+
+ it "has default value" do
+ expect(gcp.zone).to eq('us-central1-a')
+ expect(gcp.num_nodes).to eq(3)
+ expect(gcp.machine_type).to eq('n1-standard-4')
+ end
+ end
+
+ describe 'validation' do
+ subject { gcp.valid? }
+
+ context 'when validates gcp_project_id' do
+ let(:gcp) { build(:provider_gcp, gcp_project_id: gcp_project_id) }
+
+ context 'when gcp_project_id is shorter than 1' do
+ let(:gcp_project_id) { '' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when gcp_project_id is longer than 63' do
+ let(:gcp_project_id) { 'a' * 64 }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when gcp_project_id includes invalid character' do
+ let(:gcp_project_id) { '!!!!!!' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when gcp_project_id is valid' do
+ let(:gcp_project_id) { 'gcp-project-1' }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'when validates num_nodes' do
+ let(:gcp) { build(:provider_gcp, num_nodes: num_nodes) }
+
+ context 'when num_nodes is string' do
+ let(:num_nodes) { 'A3' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when num_nodes is nil' do
+ let(:num_nodes) { nil }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when num_nodes is smaller than 1' do
+ let(:num_nodes) { 0 }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when num_nodes is valid' do
+ let(:num_nodes) { 3 }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+ end
+
+ describe '#state_machine' do
+ context 'when any => [:created]' do
+ let(:gcp) { build(:provider_gcp, :creating) }
+
+ before do
+ gcp.make_created
+ end
+
+ it 'nullify access_token and operation_id' do
+ expect(gcp.access_token).to be_nil
+ expect(gcp.operation_id).to be_nil
+ expect(gcp).to be_created
+ end
+ end
+
+ context 'when any => [:creating]' do
+ let(:gcp) { build(:provider_gcp) }
+
+ context 'when operation_id is present' do
+ let(:operation_id) { 'operation-xxx' }
+
+ before do
+ gcp.make_creating(operation_id)
+ end
+
+ it 'sets operation_id' do
+ expect(gcp.operation_id).to eq(operation_id)
+ expect(gcp).to be_creating
+ end
+ end
+
+ context 'when operation_id is nil' do
+ let(:operation_id) { nil }
+
+ it 'raises an error' do
+ expect { gcp.make_creating(operation_id) }
+ .to raise_error('operation_id is required')
+ end
+ end
+ end
+
+ context 'when any => [:errored]' do
+ let(:gcp) { build(:provider_gcp, :creating) }
+ let(:status_reason) { 'err msg' }
+
+ it 'nullify access_token and operation_id' do
+ gcp.make_errored(status_reason)
+
+ expect(gcp.access_token).to be_nil
+ expect(gcp.operation_id).to be_nil
+ expect(gcp.status_reason).to eq(status_reason)
+ expect(gcp).to be_errored
+ end
+
+ context 'when status_reason is nil' do
+ let(:gcp) { build(:provider_gcp, :errored) }
+
+ it 'does not set status_reason' do
+ gcp.make_errored(nil)
+
+ expect(gcp.status_reason).not_to be_nil
+ end
+ end
+ end
+ end
+
+ describe '#on_creation?' do
+ subject { gcp.on_creation? }
+
+ context 'when status is creating' do
+ let(:gcp) { create(:provider_gcp, :creating) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when status is created' do
+ let(:gcp) { create(:provider_gcp, :created) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#api_client' do
+ subject { gcp.api_client }
+
+ context 'when status is creating' do
+ let(:gcp) { build(:provider_gcp, :creating) }
+
+ it 'returns Cloud Platform API clinet' do
+ expect(subject).to be_an_instance_of(GoogleApi::CloudPlatform::Client)
+ expect(subject.access_token).to eq(gcp.access_token)
+ end
+ end
+
+ context 'when status is created' do
+ let(:gcp) { build(:provider_gcp, :created) }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when status is errored' do
+ let(:gcp) { build(:provider_gcp, :errored) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+end
diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb
index ab8773b7ede..3106207811a 100644
--- a/spec/models/concerns/routable_spec.rb
+++ b/spec/models/concerns/routable_spec.rb
@@ -134,6 +134,7 @@ describe Group, 'Routable' do
context 'with RequestStore active', :request_store do
it 'does not load the route table more than once' do
+ group.expires_full_path_cache
expect(group).to receive(:uncached_full_path).once.and_call_original
3.times { group.full_path }
diff --git a/spec/models/concerns/subscribable_spec.rb b/spec/models/concerns/subscribable_spec.rb
index 28ff8158e0e..45dfb136aea 100644
--- a/spec/models/concerns/subscribable_spec.rb
+++ b/spec/models/concerns/subscribable_spec.rb
@@ -6,6 +6,12 @@ describe Subscribable, 'Subscribable' do
let(:user_1) { create(:user) }
describe '#subscribed?' do
+ context 'without user' do
+ it 'returns false' do
+ expect(resource.subscribed?(nil, project)).to be_falsey
+ end
+ end
+
context 'without project' do
it 'returns false when no subscription exists' do
expect(resource.subscribed?(user_1)).to be_falsey
diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb
index 882afeccfc6..dfb83578fce 100644
--- a/spec/models/concerns/token_authenticatable_spec.rb
+++ b/spec/models/concerns/token_authenticatable_spec.rb
@@ -12,7 +12,7 @@ shared_examples 'TokenAuthenticatable' do
end
describe User, 'TokenAuthenticatable' do
- let(:token_field) { :authentication_token }
+ let(:token_field) { :rss_token }
it_behaves_like 'TokenAuthenticatable'
describe 'ensures authentication token' do
diff --git a/spec/models/email_spec.rb b/spec/models/email_spec.rb
index b32dd31ae6d..47eb0717c0c 100644
--- a/spec/models/email_spec.rb
+++ b/spec/models/email_spec.rb
@@ -40,4 +40,12 @@ describe Email do
expect(user.emails.confirmed.count).to eq 1
end
end
+
+ describe 'delegation' do
+ let(:user) { create(:user) }
+
+ it 'delegates to :user' do
+ expect(build(:email, user: user).username).to eq user.username
+ end
+ end
end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index e1be23541e8..1ce1d595c60 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -18,7 +18,6 @@ describe Environment do
it { is_expected.to validate_length_of(:slug).is_at_most(24) }
it { is_expected.to validate_length_of(:external_url).is_at_most(255) }
- it { is_expected.to validate_uniqueness_of(:external_url).scoped_to(:project_id) }
describe '.order_by_last_deployed_at' do
let(:project) { create(:project, :repository) }
@@ -547,6 +546,15 @@ describe Environment do
expect(environment.slug).to eq(original_slug)
end
+
+ it "regenerates the slug if nil" do
+ environment = build(:environment, slug: nil)
+
+ new_slug = environment.slug
+
+ expect(new_slug).not_to be_nil
+ expect(environment.slug).to eq(new_slug)
+ end
end
describe '#generate_slug' do
@@ -583,6 +591,12 @@ describe Environment do
it 'returns a path that uses the slug and does not have spaces' do
expect(environment.ref_path).to start_with('refs/environments/staging-review-1-')
end
+
+ it "doesn't change when the slug is nil initially" do
+ environment.slug = nil
+
+ expect(environment.ref_path).to eq(environment.ref_path)
+ end
end
describe '#external_url_for' do
diff --git a/spec/models/fork_network_spec.rb b/spec/models/fork_network_spec.rb
index 605ccd6db06..a43baf1820a 100644
--- a/spec/models/fork_network_spec.rb
+++ b/spec/models/fork_network_spec.rb
@@ -24,6 +24,16 @@ describe ForkNetwork do
end
end
+ describe '#merge_requests' do
+ it 'finds merge requests within the fork network' do
+ project = create(:project)
+ forked_project = fork_project(project)
+ merge_request = create(:merge_request, source_project: forked_project, target_project: project)
+
+ expect(project.fork_network.merge_requests).to include(merge_request)
+ end
+ end
+
context 'for a deleted project' do
it 'keeps the fork network' do
project = create(:project, :public)
diff --git a/spec/models/gcp/cluster_spec.rb b/spec/models/gcp/cluster_spec.rb
deleted file mode 100644
index 8f39fff6394..00000000000
--- a/spec/models/gcp/cluster_spec.rb
+++ /dev/null
@@ -1,264 +0,0 @@
-require 'spec_helper'
-
-describe Gcp::Cluster do
- it { is_expected.to belong_to(:project) }
- it { is_expected.to belong_to(:user) }
- it { is_expected.to belong_to(:service) }
-
- it { is_expected.to validate_presence_of(:gcp_cluster_zone) }
-
- describe '.enabled' do
- subject { described_class.enabled }
-
- let!(:cluster) { create(:gcp_cluster, enabled: true) }
-
- before do
- create(:gcp_cluster, enabled: false)
- end
-
- it { is_expected.to contain_exactly(cluster) }
- end
-
- describe '.disabled' do
- subject { described_class.disabled }
-
- let!(:cluster) { create(:gcp_cluster, enabled: false) }
-
- before do
- create(:gcp_cluster, enabled: true)
- end
-
- it { is_expected.to contain_exactly(cluster) }
- end
-
- describe '#default_value_for' do
- let(:cluster) { described_class.new }
-
- it { expect(cluster.gcp_cluster_zone).to eq('us-central1-a') }
- it { expect(cluster.gcp_cluster_size).to eq(3) }
- it { expect(cluster.gcp_machine_type).to eq('n1-standard-4') }
- end
-
- describe '#validates' do
- subject { cluster.valid? }
-
- context 'when validates gcp_project_id' do
- let(:cluster) { build(:gcp_cluster, gcp_project_id: gcp_project_id) }
-
- context 'when valid' do
- let(:gcp_project_id) { 'gcp-project-12345' }
-
- it { is_expected.to be_truthy }
- end
-
- context 'when empty' do
- let(:gcp_project_id) { '' }
-
- it { is_expected.to be_falsey }
- end
-
- context 'when too long' do
- let(:gcp_project_id) { 'A' * 64 }
-
- it { is_expected.to be_falsey }
- end
-
- context 'when includes abnormal character' do
- let(:gcp_project_id) { '!!!!!!' }
-
- it { is_expected.to be_falsey }
- end
- end
-
- context 'when validates gcp_cluster_name' do
- let(:cluster) { build(:gcp_cluster, gcp_cluster_name: gcp_cluster_name) }
-
- context 'when valid' do
- let(:gcp_cluster_name) { 'test-cluster' }
-
- it { is_expected.to be_truthy }
- end
-
- context 'when empty' do
- let(:gcp_cluster_name) { '' }
-
- it { is_expected.to be_falsey }
- end
-
- context 'when too long' do
- let(:gcp_cluster_name) { 'A' * 64 }
-
- it { is_expected.to be_falsey }
- end
-
- context 'when includes abnormal character' do
- let(:gcp_cluster_name) { '!!!!!!' }
-
- it { is_expected.to be_falsey }
- end
- end
-
- context 'when validates gcp_cluster_size' do
- let(:cluster) { build(:gcp_cluster, gcp_cluster_size: gcp_cluster_size) }
-
- context 'when valid' do
- let(:gcp_cluster_size) { 1 }
-
- it { is_expected.to be_truthy }
- end
-
- context 'when zero' do
- let(:gcp_cluster_size) { 0 }
-
- it { is_expected.to be_falsey }
- end
- end
-
- context 'when validates project_namespace' do
- let(:cluster) { build(:gcp_cluster, project_namespace: project_namespace) }
-
- context 'when valid' do
- let(:project_namespace) { 'default-namespace' }
-
- it { is_expected.to be_truthy }
- end
-
- context 'when empty' do
- let(:project_namespace) { '' }
-
- it { is_expected.to be_truthy }
- end
-
- context 'when too long' do
- let(:project_namespace) { 'A' * 64 }
-
- it { is_expected.to be_falsey }
- end
-
- context 'when includes abnormal character' do
- let(:project_namespace) { '!!!!!!' }
-
- it { is_expected.to be_falsey }
- end
- end
-
- context 'when validates restrict_modification' do
- let(:cluster) { create(:gcp_cluster) }
-
- before do
- cluster.make_creating!
- end
-
- context 'when created' do
- before do
- cluster.make_created!
- end
-
- it { is_expected.to be_truthy }
- end
-
- context 'when creating' do
- it { is_expected.to be_falsey }
- end
- end
- end
-
- describe '#state_machine' do
- let(:cluster) { build(:gcp_cluster) }
-
- context 'when transits to created state' do
- before do
- cluster.gcp_token = 'tmp'
- cluster.gcp_operation_id = 'tmp'
- cluster.make_created!
- end
-
- it 'nullify gcp_token and gcp_operation_id' do
- expect(cluster.gcp_token).to be_nil
- expect(cluster.gcp_operation_id).to be_nil
- expect(cluster).to be_created
- end
- end
-
- context 'when transits to errored state' do
- let(:reason) { 'something wrong' }
-
- before do
- cluster.make_errored!(reason)
- end
-
- it 'sets status_reason' do
- expect(cluster.status_reason).to eq(reason)
- expect(cluster).to be_errored
- end
- end
- end
-
- describe '#project_namespace_placeholder' do
- subject { cluster.project_namespace_placeholder }
-
- let(:cluster) { create(:gcp_cluster) }
-
- it 'returns a placeholder' do
- is_expected.to eq("#{cluster.project.path}-#{cluster.project.id}")
- end
- end
-
- describe '#on_creation?' do
- subject { cluster.on_creation? }
-
- let(:cluster) { create(:gcp_cluster) }
-
- context 'when status is creating' do
- before do
- cluster.make_creating!
- end
-
- it { is_expected.to be_truthy }
- end
-
- context 'when status is created' do
- before do
- cluster.make_created!
- end
-
- it { is_expected.to be_falsey }
- end
- end
-
- describe '#api_url' do
- subject { cluster.api_url }
-
- let(:cluster) { create(:gcp_cluster, :created_on_gke) }
- let(:api_url) { 'https://' + cluster.endpoint }
-
- it { is_expected.to eq(api_url) }
- end
-
- describe '#restrict_modification' do
- subject { cluster.restrict_modification }
-
- let(:cluster) { create(:gcp_cluster) }
-
- context 'when status is created' do
- before do
- cluster.make_created!
- end
-
- it { is_expected.to be_truthy }
- end
-
- context 'when status is creating' do
- before do
- cluster.make_creating!
- end
-
- it { is_expected.to be_falsey }
-
- it 'sets error' do
- is_expected.to be_falsey
- expect(cluster.errors).not_to be_empty
- end
- end
- end
-end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index f36d6eeb327..0e1a7fdce0b 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -488,6 +488,47 @@ describe Group do
end
end
+ describe '#path_changed_hook' do
+ let(:system_hook_service) { SystemHooksService.new }
+
+ context 'for a new group' do
+ let(:group) { build(:group) }
+
+ before do
+ expect(group).to receive(:system_hook_service).and_return(system_hook_service)
+ end
+
+ it 'does not trigger system hook' do
+ expect(system_hook_service).to receive(:execute_hooks_for).with(group, :create)
+
+ group.save!
+ end
+ end
+
+ context 'for an existing group' do
+ let(:group) { create(:group, path: 'old-path') }
+
+ context 'when the path is changed' do
+ let(:new_path) { 'very-new-path' }
+
+ it 'triggers the rename system hook' do
+ expect(group).to receive(:system_hook_service).and_return(system_hook_service)
+ expect(system_hook_service).to receive(:execute_hooks_for).with(group, :rename)
+
+ group.update_attributes!(path: new_path)
+ end
+ end
+
+ context 'when the path is not changed' do
+ it 'does not trigger system hook' do
+ expect(group).not_to receive(:system_hook_service)
+
+ group.update_attributes!(name: 'new name')
+ end
+ end
+ end
+ end
+
describe '#secret_variables_for' do
let(:project) { create(:project, group: group) }
diff --git a/spec/models/identity_spec.rb b/spec/models/identity_spec.rb
index 4ca6556d0f4..3ed048744de 100644
--- a/spec/models/identity_spec.rb
+++ b/spec/models/identity_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-RSpec.describe Identity do
+describe Identity do
describe 'relations' do
it { is_expected.to belong_to(:user) }
end
@@ -22,4 +22,16 @@ RSpec.describe Identity do
expect(other_identity.ldap?).to be_falsey
end
end
+
+ describe '.with_extern_uid' do
+ context 'LDAP identity' do
+ let!(:ldap_identity) { create(:identity, provider: 'ldapmain', extern_uid: 'uid=john smith,ou=people,dc=example,dc=com') }
+
+ it 'finds the identity when the DN is formatted differently' do
+ identity = described_class.with_extern_uid('ldapmain', 'uid=John Smith, ou=People, dc=example, dc=com').first
+
+ expect(identity).to eq(ldap_identity)
+ end
+ end
+ end
end
diff --git a/spec/models/merge_request_diff_commit_spec.rb b/spec/models/merge_request_diff_commit_spec.rb
index 9d4a0ecf8c0..7709cf43200 100644
--- a/spec/models/merge_request_diff_commit_spec.rb
+++ b/spec/models/merge_request_diff_commit_spec.rb
@@ -2,14 +2,93 @@ require 'rails_helper'
describe MergeRequestDiffCommit do
let(:merge_request) { create(:merge_request) }
- subject { merge_request.commits.first }
+ let(:project) { merge_request.project }
describe '#to_hash' do
+ subject { merge_request.commits.first }
+
it 'returns the same results as Commit#to_hash, except for parent_ids' do
- commit_from_repo = merge_request.project.repository.commit(subject.sha)
+ commit_from_repo = project.repository.commit(subject.sha)
commit_from_repo_hash = commit_from_repo.to_hash.merge(parent_ids: [])
expect(subject.to_hash).to eq(commit_from_repo_hash)
end
end
+
+ describe '.create_bulk' do
+ let(:sha_attribute) { Gitlab::Database::ShaAttribute.new }
+ let(:merge_request_diff_id) { merge_request.merge_request_diff.id }
+ let(:commits) do
+ [
+ project.commit('5937ac0a7beb003549fc5fd26fc247adbce4a52e'),
+ project.commit('570e7b2abdd848b95f2f578043fc23bd6f6fd24d')
+ ]
+ end
+ let(:rows) do
+ [
+ {
+ "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "authored_date": "2014-02-27T10:01:38.000+01:00".to_time,
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dmitriy.zaporozhets@gmail.com",
+ "committed_date": "2014-02-27T10:01:38.000+01:00".to_time,
+ "committer_name": "Dmitriy Zaporozhets",
+ "committer_email": "dmitriy.zaporozhets@gmail.com",
+ "merge_request_diff_id": merge_request_diff_id,
+ "relative_order": 0,
+ "sha": sha_attribute.type_cast_for_database('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
+ },
+ {
+ "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "authored_date": "2014-02-27T09:57:31.000+01:00".to_time,
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dmitriy.zaporozhets@gmail.com",
+ "committed_date": "2014-02-27T09:57:31.000+01:00".to_time,
+ "committer_name": "Dmitriy Zaporozhets",
+ "committer_email": "dmitriy.zaporozhets@gmail.com",
+ "merge_request_diff_id": merge_request_diff_id,
+ "relative_order": 1,
+ "sha": sha_attribute.type_cast_for_database('570e7b2abdd848b95f2f578043fc23bd6f6fd24d')
+ }
+ ]
+ end
+
+ subject { described_class.create_bulk(merge_request_diff_id, commits) }
+
+ it 'inserts the commits into the database en masse' do
+ expect(Gitlab::Database).to receive(:bulk_insert)
+ .with(described_class.table_name, rows)
+
+ subject
+ end
+
+ context 'with dates larger than the DB limit' do
+ let(:commits) do
+ # This commit's date is "Sun Aug 17 07:12:55 292278994 +0000"
+ [project.commit('ba3343bc4fa403a8dfbfcab7fc1a8c29ee34bd69')]
+ end
+ let(:timestamp) { Time.at((1 << 31) - 1) }
+ let(:rows) do
+ [{
+ "message": "Weird commit date\n",
+ "authored_date": timestamp,
+ "author_name": "Alejandro Rodríguez",
+ "author_email": "alejorro70@gmail.com",
+ "committed_date": timestamp,
+ "committer_name": "Alejandro Rodríguez",
+ "committer_email": "alejorro70@gmail.com",
+ "merge_request_diff_id": merge_request_diff_id,
+ "relative_order": 0,
+ "sha": sha_attribute.type_cast_for_database('ba3343bc4fa403a8dfbfcab7fc1a8c29ee34bd69')
+ }]
+ end
+
+ it 'uses a sanitized date' do
+ expect(Gitlab::Database).to receive(:bulk_insert)
+ .with(described_class.table_name, rows)
+
+ subject
+ end
+ end
+ end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 73e038b61ed..476a2697605 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -86,7 +86,7 @@ describe MergeRequest do
context 'when the target branch does not exist' do
before do
- project.repository.raw_repository.delete_branch(subject.target_branch)
+ project.repository.rm_branch(subject.author, subject.target_branch)
end
it 'returns nil' do
@@ -1388,7 +1388,7 @@ describe MergeRequest do
context 'when the target branch does not exist' do
before do
- subject.project.repository.raw_repository.delete_branch(subject.target_branch)
+ subject.project.repository.rm_branch(subject.author, subject.target_branch)
end
it 'returns nil' do
@@ -1460,6 +1460,12 @@ describe MergeRequest do
end
describe '#merge_ongoing?' do
+ it 'returns true when the merge request is locked' do
+ merge_request = build_stubbed(:merge_request, state: :locked)
+
+ expect(merge_request.merge_ongoing?).to be(true)
+ end
+
it 'returns true when merge_id, MR is not merged and it has no running job' do
merge_request = build_stubbed(:merge_request, state: :open, merge_jid: 'foo')
allow(Gitlab::SidekiqStatus).to receive(:running?).with('foo') { true }
diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb
index 2298dcab55f..7617e1f89b1 100644
--- a/spec/models/project_services/kubernetes_service_spec.rb
+++ b/spec/models/project_services/kubernetes_service_spec.rb
@@ -99,45 +99,34 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
describe '#actual_namespace' do
subject { service.actual_namespace }
- it "returns the default namespace" do
- is_expected.to eq(service.send(:default_namespace))
- end
-
- context 'when namespace is specified' do
- before do
- service.namespace = 'my-namespace'
+ shared_examples 'a correctly formatted namespace' do
+ it 'returns a valid Kubernetes namespace name' do
+ expect(subject).to match(Gitlab::Regex.kubernetes_namespace_regex)
+ expect(subject).to eq(expected_namespace)
end
+ end
- it "returns the user-namespace" do
- is_expected.to eq('my-namespace')
- end
+ it_behaves_like 'a correctly formatted namespace' do
+ let(:expected_namespace) { service.send(:default_namespace) }
end
- context 'when service is not assigned to project' do
+ context 'when the project path contains forbidden characters' do
before do
- service.project = nil
+ project.path = '-a_Strange.Path--forSure'
end
- it "does not return namespace" do
- is_expected.to be_nil
+ it_behaves_like 'a correctly formatted namespace' do
+ let(:expected_namespace) { "a-strange-path--forsure-#{project.id}" }
end
end
- end
-
- describe '#actual_namespace' do
- subject { service.actual_namespace }
-
- it "returns the default namespace" do
- is_expected.to eq(service.send(:default_namespace))
- end
context 'when namespace is specified' do
before do
service.namespace = 'my-namespace'
end
- it "returns the user-namespace" do
- is_expected.to eq('my-namespace')
+ it_behaves_like 'a correctly formatted namespace' do
+ let(:expected_namespace) { 'my-namespace' }
end
end
@@ -146,7 +135,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
service.project = nil
end
- it "does not return namespace" do
+ it 'does not return namespace' do
is_expected.to be_nil
end
end
@@ -156,7 +145,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
let(:discovery_url) { 'https://kubernetes.example.com/api/v1' }
before do
- stub_kubeclient_discover
+ stub_kubeclient_discover(service.api_url)
end
context 'with path prefix in api_url' do
@@ -164,7 +153,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
it 'tests with the prefix' do
service.api_url = 'https://kubernetes.example.com/prefix'
- stub_kubeclient_discover
+ stub_kubeclient_discover(service.api_url)
expect(service.test[:success]).to be_truthy
expect(WebMock).to have_requested(:get, discovery_url).once
diff --git a/spec/models/project_services/packagist_service_spec.rb b/spec/models/project_services/packagist_service_spec.rb
new file mode 100644
index 00000000000..6acee311700
--- /dev/null
+++ b/spec/models/project_services/packagist_service_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe PackagistService do
+ describe "Associations" do
+ it { is_expected.to belong_to :project }
+ it { is_expected.to have_one :service_hook }
+ end
+
+ let(:project) { create(:project) }
+
+ let(:packagist_server) { 'https://packagist.example.com' }
+ let(:packagist_username) { 'theUser' }
+ let(:packagist_token) { 'verySecret' }
+ let(:packagist_hook_url) do
+ "#{packagist_server}/api/update-package?username=#{packagist_username}&apiToken=#{packagist_token}"
+ end
+
+ let(:packagist_params) do
+ {
+ active: true,
+ project: project,
+ properties: {
+ username: packagist_username,
+ token: packagist_token,
+ server: packagist_server
+ }
+ }
+ end
+
+ describe '#execute' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:push_sample_data) { Gitlab::DataBuilder::Push.build_sample(project, user) }
+ let(:packagist_service) { described_class.create(packagist_params) }
+
+ before do
+ stub_request(:post, packagist_hook_url)
+ end
+
+ it 'calls Packagist API' do
+ packagist_service.execute(push_sample_data)
+
+ expect(a_request(:post, packagist_hook_url)).to have_been_made.once
+ end
+ end
+end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 74eba7e33f6..e8588975118 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -24,6 +24,7 @@ describe Project do
it { is_expected.to have_one(:slack_service) }
it { is_expected.to have_one(:microsoft_teams_service) }
it { is_expected.to have_one(:mattermost_service) }
+ it { is_expected.to have_one(:packagist_service) }
it { is_expected.to have_one(:pushover_service) }
it { is_expected.to have_one(:asana_service) }
it { is_expected.to have_many(:boards) }
@@ -1922,6 +1923,20 @@ describe Project do
expect(forked_project.in_fork_network_of?(other_project)).to be_falsy
end
end
+
+ describe '#fork_source' do
+ let!(:second_fork) { fork_project(forked_project) }
+
+ it 'returns the direct source if it exists' do
+ expect(second_fork.fork_source).to eq(forked_project)
+ end
+
+ it 'returns the root of the fork network when the directs source was deleted' do
+ forked_project.destroy
+
+ expect(second_fork.fork_source).to eq(project)
+ end
+ end
end
describe '#pushes_since_gc' do
@@ -2452,6 +2467,7 @@ describe Project do
context 'legacy storage' do
let(:project) { create(:project, :repository) }
let(:gitlab_shell) { Gitlab::Shell.new }
+ let(:project_storage) { project.send(:storage) }
before do
allow(project).to receive(:gitlab_shell).and_return(gitlab_shell)
@@ -2493,7 +2509,7 @@ describe Project do
describe '#hashed_storage?' do
it 'returns false' do
- expect(project.hashed_storage?).to be_falsey
+ expect(project.hashed_storage?(:repository)).to be_falsey
end
end
@@ -2546,6 +2562,30 @@ describe Project do
it { expect { subject }.to raise_error(StandardError) }
end
+
+ context 'gitlab pages' do
+ before do
+ expect(project_storage).to receive(:rename_repo) { true }
+ end
+
+ it 'moves pages folder to new location' do
+ expect_any_instance_of(Gitlab::PagesTransfer).to receive(:rename_project)
+
+ project.rename_repo
+ end
+ end
+
+ context 'attachments' do
+ before do
+ expect(project_storage).to receive(:rename_repo) { true }
+ end
+
+ it 'moves uploads folder to new location' do
+ expect_any_instance_of(Gitlab::UploadsTransfer).to receive(:rename_project)
+
+ project.rename_repo
+ end
+ end
end
describe '#pages_path' do
@@ -2605,8 +2645,14 @@ describe Project do
end
describe '#hashed_storage?' do
- it 'returns true' do
- expect(project.hashed_storage?).to be_truthy
+ it 'returns true if rolled out' do
+ expect(project.hashed_storage?(:attachments)).to be_truthy
+ end
+
+ it 'returns false when not rolled out yet' do
+ project.storage_version = 1
+
+ expect(project.hashed_storage?(:attachments)).to be_falsey
end
end
@@ -2649,10 +2695,6 @@ describe Project do
.to receive(:execute_hooks_for)
.with(project, :rename)
- expect_any_instance_of(Gitlab::UploadsTransfer)
- .to receive(:rename_project)
- .with('foo', project.path, project.namespace.full_path)
-
expect(project).to receive(:expire_caches_before_rename)
expect(project).to receive(:expires_full_path_cache)
@@ -2673,6 +2715,32 @@ describe Project do
it { expect { subject }.to raise_error(StandardError) }
end
+
+ context 'gitlab pages' do
+ it 'moves pages folder to new location' do
+ expect_any_instance_of(Gitlab::PagesTransfer).to receive(:rename_project)
+
+ project.rename_repo
+ end
+ end
+
+ context 'attachments' do
+ it 'keeps uploads folder location unchanged' do
+ expect_any_instance_of(Gitlab::UploadsTransfer).not_to receive(:rename_project)
+
+ project.rename_repo
+ end
+
+ context 'when not rolled out' do
+ let(:project) { create(:project, :repository, storage_version: 1) }
+
+ it 'moves pages folder to new location' do
+ expect_any_instance_of(Gitlab::UploadsTransfer).to receive(:rename_project)
+
+ project.rename_repo
+ end
+ end
+ end
end
describe '#pages_path' do
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index f10d9383ae2..3d46434fc27 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -117,65 +117,74 @@ describe ProjectWiki do
end
describe "#find_page" do
- before do
- create_page("index page", "This is an awesome Gollum Wiki")
- end
+ shared_examples 'finding a wiki page' do
+ before do
+ create_page("index page", "This is an awesome Gollum Wiki")
+ end
- after do
- destroy_page(subject.pages.first.page)
- end
+ after do
+ destroy_page(subject.pages.first.page)
+ end
- it "returns the latest version of the page if it exists" do
- page = subject.find_page("index page")
- expect(page.title).to eq("index page")
- end
+ it "returns the latest version of the page if it exists" do
+ page = subject.find_page("index page")
+ expect(page.title).to eq("index page")
+ end
- it "returns nil if the page does not exist" do
- expect(subject.find_page("non-existant")).to eq(nil)
+ it "returns nil if the page does not exist" do
+ expect(subject.find_page("non-existant")).to eq(nil)
+ end
+
+ it "can find a page by slug" do
+ page = subject.find_page("index-page")
+ expect(page.title).to eq("index page")
+ end
+
+ it "returns a WikiPage instance" do
+ page = subject.find_page("index page")
+ expect(page).to be_a WikiPage
+ end
end
- it "can find a page by slug" do
- page = subject.find_page("index-page")
- expect(page.title).to eq("index page")
+ context 'when Gitaly wiki_find_page is enabled' do
+ it_behaves_like 'finding a wiki page'
end
- it "returns a WikiPage instance" do
- page = subject.find_page("index page")
- expect(page).to be_a WikiPage
+ context 'when Gitaly wiki_find_page is disabled', :skip_gitaly_mock do
+ it_behaves_like 'finding a wiki page'
end
end
describe '#find_file' do
- before do
- file = Gollum::File.new(subject.wiki)
- allow_any_instance_of(Gollum::Wiki)
- .to receive(:file).with('image.jpg', 'master')
- .and_return(file)
- allow_any_instance_of(Gollum::File)
- .to receive(:mime_type)
- .and_return('image/jpeg')
- allow_any_instance_of(Gollum::Wiki)
- .to receive(:file).with('non-existant', 'master')
- .and_return(nil)
- end
+ shared_examples 'finding a wiki file' do
+ before do
+ file = File.open(Rails.root.join('spec', 'fixtures', 'dk.png'))
+ subject.wiki # Make sure the wiki repo exists
- after do
- allow_any_instance_of(Gollum::Wiki).to receive(:file).and_call_original
- allow_any_instance_of(Gollum::File).to receive(:mime_type).and_call_original
- end
+ BareRepoOperations.new(subject.repository.path_to_repo).commit_file(file, 'image.png')
+ end
+
+ it 'returns the latest version of the file if it exists' do
+ file = subject.find_file('image.png')
+ expect(file.mime_type).to eq('image/png')
+ end
+
+ it 'returns nil if the page does not exist' do
+ expect(subject.find_file('non-existant')).to eq(nil)
+ end
- it 'returns the latest version of the file if it exists' do
- file = subject.find_file('image.jpg')
- expect(file.mime_type).to eq('image/jpeg')
+ it 'returns a Gitlab::Git::WikiFile instance' do
+ file = subject.find_file('image.png')
+ expect(file).to be_a Gitlab::Git::WikiFile
+ end
end
- it 'returns nil if the page does not exist' do
- expect(subject.find_file('non-existant')).to eq(nil)
+ context 'when Gitaly wiki_find_file is enabled' do
+ it_behaves_like 'finding a wiki file'
end
- it 'returns a Gitlab::Git::WikiFile instance' do
- file = subject.find_file('image.jpg')
- expect(file).to be_a Gitlab::Git::WikiFile
+ context 'when Gitaly wiki_find_file is disabled', :skip_gitaly_mock do
+ it_behaves_like 'finding a wiki file'
end
end
@@ -265,23 +274,33 @@ describe ProjectWiki do
end
describe "#delete_page" do
- before do
- create_page("index", "some content")
- @page = subject.wiki.page(title: "index")
- end
+ shared_examples 'deleting a wiki page' do
+ before do
+ create_page("index", "some content")
+ @page = subject.wiki.page(title: "index")
+ end
- it "deletes the page" do
- subject.delete_page(@page)
- expect(subject.pages.count).to eq(0)
- end
+ it "deletes the page" do
+ subject.delete_page(@page)
+ expect(subject.pages.count).to eq(0)
+ end
- it 'updates project activity' do
- subject.delete_page(@page)
+ it 'updates project activity' do
+ subject.delete_page(@page)
- project.reload
+ project.reload
- expect(project.last_activity_at).to be_within(1.minute).of(Time.now)
- expect(project.last_repository_updated_at).to be_within(1.minute).of(Time.now)
+ expect(project.last_activity_at).to be_within(1.minute).of(Time.now)
+ expect(project.last_repository_updated_at).to be_within(1.minute).of(Time.now)
+ end
+ end
+
+ context 'when Gitaly wiki_delete_page is enabled' do
+ it_behaves_like 'deleting a wiki page'
+ end
+
+ context 'when Gitaly wiki_delete_page is disabled', :skip_gitaly_mock do
+ it_behaves_like 'deleting a wiki page'
end
end
@@ -343,6 +362,6 @@ describe ProjectWiki do
end
def destroy_page(page)
- subject.delete_page(page, commit_details)
+ subject.delete_page(page, "test commit")
end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 874368ada67..8a6aa767ce6 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -299,6 +299,24 @@ describe Repository do
it { is_expected.to be_falsey }
end
+
+ context 'when pre-loaded merged branches are provided' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:branch, :pre_loaded, :expected) do
+ 'not-merged-branch' | ['branch-merged'] | false
+ 'branch-merged' | ['not-merged-branch'] | false
+ 'branch-merged' | ['branch-merged'] | true
+ 'not-merged-branch' | ['not-merged-branch'] | false
+ 'master' | ['master'] | false
+ end
+
+ with_them do
+ subject { repository.merged_to_root_ref?(branch, pre_loaded) }
+
+ it { is_expected.to eq(expected) }
+ end
+ end
end
describe '#can_be_merged?' do
@@ -2260,4 +2278,44 @@ describe Repository do
end
end
end
+
+ describe 'commit cache' do
+ set(:project) { create(:project, :repository) }
+
+ it 'caches based on SHA' do
+ # Gets the commit oid, and warms the cache
+ oid = project.commit.id
+
+ expect(Gitlab::Git::Commit).not_to receive(:find).once
+
+ project.commit_by(oid: oid)
+ end
+
+ it 'caches nil values' do
+ expect(Gitlab::Git::Commit).to receive(:find).once
+
+ project.commit_by(oid: '1' * 40)
+ project.commit_by(oid: '1' * 40)
+ end
+ end
+
+ describe '#raw_repository' do
+ subject { repository.raw_repository }
+
+ it 'returns a Gitlab::Git::Repository representation of the repository' do
+ expect(subject).to be_a(Gitlab::Git::Repository)
+ expect(subject.relative_path).to eq(project.disk_path + '.git')
+ expect(subject.gl_repository).to eq("project-#{project.id}")
+ end
+
+ context 'with a wiki repository' do
+ let(:repository) { project.wiki.repository }
+
+ it 'creates a Gitlab::Git::Repository with the proper attributes' do
+ expect(subject).to be_a(Gitlab::Git::Repository)
+ expect(subject.relative_path).to eq(project.disk_path + '.wiki.git')
+ expect(subject.gl_repository).to eq("wiki-#{project.id}")
+ end
+ end
+ end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 1c3c9068f12..e0896d64c8f 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -346,7 +346,6 @@ describe User do
describe "Respond to" do
it { is_expected.to respond_to(:admin?) }
it { is_expected.to respond_to(:name) }
- it { is_expected.to respond_to(:private_token) }
it { is_expected.to respond_to(:external?) }
end
@@ -526,14 +525,6 @@ describe User do
end
end
- describe 'authentication token' do
- it "has authentication token" do
- user = create(:user)
-
- expect(user.authentication_token).not_to be_blank
- end
- end
-
describe 'ensure incoming email token' do
it 'has incoming email token' do
user = create(:user)
@@ -2226,6 +2217,42 @@ describe User do
end
end
+ describe '#username_changed_hook' do
+ context 'for a new user' do
+ let(:user) { build(:user) }
+
+ it 'does not trigger system hook' do
+ expect(user).not_to receive(:system_hook_service)
+
+ user.save!
+ end
+ end
+
+ context 'for an existing user' do
+ let(:user) { create(:user, username: 'old-username') }
+
+ context 'when the username is changed' do
+ let(:new_username) { 'very-new-name' }
+
+ it 'triggers the rename system hook' do
+ system_hook_service = SystemHooksService.new
+ expect(system_hook_service).to receive(:execute_hooks_for).with(user, :rename)
+ expect(user).to receive(:system_hook_service).and_return(system_hook_service)
+
+ user.update_attributes!(username: new_username)
+ end
+ end
+
+ context 'when the username is not changed' do
+ it 'does not trigger system hook' do
+ expect(user).not_to receive(:system_hook_service)
+
+ user.update_attributes!(email: 'asdf@asdf.com')
+ end
+ end
+ end
+ end
+
describe '#sync_attribute?' do
let(:user) { described_class.new }
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index 1f14d06997e..a7227b38850 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -402,7 +402,7 @@ describe WikiPage do
def destroy_page(title)
page = wiki.wiki.page(title: title)
- wiki.delete_page(page, commit_details)
+ wiki.delete_page(page, "test commit")
end
def get_slugs(page_or_dir)
diff --git a/spec/policies/gcp/cluster_policy_spec.rb b/spec/policies/clusters/cluster_policy_spec.rb
index e213aa3d557..4207f42b07f 100644
--- a/spec/policies/gcp/cluster_policy_spec.rb
+++ b/spec/policies/clusters/cluster_policy_spec.rb
@@ -1,8 +1,8 @@
require 'spec_helper'
-describe Gcp::ClusterPolicy, :models do
- set(:project) { create(:project) }
- set(:cluster) { create(:gcp_cluster, project: project) }
+describe Clusters::ClusterPolicy, :models do
+ let(:cluster) { create(:cluster, :project) }
+ let(:project) { cluster.project }
let(:user) { create(:user) }
let(:policy) { described_class.new(user, cluster) }
diff --git a/spec/presenters/gcp/cluster_presenter_spec.rb b/spec/presenters/clusters/cluster_presenter_spec.rb
index 8d86dc31582..48d4f3671c5 100644
--- a/spec/presenters/gcp/cluster_presenter_spec.rb
+++ b/spec/presenters/clusters/cluster_presenter_spec.rb
@@ -1,8 +1,7 @@
require 'spec_helper'
-describe Gcp::ClusterPresenter do
- let(:project) { create(:project) }
- let(:cluster) { create(:gcp_cluster, project: project) }
+describe Clusters::ClusterPresenter do
+ let(:cluster) { create(:cluster, :provided_by_gcp) }
subject(:presenter) do
described_class.new(cluster)
@@ -22,14 +21,14 @@ describe Gcp::ClusterPresenter do
end
it 'forwards missing methods to cluster' do
- expect(presenter.gcp_cluster_zone).to eq(cluster.gcp_cluster_zone)
+ expect(presenter.status).to eq(cluster.status)
end
end
describe '#gke_cluster_url' do
subject { described_class.new(cluster).gke_cluster_url }
- it { is_expected.to include(cluster.gcp_cluster_zone) }
- it { is_expected.to include(cluster.gcp_cluster_name) }
+ it { is_expected.to include(cluster.provider.zone) }
+ it { is_expected.to include(cluster.name) }
end
end
diff --git a/spec/requests/api/doorkeeper_access_spec.rb b/spec/requests/api/doorkeeper_access_spec.rb
index de7ce848a31..308134eba72 100644
--- a/spec/requests/api/doorkeeper_access_spec.rb
+++ b/spec/requests/api/doorkeeper_access_spec.rb
@@ -25,7 +25,7 @@ describe 'doorkeeper access' do
end
end
- describe "authorization by private token" do
+ describe "authorization by OAuth token" do
it "returns authentication success" do
get api("/user", user)
expect(response).to have_gitlab_http_status(200)
@@ -39,20 +39,20 @@ describe 'doorkeeper access' do
end
describe "when user is blocked" do
- it "returns authentication error" do
+ it "returns authorization error" do
user.block
get api("/user"), access_token: token.token
- expect(response).to have_gitlab_http_status(401)
+ expect(response).to have_gitlab_http_status(403)
end
end
describe "when user is ldap_blocked" do
- it "returns authentication error" do
+ it "returns authorization error" do
user.ldap_block
get api("/user"), access_token: token.token
- expect(response).to have_gitlab_http_status(401)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index 9f3b5a809d7..6c0996c543d 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -28,39 +28,11 @@ describe API::Helpers do
allow_any_instance_of(self.class).to receive(:options).and_return({})
end
- def set_env(user_or_token, identifier)
- clear_env
- clear_param
- env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token
- env[API::Helpers::SUDO_HEADER] = identifier.to_s
- end
-
- def set_param(user_or_token, identifier)
- clear_env
- clear_param
- params[API::APIGuard::PRIVATE_TOKEN_PARAM] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token
- params[API::Helpers::SUDO_PARAM] = identifier.to_s
- end
-
- def clear_env
- env.delete(API::APIGuard::PRIVATE_TOKEN_HEADER)
- env.delete(API::Helpers::SUDO_HEADER)
- end
-
- def clear_param
- params.delete(API::APIGuard::PRIVATE_TOKEN_PARAM)
- params.delete(API::Helpers::SUDO_PARAM)
- end
-
def warden_authenticate_returns(value)
warden = double("warden", authenticate: value)
env['warden'] = warden
end
- def doorkeeper_guard_returns(value)
- allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { value }
- end
-
def error!(message, status, header)
raise Exception.new("#{status} - #{message}")
end
@@ -69,10 +41,6 @@ describe API::Helpers do
subject { current_user }
describe "Warden authentication", :allow_forgery_protection do
- before do
- doorkeeper_guard_returns false
- end
-
context "with invalid credentials" do
context "GET request" do
before do
@@ -160,75 +128,32 @@ describe API::Helpers do
end
end
- describe "when authenticating using a user's private token" do
- it "returns a 401 response for an invalid token" do
- env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token'
- allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { false }
-
- expect { current_user }.to raise_error /401/
- end
-
- it "returns a 401 response for a user without access" do
- env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token
- allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
-
- expect { current_user }.to raise_error /401/
- end
-
- it 'returns a 401 response for a user who is blocked' do
- user.block!
- env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token
-
- expect { current_user }.to raise_error /401/
- end
-
- it "leaves user as is when sudo not specified" do
- env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token
-
- expect(current_user).to eq(user)
-
- clear_env
-
- params[API::APIGuard::PRIVATE_TOKEN_PARAM] = user.private_token
-
- expect(current_user).to eq(user)
- end
- end
-
describe "when authenticating using a user's personal access tokens" do
let(:personal_access_token) { create(:personal_access_token, user: user) }
- before do
- allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { false }
- end
-
it "returns a 401 response for an invalid token" do
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token'
expect { current_user }.to raise_error /401/
end
- it "returns a 401 response for a user without access" do
+ it "returns a 403 response for a user without access" do
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
- expect { current_user }.to raise_error /401/
+ expect { current_user }.to raise_error /403/
end
- it 'returns a 401 response for a user who is blocked' do
+ it 'returns a 403 response for a user who is blocked' do
user.block!
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
- expect { current_user }.to raise_error /401/
+ expect { current_user }.to raise_error /403/
end
- it "leaves user as is when sudo not specified" do
+ it "sets current_user" do
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect(current_user).to eq(user)
- clear_env
- params[API::APIGuard::PRIVATE_TOKEN_PARAM] = personal_access_token.token
-
- expect(current_user).to eq(user)
end
it "does not allow tokens without the appropriate scope" do
@@ -252,210 +177,6 @@ describe API::Helpers do
expect { current_user }.to raise_error API::APIGuard::ExpiredError
end
end
-
- context 'sudo usage' do
- context 'with admin' do
- context 'with header' do
- context 'with id' do
- it 'changes current_user to sudo' do
- set_env(admin, user.id)
-
- expect(current_user).to eq(user)
- end
-
- it 'memoize the current_user: sudo permissions are not run against the sudoed user' do
- set_env(admin, user.id)
-
- expect(current_user).to eq(user)
- expect(current_user).to eq(user)
- end
-
- it 'handles sudo to oneself' do
- set_env(admin, admin.id)
-
- expect(current_user).to eq(admin)
- end
-
- it 'throws an error when user cannot be found' do
- id = user.id + admin.id
- expect(user.id).not_to eq(id)
- expect(admin.id).not_to eq(id)
-
- set_env(admin, id)
-
- expect { current_user }.to raise_error(Exception)
- end
- end
-
- context 'with username' do
- it 'changes current_user to sudo' do
- set_env(admin, user.username)
-
- expect(current_user).to eq(user)
- end
-
- it 'handles sudo to oneself' do
- set_env(admin, admin.username)
-
- expect(current_user).to eq(admin)
- end
-
- it "throws an error when the user cannot be found for a given username" do
- username = "#{user.username}#{admin.username}"
- expect(user.username).not_to eq(username)
- expect(admin.username).not_to eq(username)
-
- set_env(admin, username)
-
- expect { current_user }.to raise_error(Exception)
- end
- end
- end
-
- context 'with param' do
- context 'with id' do
- it 'changes current_user to sudo' do
- set_param(admin, user.id)
-
- expect(current_user).to eq(user)
- end
-
- it 'handles sudo to oneself' do
- set_param(admin, admin.id)
-
- expect(current_user).to eq(admin)
- end
-
- it 'handles sudo to oneself using string' do
- set_env(admin, user.id.to_s)
-
- expect(current_user).to eq(user)
- end
-
- it 'throws an error when user cannot be found' do
- id = user.id + admin.id
- expect(user.id).not_to eq(id)
- expect(admin.id).not_to eq(id)
-
- set_param(admin, id)
-
- expect { current_user }.to raise_error(Exception)
- end
- end
-
- context 'with username' do
- it 'changes current_user to sudo' do
- set_param(admin, user.username)
-
- expect(current_user).to eq(user)
- end
-
- it 'handles sudo to oneself' do
- set_param(admin, admin.username)
-
- expect(current_user).to eq(admin)
- end
-
- it "throws an error when the user cannot be found for a given username" do
- username = "#{user.username}#{admin.username}"
- expect(user.username).not_to eq(username)
- expect(admin.username).not_to eq(username)
-
- set_param(admin, username)
-
- expect { current_user }.to raise_error(Exception)
- end
- end
- end
-
- context 'when user is blocked' do
- before do
- user.block!
- end
-
- it 'changes current_user to sudo' do
- set_env(admin, user.id)
-
- expect(current_user).to eq(user)
- end
- end
- end
-
- context 'with regular user' do
- context 'with env' do
- it 'changes current_user to sudo when admin and user id' do
- set_env(user, admin.id)
-
- expect { current_user }.to raise_error(Exception)
- end
-
- it 'changes current_user to sudo when admin and user username' do
- set_env(user, admin.username)
-
- expect { current_user }.to raise_error(Exception)
- end
- end
-
- context 'with params' do
- it 'changes current_user to sudo when admin and user id' do
- set_param(user, admin.id)
-
- expect { current_user }.to raise_error(Exception)
- end
-
- it 'changes current_user to sudo when admin and user username' do
- set_param(user, admin.username)
-
- expect { current_user }.to raise_error(Exception)
- end
- end
- end
- end
- end
-
- describe '.sudo?' do
- context 'when no sudo env or param is passed' do
- before do
- doorkeeper_guard_returns(nil)
- end
-
- it 'returns false' do
- expect(sudo?).to be_falsy
- end
- end
-
- context 'when sudo env or param is passed', 'user is not an admin' do
- before do
- set_env(user, '123')
- end
-
- it 'returns an 403 Forbidden' do
- expect { sudo? }.to raise_error '403 - {"message"=>"403 Forbidden - Must be admin to use sudo"}'
- end
- end
-
- context 'when sudo env or param is passed', 'user is admin' do
- context 'personal access token is used' do
- before do
- personal_access_token = create(:personal_access_token, user: admin)
- set_env(personal_access_token.token, user.id)
- end
-
- it 'returns an 403 Forbidden' do
- expect { sudo? }.to raise_error '403 - {"message"=>"403 Forbidden - Private token must be specified in order to use sudo"}'
- end
- end
-
- context 'private access token is used' do
- before do
- set_env(admin.private_token, user.id)
- end
-
- it 'returns true' do
- expect(sudo?).to be_truthy
- end
- end
- end
end
describe '.handle_api_exception' do
@@ -582,4 +303,147 @@ describe API::Helpers do
end
end
end
+
+ context 'sudo' do
+ shared_examples 'successful sudo' do
+ it 'sets current_user' do
+ expect(current_user).to eq(user)
+ end
+
+ it 'sets sudo?' do
+ expect(sudo?).to be_truthy
+ end
+ end
+
+ shared_examples 'sudo' do
+ context 'when admin' do
+ before do
+ token.user = admin
+ token.save!
+ end
+
+ context 'when token has sudo scope' do
+ before do
+ token.scopes = %w[sudo]
+ token.save!
+ end
+
+ context 'when user exists' do
+ context 'when using header' do
+ context 'when providing username' do
+ before do
+ env[API::Helpers::SUDO_HEADER] = user.username
+ end
+
+ it_behaves_like 'successful sudo'
+ end
+
+ context 'when providing user ID' do
+ before do
+ env[API::Helpers::SUDO_HEADER] = user.id.to_s
+ end
+
+ it_behaves_like 'successful sudo'
+ end
+ end
+
+ context 'when using param' do
+ context 'when providing username' do
+ before do
+ params[API::Helpers::SUDO_PARAM] = user.username
+ end
+
+ it_behaves_like 'successful sudo'
+ end
+
+ context 'when providing user ID' do
+ before do
+ params[API::Helpers::SUDO_PARAM] = user.id.to_s
+ end
+
+ it_behaves_like 'successful sudo'
+ end
+ end
+ end
+
+ context 'when user does not exist' do
+ before do
+ params[API::Helpers::SUDO_PARAM] = 'nonexistent'
+ end
+
+ it 'raises an error' do
+ expect { current_user }.to raise_error /User with ID or username 'nonexistent' Not Found/
+ end
+ end
+ end
+
+ context 'when token does not have sudo scope' do
+ before do
+ token.scopes = %w[api]
+ token.save!
+
+ params[API::Helpers::SUDO_PARAM] = user.id.to_s
+ end
+
+ it 'raises an error' do
+ expect { current_user }.to raise_error API::APIGuard::InsufficientScopeError
+ end
+ end
+ end
+
+ context 'when not admin' do
+ before do
+ token.user = user
+ token.save!
+
+ params[API::Helpers::SUDO_PARAM] = user.id.to_s
+ end
+
+ it 'raises an error' do
+ expect { current_user }.to raise_error /Must be admin to use sudo/
+ end
+ end
+ end
+
+ context 'using an OAuth token' do
+ let(:token) { create(:oauth_access_token) }
+
+ before do
+ env['HTTP_AUTHORIZATION'] = "Bearer #{token.token}"
+ end
+
+ it_behaves_like 'sudo'
+ end
+
+ context 'using a personal access token' do
+ let(:token) { create(:personal_access_token) }
+
+ context 'passed as param' do
+ before do
+ params[API::APIGuard::PRIVATE_TOKEN_PARAM] = token.token
+ end
+
+ it_behaves_like 'sudo'
+ end
+
+ context 'passed as header' do
+ before do
+ env[API::APIGuard::PRIVATE_TOKEN_HEADER] = token.token
+ end
+
+ it_behaves_like 'sudo'
+ end
+ end
+
+ context 'using warden authentication' do
+ before do
+ warden_authenticate_returns admin
+ env[API::Helpers::SUDO_HEADER] = user.username
+ end
+
+ it 'raises an error' do
+ expect { current_user }.to raise_error /Must be authenticated using an OAuth or Personal Access Token to use sudo/
+ end
+ end
+ end
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 28b1404a4f7..024cfe8b372 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -1061,6 +1061,30 @@ describe API::MergeRequests do
end
end
+ describe 'POST :id/merge_requests/:merge_request_iid/cancel_merge_when_pipeline_succeeds' do
+ before do
+ ::MergeRequests::MergeWhenPipelineSucceedsService.new(merge_request.target_project, user).execute(merge_request)
+ end
+
+ it 'removes the merge_when_pipeline_succeeds status' do
+ post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/cancel_merge_when_pipeline_succeeds", user)
+
+ expect(response).to have_gitlab_http_status(201)
+ end
+
+ it 'returns 404 if the merge request is not found' do
+ post api("/projects/#{project.id}/merge_requests/123/merge_when_pipeline_succeeds", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns 404 if the merge request id is used instead of iid' do
+ post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge_when_pipeline_succeeds", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
describe 'Time tracking' do
let(:issuable) { merge_request }
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index 5fd76fce7df..47f4ccd4887 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -385,7 +385,7 @@ describe API::Runner do
end
context 'when job is made for tag' do
- let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
+ let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
it 'sets branch as ref_type' do
request_job
@@ -436,8 +436,8 @@ describe API::Runner do
end
context 'when project and pipeline have multiple jobs' do
- let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
- let!(:job2) { create(:ci_build_tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
+ let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
+ let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) }
before do
@@ -458,7 +458,7 @@ describe API::Runner do
end
context 'when pipeline have jobs with artifacts' do
- let!(:job) { create(:ci_build_tag, :artifacts, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
+ let!(:job) { create(:ci_build, :tag, :artifacts, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) }
before do
@@ -478,8 +478,8 @@ describe API::Runner do
end
context 'when explicit dependencies are defined' do
- let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
- let!(:job2) { create(:ci_build_tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
+ let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
+ let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
let!(:test_job) do
create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'deploy',
stage: 'deploy', stage_idx: 1,
@@ -502,8 +502,8 @@ describe API::Runner do
end
context 'when dependencies is an empty array' do
- let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
- let!(:job2) { create(:ci_build_tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
+ let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
+ let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
let!(:empty_dependencies_job) do
create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'empty_dependencies_job',
stage: 'deploy', stage_idx: 1,
diff --git a/spec/requests/api/session_spec.rb b/spec/requests/api/session_spec.rb
deleted file mode 100644
index 83d09878813..00000000000
--- a/spec/requests/api/session_spec.rb
+++ /dev/null
@@ -1,107 +0,0 @@
-require 'spec_helper'
-
-describe API::Session do
- let(:user) { create(:user) }
-
- describe "POST /session" do
- context "when valid password" do
- it "returns private token" do
- post api("/session"), email: user.email, password: '12345678'
- expect(response).to have_gitlab_http_status(201)
-
- expect(json_response['email']).to eq(user.email)
- expect(json_response['private_token']).to eq(user.private_token)
- expect(json_response['is_admin']).to eq(user.admin?)
- expect(json_response['can_create_project']).to eq(user.can_create_project?)
- expect(json_response['can_create_group']).to eq(user.can_create_group?)
- end
-
- context 'with 2FA enabled' do
- it 'rejects sign in attempts' do
- user = create(:user, :two_factor)
-
- post api('/session'), email: user.email, password: user.password
-
- expect(response).to have_gitlab_http_status(401)
- expect(response.body).to include('You have 2FA enabled.')
- end
- end
- end
-
- context 'when email has case-typo and password is valid' do
- it 'returns private token' do
- post api('/session'), email: user.email.upcase, password: '12345678'
- expect(response.status).to eq 201
-
- expect(json_response['email']).to eq user.email
- expect(json_response['private_token']).to eq user.private_token
- expect(json_response['is_admin']).to eq user.admin?
- expect(json_response['can_create_project']).to eq user.can_create_project?
- expect(json_response['can_create_group']).to eq user.can_create_group?
- end
- end
-
- context 'when login has case-typo and password is valid' do
- it 'returns private token' do
- post api('/session'), login: user.username.upcase, password: '12345678'
- expect(response.status).to eq 201
-
- expect(json_response['email']).to eq user.email
- expect(json_response['private_token']).to eq user.private_token
- expect(json_response['is_admin']).to eq user.admin?
- expect(json_response['can_create_project']).to eq user.can_create_project?
- expect(json_response['can_create_group']).to eq user.can_create_group?
- end
- end
-
- context "when invalid password" do
- it "returns authentication error" do
- post api("/session"), email: user.email, password: '123'
- expect(response).to have_gitlab_http_status(401)
-
- expect(json_response['email']).to be_nil
- expect(json_response['private_token']).to be_nil
- end
- end
-
- context "when empty password" do
- it "returns authentication error with email" do
- post api("/session"), email: user.email
-
- expect(response).to have_gitlab_http_status(400)
- end
-
- it "returns authentication error with username" do
- post api("/session"), email: user.username
-
- expect(response).to have_gitlab_http_status(400)
- end
- end
-
- context "when empty name" do
- it "returns authentication error" do
- post api("/session"), password: user.password
-
- expect(response).to have_gitlab_http_status(400)
- end
- end
-
- context "when user is blocked" do
- it "returns authentication error" do
- user.block
- post api("/session"), email: user.username, password: user.password
-
- expect(response).to have_gitlab_http_status(401)
- end
- end
-
- context "when user is ldap_blocked" do
- it "returns authentication error" do
- user.ldap_block
- post api("/session"), email: user.username, password: user.password
-
- expect(response).to have_gitlab_http_status(401)
- end
- end
- end
-end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 4737f034f21..634c8dae0ba 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -127,8 +127,8 @@ describe API::Users do
context "when admin" do
context 'when sudo is defined' do
it 'does not return 500' do
- admin_personal_access_token = create(:personal_access_token, user: admin).token
- get api("/users?private_token=#{admin_personal_access_token}&sudo=#{user.id}", admin)
+ admin_personal_access_token = create(:personal_access_token, user: admin, scopes: [:sudo])
+ get api("/users?sudo=#{user.id}", admin, personal_access_token: admin_personal_access_token)
expect(response).to have_gitlab_http_status(:success)
end
@@ -1097,14 +1097,6 @@ describe API::Users do
end
end
- context 'with private token' do
- it 'returns 403 without private token when sudo defined' do
- get api("/user?private_token=#{user.private_token}&sudo=123")
-
- expect(response).to have_gitlab_http_status(403)
- end
- end
-
it 'returns current user without private token when sudo not defined' do
get api("/user", user)
@@ -1139,24 +1131,6 @@ describe API::Users do
expect(json_response['id']).to eq(admin.id)
end
end
-
- context 'with private token' do
- it 'returns sudoed user with private token when sudo defined' do
- get api("/user?private_token=#{admin.private_token}&sudo=#{user.id}")
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to match_response_schema('public_api/v4/user/login')
- expect(json_response['id']).to eq(user.id)
- end
-
- it 'returns initial current user without private token but with is_admin when sudo not defined' do
- get api("/user?private_token=#{admin.private_token}")
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to match_response_schema('public_api/v4/user/admin')
- expect(json_response['id']).to eq(admin.id)
- end
- end
end
context 'with unauthenticated user' do
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index 39d44245c3f..fb1281a6b42 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -426,18 +426,23 @@ describe 'project routing' do
end
end
- # project_milestones GET /:project_id/milestones(.:format) milestones#index
- # POST /:project_id/milestones(.:format) milestones#create
- # new_project_milestone GET /:project_id/milestones/new(.:format) milestones#new
- # edit_project_milestone GET /:project_id/milestones/:id/edit(.:format) milestones#edit
- # project_milestone GET /:project_id/milestones/:id(.:format) milestones#show
- # PUT /:project_id/milestones/:id(.:format) milestones#update
- # DELETE /:project_id/milestones/:id(.:format) milestones#destroy
+ # project_milestones GET /:project_id/milestones(.:format) milestones#index
+ # POST /:project_id/milestones(.:format) milestones#create
+ # new_project_milestone GET /:project_id/milestones/new(.:format) milestones#new
+ # edit_project_milestone GET /:project_id/milestones/:id/edit(.:format) milestones#edit
+ # project_milestone GET /:project_id/milestones/:id(.:format) milestones#show
+ # PUT /:project_id/milestones/:id(.:format) milestones#update
+ # DELETE /:project_id/milestones/:id(.:format) milestones#destroy
+ # promote_project_milestone POST /:project_id/milestones/:id/promote milestones#promote
describe Projects::MilestonesController, 'routing' do
it_behaves_like 'RESTful project resources' do
let(:controller) { 'milestones' }
let(:actions) { [:index, :create, :new, :edit, :show, :update] }
end
+
+ it 'to #promote' do
+ expect(post('/gitlab/gitlabhq/milestones/1/promote')).to route_to('projects/milestones#promote', namespace_id: 'gitlab', project_id: 'gitlabhq', id: "1")
+ end
end
# project_labels GET /:project_id/labels(.:format) labels#index
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index 407d19c3b2a..609481603af 100644
--- a/spec/routing/routing_spec.rb
+++ b/spec/routing/routing_spec.rb
@@ -135,7 +135,6 @@ end
# profile_history GET /profile/history(.:format) profile#history
# profile_password PUT /profile/password(.:format) profile#password_update
# profile_token GET /profile/token(.:format) profile#token
-# profile_reset_private_token PUT /profile/reset_private_token(.:format) profile#reset_private_token
# profile GET /profile(.:format) profile#show
# profile_update PUT /profile/update(.:format) profile#update
describe ProfilesController, "routing" do
@@ -147,10 +146,6 @@ describe ProfilesController, "routing" do
expect(get("/profile/audit_log")).to route_to('profiles#audit_log')
end
- it "to #reset_private_token" do
- expect(put("/profile/reset_private_token")).to route_to('profiles#reset_private_token')
- end
-
it "to #reset_rss_token" do
expect(put("/profile/reset_rss_token")).to route_to('profiles#reset_rss_token')
end
diff --git a/spec/serializers/cluster_entity_spec.rb b/spec/serializers/cluster_entity_spec.rb
index 2c7f49974f1..7b132a1b84d 100644
--- a/spec/serializers/cluster_entity_spec.rb
+++ b/spec/serializers/cluster_entity_spec.rb
@@ -1,22 +1,38 @@
require 'spec_helper'
describe ClusterEntity do
- set(:cluster) { create(:gcp_cluster, :errored) }
- let(:request) { double('request') }
+ describe '#as_json' do
+ subject { described_class.new(cluster).as_json }
- let(:entity) do
- described_class.new(cluster)
- end
+ context 'when provider type is gcp' do
+ let(:cluster) { create(:cluster, provider_type: :gcp, provider_gcp: provider) }
- describe '#as_json' do
- subject { entity.as_json }
+ context 'when status is creating' do
+ let(:provider) { create(:provider_gcp, :creating) }
- it 'contains status' do
- expect(subject[:status]).to eq(:errored)
+ it 'has corresponded data' do
+ expect(subject[:status]).to eq(:creating)
+ expect(subject[:status_reason]).to be_nil
+ end
+ end
+
+ context 'when status is errored' do
+ let(:provider) { create(:provider_gcp, :errored) }
+
+ it 'has corresponded data' do
+ expect(subject[:status]).to eq(:errored)
+ expect(subject[:status_reason]).to eq(provider.status_reason)
+ end
+ end
end
- it 'contains status reason' do
- expect(subject[:status_reason]).to eq('general error')
+ context 'when provider type is user' do
+ let(:cluster) { create(:cluster, provider_type: :user) }
+
+ it 'has nil' do
+ expect(subject[:status]).to be_nil
+ expect(subject[:status_reason]).to be_nil
+ end
end
end
end
diff --git a/spec/serializers/cluster_serializer_spec.rb b/spec/serializers/cluster_serializer_spec.rb
index 1ac6784d28f..e5da92a451e 100644
--- a/spec/serializers/cluster_serializer_spec.rb
+++ b/spec/serializers/cluster_serializer_spec.rb
@@ -1,15 +1,20 @@
require 'spec_helper'
describe ClusterSerializer do
- let(:serializer) do
- described_class.new
- end
-
describe '#represent_status' do
- subject { serializer.represent_status(resource) }
+ subject { described_class.new.represent_status(cluster) }
+
+ context 'when provider type is gcp' do
+ let(:cluster) { create(:cluster, provider_type: :gcp, provider_gcp: provider) }
+ let(:provider) { create(:provider_gcp, :errored) }
+
+ it 'serializes only status' do
+ expect(subject.keys).to contain_exactly(:status, :status_reason)
+ end
+ end
- context 'when represents only status' do
- let(:resource) { create(:gcp_cluster, :errored) }
+ context 'when provider type is user' do
+ let(:cluster) { create(:cluster, provider_type: :user) }
it 'serializes only status' do
expect(subject.keys).to contain_exactly(:status, :status_reason)
diff --git a/spec/serializers/issue_entity_spec.rb b/spec/serializers/issue_entity_spec.rb
new file mode 100644
index 00000000000..caa3e41402b
--- /dev/null
+++ b/spec/serializers/issue_entity_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe IssueEntity do
+ let(:project) { create(:project) }
+ let(:resource) { create(:issue, project: project) }
+ let(:user) { create(:user) }
+
+ let(:request) { double('request', current_user: user) }
+
+ subject { described_class.new(resource, request: request).as_json }
+
+ it 'has Issuable attributes' do
+ expect(subject).to include(:id, :iid, :author_id, :description, :lock_version, :milestone_id,
+ :title, :updated_by_id, :created_at, :updated_at, :milestone, :labels)
+ end
+
+ it 'has time estimation attributes' do
+ expect(subject).to include(:time_estimate, :total_time_spent, :human_time_estimate, :human_total_time_spent)
+ end
+end
diff --git a/spec/serializers/issue_serializer_spec.rb b/spec/serializers/issue_serializer_spec.rb
new file mode 100644
index 00000000000..75578816e75
--- /dev/null
+++ b/spec/serializers/issue_serializer_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe IssueSerializer do
+ let(:resource) { create(:issue) }
+ let(:user) { create(:user) }
+ let(:json_entity) do
+ described_class.new(current_user: user)
+ .represent(resource, serializer: serializer)
+ .with_indifferent_access
+ end
+
+ context 'non-sidebar issue serialization' do
+ let(:serializer) { nil }
+
+ it 'matches issue json schema' do
+ expect(json_entity).to match_schema('entities/issue')
+ end
+ end
+
+ context 'sidebar issue serialization' do
+ let(:serializer) { 'sidebar' }
+
+ it 'matches sidebar issue json schema' do
+ expect(json_entity).to match_schema('entities/issue_sidebar')
+ end
+ end
+end
diff --git a/spec/serializers/merge_request_basic_serializer_spec.rb b/spec/serializers/merge_request_basic_serializer_spec.rb
index 4daf5a59d0c..1fad8e6bc5d 100644
--- a/spec/serializers/merge_request_basic_serializer_spec.rb
+++ b/spec/serializers/merge_request_basic_serializer_spec.rb
@@ -4,9 +4,13 @@ describe MergeRequestBasicSerializer do
let(:resource) { create(:merge_request) }
let(:user) { create(:user) }
- subject { described_class.new.represent(resource) }
+ let(:json_entity) do
+ described_class.new(current_user: user)
+ .represent(resource, serializer: 'basic')
+ .with_indifferent_access
+ end
- it 'has important MergeRequest attributes' do
- expect(subject).to include(:merge_status)
+ it 'matches basic merge request json' do
+ expect(json_entity).to match_schema('entities/merge_request_basic')
end
end
diff --git a/spec/serializers/merge_request_entity_spec.rb b/spec/serializers/merge_request_entity_spec.rb
index 87832b3dca1..f9285049c0d 100644
--- a/spec/serializers/merge_request_entity_spec.rb
+++ b/spec/serializers/merge_request_entity_spec.rb
@@ -30,8 +30,17 @@ describe MergeRequestEntity do
:assign_to_closing)
end
+ it 'has Issuable attributes' do
+ expect(subject).to include(:id, :iid, :author_id, :description, :lock_version, :milestone_id,
+ :title, :updated_by_id, :created_at, :updated_at, :milestone, :labels)
+ end
+
+ it 'has time estimation attributes' do
+ expect(subject).to include(:time_estimate, :total_time_spent, :human_time_estimate, :human_total_time_spent)
+ end
+
it 'has important MergeRequest attributes' do
- expect(subject).to include(:diff_head_sha, :merge_commit_message,
+ expect(subject).to include(:state, :deleted_at, :diff_head_sha, :merge_commit_message,
:has_conflicts, :has_ci, :merge_path,
:conflict_resolution_path,
:cancel_merge_when_pipeline_succeeds_path,
diff --git a/spec/serializers/merge_request_serializer_spec.rb b/spec/serializers/merge_request_serializer_spec.rb
index 73fbecc153d..e3abefa6d63 100644
--- a/spec/serializers/merge_request_serializer_spec.rb
+++ b/spec/serializers/merge_request_serializer_spec.rb
@@ -9,11 +9,11 @@ describe MergeRequestSerializer do
end
describe '#represent' do
- let(:opts) { { basic: basic } }
- subject { serializer.represent(merge_request, basic: basic) }
+ let(:opts) { { serializer: serializer_entity } }
+ subject { serializer.represent(merge_request, serializer: serializer_entity) }
- context 'when basic param is truthy' do
- let(:basic) { true }
+ context 'when passing basic serializer param' do
+ let(:serializer_entity) { 'basic' }
it 'calls super class #represent with correct params' do
expect_any_instance_of(BaseSerializer).to receive(:represent)
@@ -23,8 +23,8 @@ describe MergeRequestSerializer do
end
end
- context 'when basic param is falsy' do
- let(:basic) { false }
+ context 'when serializer param is falsy' do
+ let(:serializer_entity) { nil }
it 'calls super class #represent with correct params' do
expect_any_instance_of(BaseSerializer).to receive(:represent)
diff --git a/spec/services/applications/create_service_spec.rb b/spec/services/applications/create_service_spec.rb
new file mode 100644
index 00000000000..47a2a9d6403
--- /dev/null
+++ b/spec/services/applications/create_service_spec.rb
@@ -0,0 +1,13 @@
+require 'spec_helper'
+
+describe ::Applications::CreateService do
+ let(:user) { create(:user) }
+ let(:params) { attributes_for(:application) }
+ let(:request) { ActionController::TestRequest.new(remote_ip: '127.0.0.1') }
+
+ subject { described_class.new(user, params) }
+
+ it 'creates an application' do
+ expect { subject.execute(request) }.to change { Doorkeeper::Application.count }.by(1)
+ end
+end
diff --git a/spec/services/ci/create_cluster_service_spec.rb b/spec/services/ci/create_cluster_service_spec.rb
deleted file mode 100644
index 6e7398fbffa..00000000000
--- a/spec/services/ci/create_cluster_service_spec.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-require 'spec_helper'
-
-describe Ci::CreateClusterService do
- describe '#execute' do
- let(:access_token) { 'xxx' }
- let(:project) { create(:project) }
- let(:user) { create(:user) }
- let(:result) { described_class.new(project, user, params).execute(access_token) }
-
- context 'when correct params' do
- let(:params) do
- {
- gcp_project_id: 'gcp-project',
- gcp_cluster_name: 'test-cluster',
- gcp_cluster_zone: 'us-central1-a',
- gcp_cluster_size: 1
- }
- end
-
- it 'creates a cluster object' do
- expect(ClusterProvisionWorker).to receive(:perform_async)
- expect { result }.to change { Gcp::Cluster.count }.by(1)
- expect(result.gcp_project_id).to eq('gcp-project')
- expect(result.gcp_cluster_name).to eq('test-cluster')
- expect(result.gcp_cluster_zone).to eq('us-central1-a')
- expect(result.gcp_cluster_size).to eq(1)
- expect(result.gcp_token).to eq(access_token)
- end
- end
-
- context 'when invalid params' do
- let(:params) do
- {
- gcp_project_id: 'gcp-project',
- gcp_cluster_name: 'test-cluster',
- gcp_cluster_zone: 'us-central1-a',
- gcp_cluster_size: 'ABC'
- }
- end
-
- it 'returns an error' do
- expect(ClusterProvisionWorker).not_to receive(:perform_async)
- expect { result }.to change { Gcp::Cluster.count }.by(0)
- end
- end
- end
-end
diff --git a/spec/services/ci/fetch_gcp_operation_service_spec.rb b/spec/services/ci/fetch_gcp_operation_service_spec.rb
deleted file mode 100644
index 7792979c5cb..00000000000
--- a/spec/services/ci/fetch_gcp_operation_service_spec.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-require 'spec_helper'
-require 'google/apis'
-
-describe Ci::FetchGcpOperationService do
- describe '#execute' do
- let(:cluster) { create(:gcp_cluster) }
- let(:operation) { double }
-
- context 'when suceeded' do
- before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:projects_zones_operations).and_return(operation)
- end
-
- it 'fetch the gcp operaion' do
- expect { |b| described_class.new.execute(cluster, &b) }
- .to yield_with_args(operation)
- end
- end
-
- context 'when raises an error' do
- let(:error) { Google::Apis::ServerError.new('a') }
-
- before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:projects_zones_operations).and_raise(error)
- end
-
- it 'sets an error to cluster object' do
- expect { |b| described_class.new.execute(cluster, &b) }
- .not_to yield_with_args
- expect(cluster.reload).to be_errored
- end
- end
- end
-end
diff --git a/spec/services/ci/finalize_cluster_creation_service_spec.rb b/spec/services/ci/finalize_cluster_creation_service_spec.rb
deleted file mode 100644
index def3709fdb4..00000000000
--- a/spec/services/ci/finalize_cluster_creation_service_spec.rb
+++ /dev/null
@@ -1,61 +0,0 @@
-require 'spec_helper'
-
-describe Ci::FinalizeClusterCreationService do
- describe '#execute' do
- let(:cluster) { create(:gcp_cluster) }
- let(:result) { described_class.new.execute(cluster) }
-
- context 'when suceeded to get cluster from api' do
- let(:gke_cluster) { double }
-
- before do
- allow(gke_cluster).to receive(:endpoint).and_return('111.111.111.111')
- allow(gke_cluster).to receive(:master_auth).and_return(spy)
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:projects_zones_clusters_get).and_return(gke_cluster)
- end
-
- context 'when suceeded to get kubernetes token' do
- let(:kubernetes_token) { 'abc' }
-
- before do
- allow_any_instance_of(Ci::FetchKubernetesTokenService)
- .to receive(:execute).and_return(kubernetes_token)
- end
-
- it 'executes integration cluster' do
- expect_any_instance_of(Ci::IntegrateClusterService).to receive(:execute)
- described_class.new.execute(cluster)
- end
- end
-
- context 'when failed to get kubernetes token' do
- before do
- allow_any_instance_of(Ci::FetchKubernetesTokenService)
- .to receive(:execute).and_return(nil)
- end
-
- it 'sets an error to cluster object' do
- described_class.new.execute(cluster)
-
- expect(cluster.reload).to be_errored
- end
- end
- end
-
- context 'when failed to get cluster from api' do
- let(:error) { Google::Apis::ServerError.new('a') }
-
- before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:projects_zones_clusters_get).and_raise(error)
- end
-
- it 'sets an error to cluster object' do
- described_class.new.execute(cluster)
-
- expect(cluster.reload).to be_errored
- end
- end
- end
-end
diff --git a/spec/services/ci/integrate_cluster_service_spec.rb b/spec/services/ci/integrate_cluster_service_spec.rb
deleted file mode 100644
index 3a79c205bd1..00000000000
--- a/spec/services/ci/integrate_cluster_service_spec.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-require 'spec_helper'
-
-describe Ci::IntegrateClusterService do
- describe '#execute' do
- let(:cluster) { create(:gcp_cluster, :custom_project_namespace) }
- let(:endpoint) { '123.123.123.123' }
- let(:ca_cert) { 'ca_cert_xxx' }
- let(:token) { 'token_xxx' }
- let(:username) { 'username_xxx' }
- let(:password) { 'password_xxx' }
-
- before do
- described_class
- .new.execute(cluster, endpoint, ca_cert, token, username, password)
-
- cluster.reload
- end
-
- context 'when correct params' do
- it 'creates a cluster object' do
- expect(cluster.endpoint).to eq(endpoint)
- expect(cluster.ca_cert).to eq(ca_cert)
- expect(cluster.kubernetes_token).to eq(token)
- expect(cluster.username).to eq(username)
- expect(cluster.password).to eq(password)
- expect(cluster.service.active).to be_truthy
- expect(cluster.service.api_url).to eq(cluster.api_url)
- expect(cluster.service.ca_pem).to eq(ca_cert)
- expect(cluster.service.namespace).to eq(cluster.project_namespace)
- expect(cluster.service.token).to eq(token)
- end
- end
-
- context 'when invalid params' do
- let(:endpoint) { nil }
-
- it 'sets an error to cluster object' do
- expect(cluster).to be_errored
- end
- end
- end
-end
diff --git a/spec/services/ci/provision_cluster_service_spec.rb b/spec/services/ci/provision_cluster_service_spec.rb
deleted file mode 100644
index 5ce5c788314..00000000000
--- a/spec/services/ci/provision_cluster_service_spec.rb
+++ /dev/null
@@ -1,85 +0,0 @@
-require 'spec_helper'
-
-describe Ci::ProvisionClusterService do
- describe '#execute' do
- let(:cluster) { create(:gcp_cluster) }
- let(:operation) { spy }
-
- shared_examples 'error' do
- it 'sets an error to cluster object' do
- described_class.new.execute(cluster)
-
- expect(cluster.reload).to be_errored
- end
- end
-
- context 'when suceeded to request provision' do
- before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:projects_zones_clusters_create).and_return(operation)
- end
-
- context 'when operation status is RUNNING' do
- before do
- allow(operation).to receive(:status).and_return('RUNNING')
- end
-
- context 'when suceeded to parse gcp operation id' do
- before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:parse_operation_id).and_return('operation-123')
- end
-
- context 'when cluster status is scheduled' do
- before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:parse_operation_id).and_return('operation-123')
- end
-
- it 'schedules a worker for status minitoring' do
- expect(WaitForClusterCreationWorker).to receive(:perform_in)
-
- described_class.new.execute(cluster)
- end
- end
-
- context 'when cluster status is creating' do
- before do
- cluster.make_creating!
- end
-
- it_behaves_like 'error'
- end
- end
-
- context 'when failed to parse gcp operation id' do
- before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:parse_operation_id).and_return(nil)
- end
-
- it_behaves_like 'error'
- end
- end
-
- context 'when operation status is others' do
- before do
- allow(operation).to receive(:status).and_return('others')
- end
-
- it_behaves_like 'error'
- end
- end
-
- context 'when failed to request provision' do
- let(:error) { Google::Apis::ServerError.new('a') }
-
- before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:projects_zones_clusters_create).and_raise(error)
- end
-
- it_behaves_like 'error'
- end
- end
-end
diff --git a/spec/services/ci/update_cluster_service_spec.rb b/spec/services/ci/update_cluster_service_spec.rb
deleted file mode 100644
index a289385b88f..00000000000
--- a/spec/services/ci/update_cluster_service_spec.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-require 'spec_helper'
-
-describe Ci::UpdateClusterService do
- describe '#execute' do
- let(:cluster) { create(:gcp_cluster, :created_on_gke, :with_kubernetes_service) }
-
- before do
- described_class.new(cluster.project, cluster.user, params).execute(cluster)
-
- cluster.reload
- end
-
- context 'when correct params' do
- context 'when enabled is true' do
- let(:params) { { 'enabled' => 'true' } }
-
- it 'enables cluster and overwrite kubernetes service' do
- expect(cluster.enabled).to be_truthy
- expect(cluster.service.active).to be_truthy
- expect(cluster.service.api_url).to eq(cluster.api_url)
- expect(cluster.service.ca_pem).to eq(cluster.ca_cert)
- expect(cluster.service.namespace).to eq(cluster.project_namespace)
- expect(cluster.service.token).to eq(cluster.kubernetes_token)
- end
- end
-
- context 'when enabled is false' do
- let(:params) { { 'enabled' => 'false' } }
-
- it 'disables cluster and kubernetes service' do
- expect(cluster.enabled).to be_falsy
- expect(cluster.service.active).to be_falsy
- end
- end
- end
- end
-end
diff --git a/spec/services/clusters/create_service_spec.rb b/spec/services/clusters/create_service_spec.rb
new file mode 100644
index 00000000000..9c2c288a2fa
--- /dev/null
+++ b/spec/services/clusters/create_service_spec.rb
@@ -0,0 +1,117 @@
+require 'spec_helper'
+
+describe Clusters::CreateService do
+ let(:access_token) { 'xxx' }
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:result) { described_class.new(project, user, params).execute(access_token) }
+
+ context 'when provider is gcp' do
+ context 'when correct 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
+
+ it 'creates a cluster object and performs a worker' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+
+ expect { result }
+ .to change { Clusters::Cluster.count }.by(1)
+ .and change { Clusters::Providers::Gcp.count }.by(1)
+
+ expect(result.name).to eq('test-cluster')
+ expect(result.user).to eq(user)
+ expect(result.project).to eq(project)
+ expect(result.provider.gcp_project_id).to eq('gcp-project')
+ expect(result.provider.zone).to eq('us-central1-a')
+ expect(result.provider.num_nodes).to eq(1)
+ expect(result.provider.machine_type).to eq('machine_type-a')
+ expect(result.provider.access_token).to eq(access_token)
+ expect(result.platform).to be_nil
+ end
+ end
+
+ context 'when 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
+
+ it 'returns an error' do
+ expect(ClusterProvisionWorker).not_to receive(:perform_async)
+ expect { result }.to change { Clusters::Cluster.count }.by(0)
+ expect(result.errors[:"provider_gcp.gcp_project_id"]).to be_present
+ end
+ end
+ end
+
+ # TODO: This will be active in 10.3
+ # context 'when provider is user' do
+ # context 'when correct params' do
+ # let(:params) do
+ # {
+ # name: 'test-cluster',
+ # platform_type: :kubernetes,
+ # provider_type: :user,
+ # platform_kubernetes_attributes: {
+ # namespace: 'custom-namespace',
+ # api_url: 'https://111.111.111.111',
+ # token: 'token'
+ # }
+ # }
+ # end
+
+ # it 'creates a cluster object and performs a worker' do
+ # expect(ClusterProvisionWorker).to receive(:perform_async)
+
+ # expect { result }
+ # .to change { Clusters::Cluster.count }.by(1)
+ # .and change { Clusters::Platforms::Kubernetes.count }.by(1)
+
+ # expect(result.name).to eq('test-cluster')
+ # expect(result.user).to eq(user)
+ # expect(result.project).to eq(project)
+ # expect(result.provider).to be_nil
+ # expect(result.platform.namespace).to eq('custom-namespace')
+ # end
+ # end
+
+ # context 'when invalid params' do
+ # let(:params) do
+ # {
+ # name: 'test-cluster',
+ # platform_type: :kubernetes,
+ # provider_type: :user,
+ # platform_kubernetes_attributes: {
+ # namespace: 'custom-namespace',
+ # api_url: '!!!!!',
+ # token: 'token'
+ # }
+ # }
+ # end
+
+ # it 'returns an error' do
+ # # expect(ClusterProvisionWorker).not_to receive(:perform_async)
+ # expect { result }.to change { Clusters::Cluster.count }.by(0)
+ # expect(result.errors[:"platform_kubernetes.api_url"]).to be_present
+ # end
+ # end
+ # end
+end
diff --git a/spec/services/clusters/gcp/fetch_operation_service_spec.rb b/spec/services/clusters/gcp/fetch_operation_service_spec.rb
new file mode 100644
index 00000000000..20d46608033
--- /dev/null
+++ b/spec/services/clusters/gcp/fetch_operation_service_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe Clusters::Gcp::FetchOperationService do
+ include GoogleApi::CloudPlatformHelpers
+
+ describe '#execute' do
+ let(:provider) { create(:provider_gcp, :creating) }
+ let(:gcp_project_id) { provider.gcp_project_id }
+ let(:zone) { provider.zone }
+ let(:operation_id) { provider.operation_id }
+
+ shared_examples 'success' do
+ it 'yields' do
+ expect { |b| described_class.new.execute(provider, &b) }
+ .to yield_with_args
+ end
+ end
+
+ shared_examples 'error' do
+ it 'sets an error to provider object' do
+ expect { |b| described_class.new.execute(provider, &b) }
+ .not_to yield_with_args
+ expect(provider.reload).to be_errored
+ end
+ end
+
+ context 'when suceeded to fetch operation' do
+ before do
+ stub_cloud_platform_get_zone_operation(gcp_project_id, zone, operation_id)
+ end
+
+ it_behaves_like 'success'
+ end
+
+ context 'when Internal Server Error happened' do
+ before do
+ stub_cloud_platform_get_zone_operation_error(gcp_project_id, zone, operation_id)
+ end
+
+ it_behaves_like 'error'
+ end
+ end
+end
diff --git a/spec/services/clusters/gcp/finalize_creation_service_spec.rb b/spec/services/clusters/gcp/finalize_creation_service_spec.rb
new file mode 100644
index 00000000000..0cf91307589
--- /dev/null
+++ b/spec/services/clusters/gcp/finalize_creation_service_spec.rb
@@ -0,0 +1,111 @@
+require 'spec_helper'
+
+describe Clusters::Gcp::FinalizeCreationService do
+ include GoogleApi::CloudPlatformHelpers
+ include KubernetesHelpers
+
+ describe '#execute' do
+ let(:cluster) { create(:cluster, :project, :providing_by_gcp) }
+ let(:provider) { cluster.provider }
+ let(:platform) { cluster.platform }
+ let(:gcp_project_id) { provider.gcp_project_id }
+ let(:zone) { provider.zone }
+ let(:cluster_name) { cluster.name }
+
+ shared_examples 'success' do
+ it 'configures provider and kubernetes' do
+ described_class.new.execute(provider)
+
+ expect(provider).to be_created
+ end
+ end
+
+ shared_examples 'error' do
+ it 'sets an error to provider object' do
+ described_class.new.execute(provider)
+
+ expect(provider.reload).to be_errored
+ end
+ end
+
+ context 'when suceeded to fetch gke cluster info' do
+ let(:endpoint) { '111.111.111.111' }
+ let(:api_url) { 'https://' + endpoint }
+ let(:username) { 'sample-username' }
+ let(:password) { 'sample-password' }
+
+ before do
+ stub_cloud_platform_get_zone_cluster(
+ gcp_project_id, zone, cluster_name,
+ {
+ endpoint: endpoint,
+ username: username,
+ password: password
+ }
+ )
+
+ stub_kubeclient_discover(api_url)
+ end
+
+ context 'when suceeded to fetch kuberenetes token' do
+ let(:token) { 'sample-token' }
+
+ before do
+ stub_kubeclient_get_secrets(
+ api_url,
+ {
+ token: Base64.encode64(token)
+ } )
+ end
+
+ it_behaves_like 'success'
+
+ it 'has corresponded data' do
+ described_class.new.execute(provider)
+ cluster.reload
+ provider.reload
+ platform.reload
+
+ expect(provider.endpoint).to eq(endpoint)
+ expect(platform.api_url).to eq(api_url)
+ expect(platform.ca_cert).to eq(Base64.decode64(load_sample_cert))
+ expect(platform.username).to eq(username)
+ expect(platform.password).to eq(password)
+ expect(platform.token).to eq(token)
+ end
+ end
+
+ context 'when default-token is not found' do
+ before do
+ stub_kubeclient_get_secrets(api_url, metadata_name: 'aaaa')
+ end
+
+ it_behaves_like 'error'
+ end
+
+ context 'when token is empty' do
+ before do
+ stub_kubeclient_get_secrets(api_url, token: '')
+ end
+
+ it_behaves_like 'error'
+ end
+
+ context 'when failed to fetch kuberenetes token' do
+ before do
+ stub_kubeclient_get_secrets_error(api_url)
+ end
+
+ it_behaves_like 'error'
+ end
+ end
+
+ context 'when failed to fetch gke cluster info' do
+ before do
+ stub_cloud_platform_get_zone_cluster_error(gcp_project_id, zone, cluster_name)
+ end
+
+ it_behaves_like 'error'
+ end
+ end
+end
diff --git a/spec/services/clusters/gcp/provision_service_spec.rb b/spec/services/clusters/gcp/provision_service_spec.rb
new file mode 100644
index 00000000000..f5f9d4800fd
--- /dev/null
+++ b/spec/services/clusters/gcp/provision_service_spec.rb
@@ -0,0 +1,69 @@
+require 'spec_helper'
+
+describe Clusters::Gcp::ProvisionService do
+ include GoogleApi::CloudPlatformHelpers
+
+ describe '#execute' do
+ let(:provider) { create(:provider_gcp, :scheduled) }
+ let(:gcp_project_id) { provider.gcp_project_id }
+ let(:zone) { provider.zone }
+
+ shared_examples 'success' do
+ it 'schedules a worker for status minitoring' do
+ expect(WaitForClusterCreationWorker).to receive(:perform_in)
+
+ described_class.new.execute(provider)
+
+ expect(provider.reload).to be_creating
+ end
+ end
+
+ shared_examples 'error' do
+ it 'sets an error to provider object' do
+ described_class.new.execute(provider)
+
+ expect(provider.reload).to be_errored
+ end
+ end
+
+ context 'when suceeded to request provision' do
+ before do
+ stub_cloud_platform_create_cluster(gcp_project_id, zone)
+ end
+
+ it_behaves_like 'success'
+ end
+
+ context 'when operation status is unexpected' do
+ before do
+ stub_cloud_platform_create_cluster(
+ gcp_project_id, zone,
+ {
+ "status": 'unexpected'
+ } )
+ end
+
+ it_behaves_like 'error'
+ end
+
+ context 'when selfLink is unexpected' do
+ before do
+ stub_cloud_platform_create_cluster(
+ gcp_project_id, zone,
+ {
+ "selfLink": 'unexpected'
+ })
+ end
+
+ it_behaves_like 'error'
+ end
+
+ context 'when Internal Server Error happened' do
+ before do
+ stub_cloud_platform_create_cluster_error(gcp_project_id, zone)
+ end
+
+ it_behaves_like 'error'
+ end
+ end
+end
diff --git a/spec/services/clusters/gcp/verify_provision_status_service_spec.rb b/spec/services/clusters/gcp/verify_provision_status_service_spec.rb
new file mode 100644
index 00000000000..666fcf13cac
--- /dev/null
+++ b/spec/services/clusters/gcp/verify_provision_status_service_spec.rb
@@ -0,0 +1,107 @@
+require 'spec_helper'
+
+describe Clusters::Gcp::VerifyProvisionStatusService do
+ include GoogleApi::CloudPlatformHelpers
+
+ describe '#execute' do
+ let(:provider) { create(:provider_gcp, :creating) }
+ let(:gcp_project_id) { provider.gcp_project_id }
+ let(:zone) { provider.zone }
+ let(:operation_id) { provider.operation_id }
+
+ shared_examples 'continue_creation' do
+ it 'schedules a worker for status minitoring' do
+ expect(WaitForClusterCreationWorker).to receive(:perform_in)
+
+ described_class.new.execute(provider)
+ end
+ end
+
+ shared_examples 'finalize_creation' do
+ it 'schedules a worker for status minitoring' do
+ expect_any_instance_of(Clusters::Gcp::FinalizeCreationService).to receive(:execute)
+
+ described_class.new.execute(provider)
+ end
+ end
+
+ shared_examples 'error' do
+ it 'sets an error to provider object' do
+ described_class.new.execute(provider)
+
+ expect(provider.reload).to be_errored
+ end
+ end
+
+ context 'when operation status is RUNNING' do
+ before do
+ stub_cloud_platform_get_zone_operation(
+ gcp_project_id, zone, operation_id,
+ {
+ "status": 'RUNNING',
+ "startTime": 1.minute.ago.strftime("%FT%TZ")
+ } )
+ end
+
+ it_behaves_like 'continue_creation'
+
+ context 'when cluster creation time exceeds timeout' do
+ before do
+ stub_cloud_platform_get_zone_operation(
+ gcp_project_id, zone, operation_id,
+ {
+ "status": 'RUNNING',
+ "startTime": 30.minutes.ago.strftime("%FT%TZ")
+ } )
+ end
+
+ it_behaves_like 'error'
+ end
+ end
+
+ context 'when operation status is PENDING' do
+ before do
+ stub_cloud_platform_get_zone_operation(
+ gcp_project_id, zone, operation_id,
+ {
+ "status": 'PENDING',
+ "startTime": 1.minute.ago.strftime("%FT%TZ")
+ } )
+ end
+
+ it_behaves_like 'continue_creation'
+ end
+
+ context 'when operation status is DONE' do
+ before do
+ stub_cloud_platform_get_zone_operation(
+ gcp_project_id, zone, operation_id,
+ {
+ "status": 'DONE'
+ } )
+ end
+
+ it_behaves_like 'finalize_creation'
+ end
+
+ context 'when operation status is unexpected' do
+ before do
+ stub_cloud_platform_get_zone_operation(
+ gcp_project_id, zone, operation_id,
+ {
+ "status": 'unexpected'
+ } )
+ end
+
+ it_behaves_like 'error'
+ end
+
+ context 'when failed to get operation status' do
+ before do
+ stub_cloud_platform_get_zone_operation_error(gcp_project_id, zone, operation_id)
+ end
+
+ it_behaves_like 'error'
+ end
+ end
+end
diff --git a/spec/services/clusters/update_service_spec.rb b/spec/services/clusters/update_service_spec.rb
new file mode 100644
index 00000000000..2d91a21035d
--- /dev/null
+++ b/spec/services/clusters/update_service_spec.rb
@@ -0,0 +1,59 @@
+require 'spec_helper'
+
+describe Clusters::UpdateService do
+ describe '#execute' do
+ subject { described_class.new(cluster.project, cluster.user, params).execute(cluster) }
+
+ let(:cluster) { create(:cluster, :project, :provided_by_user) }
+
+ context 'when correct params' do
+ context 'when enabled is true' do
+ let(:params) { { enabled: true } }
+
+ it 'enables cluster' do
+ is_expected.to eq(true)
+ expect(cluster.enabled).to be_truthy
+ end
+ end
+
+ context 'when enabled is false' do
+ let(:params) { { enabled: false } }
+
+ it 'disables cluster' do
+ is_expected.to eq(true)
+ expect(cluster.enabled).to be_falsy
+ end
+ end
+
+ context 'when namespace is specified' do
+ let(:params) do
+ {
+ platform_kubernetes_attributes: {
+ namespace: 'custom-namespace'
+ }
+ }
+ end
+
+ it 'updates namespace' do
+ is_expected.to eq(true)
+ expect(cluster.platform.namespace).to eq('custom-namespace')
+ end
+ end
+ end
+
+ context 'when invalid params' do
+ let(:params) do
+ {
+ platform_kubernetes_attributes: {
+ namespace: '!!!'
+ }
+ }
+ end
+
+ it 'returns false' do
+ is_expected.to eq(false)
+ expect(cluster.errors[:"platform_kubernetes.namespace"]).to be_present
+ end
+ end
+ end
+end
diff --git a/spec/services/issuable/common_system_notes_service_spec.rb b/spec/services/issuable/common_system_notes_service_spec.rb
new file mode 100644
index 00000000000..9f92b662be1
--- /dev/null
+++ b/spec/services/issuable/common_system_notes_service_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe Issuable::CommonSystemNotesService do
+ let(:user) { create(:user) }
+ 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('Issue')
+ 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'
+ it_behaves_like 'system note creation', { discussion_locked: true }, 'locked this issue'
+ it_behaves_like 'system note creation', { time_estimate: 5 }, 'changed time estimate'
+
+ context 'when new label is added' do
+ before do
+ label = create(:label, project: project)
+ issuable.labels << label
+ end
+
+ it_behaves_like 'system note creation', {}, /added ~\w+ label/
+ end
+
+ context 'when new milestone is assigned' do
+ before do
+ milestone = create(:milestone, project: project)
+ issuable.milestone_id = milestone.id
+ end
+
+ it_behaves_like 'system note creation', {}, 'changed milestone'
+ end
+ end
+end
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index d1043f99b5a..ac196e92601 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -12,55 +12,6 @@ describe MergeRequests::MergeService do
end
describe '#execute' do
- context 'MergeRequest#merge_jid' do
- let(:service) do
- described_class.new(project, user, commit_message: 'Awesome message')
- end
-
- before do
- merge_request.update_column(:merge_jid, 'hash-123')
- end
-
- it 'is cleaned when no error is raised' do
- service.execute(merge_request)
-
- expect(merge_request.reload.merge_jid).to be_nil
- end
-
- it 'is cleaned when expected error is raised' do
- allow(service).to receive(:commit).and_raise(described_class::MergeError)
-
- service.execute(merge_request)
-
- expect(merge_request.reload.merge_jid).to be_nil
- end
-
- it 'is cleaned when merge request is not mergeable' do
- allow(merge_request).to receive(:mergeable?).and_return(false)
-
- service.execute(merge_request)
-
- expect(merge_request.reload.merge_jid).to be_nil
- end
-
- it 'is cleaned when no source is found' do
- allow(merge_request).to receive(:diff_head_sha).and_return(nil)
-
- service.execute(merge_request)
-
- expect(merge_request.reload.merge_jid).to be_nil
- end
-
- it 'is not cleaned when unexpected error is raised' do
- service = described_class.new(project, user, commit_message: 'Awesome message')
- allow(service).to receive(:commit).and_raise(StandardError)
-
- expect { service.execute(merge_request) }.to raise_error(StandardError)
-
- expect(merge_request.reload.merge_jid).to be_present
- end
- end
-
context 'valid params' do
let(:service) { described_class.new(project, user, commit_message: 'Awesome message') }
diff --git a/spec/services/milestones/promote_service_spec.rb b/spec/services/milestones/promote_service_spec.rb
new file mode 100644
index 00000000000..9f2df6d6d19
--- /dev/null
+++ b/spec/services/milestones/promote_service_spec.rb
@@ -0,0 +1,77 @@
+require 'spec_helper'
+
+describe Milestones::PromoteService do
+ let(:group) { create(:group) }
+ let(:project) { create(:project, namespace: group) }
+ let(:user) { create(:user) }
+ let(:milestone_title) { 'project milestone' }
+ let(:milestone) { create(:milestone, project: project, title: milestone_title) }
+ let(:service) { described_class.new(project, user) }
+
+ describe '#execute' do
+ before do
+ group.add_master(user)
+ end
+
+ context 'validations' do
+ it 'raises error if milestone does not belong to a project' do
+ allow(milestone).to receive(:project_milestone?).and_return(false)
+
+ expect { service.execute(milestone) }.to raise_error(described_class::PromoteMilestoneError)
+ end
+
+ it 'raises error if project does not belong to a group' do
+ project.update(namespace: user.namespace)
+
+ expect { service.execute(milestone) }.to raise_error(described_class::PromoteMilestoneError)
+ end
+ end
+
+ context 'without duplicated milestone titles across projects' do
+ it 'promotes project milestone to group milestone' do
+ promoted_milestone = service.execute(milestone)
+
+ expect(promoted_milestone).to be_group_milestone
+ end
+
+ it 'sets issuables with new promoted milestone' do
+ issue = create(:issue, milestone: milestone, project: project)
+ merge_request = create(:merge_request, milestone: milestone, source_project: project)
+
+ promoted_milestone = service.execute(milestone)
+
+ expect(promoted_milestone).to be_group_milestone
+ expect(issue.reload.milestone).to eq(promoted_milestone)
+ expect(merge_request.reload.milestone).to eq(promoted_milestone)
+ end
+ end
+
+ context 'with duplicated milestone titles across projects' do
+ let(:project_2) { create(:project, namespace: group) }
+ let!(:milestone_2) { create(:milestone, project: project_2, title: milestone_title) }
+
+ it 'deletes project milestones with the same title' do
+ promoted_milestone = service.execute(milestone)
+
+ expect(promoted_milestone).to be_group_milestone
+ expect(promoted_milestone).to be_valid
+ expect(Milestone.exists?(milestone.id)).to be_falsy
+ expect(Milestone.exists?(milestone_2.id)).to be_falsy
+ end
+
+ it 'sets all issuables with new promoted milestone' do
+ issue = create(:issue, milestone: milestone, project: project)
+ issue_2 = create(:issue, milestone: milestone_2, project: project_2)
+ merge_request = create(:merge_request, milestone: milestone, source_project: project)
+ merge_request_2 = create(:merge_request, milestone: milestone_2, source_project: project_2)
+
+ promoted_milestone = service.execute(milestone)
+
+ expect(issue.reload.milestone).to eq(promoted_milestone)
+ expect(issue_2.reload.milestone).to eq(promoted_milestone)
+ expect(merge_request.reload.milestone).to eq(promoted_milestone)
+ expect(merge_request_2.reload.milestone).to eq(promoted_milestone)
+ end
+ end
+ end
+end
diff --git a/spec/services/projects/group_links/create_service_spec.rb b/spec/services/projects/group_links/create_service_spec.rb
new file mode 100644
index 00000000000..ffb270d277e
--- /dev/null
+++ b/spec/services/projects/group_links/create_service_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe Projects::GroupLinks::CreateService, '#execute' do
+ let(:user) { create :user }
+ let(:group) { create :group }
+ let(:project) { create :project }
+ let(:opts) do
+ {
+ link_group_access: '30',
+ expires_at: nil
+ }
+ end
+ let(:subject) { described_class.new(project, user, opts) }
+
+ it 'adds group to project' do
+ expect { subject.execute(group) }.to change { project.project_group_links.count }.from(0).to(1)
+ end
+
+ it 'returns false if group is blank' do
+ expect { subject.execute(nil) }.not_to change { project.project_group_links.count }
+ end
+end
diff --git a/spec/services/projects/group_links/destroy_service_spec.rb b/spec/services/projects/group_links/destroy_service_spec.rb
new file mode 100644
index 00000000000..336ee01ae50
--- /dev/null
+++ b/spec/services/projects/group_links/destroy_service_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe Projects::GroupLinks::DestroyService, '#execute' do
+ let(:group_link) { create :project_group_link }
+ let(:project) { group_link.project }
+ let(:user) { create :user }
+ let(:subject) { described_class.new(project, user) }
+
+ it 'removes group from project' do
+ expect { subject.execute(group_link) }.to change { project.project_group_links.count }.from(1).to(0)
+ end
+
+ it 'returns false if group_link is blank' do
+ expect { subject.execute(nil) }.not_to change { project.project_group_links.count }
+ end
+end
diff --git a/spec/services/projects/hashed_storage_migration_service_spec.rb b/spec/services/projects/hashed_storage_migration_service_spec.rb
index aa1988d29d6..b71b47c59b6 100644
--- a/spec/services/projects/hashed_storage_migration_service_spec.rb
+++ b/spec/services/projects/hashed_storage_migration_service_spec.rb
@@ -23,7 +23,7 @@ describe Projects::HashedStorageMigrationService do
it 'updates project to be hashed and not read-only' do
service.execute
- expect(project.hashed_storage?).to be_truthy
+ expect(project.hashed_storage?(:repository)).to be_truthy
expect(project.repository_read_only).to be_falsey
end
diff --git a/spec/services/projects/unlink_fork_service_spec.rb b/spec/services/projects/unlink_fork_service_spec.rb
index 50d3a4ec982..2bba71fef4f 100644
--- a/spec/services/projects/unlink_fork_service_spec.rb
+++ b/spec/services/projects/unlink_fork_service_spec.rb
@@ -12,6 +12,9 @@ describe Projects::UnlinkForkService do
context 'with opened merge request on the source project' do
let(:merge_request) { create(:merge_request, source_project: forked_project, target_project: fork_link.forked_from_project) }
+ let(:merge_request2) { create(:merge_request, source_project: forked_project, target_project: fork_project(project)) }
+ let(:merge_request_in_fork) { create(:merge_request, source_project: forked_project, target_project: forked_project) }
+
let(:mr_close_service) { MergeRequests::CloseService.new(forked_project, user) }
before do
@@ -22,9 +25,14 @@ describe Projects::UnlinkForkService do
it 'close all pending merge requests' do
expect(mr_close_service).to receive(:execute).with(merge_request)
+ expect(mr_close_service).to receive(:execute).with(merge_request2)
subject.execute
end
+
+ it 'does not close merge requests for the project being unlinked' do
+ expect(mr_close_service).not_to receive(:execute).with(merge_request_in_fork)
+ end
end
it 'remove fork relation' do
@@ -53,4 +61,14 @@ describe Projects::UnlinkForkService do
expect(source.forks_count).to be_zero
end
+
+ context 'when the original project was deleted' do
+ it 'does not fail when the original project is deleted' do
+ source = forked_project.forked_from_project
+ source.destroy
+ forked_project.reload
+
+ expect { subject.execute }.not_to raise_error
+ end
+ end
end
diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb
index 8b5d9187785..46cd10cdc12 100644
--- a/spec/services/system_hooks_service_spec.rb
+++ b/spec/services/system_hooks_service_spec.rb
@@ -63,11 +63,54 @@ describe SystemHooksService do
:group_id, :user_id, :user_username, :user_name, :user_email, :group_access
)
end
+
+ it 'includes the correct project visibility level' do
+ data = event_data(project, :create)
+
+ expect(data[:project_visibility]).to eq('private')
+ end
+
+ context 'group_rename' do
+ it 'contains old and new path' do
+ allow(group).to receive(:path_was).and_return('old-path')
+
+ data = event_data(group, :rename)
+
+ expect(data).to include(:event_name, :name, :created_at, :updated_at, :full_path, :path, :group_id, :old_path, :old_full_path)
+ expect(data[:path]).to eq(group.path)
+ expect(data[:full_path]).to eq(group.path)
+ expect(data[:old_path]).to eq(group.path_was)
+ expect(data[:old_full_path]).to eq(group.path_was)
+ end
+
+ it 'contains old and new full_path for subgroup' do
+ subgroup = create(:group, parent: group)
+ allow(subgroup).to receive(:path_was).and_return('old-path')
+
+ data = event_data(subgroup, :rename)
+
+ expect(data[:full_path]).to eq(subgroup.full_path)
+ expect(data[:old_path]).to eq('old-path')
+ end
+ end
+
+ context 'user_rename' do
+ it 'contains old and new username' do
+ allow(user).to receive(:username_was).and_return('old-username')
+
+ data = event_data(user, :rename)
+
+ expect(data).to include(:event_name, :name, :created_at, :updated_at, :email, :user_id, :username, :old_username)
+ expect(data[:username]).to eq(user.username)
+ expect(data[:old_username]).to eq(user.username_was)
+ end
+ end
end
context 'event names' do
it { expect(event_name(user, :create)).to eq "user_create" }
it { expect(event_name(user, :destroy)).to eq "user_destroy" }
+ it { expect(event_name(user, :rename)).to eq 'user_rename' }
it { expect(event_name(project, :create)).to eq "project_create" }
it { expect(event_name(project, :destroy)).to eq "project_destroy" }
it { expect(event_name(project, :rename)).to eq "project_rename" }
@@ -79,6 +122,7 @@ describe SystemHooksService do
it { expect(event_name(key, :destroy)).to eq 'key_destroy' }
it { expect(event_name(group, :create)).to eq 'group_create' }
it { expect(event_name(group, :destroy)).to eq 'group_destroy' }
+ it { expect(event_name(group, :rename)).to eq 'group_rename' }
it { expect(event_name(group_member, :create)).to eq 'user_add_to_group' }
it { expect(event_name(group_member, :destroy)).to eq 'user_remove_from_group' }
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 48cacba6a8a..7c8331f6c60 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -48,7 +48,11 @@ RSpec.configure do |config|
config.include Warden::Test::Helpers, type: :request
config.include LoginHelpers, type: :feature
config.include SearchHelpers, type: :feature
+ config.include CookieHelper, :js
+ config.include InputHelper, :js
+ config.include InspectRequests, :js
config.include WaitForRequests, :js
+ config.include LiveDebugger, :js
config.include StubConfiguration
config.include EmailHelpers, :mailer, type: :mailer
config.include TestEnv
diff --git a/spec/support/api_helpers.rb b/spec/support/api_helpers.rb
index 01aca74274c..ac0c7a9b493 100644
--- a/spec/support/api_helpers.rb
+++ b/spec/support/api_helpers.rb
@@ -18,21 +18,23 @@ module ApiHelpers
#
# Returns the relative path to the requested API resource
def api(path, user = nil, version: API::API.version, personal_access_token: nil, oauth_access_token: nil)
- "/api/#{version}#{path}" +
+ full_path = "/api/#{version}#{path}"
- # Normalize query string
- (path.index('?') ? '' : '?') +
+ if oauth_access_token
+ query_string = "access_token=#{oauth_access_token.token}"
+ elsif personal_access_token
+ query_string = "private_token=#{personal_access_token.token}"
+ elsif user
+ personal_access_token = create(:personal_access_token, user: user)
+ query_string = "private_token=#{personal_access_token.token}"
+ end
- if personal_access_token.present?
- "&private_token=#{personal_access_token.token}"
- elsif oauth_access_token.present?
- "&access_token=#{oauth_access_token.token}"
- # Append private_token if given a User object
- elsif user.respond_to?(:private_token)
- "&private_token=#{user.private_token}"
- else
- ''
- end
+ if query_string
+ full_path << (path.index('?') ? '&' : '?')
+ full_path << query_string
+ end
+
+ full_path
end
# Temporary helper method for simplifying V3 exclusive API specs
diff --git a/spec/support/bare_repo_operations.rb b/spec/support/bare_repo_operations.rb
new file mode 100644
index 00000000000..38d11992dc2
--- /dev/null
+++ b/spec/support/bare_repo_operations.rb
@@ -0,0 +1,60 @@
+require 'zlib'
+
+class BareRepoOperations
+ # The ID of empty tree.
+ # See http://stackoverflow.com/a/40884093/1856239 and https://github.com/git/git/blob/3ad8b5bf26362ac67c9020bf8c30eee54a84f56d/cache.h#L1011-L1012
+ EMPTY_TREE_ID = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'.freeze
+
+ include Gitlab::Popen
+
+ def initialize(path_to_repo)
+ @path_to_repo = path_to_repo
+ end
+
+ # Based on https://stackoverflow.com/a/25556917/1856239
+ def commit_file(file, dst_path, branch = 'master')
+ head_id = execute(['show', '--format=format:%H', '--no-patch', branch], allow_failure: true)[0] || EMPTY_TREE_ID
+
+ execute(['read-tree', '--empty'])
+ execute(['read-tree', head_id])
+
+ blob_id = execute(['hash-object', '--stdin', '-w']) do |stdin|
+ stdin.write(file.read)
+ end
+
+ execute(['update-index', '--add', '--cacheinfo', '100644', blob_id[0], dst_path])
+
+ tree_id = execute(['write-tree'])
+
+ commit_tree_args = ['commit-tree', tree_id[0], '-m', "Add #{dst_path}"]
+ commit_tree_args += ['-p', head_id] unless head_id == EMPTY_TREE_ID
+ commit_id = execute(commit_tree_args)
+
+ execute(['update-ref', "refs/heads/#{branch}", commit_id[0]])
+ end
+
+ private
+
+ def execute(args, allow_failure: false)
+ output, status = popen(base_args + args, nil) do |stdin|
+ yield stdin if block_given?
+ end
+
+ unless status.zero?
+ if allow_failure
+ return []
+ else
+ raise "Got a non-zero exit code while calling out `#{args.join(' ')}`: #{output}"
+ end
+ end
+
+ output.split("\n")
+ end
+
+ def base_args
+ [
+ Gitlab.config.git.bin_path,
+ "--git-dir=#{@path_to_repo}"
+ ]
+ end
+end
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index c45c4a4310d..9f672bc92fc 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -1,25 +1,25 @@
# rubocop:disable Style/GlobalVars
require 'capybara/rails'
require 'capybara/rspec'
-require 'capybara/poltergeist'
require 'capybara-screenshot/rspec'
+require 'selenium-webdriver'
# Give CI some extra time
timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 60 : 30
-Capybara.javascript_driver = :poltergeist
-Capybara.register_driver :poltergeist do |app|
- Capybara::Poltergeist::Driver.new(
- app,
- js_errors: true,
- timeout: timeout,
- window_size: [1366, 768],
- url_whitelist: %w[localhost 127.0.0.1],
- url_blacklist: %w[.mp4 .png .gif .avi .bmp .jpg .jpeg],
- phantomjs_options: [
- '--load-images=yes'
- ]
+Capybara.javascript_driver = :chrome
+Capybara.register_driver :chrome do |app|
+ extra_args = []
+ extra_args << 'headless' unless ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i
+
+ capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
+ chromeOptions: {
+ 'args' => %w[no-sandbox disable-gpu --window-size=1240,1400] + extra_args
+ }
)
+
+ Capybara::Selenium::Driver
+ .new(app, browser: :chrome, desired_capabilities: capabilities)
end
Capybara.default_max_wait_time = timeout
@@ -27,6 +27,10 @@ Capybara.ignore_hidden_elements = true
# 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
RSpec.configure do |config|
config.before(:context, :js) do
@@ -37,13 +41,23 @@ RSpec.configure do |config|
end
config.before(:example, :js) do
+ session = Capybara.current_session
+
allow(Gitlab::Application.routes).to receive(:default_url_options).and_return(
- host: Capybara.current_session.server.host,
- port: Capybara.current_session.server.port,
+ host: session.server.host,
+ port: session.server.port,
protocol: 'http')
+
+ # reset window size between tests
+ unless session.current_window.size == [1240, 1400]
+ session.current_window.resize_to(1240, 1400) rescue nil
+ end
end
config.after(:example, :js) do |example|
+ # prevent localstorage from introducing side effects based on test order
+ execute_script("localStorage.clear();")
+
# capybara/rspec already calls Capybara.reset_sessions! in an `after` hook,
# but `block_and_wait_for_requests_complete` is called before it so by
# calling it explicitely here, we prevent any new requests from being fired
diff --git a/spec/support/capybara_helpers.rb b/spec/support/capybara_helpers.rb
index 3eb7bea3227..868233416bf 100644
--- a/spec/support/capybara_helpers.rb
+++ b/spec/support/capybara_helpers.rb
@@ -38,7 +38,7 @@ module CapybaraHelpers
# Simulate a browser restart by clearing the session cookie.
def clear_browser_session
- page.driver.remove_cookie('_gitlab_session')
+ page.driver.browser.manage.delete_cookie('_gitlab_session')
end
end
diff --git a/spec/support/cookie_helper.rb b/spec/support/cookie_helper.rb
new file mode 100644
index 00000000000..224619c899c
--- /dev/null
+++ b/spec/support/cookie_helper.rb
@@ -0,0 +1,17 @@
+# Helper for setting cookies in Selenium/WebDriver
+#
+module CookieHelper
+ def set_cookie(name, value, options = {})
+ # Selenium driver will not set cookies for a given domain when the browser is at `about:blank`.
+ # It also doesn't appear to allow overriding the cookie path. loading `/` is the most inclusive.
+ visit options.fetch(:path, '/') unless on_a_page?
+ page.driver.browser.manage.add_cookie(name: name, value: value, **options)
+ end
+
+ private
+
+ def on_a_page?
+ current_url = Capybara.current_session.driver.browser.current_url
+ current_url && current_url != '' && current_url != 'about:blank' && current_url != 'data:,'
+ end
+end
diff --git a/spec/support/features/discussion_comments_shared_example.rb b/spec/support/features/discussion_comments_shared_example.rb
index 7132b9cd221..aabc64d972b 100644
--- a/spec/support/features/discussion_comments_shared_example.rb
+++ b/spec/support/features/discussion_comments_shared_example.rb
@@ -71,26 +71,28 @@ shared_examples 'discussion comments' do |resource_name|
expect(page).not_to have_selector menu_selector
find(toggle_selector).click
- find('body').trigger 'click'
+ find('body').click
expect(page).not_to have_selector menu_selector
end
it 'clicking the ul padding or divider should not change the text' do
- find(menu_selector).trigger 'click'
+ execute_script("document.querySelector('#{menu_selector}').click()")
+ # on issues page, the menu closes when clicking anywhere, on other pages it will
+ # remain open if clicking divider or menu padding, but should not change button action
if resource_name == 'issue'
expect(find(dropdown_selector)).to have_content 'Comment'
find(toggle_selector).click
- find("#{menu_selector} .divider").trigger 'click'
+ execute_script("document.querySelector('#{menu_selector} .divider').click()")
else
- find(menu_selector).trigger 'click'
+ execute_script("document.querySelector('#{menu_selector}').click()")
expect(page).to have_selector menu_selector
expect(find(dropdown_selector)).to have_content 'Comment'
- find("#{menu_selector} .divider").trigger 'click'
+ execute_script("document.querySelector('#{menu_selector} .divider').click()")
expect(page).to have_selector menu_selector
end
@@ -105,7 +107,12 @@ shared_examples 'discussion comments' do |resource_name|
end
it 'updates the submit button text and closes the dropdown' do
- expect(find(dropdown_selector)).to have_content 'Start discussion'
+ # on issues page, the submit input is a <button>, on other pages it is <input>
+ if resource_name == 'issue'
+ expect(find(submit_selector)).to have_content 'Start discussion'
+ else
+ expect(find(submit_selector).value).to eq 'Start discussion'
+ end
expect(page).not_to have_selector menu_selector
end
@@ -187,7 +194,12 @@ shared_examples 'discussion comments' do |resource_name|
end
it 'updates the submit button text and closes the dropdown' do
- expect(find(dropdown_selector)).to have_content 'Comment'
+ # on issues page, the submit input is a <button>, on other pages it is <input>
+ if resource_name == 'issue'
+ expect(find(submit_selector)).to have_content 'Comment'
+ else
+ expect(find(submit_selector).value).to eq 'Comment'
+ end
expect(page).not_to have_selector menu_selector
end
@@ -226,6 +238,7 @@ shared_examples 'discussion comments' do |resource_name|
describe "on a closed #{resource_name}" do
before do
find("#{form_selector} .js-note-target-close").click
+ wait_for_requests
find("#{form_selector} .note-textarea").send_keys('a')
end
diff --git a/spec/support/features/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb
index 061e0d35590..08e21ee2537 100644
--- a/spec/support/features/issuable_slash_commands_shared_examples.rb
+++ b/spec/support/features/issuable_slash_commands_shared_examples.rb
@@ -61,7 +61,7 @@ shared_examples 'issuable record that supports quick actions in its description
context 'with a note containing commands' do
it 'creates a note without the commands and interpret the commands accordingly' do
assignee = create(:user, username: 'bob')
- write_note("Awesome!\n/assign @bob\n/label ~bug\n/milestone %\"ASAP\"")
+ write_note("Awesome!\n\n/assign @bob\n\n/label ~bug\n\n/milestone %\"ASAP\"")
expect(page).to have_content 'Awesome!'
expect(page).not_to have_content '/assign @bob'
@@ -82,7 +82,7 @@ shared_examples 'issuable record that supports quick actions in its description
context 'with a note containing only commands' do
it 'does not create a note but interpret the commands accordingly' do
assignee = create(:user, username: 'bob')
- write_note("/assign @bob\n/label ~bug\n/milestone %\"ASAP\"")
+ write_note("/assign @bob\n\n/label ~bug\n\n/milestone %\"ASAP\"")
expect(page).not_to have_content '/assign @bob'
expect(page).not_to have_content '/label ~bug'
diff --git a/spec/support/features/reportable_note_shared_examples.rb b/spec/support/features/reportable_note_shared_examples.rb
index 192a2fed0a8..836e5e7be23 100644
--- a/spec/support/features/reportable_note_shared_examples.rb
+++ b/spec/support/features/reportable_note_shared_examples.rb
@@ -39,7 +39,7 @@ shared_examples 'reportable note' do |type|
end
def open_dropdown(dropdown)
- dropdown.find('.more-actions-toggle').trigger('click')
+ dropdown.find('.more-actions-toggle').click
dropdown.find('.dropdown-menu li', match: :first)
end
end
diff --git a/spec/support/gitlab-git-test.git/objects/88/3e379fcaa5f818fca81cdbabd7a497794d6535 b/spec/support/gitlab-git-test.git/objects/88/3e379fcaa5f818fca81cdbabd7a497794d6535
new file mode 100644
index 00000000000..1c47f34b9a5
--- /dev/null
+++ b/spec/support/gitlab-git-test.git/objects/88/3e379fcaa5f818fca81cdbabd7a497794d6535
Binary files differ
diff --git a/spec/support/gitlab-git-test.git/objects/c8/b1ab16c858c67b680eea4644cf652485f555cf b/spec/support/gitlab-git-test.git/objects/c8/b1ab16c858c67b680eea4644cf652485f555cf
new file mode 100644
index 00000000000..ca13c8df66a
--- /dev/null
+++ b/spec/support/gitlab-git-test.git/objects/c8/b1ab16c858c67b680eea4644cf652485f555cf
Binary files differ
diff --git a/spec/support/gitlab-git-test.git/objects/e3/7697aea12699f0b44544332a7c0f41ace5fb16 b/spec/support/gitlab-git-test.git/objects/e3/7697aea12699f0b44544332a7c0f41ace5fb16
new file mode 100644
index 00000000000..3be244dbda4
--- /dev/null
+++ b/spec/support/gitlab-git-test.git/objects/e3/7697aea12699f0b44544332a7c0f41ace5fb16
@@ -0,0 +1,2 @@
+x¥ŽK
+Â0EgNIÒ|ADtè*^’ mZ qGîÄY×àð8—×ZK©ý®7"ÈFc’Ò%oH¢D²Ü9rZÛLÎs“MJ2Œ™=±ÑÒAå…CmeFg²·V¨xI9øH2†¯þXÜJ…ár»pÅ6‡Ï;NÔà8•zˆ??>ß+–ù×z¡¹WÆBÞ ÎÙf·Ç}«þßb¡N@K\SYîì •iSC \ No newline at end of file
diff --git a/spec/support/gitlab-git-test.git/objects/eb/a0c153ed20d927bab00507f356043b6b4be31e b/spec/support/gitlab-git-test.git/objects/eb/a0c153ed20d927bab00507f356043b6b4be31e
new file mode 100644
index 00000000000..2bf27fe5048
--- /dev/null
+++ b/spec/support/gitlab-git-test.git/objects/eb/a0c153ed20d927bab00507f356043b6b4be31e
Binary files differ
diff --git a/spec/support/gitlab-git-test.git/objects/f6/5ad228d96e2a2ae7088e8557fe8906f6dd2b3f b/spec/support/gitlab-git-test.git/objects/f6/5ad228d96e2a2ae7088e8557fe8906f6dd2b3f
new file mode 100644
index 00000000000..8ab8606c6be
--- /dev/null
+++ b/spec/support/gitlab-git-test.git/objects/f6/5ad228d96e2a2ae7088e8557fe8906f6dd2b3f
Binary files differ
diff --git a/spec/support/gitlab_stubs/session.json b/spec/support/gitlab_stubs/session.json
index 688175369ae..658ff5871b0 100644
--- a/spec/support/gitlab_stubs/session.json
+++ b/spec/support/gitlab_stubs/session.json
@@ -14,7 +14,5 @@
"provider":null,
"is_admin":false,
"can_create_group":false,
- "can_create_project":false,
- "private_token":"Wvjy2Krpb7y8xi93owUz",
- "access_token":"Wvjy2Krpb7y8xi93owUz"
+ "can_create_project":false
}
diff --git a/spec/support/gitlab_stubs/user.json b/spec/support/gitlab_stubs/user.json
index ce8dfe5ae75..658ff5871b0 100644
--- a/spec/support/gitlab_stubs/user.json
+++ b/spec/support/gitlab_stubs/user.json
@@ -14,7 +14,5 @@
"provider":null,
"is_admin":false,
"can_create_group":false,
- "can_create_project":false,
- "private_token":"Wvjy2Krpb7y8xi93owUz",
- "access_token":"Wvjy2Krpb7y8xi93owUz"
-} \ No newline at end of file
+ "can_create_project":false
+}
diff --git a/spec/support/google_api/cloud_platform_helpers.rb b/spec/support/google_api/cloud_platform_helpers.rb
new file mode 100644
index 00000000000..4b785611ab5
--- /dev/null
+++ b/spec/support/google_api/cloud_platform_helpers.rb
@@ -0,0 +1,155 @@
+module GoogleApi
+ module CloudPlatformHelpers
+ def stub_google_api_validate_token
+ request.session[GoogleApi::CloudPlatform::Client.session_key_for_token] = 'token'
+ request.session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] = 1.hour.since.to_i.to_s
+ end
+
+ def stub_google_api_expired_token
+ request.session[GoogleApi::CloudPlatform::Client.session_key_for_token] = 'token'
+ request.session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] = 1.hour.ago.to_i.to_s
+ end
+
+ def stub_cloud_platform_get_zone_cluster(project_id, zone, cluster_id, **options)
+ WebMock.stub_request(:get, cloud_platform_get_zone_cluster_url(project_id, zone, cluster_id))
+ .to_return(cloud_platform_response(cloud_platform_cluster_body(options)))
+ end
+
+ def stub_cloud_platform_get_zone_cluster_error(project_id, zone, cluster_id)
+ WebMock.stub_request(:get, cloud_platform_get_zone_cluster_url(project_id, zone, cluster_id))
+ .to_return(status: [500, "Internal Server Error"])
+ end
+
+ def stub_cloud_platform_create_cluster(project_id, zone, **options)
+ WebMock.stub_request(:post, cloud_platform_create_cluster_url(project_id, zone))
+ .to_return(cloud_platform_response(cloud_platform_operation_body(options)))
+ end
+
+ def stub_cloud_platform_create_cluster_error(project_id, zone)
+ WebMock.stub_request(:post, cloud_platform_create_cluster_url(project_id, zone))
+ .to_return(status: [500, "Internal Server Error"])
+ end
+
+ def stub_cloud_platform_get_zone_operation(project_id, zone, operation_id, **options)
+ WebMock.stub_request(:get, cloud_platform_get_zone_operation_url(project_id, zone, operation_id))
+ .to_return(cloud_platform_response(cloud_platform_operation_body(options)))
+ end
+
+ def stub_cloud_platform_get_zone_operation_error(project_id, zone, operation_id)
+ WebMock.stub_request(:get, cloud_platform_get_zone_operation_url(project_id, zone, operation_id))
+ .to_return(status: [500, "Internal Server Error"])
+ end
+
+ def cloud_platform_get_zone_cluster_url(project_id, zone, cluster_id)
+ "https://container.googleapis.com/v1/projects/#{project_id}/zones/#{zone}/clusters/#{cluster_id}"
+ end
+
+ def cloud_platform_create_cluster_url(project_id, zone)
+ "https://container.googleapis.com/v1/projects/#{project_id}/zones/#{zone}/clusters"
+ end
+
+ def cloud_platform_get_zone_operation_url(project_id, zone, operation_id)
+ "https://container.googleapis.com/v1/projects/#{project_id}/zones/#{zone}/operations/#{operation_id}"
+ end
+
+ def cloud_platform_response(body)
+ { status: 200, headers: { 'Content-Type' => 'application/json' }, body: body.to_json }
+ end
+
+ def load_sample_cert
+ pem_file = File.expand_path(Rails.root.join('spec/fixtures/clusters/sample_cert.pem'))
+ Base64.encode64(File.read(pem_file))
+ end
+
+ ##
+ # gcloud container clusters create
+ # https://cloud.google.com/container-engine/reference/rest/v1/projects.zones.clusters/create
+ # rubocop:disable Metrics/CyclomaticComplexity
+ # rubocop:disable Metrics/PerceivedComplexity
+ def cloud_platform_cluster_body(**options)
+ {
+ "name": options[:name] || 'string',
+ "description": options[:description] || 'string',
+ "initialNodeCount": options[:initialNodeCount] || 'number',
+ # "nodeConfig": {,
+ # object(NodeConfig),
+ # },,
+ "masterAuth": {
+ "username": options[:username] || 'string',
+ "password": options[:password] || 'string',
+ # "clientCertificateConfig": {
+ # object(ClientCertificateConfig)
+ # },
+ "clusterCaCertificate": options[:clusterCaCertificate] || load_sample_cert,
+ "clientCertificate": options[:clientCertificate] || 'string',
+ "clientKey": options[:clientKey] || 'string'
+ },
+ "loggingService": options[:loggingService] || 'string',
+ "monitoringService": options[:monitoringService] || 'string',
+ "network": options[:network] || 'string',
+ "clusterIpv4Cidr": options[:clusterIpv4Cidr] || 'string',
+ # "addonsConfig": {,
+ # object(AddonsConfig),
+ # },,
+ "subnetwork": options[:subnetwork] || 'string',
+ # "nodePools": [,
+ # {,
+ # object(NodePool),
+ # },
+ # ],,
+ # "locations": [,
+ # string,
+ # ],,
+ "enableKubernetesAlpha": options[:enableKubernetesAlpha] || 'boolean',
+ # "resourceLabels": {,
+ # string: string,,
+ # ...,
+ # },,
+ "labelFingerprint": options[:labelFingerprint] || 'string',
+ # "legacyAbac": {,
+ # object(LegacyAbac),
+ # },
+ # "networkPolicy": {,
+ # object(NetworkPolicy),
+ # },
+ # "ipAllocationPolicy": {,
+ # object(IPAllocationPolicy),
+ # },
+ # "masterAuthorizedNetworksConfig": {,
+ # object(MasterAuthorizedNetworksConfig),
+ # },
+ "selfLink": options[:selfLink] || 'string',
+ "zone": options[:zone] || 'string',
+ "endpoint": options[:endpoint] || 'string',
+ "initialClusterVersion": options[:initialClusterVersion] || 'string',
+ "currentMasterVersion": options[:currentMasterVersion] || 'string',
+ "currentNodeVersion": options[:currentNodeVersion] || 'string',
+ "createTime": options[:createTime] || 'string',
+ "status": options[:status] || 'RUNNING',
+ "statusMessage": options[:statusMessage] || 'string',
+ "nodeIpv4CidrSize": options[:nodeIpv4CidrSize] || 'number',
+ "servicesIpv4Cidr": options[:servicesIpv4Cidr] || 'string',
+ # "instanceGroupUrls": [,
+ # string,
+ # ],,
+ "currentNodeCount": options[:currentNodeCount] || 'number',
+ "expireTime": options[:expireTime] || 'string'
+ }
+ end
+
+ def cloud_platform_operation_body(**options)
+ {
+ "name": options[:name] || 'operation-1234567891234-1234567',
+ "zone": options[:zone] || 'us-central1-a',
+ "operationType": options[:operationType] || 'CREATE_CLUSTER',
+ "status": options[:status] || 'PENDING',
+ "detail": options[:detail] || 'detail',
+ "statusMessage": options[:statusMessage] || '',
+ "selfLink": options[:selfLink] || 'https://container.googleapis.com/v1/projects/123456789101/zones/us-central1-a/operations/operation-1234567891234-1234567',
+ "targetLink": options[:targetLink] || 'https://container.googleapis.com/v1/projects/123456789101/zones/us-central1-a/clusters/test-cluster',
+ "startTime": options[:startTime] || '2017-09-13T16:49:13.055601589Z',
+ "endTime": options[:endTime] || ''
+ }
+ end
+ end
+end
diff --git a/spec/support/helpers/merge_request_diff_helpers.rb b/spec/support/helpers/merge_request_diff_helpers.rb
index fd22e384b1b..c98aa503ed1 100644
--- a/spec/support/helpers/merge_request_diff_helpers.rb
+++ b/spec/support/helpers/merge_request_diff_helpers.rb
@@ -2,7 +2,7 @@ module MergeRequestDiffHelpers
def click_diff_line(line_holder, diff_side = nil)
line = get_line_components(line_holder, diff_side)
line[:content].hover
- line[:num].find('.add-diff-note').trigger('click')
+ line[:num].find('.add-diff-note', visible: false).send_keys(:return)
end
def get_line_components(line_holder, diff_side = nil)
diff --git a/spec/support/helpers/note_interaction_helpers.rb b/spec/support/helpers/note_interaction_helpers.rb
index 86008698692..79a0aa174b1 100644
--- a/spec/support/helpers/note_interaction_helpers.rb
+++ b/spec/support/helpers/note_interaction_helpers.rb
@@ -2,7 +2,7 @@ module NoteInteractionHelpers
def open_more_actions_dropdown(note)
note_element = find("#note_#{note.id}")
- note_element.find('.more-actions-toggle').trigger('click')
+ note_element.find('.more-actions-toggle').click
note_element.find('.more-actions .dropdown-menu li', match: :first)
end
end
diff --git a/spec/support/input_helper.rb b/spec/support/input_helper.rb
new file mode 100644
index 00000000000..acbb42274ec
--- /dev/null
+++ b/spec/support/input_helper.rb
@@ -0,0 +1,7 @@
+# see app/assets/javascripts/test_utils/simulate_input.js
+
+module InputHelper
+ def simulate_input(selector, input = '')
+ evaluate_script("window.simulateInput(#{selector.to_json}, #{input.to_json});")
+ end
+end
diff --git a/spec/support/inspect_requests.rb b/spec/support/inspect_requests.rb
new file mode 100644
index 00000000000..88ddc5c7f6c
--- /dev/null
+++ b/spec/support/inspect_requests.rb
@@ -0,0 +1,17 @@
+require_relative './wait_for_requests'
+
+module InspectRequests
+ extend self
+ include WaitForRequests
+
+ def inspect_requests(inject_headers: {})
+ Gitlab::Testing::RequestInspectorMiddleware.log_requests!(inject_headers)
+
+ yield
+
+ wait_for_all_requests
+ Gitlab::Testing::RequestInspectorMiddleware.requests
+ ensure
+ Gitlab::Testing::RequestInspectorMiddleware.stop_logging!
+ end
+end
diff --git a/spec/support/kubernetes_helpers.rb b/spec/support/kubernetes_helpers.rb
index c92f78b324c..e46b61b6461 100644
--- a/spec/support/kubernetes_helpers.rb
+++ b/spec/support/kubernetes_helpers.rb
@@ -9,22 +9,51 @@ module KubernetesHelpers
kube_response(kube_pods_body)
end
- def stub_kubeclient_discover
- WebMock.stub_request(:get, service.api_url + '/api/v1').to_return(kube_response(kube_v1_discovery_body))
+ def stub_kubeclient_discover(api_url)
+ WebMock.stub_request(:get, api_url + '/api/v1').to_return(kube_response(kube_v1_discovery_body))
end
def stub_kubeclient_pods(response = nil)
- stub_kubeclient_discover
+ stub_kubeclient_discover(service.api_url)
pods_url = service.api_url + "/api/v1/namespaces/#{service.actual_namespace}/pods"
WebMock.stub_request(:get, pods_url).to_return(response || kube_pods_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)))
+ end
+
+ def stub_kubeclient_get_secrets_error(api_url)
+ WebMock.stub_request(:get, api_url + '/api/v1/secrets')
+ .to_return(status: [404, "Internal Server Error"])
+ end
+
+ def kube_v1_secrets_body(**options)
+ {
+ "kind" => "SecretList",
+ "apiVersion": "v1",
+ "items" => [
+ {
+ "metadata": {
+ "name": options[:metadata_name] || "default-token-1",
+ "namespace": "kube-system"
+ },
+ "data": {
+ "token": options[:token] || Base64.encode64('token-sample-123')
+ }
+ }
+ ]
+ }
+ end
+
def kube_v1_discovery_body
{
"kind" => "APIResourceList",
"resources" => [
- { "name" => "pods", "namespaced" => true, "kind" => "Pod" }
+ { "name" => "pods", "namespaced" => true, "kind" => "Pod" },
+ { "name" => "secrets", "namespaced" => true, "kind" => "Secret" }
]
}
end
diff --git a/spec/support/live_debugger.rb b/spec/support/live_debugger.rb
new file mode 100644
index 00000000000..911eb48a8ca
--- /dev/null
+++ b/spec/support/live_debugger.rb
@@ -0,0 +1,17 @@
+require 'io/console'
+
+module LiveDebugger
+ def live_debug
+ puts
+ puts "Current example is paused for live debugging."
+ puts "Opening #{current_url} in your default browser..."
+ puts "The current user credentials are: #{@current_user.username} / #{@current_user.password}" if @current_user
+ puts "Press any key to resume the execution of the example!!"
+
+ `open #{current_url}`
+
+ loop until $stdin.getch
+
+ puts "Back to the example!"
+ end
+end
diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb
index 4aed40bf22d..50702a0ac88 100644
--- a/spec/support/login_helpers.rb
+++ b/spec/support/login_helpers.rb
@@ -3,6 +3,21 @@ require_relative 'devise_helpers'
module LoginHelpers
include DeviseHelpers
+ # Overriding Devise::Test::IntegrationHelpers#sign_in to store @current_user
+ # since we may need it in LiveDebugger#live_debug.
+ def sign_in(resource, scope: nil)
+ super
+
+ @current_user = resource
+ end
+
+ # Overriding Devise::Test::IntegrationHelpers#sign_out to clear @current_user.
+ def sign_out(resource_or_scope)
+ super
+
+ @current_user = nil
+ end
+
# Internal: Log in as a specific user or a new user of a specific role
#
# user_or_role - User object, or a role to create (e.g., :admin, :user)
@@ -28,7 +43,7 @@ module LoginHelpers
gitlab_sign_in_with(user, **kwargs)
- user
+ @current_user = user
end
def gitlab_sign_in_via(provider, user, uid)
@@ -41,6 +56,7 @@ module LoginHelpers
def gitlab_sign_out
find(".header-user-dropdown-toggle").click
click_link "Sign out"
+ @current_user = nil
expect(page).to have_button('Sign in')
end
diff --git a/spec/support/mobile_helpers.rb b/spec/support/mobile_helpers.rb
index 431f20a2a5c..3b9eb84e824 100644
--- a/spec/support/mobile_helpers.rb
+++ b/spec/support/mobile_helpers.rb
@@ -12,6 +12,6 @@ module MobileHelpers
end
def resize_window(width, height)
- page.driver.resize_window width, height
+ Capybara.current_session.current_window.resize_to(width, height)
end
end
diff --git a/spec/support/protected_tags/access_control_ce_shared_examples.rb b/spec/support/protected_tags/access_control_ce_shared_examples.rb
index 421a51fc336..2770cdcbefc 100644
--- a/spec/support/protected_tags/access_control_ce_shared_examples.rb
+++ b/spec/support/protected_tags/access_control_ce_shared_examples.rb
@@ -9,7 +9,7 @@ RSpec.shared_examples "protected tags > access control > CE" do
allowed_to_create_button = find(".js-allowed-to-create")
unless allowed_to_create_button.text == access_type_name
- allowed_to_create_button.trigger('click')
+ allowed_to_create_button.click
find('.create_access_levels-container .dropdown-menu li', match: :first)
within('.create_access_levels-container .dropdown-menu') { click_on access_type_name }
end
diff --git a/spec/support/quick_actions_helpers.rb b/spec/support/quick_actions_helpers.rb
index d2aaae7518f..361190aa352 100644
--- a/spec/support/quick_actions_helpers.rb
+++ b/spec/support/quick_actions_helpers.rb
@@ -3,7 +3,7 @@ module QuickActionsHelpers
Sidekiq::Testing.fake! do
page.within('.js-main-target-form') do
fill_in 'note[note]', with: text
- find('.js-comment-submit-button').trigger('click')
+ find('.js-comment-submit-button').click
end
end
end
diff --git a/spec/support/shared_examples/features/protected_branches_access_control_ce.rb b/spec/support/shared_examples/features/protected_branches_access_control_ce.rb
index d5bc12f3bc5..5fde91512da 100644
--- a/spec/support/shared_examples/features/protected_branches_access_control_ce.rb
+++ b/spec/support/shared_examples/features/protected_branches_access_control_ce.rb
@@ -9,7 +9,7 @@ shared_examples "protected branches > access control > CE" do
allowed_to_push_button = find(".js-allowed-to-push")
unless allowed_to_push_button.text == access_type_name
- allowed_to_push_button.trigger('click')
+ allowed_to_push_button.click
within(".dropdown.open .dropdown-menu") { click_on access_type_name }
end
end
@@ -34,7 +34,7 @@ shared_examples "protected branches > access control > CE" do
within('.js-allowed-to-push-container') do
expect(first("li")).to have_content("Roles")
- click_on access_type_name
+ find(:link, access_type_name).click
end
end
@@ -79,7 +79,7 @@ shared_examples "protected branches > access control > CE" do
within('.js-allowed-to-merge-container') do
expect(first("li")).to have_content("Roles")
- click_on access_type_name
+ find(:link, access_type_name).click
end
end
diff --git a/spec/support/stub_configuration.rb b/spec/support/stub_configuration.rb
index 4d448a55978..4ead78529c3 100644
--- a/spec/support/stub_configuration.rb
+++ b/spec/support/stub_configuration.rb
@@ -38,6 +38,10 @@ module StubConfiguration
allow(Gitlab.config.backup).to receive_messages(to_settings(messages))
end
+ def stub_lfs_setting(messages)
+ allow(Gitlab.config.lfs).to receive_messages(to_settings(messages))
+ end
+
def stub_storage_settings(messages)
# Default storage is always required
messages['default'] ||= Gitlab.config.repositories.storages.default
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index a27bfdee3d2..fff120fcb88 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -182,6 +182,8 @@ module TestEnv
return unless @gitaly_pid
Process.kill('KILL', @gitaly_pid)
+ rescue Errno::ESRCH
+ # The process can already be gone if the test run was INTerrupted.
end
def setup_factory_repo
diff --git a/spec/support/time_tracking_shared_examples.rb b/spec/support/time_tracking_shared_examples.rb
index 0fa74f911f6..909d4e2ee8d 100644
--- a/spec/support/time_tracking_shared_examples.rb
+++ b/spec/support/time_tracking_shared_examples.rb
@@ -80,6 +80,6 @@ end
def submit_time(quick_action)
fill_in 'note[note]', with: quick_action
- find('.js-comment-submit-button').trigger('click')
+ find('.js-comment-submit-button').click
wait_for_requests
end
diff --git a/spec/support/update_invalid_issuable.rb b/spec/support/update_invalid_issuable.rb
index 50a1d4a56e2..1490287681b 100644
--- a/spec/support/update_invalid_issuable.rb
+++ b/spec/support/update_invalid_issuable.rb
@@ -25,13 +25,11 @@ shared_examples 'update invalid issuable' do |klass|
.and_raise(ActiveRecord::StaleObjectError.new(issuable, :save))
end
- if klass == MergeRequest
- it 'renders edit when format is html' do
- put :update, params
+ it 'renders edit when format is html' do
+ put :update, params
- expect(response).to render_template(:edit)
- expect(assigns[:conflict]).to be_truthy
- end
+ expect(response).to render_template(:edit)
+ expect(assigns[:conflict]).to be_truthy
end
it 'renders json error message when format is json' do
@@ -44,17 +42,16 @@ shared_examples 'update invalid issuable' do |klass|
end
end
- if klass == MergeRequest
- context 'when updating an invalid issuable' do
- before do
- params[:merge_request][:title] = ""
- end
+ context 'when updating an invalid issuable' do
+ before do
+ key = klass == Issue ? :issue : :merge_request
+ params[key][:title] = ""
+ end
- it 'renders edit when merge request is invalid' do
- put :update, params
+ it 'renders edit when merge request is invalid' do
+ put :update, params
- expect(response).to render_template(:edit)
- end
+ expect(response).to render_template(:edit)
end
end
end
diff --git a/spec/support/wait_for_requests.rb b/spec/support/wait_for_requests.rb
index b5c3c0f55b8..f4130d68271 100644
--- a/spec/support/wait_for_requests.rb
+++ b/spec/support/wait_for_requests.rb
@@ -1,25 +1,47 @@
-require_relative './wait_for_requests'
-
module WaitForRequests
extend self
# This is inspired by http://www.salsify.com/blog/engineering/tearing-capybara-ajax-tests
def block_and_wait_for_requests_complete
+ block_requests { wait_for_all_requests }
+ end
+
+ # Block all requests inside block with 503 response
+ def block_requests
Gitlab::Testing::RequestBlockerMiddleware.block_requests!
- wait_for('pending requests complete') do
- Gitlab::Testing::RequestBlockerMiddleware.num_active_requests.zero? && finished_all_requests?
- end
+ yield
ensure
Gitlab::Testing::RequestBlockerMiddleware.allow_requests!
end
+ # Slow down requests inside block by injecting `sleep 0.2` before each response
+ def slow_requests
+ Gitlab::Testing::RequestBlockerMiddleware.slow_requests!
+ yield
+ ensure
+ Gitlab::Testing::RequestBlockerMiddleware.allow_requests!
+ end
+
+ # Wait for client-side AJAX requests
def wait_for_requests
- wait_for('JS requests') { finished_all_requests? }
+ wait_for('JS requests complete') { finished_all_js_requests? }
+ end
+
+ # Wait for active Rack requests and client-side AJAX requests
+ def wait_for_all_requests
+ wait_for('pending requests complete') do
+ finished_all_rack_reqiests? &&
+ finished_all_js_requests?
+ end
end
private
- def finished_all_requests?
+ def finished_all_rack_reqiests?
+ Gitlab::Testing::RequestBlockerMiddleware.num_active_requests.zero?
+ end
+
+ def finished_all_js_requests?
return true unless javascript_test?
finished_all_ajax_requests? &&
diff --git a/spec/tasks/gitlab/users_rake_spec.rb b/spec/tasks/gitlab/users_rake_spec.rb
deleted file mode 100644
index 972670e7f91..00000000000
--- a/spec/tasks/gitlab/users_rake_spec.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-require 'spec_helper'
-require 'rake'
-
-describe 'gitlab:users namespace rake task' do
- let(:enable_registry) { true }
-
- before :all do
- Rake.application.rake_require 'tasks/gitlab/helpers'
- Rake.application.rake_require 'tasks/gitlab/users'
-
- # empty task as env is already loaded
- Rake::Task.define_task :environment
- end
-
- def run_rake_task(task_name)
- Rake::Task[task_name].reenable
- Rake.application.invoke_task task_name
- end
-
- describe 'clear_all_authentication_tokens' do
- before do
- # avoid writing task output to spec progress
- allow($stdout).to receive :write
- end
-
- context 'gitlab version' do
- it 'clears the authentication token for all users' do
- create_list(:user, 2)
-
- expect(User.pluck(:authentication_token)).to all(be_present)
-
- run_rake_task('gitlab:users:clear_all_authentication_tokens')
-
- expect(User.pluck(:authentication_token)).to all(be_nil)
- end
- end
- end
-end
diff --git a/spec/tasks/tokens_spec.rb b/spec/tasks/tokens_spec.rb
index b84137eb365..51f7a536cbb 100644
--- a/spec/tasks/tokens_spec.rb
+++ b/spec/tasks/tokens_spec.rb
@@ -7,12 +7,6 @@ describe 'tokens rake tasks' do
Rake.application.rake_require 'tasks/tokens'
end
- describe 'reset_all task' do
- it 'invokes create_hooks task' do
- expect { run_rake_task('tokens:reset_all_auth') }.to change { user.reload.authentication_token }
- end
- end
-
describe 'reset_all_email task' do
it 'invokes create_hooks task' do
expect { run_rake_task('tokens:reset_all_email') }.to change { user.reload.incoming_email_token }
diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb
index 2492d56a5cf..f52b2bab05b 100644
--- a/spec/uploaders/file_uploader_spec.rb
+++ b/spec/uploaders/file_uploader_spec.rb
@@ -3,25 +3,51 @@ require 'spec_helper'
describe FileUploader do
let(:uploader) { described_class.new(build_stubbed(:project)) }
- describe '.absolute_path' do
- it 'returns the correct absolute path by building it dynamically' do
- project = build_stubbed(:project)
- upload = double(model: project, path: 'secret/foo.jpg')
+ context 'legacy storage' do
+ let(:project) { build_stubbed(:project) }
- dynamic_segment = project.path_with_namespace
+ describe '.absolute_path' do
+ it 'returns the correct absolute path by building it dynamically' do
+ upload = double(model: project, path: 'secret/foo.jpg')
- expect(described_class.absolute_path(upload))
- .to end_with("#{dynamic_segment}/secret/foo.jpg")
+ dynamic_segment = project.full_path
+
+ expect(described_class.absolute_path(upload))
+ .to end_with("#{dynamic_segment}/secret/foo.jpg")
+ end
+ end
+
+ describe "#store_dir" do
+ it "stores in the namespace path" do
+ uploader = described_class.new(project)
+
+ expect(uploader.store_dir).to include(project.full_path)
+ expect(uploader.store_dir).not_to include("system")
+ end
end
end
- describe "#store_dir" do
- it "stores in the namespace path" do
- project = build_stubbed(:project)
- uploader = described_class.new(project)
+ context 'hashed storage' do
+ let(:project) { build_stubbed(:project, :hashed) }
+
+ describe '.absolute_path' do
+ it 'returns the correct absolute path by building it dynamically' do
+ upload = double(model: project, path: 'secret/foo.jpg')
+
+ dynamic_segment = project.disk_path
+
+ expect(described_class.absolute_path(upload))
+ .to end_with("#{dynamic_segment}/secret/foo.jpg")
+ end
+ end
+
+ describe "#store_dir" do
+ it "stores in the namespace path" do
+ uploader = described_class.new(project)
- expect(uploader.store_dir).to include(project.path_with_namespace)
- expect(uploader.store_dir).not_to include("system")
+ expect(uploader.store_dir).to include(project.disk_path)
+ expect(uploader.store_dir).not_to include("system")
+ end
end
end
diff --git a/spec/views/shared/issuable/_participants.html.haml.rb b/spec/views/shared/issuable/_participants.html.haml.rb
deleted file mode 100644
index 51059d4c0d7..00000000000
--- a/spec/views/shared/issuable/_participants.html.haml.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-require 'spec_helper'
-require 'nokogiri'
-
-describe 'shared/issuable/_participants.html.haml' do
- let(:project) { create(:project) }
- let(:participants) { create_list(:user, 100) }
-
- before do
- allow(view).to receive_messages(project: project,
- participants: participants)
- end
-
- it 'renders lazy loaded avatars' do
- render 'shared/issuable/participants'
-
- html = Nokogiri::HTML(rendered)
-
- avatars = html.css('.participants-author img')
-
- avatars.each do |avatar|
- expect(avatar[:class]).to include('lazy')
- expect(avatar[:src]).to eql(LazyImageTagHelper.placeholder_image)
- expect(avatar[:"data-src"]).to match('http://www.gravatar.com/avatar/')
- end
- end
-end
diff --git a/spec/workers/cluster_provision_worker_spec.rb b/spec/workers/cluster_provision_worker_spec.rb
index 11f208289db..85c7dc20ede 100644
--- a/spec/workers/cluster_provision_worker_spec.rb
+++ b/spec/workers/cluster_provision_worker_spec.rb
@@ -2,11 +2,22 @@ require 'spec_helper'
describe ClusterProvisionWorker do
describe '#perform' do
- context 'when cluster exists' do
- let(:cluster) { create(:gcp_cluster) }
+ context 'when provider type is gcp' do
+ let(:cluster) { create(:cluster, provider_type: :gcp, provider_gcp: provider) }
+ let(:provider) { create(:provider_gcp, :scheduled) }
it 'provision a cluster' do
- expect_any_instance_of(Ci::ProvisionClusterService).to receive(:execute)
+ expect_any_instance_of(Clusters::Gcp::ProvisionService).to receive(:execute)
+
+ described_class.new.perform(cluster.id)
+ end
+ end
+
+ context 'when provider type is user' do
+ let(:cluster) { create(:cluster, provider_type: :user) }
+
+ it 'does not provision a cluster' do
+ expect_any_instance_of(Clusters::Gcp::ProvisionService).not_to receive(:execute)
described_class.new.perform(cluster.id)
end
@@ -14,7 +25,7 @@ describe ClusterProvisionWorker do
context 'when cluster does not exist' do
it 'does not provision a cluster' do
- expect_any_instance_of(Ci::ProvisionClusterService).not_to receive(:execute)
+ expect_any_instance_of(Clusters::Gcp::ProvisionService).not_to receive(:execute)
described_class.new.perform(123)
end
diff --git a/spec/workers/stuck_merge_jobs_worker_spec.rb b/spec/workers/stuck_merge_jobs_worker_spec.rb
index a5ad78393c9..f8b55e873df 100644
--- a/spec/workers/stuck_merge_jobs_worker_spec.rb
+++ b/spec/workers/stuck_merge_jobs_worker_spec.rb
@@ -12,8 +12,13 @@ describe StuckMergeJobsWorker do
worker.perform
- expect(mr_with_sha.reload).to be_merged
- expect(mr_without_sha.reload).to be_opened
+ mr_with_sha.reload
+ mr_without_sha.reload
+
+ expect(mr_with_sha).to be_merged
+ expect(mr_without_sha).to be_opened
+ expect(mr_with_sha.merge_jid).to be_present
+ expect(mr_without_sha.merge_jid).to be_nil
end
it 'updates merge request to opened when locked but has not been merged' do
diff --git a/spec/workers/wait_for_cluster_creation_worker_spec.rb b/spec/workers/wait_for_cluster_creation_worker_spec.rb
index dcd4a3b9aec..29812408396 100644
--- a/spec/workers/wait_for_cluster_creation_worker_spec.rb
+++ b/spec/workers/wait_for_cluster_creation_worker_spec.rb
@@ -2,65 +2,32 @@ require 'spec_helper'
describe WaitForClusterCreationWorker do
describe '#perform' do
- context 'when cluster exists' do
- let(:cluster) { create(:gcp_cluster) }
- let(:operation) { double }
+ context 'when provider type is gcp' do
+ let(:cluster) { create(:cluster, provider_type: :gcp, provider_gcp: provider) }
+ let(:provider) { create(:provider_gcp, :creating) }
- before do
- allow(operation).to receive(:status).and_return(status)
- allow(operation).to receive(:start_time).and_return(1.minute.ago)
- allow(operation).to receive(:status_message).and_return('error')
- allow_any_instance_of(Ci::FetchGcpOperationService).to receive(:execute).and_yield(operation)
- end
-
- context 'when operation status is RUNNING' do
- let(:status) { 'RUNNING' }
-
- it 'reschedules worker' do
- expect(described_class).to receive(:perform_in)
-
- described_class.new.perform(cluster.id)
- end
-
- context 'when operation timeout' do
- before do
- allow(operation).to receive(:start_time).and_return(30.minutes.ago.utc)
- end
-
- it 'sets an error message on cluster' do
- described_class.new.perform(cluster.id)
+ it 'provision a cluster' do
+ expect_any_instance_of(Clusters::Gcp::VerifyProvisionStatusService).to receive(:execute)
- expect(cluster.reload).to be_errored
- end
- end
- end
-
- context 'when operation status is DONE' do
- let(:status) { 'DONE' }
-
- it 'finalizes cluster creation' do
- expect_any_instance_of(Ci::FinalizeClusterCreationService).to receive(:execute)
-
- described_class.new.perform(cluster.id)
- end
+ described_class.new.perform(cluster.id)
end
+ end
- context 'when operation status is others' do
- let(:status) { 'others' }
+ context 'when provider type is user' do
+ let(:cluster) { create(:cluster, provider_type: :user) }
- it 'sets an error message on cluster' do
- described_class.new.perform(cluster.id)
+ it 'does not provision a cluster' do
+ expect_any_instance_of(Clusters::Gcp::VerifyProvisionStatusService).not_to receive(:execute)
- expect(cluster.reload).to be_errored
- end
+ described_class.new.perform(cluster.id)
end
end
context 'when cluster does not exist' do
it 'does not provision a cluster' do
- expect_any_instance_of(Ci::FetchGcpOperationService).not_to receive(:execute)
+ expect_any_instance_of(Clusters::Gcp::VerifyProvisionStatusService).not_to receive(:execute)
- described_class.new.perform(1234)
+ described_class.new.perform(123)
end
end
end
diff --git a/vendor/assets/javascripts/autosize.js b/vendor/assets/javascripts/autosize.js
deleted file mode 100644
index cfa49e72c50..00000000000
--- a/vendor/assets/javascripts/autosize.js
+++ /dev/null
@@ -1,243 +0,0 @@
-/*!
- Autosize 3.0.14
- license: MIT
- http://www.jacklmoore.com/autosize
-*/
-(function (global, factory) {
- if (typeof define === 'function' && define.amd) {
- define(['exports', 'module'], factory);
- } else if (typeof exports !== 'undefined' && typeof module !== 'undefined') {
- factory(exports, module);
- } else {
- var mod = {
- exports: {}
- };
- factory(mod.exports, mod);
- global.autosize = mod.exports;
- }
-})(this, function (exports, module) {
- 'use strict';
-
- var set = typeof Set === 'function' ? new Set() : (function () {
- var list = [];
-
- return {
- has: function has(key) {
- return Boolean(list.indexOf(key) > -1);
- },
- add: function add(key) {
- list.push(key);
- },
- 'delete': function _delete(key) {
- list.splice(list.indexOf(key), 1);
- } };
- })();
-
- function assign(ta) {
- var _ref = arguments[1] === undefined ? {} : arguments[1];
-
- var _ref$setOverflowX = _ref.setOverflowX;
- var setOverflowX = _ref$setOverflowX === undefined ? true : _ref$setOverflowX;
- var _ref$setOverflowY = _ref.setOverflowY;
- var setOverflowY = _ref$setOverflowY === undefined ? true : _ref$setOverflowY;
-
- if (!ta || !ta.nodeName || ta.nodeName !== 'TEXTAREA' || set.has(ta)) return;
-
- var heightOffset = null;
- var overflowY = null;
- var clientWidth = ta.clientWidth;
-
- function init() {
- var style = window.getComputedStyle(ta, null);
-
- overflowY = style.overflowY;
-
- if (style.resize === 'vertical') {
- ta.style.resize = 'none';
- } else if (style.resize === 'both') {
- ta.style.resize = 'horizontal';
- }
-
- if (style.boxSizing === 'content-box') {
- heightOffset = -(parseFloat(style.paddingTop) + parseFloat(style.paddingBottom));
- } else {
- heightOffset = parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth);
- }
- // Fix when a textarea is not on document body and heightOffset is Not a Number
- if (isNaN(heightOffset)) {
- heightOffset = 0;
- }
-
- update();
- }
-
- function changeOverflow(value) {
- {
- // Chrome/Safari-specific fix:
- // When the textarea y-overflow is hidden, Chrome/Safari do not reflow the text to account for the space
- // made available by removing the scrollbar. The following forces the necessary text reflow.
- var width = ta.style.width;
- ta.style.width = '0px';
- // Force reflow:
- /* jshint ignore:start */
- ta.offsetWidth;
- /* jshint ignore:end */
- ta.style.width = width;
- }
-
- overflowY = value;
-
- if (setOverflowY) {
- ta.style.overflowY = value;
- }
-
- resize();
- }
-
- function resize() {
- var htmlTop = window.pageYOffset;
- var bodyTop = document.body.scrollTop;
- var originalHeight = ta.style.height;
-
- ta.style.height = 'auto';
-
- var endHeight = ta.scrollHeight + heightOffset;
-
- if (ta.scrollHeight === 0) {
- // If the scrollHeight is 0, then the element probably has display:none or is detached from the DOM.
- ta.style.height = originalHeight;
- return;
- }
-
- ta.style.height = endHeight + 'px';
-
- // used to check if an update is actually necessary on window.resize
- clientWidth = ta.clientWidth;
-
- // prevents scroll-position jumping
- document.documentElement.scrollTop = htmlTop;
- document.body.scrollTop = bodyTop;
- }
-
- function update() {
- var startHeight = ta.style.height;
-
- resize();
-
- var style = window.getComputedStyle(ta, null);
-
- if (style.height !== ta.style.height) {
- if (overflowY !== 'visible') {
- changeOverflow('visible');
- }
- } else {
- if (overflowY !== 'hidden') {
- changeOverflow('hidden');
- }
- }
-
- if (startHeight !== ta.style.height) {
- var evt = document.createEvent('Event');
- evt.initEvent('autosize:resized', true, false);
- ta.dispatchEvent(evt);
- }
- }
-
- var pageResize = function pageResize() {
- if (ta.clientWidth !== clientWidth) {
- update();
- }
- };
-
- var destroy = (function (style) {
- window.removeEventListener('resize', pageResize, false);
- ta.removeEventListener('input', update, false);
- ta.removeEventListener('keyup', update, false);
- ta.removeEventListener('autosize:destroy', destroy, false);
- ta.removeEventListener('autosize:update', update, false);
- set['delete'](ta);
-
- Object.keys(style).forEach(function (key) {
- ta.style[key] = style[key];
- });
- }).bind(ta, {
- height: ta.style.height,
- resize: ta.style.resize,
- overflowY: ta.style.overflowY,
- overflowX: ta.style.overflowX,
- wordWrap: ta.style.wordWrap });
-
- ta.addEventListener('autosize:destroy', destroy, false);
-
- // IE9 does not fire onpropertychange or oninput for deletions,
- // so binding to onkeyup to catch most of those events.
- // There is no way that I know of to detect something like 'cut' in IE9.
- if ('onpropertychange' in ta && 'oninput' in ta) {
- ta.addEventListener('keyup', update, false);
- }
-
- window.addEventListener('resize', pageResize, false);
- ta.addEventListener('input', update, false);
- ta.addEventListener('autosize:update', update, false);
- set.add(ta);
-
- if (setOverflowX) {
- ta.style.overflowX = 'hidden';
- ta.style.wordWrap = 'break-word';
- }
-
- init();
- }
-
- function destroy(ta) {
- if (!(ta && ta.nodeName && ta.nodeName === 'TEXTAREA')) return;
- var evt = document.createEvent('Event');
- evt.initEvent('autosize:destroy', true, false);
- ta.dispatchEvent(evt);
- }
-
- function update(ta) {
- if (!(ta && ta.nodeName && ta.nodeName === 'TEXTAREA')) return;
- var evt = document.createEvent('Event');
- evt.initEvent('autosize:update', true, false);
- ta.dispatchEvent(evt);
- }
-
- var autosize = null;
-
- // Do nothing in Node.js environment and IE8 (or lower)
- if (typeof window === 'undefined' || typeof window.getComputedStyle !== 'function') {
- autosize = function (el) {
- return el;
- };
- autosize.destroy = function (el) {
- return el;
- };
- autosize.update = function (el) {
- return el;
- };
- } else {
- autosize = function (el, options) {
- if (el) {
- Array.prototype.forEach.call(el.length ? el : [el], function (x) {
- return assign(x, options);
- });
- }
- return el;
- };
- autosize.destroy = function (el) {
- if (el) {
- Array.prototype.forEach.call(el.length ? el : [el], destroy);
- }
- return el;
- };
- autosize.update = function (el) {
- if (el) {
- Array.prototype.forEach.call(el.length ? el : [el], update);
- }
- return el;
- };
- }
-
- module.exports = autosize;
-}); \ No newline at end of file
diff --git a/vendor/assets/javascripts/fuzzaldrin-plus.js b/vendor/assets/javascripts/fuzzaldrin-plus.js
deleted file mode 100644
index 1985e3f8f6c..00000000000
--- a/vendor/assets/javascripts/fuzzaldrin-plus.js
+++ /dev/null
@@ -1,1161 +0,0 @@
-/*!
- * fuzzaldrin-plus.js - 0.3.1
- * https://github.com/jeancroy/fuzzaldrin-plus
- *
- * Copyright 2016 - Jean Christophe Roy
- * Released under the MIT license
- * https://github.com/jeancroy/fuzzaldrin-plus/raw/master/LICENSE.md
- */
-(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
-fuzzaldrinPlus = require('fuzzaldrin-plus');
-
-},{"fuzzaldrin-plus":3}],2:[function(require,module,exports){
-(function() {
- var PathSeparator, legacy_scorer, pluckCandidates, scorer, sortCandidates;
-
- scorer = require('./scorer');
-
- legacy_scorer = require('./legacy');
-
- pluckCandidates = function(a) {
- return a.candidate;
- };
-
- sortCandidates = function(a, b) {
- return b.score - a.score;
- };
-
- PathSeparator = require('path').sep;
-
- module.exports = function(candidates, query, _arg) {
- var allowErrors, bAllowErrors, bKey, candidate, coreQuery, key, legacy, maxInners, maxResults, prepQuery, queryHasSlashes, score, scoredCandidates, spotLeft, string, _i, _j, _len, _len1, _ref;
- _ref = _arg != null ? _arg : {}, key = _ref.key, maxResults = _ref.maxResults, maxInners = _ref.maxInners, allowErrors = _ref.allowErrors, legacy = _ref.legacy;
- scoredCandidates = [];
- spotLeft = (maxInners != null) && maxInners > 0 ? maxInners : candidates.length;
- bAllowErrors = !!allowErrors;
- bKey = key != null;
- prepQuery = scorer.prepQuery(query);
- if (!legacy) {
- for (_i = 0, _len = candidates.length; _i < _len; _i++) {
- candidate = candidates[_i];
- string = bKey ? candidate[key] : candidate;
- if (!string) {
- continue;
- }
- score = scorer.score(string, query, prepQuery, bAllowErrors);
- if (score > 0) {
- scoredCandidates.push({
- candidate: candidate,
- score: score
- });
- if (!--spotLeft) {
- break;
- }
- }
- }
- } else {
- queryHasSlashes = prepQuery.depth > 0;
- coreQuery = prepQuery.core;
- for (_j = 0, _len1 = candidates.length; _j < _len1; _j++) {
- candidate = candidates[_j];
- string = key != null ? candidate[key] : candidate;
- if (!string) {
- continue;
- }
- score = legacy_scorer.score(string, coreQuery, queryHasSlashes);
- if (!queryHasSlashes) {
- score = legacy_scorer.basenameScore(string, coreQuery, score);
- }
- if (score > 0) {
- scoredCandidates.push({
- candidate: candidate,
- score: score
- });
- }
- }
- }
- scoredCandidates.sort(sortCandidates);
- candidates = scoredCandidates.map(pluckCandidates);
- if (maxResults != null) {
- candidates = candidates.slice(0, maxResults);
- }
- return candidates;
- };
-
-}).call(this);
-
-},{"./legacy":4,"./scorer":6,"path":7}],3:[function(require,module,exports){
-(function() {
- var PathSeparator, filter, legacy_scorer, matcher, prepQueryCache, scorer;
-
- scorer = require('./scorer');
-
- legacy_scorer = require('./legacy');
-
- filter = require('./filter');
-
- matcher = require('./matcher');
-
- PathSeparator = require('path').sep;
-
- prepQueryCache = null;
-
- module.exports = {
- filter: function(candidates, query, options) {
- if (!((query != null ? query.length : void 0) && (candidates != null ? candidates.length : void 0))) {
- return [];
- }
- return filter(candidates, query, options);
- },
- prepQuery: function(query) {
- return scorer.prepQuery(query);
- },
- score: function(string, query, prepQuery, _arg) {
- var allowErrors, coreQuery, legacy, queryHasSlashes, score, _ref;
- _ref = _arg != null ? _arg : {}, allowErrors = _ref.allowErrors, legacy = _ref.legacy;
- if (!((string != null ? string.length : void 0) && (query != null ? query.length : void 0))) {
- return 0;
- }
- if (prepQuery == null) {
- prepQuery = prepQueryCache && prepQueryCache.query === query ? prepQueryCache : (prepQueryCache = scorer.prepQuery(query));
- }
- if (!legacy) {
- score = scorer.score(string, query, prepQuery, !!allowErrors);
- } else {
- queryHasSlashes = prepQuery.depth > 0;
- coreQuery = prepQuery.core;
- score = legacy_scorer.score(string, coreQuery, queryHasSlashes);
- if (!queryHasSlashes) {
- score = legacy_scorer.basenameScore(string, coreQuery, score);
- }
- }
- return score;
- },
- match: function(string, query, prepQuery, _arg) {
- var allowErrors, baseMatches, matches, query_lw, string_lw, _i, _ref, _results;
- allowErrors = (_arg != null ? _arg : {}).allowErrors;
- if (!string) {
- return [];
- }
- if (!query) {
- return [];
- }
- if (string === query) {
- return (function() {
- _results = [];
- for (var _i = 0, _ref = string.length; 0 <= _ref ? _i < _ref : _i > _ref; 0 <= _ref ? _i++ : _i--){ _results.push(_i); }
- return _results;
- }).apply(this);
- }
- if (prepQuery == null) {
- prepQuery = prepQueryCache && prepQueryCache.query === query ? prepQueryCache : (prepQueryCache = scorer.prepQuery(query));
- }
- if (!(allowErrors || scorer.isMatch(string, prepQuery.core_lw, prepQuery.core_up))) {
- return [];
- }
- string_lw = string.toLowerCase();
- query_lw = prepQuery.query_lw;
- matches = matcher.match(string, string_lw, prepQuery);
- if (matches.length === 0) {
- return matches;
- }
- if (string.indexOf(PathSeparator) > -1) {
- baseMatches = matcher.basenameMatch(string, string_lw, prepQuery);
- matches = matcher.mergeMatches(matches, baseMatches);
- }
- return matches;
- }
- };
-
-}).call(this);
-
-},{"./filter":2,"./legacy":4,"./matcher":5,"./scorer":6,"path":7}],4:[function(require,module,exports){
-(function() {
- var PathSeparator, queryIsLastPathSegment;
-
- PathSeparator = require('path').sep;
-
- exports.basenameScore = function(string, query, score) {
- var base, depth, index, lastCharacter, segmentCount, slashCount;
- index = string.length - 1;
- while (string[index] === PathSeparator) {
- index--;
- }
- slashCount = 0;
- lastCharacter = index;
- base = null;
- while (index >= 0) {
- if (string[index] === PathSeparator) {
- slashCount++;
- if (base == null) {
- base = string.substring(index + 1, lastCharacter + 1);
- }
- } else if (index === 0) {
- if (lastCharacter < string.length - 1) {
- if (base == null) {
- base = string.substring(0, lastCharacter + 1);
- }
- } else {
- if (base == null) {
- base = string;
- }
- }
- }
- index--;
- }
- if (base === string) {
- score *= 2;
- } else if (base) {
- score += exports.score(base, query);
- }
- segmentCount = slashCount + 1;
- depth = Math.max(1, 10 - segmentCount);
- score *= depth * 0.01;
- return score;
- };
-
- exports.score = function(string, query) {
- var character, characterScore, indexInQuery, indexInString, lowerCaseIndex, minIndex, queryLength, queryScore, stringLength, totalCharacterScore, upperCaseIndex, _ref;
- if (string === query) {
- return 1;
- }
- if (queryIsLastPathSegment(string, query)) {
- return 1;
- }
- totalCharacterScore = 0;
- queryLength = query.length;
- stringLength = string.length;
- indexInQuery = 0;
- indexInString = 0;
- while (indexInQuery < queryLength) {
- character = query[indexInQuery++];
- lowerCaseIndex = string.indexOf(character.toLowerCase());
- upperCaseIndex = string.indexOf(character.toUpperCase());
- minIndex = Math.min(lowerCaseIndex, upperCaseIndex);
- if (minIndex === -1) {
- minIndex = Math.max(lowerCaseIndex, upperCaseIndex);
- }
- indexInString = minIndex;
- if (indexInString === -1) {
- return 0;
- }
- characterScore = 0.1;
- if (string[indexInString] === character) {
- characterScore += 0.1;
- }
- if (indexInString === 0 || string[indexInString - 1] === PathSeparator) {
- characterScore += 0.8;
- } else if ((_ref = string[indexInString - 1]) === '-' || _ref === '_' || _ref === ' ') {
- characterScore += 0.7;
- }
- string = string.substring(indexInString + 1, stringLength);
- totalCharacterScore += characterScore;
- }
- queryScore = totalCharacterScore / queryLength;
- return ((queryScore * (queryLength / stringLength)) + queryScore) / 2;
- };
-
- queryIsLastPathSegment = function(string, query) {
- if (string[string.length - query.length - 1] === PathSeparator) {
- return string.lastIndexOf(query) === string.length - query.length;
- }
- };
-
- exports.match = function(string, query, stringOffset) {
- var character, indexInQuery, indexInString, lowerCaseIndex, matches, minIndex, queryLength, stringLength, upperCaseIndex, _i, _ref, _results;
- if (stringOffset == null) {
- stringOffset = 0;
- }
- if (string === query) {
- return (function() {
- _results = [];
- for (var _i = stringOffset, _ref = stringOffset + string.length; stringOffset <= _ref ? _i < _ref : _i > _ref; stringOffset <= _ref ? _i++ : _i--){ _results.push(_i); }
- return _results;
- }).apply(this);
- }
- queryLength = query.length;
- stringLength = string.length;
- indexInQuery = 0;
- indexInString = 0;
- matches = [];
- while (indexInQuery < queryLength) {
- character = query[indexInQuery++];
- lowerCaseIndex = string.indexOf(character.toLowerCase());
- upperCaseIndex = string.indexOf(character.toUpperCase());
- minIndex = Math.min(lowerCaseIndex, upperCaseIndex);
- if (minIndex === -1) {
- minIndex = Math.max(lowerCaseIndex, upperCaseIndex);
- }
- indexInString = minIndex;
- if (indexInString === -1) {
- return [];
- }
- matches.push(stringOffset + indexInString);
- stringOffset += indexInString + 1;
- string = string.substring(indexInString + 1, stringLength);
- }
- return matches;
- };
-
-}).call(this);
-
-},{"path":7}],5:[function(require,module,exports){
-(function() {
- var PathSeparator, scorer;
-
- PathSeparator = require('path').sep;
-
- scorer = require('./scorer');
-
- exports.basenameMatch = function(subject, subject_lw, prepQuery) {
- var basePos, depth, end;
- end = subject.length - 1;
- while (subject[end] === PathSeparator) {
- end--;
- }
- basePos = subject.lastIndexOf(PathSeparator, end);
- if (basePos === -1) {
- return [];
- }
- depth = prepQuery.depth;
- while (depth-- > 0) {
- basePos = subject.lastIndexOf(PathSeparator, basePos - 1);
- if (basePos === -1) {
- return [];
- }
- }
- basePos++;
- end++;
- return exports.match(subject.slice(basePos, end), subject_lw.slice(basePos, end), prepQuery, basePos);
- };
-
- exports.mergeMatches = function(a, b) {
- var ai, bj, i, j, m, n, out;
- m = a.length;
- n = b.length;
- if (n === 0) {
- return a.slice();
- }
- if (m === 0) {
- return b.slice();
- }
- i = -1;
- j = 0;
- bj = b[j];
- out = [];
- while (++i < m) {
- ai = a[i];
- while (bj <= ai && ++j < n) {
- if (bj < ai) {
- out.push(bj);
- }
- bj = b[j];
- }
- out.push(ai);
- }
- while (j < n) {
- out.push(b[j++]);
- }
- return out;
- };
-
- exports.match = function(subject, subject_lw, prepQuery, offset) {
- var DIAGONAL, LEFT, STOP, UP, acro_score, align, backtrack, csc_diag, csc_row, csc_score, i, j, m, matches, move, n, pos, query, query_lw, score, score_diag, score_row, score_up, si_lw, start, trace;
- if (offset == null) {
- offset = 0;
- }
- query = prepQuery.query;
- query_lw = prepQuery.query_lw;
- m = subject.length;
- n = query.length;
- acro_score = scorer.scoreAcronyms(subject, subject_lw, query, query_lw).score;
- score_row = new Array(n);
- csc_row = new Array(n);
- STOP = 0;
- UP = 1;
- LEFT = 2;
- DIAGONAL = 3;
- trace = new Array(m * n);
- pos = -1;
- j = -1;
- while (++j < n) {
- score_row[j] = 0;
- csc_row[j] = 0;
- }
- i = -1;
- while (++i < m) {
- score = 0;
- score_up = 0;
- csc_diag = 0;
- si_lw = subject_lw[i];
- j = -1;
- while (++j < n) {
- csc_score = 0;
- align = 0;
- score_diag = score_up;
- if (query_lw[j] === si_lw) {
- start = scorer.isWordStart(i, subject, subject_lw);
- csc_score = csc_diag > 0 ? csc_diag : scorer.scoreConsecutives(subject, subject_lw, query, query_lw, i, j, start);
- align = score_diag + scorer.scoreCharacter(i, j, start, acro_score, csc_score);
- }
- score_up = score_row[j];
- csc_diag = csc_row[j];
- if (score > score_up) {
- move = LEFT;
- } else {
- score = score_up;
- move = UP;
- }
- if (align > score) {
- score = align;
- move = DIAGONAL;
- } else {
- csc_score = 0;
- }
- score_row[j] = score;
- csc_row[j] = csc_score;
- trace[++pos] = score > 0 ? move : STOP;
- }
- }
- i = m - 1;
- j = n - 1;
- pos = i * n + j;
- backtrack = true;
- matches = [];
- while (backtrack && i >= 0 && j >= 0) {
- switch (trace[pos]) {
- case UP:
- i--;
- pos -= n;
- break;
- case LEFT:
- j--;
- pos--;
- break;
- case DIAGONAL:
- matches.push(i + offset);
- j--;
- i--;
- pos -= n + 1;
- break;
- default:
- backtrack = false;
- }
- }
- matches.reverse();
- return matches;
- };
-
-}).call(this);
-
-},{"./scorer":6,"path":7}],6:[function(require,module,exports){
-(function() {
- var AcronymResult, PathSeparator, Query, basenameScore, coreChars, countDir, doScore, emptyAcronymResult, file_coeff, isMatch, isSeparator, isWordEnd, isWordStart, miss_coeff, opt_char_re, pos_bonus, scoreAcronyms, scoreCharacter, scoreConsecutives, scoreExact, scoreExactMatch, scorePattern, scorePosition, scoreSize, tau_depth, tau_size, truncatedUpperCase, wm;
-
- PathSeparator = require('path').sep;
-
- wm = 150;
-
- pos_bonus = 20;
-
- tau_depth = 13;
-
- tau_size = 85;
-
- file_coeff = 1.2;
-
- miss_coeff = 0.75;
-
- opt_char_re = /[ _\-:\/\\]/g;
-
- exports.coreChars = coreChars = function(query) {
- return query.replace(opt_char_re, '');
- };
-
- exports.score = function(string, query, prepQuery, allowErrors) {
- var score, string_lw;
- if (prepQuery == null) {
- prepQuery = new Query(query);
- }
- if (allowErrors == null) {
- allowErrors = false;
- }
- if (!(allowErrors || isMatch(string, prepQuery.core_lw, prepQuery.core_up))) {
- return 0;
- }
- string_lw = string.toLowerCase();
- score = doScore(string, string_lw, prepQuery);
- return Math.ceil(basenameScore(string, string_lw, prepQuery, score));
- };
-
- Query = (function() {
- function Query(query) {
- if (!(query != null ? query.length : void 0)) {
- return null;
- }
- this.query = query;
- this.query_lw = query.toLowerCase();
- this.core = coreChars(query);
- this.core_lw = this.core.toLowerCase();
- this.core_up = truncatedUpperCase(this.core);
- this.depth = countDir(query, query.length);
- }
-
- return Query;
-
- })();
-
- exports.prepQuery = function(query) {
- return new Query(query);
- };
-
- exports.isMatch = isMatch = function(subject, query_lw, query_up) {
- var i, j, m, n, qj_lw, qj_up, si;
- m = subject.length;
- n = query_lw.length;
- if (!m || n > m) {
- return false;
- }
- i = -1;
- j = -1;
- while (++j < n) {
- qj_lw = query_lw[j];
- qj_up = query_up[j];
- while (++i < m) {
- si = subject[i];
- if (si === qj_lw || si === qj_up) {
- break;
- }
- }
- if (i === m) {
- return false;
- }
- }
- return true;
- };
-
- doScore = function(subject, subject_lw, prepQuery) {
- var acro, acro_score, align, csc_diag, csc_row, csc_score, i, j, m, miss_budget, miss_left, mm, n, pos, query, query_lw, record_miss, score, score_diag, score_row, score_up, si_lw, start, sz;
- query = prepQuery.query;
- query_lw = prepQuery.query_lw;
- m = subject.length;
- n = query.length;
- acro = scoreAcronyms(subject, subject_lw, query, query_lw);
- acro_score = acro.score;
- if (acro.count === n) {
- return scoreExact(n, m, acro_score, acro.pos);
- }
- pos = subject_lw.indexOf(query_lw);
- if (pos > -1) {
- return scoreExactMatch(subject, subject_lw, query, query_lw, pos, n, m);
- }
- score_row = new Array(n);
- csc_row = new Array(n);
- sz = scoreSize(n, m);
- miss_budget = Math.ceil(miss_coeff * n) + 5;
- miss_left = miss_budget;
- j = -1;
- while (++j < n) {
- score_row[j] = 0;
- csc_row[j] = 0;
- }
- i = subject_lw.indexOf(query_lw[0]);
- if (i > -1) {
- i--;
- }
- mm = subject_lw.lastIndexOf(query_lw[n - 1], m);
- if (mm > i) {
- m = mm + 1;
- }
- while (++i < m) {
- score = 0;
- score_diag = 0;
- csc_diag = 0;
- si_lw = subject_lw[i];
- record_miss = true;
- j = -1;
- while (++j < n) {
- score_up = score_row[j];
- if (score_up > score) {
- score = score_up;
- }
- csc_score = 0;
- if (query_lw[j] === si_lw) {
- start = isWordStart(i, subject, subject_lw);
- csc_score = csc_diag > 0 ? csc_diag : scoreConsecutives(subject, subject_lw, query, query_lw, i, j, start);
- align = score_diag + scoreCharacter(i, j, start, acro_score, csc_score);
- if (align > score) {
- score = align;
- miss_left = miss_budget;
- } else {
- if (record_miss && --miss_left <= 0) {
- return score_row[n - 1] * sz;
- }
- record_miss = false;
- }
- }
- score_diag = score_up;
- csc_diag = csc_row[j];
- csc_row[j] = csc_score;
- score_row[j] = score;
- }
- }
- return score * sz;
- };
-
- exports.isWordStart = isWordStart = function(pos, subject, subject_lw) {
- var curr_s, prev_s;
- if (pos === 0) {
- return true;
- }
- curr_s = subject[pos];
- prev_s = subject[pos - 1];
- return isSeparator(curr_s) || isSeparator(prev_s) || (curr_s !== subject_lw[pos] && prev_s === subject_lw[pos - 1]);
- };
-
- exports.isWordEnd = isWordEnd = function(pos, subject, subject_lw, len) {
- var curr_s, next_s;
- if (pos === len - 1) {
- return true;
- }
- curr_s = subject[pos];
- next_s = subject[pos + 1];
- return isSeparator(curr_s) || isSeparator(next_s) || (curr_s === subject_lw[pos] && next_s !== subject_lw[pos + 1]);
- };
-
- isSeparator = function(c) {
- return c === ' ' || c === '.' || c === '-' || c === '_' || c === '/' || c === '\\';
- };
-
- scorePosition = function(pos) {
- var sc;
- if (pos < pos_bonus) {
- sc = pos_bonus - pos;
- return 100 + sc * sc;
- } else {
- return Math.max(100 + pos_bonus - pos, 0);
- }
- };
-
- scoreSize = function(n, m) {
- return tau_size / (tau_size + Math.abs(m - n));
- };
-
- scoreExact = function(n, m, quality, pos) {
- return 2 * n * (wm * quality + scorePosition(pos)) * scoreSize(n, m);
- };
-
- exports.scorePattern = scorePattern = function(count, len, sameCase, start, end) {
- var bonus, sz;
- sz = count;
- bonus = 6;
- if (sameCase === count) {
- bonus += 2;
- }
- if (start) {
- bonus += 3;
- }
- if (end) {
- bonus += 1;
- }
- if (count === len) {
- if (start) {
- if (sameCase === len) {
- sz += 2;
- } else {
- sz += 1;
- }
- }
- if (end) {
- bonus += 1;
- }
- }
- return sameCase + sz * (sz + bonus);
- };
-
- exports.scoreCharacter = scoreCharacter = function(i, j, start, acro_score, csc_score) {
- var posBonus;
- posBonus = scorePosition(i);
- if (start) {
- return posBonus + wm * ((acro_score > csc_score ? acro_score : csc_score) + 10);
- }
- return posBonus + wm * csc_score;
- };
-
- exports.scoreConsecutives = scoreConsecutives = function(subject, subject_lw, query, query_lw, i, j, start) {
- var k, m, mi, n, nj, sameCase, startPos, sz;
- m = subject.length;
- n = query.length;
- mi = m - i;
- nj = n - j;
- k = mi < nj ? mi : nj;
- startPos = i;
- sameCase = 0;
- sz = 0;
- if (query[j] === subject[i]) {
- sameCase++;
- }
- while (++sz < k && query_lw[++j] === subject_lw[++i]) {
- if (query[j] === subject[i]) {
- sameCase++;
- }
- }
- if (sz === 1) {
- return 1 + 2 * sameCase;
- }
- return scorePattern(sz, n, sameCase, start, isWordEnd(i, subject, subject_lw, m));
- };
-
- exports.scoreExactMatch = scoreExactMatch = function(subject, subject_lw, query, query_lw, pos, n, m) {
- var end, i, pos2, sameCase, start;
- start = isWordStart(pos, subject, subject_lw);
- if (!start) {
- pos2 = subject_lw.indexOf(query_lw, pos + 1);
- if (pos2 > -1) {
- start = isWordStart(pos2, subject, subject_lw);
- if (start) {
- pos = pos2;
- }
- }
- }
- i = -1;
- sameCase = 0;
- while (++i < n) {
- if (query[pos + i] === subject[i]) {
- sameCase++;
- }
- }
- end = isWordEnd(pos + n - 1, subject, subject_lw, m);
- return scoreExact(n, m, scorePattern(n, n, sameCase, start, end), pos);
- };
-
- AcronymResult = (function() {
- function AcronymResult(score, pos, count) {
- this.score = score;
- this.pos = pos;
- this.count = count;
- }
-
- return AcronymResult;
-
- })();
-
- emptyAcronymResult = new AcronymResult(0, 0.1, 0);
-
- exports.scoreAcronyms = scoreAcronyms = function(subject, subject_lw, query, query_lw) {
- var count, i, j, m, n, pos, qj_lw, sameCase, score;
- m = subject.length;
- n = query.length;
- if (!(m > 1 && n > 1)) {
- return emptyAcronymResult;
- }
- count = 0;
- pos = 0;
- sameCase = 0;
- i = -1;
- j = -1;
- while (++j < n) {
- qj_lw = query_lw[j];
- while (++i < m) {
- if (qj_lw === subject_lw[i] && isWordStart(i, subject, subject_lw)) {
- if (query[j] === subject[i]) {
- sameCase++;
- }
- pos += i;
- count++;
- break;
- }
- }
- if (i === m) {
- break;
- }
- }
- if (count < 2) {
- return emptyAcronymResult;
- }
- score = scorePattern(count, n, sameCase, true, false);
- return new AcronymResult(score, pos / count, count);
- };
-
- basenameScore = function(subject, subject_lw, prepQuery, fullPathScore) {
- var alpha, basePathScore, basePos, depth, end;
- if (fullPathScore === 0) {
- return 0;
- }
- end = subject.length - 1;
- while (subject[end] === PathSeparator) {
- end--;
- }
- basePos = subject.lastIndexOf(PathSeparator, end);
- if (basePos === -1) {
- return fullPathScore;
- }
- depth = prepQuery.depth;
- while (depth-- > 0) {
- basePos = subject.lastIndexOf(PathSeparator, basePos - 1);
- if (basePos === -1) {
- return fullPathScore;
- }
- }
- basePos++;
- end++;
- basePathScore = doScore(subject.slice(basePos, end), subject_lw.slice(basePos, end), prepQuery);
- alpha = 0.5 * tau_depth / (tau_depth + countDir(subject, end + 1));
- return alpha * basePathScore + (1 - alpha) * fullPathScore * scoreSize(0, file_coeff * (end - basePos));
- };
-
- exports.countDir = countDir = function(path, end) {
- var count, i;
- if (end < 1) {
- return 0;
- }
- count = 0;
- i = -1;
- while (++i < end && path[i] === PathSeparator) {
- continue;
- }
- while (++i < end) {
- if (path[i] === PathSeparator) {
- count++;
- while (++i < end && path[i] === PathSeparator) {
- continue;
- }
- }
- }
- return count;
- };
-
- truncatedUpperCase = function(str) {
- var char, upper, _i, _len;
- upper = "";
- for (_i = 0, _len = str.length; _i < _len; _i++) {
- char = str[_i];
- upper += char.toUpperCase()[0];
- }
- return upper;
- };
-
-}).call(this);
-
-},{"path":7}],7:[function(require,module,exports){
-(function (process){
-// Copyright Joyent, Inc. and other Node contributors.
-//
-// Permission is hereby granted, free of charge, to any person obtaining a
-// copy of this software and associated documentation files (the
-// "Software"), to deal in the Software without restriction, including
-// without limitation the rights to use, copy, modify, merge, publish,
-// distribute, sublicense, and/or sell copies of the Software, and to permit
-// persons to whom the Software is furnished to do so, subject to the
-// following conditions:
-//
-// The above copyright notice and this permission notice shall be included
-// in all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
-// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
-// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
-// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
-// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
-// USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-// resolves . and .. elements in a path array with directory names there
-// must be no slashes, empty elements, or device names (c:\) in the array
-// (so also no leading and trailing slashes - it does not distinguish
-// relative and absolute paths)
-function normalizeArray(parts, allowAboveRoot) {
- // if the path tries to go above the root, `up` ends up > 0
- var up = 0;
- for (var i = parts.length - 1; i >= 0; i--) {
- var last = parts[i];
- if (last === '.') {
- parts.splice(i, 1);
- } else if (last === '..') {
- parts.splice(i, 1);
- up++;
- } else if (up) {
- parts.splice(i, 1);
- up--;
- }
- }
-
- // if the path is allowed to go above the root, restore leading ..s
- if (allowAboveRoot) {
- for (; up--; up) {
- parts.unshift('..');
- }
- }
-
- return parts;
-}
-
-// Split a filename into [root, dir, basename, ext], unix version
-// 'root' is just a slash, or nothing.
-var splitPathRe =
- /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;
-var splitPath = function(filename) {
- return splitPathRe.exec(filename).slice(1);
-};
-
-// path.resolve([from ...], to)
-// posix version
-exports.resolve = function() {
- var resolvedPath = '',
- resolvedAbsolute = false;
-
- for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) {
- var path = (i >= 0) ? arguments[i] : process.cwd();
-
- // Skip empty and invalid entries
- if (typeof path !== 'string') {
- throw new TypeError('Arguments to path.resolve must be strings');
- } else if (!path) {
- continue;
- }
-
- resolvedPath = path + '/' + resolvedPath;
- resolvedAbsolute = path.charAt(0) === '/';
- }
-
- // At this point the path should be resolved to a full absolute path, but
- // handle relative paths to be safe (might happen when process.cwd() fails)
-
- // Normalize the path
- resolvedPath = normalizeArray(filter(resolvedPath.split('/'), function(p) {
- return !!p;
- }), !resolvedAbsolute).join('/');
-
- return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.';
-};
-
-// path.normalize(path)
-// posix version
-exports.normalize = function(path) {
- var isAbsolute = exports.isAbsolute(path),
- trailingSlash = substr(path, -1) === '/';
-
- // Normalize the path
- path = normalizeArray(filter(path.split('/'), function(p) {
- return !!p;
- }), !isAbsolute).join('/');
-
- if (!path && !isAbsolute) {
- path = '.';
- }
- if (path && trailingSlash) {
- path += '/';
- }
-
- return (isAbsolute ? '/' : '') + path;
-};
-
-// posix version
-exports.isAbsolute = function(path) {
- return path.charAt(0) === '/';
-};
-
-// posix version
-exports.join = function() {
- var paths = Array.prototype.slice.call(arguments, 0);
- return exports.normalize(filter(paths, function(p, index) {
- if (typeof p !== 'string') {
- throw new TypeError('Arguments to path.join must be strings');
- }
- return p;
- }).join('/'));
-};
-
-
-// path.relative(from, to)
-// posix version
-exports.relative = function(from, to) {
- from = exports.resolve(from).substr(1);
- to = exports.resolve(to).substr(1);
-
- function trim(arr) {
- var start = 0;
- for (; start < arr.length; start++) {
- if (arr[start] !== '') break;
- }
-
- var end = arr.length - 1;
- for (; end >= 0; end--) {
- if (arr[end] !== '') break;
- }
-
- if (start > end) return [];
- return arr.slice(start, end - start + 1);
- }
-
- var fromParts = trim(from.split('/'));
- var toParts = trim(to.split('/'));
-
- var length = Math.min(fromParts.length, toParts.length);
- var samePartsLength = length;
- for (var i = 0; i < length; i++) {
- if (fromParts[i] !== toParts[i]) {
- samePartsLength = i;
- break;
- }
- }
-
- var outputParts = [];
- for (var i = samePartsLength; i < fromParts.length; i++) {
- outputParts.push('..');
- }
-
- outputParts = outputParts.concat(toParts.slice(samePartsLength));
-
- return outputParts.join('/');
-};
-
-exports.sep = '/';
-exports.delimiter = ':';
-
-exports.dirname = function(path) {
- var result = splitPath(path),
- root = result[0],
- dir = result[1];
-
- if (!root && !dir) {
- // No dirname whatsoever
- return '.';
- }
-
- if (dir) {
- // It has a dirname, strip trailing slash
- dir = dir.substr(0, dir.length - 1);
- }
-
- return root + dir;
-};
-
-
-exports.basename = function(path, ext) {
- var f = splitPath(path)[2];
- // TODO: make this comparison case-insensitive on windows?
- if (ext && f.substr(-1 * ext.length) === ext) {
- f = f.substr(0, f.length - ext.length);
- }
- return f;
-};
-
-
-exports.extname = function(path) {
- return splitPath(path)[3];
-};
-
-function filter (xs, f) {
- if (xs.filter) return xs.filter(f);
- var res = [];
- for (var i = 0; i < xs.length; i++) {
- if (f(xs[i], i, xs)) res.push(xs[i]);
- }
- return res;
-}
-
-// String.prototype.substr - negative index don't work in IE8
-var substr = 'ab'.substr(-1) === 'b'
- ? function (str, start, len) { return str.substr(start, len) }
- : function (str, start, len) {
- if (start < 0) start = str.length + start;
- return str.substr(start, len);
- }
-;
-
-}).call(this,require('_process'))
-},{"_process":8}],8:[function(require,module,exports){
-// shim for using process in browser
-
-var process = module.exports = {};
-var queue = [];
-var draining = false;
-var currentQueue;
-var queueIndex = -1;
-
-function cleanUpNextTick() {
- draining = false;
- if (currentQueue.length) {
- queue = currentQueue.concat(queue);
- } else {
- queueIndex = -1;
- }
- if (queue.length) {
- drainQueue();
- }
-}
-
-function drainQueue() {
- if (draining) {
- return;
- }
- var timeout = setTimeout(cleanUpNextTick);
- draining = true;
-
- var len = queue.length;
- while(len) {
- currentQueue = queue;
- queue = [];
- while (++queueIndex < len) {
- if (currentQueue) {
- currentQueue[queueIndex].run();
- }
- }
- queueIndex = -1;
- len = queue.length;
- }
- currentQueue = null;
- draining = false;
- clearTimeout(timeout);
-}
-
-process.nextTick = function (fun) {
- var args = new Array(arguments.length - 1);
- if (arguments.length > 1) {
- for (var i = 1; i < arguments.length; i++) {
- args[i - 1] = arguments[i];
- }
- }
- queue.push(new Item(fun, args));
- if (queue.length === 1 && !draining) {
- setTimeout(drainQueue, 0);
- }
-};
-
-// v8 likes predictible objects
-function Item(fun, array) {
- this.fun = fun;
- this.array = array;
-}
-Item.prototype.run = function () {
- this.fun.apply(null, this.array);
-};
-process.title = 'browser';
-process.browser = true;
-process.env = {};
-process.argv = [];
-process.version = ''; // empty string to avoid regexp issues
-process.versions = {};
-
-function noop() {}
-
-process.on = noop;
-process.addListener = noop;
-process.once = noop;
-process.off = noop;
-process.removeListener = noop;
-process.removeAllListeners = noop;
-process.emit = noop;
-
-process.binding = function (name) {
- throw new Error('process.binding is not supported');
-};
-
-process.cwd = function () { return '/' };
-process.chdir = function (dir) {
- throw new Error('process.chdir is not supported');
-};
-process.umask = function() { return 0; };
-
-},{}]},{},[1]);
diff --git a/vendor/assets/javascripts/peek.js b/vendor/assets/javascripts/peek.js
index f7e77de34ff..6a341a3f0fe 100644
--- a/vendor/assets/javascripts/peek.js
+++ b/vendor/assets/javascripts/peek.js
@@ -1,5 +1,14 @@
+/*
+ * This is a modified version of https://github.com/peek/peek/blob/master/app/assets/javascripts/peek.js
+ *
+ * - Removed the dependency on jquery.tipsy
+ * - Removed the initializeTipsy and toggleBar functions
+ * - Customized updatePerformanceBar to handle SQL queries report specificities
+ * - Changed /peek/results to /-/peek/results
+ * - Removed the keypress, pjax:end, page:change, and turbolinks:load handlers
+ */
(function($) {
- var fetchRequestResults, getRequestId, peekEnabled, toggleBar, updatePerformanceBar;
+ var fetchRequestResults, getRequestId, peekEnabled, updatePerformanceBar;
getRequestId = function() {
return $('#peek').data('request-id');
};
@@ -41,22 +50,6 @@
});
return $(document).trigger('peek:render', [getRequestId(), results]);
};
- toggleBar = function(event) {
- var wrapper;
- if ($(event.target).is(':input')) {
- return;
- }
- if (event.which === 96 && !event.metaKey) {
- wrapper = $('#peek');
- if (wrapper.hasClass('disabled')) {
- wrapper.removeClass('disabled');
- return document.cookie = "peek=true; path=/";
- } else {
- wrapper.addClass('disabled');
- return document.cookie = "peek=false; path=/";
- }
- }
- };
fetchRequestResults = function() {
return $.ajax('/-/peek/results', {
data: {
@@ -68,7 +61,6 @@
error: function(xhr, textStatus, error) {}
});
};
- $(document).on('keypress', toggleBar);
$(document).on('peek:update', fetchRequestResults);
return $(function() {
if (peekEnabled()) {
diff --git a/yarn.lock b/yarn.lock
index 91ffbe5d4b0..ee00c1f4f3e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -248,6 +248,10 @@ autoprefixer@^6.3.1:
postcss "^5.2.16"
postcss-value-parser "^3.2.3"
+autosize@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/autosize/-/autosize-4.0.0.tgz#7a0599b1ba84d73bd7589b0d9da3870152c69237"
+
aws-sign2@~0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f"
@@ -2671,6 +2675,10 @@ function-bind@^1.1.1, function-bind@~1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+fuzzaldrin-plus@^0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/fuzzaldrin-plus/-/fuzzaldrin-plus-0.5.0.tgz#ef5f26f0c2fc7e9e9a16ea149a802d6cb4804b1e"
+
gauge@~2.7.3:
version "2.7.4"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
@@ -6390,9 +6398,9 @@ vue-style-loader@^2.0.0:
hash-sum "^1.0.2"
loader-utils "^1.0.2"
-vue-template-compiler@^2.2.6:
- version "2.2.6"
- resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.2.6.tgz#2e2928daf0cd0feca9dfc35a9729adeae173ec68"
+vue-template-compiler@^2.5.2:
+ version "2.5.2"
+ resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.5.2.tgz#6f198ebc677b8f804315cd33b91e849315ae7177"
dependencies:
de-indent "^1.0.2"
he "^1.1.0"
@@ -6401,9 +6409,9 @@ vue-template-es2015-compiler@^1.2.2:
version "1.5.1"
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.5.1.tgz#0c36cc57aa3a9ec13e846342cb14a72fcac8bd93"
-vue@^2.2.6:
- version "2.2.6"
- resolved "https://registry.yarnpkg.com/vue/-/vue-2.2.6.tgz#451714b394dd6d4eae7b773c40c2034a59621aed"
+vue@^2.5.2:
+ version "2.5.2"
+ resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.2.tgz#fd367a87bae7535e47f9dc5c9ec3b496e5feb5a4"
vuex@^3.0.0:
version "3.0.0"