summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Greiling <mike@pixelcog.com>2018-02-06 13:28:13 -0600
committerMike Greiling <mike@pixelcog.com>2018-02-06 13:28:13 -0600
commit47f2754a14549ccd18e4be8b3b6604b378450d6d (patch)
treef2235166a76bcf9e7761e1b613e63110961869da
parente71a27f082c49a8e132be632cb8fe97f810af987 (diff)
parente776096e84d01ab3d1d07a028b65e7430e195114 (diff)
downloadgitlab-ce-47f2754a14549ccd18e4be8b3b6604b378450d6d.tar.gz
Merge branch 'master' into pawel/connect_to_prometheus_through_proxy-30480
* master: (242 commits) Validate user namespace before saving so that errors persist on model Reset Project's column information in spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_spec.rb Explicitly set cwd in Sidekiq memory killer instead of depending on getcwd Downgrade google-protobuf Close low level rugged repository in project cache worker File upload UI obeys LFS filters Resolve "Add a link to documentation on how to get external ip in the Kubernetes cluster details page" Upgrade GitLab Workhorse to v3.6.0 Add sorting options for /users API (admin only) improvements from feedback [ci-skip] add changelog remove file after `Upload#destroy` Fix a hardcoded pipeline ID in a spinach step Override group sidebar links Replace "cluster" with "Kubernetes cluster" Reorder async/sync tasks in BuildFinishedWorker to read traces efficiently Fix tests for Drop filename enforcement Revert using expand_fixture_path in factory Revert "Add FixtureHelpers for FactoryGirl" Refactor :trace to :trace_live in spec ...
-rw-r--r--.flayignore2
-rw-r--r--.gitlab-ci.yml5
-rw-r--r--.rubocop_todo.yml4
-rw-r--r--CONTRIBUTING.md3
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile8
-rw-r--r--Gemfile.lock11
-rw-r--r--app/assets/images/icons.json2
-rw-r--r--app/assets/images/icons.svg2
-rw-r--r--app/assets/images/illustrations/cluster_popover.svg1
-rw-r--r--app/assets/javascripts/awards_handler.js4
-rw-r--r--app/assets/javascripts/behaviors/secret_values.js8
-rw-r--r--app/assets/javascripts/blob/file_template_mediator.js2
-rw-r--r--app/assets/javascripts/ci_variable_list/ci_variable_list.js205
-rw-r--r--app/assets/javascripts/ci_variable_list/native_form_variable_list.js26
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js6
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue29
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js4
-rw-r--r--app/assets/javascripts/commons/polyfills.js2
-rw-r--r--app/assets/javascripts/dispatcher.js52
-rw-r--r--app/assets/javascripts/feature_highlight/feature_highlight.js65
-rw-r--r--app/assets/javascripts/feature_highlight/feature_highlight_helper.js59
-rw-r--r--app/assets/javascripts/feature_highlight/feature_highlight_options.js12
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js2
-rw-r--r--app/assets/javascripts/groups/transfer_dropdown.js34
-rw-r--r--app/assets/javascripts/issue.js40
-rw-r--r--app/assets/javascripts/job.js25
-rw-r--r--app/assets/javascripts/labels_select.js162
-rw-r--r--app/assets/javascripts/lib/utils/ajax_cache.js32
-rw-r--r--app/assets/javascripts/lib/utils/axios_utils.js4
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js18
-rw-r--r--app/assets/javascripts/main.js5
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_service.js14
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js19
-rw-r--r--app/assets/javascripts/merge_request_tabs.js47
-rw-r--r--app/assets/javascripts/milestone.js18
-rw-r--r--app/assets/javascripts/milestone_select.js119
-rw-r--r--app/assets/javascripts/mini_pipeline_graph_dropdown.js26
-rw-r--r--app/assets/javascripts/network/branch_graph.js16
-rw-r--r--app/assets/javascripts/notes.js57
-rw-r--r--app/assets/javascripts/notifications_form.js38
-rw-r--r--app/assets/javascripts/pager.js33
-rw-r--r--app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js17
-rw-r--r--app/assets/javascripts/pages/admin/cohorts/usage_ping.js19
-rw-r--r--app/assets/javascripts/pages/dashboard/milestones/index/index.js2
-rw-r--r--app/assets/javascripts/pages/dashboard/todos/index/todos.js37
-rw-r--r--app/assets/javascripts/pages/groups/edit/index.js6
-rw-r--r--app/assets/javascripts/pages/groups/show/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/boards/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/issues/index/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/issues/show/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/index/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/project.js19
-rw-r--r--app/assets/javascripts/pages/projects/show/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/tree/show/index.js22
-rw-r--r--app/assets/javascripts/pages/sessions/new/index.js4
-rw-r--r--app/assets/javascripts/pages/sessions/new/username_validator.js12
-rw-r--r--app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js7
-rw-r--r--app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js73
-rw-r--r--app/assets/javascripts/preview_markdown.js30
-rw-r--r--app/assets/javascripts/project_find_file.js24
-rw-r--r--app/assets/javascripts/project_label_subscription.js11
-rw-r--r--app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue120
-rw-r--r--app/assets/javascripts/projects/tree/services/commit_pipeline_service.js11
-rw-r--r--app/assets/javascripts/prometheus_metrics/prometheus_metrics.js17
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit.js40
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_edit.js31
-rw-r--r--app/assets/javascripts/right_sidebar.js31
-rw-r--r--app/assets/javascripts/shortcuts.js27
-rw-r--r--app/assets/javascripts/task_list.js18
-rw-r--r--app/assets/javascripts/toggle_buttons.js10
-rw-r--r--app/assets/javascripts/users/activity_calendar.js23
-rw-r--r--app/assets/javascripts/users/user_tabs.js33
-rw-r--r--app/assets/javascripts/users_select.js98
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.js28
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue53
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.js27
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue42
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js117
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue145
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.js23
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.vue41
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/dependencies.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/confirmation_input.vue62
-rw-r--r--app/assets/javascripts/vue_shared/components/navigation_tabs.vue2
-rw-r--r--app/assets/stylesheets/framework.scss2
-rw-r--r--app/assets/stylesheets/framework/buttons.scss5
-rw-r--r--app/assets/stylesheets/framework/ci_variable_list.scss88
-rw-r--r--app/assets/stylesheets/framework/feature_highlight.scss103
-rw-r--r--app/assets/stylesheets/framework/gfm.scss28
-rw-r--r--app/assets/stylesheets/framework/modal.scss5
-rw-r--r--app/assets/stylesheets/framework/secondary-navigation-elements.scss7
-rw-r--r--app/assets/stylesheets/framework/variables.scss5
-rw-r--r--app/assets/stylesheets/pages/commits.scss17
-rw-r--r--app/assets/stylesheets/pages/groups.scss13
-rw-r--r--app/assets/stylesheets/pages/labels.scss42
-rw-r--r--app/assets/stylesheets/pages/note_form.scss1
-rw-r--r--app/assets/stylesheets/pages/pipeline_schedules.scss81
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss2
-rw-r--r--app/assets/stylesheets/pages/wiki.scss8
-rw-r--r--app/controllers/admin/broadcast_messages_controller.rb5
-rw-r--r--app/controllers/admin/cohorts_controller.rb2
-rw-r--r--app/controllers/admin/services_controller.rb5
-rw-r--r--app/controllers/application_controller.rb23
-rw-r--r--app/controllers/boards/issues_controller.rb6
-rw-r--r--app/controllers/concerns/enforces_two_factor_authentication.rb6
-rw-r--r--app/controllers/concerns/issuable_collections.rb1
-rw-r--r--app/controllers/concerns/requires_whitelisted_monitoring_client.rb4
-rw-r--r--app/controllers/concerns/uploads_actions.rb61
-rw-r--r--app/controllers/groups/labels_controller.rb2
-rw-r--r--app/controllers/groups/uploads_controller.rb30
-rw-r--r--app/controllers/groups_controller.rb15
-rw-r--r--app/controllers/help_controller.rb2
-rw-r--r--app/controllers/import/gitlab_projects_controller.rb5
-rw-r--r--app/controllers/invites_controller.rb2
-rw-r--r--app/controllers/koding_controller.rb2
-rw-r--r--app/controllers/oauth/applications_controller.rb3
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb2
-rw-r--r--app/controllers/passwords_controller.rb4
-rw-r--r--app/controllers/projects/clusters/gcp_controller.rb2
-rw-r--r--app/controllers/projects/clusters_controller.rb6
-rw-r--r--app/controllers/projects/commits_controller.rb5
-rw-r--r--app/controllers/projects/cycle_analytics_controller.rb5
-rw-r--r--app/controllers/projects/forks_controller.rb5
-rw-r--r--app/controllers/projects/issues_controller.rb10
-rw-r--r--app/controllers/projects/lfs_storage_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests/creations_controller.rb5
-rw-r--r--app/controllers/projects/merge_requests_controller.rb6
-rw-r--r--app/controllers/projects/network_controller.rb5
-rw-r--r--app/controllers/projects/notes_controller.rb5
-rw-r--r--app/controllers/projects/pipelines_controller.rb6
-rw-r--r--app/controllers/projects/uploads_controller.rb21
-rw-r--r--app/controllers/projects/wikis_controller.rb8
-rw-r--r--app/controllers/projects_controller.rb9
-rw-r--r--app/controllers/registrations_controller.rb6
-rw-r--r--app/controllers/root_controller.rb6
-rw-r--r--app/controllers/uploads_controller.rb75
-rw-r--r--app/controllers/user_callouts_controller.rb23
-rw-r--r--app/finders/group_projects_finder.rb11
-rw-r--r--app/finders/issuable_finder.rb4
-rw-r--r--app/helpers/application_settings_helper.rb18
-rw-r--r--app/helpers/auth_helper.rb4
-rw-r--r--app/helpers/groups_helper.rb17
-rw-r--r--app/helpers/projects_helper.rb10
-rw-r--r--app/helpers/sidekiq_helper.rb6
-rw-r--r--app/helpers/submodule_helper.rb10
-rw-r--r--app/helpers/tree_helper.rb2
-rw-r--r--app/helpers/user_callouts_helper.rb14
-rw-r--r--app/helpers/version_check_helper.rb2
-rw-r--r--app/helpers/visibility_level_helper.rb4
-rw-r--r--app/helpers/webpack_helper.rb18
-rw-r--r--app/helpers/wiki_helper.rb18
-rw-r--r--app/mailers/abuse_report_mailer.rb6
-rw-r--r--app/mailers/base_mailer.rb4
-rw-r--r--app/models/appearance.rb1
-rw-r--r--app/models/ci/build.rb3
-rw-r--r--app/models/ci/job_artifact.rb5
-rw-r--r--app/models/clusters/applications/ingress.rb6
-rw-r--r--app/models/clusters/platforms/kubernetes.rb5
-rw-r--r--app/models/commit_status.rb2
-rw-r--r--app/models/concerns/artifact_migratable.rb3
-rw-r--r--app/models/concerns/avatarable.rb24
-rw-r--r--app/models/concerns/storage/legacy_namespace.rb20
-rw-r--r--app/models/concerns/taskable.rb4
-rw-r--r--app/models/environment.rb2
-rw-r--r--app/models/group.rb20
-rw-r--r--app/models/issue_assignee.rb2
-rw-r--r--app/models/key.rb12
-rw-r--r--app/models/namespace.rb42
-rw-r--r--app/models/note.rb6
-rw-r--r--app/models/project.rb50
-rw-r--r--app/models/project_services/asana_service.rb2
-rw-r--r--app/models/project_services/issue_tracker_service.rb4
-rw-r--r--app/models/project_services/jira_service.rb2
-rw-r--r--app/models/project_services/kubernetes_service.rb14
-rw-r--r--app/models/project_wiki.rb4
-rw-r--r--app/models/protected_branch.rb6
-rw-r--r--app/models/repository.rb18
-rw-r--r--app/models/snippet.rb4
-rw-r--r--app/models/todo.rb1
-rw-r--r--app/models/upload.rb66
-rw-r--r--app/models/user.rb63
-rw-r--r--app/models/user_callout.rb13
-rw-r--r--app/models/wiki_page.rb66
-rw-r--r--app/policies/ci/pipeline_schedule_policy.rb8
-rw-r--r--app/services/akismet_service.rb6
-rw-r--r--app/services/auth/container_registry_authentication_service.rb4
-rw-r--r--app/services/base_service.rb1
-rw-r--r--app/services/ci/create_trace_artifact_service.rb16
-rw-r--r--app/services/ci/register_job_service.rb2
-rw-r--r--app/services/clusters/create_service.rb2
-rw-r--r--app/services/clusters/gcp/verify_provision_status_service.rb2
-rw-r--r--app/services/files/create_service.rb12
-rw-r--r--app/services/git_push_service.rb1
-rw-r--r--app/services/gravatar_service.rb4
-rw-r--r--app/services/groups/transfer_service.rb96
-rw-r--r--app/services/lfs/file_modification_handler.rb42
-rw-r--r--app/services/merge_requests/build_service.rb30
-rw-r--r--app/services/merge_requests/refresh_service.rb7
-rw-r--r--app/services/projects/hashed_storage/migrate_attachments_service.rb4
-rw-r--r--app/services/projects/housekeeping_service.rb10
-rw-r--r--app/services/projects/update_pages_service.rb4
-rw-r--r--app/services/projects/update_service.rb2
-rw-r--r--app/services/submit_usage_ping_service.rb4
-rw-r--r--app/services/system_note_service.rb26
-rw-r--r--app/services/upload_service.rb4
-rw-r--r--app/services/users/build_service.rb6
-rw-r--r--app/uploaders/attachment_uploader.rb8
-rw-r--r--app/uploaders/avatar_uploader.rb19
-rw-r--r--app/uploaders/file_mover.rb7
-rw-r--r--app/uploaders/file_uploader.rb142
-rw-r--r--app/uploaders/gitlab_uploader.rb81
-rw-r--r--app/uploaders/job_artifact_uploader.rb26
-rw-r--r--app/uploaders/legacy_artifact_uploader.rb26
-rw-r--r--app/uploaders/lfs_object_uploader.rb21
-rw-r--r--app/uploaders/namespace_file_uploader.rb18
-rw-r--r--app/uploaders/personal_file_uploader.rb30
-rw-r--r--app/uploaders/records_uploads.rb81
-rw-r--r--app/uploaders/uploader_helper.rb9
-rw-r--r--app/uploaders/workhorse.rb7
-rw-r--r--app/views/admin/application_settings/_form.html.haml3
-rw-r--r--app/views/admin/broadcast_messages/preview.js.haml1
-rw-r--r--app/views/admin/conversational_development_index/show.html.haml2
-rw-r--r--app/views/admin/dashboard/index.html.haml2
-rw-r--r--app/views/admin/health_check/show.html.haml8
-rw-r--r--app/views/admin/runners/index.html.haml2
-rw-r--r--app/views/ci/variables/_variable_row.html.haml49
-rw-r--r--app/views/dashboard/_groups_head.html.haml2
-rw-r--r--app/views/dashboard/_projects_head.html.haml2
-rw-r--r--app/views/dashboard/projects/_nav.html.haml2
-rw-r--r--app/views/dashboard/todos/index.html.haml2
-rw-r--r--app/views/devise/confirmations/almost_there.haml4
-rw-r--r--app/views/groups/edit.html.haml16
-rw-r--r--app/views/help/index.html.haml8
-rw-r--r--app/views/koding/index.html.haml2
-rw-r--r--app/views/layouts/_head.html.haml4
-rw-r--r--app/views/layouts/devise.html.haml6
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_profile.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml27
-rw-r--r--app/views/notify/_note_email.html.haml2
-rw-r--r--app/views/notify/_note_email.text.erb2
-rw-r--r--app/views/notify/new_issue_email.html.haml2
-rw-r--r--app/views/notify/new_merge_request_email.html.haml2
-rw-r--r--app/views/notify/new_user_email.html.haml2
-rw-r--r--app/views/projects/_export.html.haml2
-rw-r--r--app/views/projects/clusters/_advanced_settings.html.haml8
-rw-r--r--app/views/projects/clusters/_banner.html.haml10
-rw-r--r--app/views/projects/clusters/_cluster.html.haml4
-rw-r--r--app/views/projects/clusters/_dropdown.html.haml6
-rw-r--r--app/views/projects/clusters/_empty_state.html.haml9
-rw-r--r--app/views/projects/clusters/_integration_form.html.haml10
-rw-r--r--app/views/projects/clusters/_sidebar.html.haml6
-rw-r--r--app/views/projects/clusters/gcp/_form.html.haml8
-rw-r--r--app/views/projects/clusters/gcp/_header.html.haml6
-rw-r--r--app/views/projects/clusters/gcp/_show.html.haml4
-rw-r--r--app/views/projects/clusters/gcp/login.html.haml4
-rw-r--r--app/views/projects/clusters/gcp/new.html.haml6
-rw-r--r--app/views/projects/clusters/index.html.haml8
-rw-r--r--app/views/projects/clusters/new.html.haml10
-rw-r--r--app/views/projects/clusters/show.html.haml11
-rw-r--r--app/views/projects/clusters/user/_form.html.haml6
-rw-r--r--app/views/projects/clusters/user/_header.html.haml4
-rw-r--r--app/views/projects/clusters/user/_show.html.haml4
-rw-r--r--app/views/projects/clusters/user/new.html.haml6
-rw-r--r--app/views/projects/commits/_commit.html.haml3
-rw-r--r--app/views/projects/issues/_new_branch.html.haml4
-rw-r--r--app/views/projects/pipeline_schedules/_form.html.haml16
-rw-r--r--app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml3
-rw-r--r--app/views/projects/pipeline_schedules/_tabs.html.haml2
-rw-r--r--app/views/projects/pipeline_schedules/_variable_row.html.haml17
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml2
-rw-r--r--app/views/projects/pipelines_settings/_show.html.haml2
-rw-r--r--app/views/projects/runners/_shared_runners.html.haml4
-rw-r--r--app/views/projects/show.html.haml4
-rw-r--r--app/views/projects/tags/_tag.html.haml2
-rw-r--r--app/views/projects/wikis/_form.html.haml8
-rw-r--r--app/views/projects/wikis/edit.html.haml5
-rw-r--r--app/views/shared/_delete_label_modal.html.haml20
-rw-r--r--app/views/shared/_event_filter.html.haml25
-rw-r--r--app/views/shared/_label.html.haml37
-rw-r--r--app/views/shared/_label_row.html.haml18
-rw-r--r--app/views/shared/_milestones_filter.html.haml2
-rw-r--r--app/views/shared/builds/_tabs.html.haml2
-rw-r--r--app/views/shared/issuable/_nav.html.haml2
-rw-r--r--app/views/snippets/_snippets_scope_menu.html.haml2
-rw-r--r--app/workers/all_queues.yml1
-rw-r--r--app/workers/build_finished_worker.rb8
-rw-r--r--app/workers/create_trace_artifact_worker.rb10
-rw-r--r--app/workers/git_garbage_collect_worker.rb3
-rw-r--r--app/workers/project_cache_worker.rb2
-rw-r--r--app/workers/upload_checksum_worker.rb2
-rw-r--r--changelogs/unreleased/14256-upload-destroy-removes-file.yml5
-rw-r--r--changelogs/unreleased/24167__color_label.yml5
-rw-r--r--changelogs/unreleased/25327-coverage-badge-rounding.yml5
-rw-r--r--changelogs/unreleased/26468-fix-sort-by-recent-sign-in.yml4
-rw-r--r--changelogs/unreleased/30106-group-issues.yml5
-rw-r--r--changelogs/unreleased/31885-ability-to-transfer-groups-to-another-group.yml5
-rw-r--r--changelogs/unreleased/32282-add-foreign-keys-to-todos.yml5
-rw-r--r--changelogs/unreleased/32283-trending-projects-unique-constraint2.yml5
-rw-r--r--changelogs/unreleased/35285-user-interface-bugs-for-schedule-pipelines.yml5
-rw-r--r--changelogs/unreleased/35779-realtime-update-of-pipeline-status-in-files-view.yml5
-rw-r--r--changelogs/unreleased/39985-enable-prometheus-metrics-for-deployed-ingresses.yml5
-rw-r--r--changelogs/unreleased/40552-sanitize-extra-blank-spaces-used-when-uploading-a-ssh-key.yml5
-rw-r--r--changelogs/unreleased/40793-fix-mr-title-for-jira.yml5
-rw-r--r--changelogs/unreleased/41672-emphasize-gke-cluster-to-new-users.yml5
-rw-r--r--changelogs/unreleased/42270-fix-namespace-remove-exports-for-hashed-storage.yml6
-rw-r--r--changelogs/unreleased/42497-rubocop-style-regexpliteral.yml5
-rw-r--r--changelogs/unreleased/42547-upload-store-mount-point.yml5
-rw-r--r--changelogs/unreleased/42584-fix-margins-in-tag-list.yml5
-rw-r--r--changelogs/unreleased/42669-allow-order_by-users-in-gitlab-api.yml5
-rw-r--r--changelogs/unreleased/42684-set-up-ci-set-up-ci-cd.yml5
-rw-r--r--changelogs/unreleased/42693-42693-add-a-link-to-documentation-on-how-to-get-external-ip-in-the-kubernetes-cluster-details-page.yml5
-rw-r--r--changelogs/unreleased/42696-gitlab-import-leaves-group_id-on-projectlabel.yml5
-rw-r--r--changelogs/unreleased/42730-close-rugged-repository.yml5
-rw-r--r--changelogs/unreleased/add-confirmation-input-for-modals.yml5
-rw-r--r--changelogs/unreleased/bump-workhorse.yml5
-rw-r--r--changelogs/unreleased/bvl-fix-500-on-fork-without-restricted-visibility-levels.yml5
-rw-r--r--changelogs/unreleased/dm-user-namespace-route-path-validation.yml5
-rw-r--r--changelogs/unreleased/feature-sm-artifacts-trace.yml5
-rw-r--r--changelogs/unreleased/fix-adjust-button-group-width-on-mobile.yml5
-rw-r--r--changelogs/unreleased/fix-improve-issue-note-dropdown.yml5
-rw-r--r--changelogs/unreleased/fix-show-sidebar-sub-level-items-for-billing.yml5
-rw-r--r--changelogs/unreleased/fj-22607-lowercase-usernames-from-ldap.yml5
-rw-r--r--changelogs/unreleased/fj-37273-moving-wiki-pages-from-the-ui.yml5
-rw-r--r--changelogs/unreleased/issue-42689-new-file-template.yml5
-rw-r--r--changelogs/unreleased/issues-closed-at-steal.yml5
-rw-r--r--changelogs/unreleased/jej-upload-file-tracks-lfs.yml5
-rw-r--r--changelogs/unreleased/osw-markdown-bypass-for-commit-messages.yml5
-rw-r--r--changelogs/unreleased/osw-system-notes-for-commits-regression.yml5
-rw-r--r--changelogs/unreleased/persistent-callouts.yml5
-rw-r--r--changelogs/unreleased/query-counts.yml5
-rw-r--r--changelogs/unreleased/refactor-ci-variable-list-for-future-usage-in-4110.yml5
-rw-r--r--changelogs/unreleased/style-include-branch-in-mobile-view.yml5
-rw-r--r--changelogs/unreleased/winh-kubernetes-clusters.yml5
-rw-r--r--changelogs/unreleased/zj-protobuf.yml5
-rw-r--r--config/gitlab.yml.example11
-rw-r--r--config/initializers/1_settings.rb20
-rw-r--r--config/initializers/active_record_data_types.rb2
-rw-r--r--config/initializers/gollum.rb82
-rw-r--r--config/initializers/query_limiting.rb9
-rw-r--r--config/routes.rb3
-rw-r--r--config/routes/admin.rb2
-rw-r--r--config/routes/group.rb5
-rw-r--r--config/routes/project.rb10
-rw-r--r--config/routes/uploads.rb10
-rw-r--r--config/webpack.config.js23
-rw-r--r--db/migrate/20180119135717_add_uploader_index_to_uploads.rb20
-rw-r--r--db/migrate/20180125214301_create_user_callouts.rb16
-rw-r--r--db/migrate/20180129193323_add_uploads_builder_context.rb14
-rw-r--r--db/migrate/20180201102129_add_unique_constraint_to_trending_projects_project_id.rb19
-rw-r--r--db/migrate/20180201110056_add_foreign_keys_to_todos.rb38
-rw-r--r--db/migrate/20180201145907_migrate_remaining_issues_closed_at.rb55
-rw-r--r--db/post_migrate/20171207150300_remove_project_labels_group_id_copy.rb21
-rw-r--r--db/post_migrate/20180202111106_remove_project_labels_group_id.rb19
-rw-r--r--db/post_migrate/20180204200836_change_author_id_to_not_null_in_todos.rb26
-rw-r--r--db/schema.rb22
-rw-r--r--doc/README.md28
-rw-r--r--doc/administration/auth/ldap.md39
-rw-r--r--doc/administration/environment_variables.md2
-rw-r--r--doc/api/users.md5
-rw-r--r--doc/development/README.md1
-rw-r--r--doc/development/automatic_ce_ee_merge.md31
-rw-r--r--doc/development/file_storage.md108
-rw-r--r--doc/development/query_count_limits.md65
-rw-r--r--doc/install/kubernetes/gitlab_runner_chart.md4
-rw-r--r--doc/topics/autodevops/index.md2
-rw-r--r--doc/user/feature_highlight.md15
-rw-r--r--doc/user/group/index.md14
-rw-r--r--doc/user/group/subgroups/img/create_subgroup_button.pngbin8402 -> 11161 bytes
-rw-r--r--doc/user/group/subgroups/index.md3
-rw-r--r--doc/user/img/feature_highlight_example.pngbin0 -> 27262 bytes
-rw-r--r--doc/user/index.md2
-rw-r--r--doc/user/markdown.md41
-rw-r--r--doc/user/project/import/github.md2
-rw-r--r--doc/user/project/index.md3
-rw-r--r--doc/user/project/pages/getting_started_part_two.md8
-rw-r--r--doc/user/project/repository/web_editor.md2
-rw-r--r--doc/user/project/wiki/img/wiki_move_page_1.pngbin0 -> 54550 bytes
-rw-r--r--doc/user/project/wiki/img/wiki_move_page_2.pngbin0 -> 33535 bytes
-rw-r--r--doc/user/project/wiki/index.md12
-rw-r--r--features/steps/project/commits/commits.rb6
-rw-r--r--features/steps/project/issues/labels.rb5
-rw-r--r--features/steps/project/source/markdown_render.rb6
-rw-r--r--lib/api/api_guard.rb6
-rw-r--r--lib/api/branches.rb2
-rw-r--r--lib/api/helpers/runner.rb6
-rw-r--r--lib/api/issues.rb6
-rw-r--r--lib/api/merge_requests.rb6
-rw-r--r--lib/api/pipelines.rb2
-rw-r--r--lib/api/projects.rb2
-rw-r--r--lib/api/runner.rb6
-rw-r--r--lib/api/templates.rb8
-rw-r--r--lib/api/triggers.rb2
-rw-r--r--lib/api/users.rb21
-rw-r--r--lib/api/v3/branches.rb2
-rw-r--r--lib/api/v3/issues.rb6
-rw-r--r--lib/api/v3/merge_requests.rb4
-rw-r--r--lib/api/v3/pipelines.rb2
-rw-r--r--lib/api/v3/projects.rb2
-rw-r--r--lib/api/v3/templates.rb8
-rw-r--r--lib/api/v3/triggers.rb2
-rw-r--r--lib/backup/artifacts.rb2
-rw-r--r--lib/banzai/color_parser.rb44
-rw-r--r--lib/banzai/filter/color_filter.rb31
-rw-r--r--lib/banzai/filter/emoji_filter.rb4
-rw-r--r--lib/banzai/filter/gollum_tags_filter.rb4
-rw-r--r--lib/banzai/pipeline/broadcast_message_pipeline.rb1
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb1
-rw-r--r--lib/carrier_wave_string_file.rb5
-rw-r--r--lib/container_registry/registry.rb2
-rw-r--r--lib/email_template_interceptor.rb4
-rw-r--r--lib/extracts_path.rb4
-rw-r--r--lib/gitlab/asciidoc.rb8
-rw-r--r--lib/gitlab/auth.rb6
-rw-r--r--lib/gitlab/background_migration/populate_untracked_uploads.rb4
-rw-r--r--lib/gitlab/background_migration/prepare_untracked_uploads.rb9
-rw-r--r--lib/gitlab/badge/coverage/report.rb2
-rw-r--r--lib/gitlab/badge/coverage/template.rb4
-rw-r--r--lib/gitlab/checks/force_push.rb4
-rw-r--r--lib/gitlab/ci/build/artifacts/metadata/entry.rb2
-rw-r--r--lib/gitlab/ci/trace.rb12
-rw-r--r--lib/gitlab/current_settings.rb108
-rw-r--r--lib/gitlab/dependency_linker/composer_json_linker.rb2
-rw-r--r--lib/gitlab/dependency_linker/gemfile_linker.rb2
-rw-r--r--lib/gitlab/dependency_linker/podspec_linker.rb2
-rw-r--r--lib/gitlab/email/reply_parser.rb2
-rw-r--r--lib/gitlab/file_detector.rb22
-rw-r--r--lib/gitlab/gfm/uploads_rewriter.rb4
-rw-r--r--lib/gitlab/git.rb3
-rw-r--r--lib/gitlab/git/blob.rb4
-rw-r--r--lib/gitlab/git/branch.rb2
-rw-r--r--lib/gitlab/git/lfs_changes.rb3
-rw-r--r--lib/gitlab/git/lfs_pointer_file.rb25
-rw-r--r--lib/gitlab/git/path_helper.rb2
-rw-r--r--lib/gitlab/git/popen.rb2
-rw-r--r--lib/gitlab/git/ref.rb2
-rw-r--r--lib/gitlab/git/repository.rb186
-rw-r--r--lib/gitlab/git/repository_mirroring.rb2
-rw-r--r--lib/gitlab/git/rev_list.rb63
-rw-r--r--lib/gitlab/git/tag.rb2
-rw-r--r--lib/gitlab/git/wiki.rb44
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb2
-rw-r--r--lib/gitlab/gitaly_client/operation_service.rb26
-rw-r--r--lib/gitlab/gitaly_client/ref_service.rb33
-rw-r--r--lib/gitlab/gitaly_client/repository_service.rb13
-rw-r--r--lib/gitlab/gitaly_client/wiki_page.rb5
-rw-r--r--lib/gitlab/gitaly_client/wiki_service.rb38
-rw-r--r--lib/gitlab/github_import/representation/diff_note.rb2
-rw-r--r--lib/gitlab/github_import/representation/note.rb2
-rw-r--r--lib/gitlab/gon_helper.rb5
-rw-r--r--lib/gitlab/import_export/file_importer.rb2
-rw-r--r--lib/gitlab/import_export/relation_factory.rb5
-rw-r--r--lib/gitlab/import_export/uploads_saver.rb8
-rw-r--r--lib/gitlab/kubernetes/helm/pod.rb8
-rw-r--r--lib/gitlab/ldap/auth_hash.rb6
-rw-r--r--lib/gitlab/ldap/config.rb4
-rw-r--r--lib/gitlab/ldap/person.rb4
-rw-r--r--lib/gitlab/legacy_github_import/project_creator.rb4
-rw-r--r--lib/gitlab/metrics/prometheus.rb3
-rw-r--r--lib/gitlab/metrics/subscribers/action_view.rb2
-rw-r--r--lib/gitlab/middleware/go.rb7
-rw-r--r--lib/gitlab/middleware/static.rb2
-rw-r--r--lib/gitlab/performance_bar.rb4
-rw-r--r--lib/gitlab/polling_interval.rb6
-rw-r--r--lib/gitlab/protocol_access.rb6
-rw-r--r--lib/gitlab/query_limiting.rb36
-rw-r--r--lib/gitlab/query_limiting/active_support_subscriber.rb11
-rw-r--r--lib/gitlab/query_limiting/middleware.rb55
-rw-r--r--lib/gitlab/query_limiting/transaction.rb83
-rw-r--r--lib/gitlab/quick_actions/spend_time_and_date_separator.rb2
-rw-r--r--lib/gitlab/recaptcha.rb10
-rw-r--r--lib/gitlab/repo_path.rb2
-rw-r--r--lib/gitlab/sentry.rb4
-rw-r--r--lib/gitlab/setup_helper.rb2
-rw-r--r--lib/gitlab/sherlock/file_sample.rb2
-rw-r--r--lib/gitlab/sherlock/middleware.rb2
-rw-r--r--lib/gitlab/sherlock/query.rb4
-rw-r--r--lib/gitlab/sidekiq_middleware/memory_killer.rb2
-rw-r--r--lib/gitlab/ssh_public_key.rb28
-rw-r--r--lib/gitlab/upgrader.rb2
-rw-r--r--lib/gitlab/uploads_transfer.rb2
-rw-r--r--lib/gitlab/usage_data.rb8
-rw-r--r--lib/gitlab/visibility_level.rb7
-rw-r--r--lib/gitlab/workhorse.rb21
-rw-r--r--lib/tasks/flay.rake2
-rw-r--r--locale/gitlab.pot330
-rw-r--r--package.json5
-rw-r--r--qa/qa/factory/resource/issue.rb2
-rw-r--r--qa/qa/page/component/dropzone.rb2
-rw-r--r--qa/qa/page/menu/side.rb2
-rw-r--r--qa/qa/page/project/issue/show.rb2
-rw-r--r--qa/qa/page/project/show.rb3
-rw-r--r--qa/qa/runtime/browser.rb2
-rw-r--r--qa/qa/specs/features/project/create_issue_spec.rb18
-rw-r--r--qa/spec/runtime/rsa_key.rb2
-rw-r--r--rubocop/cop/migration/update_column_in_batches.rb2
-rwxr-xr-xscripts/lint-rugged5
-rw-r--r--spec/controllers/groups/uploads_controller_spec.rb4
-rw-r--r--spec/controllers/groups_controller_spec.rb83
-rw-r--r--spec/controllers/health_check_controller_spec.rb2
-rw-r--r--spec/controllers/health_controller_spec.rb2
-rw-r--r--spec/controllers/oauth/applications_controller_spec.rb3
-rw-r--r--spec/controllers/projects/artifacts_controller_spec.rb3
-rw-r--r--spec/controllers/projects/clusters_controller_spec.rb10
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb35
-rw-r--r--spec/controllers/projects/raw_controller_spec.rb2
-rw-r--r--spec/controllers/projects/todos_controller_spec.rb4
-rw-r--r--spec/controllers/uploads_controller_spec.rb13
-rw-r--r--spec/controllers/user_callouts_controller_spec.rb49
-rw-r--r--spec/factories/ci/builds.rb10
-rw-r--r--spec/factories/ci/job_artifacts.rb9
-rw-r--r--spec/factories/commits.rb2
-rw-r--r--spec/factories/deployments.rb3
-rw-r--r--spec/factories/events.rb2
-rw-r--r--spec/factories/groups.rb2
-rw-r--r--spec/factories/issues.rb2
-rw-r--r--spec/factories/keys.rb4
-rw-r--r--spec/factories/merge_requests.rb2
-rw-r--r--spec/factories/notes.rb6
-rw-r--r--spec/factories/project_wikis.rb2
-rw-r--r--spec/factories/projects.rb8
-rw-r--r--spec/factories/sent_notifications.rb2
-rw-r--r--spec/factories/snippets.rb1
-rw-r--r--spec/factories/subscriptions.rb2
-rw-r--r--spec/factories/timelogs.rb2
-rw-r--r--spec/factories/todos.rb4
-rw-r--r--spec/factories/uploads.rb32
-rw-r--r--spec/factories/user_callouts.rb7
-rw-r--r--spec/factories/users.rb2
-rw-r--r--spec/features/admin/admin_health_check_spec.rb4
-rw-r--r--spec/features/admin/admin_runners_spec.rb2
-rw-r--r--spec/features/admin/admin_settings_spec.rb12
-rw-r--r--spec/features/atom/users_spec.rb4
-rw-r--r--spec/features/dashboard/merge_requests_spec.rb22
-rw-r--r--spec/features/groups/issues_spec.rb69
-rw-r--r--spec/features/issues/spam_issues_spec.rb2
-rw-r--r--spec/features/markdown_spec.rb8
-rw-r--r--spec/features/merge_request/user_awards_emoji_spec.rb2
-rw-r--r--spec/features/merge_request/user_resolves_conflicts_spec.rb10
-rw-r--r--spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb2
-rw-r--r--spec/features/projects/badges/coverage_spec.rb4
-rw-r--r--spec/features/projects/clusters/applications_spec.rb4
-rw-r--r--spec/features/projects/clusters/gcp_spec.rb32
-rw-r--r--spec/features/projects/clusters/user_spec.rb18
-rw-r--r--spec/features/projects/clusters_spec.rb4
-rw-r--r--spec/features/projects/import_export/namespace_export_file_spec.rb55
-rw-r--r--spec/features/projects/jobs_spec.rb36
-rw-r--r--spec/features/projects/labels/update_prioritization_spec.rb2
-rw-r--r--spec/features/projects/pipeline_schedules_spec.rb30
-rw-r--r--spec/features/projects/wiki/user_updates_wiki_page_spec.rb74
-rw-r--r--spec/features/projects/wiki/user_views_wiki_page_spec.rb3
-rw-r--r--spec/finders/group_projects_finder_spec.rb76
-rw-r--r--spec/finders/issues_finder_spec.rb44
-rw-r--r--spec/finders/merge_requests_finder_spec.rb41
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/user/basic.json4
-rw-r--r--spec/fixtures/markdown.md.erb12
-rw-r--r--spec/fixtures/trace/sample_trace1185
-rw-r--r--spec/helpers/groups_helper_spec.rb4
-rw-r--r--spec/helpers/labels_helper_spec.rb2
-rw-r--r--spec/helpers/user_callouts_helper_spec.rb47
-rw-r--r--spec/helpers/version_check_helper_spec.rb6
-rw-r--r--spec/javascripts/ci_variable_list/ci_variable_list_spec.js163
-rw-r--r--spec/javascripts/ci_variable_list/native_form_variable_list_spec.js30
-rw-r--r--spec/javascripts/clusters/clusters_bundle_spec.js13
-rw-r--r--spec/javascripts/clusters/stores/clusters_store_spec.js1
-rw-r--r--spec/javascripts/collapsed_sidebar_todo_spec.js139
-rw-r--r--spec/javascripts/commit/commit_pipeline_status_component_spec.js104
-rw-r--r--spec/javascripts/feature_highlight/feature_highlight_helper_spec.js231
-rw-r--r--spec/javascripts/feature_highlight/feature_highlight_options_spec.js30
-rw-r--r--spec/javascripts/feature_highlight/feature_highlight_spec.js131
-rw-r--r--spec/javascripts/fixtures/jobs.rb2
-rw-r--r--spec/javascripts/fixtures/pipeline_schedules.rb43
-rw-r--r--spec/javascripts/gfm_auto_complete_spec.js13
-rw-r--r--spec/javascripts/issue_spec.js144
-rw-r--r--spec/javascripts/job_spec.js252
-rw-r--r--spec/javascripts/labels_issue_sidebar_spec.js43
-rw-r--r--spec/javascripts/lib/utils/ajax_cache_spec.js68
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js14
-rw-r--r--spec/javascripts/merge_request_spec.js28
-rw-r--r--spec/javascripts/merge_request_tabs_spec.js96
-rw-r--r--spec/javascripts/mini_pipeline_graph_dropdown_spec.js73
-rw-r--r--spec/javascripts/notes_spec.js212
-rw-r--r--spec/javascripts/pager_spec.js81
-rw-r--r--spec/javascripts/pipeline_schedules/setup_pipeline_variable_list_spec.js145
-rw-r--r--spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js44
-rw-r--r--spec/javascripts/right_sidebar_spec.js26
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js58
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js79
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js273
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js79
-rw-r--r--spec/javascripts/vue_shared/components/confirmation_input_spec.js63
-rw-r--r--spec/lib/banzai/color_parser_spec.rb90
-rw-r--r--spec/lib/banzai/filter/color_filter_spec.rb61
-rw-r--r--spec/lib/banzai/filter/commit_range_reference_filter_spec.rb4
-rw-r--r--spec/lib/banzai/filter/commit_reference_filter_spec.rb6
-rw-r--r--spec/lib/banzai/filter/external_issue_reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/image_link_filter_spec.rb4
-rw-r--r--spec/lib/banzai/filter/issue_reference_filter_spec.rb6
-rw-r--r--spec/lib/banzai/filter/merge_request_reference_filter_spec.rb4
-rw-r--r--spec/lib/banzai/filter/snippet_reference_filter_spec.rb6
-rw-r--r--spec/lib/banzai/filter/user_reference_filter_spec.rb4
-rw-r--r--spec/lib/file_size_validator_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb57
-rw-r--r--spec/lib/gitlab/badge/coverage/template_spec.rb18
-rw-r--r--spec/lib/gitlab/checks/force_push_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/trace_spec.rb87
-rw-r--r--spec/lib/gitlab/closing_issue_extractor_spec.rb3
-rw-r--r--spec/lib/gitlab/current_settings_spec.rb40
-rw-r--r--spec/lib/gitlab/gfm/uploads_rewriter_spec.rb2
-rw-r--r--spec/lib/gitlab/git/lfs_pointer_file_spec.rb37
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb149
-rw-r--r--spec/lib/gitlab/git/rev_list_spec.rb57
-rw-r--r--spec/lib/gitlab/git/wiki_spec.rb36
-rw-r--r--spec/lib/gitlab/gitaly_client/operation_service_spec.rb49
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/uploads_restorer_spec.rb9
-rw-r--r--spec/lib/gitlab/import_export/uploads_saver_spec.rb4
-rw-r--r--spec/lib/gitlab/kubernetes/helm/pod_spec.rb4
-rw-r--r--spec/lib/gitlab/ldap/auth_hash_spec.rb24
-rw-r--r--spec/lib/gitlab/ldap/person_spec.rb21
-rw-r--r--spec/lib/gitlab/metrics_spec.rb2
-rw-r--r--spec/lib/gitlab/path_regex_spec.rb2
-rw-r--r--spec/lib/gitlab/query_limiting/active_support_subscriber_spec.rb19
-rw-r--r--spec/lib/gitlab/query_limiting/middleware_spec.rb72
-rw-r--r--spec/lib/gitlab/query_limiting/transaction_spec.rb144
-rw-r--r--spec/lib/gitlab/query_limiting_spec.rb65
-rw-r--r--spec/lib/gitlab/slash_commands/issue_search_spec.rb2
-rw-r--r--spec/lib/gitlab/ssh_public_key_spec.rb35
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb6
-rw-r--r--spec/lib/gitlab/visibility_level_spec.rb9
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb17
-rw-r--r--spec/mailers/notify_spec.rb2
-rw-r--r--spec/migrations/add_foreign_keys_to_todos_spec.rb65
-rw-r--r--spec/migrations/remove_project_labels_group_id_spec.rb21
-rw-r--r--spec/models/ci/build_spec.rb8
-rw-r--r--spec/models/ci/job_artifact_spec.rb3
-rw-r--r--spec/models/group_spec.rb16
-rw-r--r--spec/models/key_spec.rb58
-rw-r--r--spec/models/namespace_spec.rb68
-rw-r--r--spec/models/note_spec.rb4
-rw-r--r--spec/models/project_services/jira_service_spec.rb4
-rw-r--r--spec/models/project_services/kubernetes_service_spec.rb2
-rw-r--r--spec/models/project_spec.rb32
-rw-r--r--spec/models/project_wiki_spec.rb13
-rw-r--r--spec/models/repository_spec.rb61
-rw-r--r--spec/models/todo_spec.rb1
-rw-r--r--spec/models/upload_spec.rb81
-rw-r--r--spec/models/user_callout_spec.rb16
-rw-r--r--spec/models/user_spec.rb51
-rw-r--r--spec/models/wiki_page_spec.rb152
-rw-r--r--spec/policies/ci/pipeline_schedule_policy_spec.rb14
-rw-r--r--spec/policies/project_policy_spec.rb2
-rw-r--r--spec/requests/api/groups_spec.rb15
-rw-r--r--spec/requests/api/jobs_spec.rb31
-rw-r--r--spec/requests/api/projects_spec.rb2
-rw-r--r--spec/requests/api/runner_spec.rb22
-rw-r--r--spec/requests/api/users_spec.rb18
-rw-r--r--spec/requests/api/v3/builds_spec.rb8
-rw-r--r--spec/requests/api/v3/projects_spec.rb2
-rw-r--r--spec/requests/lfs_http_spec.rb4
-rw-r--r--spec/services/ci/create_trace_artifact_service_spec.rb43
-rw-r--r--spec/services/ci/retry_build_service_spec.rb5
-rw-r--r--spec/services/files/create_service_spec.rb78
-rw-r--r--spec/services/groups/transfer_service_spec.rb414
-rw-r--r--spec/services/issues/close_service_spec.rb2
-rw-r--r--spec/services/issues/move_service_spec.rb4
-rw-r--r--spec/services/issues/update_service_spec.rb3
-rw-r--r--spec/services/merge_requests/build_service_spec.rb58
-rw-r--r--spec/services/merge_requests/close_service_spec.rb2
-rw-r--r--spec/services/merge_requests/ff_merge_service_spec.rb3
-rw-r--r--spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb2
-rw-r--r--spec/services/merge_requests/rebase_service_spec.rb2
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb54
-rw-r--r--spec/services/merge_requests/reopen_service_spec.rb2
-rw-r--r--spec/services/merge_requests/update_service_spec.rb3
-rw-r--r--spec/services/notification_service_spec.rb6
-rw-r--r--spec/services/projects/gitlab_projects_import_service_spec.rb2
-rw-r--r--spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb4
-rw-r--r--spec/services/system_note_service_spec.rb37
-rw-r--r--spec/support/matchers/markdown_matchers.rb21
-rw-r--r--spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb62
-rw-r--r--spec/support/shared_examples/uploaders/gitlab_uploader_shared_examples.rb48
-rw-r--r--spec/support/stored_repositories.rb4
-rw-r--r--spec/support/stub_env.rb2
-rw-r--r--spec/support/test_env.rb2
-rw-r--r--spec/support/track_untracked_uploads_helpers.rb2
-rw-r--r--spec/support/unique_ip_check_shared_examples.rb6
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/git_rake_spec.rb2
-rw-r--r--spec/uploaders/attachment_uploader_spec.rb30
-rw-r--r--spec/uploaders/avatar_uploader_spec.rb18
-rw-r--r--spec/uploaders/file_mover_spec.rb18
-rw-r--r--spec/uploaders/file_uploader_spec.rb143
-rw-r--r--spec/uploaders/gitlab_uploader_spec.rb2
-rw-r--r--spec/uploaders/job_artifact_uploader_spec.rb57
-rw-r--r--spec/uploaders/legacy_artifact_uploader_spec.rb49
-rw-r--r--spec/uploaders/lfs_object_uploader_spec.rb38
-rw-r--r--spec/uploaders/namespace_file_uploader_spec.rb21
-rw-r--r--spec/uploaders/personal_file_uploader_spec.rb28
-rw-r--r--spec/uploaders/records_uploads_spec.rb73
-rw-r--r--spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb47
-rw-r--r--spec/workers/build_finished_worker_spec.rb14
-rw-r--r--spec/workers/create_trace_artifact_worker_spec.rb29
-rw-r--r--spec/workers/upload_checksum_worker_spec.rb29
-rw-r--r--vendor/ingress/values.yaml8
-rw-r--r--vendor/prometheus/values.yaml34
-rw-r--r--yarn.lock8
715 files changed, 11964 insertions, 4180 deletions
diff --git a/.flayignore b/.flayignore
index acac0ce14c9..87cb3507b05 100644
--- a/.flayignore
+++ b/.flayignore
@@ -6,3 +6,5 @@ app/models/concerns/relative_positioning.rb
app/workers/stuck_merge_jobs_worker.rb
lib/gitlab/redis/*.rb
lib/gitlab/gitaly_client/operation_service.rb
+lib/gitlab/background_migration/*
+app/models/project_services/kubernetes_service.rb
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index be18520b876..9c3556f5cce 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -627,6 +627,8 @@ codequality:
sast:
<<: *except-docs
image: registry.gitlab.com/gitlab-org/gl-sast:latest
+ variables:
+ CONFIDENCE_LEVEL: 2
before_script: []
script:
- /app/bin/run .
@@ -738,8 +740,9 @@ cache gems:
gitlab_git_test:
<<: *dedicated-runner
<<: *except-docs-and-qa
- <<: *pull-cache
variables:
SETUP_DB: "false"
+ before_script: []
+ cache: {}
script:
- spec/support/prepare-gitlab-git-test-for-commit --check-for-changes
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 442d61bcf4f..7a12c8473f3 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -704,7 +704,9 @@ Style/RedundantSelf:
# Configuration parameters: EnforcedStyle, AllowInnerSlashes.
# SupportedStyles: slashes, percent_r, mixed
Style/RegexpLiteral:
- Enabled: false
+ Enabled: true
+ EnforcedStyle: mixed
+ AllowInnerSlashes: false
# Offense count: 36
# Cop supports --auto-correct.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index b366ae6f069..ed56da0353d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -50,6 +50,9 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
## Contribute to GitLab
+For a first-time step-by-step guide to the contribution process, see
+["Contributing to GitLab"](https://about.gitlab.com/contributing/).
+
Thank you for your interest in contributing to GitLab. This guide details how
to contribute to GitLab in a way that is efficient for everyone.
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 62df9f538d8..b53d377d909 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.76.0
+0.80.0
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index d5c0c991428..40c341bdcdb 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-3.5.1
+3.6.0
diff --git a/Gemfile b/Gemfile
index 7bffae06b51..27c745b99a2 100644
--- a/Gemfile
+++ b/Gemfile
@@ -69,6 +69,10 @@ gem 'net-ldap'
# Git Wiki
# Required manually in config/initializers/gollum.rb to control load order
+# Before updating this gem, check if
+# https://github.com/gollum/gollum-lib/pull/292 has been merged.
+# If it has, then remove the monkey patch for update_page, rename_page and raw_data_in_committer
+# in config/initializers/gollum.rb
gem 'gollum-lib', '~> 4.2', require: false
# Before updating this gem, check if
@@ -349,7 +353,7 @@ group :development, :test do
gem 'scss_lint', '~> 0.56.0', require: false
gem 'haml_lint', '~> 0.26.0', require: false
gem 'simplecov', '~> 0.14.0', require: false
- gem 'flay', '~> 2.8.0', require: false
+ gem 'flay', '~> 2.10.0', require: false
gem 'bundler-audit', '~> 0.5.0', require: false
gem 'benchmark-ips', '~> 2.3.0', require: false
@@ -407,6 +411,8 @@ end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.83.0', require: 'gitaly'
+# Locked until https://github.com/google/protobuf/issues/4210 is closed
+gem 'google-protobuf', '= 3.5.1'
gem 'toml-rb', '~> 0.3.15', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index f770a7019e7..e78c3c5f794 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -211,7 +211,7 @@ GEM
fast_gettext (1.4.0)
ffaker (2.4.0)
ffi (1.9.18)
- flay (2.8.1)
+ flay (2.10.0)
erubis (~> 2.7.0)
path_expander (~> 1.0)
ruby_parser (~> 3.0)
@@ -340,7 +340,7 @@ GEM
mime-types (~> 3.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
- google-protobuf (3.5.1.1-universal-darwin)
+ google-protobuf (3.5.1)
googleapis-common-protos-types (1.0.1)
google-protobuf (~> 3.0)
googleauth (0.5.3)
@@ -369,7 +369,7 @@ GEM
rake
grape_logging (1.7.0)
grape
- grpc (1.8.3-universal-darwin)
+ grpc (1.8.3)
google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0)
googleauth (>= 0.5.1, < 0.7)
@@ -588,7 +588,7 @@ GEM
ast (~> 2.3)
parslet (1.5.0)
blankslate (~> 2.0)
- path_expander (1.0.1)
+ path_expander (1.0.2)
peek (1.0.1)
concurrent-ruby (>= 0.9.0)
concurrent-ruby-ext (>= 0.9.0)
@@ -1037,7 +1037,7 @@ DEPENDENCIES
faraday (~> 0.12)
fast_blank
ffaker (~> 2.4)
- flay (~> 2.8.0)
+ flay (~> 2.10.0)
flipper (~> 0.11.0)
flipper-active_record (~> 0.11.0)
flipper-active_support_cache_store (~> 0.11.0)
@@ -1066,6 +1066,7 @@ DEPENDENCIES
gollum-rugged_adapter (~> 0.4.4)
gon (~> 6.1.0)
google-api-client (~> 0.13.6)
+ google-protobuf (= 3.5.1)
gpgme
grape (~> 1.0)
grape-entity (~> 0.6.0)
diff --git a/app/assets/images/icons.json b/app/assets/images/icons.json
index 132a373baec..19843d24e22 100644
--- a/app/assets/images/icons.json
+++ b/app/assets/images/icons.json
@@ -1 +1 @@
-{"iconCount":189,"spriteSize":85900,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-down","arrow-right","assignee","bold","book","bookmark","branch","bullhorn","calendar","cancel","chart","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","cut","dashboard","disk","doc_code","doc_image","doc_text","double-headed-arrow","download","duplicate","earth","ellipsis_v","emoji_slightly_smiling_face","emoji_smile","emoji_smiley","epic","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder-o","folder-open","folder","fork","geo-nodes","git-merge","group","history","home","hook","hourglass","image-comment-dark","image-comment-light","import","issue-block","issue-child","issue-close","issue-duplicate","issue-external","issue-new","issue-open-m","issue-open","issue-parent","issues","italic","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil-square","pencil","pipeline","play","plus-square-o","plus-square","plus","podcast","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","spinner","staged","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_notfound","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","task-done","template","terminal","thumb-down","thumb-up","thumbtack","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","unstaged","user","users","volume-up","warning","work"]} \ No newline at end of file
+{"iconCount":191,"spriteSize":86607,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-down","arrow-right","assignee","bold","book","bookmark","branch","bullhorn","calendar","cancel","chart","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","cut","dashboard","disk","doc_code","doc_image","doc_text","double-headed-arrow","download","duplicate","earth","ellipsis_v","emoji_slightly_smiling_face","emoji_smile","emoji_smiley","epic","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder-o","folder-open","folder","fork","geo-nodes","git-merge","group","history","home","hook","hourglass","image-comment-dark","image-comment-light","import","issue-block","issue-child","issue-close","issue-duplicate","issue-external","issue-new","issue-open-m","issue-open","issue-parent","issues","italic","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil-square","pencil","pipeline","play","plus-square-o","plus-square","plus","podcast","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","soft-unwrap","soft-wrap","spam","spinner","staged","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_notfound","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","task-done","template","terminal","thumb-down","thumb-up","thumbtack","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","unstaged","user","users","volume-up","warning","work"]} \ No newline at end of file
diff --git a/app/assets/images/icons.svg b/app/assets/images/icons.svg
index 09efe331f93..6aec54d0543 100644
--- a/app/assets/images/icons.svg
+++ b/app/assets/images/icons.svg
@@ -1 +1 @@
-<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol viewBox="0 0 16 16" id="abuse" xmlns="http://www.w3.org/2000/svg"><path d="M11.408.328l4.029 3.222A1.5 1.5 0 0 1 16 4.72v6.555a1.5 1.5 0 0 1-.563 1.171l-4.026 3.224a1.5 1.5 0 0 1-.937.329H5.529a1.5 1.5 0 0 1-.937-.328L.563 12.45A1.5 1.5 0 0 1 0 11.28V4.724a1.5 1.5 0 0 1 .563-1.171L4.589.329A1.5 1.5 0 0 1 5.526 0h4.945c.34 0 .67.116.937.328zM10.296 2H5.702L2 4.964v6.074L5.704 14h4.594L14 11.036V4.962L10.296 2zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="account" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.195 9.965l-.568-.875a.25.25 0 0 1 .015-.294l.405-.5a.25.25 0 0 1 .283-.075l.938.36c.257-.183.543-.325.851-.42l.322-.988A.25.25 0 0 1 11.679 7h.642a.25.25 0 0 1 .238.173l.322.988c.308.095.594.237.851.42l.938-.36a.25.25 0 0 1 .283.076l.405.5a.25.25 0 0 1 .015.293l-.568.875c.113.297.18.616.193.95l.898.54a.25.25 0 0 1 .115.27l-.144.626a.25.25 0 0 1-.222.193l-1.115.098a3.015 3.015 0 0 1-.512.608l.165 1.18a.25.25 0 0 1-.138.259l-.577.281a.25.25 0 0 1-.29-.05l-.874-.905a3.035 3.035 0 0 1-.608 0l-.875.904a.25.25 0 0 1-.289.051l-.577-.281a.25.25 0 0 1-.138-.26l.165-1.18a3.015 3.015 0 0 1-.512-.607l-1.115-.098a.25.25 0 0 1-.222-.193l-.144-.626a.25.25 0 0 1 .115-.27l.898-.54c.013-.334.08-.653.193-.95zM6.789 8.023A12.845 12.845 0 0 0 6 8c-5.036 0-6 2.74-6 4.48C0 14.22.076 15 6 15c.553 0 1.055-.006 1.51-.02A5.977 5.977 0 0 1 6 11c0-1.083.287-2.1.79-2.977zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM12 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="admin" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.162 2.5a3.5 3.5 0 0 1-3.163 5.479L6.08 14.766a1.5 1.5 0 0 1-2.598-1.5L7.4 6.479A3.5 3.5 0 0 1 10.564 1L8.9 3.88l2.599 1.5 1.663-2.88zm-8.63 11.949a.5.5 0 1 0 .5-.866.5.5 0 0 0-.5.866z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.414 7.95l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 1 0 1.414-1.415L10.414 7.95zm-7 0l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 0 0 1.414-1.415L3.414 7.95z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.536 7.95L1.293 3.707a1 1 0 0 1 1.414-1.414l4.95 4.95a.997.997 0 0 1 0 1.414l-4.95 4.95a1 1 0 1 1-1.414-1.415L5.536 7.95zm7 0L8.293 3.707a1 1 0 0 1 1.414-1.414l4.95 4.95a.997.997 0 0 1 0 1.414l-4.95 4.95a1 1 0 0 1-1.414-1.415l4.243-4.242z"/></symbol><symbol viewBox="0 0 16 16" id="angle-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 10.243l-4.95-4.95a1 1 0 0 0-1.414 1.414l5.657 5.657a.997.997 0 0 0 1.414 0l5.657-5.657a1 1 0 0 0-1.414-1.414L8 10.243z"/></symbol><symbol viewBox="0 0 16 16" id="angle-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.757 8l4.95-4.95a1 1 0 1 0-1.414-1.414L3.636 7.293a.997.997 0 0 0 0 1.414l5.657 5.657a1 1 0 0 0 1.414-1.414L5.757 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.243 8l-4.95-4.95a1 1 0 0 1 1.414-1.414l5.657 5.657a.997.997 0 0 1 0 1.414l-5.657 5.657a1 1 0 0 1-1.414-1.414L10.243 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 6.757l-4.95 4.95a1 1 0 1 1-1.414-1.414l5.657-5.657a.997.997 0 0 1 1.414 0l5.657 5.657a1 1 0 0 1-1.414 1.414L8 6.757z"/></symbol><symbol viewBox="0 0 16 16" id="appearance" xmlns="http://www.w3.org/2000/svg"><path d="M11.161 12.456l.232.121c.1.053.175.094.249.137.53.318.844.75.857 1.402.012 1.397-1.116 1.756-3.12 1.858a23.85 23.85 0 0 1-1.38.026A8 8 0 0 1 0 8a8 8 0 0 1 8-8c4.417 0 7.998 3.582 7.998 7.977.06 2.621-1.312 3.586-4.48 3.648-.602.008-1.068.043-1.4.104.228.192.598.47 1.043.727zm-3.287-.943c-.019-1.495 1.228-1.856 3.611-1.888C13.67 9.582 14.028 9.33 13.998 8A6 6 0 1 0 8 14c.603 0 .91-.004 1.277-.023a9.7 9.7 0 0 0 .478-.035c-1.172-.738-1.868-1.47-1.88-2.43zM6 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-2-3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM4 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="applications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 1v2h2V1H7zm0 5h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm0 1v2h2V7h-2zM1 12h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm0 1v2h2v-2H1zm6-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm6 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="approval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.536 10.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 1 1 9.12 9.243l1.415 1.414zM7.632 8.109A2 2 0 0 0 7 11.364l2.121 2.121a1.996 1.996 0 0 0 2.807.021C11.686 14.554 10.627 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.6 0 1.142.038 1.632.109zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="arrow-down" xmlns="http://www.w3.org/2000/svg"><path d="M10.472 7.282a.862.862 0 0 1 1.26-.006c.357.364.357.958 0 1.285L8.627 11.73A.886.886 0 0 1 8 12a.849.849 0 0 1-.627-.27L4.275 8.561a.904.904 0 0 1-.013-1.285.861.861 0 0 1 1.26-.007l2.486 2.527z"/></symbol><symbol viewBox="0 0 16 16" id="arrow-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 6H2a2 2 0 1 0 0 4h7v2.586a1 1 0 0 0 1.707.707l4.586-4.586a1 1 0 0 0 0-1.414l-4.586-4.586A1 1 0 0 0 9 3.414V6z"/></symbol><symbol viewBox="0 0 16 16" id="assignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 5V4a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V7h-1a1 1 0 0 1 0-2h1zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="bold" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4 12.5v-9A1.5 1.5 0 0 1 5.5 2h2.104c2.182 0 3.879.681 3.879 2.982 0 1.067-.517 2.227-1.374 2.595v.073C11.176 7.963 12 8.865 12 10.466 12 12.914 10.19 14 7.911 14H5.5A1.5 1.5 0 0 1 4 12.5zm2.376-5.696H7.49c1.164 0 1.665-.552 1.665-1.417 0-.94-.534-1.289-1.649-1.289h-1.13v2.706zm0 5.098h1.341c1.293 0 1.956-.515 1.956-1.62 0-1.049-.647-1.472-1.956-1.472H6.376v3.092z"/></symbol><symbol viewBox="0 0 16 16" id="book" xmlns="http://www.w3.org/2000/svg"><path d="M7 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2v4.191a.5.5 0 0 1-.724.447l-1.052-.526a.5.5 0 0 0-.448 0l-1.052.526A.5.5 0 0 1 7 6.191V2zM5 0h6a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="bookmark" xmlns="http://www.w3.org/2000/svg"><path d="M6.746 10.505a2 2 0 0 1 2.508 0L11 11.911V3H5v8.91l1.746-1.405zM5 1h6a2 2 0 0 1 2 2v10.999a1 1 0 0 1-1.627.779L8 12.064l-3.373 2.714A1 1 0 0 1 3 13.998V3a2 2 0 0 1 2-2z"/></symbol><symbol viewBox="0 0 16 16" id="branch" xmlns="http://www.w3.org/2000/svg"><path d="M6 11.978v.29a2 2 0 1 1-2 0V3.732a2 2 0 1 1 2 0v3.849c.592-.491 1.31-.854 2.15-1.081 1.308-.353 1.875-.882 1.893-1.743a2 2 0 1 1 2.002-.051C12.053 6.54 10.857 7.84 8.67 8.43 7.056 8.867 6.195 9.98 6 11.978zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm6 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 15a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="bullhorn" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6.143 10H7V4H3a3 3 0 1 0 0 6h.143l.734 5.141a1 1 0 0 0 .99.859h1.556a.5.5 0 0 0 .495-.57L6.143 10zM8 4c1.034.02 2.039-.274 3.014-.883.727-.455 1.836-1.334 3.328-2.637A1 1 0 0 1 16 1.233v10.764a1 1 0 0 1-1.595.803c-1.658-1.227-2.788-1.992-3.392-2.294-.781-.39-1.785-.559-3.013-.506V4z"/></symbol><symbol viewBox="0 0 16 16" id="calendar" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 2h2a2 2 0 0 1 2 2H0a2 2 0 0 1 2-2h2V1a1 1 0 1 1 2 0v1h4V1a1 1 0 1 1 2 0v1zM0 4h16v9a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4zm2 2.5V13a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6.5a.5.5 0 0 0-.5-.5h-11a.5.5 0 0 0-.5.5zM5 8h2a1 1 0 1 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="cancel" xmlns="http://www.w3.org/2000/svg"><path d="M3.11 4.523a6 6 0 0 0 8.367 8.367L3.109 4.524zM4.522 3.11l8.368 8.368A6 6 0 0 0 4.524 3.11zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 16 16" id="chart" xmlns="http://www.w3.org/2000/svg"><path d="M15 14a1 1 0 0 1 0 2H2a2 2 0 0 1-2-2V1a1 1 0 1 1 2 0v13h13zM3.142 8.735l2.502-2.561a.5.5 0 0 1 .714-.003L8 7.833l3.592-4.553a.5.5 0 0 1 .796.015l2.516 3.454a.5.5 0 0 1 .096.295V12.5a.5.5 0 0 1-.5.5h-11a.5.5 0 0 1-.5-.5V9.085a.5.5 0 0 1 .142-.35z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.078 8.2l3.535-3.536a2 2 0 0 1 2.828 2.828l-4.949 4.95c-.39.39-.902.586-1.414.586a1.994 1.994 0 0 1-1.414-.586l-4.95-4.95a2 2 0 1 1 2.828-2.828l3.536 3.535z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.977 7.998l3.535-3.535a2 2 0 1 0-2.828-2.828l-4.95 4.949c-.39.39-.586.902-.586 1.414 0 .512.196 1.024.586 1.414l4.95 4.95a2 2 0 1 0 2.828-2.828L7.977 7.998z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.22 7.998L4.683 4.463a2 2 0 0 1 2.828-2.828l4.95 4.949c.39.39.586.902.586 1.414a1.99 1.99 0 0 1-.586 1.414l-4.95 4.95a2 2 0 0 1-2.828-2.828l3.535-3.536z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.778 8.957l3.535 3.535a2 2 0 1 0 2.828-2.828l-4.949-4.95a1.994 1.994 0 0 0-1.414-.586c-.512 0-1.024.196-1.414.586l-4.95 4.95a2 2 0 1 0 2.828 2.828l3.536-3.535z"/></symbol><symbol viewBox="0 0 16 16" id="clock" xmlns="http://www.w3.org/2000/svg"><path d="M9 7h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V5a1 1 0 1 1 2 0v2zm-1 9A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.414 8l4.95-4.95a1 1 0 0 0-1.414-1.414L8 6.586l-4.95-4.95A1 1 0 0 0 1.636 3.05L6.586 8l-4.95 4.95a1 1 0 1 0 1.414 1.414L8 9.414l4.95 4.95a1 1 0 1 0 1.414-1.414L9.414 8z"/></symbol><symbol viewBox="0 0 16 16" id="code" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M15.871 8.243a.997.997 0 0 0-.293-.707L12.75 4.707a1 1 0 0 0-1.414 1.414l2.12 2.122-2.12 2.121a1 1 0 0 0 1.414 1.414l2.828-2.828a.997.997 0 0 0 .293-.707zm-13.243 0L4.75 6.12a1 1 0 1 0-1.414-1.414L.507 7.536a.997.997 0 0 0 0 1.414l2.829 2.828a1 1 0 1 0 1.414-1.414L2.628 8.243zm6.407-4.107a1 1 0 0 1 .707 1.225L8.19 11.157a1 1 0 1 1-1.931-.518L7.81 4.843a1 1 0 0 1 1.224-.707z"/></symbol><symbol viewBox="0 0 9 13" id="collapse"><path d="M.084.25C.01.18-.015.12.008.071.031.024.093 0 .194 0h8.521c.1 0 .162.024.185.072.023.048-.002.107-.075.177l-4.11 3.935a.372.372 0 0 1-.11.072h-.301a.508.508 0 0 1-.11-.072L.084.249zM.377 6.88a.364.364 0 0 1-.26-.105.334.334 0 0 1-.11-.25v-.709c0-.096.036-.179.11-.249a.364.364 0 0 1 .26-.105h8.15c.101 0 .188.035.261.105.074.07.11.153.11.25v.709c0 .096-.036.179-.11.249a.364.364 0 0 1-.26.105H.377zM.084 12.132c-.074.07-.099.129-.076.177.023.048.085.072.186.072h8.521c.1 0 .162-.024.185-.072.023-.048-.002-.107-.075-.177l-4.11-3.935a.372.372 0 0 0-.11-.072h-.301a.508.508 0 0 0-.11.072l-4.11 3.935z"/></symbol><symbol viewBox="0 0 16 16" id="comment" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comment-dots" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586zM5 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="comment-next" xmlns="http://www.w3.org/2000/svg"><path d="M8 5V4a.5.5 0 0 1 .8-.4l2.667 2a.5.5 0 0 1 0 .8L8.8 8.4A.5.5 0 0 1 8 8V7H6a1 1 0 1 1 0-2h2zM1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comments" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.75 10L0 13V3a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2H3.75zM13 5h1a2 2 0 0 1 2 2v8l-2.667-2H8a2 2 0 0 1-2-2h4a3 3 0 0 0 3-3V5z"/></symbol><symbol viewBox="0 0 16 16" id="commit" xmlns="http://www.w3.org/2000/svg"><path d="M8 10a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm3.876-1.008a4.002 4.002 0 0 1-7.752 0A1.01 1.01 0 0 1 4 9H1a1 1 0 1 1 0-2h3c.042 0 .083.003.124.008a4.002 4.002 0 0 1 7.752 0A1.01 1.01 0 0 1 12 7h3a1 1 0 0 1 0 2h-3a1.01 1.01 0 0 1-.124-.008z"/></symbol><symbol viewBox="0 0 16 16" id="credit-card" xmlns="http://www.w3.org/2000/svg"><path d="M14 5a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1h12zm0 3H2v3a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V8zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm6.5 8h3a.5.5 0 1 1 0 1h-3a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="cut" xmlns="http://www.w3.org/2000/svg"><rect width="16" height="2" y="7" fill-rule="evenodd" rx="1"/></symbol><symbol viewBox="0 0 16 16" id="dashboard" xmlns="http://www.w3.org/2000/svg"><path d="M7.709 10.021l.696-2.6a.5.5 0 0 1 .966.26l-.657 2.45A2 2 0 0 1 10 12H6a2 2 0 0 1 1.709-1.979zM0 8.9a8 8 0 0 1 15.998 0H16v3.6a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5V8.9zM14 9A6 6 0 1 0 2 9v3.5a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5V9zM3.5 9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm9 0a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-7-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm5 0a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1z"/></symbol><symbol viewBox="0 0 16 16" id="disk" xmlns="http://www.w3.org/2000/svg"><path d="M16 11.764V3a3 3 0 0 0-3-3H3a3 3 0 0 0-3 3v8.764A2.989 2.989 0 0 1 2 11V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v8c.768 0 1.47.289 2 .764zM2 12h12a2 2 0 1 1 0 4H2a2 2 0 1 1 0-4zm10 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="doc_code" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zm1.036 7.607a.498.498 0 0 1-.147.354l-1.414 1.414a.5.5 0 0 1-.707-.707l1.06-1.06-1.06-1.061a.5.5 0 0 1 .707-.707l1.414 1.414a.498.498 0 0 1 .147.353zm-4.822 0l1.06 1.061a.5.5 0 0 1-.706.707l-1.414-1.414a.498.498 0 0 1 0-.707l1.414-1.414a.5.5 0 1 1 .707.707l-1.06 1.06zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="doc_image" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM7.333 9.667l1.313-1.313a.5.5 0 0 1 .708 0L12 11H4l2.188-1.75a.5.5 0 0 1 .624 0l.521.417zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 8a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zM4 11h8v.7a.3.3 0 0 1-.3.3H4.3a.3.3 0 0 1-.3-.3V11z"/></symbol><symbol viewBox="0 0 16 16" id="doc_text" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 11h5a.5.5 0 1 1 0 1h-5a.5.5 0 1 1 0-1zm0-2h5a.5.5 0 1 1 0 1h-5a.5.5 0 0 1 0-1zm0-2h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1z"/></symbol><symbol viewBox="0 0 105 26" id="double-headed-arrow" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1.018 11.089L15.138.614c1.23-.911 3.086-.795 4.147.26.461.46.715 1.045.715 1.651v20.95C20 24.869 18.684 26 17.06 26a3.238 3.238 0 0 1-1.921-.614L1.019 14.911C-.212 14-.347 12.405.714 11.35c.094-.094.195-.18.303-.261zm102.964 0c.108.08.21.167.303.26 1.061 1.056.925 2.65-.303 3.562l-14.12 10.475A3.238 3.238 0 0 1 87.94 26C86.316 26 85 24.87 85 23.475V2.525c0-.606.254-1.192.715-1.65 1.061-1.056 2.917-1.172 4.146-.26l14.12 10.474zM35 17a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm18 0a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm18 0a4 4 0 1 1 0-8 4 4 0 0 1 0 8z"/></symbol><symbol viewBox="0 0 16 16" id="download" xmlns="http://www.w3.org/2000/svg"><path d="M9 12h1a.5.5 0 0 1 .4.8l-2 2.667a.5.5 0 0 1-.8 0l-2-2.667A.5.5 0 0 1 6 12h1V8a1 1 0 1 1 2 0v4zM4 9a1 1 0 1 1 0 2 4 4 0 0 1-1.971-7.481 4 4 0 0 1 6.633-2.505 3.999 3.999 0 0 1 3.82 2.014A4 4 0 0 1 12 11a1 1 0 0 1 0-2 2 2 0 1 0 0-4h-1a2 2 0 0 0-3.112-1.662A2 2 0 1 0 4.268 5H4a2 2 0 1 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M14 10h-3a1 1 0 0 1-1-1V6H8.527A.527.527 0 0 0 8 6.527V13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-3zm-4-7H8.527c-.18 0-.355.013-.527.04V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2v2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h4a3 3 0 0 1 3 3zM8.527 4h2.323a.5.5 0 0 1 .35.143l4.65 4.551a.5.5 0 0 1 .15.357V13a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3V6.527A2.527 2.527 0 0 1 8.527 4z"/></symbol><symbol viewBox="0 0 16 16" id="earth" xmlns="http://www.w3.org/2000/svg"><path d="M8.7 2.04l-.082.177c.283.223.422.413.417.571-.008.237-.311.057-.444.274-.133.218.038.542-.112.637-.15.096-.398-.386-.479-.46-.054-.049-.166-.257-.336-.625l-.216-.225a.844.844 0 0 0-.418-.035c-.177.038-.075.1-.035.132.04.032.32.037.452.2.132.164.03.224-.05.298-.054.05-.157.062-.31.035H5.952l-.402.398.03.325.229.455.324-.463c.008-.206.058-.342.15-.41.14-.1.342-.15.534-.085.191.066-.057.218.011.271.068.053.204-.098.313-.02.11.08.07.155.104.322.036.167.254.114.398.328.144.215.19.29.147.483-.043.195-.168.26-.305.232-.138-.028-.107-.246-.275-.348-.168-.102-.266-.114-.386-.054-.12.06-.016.129.023.235.04.106.274.321.224.43-.05.107-.108.116-.42 0-.21-.077-.414-.007-.615.212l-.76.722c-.153.715-.3 1.13-.44 1.243-.211.17-.177-.483-.483-.656-.306-.174-.494-.047-.8-.07-.307-.023-.42.65-.38.873a.434.434 0 0 0 .221.321c.236-.141.39-.184.465-.128.11.084-.144.267-.074.425.07.158.314.069.386.283.073.213.084.48-.05.706-.135.227-.275.178-.4.053-.127-.126-.033-.375-.255-.704-.223-.329-.381-.337-.63-.787-.158-.287-.35-.743-.575-1.366a6 6 0 0 0 3.21 7.198l.001-.075c0-.577-.004-.944-.012-1.102-.011-.236-.95-.945-1.104-1.2-.154-.256-.34-.595-.355-.746-.016-.151.185-.232.344-.325.16-.093-.11-.367.028-.626.137-.258.395-.438.496-.356.101.081.058.228.267.333.209.104.077-.213.456-.178.38.035.143.201.252.216.11.016.113-.127.299-.143.186-.015.282.445.471.622.19.178.452.008.611.043.159.034.267.09.402.255.136.166-.03.352.073.557.103.205 1.07.22 1.433.255.364.034.371.011.371.324s-.166.314-.453.507c-.286.193-.166.462-.38.762-.212.3-.316.062-.622.14-.306.077-.413.382-.452.568-.039.186-.386.094-.877.232-.29.082-.429.144-.569.204a6.002 6.002 0 0 0 7.682-4.3c-.094-.384-.18-.63-.258-.74-.213-.297-.36.21-.924.49-.564.278-.57-.288-.81-.49-.16-.133-.212-.44-.158-.92-.005-.478.02-.828.077-1.049.057-.221.126-.543.207-.965.351-.373.606-.572.764-.595.237-.034.336.374.658.3a.315.315 0 0 0 .035-.01 5.993 5.993 0 0 0-.475-.824l-.309-.043a.646.646 0 0 0-.332-.117c-.205-.02-.025.128-.089.24-.064.112-.235.724-.437.685-.201-.039-.204-.374-.17-.668.036-.294-.077-.35-.2-.412-.124-.062-.325-.213-.556-.295-.232-.082-.123-.175-.093-.274.03-.1.208-.015.193-.058-.014-.044-.313-.135-.266-.167.03-.02.2-.02.506.003l.216-.012.293-.163a.58.58 0 0 0-.376-.22c-.233-.036-.513-.034-.73-.142-.205-.103-.458-.36-.643-.638A5.965 5.965 0 0 0 8.7 2.04zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 1600 1600" id="ellipsis_v" xmlns="http://www.w3.org/2000/svg"><path d="M1088 1248v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68v-192q0-40 28-68t68-28h192q40 0 68 28t28 68zm0-512v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68V736q0-40 28-68t68-28h192q40 0 68 28t28 68zm0-512v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68V224q0-40 28-68t68-28h192q40 0 68 28t28 68z"/></symbol><symbol viewBox="0 0 18 18" id="emoji_slightly_smiling_face" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369.721.721 0 0 1 .568.047.715.715 0 0 1 .37.445 2.91 2.91 0 0 0 1.084 1.518A2.93 2.93 0 0 0 9 12.75a2.93 2.93 0 0 0 1.775-.58 2.913 2.913 0 0 0 1.084-1.518.711.711 0 0 1 .375-.445.737.737 0 0 1 .575-.047c.195.063.34.186.433.37.094.183.11.372.047.568zM7.5 6c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 6 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 4.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 6 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm6 0c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 12 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 10.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 12 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm3 3a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6A7.29 7.29 0 0 0 9 16.5a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39A7.29 7.29 0 0 0 16.5 9zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 18 18" id="emoji_smile" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369c.195-.062 7.41-.062 7.606 0 .195.063.34.186.433.37.094.183.11.372.047.568zM14 6.37c0 .398-.04.755-.513.755-.473 0-.498-.272-1.237-.272-.74 0-.74.215-1.165.215-.425 0-.585-.3-.585-.698 0-.397.17-.736.513-1.017.341-.281.754-.422 1.237-.422.483 0 .896.14 1.237.422.342.28.513.62.513 1.017zm-6.5 0c0 .398-.04.755-.513.755-.473 0-.498-.272-1.237-.272-.74 0-.74.215-1.165.215-.425 0-.585-.3-.585-.698 0-.397.17-.736.513-1.017.341-.281.754-.422 1.237-.422.483 0 .896.14 1.237.422.342.28.513.62.513 1.017zm9 2.63a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6A7.29 7.29 0 0 0 9 16.5a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39A7.29 7.29 0 0 0 16.5 9zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 18 18" id="emoji_smiley" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369c.195-.062 7.41-.062 7.606 0 .195.063.34.186.433.37.094.183.11.372.047.568h.001zM7.5 6c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 6 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 4.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 6 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm6 0c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 12 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 10.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 12 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm3 3a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6c.92.397 1.91.6 2.912.598a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39c.397-.92.6-1.91.598-2.912zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z"/></symbol><symbol viewBox="0 0 16 16" id="epic" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14.985 8.044l-.757 2.272a1 1 0 0 1-.949.684H1.618a1 1 0 0 1-.894-1.447l.318-.637A2 2 0 0 0 1.618 9h11.661a2 2 0 0 0 1.706-.956zm0 3l-.757 2.272a1 1 0 0 1-.949.684H1.618a1 1 0 0 1-.894-1.447l.318-.637a2 2 0 0 0 .576.084h11.661a2 2 0 0 0 1.706-.956zM3.618 2h10.995a1 1 0 0 1 .948 1.316l-1.333 4a1 1 0 0 1-.949.684H1.618a1 1 0 0 1-.894-1.447l2-4A1 1 0 0 1 3.618 2zm-.382 4h9.322l.667-2H4.236l-1 2z"/></symbol><symbol viewBox="0 0 16 16" id="external-link" xmlns="http://www.w3.org/2000/svg"><path d="M13.121 4.177l-4.95 4.95a1 1 0 1 1-1.414-1.414l4.95-4.95-1.386-1.386a.5.5 0 0 1 .299-.85l4.709-.524a.5.5 0 0 1 .552.552l-.523 4.71a.5.5 0 0 1-.851.297l-1.386-1.385zM12 8.884a1 1 0 0 1 2 0v4a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3v-8a3 3 0 0 1 3-3h4a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-4z"/></symbol><symbol viewBox="0 0 16 16" id="eye" xmlns="http://www.w3.org/2000/svg"><path d="M8 14C4.816 14 2.253 12.284.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2s5.747 1.716 7.607 5.019a2 2 0 0 1 0 1.962C13.747 12.284 11.184 14 8 14zm0-2c2.41 0 4.338-1.29 5.864-4C12.338 5.29 10.411 4 8 4 5.59 4 3.662 5.29 2.136 8 3.662 10.71 5.589 12 8 12zm0-1a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm1-3a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="eye-slash" xmlns="http://www.w3.org/2000/svg"><path d="M13.618 2.62L1.62 14.619a1 1 0 0 1-.985-1.668l1.525-1.526C1.516 10.742.926 9.927.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2c1.074 0 2.076.195 3.006.58l.944-.944a1 1 0 0 1 1.668.985zM8.068 11a3 3 0 0 0 2.931-2.932l-2.931 2.931zm-3.02-2.462a3 3 0 0 1 3.49-3.49l.884-.884A6.044 6.044 0 0 0 8 4C5.59 4 3.662 5.29 2.136 8c.445.79.924 1.46 1.439 2.011l1.473-1.473zm.421 5.06l1.658-1.658c.283.04.575.06.873.06 2.41 0 4.338-1.29 5.864-4a11.023 11.023 0 0 0-1.133-1.664l1.418-1.418a12.799 12.799 0 0 1 1.458 2.1 2 2 0 0 1 0 1.963C13.747 12.284 11.184 14 8 14a7.883 7.883 0 0 1-2.53-.402z"/></symbol><symbol viewBox="0 0 16 16" id="file-addition" xmlns="http://www.w3.org/2000/svg"><path d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3z"/></symbol><symbol viewBox="0 0 16 16" id="file-deletion" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm2 6h6a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="file-modified" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm5 4a3 3 0 1 1 0 6 3 3 0 0 1 0-6z"/></symbol><symbol viewBox="0 0 16 16" id="filter" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 6v9l-3.724-1.862A.5.5 0 0 1 6 12.691V6L1.854 1.854A.5.5 0 0 1 2.207 1h11.586a.5.5 0 0 1 .353.854L10 6z"/></symbol><symbol viewBox="0 0 16 16" id="folder" xmlns="http://www.w3.org/2000/svg"><path d="M13 3a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a2 2 0 0 1 2-2h3.81a3 3 0 0 1 2.827 1.995L13 3z"/></symbol><symbol viewBox="0 0 16 16" id="folder-o" xmlns="http://www.w3.org/2000/svg"><path d="M13 5l-4.365-.005a2 2 0 0 1-1.882-1.33A1 1 0 0 0 5.81 3H2v9a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1zm0-2a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a2 2 0 0 1 2-2h3.81a3 3 0 0 1 2.827 1.995L13 3z"/></symbol><symbol viewBox="0 0 16 16" id="folder-open" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14.59 5.464a2.998 2.998 0 0 1 1.096 3.845l-1.666 3.436A4 4 0 0 1 10.46 15H3a3 3 0 0 1-3-3V3a2 2 0 0 1 2-2h3.558a2 2 0 0 1 1.898 1.368l.21.632h4.973a2 2 0 0 1 2 2 2 2 0 0 1-.027.329l-.023.135zM5.285 7a1 1 0 0 0-.9.564l-1.939 4a1 1 0 0 0 .9 1.436h7.074a2 2 0 0 0 1.8-1.128l1.665-3.436a1 1 0 0 0-.9-1.436h-7.7z"/></symbol><symbol viewBox="0 0 16 16" id="fork" xmlns="http://www.w3.org/2000/svg"><path d="M9 12.268a2 2 0 1 1-2 0V8.874A4.002 4.002 0 0 1 4 5V3.732a2 2 0 1 1 2 0V5a2 2 0 1 0 4 0V3.732a2 2 0 1 1 2 0V5a4.002 4.002 0 0 1-3 3.874v3.394zM11 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm3 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="geo-nodes" xmlns="http://www.w3.org/2000/svg"><path d="M9.7 13.1l-.2.2c-.7.8-2 .9-2.8.1-.1 0-.1-.1-.1-.1l-.2-.2c-2 .2-3.4.7-3.4 1.4 0 .8 2.2 1.5 5 1.5s5-.7 5-1.5c0-.7-1.4-1.2-3.3-1.4M7.3 12.7c.4.4 1 .3 1.4-.1C11.6 9.5 13 7 13 5.3 13 2.4 10.8 0 8 0S3 2.4 3 5.3C3 7 4.4 9.5 7.3 12.7M8 2c1.6 0 3 1.4 3 3.3 0 1-1 2.8-3 5.2-2-2.4-3-4.2-3-5.2C5 3.4 6.4 2 8 2"/><circle cx="8" cy="5" r="1"/></symbol><symbol viewBox="0 0 16 16" id="git-merge" xmlns="http://www.w3.org/2000/svg"><path d="M11 12.268V5a1 1 0 0 0-1-1v1a.5.5 0 0 1-.8.4l-2.667-2a.5.5 0 0 1 0-.8L9.2.6a.5.5 0 0 1 .8.4v1a3 3 0 0 1 3 3v7.268a2 2 0 1 1-2 0zm-6 0a2 2 0 1 1-2 0V4.732a2 2 0 1 1 2 0v7.536zM4 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm8 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="group" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.048 11.997C-.377 11.975.013 11.782.013 10.56.013 9.235.653 8 4 8c.444 0 .84.022 1.194.062.164.435.426.82.76 1.132-1.786.389-2.721 1.353-2.906 2.803zm2.94-7.222a2.993 2.993 0 0 0-.976 1.95 2 2 0 1 1 .975-1.95zm6.964 7.222c-.185-1.45-1.12-2.414-2.906-2.803.334-.311.596-.697.76-1.132C11.16 8.022 11.556 8 12 8c3.346 0 3.987 1.235 3.987 2.56 0 1.222.39 1.415-3.035 1.437zm-1.964-5.272a2.993 2.993 0 0 0-.976-1.95 2 2 0 1 1 .976 1.95zM8 9a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 5c-2.177 0-3.987-.115-3.987-1.44S4.653 10 8 10c3.346 0 3.987 1.235 3.987 2.56S10.177 14 8 14z"/></symbol><symbol viewBox="0 0 16 16" id="history" xmlns="http://www.w3.org/2000/svg"><path d="M2.868 3.24a7 7 0 1 1-.043 9.475 1 1 0 0 1 1.478-1.348 5 5 0 1 0 .124-6.865l.796.645a.5.5 0 0 1-.193.873l-3.232.814a.5.5 0 0 1-.622-.504L1.3 3a.5.5 0 0 1 .814-.37l.754.61zM9 8h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V6a1 1 0 1 1 2 0v2z"/></symbol><symbol viewBox="0 0 16 16" id="home" xmlns="http://www.w3.org/2000/svg"><path d="M9 13h3v-3H4v3h3v-1a1 1 0 0 1 2 0v1zm5-3v3.659c0 .729-.657 1.341-1.5 1.341h-9c-.843 0-1.5-.612-1.5-1.341V10h-.88C.502 10 0 9.486 0 8.853c0-.307.12-.601.333-.816l6.405-6.463a1.56 1.56 0 0 1 2.374-.052L15.66 8.03c.444.441.455 1.167.024 1.622a1.108 1.108 0 0 1-.804.348H14zM7.95 3.273l-4.595 4.64h9.264l-4.67-4.64z"/></symbol><symbol viewBox="0 0 16 16" id="hook" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1h4zm0 1H6v1a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V4zM7 8a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h2a3 3 0 0 1 3 3v2a3 3 0 0 1-3 3v4a2 2 0 1 0 4 0h-.44a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H15a4 4 0 0 1-7 2.646A4 4 0 0 1 1 12H.56a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H3a2 2 0 1 0 4 0V8z"/></symbol><symbol viewBox="0 0 16 16" id="hourglass" xmlns="http://www.w3.org/2000/svg"><path d="M10.331 4.889A2.988 2.988 0 0 0 11 3V2H5v1c0 .362.064.709.182 1.03l5.15.859zM3 14v-1c0-1.78.93-3.342 2.33-4.228.447-.327.67-.582.67-.764 0-.19-.242-.46-.725-.815A4.996 4.996 0 0 1 3 3V2H2a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2h-1v1a4.997 4.997 0 0 1-2.39 4.266c-.407.3-.61.545-.61.734 0 .19.203.434.61.734A4.997 4.997 0 0 1 13 13v1h1a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2h1zm8 0v-1a3 3 0 0 0-6 0v1h6z"/></symbol><symbol viewBox="0 0 38 38" id="image-comment-dark" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle cx="19" cy="19" r="18" fill="#1F78D1"/><path fill="#FFF" fill-rule="nonzero" d="M19 38C8.507 38 0 29.493 0 19S8.507 0 19 0s19 8.507 19 19-8.507 19-19 19zm0-2c9.389 0 17-7.611 17-17S28.389 2 19 2 2 9.611 2 19s7.611 17 17 17zm-6.293-8.293c-.63.63-1.707.184-1.707-.707V15a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-7.586l-3.707 3.707zM13 24.586l2.293-2.293A1 1 0 0 1 16 22h8a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1H14a1 1 0 0 0-1 1v9.586z"/></g></symbol><symbol viewBox="0 0 38 38" id="image-comment-light" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle cx="19" cy="19" r="18" fill="#FFF"/><path fill="#1F78D1" fill-rule="nonzero" d="M19 38C8.507 38 0 29.493 0 19S8.507 0 19 0s19 8.507 19 19-8.507 19-19 19zm0-2c9.389 0 17-7.611 17-17S28.389 2 19 2 2 9.611 2 19s7.611 17 17 17zm-6.293-8.293c-.63.63-1.707.184-1.707-.707V15a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-7.586l-3.707 3.707zM13 24.586l2.293-2.293A1 1 0 0 1 16 22h8a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1H14a1 1 0 0 0-1 1v9.586z"/></g></symbol><symbol viewBox="0 0 16 16" id="import" xmlns="http://www.w3.org/2000/svg"><path d="M9 8h1a.5.5 0 0 1 .4.8l-2 2.667a.5.5 0 0 1-.8 0L5.6 8.8A.5.5 0 0 1 6 8h1V1a1 1 0 1 1 2 0v7zM0 8a1 1 0 1 1 2 0 6 6 0 1 0 12 0 1 1 0 0 1 2 0A8 8 0 1 1 0 8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-block" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.803 8a5.97 5.97 0 0 0-.462 1H4.5a.5.5 0 0 1 0-1h1.303zM4.5 5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1 0-1zm7.5.083a6.04 6.04 0 0 0-2 0V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2.083a5.96 5.96 0 0 0 .72 2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v2.083zm1.121 3.796zM11 16a5 5 0 1 1 0-10 5 5 0 0 1 0 10zm-1.293-2.292a3 3 0 0 0 4.001-4.001l-4.001 4zm-1.415-1.415l4.001-4a3 3 0 0 0-4.001 4.001z"/></symbol><symbol viewBox="0 0 16 16" id="issue-child" xmlns="http://www.w3.org/2000/svg"><path d="M11 8H5v1h1a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h2V7a.997.997 0 0 1 1-1h3V4H4.5a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9v2h3a.997.997 0 0 1 1 1v2h2a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h1V8zm-9 3v2h3v-2H2zm9 0v2h3v-2h-3z"/></symbol><symbol viewBox="0 0 16 16" id="issue-close" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M10.874 2H12a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3h-2c-.918 0-1.74-.413-2.29-1.063a3.987 3.987 0 0 0 1.988-.984A1 1 0 0 0 10 14h2a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1h-1V3c0-.345-.044-.68-.126-1zM4 0h3a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-external" xmlns="http://www.w3.org/2000/svg"><path d="M11 4a5.99 5.99 0 0 0-2 .341V3a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h2.528a6.003 6.003 0 0 0 2.705 1.736A2.99 2.99 0 0 1 8 16H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h4a3 3 0 0 1 3 3v1zM8.212 8.97l-.568-.876A.25.25 0 0 1 7.66 7.8l.404-.5a.25.25 0 0 1 .284-.076l.938.36c.256-.182.543-.325.85-.42l.323-.988a.25.25 0 0 1 .237-.173h.643a.25.25 0 0 1 .238.173l.321.989c.308.094.595.237.852.418l.937-.359a.25.25 0 0 1 .284.076l.404.5a.25.25 0 0 1 .016.293l-.568.875c.113.297.18.616.192.95l.9.54a.25.25 0 0 1 .114.27l-.145.627a.25.25 0 0 1-.221.192l-1.115.098a3.015 3.015 0 0 1-.512.608l.165 1.18a.25.25 0 0 1-.138.259l-.577.282a.25.25 0 0 1-.29-.051l-.874-.905a3.035 3.035 0 0 1-.608 0l-.875.905a.25.25 0 0 1-.29.05l-.577-.281a.25.25 0 0 1-.138-.26L9 12.254a3.015 3.015 0 0 1-.512-.607l-1.114-.098a.25.25 0 0 1-.222-.192l-.145-.627a.25.25 0 0 1 .115-.27l.899-.54c.012-.334.08-.653.192-.95zm2.806 2.034a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="issue-new" xmlns="http://www.w3.org/2000/svg"><path d="M10 2V1a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V4H9a1 1 0 1 1 0-2h1zm0 6a1 1 0 0 1 2 0v5a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h1a1 1 0 1 1 0 2H5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm0-2a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-2a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open-m" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-parent" xmlns="http://www.w3.org/2000/svg"><path d="M11 11H5v1h1.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H3v-2a.997.997 0 0 1 1-1h3V7H5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H9v2h3a.997.997 0 0 1 1 1v2h2.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H11v-1zM6 3v2h4V3H6z"/></symbol><symbol viewBox="0 0 16 16" id="issues" xmlns="http://www.w3.org/2000/svg"><path d="M10.458 15.012l.311.055a3 3 0 0 0 3.476-2.433l1.389-7.879A3 3 0 0 0 13.2 1.28L11.23.933a3.002 3.002 0 0 0-.824-.031c.364.59.58 1.28.593 2.02l1.854.328a1 1 0 0 1 .811 1.158l-1.389 7.879a1 1 0 0 1-1.158.81l-.118-.02a3.98 3.98 0 0 1-.541 1.935zM3 0h4a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="italic" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.5 12l2-8H6a1 1 0 1 1 0-2h6a1 1 0 0 1 0 2h-1.5l-2 8H10a1 1 0 0 1 0 2H4a1 1 0 0 1 0-2h1.5z"/></symbol><symbol viewBox="0 0 16 16" id="key" xmlns="http://www.w3.org/2000/svg"><path d="M7.575 6.689a4.002 4.002 0 0 1 6.274-4.86 4 4 0 0 1-4.86 6.274l-2.21 2.21.706.708a1 1 0 1 1-1.414 1.414l-.707-.707-.707.707.707.707a1 1 0 1 1-1.414 1.414l-.707-.707a1 1 0 0 1-1.414-1.414l5.746-5.746zm2.032-.618a2 2 0 1 0 2.828-2.828A2 2 0 0 0 9.607 6.07z"/></symbol><symbol viewBox="0 0 16 16" id="key-2" xmlns="http://www.w3.org/2000/svg"><path d="M5.172 14.157l-.344.344-2.485.133a.462.462 0 0 1-.497-.503l.14-2.24a.599.599 0 0 1 .177-.382l5.155-5.155a4 4 0 1 1 2.828 2.828l-1.439 1.44-1.06-.354-.708.707.354 1.06-.707.708-1.06-.354-.708.707.354 1.06zm6.01-8.839a1 1 0 1 0 1.414-1.414 1 1 0 0 0-1.414 1.414z"/></symbol><symbol viewBox="0 0 16 16" id="label" xmlns="http://www.w3.org/2000/svg"><path d="M11.782 14.718a3 3 0 0 1-4.242 0L1.652 8.829a2 2 0 0 1-.565-1.702l.54-3.703a2 2 0 0 1 1.69-1.69l3.703-.54a2 2 0 0 1 1.703.564l5.888 5.888a3 3 0 0 1 0 4.243l-2.829 2.829zm1.415-5.657L7.309 3.173l-3.703.54-.54 3.702 5.888 5.888a1 1 0 0 0 1.414 0l2.829-2.828a1 1 0 0 0 0-1.414zM5.732 5.525A1 1 0 1 1 7.146 6.94a1 1 0 0 1-1.414-1.414z"/></symbol><symbol viewBox="0 0 16 16" id="labels" xmlns="http://www.w3.org/2000/svg"><path d="M9.424 2.254l2.08-.905a1 1 0 0 1 1.206.326l3.013 4.12a1 1 0 0 1 .16.849l-1.947 7.264a3 3 0 0 1-3.675 2.122l-.5-.135a3.999 3.999 0 0 0 1.082-1.782 1 1 0 0 0 1.16-.722l1.823-6.802-2.258-3.087-.687.299a2 2 0 0 0-.628-.88l-.829-.667zM.377 3.7L4.4.498a1 1 0 0 1 1.25.003L9.627 3.7a1 1 0 0 1 .373.78V13a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4.482A1 1 0 0 1 .377 3.7zM2 13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V4.958L5.02 2.561 2 4.964V13zm3-6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="leave" xmlns="http://www.w3.org/2000/svg"><path d="M11 7V5.883a.5.5 0 0 1 .757-.429l3.528 2.117a.5.5 0 0 1 0 .858l-3.528 2.117a.5.5 0 0 1-.757-.43V9H7a1 1 0 1 1 0-2h4zm-2 6.256a1 1 0 0 1 2 0A2.744 2.744 0 0 1 8.256 16H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h5.19A2.81 2.81 0 0 1 11 2.81a1 1 0 0 1-2 0A.81.81 0 0 0 8.19 2H3a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h5.256c.41 0 .744-.333.744-.744z"/></symbol><symbol viewBox="0 0 16 16" id="level-up" xmlns="http://www.w3.org/2000/svg"><path fill="#2E2E2E" fill-rule="evenodd" d="M7 6h3.489a.5.5 0 0 0 .373-.832L6.374.117a.5.5 0 0 0-.748 0l-4.488 5.05A.5.5 0 0 0 1.51 6H5v7a3 3 0 0 0 3 3h6a1 1 0 0 0 0-2H8a1 1 0 0 1-1-1V6z"/></symbol><symbol viewBox="0 0 16 16" id="license" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12.56 8.9l2.66 4.606a.3.3 0 0 1-.243.45l-1.678.094a.1.1 0 0 0-.078.044l-.953 1.432a.3.3 0 0 1-.51-.016L9.097 10.9a5.994 5.994 0 0 0 3.464-2zm-5.23 2.063L4.707 15.51a.3.3 0 0 1-.51.016l-.953-1.432a.1.1 0 0 0-.078-.044l-1.678-.094a.3.3 0 0 1-.243-.45l2.48-4.297a5.983 5.983 0 0 0 3.607 1.754zM8 10A5 5 0 1 1 8 0a5 5 0 0 1 0 10zm0-2a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-1a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="link" xmlns="http://www.w3.org/2000/svg"><path d="M6.986 3.35l2.12-2.122a4 4 0 0 1 5.657 5.657l-2.828 2.829a4 4 0 0 1-5.657 0 1 1 0 0 1 1.414-1.415 2 2 0 0 0 2.829 0l2.828-2.828a2 2 0 1 0-2.828-2.828l-1.001 1a5.018 5.018 0 0 0-2.534-.294zm2.12 9.192l-2.12 2.121a4 4 0 1 1-5.658-5.656l2.829-2.829a4 4 0 0 1 5.657 0 1 1 0 1 1-1.415 1.414 2 2 0 0 0-2.828 0l-2.828 2.829a2 2 0 1 0 2.828 2.828l1.001-1.001a5.018 5.018 0 0 0 2.534.294z"/></symbol><symbol viewBox="0 0 16 16" id="list-bulleted" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-7h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm0 5h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm-4 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-2h10a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="list-numbered" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 2h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 0 1 0-2zM1.156 5v-.828h.816V2.204h-.72v-.636c.432-.084.708-.192.996-.372h.756v2.976h.684V5H1.156zm-.18 5v-.588c.9-.828 1.596-1.464 1.596-1.98 0-.342-.192-.504-.468-.504-.252 0-.444.18-.624.36l-.552-.552c.396-.42.756-.612 1.32-.612.768 0 1.308.492 1.308 1.248 0 .612-.576 1.284-1.092 1.812.192-.024.468-.048.636-.048h.636V10H.976zm1.26 5.072c-.618 0-1.068-.204-1.356-.54l.468-.648c.234.216.51.36.78.36.336 0 .552-.12.552-.36 0-.288-.15-.456-.948-.456v-.72c.636 0 .828-.168.828-.432 0-.228-.138-.348-.396-.348-.252 0-.432.108-.672.312l-.516-.624c.372-.312.768-.492 1.236-.492.84 0 1.38.384 1.38 1.074 0 .366-.204.642-.612.822v.024c.432.132.732.432.732.912 0 .72-.684 1.116-1.476 1.116z"/></symbol><symbol viewBox="0 0 16 16" id="location" xmlns="http://www.w3.org/2000/svg"><path d="M8.755 15.144a1 1 0 0 1-1.51 0C3.748 11.114 2 8.065 2 6a6 6 0 1 1 12 0c0 2.065-1.748 5.113-5.245 9.144zM12 6a4 4 0 1 0-8 0c0 1.314 1.312 3.71 4 6.944C10.688 9.71 12 7.314 12 6zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="location-dot" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6.314 13.087C4.382 13.295 3 13.85 3 14.5c0 .828 2.239 1.5 5 1.5s5-.672 5-1.5c0-.65-1.382-1.205-3.314-1.413l-.202.225a2 2 0 0 1-2.968 0l-.202-.225zm2.428-.445a1 1 0 0 1-1.484 0C4.419 9.5 3 7.037 3 5.252 3 2.353 5.239 0 8 0s5 2.352 5 5.253c0 1.784-1.42 4.247-4.258 7.389zM11 5.252C11 3.436 9.634 2 8 2S5 3.435 5 5.253c0 1.027.974 2.824 3 5.203 2.026-2.38 3-4.176 3-5.203zM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="lock" xmlns="http://www.w3.org/2000/svg"><path d="M10 5V4h2v1a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3V4h2v1h4zM4 7a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1H4zm0-3a4 4 0 1 1 8 0h-2a2 2 0 1 0-4 0H4z"/></symbol><symbol viewBox="0 0 16 16" id="lock-open" xmlns="http://www.w3.org/2000/svg"><path d="M4.044 4a4 4 0 0 1 6.99-2.658 1 1 0 1 1-1.495 1.33A2 2 0 0 0 6.044 4a.998.998 0 0 1-.07.367v.701H12a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3v-5a3 3 0 0 1 2.974-3V4h.07zM4 7.07a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="log" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4zm1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-5h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm0 3h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm-3 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-2h3a1 1 0 0 1 0 2H8a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="mail" xmlns="http://www.w3.org/2000/svg"><path d="M14 5.6L9.338 9.796a2 2 0 0 1-2.676 0L2 5.6V11a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5.6zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm.212 2L8 8.31 12.788 4H3.212z"/></symbol><symbol viewBox="0 0 16 16" id="menu" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1.143 2h13.714C15.488 2 16 2.448 16 3s-.512 1-1.143 1H1.143C.512 4 0 3.552 0 3s.512-1 1.143-1zm0 5h13.714C15.488 7 16 7.448 16 8s-.512 1-1.143 1H1.143C.512 9 0 8.552 0 8s.512-1 1.143-1zm0 5h13.714c.631 0 1.143.448 1.143 1s-.512 1-1.143 1H1.143C.512 14 0 13.552 0 13s.512-1 1.143-1z"/></symbol><symbol viewBox="0 0 16 16" id="merge-request-close" xmlns="http://www.w3.org/2000/svg"><path d="M9.414 8l1.414 1.414a1 1 0 1 1-1.414 1.414L8 9.414l-1.414 1.414a1 1 0 1 1-1.414-1.414L6.586 8 5.172 6.586a1 1 0 1 1 1.414-1.414L8 6.586l1.414-1.414a1 1 0 1 1 1.414 1.414L9.414 8zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="messages" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.588 8.942l1.173 5.862A1 1 0 0 1 8.78 16H7.22a1 1 0 0 1-.98-1.196l1.172-5.862a3.014 3.014 0 0 0 1.176 0zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4zM4.464 2.464L5.88 3.88a3 3 0 0 0 0 4.242L4.464 9.536a5 5 0 0 1 0-7.072zm7.072 7.072L10.12 8.12a3 3 0 0 0 0-4.242l1.415-1.415a5 5 0 0 1 0 7.072zM2.343.343l1.414 1.414a6 6 0 0 0 0 8.486l-1.414 1.414a8 8 0 0 1 0-11.314zm11.314 11.314l-1.414-1.414a6 6 0 0 0 0-8.486L13.657.343a8 8 0 0 1 0 11.314z"/></symbol><symbol viewBox="0 0 16 16" id="mobile-issue-close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.657 10.728L2.12 7.192A1 1 0 1 0 .707 8.607l4.243 4.242a.997.997 0 0 0 1.414 0l8.485-8.485a1 1 0 1 0-1.414-1.414l-7.778 7.778z"/></symbol><symbol viewBox="0 0 16 16" id="monitor" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 13v1h3a1 1 0 0 1 0 2H3a1 1 0 0 1 0-2h3v-1H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3h-3zM3 2a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm5.723 6.416l-2.66-1.773-1.71 1.71a.5.5 0 1 1-.707-.707l2-2a.5.5 0 0 1 .631-.062l2.66 1.773 2.71-2.71a.5.5 0 0 1 .707.707l-3 3a.5.5 0 0 1-.631.062z"/></symbol><symbol viewBox="0 0 16 16" id="more" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 4a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="notifications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 14H2.435a2 2 0 0 1-1.761-2.947c.962-1.788 1.521-3.065 1.68-3.832.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024c3.755.528 4.375 4.27 4.761 6.043.188.86.742 2.188 1.661 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0zm5.805-6.468c-.325-1.492-.37-1.674-.61-2.288C10.6 3.716 9.742 3 8.07 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.208 1.012-.827 2.424-1.877 4.375H13.64c-.993-1.937-1.6-3.396-1.835-4.468z"/></symbol><symbol viewBox="0 0 16 16" id="notifications-off" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.26 5.089c.243.757.382 1.478.5 2.017.187.86.74 2.188 1.66 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0H4.35l2-2h7.29c-.993-1.937-1.6-3.396-1.835-4.468-.07-.326-.129-.59-.178-.81l1.634-1.633zM10.943 1.75l-1.48 1.48C9.07 3.076 8.612 3 8.069 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.065.317-.17.673-.317 1.073L.45 12.242a1.99 1.99 0 0 1 .224-1.19c.962-1.787 1.521-3.064 1.68-3.831.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024 4.867 4.867 0 0 1 1.944.688zm2.932-.105a1 1 0 0 1 0 1.415L2.561 14.374a1 1 0 1 1-1.415-1.414L12.46 1.646a1 1 0 0 1 1.414 0z"/></symbol><symbol viewBox="0 0 16 16" id="overview" xmlns="http://www.w3.org/2000/svg"><path d="M2 0h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2h-3zM2 9h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3h-3z"/></symbol><symbol viewBox="0 0 16 16" id="pencil" xmlns="http://www.w3.org/2000/svg"><path d="M13.02 1.293l1.414 1.414a1 1 0 0 1 0 1.414L4.119 14.436a1 1 0 0 1-.704.293l-2.407.008L1 12.316a1 1 0 0 1 .293-.71L11.605 1.292a1 1 0 0 1 1.414 0zm-1.416 1.415l-.707.707L12.31 4.83l.707-.707-1.414-1.415zM3.411 13.73l1.123-1.122H3.12v-1.415L2 12.312l.005 1.422 1.406-.005z"/></symbol><symbol viewBox="0 0 16 16" id="pencil-square" xmlns="http://www.w3.org/2000/svg"><path d="M12 9a1 1 0 0 1 2 0v4a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h4a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V9zm.778-7.179l1.414 1.415-6.476 6.476a1 1 0 0 1-.498.27l-1.51.325.323-1.512a1 1 0 0 1 .27-.497l6.477-6.477zM15.607.407a1 1 0 0 1 0 1.414l-.708.707-1.414-1.414.707-.707a1 1 0 0 1 1.415 0z"/></symbol><symbol viewBox="0 0 16 16" id="pipeline" xmlns="http://www.w3.org/2000/svg"><path d="M8.969 7.25a2 2 0 1 1-1.938 0A1.002 1.002 0 0 1 7 7V5.083a.2.2 0 0 1 .06-.142l.877-.87a.1.1 0 0 1 .141 0l.864.87A.2.2 0 0 1 9 5.083V7c0 .086-.01.17-.031.25zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm4.5-4a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-5 9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zM8 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="play" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.765 15.835c-.545.321-1.258.159-1.593-.363A1.075 1.075 0 0 1 1 14.89V1.11C1 .496 1.518 0 2.158 0c.214 0 .424.057.607.165l11.684 6.89c.544.321.714 1.005.38 1.526a1.135 1.135 0 0 1-.38.364l-11.684 6.89z"/></symbol><symbol viewBox="0 0 16 16" id="plus" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7H2a1 1 0 1 0 0 2h5v5a1 1 0 0 0 2 0V9h5a1 1 0 0 0 0-2H9V2a1 1 0 1 0-2 0v5z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 7V4a1 1 0 1 0-2 0v3H4a1 1 0 1 0 0 2h3v3a1 1 0 0 0 2 0V9h3a1 1 0 0 0 0-2H9zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square-o" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="podcast" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.588 8.942l1.173 5.862a1 1 0 0 1-.785 1.177A1 1 0 0 1 8.78 16H7.22a1 1 0 0 1-1-1 1 1 0 0 1 .02-.196l1.172-5.862a3.014 3.014 0 0 0 1.176 0zM8 7.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zM4.464 2.464A1 1 0 0 1 5.88 3.88a3 3 0 0 0 0 4.242 1 1 0 0 1-1.415 1.415 5 5 0 0 1 0-7.072zm7.072 7.072A1 1 0 0 1 10.12 8.12a3 3 0 0 0 0-4.242 1 1 0 0 1 1.415-1.415 5 5 0 0 1 0 7.072zM2.343.343a1 1 0 1 1 1.414 1.414 6 6 0 0 0 0 8.486 1 1 0 1 1-1.414 1.414 8 8 0 0 1 0-11.314zm11.314 11.314a1 1 0 1 1-1.414-1.414 6 6 0 0 0 0-8.486A1 1 0 0 1 13.657.343a8 8 0 0 1 0 11.314z"/></symbol><symbol viewBox="0 0 16 16" id="preferences" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5 12h10a1 1 0 0 1 0 2H5a1 1 0 0 1-2 0v-2a1 1 0 0 1 2 0zm-3 0H1a1 1 0 0 0 0 2h1v-2zm11-5h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-2 0V7a1 1 0 0 1 2 0zm-3 0H1a1 1 0 1 0 0 2h9V7zM6 2h9a1 1 0 0 1 0 2H6a1 1 0 1 1-2 0V2a1 1 0 1 1 2 0zM3 2H1a1 1 0 1 0 0 2h2V2z"/></symbol><symbol viewBox="0 0 16 16" id="profile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-4.274-3.404C4.412 9.709 5.694 9 8 9c2.313 0 3.595.7 4.28 1.586A4.997 4.997 0 0 1 8 13a4.997 4.997 0 0 1-4.274-2.404zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="project" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177l-.038.044a.505.505 0 0 0 .038-.044zm-.787 0a.5.5 0 0 0 .038.043l-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol><symbol viewBox="0 0 16 16" id="push-rules" xmlns="http://www.w3.org/2000/svg"><path d="M6.268 9a2 2 0 0 1 3.464 0H11a1 1 0 0 1 0 2H9.732a2 2 0 0 1-3.464 0H5a1 1 0 0 1 0-2h1.268zM7 2H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1h-1v3.515a.3.3 0 0 1-.434.268l-1.432-.716a.3.3 0 0 0-.268 0l-1.432.716A.3.3 0 0 1 7 5.515V2zM4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm4 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="question" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm-1.46-5.602h2.233a3.97 3.97 0 0 1 .051-.558c.029-.17.073-.326.133-.469.06-.143.14-.28.242-.41.102-.13.228-.263.38-.399.26-.24.504-.467.733-.683a5.03 5.03 0 0 0 .598-.668c.17-.23.302-.477.399-.742a2.66 2.66 0 0 0 .144-.907c0-.505-.083-.95-.25-1.335a2.55 2.55 0 0 0-.723-.97 3.2 3.2 0 0 0-1.152-.589 5.441 5.441 0 0 0-1.531-.2c-.516 0-.998.063-1.445.188a3.19 3.19 0 0 0-1.168.59c-.331.268-.594.61-.79 1.027-.195.417-.295.917-.3 1.5h2.64c.006-.224.04-.416.102-.578.062-.161.142-.293.238-.394a.921.921 0 0 1 .332-.227 1.04 1.04 0 0 1 .39-.074c.34 0 .593.095.763.285.169.19.254.488.254.895 0 .328-.106.63-.317.906-.21.276-.499.565-.863.867-.214.182-.39.374-.531.574-.141.2-.253.42-.336.657a3.656 3.656 0 0 0-.176.777 7.89 7.89 0 0 0-.05.937zm-.321 2.375c0 .188.035.362.105.524.07.161.17.3.301.418.13.117.284.21.46.277.178.068.376.102.595.102.218 0 .416-.034.593-.102.178-.068.331-.16.461-.277a1.2 1.2 0 0 0 .301-.418c.07-.162.106-.336.106-.524a1.3 1.3 0 0 0-.106-.523 1.2 1.2 0 0 0-.3-.418 1.461 1.461 0 0 0-.462-.277 1.651 1.651 0 0 0-.593-.102c-.22 0-.417.034-.594.102a1.46 1.46 0 0 0-.461.277 1.2 1.2 0 0 0-.3.418 1.284 1.284 0 0 0-.106.523z"/></symbol><symbol viewBox="0 0 16 16" id="question-o" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-.778-4.151c0-.301.014-.575.044-.82a3.2 3.2 0 0 1 .154-.68c.073-.208.17-.4.294-.575.123-.176.278-.343.465-.503a4.81 4.81 0 0 0 .755-.758c.185-.242.277-.506.277-.793 0-.356-.074-.617-.222-.783-.148-.166-.37-.25-.667-.25a.92.92 0 0 0-.342.065.806.806 0 0 0-.29.199 1.04 1.04 0 0 0-.209.345 1.5 1.5 0 0 0-.088.506H5.082c.005-.51.092-.948.263-1.313.171-.364.401-.664.69-.899.29-.234.63-.406 1.023-.516a4.66 4.66 0 0 1 1.264-.164c.497 0 .944.058 1.34.174.397.117.733.289 1.008.517.276.227.487.51.633.847.146.337.218.727.218 1.17 0 .295-.042.56-.126.792a2.52 2.52 0 0 1-.349.65 4.4 4.4 0 0 1-.523.584c-.2.19-.414.389-.642.598a2.73 2.73 0 0 0-.332.349c-.089.114-.16.233-.212.359a1.868 1.868 0 0 0-.116.41 3.39 3.39 0 0 0-.044.489H7.222zm-.28 2.078c0-.164.03-.317.092-.458a1.05 1.05 0 0 1 .263-.366c.114-.103.248-.183.403-.243a1.45 1.45 0 0 1 .52-.089c.191 0 .364.03.52.09.154.059.289.14.403.242.114.103.201.224.263.366.061.141.092.294.092.458 0 .164-.03.316-.092.458a1.05 1.05 0 0 1-.263.365 1.278 1.278 0 0 1-.404.243 1.43 1.43 0 0 1-.52.089c-.19 0-.364-.03-.519-.089-.155-.06-.29-.14-.403-.243a1.05 1.05 0 0 1-.263-.365 1.135 1.135 0 0 1-.093-.458z"/></symbol><symbol viewBox="0 0 16 16" id="quote" xmlns="http://www.w3.org/2000/svg"><path d="M15 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9h-2a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1zM7 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9H3a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="redo" xmlns="http://www.w3.org/2000/svg"><path d="M4.625 4.423A4.897 4.897 0 0 1 8.079 3c2.73 0 4.944 2.239 4.944 5s-2.214 5-4.944 5c-1.41 0-2.723-.6-3.655-1.633a.98.98 0 0 0-1.397-.066 1.008 1.008 0 0 0-.064 1.413A6.87 6.87 0 0 0 8.079 15C11.9 15 15 11.866 15 8s-3.099-7-6.921-7A6.866 6.866 0 0 0 3.08 3.158L1.833 2.137a.49.49 0 0 0-.695.074.504.504 0 0 0-.11.311L1 7.26a.497.497 0 0 0 .6.492l4.576-1.013a.5.5 0 0 0 .206-.877L4.625 4.423z"/></symbol><symbol viewBox="0 0 16 16" id="remove" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 3a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2v10a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V3zm3-2a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1H5zM4 3v10a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V3H4zm2.5 2a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm3 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 16 16" id="repeat" xmlns="http://www.w3.org/2000/svg"><path d="M11.375 4.423A4.897 4.897 0 0 0 7.921 3c-2.73 0-4.944 2.239-4.944 5s2.214 5 4.944 5c1.41 0 2.723-.6 3.655-1.633a.98.98 0 0 1 1.397-.066c.403.373.432 1.005.064 1.413A6.87 6.87 0 0 1 7.921 15C4.1 15 1 11.866 1 8s3.099-7 6.921-7c1.915 0 3.706.792 4.999 2.158l1.247-1.021a.49.49 0 0 1 .695.074c.07.088.11.198.11.311L15 7.26a.497.497 0 0 1-.6.492L9.824 6.739a.5.5 0 0 1-.206-.877l1.757-1.439z"/></symbol><symbol viewBox="0 0 16 16" id="retry" xmlns="http://www.w3.org/2000/svg"><path d="M4.114 6.958a4 4 0 0 0 5.283 4.775 1 1 0 1 1 .712 1.87A6 6 0 0 1 2.182 6.44l-.741-.2a.5.5 0 0 1-.12-.915l2.195-1.268a.5.5 0 0 1 .683.183l1.268 2.196a.5.5 0 0 1-.563.733l-.79-.212zm7.777 2.084a4 4 0 0 0-5.284-4.775 1 1 0 0 1-.712-1.87 6 6 0 0 1 7.927 7.162l.742.2a.5.5 0 0 1 .12.915l-2.196 1.268a.5.5 0 0 1-.683-.183l-1.267-2.196a.5.5 0 0 1 .562-.733l.79.212z"/></symbol><symbol viewBox="0 0 16 16" id="scale" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.99 9a.792.792 0 0 0-.078-.231L13 7l-.912 1.769a.791.791 0 0 0-.077.231h1.978zm-10 0a.792.792 0 0 0-.078-.231L3 7l-.912 1.769A.791.791 0 0 0 2.011 9h1.978zM2 0h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm3 14h6a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2zM8 4a1 1 0 0 1 1 1v9H7V5a1 1 0 0 1 1-1zm-4.53-.714l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 3 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158L2.53 3.286a.53.53 0 0 1 .94 0zm10 0l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 13 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158l2.266-4.735a.53.53 0 0 1 .94 0z"/></symbol><symbol viewBox="0 0 16 16" id="screen-full" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14 14v-2a1 1 0 0 1 2 0v3a.997.997 0 0 1-1 1h-3a1 1 0 0 1 0-2h2zM2 14v-2a1 1 0 0 0-2 0v3a1 1 0 0 0 1 1h3a1 1 0 0 0 0-2H2zM15.707.293A.997.997 0 0 1 16 1v3a1 1 0 0 1-2 0V2h-2a1 1 0 0 1 0-2h3c.276 0 .526.112.707.293zM2 2v2a1 1 0 1 1-2 0V1a.997.997 0 0 1 1-1h3a1 1 0 1 1 0 2H2zm4 4h4a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="screen-normal" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3 3V1a1 1 0 1 1 2 0v3a.997.997 0 0 1-1 1H1a1 1 0 1 1 0-2h2zm10 0h2a1 1 0 0 1 0 2h-3a.997.997 0 0 1-1-1V1a1 1 0 0 1 2 0v2zM3 13H1a1 1 0 0 1 0-2h3a.997.997 0 0 1 1 1v3a1 1 0 0 1-2 0v-2zm10 0v2a1 1 0 0 1-2 0v-3a.997.997 0 0 1 1-1h3a1 1 0 0 1 0 2h-2zM6.5 7h3a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 12 16" id="scroll_down" xmlns="http://www.w3.org/2000/svg"><path class="fbfirst-triangle" d="M1.048 14.155a.508.508 0 0 0-.32.105c-.091.07-.136.154-.136.25v.71c0 .095.045.178.135.249.09.07.197.105.321.105h10.043a.51.51 0 0 0 .321-.105c.09-.07.136-.154.136-.25v-.71c0-.095-.045-.178-.136-.249a.508.508 0 0 0-.32-.105"/><path class="fbsecond-triangle" d="M.687 8.027c-.09-.087-.122-.16-.093-.22.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 12.91a.458.458 0 0 1-.136.089h-.37a.626.626 0 0 1-.136-.09"/><path class="fbthird-triangle" d="M.687 1.027C.597.94.565.867.594.807c.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 5.91a.458.458 0 0 1-.136.09h-.37a.626.626 0 0 1-.136-.09"/></symbol><symbol viewBox="0 0 12 16" id="scroll_up" xmlns="http://www.w3.org/2000/svg"><path d="M1.048 1.845a.508.508 0 0 1-.32-.105c-.091-.07-.136-.154-.136-.25V.78c0-.095.045-.178.135-.249a.508.508 0 0 1 .321-.105h10.043a.51.51 0 0 1 .321.105c.09.07.136.154.136.25v.71c0 .095-.045.178-.136.249a.508.508 0 0 1-.32.105M.687 7.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 3.09A.458.458 0 0 0 6.257 3h-.37a.626.626 0 0 0-.136.09M.687 14.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 10.09a.458.458 0 0 0-.136-.09h-.37a.626.626 0 0 0-.136.09"/></symbol><symbol viewBox="0 0 16 16" id="search" xmlns="http://www.w3.org/2000/svg"><path d="M8.853 8.854a3.5 3.5 0 1 0-4.95-4.95 3.5 3.5 0 0 0 4.95 4.95zm.207 2.328a5.5 5.5 0 1 1 2.121-2.121l3.329 3.328a1.5 1.5 0 0 1-2.121 2.121L9.06 11.182z"/></symbol><symbol viewBox="0 0 16 16" id="settings" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.415 5.803L1.317 4.084A.5.5 0 0 1 1.35 3.5l.805-.994a.5.5 0 0 1 .564-.153l1.878.704a5.975 5.975 0 0 1 1.65-.797L6.885.342A.5.5 0 0 1 7.36 0h1.28a.5.5 0 0 1 .474.342l.639 1.918a5.97 5.97 0 0 1 1.65.797l1.877-.704a.5.5 0 0 1 .565.153l.805.994a.5.5 0 0 1 .032.584l-1.097 1.719c.217.551.354 1.143.399 1.76l1.731 1.058a.5.5 0 0 1 .227.54l-.288 1.246a.5.5 0 0 1-.44.385l-2.008.19a6.026 6.026 0 0 1-1.142 1.431l.265 1.995a.5.5 0 0 1-.277.516l-1.15.56a.5.5 0 0 1-.576-.1l-1.424-1.452a6.047 6.047 0 0 1-1.804 0l-1.425 1.453a.5.5 0 0 1-.576.1l-1.15-.561a.5.5 0 0 1-.276-.516l.265-1.995a6.026 6.026 0 0 1-1.143-1.43l-2.008-.191a.5.5 0 0 1-.44-.385L.058 9.16a.5.5 0 0 1 .226-.539l1.732-1.058a5.968 5.968 0 0 1 .399-1.76zM8 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="shield" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8c1.657 0 3 1.373 3 3.067v7.346c0 1.065-.54 2.053-1.426 2.611l-4 2.52a2.944 2.944 0 0 1-3.148 0l-4-2.52A3.083 3.083 0 0 1 1 10.414V3.066C1 1.373 2.343 0 4 0zm0 2.045c-.552 0-1 .457-1 1.022v7.346c0 .355.18.685.475.87l4 2.52a.981.981 0 0 0 1.05 0l4-2.52c.295-.185.475-.515.475-.87V3.067c0-.565-.448-1.022-1-1.022H4zm0 1.533c0-.282.224-.511.5-.511h4V12.1a.52.52 0 0 1-.069.258.494.494 0 0 1-.684.183l-3.5-2.098a.513.513 0 0 1-.247-.44V3.577z"/></symbol><symbol viewBox="0 0 16 16" id="slight-frown" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-2.163-3.275a2.499 2.499 0 0 1 4.343.03.5.5 0 0 1-.871.49 1.5 1.5 0 0 0-2.607-.018.5.5 0 1 1-.865-.502zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="slight-smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-5.163 2.254a.5.5 0 1 1 .865-.502 1.499 1.499 0 0 0 2.607-.018.5.5 0 1 1 .871.49 2.499 2.499 0 0 1-4.343.03z"/></symbol><symbol viewBox="0 0 16 16" id="smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM6.18 6.27a.5.5 0 0 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zm6 0a.5.5 0 1 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zM5 9a3 3 0 0 0 6 0H5z"/></symbol><symbol viewBox="0 0 16 16" id="smiley" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM5 9h6a3 3 0 0 1-6 0z"/></symbol><symbol viewBox="0 0 16 16" id="snippet" xmlns="http://www.w3.org/2000/svg"><path d="M10.67 9.31a3.001 3.001 0 0 1 2.062 5.546 3 3 0 0 1-3.771-4.559 1.007 1.007 0 0 1-.095-.137l-4.5-7.794a1 1 0 0 1 1.732-1l4.5 7.794c.028.05.052.1.071.15zm-3.283.35l-.289.5c-.028.05-.06.095-.095.137a3.001 3.001 0 0 1-3.77 4.56A3 3 0 0 1 5.294 9.31c.02-.051.043-.102.071-.15l.866-1.5 1.155 2zm2.31-4l-1.156-2 1.325-2.294a1 1 0 0 1 1.732 1L9.696 5.66zm-5.465 7.464a1 1 0 1 0 1-1.732 1 1 0 0 0-1 1.732zm7.5 0a1 1 0 1 0-1-1.732 1 1 0 0 0 1 1.732z"/></symbol><symbol viewBox="0 0 16 16" id="spam" xmlns="http://www.w3.org/2000/svg"><path d="M8.75.433l5.428 3.134a1.5 1.5 0 0 1 .75 1.299v6.268a1.5 1.5 0 0 1-.75 1.299L8.75 15.567a1.5 1.5 0 0 1-1.5 0l-5.428-3.134a1.5 1.5 0 0 1-.75-1.299V4.866a1.5 1.5 0 0 1 .75-1.299L7.25.433a1.5 1.5 0 0 1 1.5 0zM3.072 5.155v5.69L8 13.691l4.928-2.846v-5.69L8 2.309 3.072 5.155zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 14 14" id="spinner" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle cx="7" cy="7" r="6" stroke="#000" stroke-opacity=".1" stroke-width="2"/><path fill="#000" fill-opacity=".1" fill-rule="nonzero" d="M7 0a7 7 0 0 1 7 7h-2a5 5 0 0 0-5-5V0z"/></g></symbol><symbol viewBox="0 0 16 16" id="staged" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 3h4a1 1 0 1 1 0 2H2a1 1 0 1 1 0-2zm9 6a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM2 7h4a1 1 0 1 1 0 2H2a1 1 0 1 1 0-2zm0 4h12a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="star" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.609 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 16 16" id="star-o" xmlns="http://www.w3.org/2000/svg"><path d="M10.975 10.99a3 3 0 0 1 .655-2.083l1.54-1.916-2.219-.576a3 3 0 0 1-1.825-1.37L8 3.15 6.874 5.044a3 3 0 0 1-1.825 1.371l-2.218.576 1.54 1.916a3 3 0 0 1 .654 2.083l-.165 2.4 1.965-.836a3 3 0 0 1 2.348 0l1.965.836-.164-2.399zM7.61 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 14 14" id="status_canceled" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M5.2 3.8l4.9 4.9c.2.2.2.5 0 .7l-.7.7c-.2.2-.5.2-.7 0L3.8 5.2c-.2-.2-.2-.5 0-.7l.7-.7c.2-.2.5-.2.7 0"/></g></symbol><symbol viewBox="0 0 22 22" id="status_canceled_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M8.171 5.971l7.7 7.7a.76.76 0 0 1 0 1.1l-1.1 1.1a.76.76 0 0 1-1.1 0l-7.7-7.7a.76.76 0 0 1 0-1.1l1.1-1.1a.76.76 0 0 1 1.1 0"/></symbol><symbol viewBox="0 0 16 16" id="status_closed" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.83a1 1 0 0 1 1.414 1.416l-3.535 3.535a1 1 0 0 1-1.415.001l-2.12-2.12a1 1 0 1 1 1.413-1.415zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 14 14" id="status_created" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><circle cx="7" cy="7" r="3.25"/></g></symbol><symbol viewBox="0 0 22 22" id="status_created_borderless" xmlns="http://www.w3.org/2000/svg"><circle cx="11" cy="11" r="5.107"/></symbol><symbol viewBox="0 0 14 14" id="status_failed" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 5.969L5.599 4.568a.29.29 0 0 0-.413.004l-.614.614a.294.294 0 0 0-.004.413L5.968 7l-1.4 1.401a.29.29 0 0 0 .004.413l.614.614c.113.114.3.117.413.004L7 8.032l1.401 1.4a.29.29 0 0 0 .413-.004l.614-.614a.294.294 0 0 0 .004-.413L8.032 7l1.4-1.401a.29.29 0 0 0-.004-.413l-.614-.614a.294.294 0 0 0-.413-.004L7 5.968z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_failed_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M11 9.38L8.798 7.178a.455.455 0 0 0-.65.006l-.964.965a.462.462 0 0 0-.006.65L9.38 11l-2.202 2.202a.455.455 0 0 0 .006.65l.965.964a.462.462 0 0 0 .65.006L11 12.62l2.202 2.202a.455.455 0 0 0 .65-.006l.964-.965a.462.462 0 0 0 .006-.65L12.62 11l2.202-2.202a.455.455 0 0 0-.006-.65l-.965-.964a.462.462 0 0 0-.65-.006L11 9.38z"/></symbol><symbol viewBox="0 0 14 14" id="status_manual" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M10.5 7.63V6.37l-.787-.13c-.044-.175-.132-.349-.263-.61l.481-.652-.918-.913-.657.478a2.346 2.346 0 0 0-.612-.26L7.656 3.5H6.388l-.132.783c-.219.043-.394.13-.612.26l-.657-.478-.918.913.437.652c-.131.218-.175.392-.262.61l-.744.086v1.261l.787.13c.044.218.132.392.263.61l-.438.651.92.913.655-.434c.175.086.394.173.613.26l.131.783h1.313l.131-.783c.219-.043.394-.13.613-.26l.656.478.918-.913-.48-.652c.13-.218.218-.435.262-.61l.656-.13zM7 8.283a1.285 1.285 0 0 1-1.313-1.305c0-.739.57-1.304 1.313-1.304.744 0 1.313.565 1.313 1.304 0 .74-.57 1.305-1.313 1.305z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_manual_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M16.5 11.99v-1.98l-1.238-.206c-.068-.273-.206-.546-.412-.956l.756-1.025-1.444-1.435-1.03.752a3.686 3.686 0 0 0-.963-.41L12.03 5.5h-1.994l-.206 1.23c-.343.068-.618.205-.962.41l-1.031-.752-1.444 1.435.687 1.025c-.206.341-.275.615-.412.956L5.5 9.941v1.981l1.237.205c.07.342.207.615.413.957l-.688 1.025 1.444 1.434 1.032-.683c.274.137.618.274.962.41l.206 1.23h2.063l.206-1.23c.344-.068.619-.205.963-.41l1.03.752 1.444-1.435-.756-1.025c.207-.341.344-.683.413-.956l1.031-.205zM11 13.017c-1.169 0-2.063-.889-2.063-2.05 0-1.162.894-2.05 2.063-2.05s2.063.888 2.063 2.05c0 1.161-.894 2.05-2.063 2.05z"/></symbol><symbol viewBox="0 0 14 14" id="status_notfound" xmlns="http://www.w3.org/2000/svg"><path d="M7 14A7 7 0 1 1 7 0a7 7 0 0 1 0 14z"/><path d="M7 13A6 6 0 1 0 7 1a6 6 0 0 0 0 12z" fill="#FFF"/><path d="M8.16 7.184c.519-.37.904-.857 1.07-1.477.384-1.427-.619-2.897-2.246-2.897-.732 0-1.327.26-1.766.692a2.163 2.163 0 0 0-.509.743.75.75 0 0 0 1.4.54.78.78 0 0 1 .16-.213c.168-.165.39-.262.715-.262.597 0 .936.496.798 1.007-.067.249-.235.462-.492.644-.231.165-.47.264-.601.3a.75.75 0 0 0-.556.724v1.421a.75.75 0 0 0 1.5 0v-.909a3.74 3.74 0 0 0 .526-.313z"/><ellipse cx="6.889" cy="10.634" rx="1" ry="1"/></symbol><symbol viewBox="0 0 22 22" id="status_notfound_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M12.822 11.29c.816-.581 1.421-1.348 1.683-2.322.603-2.243-.973-4.553-3.53-4.553-1.15 0-2.085.41-2.775 1.089-.42.413-.672.835-.8 1.167a1.179 1.179 0 0 0 2.2.847c.016-.043.1-.184.252-.334.264-.259.613-.412 1.123-.412.938 0 1.47.78 1.254 1.584-.105.39-.37.726-.773 1.012a3.25 3.25 0 0 1-.945.47 1.179 1.179 0 0 0-.874 1.138v2.234a1.179 1.179 0 1 0 2.358 0v-1.43a5.9 5.9 0 0 0 .827-.492z"/><ellipse cx="10.825" cy="16.711" rx="1.275" ry="1.322"/></symbol><symbol viewBox="0 0 14 14" id="status_open" xmlns="http://www.w3.org/2000/svg"><path d="M0 7c0-3.866 3.142-7 7-7 3.866 0 7 3.142 7 7 0 3.866-3.142 7-7 7-3.866 0-7-3.142-7-7z"/><path d="M1 7c0 3.309 2.69 6 6 6 3.309 0 6-2.69 6-6 0-3.309-2.69-6-6-6-3.309 0-6 2.69-6 6z" fill="#FFF"/><path d="M7 9.219a2.218 2.218 0 1 0 0-4.436A2.218 2.218 0 0 0 7 9.22zm0 1.12a3.338 3.338 0 1 1 0-6.676 3.338 3.338 0 0 1 0 6.676z"/></symbol><symbol viewBox="0 0 14 14" id="status_pending" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M4.7 5.3c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H5c-.2 0-.3-.1-.3-.3V5.3m3 0c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H8c-.2 0-.3-.1-.3-.3V5.3"/></g></symbol><symbol viewBox="0 0 22 22" id="status_pending_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M7.386 8.329c0-.315.157-.472.471-.472h1.414c.315 0 .472.157.472.472v5.342c0 .315-.157.472-.472.472H7.857c-.314 0-.471-.157-.471-.472V8.33m4.714 0c0-.315.157-.472.471-.472h1.415c.314 0 .471.157.471.472v5.342c0 .315-.157.472-.471.472H12.57c-.314 0-.471-.157-.471-.472V8.33"/></symbol><symbol viewBox="0 0 14 14" id="status_running" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 3c2.2 0 4 1.8 4 4s-1.8 4-4 4c-1.3 0-2.5-.7-3.3-1.7L7 7V3"/></g></symbol><symbol viewBox="0 0 22 22" id="status_running_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M11 4.714c3.457 0 6.286 2.829 6.286 6.286 0 3.457-2.829 6.286-6.286 6.286-2.043 0-3.929-1.1-5.186-2.672L11 11V4.714"/></symbol><symbol viewBox="0 0 14 14" id="status_skipped" xmlns="http://www.w3.org/2000/svg"><path d="M7 14A7 7 0 1 1 7 0a7 7 0 0 1 0 14z"/><path d="M7 13A6 6 0 1 0 7 1a6 6 0 0 0 0 12z" fill="#FFF"/><path d="M6.415 7.04L4.579 5.203a.295.295 0 0 1 .004-.416l.349-.349a.29.29 0 0 1 .416-.004l2.214 2.214a.289.289 0 0 1 .019.021l.132.133c.11.11.108.291 0 .398L5.341 9.573a.282.282 0 0 1-.398 0l-.331-.331a.285.285 0 0 1 0-.399L6.415 7.04zm2.54 0L7.119 5.203a.295.295 0 0 1 .004-.416l.349-.349a.29.29 0 0 1 .416-.004l2.214 2.214a.289.289 0 0 1 .019.021l.132.133c.11.11.108.291 0 .398L7.881 9.573a.282.282 0 0 1-.398 0l-.331-.331a.285.285 0 0 1 0-.399L8.955 7.04z"/></symbol><symbol viewBox="0 0 22 22" id="status_skipped_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M14.072 11.063l-2.82 2.82a.46.46 0 0 0-.001.652l.495.495a.457.457 0 0 0 .653-.001l3.7-3.7a.46.46 0 0 0 .001-.653l-.196-.196a.453.453 0 0 0-.03-.033l-3.479-3.479a.464.464 0 0 0-.654.007l-.548.548a.463.463 0 0 0-.007.654l2.886 2.886z"/><path d="M10.08 11.063l-2.819 2.82a.46.46 0 0 0-.002.652l.496.495a.457.457 0 0 0 .652-.001l3.7-3.7a.46.46 0 0 0 .002-.653l-.196-.196a.453.453 0 0 0-.03-.033l-3.48-3.479a.464.464 0 0 0-.653.007l-.548.548a.463.463 0 0 0-.007.654l2.886 2.886z"/></symbol><symbol viewBox="0 0 14 14" id="status_success" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_success_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M9.866 12.095l-1.95-1.95a.462.462 0 0 0-.647.01l-.964.964a.46.46 0 0 0-.01.646l3.013 3.014a.787.787 0 0 0 1.106.008l.425-.425 4.854-4.853a.462.462 0 0 0 .002-.659l-.964-.964a.468.468 0 0 0-.658.002l-4.207 4.207z"/></symbol><symbol viewBox="0 0 14 14" id="status_success_solid" xmlns="http://www.w3.org/2000/svg"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7zm6.278.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 14 14" id="status_warning" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6 3.5c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v4c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-4m0 6c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v1c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-1"/></g></symbol><symbol viewBox="0 0 22 22" id="status_warning_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M9.429 5.5c0-.471.314-.786.785-.786h1.572c.471 0 .785.315.785.786v6.286c0 .471-.314.785-.785.785h-1.572c-.471 0-.785-.314-.785-.785V5.5m0 9.429c0-.472.314-.786.785-.786h1.572c.471 0 .785.314.785.786V16.5c0 .471-.314.786-.785.786h-1.572c-.471 0-.785-.315-.785-.786v-1.571"/></symbol><symbol viewBox="0 0 16 16" id="stop" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 0h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2z"/></symbol><symbol viewBox="0 0 16 16" id="task-done" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="template" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm.8 2h2.4a.8.8 0 0 1 .8.8v1.4a.8.8 0 0 1-.8.8H3.8a.8.8 0 0 1-.8-.8V4.8a.8.8 0 0 1 .8-.8zm4.7 0h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm0 2h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm-5 3h9a.5.5 0 1 1 0 1h-9a.5.5 0 0 1 0-1zm0 2h9a.5.5 0 1 1 0 1h-9a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="terminal" xmlns="http://www.w3.org/2000/svg"><path d="M7 8a.997.997 0 0 1-.293.707l-1.414 1.414a1 1 0 1 1-1.414-1.414L4.586 8l-.707-.707a1 1 0 1 1 1.414-1.414l1.414 1.414A.997.997 0 0 1 7 8zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm0 2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H4zm5 7h2a1 1 0 0 1 0 2H9a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="thumb-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 11h5.282a2 2 0 0 0 1.963-2.38l-.563-2.905a3 3 0 0 0-.243-.732l-1.103-2.286A3 3 0 0 0 10.964 1H7a3 3 0 0 0-3 3v6.3a2 2 0 0 0 .436 1.247l3.11 3.9a.632.632 0 0 0 .941.053l.137-.137a1 1 0 0 0 .28-.87L8.329 11zM1 10h2V3H1a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="thumb-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 5h5.282a2 2 0 0 1 1.963 2.38l-.563 2.905a3 3 0 0 1-.243.732l-1.103 2.286A3 3 0 0 1 10.964 15H7a3 3 0 0 1-3-3V5.7a2 2 0 0 1 .436-1.247l3.11-3.9A.632.632 0 0 1 8.487.5l.137.137a1 1 0 0 1 .28.87L8.329 5zM1 6h2v7H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="thumbtack" xmlns="http://www.w3.org/2000/svg"><path d="M7.125 9h-2.19a.5.5 0 0 1-.417-.777L6 6V2L5.362.724A.5.5 0 0 1 5.809 0h4.382a.5.5 0 0 1 .447.724L10 2v4l1.482 2.223a.5.5 0 0 1-.416.777H8.875L8 16l-.875-7z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 16 16" id="timer" xmlns="http://www.w3.org/2000/svg"><path d="M12.022 3.27l.77-.77a1 1 0 0 1 1.415 1.414l-.728.729a7 7 0 1 1-1.456-1.372zM8 14A5 5 0 1 0 8 4a5 5 0 0 0 0 10zm0-9a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zM6 0h4a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-add" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 4V2a1 1 0 0 1 2 0v2h2a1 1 0 0 1 0 2h-2v2a1 1 0 0 1-2 0V6H8a1 1 0 1 1 0-2h2zm2 7a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-done" xmlns="http://www.w3.org/2000/svg"><path d="M8.243 7.485l4.95-4.95a1 1 0 1 1 1.414 1.415L8.95 9.607a.997.997 0 0 1-1.414 0L4.707 6.778a1 1 0 0 1 1.414-1.414l2.122 2.121zM12 11a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="token" xmlns="http://www.w3.org/2000/svg"><path d="M3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H3zm1 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="unapproval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11.95 8.536l1.06-1.061a1 1 0 0 1 1.415 1.414l-1.061 1.06 1.06 1.061a1 1 0 0 1-1.414 1.415l-1.06-1.061-1.06 1.06a1 1 0 1 1-1.415-1.414l1.06-1.06-1.06-1.06a1 1 0 0 1 1.414-1.415l1.06 1.06zm-3.768-.33c.006.503.201 1.006.586 1.39l.353.354-.353.353a2 2 0 1 0 2.828 2.829l.354-.354.047.048C11.964 14.363 11.527 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.834 0 1.557.074 2.182.205zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="unassignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11 5h4a1 1 0 0 1 0 2h-4a1 1 0 0 1 0-2zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="unlink" xmlns="http://www.w3.org/2000/svg"><path d="M11.295 8.845l-.659-1.664a1.78 1.78 0 0 0 .04-.04l1.415-1.414c.586-.586.654-1.468.152-1.97s-1.384-.434-1.97.152L8.859 5.323a1.781 1.781 0 0 0-.04.04l-1.664-.658c.141-.208.305-.408.491-.594l1.415-1.414c1.366-1.367 3.424-1.525 4.596-.354 1.171 1.172 1.013 3.23-.354 4.596L11.89 8.354c-.186.186-.386.35-.594.491zm-2.45 2.45a4.075 4.075 0 0 1-.491.594l-1.415 1.414c-1.366 1.367-3.424 1.525-4.596.354-1.171-1.172-1.013-3.23.354-4.596L4.11 7.646c.186-.186.386-.35.594-.491l.659 1.664a1.781 1.781 0 0 0-.04.04l-1.415 1.414c-.586.586-.654 1.468-.152 1.97s1.384.434 1.97-.152l1.414-1.414a1.78 1.78 0 0 0 .04-.04l1.664.658zm3.812-2.088h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-.05a.5.5 0 0 1 .5-.5zm-.384 2.116l1.415 1.414a.5.5 0 0 1 0 .708l-.037.036a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 0-.707l.036-.037a.5.5 0 0 1 .707 0zm-2.823 1.09a.5.5 0 0 1 .5-.5h.052a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9.95a.5.5 0 0 1-.5-.5v-2zm-2.748-9.16a.5.5 0 0 1-.5.5h-.05a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h.05a.5.5 0 0 1 .5.5v2zm-2.116.383a.5.5 0 0 1 0 .707l-.036.036a.5.5 0 0 1-.707 0L2.428 2.965a.5.5 0 0 1 0-.707l.037-.036a.5.5 0 0 1 .707 0l1.414 1.414zm-1.09 2.823h-2a.5.5 0 0 1-.5-.5v-.051a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5z"/></symbol><symbol viewBox="0 0 16 16" id="unstaged" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 3h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm0 4h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm0 4h12a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="user" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 8c-6.888 0-6.976-.78-6.976-2.52S2.144 8 8 8s6.976 2.692 6.976 4.48c0 1.788-.088 2.52-6.976 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="users" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.521 8.01C15.103 8.19 16 10.755 16 12.48c0 1.533-.056 2.29-3.808 2.475.609-.54.808-1.331.808-2.475 0-1.911-.804-3.503-2.479-4.47zm-1.67-1.228A3.987 3.987 0 0 0 9.976 4a3.987 3.987 0 0 0-1.125-2.782 3 3 0 1 1 0 5.563zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="volume-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 5h1v6H1a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1zm2 0l4.445-2.964A1 1 0 0 1 9 2.87v10.26a1 1 0 0 1-1.555.833L3 11V5zm10.283 7.89a.5.5 0 0 1-.66-.752A5.485 5.485 0 0 0 14.5 8c0-1.601-.687-3.09-1.865-4.128a.5.5 0 0 1 .661-.75A6.484 6.484 0 0 1 15.5 8a6.485 6.485 0 0 1-2.217 4.89zm-2.002-2.236a.5.5 0 1 1-.652-.758c.55-.472.871-1.157.871-1.896 0-.732-.315-1.411-.856-1.883a.5.5 0 0 1 .658-.753A3.492 3.492 0 0 1 12.5 8c0 1.033-.45 1.994-1.219 2.654z"/></symbol><symbol viewBox="0 0 16 16" id="warning" xmlns="http://www.w3.org/2000/svg"><path d="M15.572 10.506c.867 1.42.375 3.247-1.098 4.082a3.184 3.184 0 0 1-1.57.412h-9.81C1.387 15 0 13.665 0 12.018a2.9 2.9 0 0 1 .427-1.512L5.332 2.47C6.2 1.05 8.096.577 9.57 1.412c.453.257.831.622 1.098 1.059l4.905 8.035zM8.89 3.479a1.014 1.014 0 0 0-.366-.353 1.053 1.053 0 0 0-1.412.353l-4.905 8.035a.967.967 0 0 0-.143.504c0 .549.462.994 1.032.994h9.81c.184 0 .364-.048.523-.137a.974.974 0 0 0 .366-1.361L8.889 3.479zM8 5a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zm0 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="work" xmlns="http://www.w3.org/2000/svg"><path d="M12 3h1a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h1V2a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1zM6 2v1h4V2H6zM3 5a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H3zm1.5 1a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm7 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol></svg> \ No newline at end of file
+<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol viewBox="0 0 16 16" id="abuse" xmlns="http://www.w3.org/2000/svg"><path d="M11.408.328l4.029 3.222A1.5 1.5 0 0 1 16 4.72v6.555a1.5 1.5 0 0 1-.563 1.171l-4.026 3.224a1.5 1.5 0 0 1-.937.329H5.529a1.5 1.5 0 0 1-.937-.328L.563 12.45A1.5 1.5 0 0 1 0 11.28V4.724a1.5 1.5 0 0 1 .563-1.171L4.589.329A1.5 1.5 0 0 1 5.526 0h4.945c.34 0 .67.116.937.328zM10.296 2H5.702L2 4.964v6.074L5.704 14h4.594L14 11.036V4.962L10.296 2zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="account" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.195 9.965l-.568-.875a.25.25 0 0 1 .015-.294l.405-.5a.25.25 0 0 1 .283-.075l.938.36c.257-.183.543-.325.851-.42l.322-.988A.25.25 0 0 1 11.679 7h.642a.25.25 0 0 1 .238.173l.322.988c.308.095.594.237.851.42l.938-.36a.25.25 0 0 1 .283.076l.405.5a.25.25 0 0 1 .015.293l-.568.875c.113.297.18.616.193.95l.898.54a.25.25 0 0 1 .115.27l-.144.626a.25.25 0 0 1-.222.193l-1.115.098a3.015 3.015 0 0 1-.512.608l.165 1.18a.25.25 0 0 1-.138.259l-.577.281a.25.25 0 0 1-.29-.05l-.874-.905a3.035 3.035 0 0 1-.608 0l-.875.904a.25.25 0 0 1-.289.051l-.577-.281a.25.25 0 0 1-.138-.26l.165-1.18a3.015 3.015 0 0 1-.512-.607l-1.115-.098a.25.25 0 0 1-.222-.193l-.144-.626a.25.25 0 0 1 .115-.27l.898-.54c.013-.334.08-.653.193-.95zM6.789 8.023A12.845 12.845 0 0 0 6 8c-5.036 0-6 2.74-6 4.48C0 14.22.076 15 6 15c.553 0 1.055-.006 1.51-.02A5.977 5.977 0 0 1 6 11c0-1.083.287-2.1.79-2.977zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM12 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="admin" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.162 2.5a3.5 3.5 0 0 1-3.163 5.479L6.08 14.766a1.5 1.5 0 0 1-2.598-1.5L7.4 6.479A3.5 3.5 0 0 1 10.564 1L8.9 3.88l2.599 1.5 1.663-2.88zm-8.63 11.949a.5.5 0 1 0 .5-.866.5.5 0 0 0-.5.866z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.414 7.95l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 1 0 1.414-1.415L10.414 7.95zm-7 0l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 0 0 1.414-1.415L3.414 7.95z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.536 7.95L1.293 3.707a1 1 0 0 1 1.414-1.414l4.95 4.95a.997.997 0 0 1 0 1.414l-4.95 4.95a1 1 0 1 1-1.414-1.415L5.536 7.95zm7 0L8.293 3.707a1 1 0 0 1 1.414-1.414l4.95 4.95a.997.997 0 0 1 0 1.414l-4.95 4.95a1 1 0 0 1-1.414-1.415l4.243-4.242z"/></symbol><symbol viewBox="0 0 16 16" id="angle-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 10.243l-4.95-4.95a1 1 0 0 0-1.414 1.414l5.657 5.657a.997.997 0 0 0 1.414 0l5.657-5.657a1 1 0 0 0-1.414-1.414L8 10.243z"/></symbol><symbol viewBox="0 0 16 16" id="angle-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.757 8l4.95-4.95a1 1 0 1 0-1.414-1.414L3.636 7.293a.997.997 0 0 0 0 1.414l5.657 5.657a1 1 0 0 0 1.414-1.414L5.757 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.243 8l-4.95-4.95a1 1 0 0 1 1.414-1.414l5.657 5.657a.997.997 0 0 1 0 1.414l-5.657 5.657a1 1 0 0 1-1.414-1.414L10.243 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 6.757l-4.95 4.95a1 1 0 1 1-1.414-1.414l5.657-5.657a.997.997 0 0 1 1.414 0l5.657 5.657a1 1 0 0 1-1.414 1.414L8 6.757z"/></symbol><symbol viewBox="0 0 16 16" id="appearance" xmlns="http://www.w3.org/2000/svg"><path d="M11.161 12.456l.232.121c.1.053.175.094.249.137.53.318.844.75.857 1.402.012 1.397-1.116 1.756-3.12 1.858a23.85 23.85 0 0 1-1.38.026A8 8 0 0 1 0 8a8 8 0 0 1 8-8c4.417 0 7.998 3.582 7.998 7.977.06 2.621-1.312 3.586-4.48 3.648-.602.008-1.068.043-1.4.104.228.192.598.47 1.043.727zm-3.287-.943c-.019-1.495 1.228-1.856 3.611-1.888C13.67 9.582 14.028 9.33 13.998 8A6 6 0 1 0 8 14c.603 0 .91-.004 1.277-.023a9.7 9.7 0 0 0 .478-.035c-1.172-.738-1.868-1.47-1.88-2.43zM6 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-2-3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM4 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="applications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 1v2h2V1H7zm0 5h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm0 1v2h2V7h-2zM1 12h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm0 1v2h2v-2H1zm6-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm6 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="approval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.536 10.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 1 1 9.12 9.243l1.415 1.414zM7.632 8.109A2 2 0 0 0 7 11.364l2.121 2.121a1.996 1.996 0 0 0 2.807.021C11.686 14.554 10.627 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.6 0 1.142.038 1.632.109zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="arrow-down" xmlns="http://www.w3.org/2000/svg"><path d="M10.472 7.282a.862.862 0 0 1 1.26-.006c.357.364.357.958 0 1.285L8.627 11.73A.886.886 0 0 1 8 12a.849.849 0 0 1-.627-.27L4.275 8.561a.904.904 0 0 1-.013-1.285.861.861 0 0 1 1.26-.007l2.486 2.527z"/></symbol><symbol viewBox="0 0 16 16" id="arrow-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 6H2a2 2 0 1 0 0 4h7v2.586a1 1 0 0 0 1.707.707l4.586-4.586a1 1 0 0 0 0-1.414l-4.586-4.586A1 1 0 0 0 9 3.414V6z"/></symbol><symbol viewBox="0 0 16 16" id="assignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 5V4a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V7h-1a1 1 0 0 1 0-2h1zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="bold" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4 12.5v-9A1.5 1.5 0 0 1 5.5 2h2.104c2.182 0 3.879.681 3.879 2.982 0 1.067-.517 2.227-1.374 2.595v.073C11.176 7.963 12 8.865 12 10.466 12 12.914 10.19 14 7.911 14H5.5A1.5 1.5 0 0 1 4 12.5zm2.376-5.696H7.49c1.164 0 1.665-.552 1.665-1.417 0-.94-.534-1.289-1.649-1.289h-1.13v2.706zm0 5.098h1.341c1.293 0 1.956-.515 1.956-1.62 0-1.049-.647-1.472-1.956-1.472H6.376v3.092z"/></symbol><symbol viewBox="0 0 16 16" id="book" xmlns="http://www.w3.org/2000/svg"><path d="M7 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2v4.191a.5.5 0 0 1-.724.447l-1.052-.526a.5.5 0 0 0-.448 0l-1.052.526A.5.5 0 0 1 7 6.191V2zM5 0h6a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="bookmark" xmlns="http://www.w3.org/2000/svg"><path d="M6.746 10.505a2 2 0 0 1 2.508 0L11 11.911V3H5v8.91l1.746-1.405zM5 1h6a2 2 0 0 1 2 2v10.999a1 1 0 0 1-1.627.779L8 12.064l-3.373 2.714A1 1 0 0 1 3 13.998V3a2 2 0 0 1 2-2z"/></symbol><symbol viewBox="0 0 16 16" id="branch" xmlns="http://www.w3.org/2000/svg"><path d="M6 11.978v.29a2 2 0 1 1-2 0V3.732a2 2 0 1 1 2 0v3.849c.592-.491 1.31-.854 2.15-1.081 1.308-.353 1.875-.882 1.893-1.743a2 2 0 1 1 2.002-.051C12.053 6.54 10.857 7.84 8.67 8.43 7.056 8.867 6.195 9.98 6 11.978zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm6 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 15a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="bullhorn" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6.143 10H7V4H3a3 3 0 1 0 0 6h.143l.734 5.141a1 1 0 0 0 .99.859h1.556a.5.5 0 0 0 .495-.57L6.143 10zM8 4c1.034.02 2.039-.274 3.014-.883.727-.455 1.836-1.334 3.328-2.637A1 1 0 0 1 16 1.233v10.764a1 1 0 0 1-1.595.803c-1.658-1.227-2.788-1.992-3.392-2.294-.781-.39-1.785-.559-3.013-.506V4z"/></symbol><symbol viewBox="0 0 16 16" id="calendar" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 2h2a2 2 0 0 1 2 2H0a2 2 0 0 1 2-2h2V1a1 1 0 1 1 2 0v1h4V1a1 1 0 1 1 2 0v1zM0 4h16v9a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4zm2 2.5V13a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6.5a.5.5 0 0 0-.5-.5h-11a.5.5 0 0 0-.5.5zM5 8h2a1 1 0 1 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="cancel" xmlns="http://www.w3.org/2000/svg"><path d="M3.11 4.523a6 6 0 0 0 8.367 8.367L3.109 4.524zM4.522 3.11l8.368 8.368A6 6 0 0 0 4.524 3.11zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 16 16" id="chart" xmlns="http://www.w3.org/2000/svg"><path d="M15 14a1 1 0 0 1 0 2H2a2 2 0 0 1-2-2V1a1 1 0 1 1 2 0v13h13zM3.142 8.735l2.502-2.561a.5.5 0 0 1 .714-.003L8 7.833l3.592-4.553a.5.5 0 0 1 .796.015l2.516 3.454a.5.5 0 0 1 .096.295V12.5a.5.5 0 0 1-.5.5h-11a.5.5 0 0 1-.5-.5V9.085a.5.5 0 0 1 .142-.35z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.078 8.2l3.535-3.536a2 2 0 0 1 2.828 2.828l-4.949 4.95c-.39.39-.902.586-1.414.586a1.994 1.994 0 0 1-1.414-.586l-4.95-4.95a2 2 0 1 1 2.828-2.828l3.536 3.535z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.977 7.998l3.535-3.535a2 2 0 1 0-2.828-2.828l-4.95 4.949c-.39.39-.586.902-.586 1.414 0 .512.196 1.024.586 1.414l4.95 4.95a2 2 0 1 0 2.828-2.828L7.977 7.998z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.22 7.998L4.683 4.463a2 2 0 0 1 2.828-2.828l4.95 4.949c.39.39.586.902.586 1.414a1.99 1.99 0 0 1-.586 1.414l-4.95 4.95a2 2 0 0 1-2.828-2.828l3.535-3.536z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.778 8.957l3.535 3.535a2 2 0 1 0 2.828-2.828l-4.949-4.95a1.994 1.994 0 0 0-1.414-.586c-.512 0-1.024.196-1.414.586l-4.95 4.95a2 2 0 1 0 2.828 2.828l3.536-3.535z"/></symbol><symbol viewBox="0 0 16 16" id="clock" xmlns="http://www.w3.org/2000/svg"><path d="M9 7h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V5a1 1 0 1 1 2 0v2zm-1 9A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.414 8l4.95-4.95a1 1 0 0 0-1.414-1.414L8 6.586l-4.95-4.95A1 1 0 0 0 1.636 3.05L6.586 8l-4.95 4.95a1 1 0 1 0 1.414 1.414L8 9.414l4.95 4.95a1 1 0 1 0 1.414-1.414L9.414 8z"/></symbol><symbol viewBox="0 0 16 16" id="code" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M15.871 8.243a.997.997 0 0 0-.293-.707L12.75 4.707a1 1 0 0 0-1.414 1.414l2.12 2.122-2.12 2.121a1 1 0 0 0 1.414 1.414l2.828-2.828a.997.997 0 0 0 .293-.707zm-13.243 0L4.75 6.12a1 1 0 1 0-1.414-1.414L.507 7.536a.997.997 0 0 0 0 1.414l2.829 2.828a1 1 0 1 0 1.414-1.414L2.628 8.243zm6.407-4.107a1 1 0 0 1 .707 1.225L8.19 11.157a1 1 0 1 1-1.931-.518L7.81 4.843a1 1 0 0 1 1.224-.707z"/></symbol><symbol viewBox="0 0 9 13" id="collapse"><path d="M.084.25C.01.18-.015.12.008.071.031.024.093 0 .194 0h8.521c.1 0 .162.024.185.072.023.048-.002.107-.075.177l-4.11 3.935a.372.372 0 0 1-.11.072h-.301a.508.508 0 0 1-.11-.072L.084.249zM.377 6.88a.364.364 0 0 1-.26-.105.334.334 0 0 1-.11-.25v-.709c0-.096.036-.179.11-.249a.364.364 0 0 1 .26-.105h8.15c.101 0 .188.035.261.105.074.07.11.153.11.25v.709c0 .096-.036.179-.11.249a.364.364 0 0 1-.26.105H.377zM.084 12.132c-.074.07-.099.129-.076.177.023.048.085.072.186.072h8.521c.1 0 .162-.024.185-.072.023-.048-.002-.107-.075-.177l-4.11-3.935a.372.372 0 0 0-.11-.072h-.301a.508.508 0 0 0-.11.072l-4.11 3.935z"/></symbol><symbol viewBox="0 0 16 16" id="comment" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comment-dots" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586zM5 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="comment-next" xmlns="http://www.w3.org/2000/svg"><path d="M8 5V4a.5.5 0 0 1 .8-.4l2.667 2a.5.5 0 0 1 0 .8L8.8 8.4A.5.5 0 0 1 8 8V7H6a1 1 0 1 1 0-2h2zM1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comments" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.75 10L0 13V3a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2H3.75zM13 5h1a2 2 0 0 1 2 2v8l-2.667-2H8a2 2 0 0 1-2-2h4a3 3 0 0 0 3-3V5z"/></symbol><symbol viewBox="0 0 16 16" id="commit" xmlns="http://www.w3.org/2000/svg"><path d="M8 10a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm3.876-1.008a4.002 4.002 0 0 1-7.752 0A1.01 1.01 0 0 1 4 9H1a1 1 0 1 1 0-2h3c.042 0 .083.003.124.008a4.002 4.002 0 0 1 7.752 0A1.01 1.01 0 0 1 12 7h3a1 1 0 0 1 0 2h-3a1.01 1.01 0 0 1-.124-.008z"/></symbol><symbol viewBox="0 0 16 16" id="credit-card" xmlns="http://www.w3.org/2000/svg"><path d="M14 5a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1h12zm0 3H2v3a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V8zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm6.5 8h3a.5.5 0 1 1 0 1h-3a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="cut" xmlns="http://www.w3.org/2000/svg"><rect width="16" height="2" y="7" fill-rule="evenodd" rx="1"/></symbol><symbol viewBox="0 0 16 16" id="dashboard" xmlns="http://www.w3.org/2000/svg"><path d="M7.709 10.021l.696-2.6a.5.5 0 0 1 .966.26l-.657 2.45A2 2 0 0 1 10 12H6a2 2 0 0 1 1.709-1.979zM0 8.9a8 8 0 0 1 15.998 0H16v3.6a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5V8.9zM14 9A6 6 0 1 0 2 9v3.5a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5V9zM3.5 9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm9 0a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-7-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm5 0a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1z"/></symbol><symbol viewBox="0 0 16 16" id="disk" xmlns="http://www.w3.org/2000/svg"><path d="M16 11.764V3a3 3 0 0 0-3-3H3a3 3 0 0 0-3 3v8.764A2.989 2.989 0 0 1 2 11V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v8c.768 0 1.47.289 2 .764zM2 12h12a2 2 0 1 1 0 4H2a2 2 0 1 1 0-4zm10 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="doc_code" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zm1.036 7.607a.498.498 0 0 1-.147.354l-1.414 1.414a.5.5 0 0 1-.707-.707l1.06-1.06-1.06-1.061a.5.5 0 0 1 .707-.707l1.414 1.414a.498.498 0 0 1 .147.353zm-4.822 0l1.06 1.061a.5.5 0 0 1-.706.707l-1.414-1.414a.498.498 0 0 1 0-.707l1.414-1.414a.5.5 0 1 1 .707.707l-1.06 1.06zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="doc_image" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM7.333 9.667l1.313-1.313a.5.5 0 0 1 .708 0L12 11H4l2.188-1.75a.5.5 0 0 1 .624 0l.521.417zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 8a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zM4 11h8v.7a.3.3 0 0 1-.3.3H4.3a.3.3 0 0 1-.3-.3V11z"/></symbol><symbol viewBox="0 0 16 16" id="doc_text" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 11h5a.5.5 0 1 1 0 1h-5a.5.5 0 1 1 0-1zm0-2h5a.5.5 0 1 1 0 1h-5a.5.5 0 0 1 0-1zm0-2h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1z"/></symbol><symbol viewBox="0 0 105 26" id="double-headed-arrow" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1.018 11.089L15.138.614c1.23-.911 3.086-.795 4.147.26.461.46.715 1.045.715 1.651v20.95C20 24.869 18.684 26 17.06 26a3.238 3.238 0 0 1-1.921-.614L1.019 14.911C-.212 14-.347 12.405.714 11.35c.094-.094.195-.18.303-.261zm102.964 0c.108.08.21.167.303.26 1.061 1.056.925 2.65-.303 3.562l-14.12 10.475A3.238 3.238 0 0 1 87.94 26C86.316 26 85 24.87 85 23.475V2.525c0-.606.254-1.192.715-1.65 1.061-1.056 2.917-1.172 4.146-.26l14.12 10.474zM35 17a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm18 0a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm18 0a4 4 0 1 1 0-8 4 4 0 0 1 0 8z"/></symbol><symbol viewBox="0 0 16 16" id="download" xmlns="http://www.w3.org/2000/svg"><path d="M9 12h1a.5.5 0 0 1 .4.8l-2 2.667a.5.5 0 0 1-.8 0l-2-2.667A.5.5 0 0 1 6 12h1V8a1 1 0 1 1 2 0v4zM4 9a1 1 0 1 1 0 2 4 4 0 0 1-1.971-7.481 4 4 0 0 1 6.633-2.505 3.999 3.999 0 0 1 3.82 2.014A4 4 0 0 1 12 11a1 1 0 0 1 0-2 2 2 0 1 0 0-4h-1a2 2 0 0 0-3.112-1.662A2 2 0 1 0 4.268 5H4a2 2 0 1 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M14 10h-3a1 1 0 0 1-1-1V6H8.527A.527.527 0 0 0 8 6.527V13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-3zm-4-7H8.527c-.18 0-.355.013-.527.04V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2v2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h4a3 3 0 0 1 3 3zM8.527 4h2.323a.5.5 0 0 1 .35.143l4.65 4.551a.5.5 0 0 1 .15.357V13a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3V6.527A2.527 2.527 0 0 1 8.527 4z"/></symbol><symbol viewBox="0 0 16 16" id="earth" xmlns="http://www.w3.org/2000/svg"><path d="M8.7 2.04l-.082.177c.283.223.422.413.417.571-.008.237-.311.057-.444.274-.133.218.038.542-.112.637-.15.096-.398-.386-.479-.46-.054-.049-.166-.257-.336-.625l-.216-.225a.844.844 0 0 0-.418-.035c-.177.038-.075.1-.035.132.04.032.32.037.452.2.132.164.03.224-.05.298-.054.05-.157.062-.31.035H5.952l-.402.398.03.325.229.455.324-.463c.008-.206.058-.342.15-.41.14-.1.342-.15.534-.085.191.066-.057.218.011.271.068.053.204-.098.313-.02.11.08.07.155.104.322.036.167.254.114.398.328.144.215.19.29.147.483-.043.195-.168.26-.305.232-.138-.028-.107-.246-.275-.348-.168-.102-.266-.114-.386-.054-.12.06-.016.129.023.235.04.106.274.321.224.43-.05.107-.108.116-.42 0-.21-.077-.414-.007-.615.212l-.76.722c-.153.715-.3 1.13-.44 1.243-.211.17-.177-.483-.483-.656-.306-.174-.494-.047-.8-.07-.307-.023-.42.65-.38.873a.434.434 0 0 0 .221.321c.236-.141.39-.184.465-.128.11.084-.144.267-.074.425.07.158.314.069.386.283.073.213.084.48-.05.706-.135.227-.275.178-.4.053-.127-.126-.033-.375-.255-.704-.223-.329-.381-.337-.63-.787-.158-.287-.35-.743-.575-1.366a6 6 0 0 0 3.21 7.198l.001-.075c0-.577-.004-.944-.012-1.102-.011-.236-.95-.945-1.104-1.2-.154-.256-.34-.595-.355-.746-.016-.151.185-.232.344-.325.16-.093-.11-.367.028-.626.137-.258.395-.438.496-.356.101.081.058.228.267.333.209.104.077-.213.456-.178.38.035.143.201.252.216.11.016.113-.127.299-.143.186-.015.282.445.471.622.19.178.452.008.611.043.159.034.267.09.402.255.136.166-.03.352.073.557.103.205 1.07.22 1.433.255.364.034.371.011.371.324s-.166.314-.453.507c-.286.193-.166.462-.38.762-.212.3-.316.062-.622.14-.306.077-.413.382-.452.568-.039.186-.386.094-.877.232-.29.082-.429.144-.569.204a6.002 6.002 0 0 0 7.682-4.3c-.094-.384-.18-.63-.258-.74-.213-.297-.36.21-.924.49-.564.278-.57-.288-.81-.49-.16-.133-.212-.44-.158-.92-.005-.478.02-.828.077-1.049.057-.221.126-.543.207-.965.351-.373.606-.572.764-.595.237-.034.336.374.658.3a.315.315 0 0 0 .035-.01 5.993 5.993 0 0 0-.475-.824l-.309-.043a.646.646 0 0 0-.332-.117c-.205-.02-.025.128-.089.24-.064.112-.235.724-.437.685-.201-.039-.204-.374-.17-.668.036-.294-.077-.35-.2-.412-.124-.062-.325-.213-.556-.295-.232-.082-.123-.175-.093-.274.03-.1.208-.015.193-.058-.014-.044-.313-.135-.266-.167.03-.02.2-.02.506.003l.216-.012.293-.163a.58.58 0 0 0-.376-.22c-.233-.036-.513-.034-.73-.142-.205-.103-.458-.36-.643-.638A5.965 5.965 0 0 0 8.7 2.04zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 1600 1600" id="ellipsis_v" xmlns="http://www.w3.org/2000/svg"><path d="M1088 1248v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68v-192q0-40 28-68t68-28h192q40 0 68 28t28 68zm0-512v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68V736q0-40 28-68t68-28h192q40 0 68 28t28 68zm0-512v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68V224q0-40 28-68t68-28h192q40 0 68 28t28 68z"/></symbol><symbol viewBox="0 0 18 18" id="emoji_slightly_smiling_face" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369.721.721 0 0 1 .568.047.715.715 0 0 1 .37.445 2.91 2.91 0 0 0 1.084 1.518A2.93 2.93 0 0 0 9 12.75a2.93 2.93 0 0 0 1.775-.58 2.913 2.913 0 0 0 1.084-1.518.711.711 0 0 1 .375-.445.737.737 0 0 1 .575-.047c.195.063.34.186.433.37.094.183.11.372.047.568zM7.5 6c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 6 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 4.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 6 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm6 0c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 12 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 10.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 12 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm3 3a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6A7.29 7.29 0 0 0 9 16.5a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39A7.29 7.29 0 0 0 16.5 9zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 18 18" id="emoji_smile" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369c.195-.062 7.41-.062 7.606 0 .195.063.34.186.433.37.094.183.11.372.047.568zM14 6.37c0 .398-.04.755-.513.755-.473 0-.498-.272-1.237-.272-.74 0-.74.215-1.165.215-.425 0-.585-.3-.585-.698 0-.397.17-.736.513-1.017.341-.281.754-.422 1.237-.422.483 0 .896.14 1.237.422.342.28.513.62.513 1.017zm-6.5 0c0 .398-.04.755-.513.755-.473 0-.498-.272-1.237-.272-.74 0-.74.215-1.165.215-.425 0-.585-.3-.585-.698 0-.397.17-.736.513-1.017.341-.281.754-.422 1.237-.422.483 0 .896.14 1.237.422.342.28.513.62.513 1.017zm9 2.63a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6A7.29 7.29 0 0 0 9 16.5a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39A7.29 7.29 0 0 0 16.5 9zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 18 18" id="emoji_smiley" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369c.195-.062 7.41-.062 7.606 0 .195.063.34.186.433.37.094.183.11.372.047.568h.001zM7.5 6c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 6 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 4.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 6 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm6 0c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 12 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 10.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 12 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm3 3a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6c.92.397 1.91.6 2.912.598a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39c.397-.92.6-1.91.598-2.912zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z"/></symbol><symbol viewBox="0 0 16 16" id="epic" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14.985 8.044l-.757 2.272a1 1 0 0 1-.949.684H1.618a1 1 0 0 1-.894-1.447l.318-.637A2 2 0 0 0 1.618 9h11.661a2 2 0 0 0 1.706-.956zm0 3l-.757 2.272a1 1 0 0 1-.949.684H1.618a1 1 0 0 1-.894-1.447l.318-.637a2 2 0 0 0 .576.084h11.661a2 2 0 0 0 1.706-.956zM3.618 2h10.995a1 1 0 0 1 .948 1.316l-1.333 4a1 1 0 0 1-.949.684H1.618a1 1 0 0 1-.894-1.447l2-4A1 1 0 0 1 3.618 2zm-.382 4h9.322l.667-2H4.236l-1 2z"/></symbol><symbol viewBox="0 0 16 16" id="external-link" xmlns="http://www.w3.org/2000/svg"><path d="M13.121 4.177l-4.95 4.95a1 1 0 1 1-1.414-1.414l4.95-4.95-1.386-1.386a.5.5 0 0 1 .299-.85l4.709-.524a.5.5 0 0 1 .552.552l-.523 4.71a.5.5 0 0 1-.851.297l-1.386-1.385zM12 8.884a1 1 0 0 1 2 0v4a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3v-8a3 3 0 0 1 3-3h4a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-4z"/></symbol><symbol viewBox="0 0 16 16" id="eye" xmlns="http://www.w3.org/2000/svg"><path d="M8 14C4.816 14 2.253 12.284.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2s5.747 1.716 7.607 5.019a2 2 0 0 1 0 1.962C13.747 12.284 11.184 14 8 14zm0-2c2.41 0 4.338-1.29 5.864-4C12.338 5.29 10.411 4 8 4 5.59 4 3.662 5.29 2.136 8 3.662 10.71 5.589 12 8 12zm0-1a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm1-3a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="eye-slash" xmlns="http://www.w3.org/2000/svg"><path d="M13.618 2.62L1.62 14.619a1 1 0 0 1-.985-1.668l1.525-1.526C1.516 10.742.926 9.927.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2c1.074 0 2.076.195 3.006.58l.944-.944a1 1 0 0 1 1.668.985zM8.068 11a3 3 0 0 0 2.931-2.932l-2.931 2.931zm-3.02-2.462a3 3 0 0 1 3.49-3.49l.884-.884A6.044 6.044 0 0 0 8 4C5.59 4 3.662 5.29 2.136 8c.445.79.924 1.46 1.439 2.011l1.473-1.473zm.421 5.06l1.658-1.658c.283.04.575.06.873.06 2.41 0 4.338-1.29 5.864-4a11.023 11.023 0 0 0-1.133-1.664l1.418-1.418a12.799 12.799 0 0 1 1.458 2.1 2 2 0 0 1 0 1.963C13.747 12.284 11.184 14 8 14a7.883 7.883 0 0 1-2.53-.402z"/></symbol><symbol viewBox="0 0 16 16" id="file-addition" xmlns="http://www.w3.org/2000/svg"><path d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3z"/></symbol><symbol viewBox="0 0 16 16" id="file-deletion" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm2 6h6a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="file-modified" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm5 4a3 3 0 1 1 0 6 3 3 0 0 1 0-6z"/></symbol><symbol viewBox="0 0 16 16" id="filter" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 6v9l-3.724-1.862A.5.5 0 0 1 6 12.691V6L1.854 1.854A.5.5 0 0 1 2.207 1h11.586a.5.5 0 0 1 .353.854L10 6z"/></symbol><symbol viewBox="0 0 16 16" id="folder" xmlns="http://www.w3.org/2000/svg"><path d="M13 3a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a2 2 0 0 1 2-2h3.81a3 3 0 0 1 2.827 1.995L13 3z"/></symbol><symbol viewBox="0 0 16 16" id="folder-o" xmlns="http://www.w3.org/2000/svg"><path d="M13 5l-4.365-.005a2 2 0 0 1-1.882-1.33A1 1 0 0 0 5.81 3H2v9a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1zm0-2a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a2 2 0 0 1 2-2h3.81a3 3 0 0 1 2.827 1.995L13 3z"/></symbol><symbol viewBox="0 0 16 16" id="folder-open" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14.59 5.464a2.998 2.998 0 0 1 1.096 3.845l-1.666 3.436A4 4 0 0 1 10.46 15H3a3 3 0 0 1-3-3V3a2 2 0 0 1 2-2h3.558a2 2 0 0 1 1.898 1.368l.21.632h4.973a2 2 0 0 1 2 2 2 2 0 0 1-.027.329l-.023.135zM5.285 7a1 1 0 0 0-.9.564l-1.939 4a1 1 0 0 0 .9 1.436h7.074a2 2 0 0 0 1.8-1.128l1.665-3.436a1 1 0 0 0-.9-1.436h-7.7z"/></symbol><symbol viewBox="0 0 16 16" id="fork" xmlns="http://www.w3.org/2000/svg"><path d="M9 12.268a2 2 0 1 1-2 0V8.874A4.002 4.002 0 0 1 4 5V3.732a2 2 0 1 1 2 0V5a2 2 0 1 0 4 0V3.732a2 2 0 1 1 2 0V5a4.002 4.002 0 0 1-3 3.874v3.394zM11 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm3 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="geo-nodes" xmlns="http://www.w3.org/2000/svg"><path d="M9.7 13.1l-.2.2c-.7.8-2 .9-2.8.1-.1 0-.1-.1-.1-.1l-.2-.2c-2 .2-3.4.7-3.4 1.4 0 .8 2.2 1.5 5 1.5s5-.7 5-1.5c0-.7-1.4-1.2-3.3-1.4M7.3 12.7c.4.4 1 .3 1.4-.1C11.6 9.5 13 7 13 5.3 13 2.4 10.8 0 8 0S3 2.4 3 5.3C3 7 4.4 9.5 7.3 12.7M8 2c1.6 0 3 1.4 3 3.3 0 1-1 2.8-3 5.2-2-2.4-3-4.2-3-5.2C5 3.4 6.4 2 8 2"/><circle cx="8" cy="5" r="1"/></symbol><symbol viewBox="0 0 16 16" id="git-merge" xmlns="http://www.w3.org/2000/svg"><path d="M11 12.268V5a1 1 0 0 0-1-1v1a.5.5 0 0 1-.8.4l-2.667-2a.5.5 0 0 1 0-.8L9.2.6a.5.5 0 0 1 .8.4v1a3 3 0 0 1 3 3v7.268a2 2 0 1 1-2 0zm-6 0a2 2 0 1 1-2 0V4.732a2 2 0 1 1 2 0v7.536zM4 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm8 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="group" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.048 11.997C-.377 11.975.013 11.782.013 10.56.013 9.235.653 8 4 8c.444 0 .84.022 1.194.062.164.435.426.82.76 1.132-1.786.389-2.721 1.353-2.906 2.803zm2.94-7.222a2.993 2.993 0 0 0-.976 1.95 2 2 0 1 1 .975-1.95zm6.964 7.222c-.185-1.45-1.12-2.414-2.906-2.803.334-.311.596-.697.76-1.132C11.16 8.022 11.556 8 12 8c3.346 0 3.987 1.235 3.987 2.56 0 1.222.39 1.415-3.035 1.437zm-1.964-5.272a2.993 2.993 0 0 0-.976-1.95 2 2 0 1 1 .976 1.95zM8 9a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 5c-2.177 0-3.987-.115-3.987-1.44S4.653 10 8 10c3.346 0 3.987 1.235 3.987 2.56S10.177 14 8 14z"/></symbol><symbol viewBox="0 0 16 16" id="history" xmlns="http://www.w3.org/2000/svg"><path d="M2.868 3.24a7 7 0 1 1-.043 9.475 1 1 0 0 1 1.478-1.348 5 5 0 1 0 .124-6.865l.796.645a.5.5 0 0 1-.193.873l-3.232.814a.5.5 0 0 1-.622-.504L1.3 3a.5.5 0 0 1 .814-.37l.754.61zM9 8h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V6a1 1 0 1 1 2 0v2z"/></symbol><symbol viewBox="0 0 16 16" id="home" xmlns="http://www.w3.org/2000/svg"><path d="M9 13h3v-3H4v3h3v-1a1 1 0 0 1 2 0v1zm5-3v3.659c0 .729-.657 1.341-1.5 1.341h-9c-.843 0-1.5-.612-1.5-1.341V10h-.88C.502 10 0 9.486 0 8.853c0-.307.12-.601.333-.816l6.405-6.463a1.56 1.56 0 0 1 2.374-.052L15.66 8.03c.444.441.455 1.167.024 1.622a1.108 1.108 0 0 1-.804.348H14zM7.95 3.273l-4.595 4.64h9.264l-4.67-4.64z"/></symbol><symbol viewBox="0 0 16 16" id="hook" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1h4zm0 1H6v1a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V4zM7 8a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h2a3 3 0 0 1 3 3v2a3 3 0 0 1-3 3v4a2 2 0 1 0 4 0h-.44a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H15a4 4 0 0 1-7 2.646A4 4 0 0 1 1 12H.56a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H3a2 2 0 1 0 4 0V8z"/></symbol><symbol viewBox="0 0 16 16" id="hourglass" xmlns="http://www.w3.org/2000/svg"><path d="M10.331 4.889A2.988 2.988 0 0 0 11 3V2H5v1c0 .362.064.709.182 1.03l5.15.859zM3 14v-1c0-1.78.93-3.342 2.33-4.228.447-.327.67-.582.67-.764 0-.19-.242-.46-.725-.815A4.996 4.996 0 0 1 3 3V2H2a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2h-1v1a4.997 4.997 0 0 1-2.39 4.266c-.407.3-.61.545-.61.734 0 .19.203.434.61.734A4.997 4.997 0 0 1 13 13v1h1a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2h1zm8 0v-1a3 3 0 0 0-6 0v1h6z"/></symbol><symbol viewBox="0 0 38 38" id="image-comment-dark" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle cx="19" cy="19" r="18" fill="#1F78D1"/><path fill="#FFF" fill-rule="nonzero" d="M19 38C8.507 38 0 29.493 0 19S8.507 0 19 0s19 8.507 19 19-8.507 19-19 19zm0-2c9.389 0 17-7.611 17-17S28.389 2 19 2 2 9.611 2 19s7.611 17 17 17zm-6.293-8.293c-.63.63-1.707.184-1.707-.707V15a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-7.586l-3.707 3.707zM13 24.586l2.293-2.293A1 1 0 0 1 16 22h8a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1H14a1 1 0 0 0-1 1v9.586z"/></g></symbol><symbol viewBox="0 0 38 38" id="image-comment-light" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle cx="19" cy="19" r="18" fill="#FFF"/><path fill="#1F78D1" fill-rule="nonzero" d="M19 38C8.507 38 0 29.493 0 19S8.507 0 19 0s19 8.507 19 19-8.507 19-19 19zm0-2c9.389 0 17-7.611 17-17S28.389 2 19 2 2 9.611 2 19s7.611 17 17 17zm-6.293-8.293c-.63.63-1.707.184-1.707-.707V15a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-7.586l-3.707 3.707zM13 24.586l2.293-2.293A1 1 0 0 1 16 22h8a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1H14a1 1 0 0 0-1 1v9.586z"/></g></symbol><symbol viewBox="0 0 16 16" id="import" xmlns="http://www.w3.org/2000/svg"><path d="M9 8h1a.5.5 0 0 1 .4.8l-2 2.667a.5.5 0 0 1-.8 0L5.6 8.8A.5.5 0 0 1 6 8h1V1a1 1 0 1 1 2 0v7zM0 8a1 1 0 1 1 2 0 6 6 0 1 0 12 0 1 1 0 0 1 2 0A8 8 0 1 1 0 8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-block" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.803 8a5.97 5.97 0 0 0-.462 1H4.5a.5.5 0 0 1 0-1h1.303zM4.5 5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1 0-1zm7.5.083a6.04 6.04 0 0 0-2 0V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2.083a5.96 5.96 0 0 0 .72 2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v2.083zm1.121 3.796zM11 16a5 5 0 1 1 0-10 5 5 0 0 1 0 10zm-1.293-2.292a3 3 0 0 0 4.001-4.001l-4.001 4zm-1.415-1.415l4.001-4a3 3 0 0 0-4.001 4.001z"/></symbol><symbol viewBox="0 0 16 16" id="issue-child" xmlns="http://www.w3.org/2000/svg"><path d="M11 8H5v1h1a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h2V7a.997.997 0 0 1 1-1h3V4H4.5a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9v2h3a.997.997 0 0 1 1 1v2h2a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h1V8zm-9 3v2h3v-2H2zm9 0v2h3v-2h-3z"/></symbol><symbol viewBox="0 0 16 16" id="issue-close" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M10.874 2H12a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3h-2c-.918 0-1.74-.413-2.29-1.063a3.987 3.987 0 0 0 1.988-.984A1 1 0 0 0 10 14h2a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1h-1V3c0-.345-.044-.68-.126-1zM4 0h3a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-external" xmlns="http://www.w3.org/2000/svg"><path d="M11 4a5.99 5.99 0 0 0-2 .341V3a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h2.528a6.003 6.003 0 0 0 2.705 1.736A2.99 2.99 0 0 1 8 16H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h4a3 3 0 0 1 3 3v1zM8.212 8.97l-.568-.876A.25.25 0 0 1 7.66 7.8l.404-.5a.25.25 0 0 1 .284-.076l.938.36c.256-.182.543-.325.85-.42l.323-.988a.25.25 0 0 1 .237-.173h.643a.25.25 0 0 1 .238.173l.321.989c.308.094.595.237.852.418l.937-.359a.25.25 0 0 1 .284.076l.404.5a.25.25 0 0 1 .016.293l-.568.875c.113.297.18.616.192.95l.9.54a.25.25 0 0 1 .114.27l-.145.627a.25.25 0 0 1-.221.192l-1.115.098a3.015 3.015 0 0 1-.512.608l.165 1.18a.25.25 0 0 1-.138.259l-.577.282a.25.25 0 0 1-.29-.051l-.874-.905a3.035 3.035 0 0 1-.608 0l-.875.905a.25.25 0 0 1-.29.05l-.577-.281a.25.25 0 0 1-.138-.26L9 12.254a3.015 3.015 0 0 1-.512-.607l-1.114-.098a.25.25 0 0 1-.222-.192l-.145-.627a.25.25 0 0 1 .115-.27l.899-.54c.012-.334.08-.653.192-.95zm2.806 2.034a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="issue-new" xmlns="http://www.w3.org/2000/svg"><path d="M10 2V1a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V4H9a1 1 0 1 1 0-2h1zm0 6a1 1 0 0 1 2 0v5a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h1a1 1 0 1 1 0 2H5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm0-2a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-2a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open-m" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-parent" xmlns="http://www.w3.org/2000/svg"><path d="M11 11H5v1h1.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H3v-2a.997.997 0 0 1 1-1h3V7H5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H9v2h3a.997.997 0 0 1 1 1v2h2.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H11v-1zM6 3v2h4V3H6z"/></symbol><symbol viewBox="0 0 16 16" id="issues" xmlns="http://www.w3.org/2000/svg"><path d="M10.458 15.012l.311.055a3 3 0 0 0 3.476-2.433l1.389-7.879A3 3 0 0 0 13.2 1.28L11.23.933a3.002 3.002 0 0 0-.824-.031c.364.59.58 1.28.593 2.02l1.854.328a1 1 0 0 1 .811 1.158l-1.389 7.879a1 1 0 0 1-1.158.81l-.118-.02a3.98 3.98 0 0 1-.541 1.935zM3 0h4a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="italic" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.5 12l2-8H6a1 1 0 1 1 0-2h6a1 1 0 0 1 0 2h-1.5l-2 8H10a1 1 0 0 1 0 2H4a1 1 0 0 1 0-2h1.5z"/></symbol><symbol viewBox="0 0 16 16" id="key" xmlns="http://www.w3.org/2000/svg"><path d="M7.575 6.689a4.002 4.002 0 0 1 6.274-4.86 4 4 0 0 1-4.86 6.274l-2.21 2.21.706.708a1 1 0 1 1-1.414 1.414l-.707-.707-.707.707.707.707a1 1 0 1 1-1.414 1.414l-.707-.707a1 1 0 0 1-1.414-1.414l5.746-5.746zm2.032-.618a2 2 0 1 0 2.828-2.828A2 2 0 0 0 9.607 6.07z"/></symbol><symbol viewBox="0 0 16 16" id="key-2" xmlns="http://www.w3.org/2000/svg"><path d="M5.172 14.157l-.344.344-2.485.133a.462.462 0 0 1-.497-.503l.14-2.24a.599.599 0 0 1 .177-.382l5.155-5.155a4 4 0 1 1 2.828 2.828l-1.439 1.44-1.06-.354-.708.707.354 1.06-.707.708-1.06-.354-.708.707.354 1.06zm6.01-8.839a1 1 0 1 0 1.414-1.414 1 1 0 0 0-1.414 1.414z"/></symbol><symbol viewBox="0 0 16 16" id="label" xmlns="http://www.w3.org/2000/svg"><path d="M11.782 14.718a3 3 0 0 1-4.242 0L1.652 8.829a2 2 0 0 1-.565-1.702l.54-3.703a2 2 0 0 1 1.69-1.69l3.703-.54a2 2 0 0 1 1.703.564l5.888 5.888a3 3 0 0 1 0 4.243l-2.829 2.829zm1.415-5.657L7.309 3.173l-3.703.54-.54 3.702 5.888 5.888a1 1 0 0 0 1.414 0l2.829-2.828a1 1 0 0 0 0-1.414zM5.732 5.525A1 1 0 1 1 7.146 6.94a1 1 0 0 1-1.414-1.414z"/></symbol><symbol viewBox="0 0 16 16" id="labels" xmlns="http://www.w3.org/2000/svg"><path d="M9.424 2.254l2.08-.905a1 1 0 0 1 1.206.326l3.013 4.12a1 1 0 0 1 .16.849l-1.947 7.264a3 3 0 0 1-3.675 2.122l-.5-.135a3.999 3.999 0 0 0 1.082-1.782 1 1 0 0 0 1.16-.722l1.823-6.802-2.258-3.087-.687.299a2 2 0 0 0-.628-.88l-.829-.667zM.377 3.7L4.4.498a1 1 0 0 1 1.25.003L9.627 3.7a1 1 0 0 1 .373.78V13a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4.482A1 1 0 0 1 .377 3.7zM2 13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V4.958L5.02 2.561 2 4.964V13zm3-6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="leave" xmlns="http://www.w3.org/2000/svg"><path d="M11 7V5.883a.5.5 0 0 1 .757-.429l3.528 2.117a.5.5 0 0 1 0 .858l-3.528 2.117a.5.5 0 0 1-.757-.43V9H7a1 1 0 1 1 0-2h4zm-2 6.256a1 1 0 0 1 2 0A2.744 2.744 0 0 1 8.256 16H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h5.19A2.81 2.81 0 0 1 11 2.81a1 1 0 0 1-2 0A.81.81 0 0 0 8.19 2H3a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h5.256c.41 0 .744-.333.744-.744z"/></symbol><symbol viewBox="0 0 16 16" id="level-up" xmlns="http://www.w3.org/2000/svg"><path fill="#2E2E2E" fill-rule="evenodd" d="M7 6h3.489a.5.5 0 0 0 .373-.832L6.374.117a.5.5 0 0 0-.748 0l-4.488 5.05A.5.5 0 0 0 1.51 6H5v7a3 3 0 0 0 3 3h6a1 1 0 0 0 0-2H8a1 1 0 0 1-1-1V6z"/></symbol><symbol viewBox="0 0 16 16" id="license" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12.56 8.9l2.66 4.606a.3.3 0 0 1-.243.45l-1.678.094a.1.1 0 0 0-.078.044l-.953 1.432a.3.3 0 0 1-.51-.016L9.097 10.9a5.994 5.994 0 0 0 3.464-2zm-5.23 2.063L4.707 15.51a.3.3 0 0 1-.51.016l-.953-1.432a.1.1 0 0 0-.078-.044l-1.678-.094a.3.3 0 0 1-.243-.45l2.48-4.297a5.983 5.983 0 0 0 3.607 1.754zM8 10A5 5 0 1 1 8 0a5 5 0 0 1 0 10zm0-2a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-1a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="link" xmlns="http://www.w3.org/2000/svg"><path d="M6.986 3.35l2.12-2.122a4 4 0 0 1 5.657 5.657l-2.828 2.829a4 4 0 0 1-5.657 0 1 1 0 0 1 1.414-1.415 2 2 0 0 0 2.829 0l2.828-2.828a2 2 0 1 0-2.828-2.828l-1.001 1a5.018 5.018 0 0 0-2.534-.294zm2.12 9.192l-2.12 2.121a4 4 0 1 1-5.658-5.656l2.829-2.829a4 4 0 0 1 5.657 0 1 1 0 1 1-1.415 1.414 2 2 0 0 0-2.828 0l-2.828 2.829a2 2 0 1 0 2.828 2.828l1.001-1.001a5.018 5.018 0 0 0 2.534.294z"/></symbol><symbol viewBox="0 0 16 16" id="list-bulleted" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-7h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm0 5h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm-4 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-2h10a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="list-numbered" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 2h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 0 1 0-2zM1.156 5v-.828h.816V2.204h-.72v-.636c.432-.084.708-.192.996-.372h.756v2.976h.684V5H1.156zm-.18 5v-.588c.9-.828 1.596-1.464 1.596-1.98 0-.342-.192-.504-.468-.504-.252 0-.444.18-.624.36l-.552-.552c.396-.42.756-.612 1.32-.612.768 0 1.308.492 1.308 1.248 0 .612-.576 1.284-1.092 1.812.192-.024.468-.048.636-.048h.636V10H.976zm1.26 5.072c-.618 0-1.068-.204-1.356-.54l.468-.648c.234.216.51.36.78.36.336 0 .552-.12.552-.36 0-.288-.15-.456-.948-.456v-.72c.636 0 .828-.168.828-.432 0-.228-.138-.348-.396-.348-.252 0-.432.108-.672.312l-.516-.624c.372-.312.768-.492 1.236-.492.84 0 1.38.384 1.38 1.074 0 .366-.204.642-.612.822v.024c.432.132.732.432.732.912 0 .72-.684 1.116-1.476 1.116z"/></symbol><symbol viewBox="0 0 16 16" id="location" xmlns="http://www.w3.org/2000/svg"><path d="M8.755 15.144a1 1 0 0 1-1.51 0C3.748 11.114 2 8.065 2 6a6 6 0 1 1 12 0c0 2.065-1.748 5.113-5.245 9.144zM12 6a4 4 0 1 0-8 0c0 1.314 1.312 3.71 4 6.944C10.688 9.71 12 7.314 12 6zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="location-dot" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6.314 13.087C4.382 13.295 3 13.85 3 14.5c0 .828 2.239 1.5 5 1.5s5-.672 5-1.5c0-.65-1.382-1.205-3.314-1.413l-.202.225a2 2 0 0 1-2.968 0l-.202-.225zm2.428-.445a1 1 0 0 1-1.484 0C4.419 9.5 3 7.037 3 5.252 3 2.353 5.239 0 8 0s5 2.352 5 5.253c0 1.784-1.42 4.247-4.258 7.389zM11 5.252C11 3.436 9.634 2 8 2S5 3.435 5 5.253c0 1.027.974 2.824 3 5.203 2.026-2.38 3-4.176 3-5.203zM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="lock" xmlns="http://www.w3.org/2000/svg"><path d="M10 5V4h2v1a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3V4h2v1h4zM4 7a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1H4zm0-3a4 4 0 1 1 8 0h-2a2 2 0 1 0-4 0H4z"/></symbol><symbol viewBox="0 0 16 16" id="lock-open" xmlns="http://www.w3.org/2000/svg"><path d="M4.044 4a4 4 0 0 1 6.99-2.658 1 1 0 1 1-1.495 1.33A2 2 0 0 0 6.044 4a.998.998 0 0 1-.07.367v.701H12a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3v-5a3 3 0 0 1 2.974-3V4h.07zM4 7.07a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="log" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4zm1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-5h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm0 3h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm-3 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-2h3a1 1 0 0 1 0 2H8a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="mail" xmlns="http://www.w3.org/2000/svg"><path d="M14 5.6L9.338 9.796a2 2 0 0 1-2.676 0L2 5.6V11a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5.6zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm.212 2L8 8.31 12.788 4H3.212z"/></symbol><symbol viewBox="0 0 16 16" id="menu" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1.143 2h13.714C15.488 2 16 2.448 16 3s-.512 1-1.143 1H1.143C.512 4 0 3.552 0 3s.512-1 1.143-1zm0 5h13.714C15.488 7 16 7.448 16 8s-.512 1-1.143 1H1.143C.512 9 0 8.552 0 8s.512-1 1.143-1zm0 5h13.714c.631 0 1.143.448 1.143 1s-.512 1-1.143 1H1.143C.512 14 0 13.552 0 13s.512-1 1.143-1z"/></symbol><symbol viewBox="0 0 16 16" id="merge-request-close" xmlns="http://www.w3.org/2000/svg"><path d="M9.414 8l1.414 1.414a1 1 0 1 1-1.414 1.414L8 9.414l-1.414 1.414a1 1 0 1 1-1.414-1.414L6.586 8 5.172 6.586a1 1 0 1 1 1.414-1.414L8 6.586l1.414-1.414a1 1 0 1 1 1.414 1.414L9.414 8zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="messages" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.588 8.942l1.173 5.862A1 1 0 0 1 8.78 16H7.22a1 1 0 0 1-.98-1.196l1.172-5.862a3.014 3.014 0 0 0 1.176 0zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4zM4.464 2.464L5.88 3.88a3 3 0 0 0 0 4.242L4.464 9.536a5 5 0 0 1 0-7.072zm7.072 7.072L10.12 8.12a3 3 0 0 0 0-4.242l1.415-1.415a5 5 0 0 1 0 7.072zM2.343.343l1.414 1.414a6 6 0 0 0 0 8.486l-1.414 1.414a8 8 0 0 1 0-11.314zm11.314 11.314l-1.414-1.414a6 6 0 0 0 0-8.486L13.657.343a8 8 0 0 1 0 11.314z"/></symbol><symbol viewBox="0 0 16 16" id="mobile-issue-close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.657 10.728L2.12 7.192A1 1 0 1 0 .707 8.607l4.243 4.242a.997.997 0 0 0 1.414 0l8.485-8.485a1 1 0 1 0-1.414-1.414l-7.778 7.778z"/></symbol><symbol viewBox="0 0 16 16" id="monitor" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 13v1h3a1 1 0 0 1 0 2H3a1 1 0 0 1 0-2h3v-1H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3h-3zM3 2a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm5.723 6.416l-2.66-1.773-1.71 1.71a.5.5 0 1 1-.707-.707l2-2a.5.5 0 0 1 .631-.062l2.66 1.773 2.71-2.71a.5.5 0 0 1 .707.707l-3 3a.5.5 0 0 1-.631.062z"/></symbol><symbol viewBox="0 0 16 16" id="more" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 4a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="notifications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 14H2.435a2 2 0 0 1-1.761-2.947c.962-1.788 1.521-3.065 1.68-3.832.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024c3.755.528 4.375 4.27 4.761 6.043.188.86.742 2.188 1.661 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0zm5.805-6.468c-.325-1.492-.37-1.674-.61-2.288C10.6 3.716 9.742 3 8.07 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.208 1.012-.827 2.424-1.877 4.375H13.64c-.993-1.937-1.6-3.396-1.835-4.468z"/></symbol><symbol viewBox="0 0 16 16" id="notifications-off" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.26 5.089c.243.757.382 1.478.5 2.017.187.86.74 2.188 1.66 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0H4.35l2-2h7.29c-.993-1.937-1.6-3.396-1.835-4.468-.07-.326-.129-.59-.178-.81l1.634-1.633zM10.943 1.75l-1.48 1.48C9.07 3.076 8.612 3 8.069 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.065.317-.17.673-.317 1.073L.45 12.242a1.99 1.99 0 0 1 .224-1.19c.962-1.787 1.521-3.064 1.68-3.831.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024 4.867 4.867 0 0 1 1.944.688zm2.932-.105a1 1 0 0 1 0 1.415L2.561 14.374a1 1 0 1 1-1.415-1.414L12.46 1.646a1 1 0 0 1 1.414 0z"/></symbol><symbol viewBox="0 0 16 16" id="overview" xmlns="http://www.w3.org/2000/svg"><path d="M2 0h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2h-3zM2 9h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3h-3z"/></symbol><symbol viewBox="0 0 16 16" id="pencil" xmlns="http://www.w3.org/2000/svg"><path d="M13.02 1.293l1.414 1.414a1 1 0 0 1 0 1.414L4.119 14.436a1 1 0 0 1-.704.293l-2.407.008L1 12.316a1 1 0 0 1 .293-.71L11.605 1.292a1 1 0 0 1 1.414 0zm-1.416 1.415l-.707.707L12.31 4.83l.707-.707-1.414-1.415zM3.411 13.73l1.123-1.122H3.12v-1.415L2 12.312l.005 1.422 1.406-.005z"/></symbol><symbol viewBox="0 0 16 16" id="pencil-square" xmlns="http://www.w3.org/2000/svg"><path d="M12 9a1 1 0 0 1 2 0v4a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h4a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V9zm.778-7.179l1.414 1.415-6.476 6.476a1 1 0 0 1-.498.27l-1.51.325.323-1.512a1 1 0 0 1 .27-.497l6.477-6.477zM15.607.407a1 1 0 0 1 0 1.414l-.708.707-1.414-1.414.707-.707a1 1 0 0 1 1.415 0z"/></symbol><symbol viewBox="0 0 16 16" id="pipeline" xmlns="http://www.w3.org/2000/svg"><path d="M8.969 7.25a2 2 0 1 1-1.938 0A1.002 1.002 0 0 1 7 7V5.083a.2.2 0 0 1 .06-.142l.877-.87a.1.1 0 0 1 .141 0l.864.87A.2.2 0 0 1 9 5.083V7c0 .086-.01.17-.031.25zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm4.5-4a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-5 9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zM8 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="play" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.765 15.835c-.545.321-1.258.159-1.593-.363A1.075 1.075 0 0 1 1 14.89V1.11C1 .496 1.518 0 2.158 0c.214 0 .424.057.607.165l11.684 6.89c.544.321.714 1.005.38 1.526a1.135 1.135 0 0 1-.38.364l-11.684 6.89z"/></symbol><symbol viewBox="0 0 16 16" id="plus" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7H2a1 1 0 1 0 0 2h5v5a1 1 0 0 0 2 0V9h5a1 1 0 0 0 0-2H9V2a1 1 0 1 0-2 0v5z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 7V4a1 1 0 1 0-2 0v3H4a1 1 0 1 0 0 2h3v3a1 1 0 0 0 2 0V9h3a1 1 0 0 0 0-2H9zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square-o" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="podcast" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.588 8.942l1.173 5.862a1 1 0 0 1-.785 1.177A1 1 0 0 1 8.78 16H7.22a1 1 0 0 1-1-1 1 1 0 0 1 .02-.196l1.172-5.862a3.014 3.014 0 0 0 1.176 0zM8 7.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zM4.464 2.464A1 1 0 0 1 5.88 3.88a3 3 0 0 0 0 4.242 1 1 0 0 1-1.415 1.415 5 5 0 0 1 0-7.072zm7.072 7.072A1 1 0 0 1 10.12 8.12a3 3 0 0 0 0-4.242 1 1 0 0 1 1.415-1.415 5 5 0 0 1 0 7.072zM2.343.343a1 1 0 1 1 1.414 1.414 6 6 0 0 0 0 8.486 1 1 0 1 1-1.414 1.414 8 8 0 0 1 0-11.314zm11.314 11.314a1 1 0 1 1-1.414-1.414 6 6 0 0 0 0-8.486A1 1 0 0 1 13.657.343a8 8 0 0 1 0 11.314z"/></symbol><symbol viewBox="0 0 16 16" id="preferences" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5 12h10a1 1 0 0 1 0 2H5a1 1 0 0 1-2 0v-2a1 1 0 0 1 2 0zm-3 0H1a1 1 0 0 0 0 2h1v-2zm11-5h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-2 0V7a1 1 0 0 1 2 0zm-3 0H1a1 1 0 1 0 0 2h9V7zM6 2h9a1 1 0 0 1 0 2H6a1 1 0 1 1-2 0V2a1 1 0 1 1 2 0zM3 2H1a1 1 0 1 0 0 2h2V2z"/></symbol><symbol viewBox="0 0 16 16" id="profile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-4.274-3.404C4.412 9.709 5.694 9 8 9c2.313 0 3.595.7 4.28 1.586A4.997 4.997 0 0 1 8 13a4.997 4.997 0 0 1-4.274-2.404zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="project" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177l-.038.044a.505.505 0 0 0 .038-.044zm-.787 0a.5.5 0 0 0 .038.043l-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol><symbol viewBox="0 0 16 16" id="push-rules" xmlns="http://www.w3.org/2000/svg"><path d="M6.268 9a2 2 0 0 1 3.464 0H11a1 1 0 0 1 0 2H9.732a2 2 0 0 1-3.464 0H5a1 1 0 0 1 0-2h1.268zM7 2H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1h-1v3.515a.3.3 0 0 1-.434.268l-1.432-.716a.3.3 0 0 0-.268 0l-1.432.716A.3.3 0 0 1 7 5.515V2zM4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm4 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="question" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm-1.46-5.602h2.233a3.97 3.97 0 0 1 .051-.558c.029-.17.073-.326.133-.469.06-.143.14-.28.242-.41.102-.13.228-.263.38-.399.26-.24.504-.467.733-.683a5.03 5.03 0 0 0 .598-.668c.17-.23.302-.477.399-.742a2.66 2.66 0 0 0 .144-.907c0-.505-.083-.95-.25-1.335a2.55 2.55 0 0 0-.723-.97 3.2 3.2 0 0 0-1.152-.589 5.441 5.441 0 0 0-1.531-.2c-.516 0-.998.063-1.445.188a3.19 3.19 0 0 0-1.168.59c-.331.268-.594.61-.79 1.027-.195.417-.295.917-.3 1.5h2.64c.006-.224.04-.416.102-.578.062-.161.142-.293.238-.394a.921.921 0 0 1 .332-.227 1.04 1.04 0 0 1 .39-.074c.34 0 .593.095.763.285.169.19.254.488.254.895 0 .328-.106.63-.317.906-.21.276-.499.565-.863.867-.214.182-.39.374-.531.574-.141.2-.253.42-.336.657a3.656 3.656 0 0 0-.176.777 7.89 7.89 0 0 0-.05.937zm-.321 2.375c0 .188.035.362.105.524.07.161.17.3.301.418.13.117.284.21.46.277.178.068.376.102.595.102.218 0 .416-.034.593-.102.178-.068.331-.16.461-.277a1.2 1.2 0 0 0 .301-.418c.07-.162.106-.336.106-.524a1.3 1.3 0 0 0-.106-.523 1.2 1.2 0 0 0-.3-.418 1.461 1.461 0 0 0-.462-.277 1.651 1.651 0 0 0-.593-.102c-.22 0-.417.034-.594.102a1.46 1.46 0 0 0-.461.277 1.2 1.2 0 0 0-.3.418 1.284 1.284 0 0 0-.106.523z"/></symbol><symbol viewBox="0 0 16 16" id="question-o" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-.778-4.151c0-.301.014-.575.044-.82a3.2 3.2 0 0 1 .154-.68c.073-.208.17-.4.294-.575.123-.176.278-.343.465-.503a4.81 4.81 0 0 0 .755-.758c.185-.242.277-.506.277-.793 0-.356-.074-.617-.222-.783-.148-.166-.37-.25-.667-.25a.92.92 0 0 0-.342.065.806.806 0 0 0-.29.199 1.04 1.04 0 0 0-.209.345 1.5 1.5 0 0 0-.088.506H5.082c.005-.51.092-.948.263-1.313.171-.364.401-.664.69-.899.29-.234.63-.406 1.023-.516a4.66 4.66 0 0 1 1.264-.164c.497 0 .944.058 1.34.174.397.117.733.289 1.008.517.276.227.487.51.633.847.146.337.218.727.218 1.17 0 .295-.042.56-.126.792a2.52 2.52 0 0 1-.349.65 4.4 4.4 0 0 1-.523.584c-.2.19-.414.389-.642.598a2.73 2.73 0 0 0-.332.349c-.089.114-.16.233-.212.359a1.868 1.868 0 0 0-.116.41 3.39 3.39 0 0 0-.044.489H7.222zm-.28 2.078c0-.164.03-.317.092-.458a1.05 1.05 0 0 1 .263-.366c.114-.103.248-.183.403-.243a1.45 1.45 0 0 1 .52-.089c.191 0 .364.03.52.09.154.059.289.14.403.242.114.103.201.224.263.366.061.141.092.294.092.458 0 .164-.03.316-.092.458a1.05 1.05 0 0 1-.263.365 1.278 1.278 0 0 1-.404.243 1.43 1.43 0 0 1-.52.089c-.19 0-.364-.03-.519-.089-.155-.06-.29-.14-.403-.243a1.05 1.05 0 0 1-.263-.365 1.135 1.135 0 0 1-.093-.458z"/></symbol><symbol viewBox="0 0 16 16" id="quote" xmlns="http://www.w3.org/2000/svg"><path d="M15 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9h-2a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1zM7 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9H3a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="redo" xmlns="http://www.w3.org/2000/svg"><path d="M4.625 4.423A4.897 4.897 0 0 1 8.079 3c2.73 0 4.944 2.239 4.944 5s-2.214 5-4.944 5c-1.41 0-2.723-.6-3.655-1.633a.98.98 0 0 0-1.397-.066 1.008 1.008 0 0 0-.064 1.413A6.87 6.87 0 0 0 8.079 15C11.9 15 15 11.866 15 8s-3.099-7-6.921-7A6.866 6.866 0 0 0 3.08 3.158L1.833 2.137a.49.49 0 0 0-.695.074.504.504 0 0 0-.11.311L1 7.26a.497.497 0 0 0 .6.492l4.576-1.013a.5.5 0 0 0 .206-.877L4.625 4.423z"/></symbol><symbol viewBox="0 0 16 16" id="remove" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 3a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2v10a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V3zm3-2a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1H5zM4 3v10a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V3H4zm2.5 2a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm3 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 16 16" id="repeat" xmlns="http://www.w3.org/2000/svg"><path d="M11.375 4.423A4.897 4.897 0 0 0 7.921 3c-2.73 0-4.944 2.239-4.944 5s2.214 5 4.944 5c1.41 0 2.723-.6 3.655-1.633a.98.98 0 0 1 1.397-.066c.403.373.432 1.005.064 1.413A6.87 6.87 0 0 1 7.921 15C4.1 15 1 11.866 1 8s3.099-7 6.921-7c1.915 0 3.706.792 4.999 2.158l1.247-1.021a.49.49 0 0 1 .695.074c.07.088.11.198.11.311L15 7.26a.497.497 0 0 1-.6.492L9.824 6.739a.5.5 0 0 1-.206-.877l1.757-1.439z"/></symbol><symbol viewBox="0 0 16 16" id="retry" xmlns="http://www.w3.org/2000/svg"><path d="M4.114 6.958a4 4 0 0 0 5.283 4.775 1 1 0 1 1 .712 1.87A6 6 0 0 1 2.182 6.44l-.741-.2a.5.5 0 0 1-.12-.915l2.195-1.268a.5.5 0 0 1 .683.183l1.268 2.196a.5.5 0 0 1-.563.733l-.79-.212zm7.777 2.084a4 4 0 0 0-5.284-4.775 1 1 0 0 1-.712-1.87 6 6 0 0 1 7.927 7.162l.742.2a.5.5 0 0 1 .12.915l-2.196 1.268a.5.5 0 0 1-.683-.183l-1.267-2.196a.5.5 0 0 1 .562-.733l.79.212z"/></symbol><symbol viewBox="0 0 16 16" id="scale" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.99 9a.792.792 0 0 0-.078-.231L13 7l-.912 1.769a.791.791 0 0 0-.077.231h1.978zm-10 0a.792.792 0 0 0-.078-.231L3 7l-.912 1.769A.791.791 0 0 0 2.011 9h1.978zM2 0h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm3 14h6a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2zM8 4a1 1 0 0 1 1 1v9H7V5a1 1 0 0 1 1-1zm-4.53-.714l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 3 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158L2.53 3.286a.53.53 0 0 1 .94 0zm10 0l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 13 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158l2.266-4.735a.53.53 0 0 1 .94 0z"/></symbol><symbol viewBox="0 0 16 16" id="screen-full" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14 14v-2a1 1 0 0 1 2 0v3a.997.997 0 0 1-1 1h-3a1 1 0 0 1 0-2h2zM2 14v-2a1 1 0 0 0-2 0v3a1 1 0 0 0 1 1h3a1 1 0 0 0 0-2H2zM15.707.293A.997.997 0 0 1 16 1v3a1 1 0 0 1-2 0V2h-2a1 1 0 0 1 0-2h3c.276 0 .526.112.707.293zM2 2v2a1 1 0 1 1-2 0V1a.997.997 0 0 1 1-1h3a1 1 0 1 1 0 2H2zm4 4h4a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="screen-normal" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3 3V1a1 1 0 1 1 2 0v3a.997.997 0 0 1-1 1H1a1 1 0 1 1 0-2h2zm10 0h2a1 1 0 0 1 0 2h-3a.997.997 0 0 1-1-1V1a1 1 0 0 1 2 0v2zM3 13H1a1 1 0 0 1 0-2h3a.997.997 0 0 1 1 1v3a1 1 0 0 1-2 0v-2zm10 0v2a1 1 0 0 1-2 0v-3a.997.997 0 0 1 1-1h3a1 1 0 0 1 0 2h-2zM6.5 7h3a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 12 16" id="scroll_down" xmlns="http://www.w3.org/2000/svg"><path class="fbfirst-triangle" d="M1.048 14.155a.508.508 0 0 0-.32.105c-.091.07-.136.154-.136.25v.71c0 .095.045.178.135.249.09.07.197.105.321.105h10.043a.51.51 0 0 0 .321-.105c.09-.07.136-.154.136-.25v-.71c0-.095-.045-.178-.136-.249a.508.508 0 0 0-.32-.105"/><path class="fbsecond-triangle" d="M.687 8.027c-.09-.087-.122-.16-.093-.22.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 12.91a.458.458 0 0 1-.136.089h-.37a.626.626 0 0 1-.136-.09"/><path class="fbthird-triangle" d="M.687 1.027C.597.94.565.867.594.807c.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 5.91a.458.458 0 0 1-.136.09h-.37a.626.626 0 0 1-.136-.09"/></symbol><symbol viewBox="0 0 12 16" id="scroll_up" xmlns="http://www.w3.org/2000/svg"><path d="M1.048 1.845a.508.508 0 0 1-.32-.105c-.091-.07-.136-.154-.136-.25V.78c0-.095.045-.178.135-.249a.508.508 0 0 1 .321-.105h10.043a.51.51 0 0 1 .321.105c.09.07.136.154.136.25v.71c0 .095-.045.178-.136.249a.508.508 0 0 1-.32.105M.687 7.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 3.09A.458.458 0 0 0 6.257 3h-.37a.626.626 0 0 0-.136.09M.687 14.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 10.09a.458.458 0 0 0-.136-.09h-.37a.626.626 0 0 0-.136.09"/></symbol><symbol viewBox="0 0 16 16" id="search" xmlns="http://www.w3.org/2000/svg"><path d="M8.853 8.854a3.5 3.5 0 1 0-4.95-4.95 3.5 3.5 0 0 0 4.95 4.95zm.207 2.328a5.5 5.5 0 1 1 2.121-2.121l3.329 3.328a1.5 1.5 0 0 1-2.121 2.121L9.06 11.182z"/></symbol><symbol viewBox="0 0 16 16" id="settings" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.415 5.803L1.317 4.084A.5.5 0 0 1 1.35 3.5l.805-.994a.5.5 0 0 1 .564-.153l1.878.704a5.975 5.975 0 0 1 1.65-.797L6.885.342A.5.5 0 0 1 7.36 0h1.28a.5.5 0 0 1 .474.342l.639 1.918a5.97 5.97 0 0 1 1.65.797l1.877-.704a.5.5 0 0 1 .565.153l.805.994a.5.5 0 0 1 .032.584l-1.097 1.719c.217.551.354 1.143.399 1.76l1.731 1.058a.5.5 0 0 1 .227.54l-.288 1.246a.5.5 0 0 1-.44.385l-2.008.19a6.026 6.026 0 0 1-1.142 1.431l.265 1.995a.5.5 0 0 1-.277.516l-1.15.56a.5.5 0 0 1-.576-.1l-1.424-1.452a6.047 6.047 0 0 1-1.804 0l-1.425 1.453a.5.5 0 0 1-.576.1l-1.15-.561a.5.5 0 0 1-.276-.516l.265-1.995a6.026 6.026 0 0 1-1.143-1.43l-2.008-.191a.5.5 0 0 1-.44-.385L.058 9.16a.5.5 0 0 1 .226-.539l1.732-1.058a5.968 5.968 0 0 1 .399-1.76zM8 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="shield" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8c1.657 0 3 1.373 3 3.067v7.346c0 1.065-.54 2.053-1.426 2.611l-4 2.52a2.944 2.944 0 0 1-3.148 0l-4-2.52A3.083 3.083 0 0 1 1 10.414V3.066C1 1.373 2.343 0 4 0zm0 2.045c-.552 0-1 .457-1 1.022v7.346c0 .355.18.685.475.87l4 2.52a.981.981 0 0 0 1.05 0l4-2.52c.295-.185.475-.515.475-.87V3.067c0-.565-.448-1.022-1-1.022H4zm0 1.533c0-.282.224-.511.5-.511h4V12.1a.52.52 0 0 1-.069.258.494.494 0 0 1-.684.183l-3.5-2.098a.513.513 0 0 1-.247-.44V3.577z"/></symbol><symbol viewBox="0 0 16 16" id="slight-frown" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-2.163-3.275a2.499 2.499 0 0 1 4.343.03.5.5 0 0 1-.871.49 1.5 1.5 0 0 0-2.607-.018.5.5 0 1 1-.865-.502zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="slight-smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-5.163 2.254a.5.5 0 1 1 .865-.502 1.499 1.499 0 0 0 2.607-.018.5.5 0 1 1 .871.49 2.499 2.499 0 0 1-4.343.03z"/></symbol><symbol viewBox="0 0 16 16" id="smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM6.18 6.27a.5.5 0 0 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zm6 0a.5.5 0 1 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zM5 9a3 3 0 0 0 6 0H5z"/></symbol><symbol viewBox="0 0 16 16" id="smiley" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM5 9h6a3 3 0 0 1-6 0z"/></symbol><symbol viewBox="0 0 16 16" id="snippet" xmlns="http://www.w3.org/2000/svg"><path d="M10.67 9.31a3.001 3.001 0 0 1 2.062 5.546 3 3 0 0 1-3.771-4.559 1.007 1.007 0 0 1-.095-.137l-4.5-7.794a1 1 0 0 1 1.732-1l4.5 7.794c.028.05.052.1.071.15zm-3.283.35l-.289.5c-.028.05-.06.095-.095.137a3.001 3.001 0 0 1-3.77 4.56A3 3 0 0 1 5.294 9.31c.02-.051.043-.102.071-.15l.866-1.5 1.155 2zm2.31-4l-1.156-2 1.325-2.294a1 1 0 0 1 1.732 1L9.696 5.66zm-5.465 7.464a1 1 0 1 0 1-1.732 1 1 0 0 0-1 1.732zm7.5 0a1 1 0 1 0-1-1.732 1 1 0 0 0 1 1.732z"/></symbol><symbol viewBox="0 0 16 16" id="soft-unwrap" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6.5 11v-.598a.5.5 0 0 1 .765-.424l2.557 1.598a.5.5 0 0 1 0 .848l-2.557 1.598a.5.5 0 0 1-.765-.424V13H2a1 1 0 0 1 0-2h4.5zM2 3h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm0 4h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm10 4h2a1 1 0 0 1 0 2h-2a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="soft-wrap" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.5 13v.598a.5.5 0 0 1-.765.424l-2.557-1.598a.5.5 0 0 1 0-.848l2.557-1.598a.5.5 0 0 1 .765.424V11H12a1 1 0 0 0 0-2H2a1 1 0 1 1 0-2h10a3 3 0 0 1 0 6h-1.5zM2 3h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm0 8h3a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="spam" xmlns="http://www.w3.org/2000/svg"><path d="M8.75.433l5.428 3.134a1.5 1.5 0 0 1 .75 1.299v6.268a1.5 1.5 0 0 1-.75 1.299L8.75 15.567a1.5 1.5 0 0 1-1.5 0l-5.428-3.134a1.5 1.5 0 0 1-.75-1.299V4.866a1.5 1.5 0 0 1 .75-1.299L7.25.433a1.5 1.5 0 0 1 1.5 0zM3.072 5.155v5.69L8 13.691l4.928-2.846v-5.69L8 2.309 3.072 5.155zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 14 14" id="spinner" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle cx="7" cy="7" r="6" stroke="#000" stroke-opacity=".1" stroke-width="2"/><path fill="#000" fill-opacity=".1" fill-rule="nonzero" d="M7 0a7 7 0 0 1 7 7h-2a5 5 0 0 0-5-5V0z"/></g></symbol><symbol viewBox="0 0 16 16" id="staged" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 3h4a1 1 0 1 1 0 2H2a1 1 0 1 1 0-2zm9 6a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM2 7h4a1 1 0 1 1 0 2H2a1 1 0 1 1 0-2zm0 4h12a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="star" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.609 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 16 16" id="star-o" xmlns="http://www.w3.org/2000/svg"><path d="M10.975 10.99a3 3 0 0 1 .655-2.083l1.54-1.916-2.219-.576a3 3 0 0 1-1.825-1.37L8 3.15 6.874 5.044a3 3 0 0 1-1.825 1.371l-2.218.576 1.54 1.916a3 3 0 0 1 .654 2.083l-.165 2.4 1.965-.836a3 3 0 0 1 2.348 0l1.965.836-.164-2.399zM7.61 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 14 14" id="status_canceled" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M5.2 3.8l4.9 4.9c.2.2.2.5 0 .7l-.7.7c-.2.2-.5.2-.7 0L3.8 5.2c-.2-.2-.2-.5 0-.7l.7-.7c.2-.2.5-.2.7 0"/></g></symbol><symbol viewBox="0 0 22 22" id="status_canceled_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M8.171 5.971l7.7 7.7a.76.76 0 0 1 0 1.1l-1.1 1.1a.76.76 0 0 1-1.1 0l-7.7-7.7a.76.76 0 0 1 0-1.1l1.1-1.1a.76.76 0 0 1 1.1 0"/></symbol><symbol viewBox="0 0 16 16" id="status_closed" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.83a1 1 0 0 1 1.414 1.416l-3.535 3.535a1 1 0 0 1-1.415.001l-2.12-2.12a1 1 0 1 1 1.413-1.415zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 14 14" id="status_created" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><circle cx="7" cy="7" r="3.25"/></g></symbol><symbol viewBox="0 0 22 22" id="status_created_borderless" xmlns="http://www.w3.org/2000/svg"><circle cx="11" cy="11" r="5.107"/></symbol><symbol viewBox="0 0 14 14" id="status_failed" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 5.969L5.599 4.568a.29.29 0 0 0-.413.004l-.614.614a.294.294 0 0 0-.004.413L5.968 7l-1.4 1.401a.29.29 0 0 0 .004.413l.614.614c.113.114.3.117.413.004L7 8.032l1.401 1.4a.29.29 0 0 0 .413-.004l.614-.614a.294.294 0 0 0 .004-.413L8.032 7l1.4-1.401a.29.29 0 0 0-.004-.413l-.614-.614a.294.294 0 0 0-.413-.004L7 5.968z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_failed_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M11 9.38L8.798 7.178a.455.455 0 0 0-.65.006l-.964.965a.462.462 0 0 0-.006.65L9.38 11l-2.202 2.202a.455.455 0 0 0 .006.65l.965.964a.462.462 0 0 0 .65.006L11 12.62l2.202 2.202a.455.455 0 0 0 .65-.006l.964-.965a.462.462 0 0 0 .006-.65L12.62 11l2.202-2.202a.455.455 0 0 0-.006-.65l-.965-.964a.462.462 0 0 0-.65-.006L11 9.38z"/></symbol><symbol viewBox="0 0 14 14" id="status_manual" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M10.5 7.63V6.37l-.787-.13c-.044-.175-.132-.349-.263-.61l.481-.652-.918-.913-.657.478a2.346 2.346 0 0 0-.612-.26L7.656 3.5H6.388l-.132.783c-.219.043-.394.13-.612.26l-.657-.478-.918.913.437.652c-.131.218-.175.392-.262.61l-.744.086v1.261l.787.13c.044.218.132.392.263.61l-.438.651.92.913.655-.434c.175.086.394.173.613.26l.131.783h1.313l.131-.783c.219-.043.394-.13.613-.26l.656.478.918-.913-.48-.652c.13-.218.218-.435.262-.61l.656-.13zM7 8.283a1.285 1.285 0 0 1-1.313-1.305c0-.739.57-1.304 1.313-1.304.744 0 1.313.565 1.313 1.304 0 .74-.57 1.305-1.313 1.305z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_manual_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M16.5 11.99v-1.98l-1.238-.206c-.068-.273-.206-.546-.412-.956l.756-1.025-1.444-1.435-1.03.752a3.686 3.686 0 0 0-.963-.41L12.03 5.5h-1.994l-.206 1.23c-.343.068-.618.205-.962.41l-1.031-.752-1.444 1.435.687 1.025c-.206.341-.275.615-.412.956L5.5 9.941v1.981l1.237.205c.07.342.207.615.413.957l-.688 1.025 1.444 1.434 1.032-.683c.274.137.618.274.962.41l.206 1.23h2.063l.206-1.23c.344-.068.619-.205.963-.41l1.03.752 1.444-1.435-.756-1.025c.207-.341.344-.683.413-.956l1.031-.205zM11 13.017c-1.169 0-2.063-.889-2.063-2.05 0-1.162.894-2.05 2.063-2.05s2.063.888 2.063 2.05c0 1.161-.894 2.05-2.063 2.05z"/></symbol><symbol viewBox="0 0 14 14" id="status_notfound" xmlns="http://www.w3.org/2000/svg"><path d="M7 14A7 7 0 1 1 7 0a7 7 0 0 1 0 14z"/><path d="M7 13A6 6 0 1 0 7 1a6 6 0 0 0 0 12z" fill="#FFF"/><path d="M8.16 7.184c.519-.37.904-.857 1.07-1.477.384-1.427-.619-2.897-2.246-2.897-.732 0-1.327.26-1.766.692a2.163 2.163 0 0 0-.509.743.75.75 0 0 0 1.4.54.78.78 0 0 1 .16-.213c.168-.165.39-.262.715-.262.597 0 .936.496.798 1.007-.067.249-.235.462-.492.644-.231.165-.47.264-.601.3a.75.75 0 0 0-.556.724v1.421a.75.75 0 0 0 1.5 0v-.909a3.74 3.74 0 0 0 .526-.313z"/><ellipse cx="6.889" cy="10.634" rx="1" ry="1"/></symbol><symbol viewBox="0 0 22 22" id="status_notfound_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M12.822 11.29c.816-.581 1.421-1.348 1.683-2.322.603-2.243-.973-4.553-3.53-4.553-1.15 0-2.085.41-2.775 1.089-.42.413-.672.835-.8 1.167a1.179 1.179 0 0 0 2.2.847c.016-.043.1-.184.252-.334.264-.259.613-.412 1.123-.412.938 0 1.47.78 1.254 1.584-.105.39-.37.726-.773 1.012a3.25 3.25 0 0 1-.945.47 1.179 1.179 0 0 0-.874 1.138v2.234a1.179 1.179 0 1 0 2.358 0v-1.43a5.9 5.9 0 0 0 .827-.492z"/><ellipse cx="10.825" cy="16.711" rx="1.275" ry="1.322"/></symbol><symbol viewBox="0 0 14 14" id="status_open" xmlns="http://www.w3.org/2000/svg"><path d="M0 7c0-3.866 3.142-7 7-7 3.866 0 7 3.142 7 7 0 3.866-3.142 7-7 7-3.866 0-7-3.142-7-7z"/><path d="M1 7c0 3.309 2.69 6 6 6 3.309 0 6-2.69 6-6 0-3.309-2.69-6-6-6-3.309 0-6 2.69-6 6z" fill="#FFF"/><path d="M7 9.219a2.218 2.218 0 1 0 0-4.436A2.218 2.218 0 0 0 7 9.22zm0 1.12a3.338 3.338 0 1 1 0-6.676 3.338 3.338 0 0 1 0 6.676z"/></symbol><symbol viewBox="0 0 14 14" id="status_pending" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M4.7 5.3c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H5c-.2 0-.3-.1-.3-.3V5.3m3 0c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H8c-.2 0-.3-.1-.3-.3V5.3"/></g></symbol><symbol viewBox="0 0 22 22" id="status_pending_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M7.386 8.329c0-.315.157-.472.471-.472h1.414c.315 0 .472.157.472.472v5.342c0 .315-.157.472-.472.472H7.857c-.314 0-.471-.157-.471-.472V8.33m4.714 0c0-.315.157-.472.471-.472h1.415c.314 0 .471.157.471.472v5.342c0 .315-.157.472-.471.472H12.57c-.314 0-.471-.157-.471-.472V8.33"/></symbol><symbol viewBox="0 0 14 14" id="status_running" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 3c2.2 0 4 1.8 4 4s-1.8 4-4 4c-1.3 0-2.5-.7-3.3-1.7L7 7V3"/></g></symbol><symbol viewBox="0 0 22 22" id="status_running_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M11 4.714c3.457 0 6.286 2.829 6.286 6.286 0 3.457-2.829 6.286-6.286 6.286-2.043 0-3.929-1.1-5.186-2.672L11 11V4.714"/></symbol><symbol viewBox="0 0 14 14" id="status_skipped" xmlns="http://www.w3.org/2000/svg"><path d="M7 14A7 7 0 1 1 7 0a7 7 0 0 1 0 14z"/><path d="M7 13A6 6 0 1 0 7 1a6 6 0 0 0 0 12z" fill="#FFF"/><path d="M6.415 7.04L4.579 5.203a.295.295 0 0 1 .004-.416l.349-.349a.29.29 0 0 1 .416-.004l2.214 2.214a.289.289 0 0 1 .019.021l.132.133c.11.11.108.291 0 .398L5.341 9.573a.282.282 0 0 1-.398 0l-.331-.331a.285.285 0 0 1 0-.399L6.415 7.04zm2.54 0L7.119 5.203a.295.295 0 0 1 .004-.416l.349-.349a.29.29 0 0 1 .416-.004l2.214 2.214a.289.289 0 0 1 .019.021l.132.133c.11.11.108.291 0 .398L7.881 9.573a.282.282 0 0 1-.398 0l-.331-.331a.285.285 0 0 1 0-.399L8.955 7.04z"/></symbol><symbol viewBox="0 0 22 22" id="status_skipped_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M14.072 11.063l-2.82 2.82a.46.46 0 0 0-.001.652l.495.495a.457.457 0 0 0 .653-.001l3.7-3.7a.46.46 0 0 0 .001-.653l-.196-.196a.453.453 0 0 0-.03-.033l-3.479-3.479a.464.464 0 0 0-.654.007l-.548.548a.463.463 0 0 0-.007.654l2.886 2.886z"/><path d="M10.08 11.063l-2.819 2.82a.46.46 0 0 0-.002.652l.496.495a.457.457 0 0 0 .652-.001l3.7-3.7a.46.46 0 0 0 .002-.653l-.196-.196a.453.453 0 0 0-.03-.033l-3.48-3.479a.464.464 0 0 0-.653.007l-.548.548a.463.463 0 0 0-.007.654l2.886 2.886z"/></symbol><symbol viewBox="0 0 14 14" id="status_success" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_success_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M9.866 12.095l-1.95-1.95a.462.462 0 0 0-.647.01l-.964.964a.46.46 0 0 0-.01.646l3.013 3.014a.787.787 0 0 0 1.106.008l.425-.425 4.854-4.853a.462.462 0 0 0 .002-.659l-.964-.964a.468.468 0 0 0-.658.002l-4.207 4.207z"/></symbol><symbol viewBox="0 0 14 14" id="status_success_solid" xmlns="http://www.w3.org/2000/svg"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7zm6.278.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 14 14" id="status_warning" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6 3.5c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v4c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-4m0 6c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v1c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-1"/></g></symbol><symbol viewBox="0 0 22 22" id="status_warning_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M9.429 5.5c0-.471.314-.786.785-.786h1.572c.471 0 .785.315.785.786v6.286c0 .471-.314.785-.785.785h-1.572c-.471 0-.785-.314-.785-.785V5.5m0 9.429c0-.472.314-.786.785-.786h1.572c.471 0 .785.314.785.786V16.5c0 .471-.314.786-.785.786h-1.572c-.471 0-.785-.315-.785-.786v-1.571"/></symbol><symbol viewBox="0 0 16 16" id="stop" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 0h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2z"/></symbol><symbol viewBox="0 0 16 16" id="task-done" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="template" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm.8 2h2.4a.8.8 0 0 1 .8.8v1.4a.8.8 0 0 1-.8.8H3.8a.8.8 0 0 1-.8-.8V4.8a.8.8 0 0 1 .8-.8zm4.7 0h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm0 2h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm-5 3h9a.5.5 0 1 1 0 1h-9a.5.5 0 0 1 0-1zm0 2h9a.5.5 0 1 1 0 1h-9a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="terminal" xmlns="http://www.w3.org/2000/svg"><path d="M7 8a.997.997 0 0 1-.293.707l-1.414 1.414a1 1 0 1 1-1.414-1.414L4.586 8l-.707-.707a1 1 0 1 1 1.414-1.414l1.414 1.414A.997.997 0 0 1 7 8zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm0 2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H4zm5 7h2a1 1 0 0 1 0 2H9a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="thumb-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 11h5.282a2 2 0 0 0 1.963-2.38l-.563-2.905a3 3 0 0 0-.243-.732l-1.103-2.286A3 3 0 0 0 10.964 1H7a3 3 0 0 0-3 3v6.3a2 2 0 0 0 .436 1.247l3.11 3.9a.632.632 0 0 0 .941.053l.137-.137a1 1 0 0 0 .28-.87L8.329 11zM1 10h2V3H1a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="thumb-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 5h5.282a2 2 0 0 1 1.963 2.38l-.563 2.905a3 3 0 0 1-.243.732l-1.103 2.286A3 3 0 0 1 10.964 15H7a3 3 0 0 1-3-3V5.7a2 2 0 0 1 .436-1.247l3.11-3.9A.632.632 0 0 1 8.487.5l.137.137a1 1 0 0 1 .28.87L8.329 5zM1 6h2v7H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="thumbtack" xmlns="http://www.w3.org/2000/svg"><path d="M7.125 9h-2.19a.5.5 0 0 1-.417-.777L6 6V2L5.362.724A.5.5 0 0 1 5.809 0h4.382a.5.5 0 0 1 .447.724L10 2v4l1.482 2.223a.5.5 0 0 1-.416.777H8.875L8 16l-.875-7z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 16 16" id="timer" xmlns="http://www.w3.org/2000/svg"><path d="M12.022 3.27l.77-.77a1 1 0 0 1 1.415 1.414l-.728.729a7 7 0 1 1-1.456-1.372zM8 14A5 5 0 1 0 8 4a5 5 0 0 0 0 10zm0-9a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zM6 0h4a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-add" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 4V2a1 1 0 0 1 2 0v2h2a1 1 0 0 1 0 2h-2v2a1 1 0 0 1-2 0V6H8a1 1 0 1 1 0-2h2zm2 7a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-done" xmlns="http://www.w3.org/2000/svg"><path d="M8.243 7.485l4.95-4.95a1 1 0 1 1 1.414 1.415L8.95 9.607a.997.997 0 0 1-1.414 0L4.707 6.778a1 1 0 0 1 1.414-1.414l2.122 2.121zM12 11a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="token" xmlns="http://www.w3.org/2000/svg"><path d="M3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H3zm1 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="unapproval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11.95 8.536l1.06-1.061a1 1 0 0 1 1.415 1.414l-1.061 1.06 1.06 1.061a1 1 0 0 1-1.414 1.415l-1.06-1.061-1.06 1.06a1 1 0 1 1-1.415-1.414l1.06-1.06-1.06-1.06a1 1 0 0 1 1.414-1.415l1.06 1.06zm-3.768-.33c.006.503.201 1.006.586 1.39l.353.354-.353.353a2 2 0 1 0 2.828 2.829l.354-.354.047.048C11.964 14.363 11.527 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.834 0 1.557.074 2.182.205zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="unassignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11 5h4a1 1 0 0 1 0 2h-4a1 1 0 0 1 0-2zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="unlink" xmlns="http://www.w3.org/2000/svg"><path d="M11.295 8.845l-.659-1.664a1.78 1.78 0 0 0 .04-.04l1.415-1.414c.586-.586.654-1.468.152-1.97s-1.384-.434-1.97.152L8.859 5.323a1.781 1.781 0 0 0-.04.04l-1.664-.658c.141-.208.305-.408.491-.594l1.415-1.414c1.366-1.367 3.424-1.525 4.596-.354 1.171 1.172 1.013 3.23-.354 4.596L11.89 8.354c-.186.186-.386.35-.594.491zm-2.45 2.45a4.075 4.075 0 0 1-.491.594l-1.415 1.414c-1.366 1.367-3.424 1.525-4.596.354-1.171-1.172-1.013-3.23.354-4.596L4.11 7.646c.186-.186.386-.35.594-.491l.659 1.664a1.781 1.781 0 0 0-.04.04l-1.415 1.414c-.586.586-.654 1.468-.152 1.97s1.384.434 1.97-.152l1.414-1.414a1.78 1.78 0 0 0 .04-.04l1.664.658zm3.812-2.088h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-.05a.5.5 0 0 1 .5-.5zm-.384 2.116l1.415 1.414a.5.5 0 0 1 0 .708l-.037.036a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 0-.707l.036-.037a.5.5 0 0 1 .707 0zm-2.823 1.09a.5.5 0 0 1 .5-.5h.052a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9.95a.5.5 0 0 1-.5-.5v-2zm-2.748-9.16a.5.5 0 0 1-.5.5h-.05a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h.05a.5.5 0 0 1 .5.5v2zm-2.116.383a.5.5 0 0 1 0 .707l-.036.036a.5.5 0 0 1-.707 0L2.428 2.965a.5.5 0 0 1 0-.707l.037-.036a.5.5 0 0 1 .707 0l1.414 1.414zm-1.09 2.823h-2a.5.5 0 0 1-.5-.5v-.051a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5z"/></symbol><symbol viewBox="0 0 16 16" id="unstaged" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 3h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm0 4h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm0 4h12a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="user" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 8c-6.888 0-6.976-.78-6.976-2.52S2.144 8 8 8s6.976 2.692 6.976 4.48c0 1.788-.088 2.52-6.976 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="users" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.521 8.01C15.103 8.19 16 10.755 16 12.48c0 1.533-.056 2.29-3.808 2.475.609-.54.808-1.331.808-2.475 0-1.911-.804-3.503-2.479-4.47zm-1.67-1.228A3.987 3.987 0 0 0 9.976 4a3.987 3.987 0 0 0-1.125-2.782 3 3 0 1 1 0 5.563zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="volume-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 5h1v6H1a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1zm2 0l4.445-2.964A1 1 0 0 1 9 2.87v10.26a1 1 0 0 1-1.555.833L3 11V5zm10.283 7.89a.5.5 0 0 1-.66-.752A5.485 5.485 0 0 0 14.5 8c0-1.601-.687-3.09-1.865-4.128a.5.5 0 0 1 .661-.75A6.484 6.484 0 0 1 15.5 8a6.485 6.485 0 0 1-2.217 4.89zm-2.002-2.236a.5.5 0 1 1-.652-.758c.55-.472.871-1.157.871-1.896 0-.732-.315-1.411-.856-1.883a.5.5 0 0 1 .658-.753A3.492 3.492 0 0 1 12.5 8c0 1.033-.45 1.994-1.219 2.654z"/></symbol><symbol viewBox="0 0 16 16" id="warning" xmlns="http://www.w3.org/2000/svg"><path d="M15.572 10.506c.867 1.42.375 3.247-1.098 4.082a3.184 3.184 0 0 1-1.57.412h-9.81C1.387 15 0 13.665 0 12.018a2.9 2.9 0 0 1 .427-1.512L5.332 2.47C6.2 1.05 8.096.577 9.57 1.412c.453.257.831.622 1.098 1.059l4.905 8.035zM8.89 3.479a1.014 1.014 0 0 0-.366-.353 1.053 1.053 0 0 0-1.412.353l-4.905 8.035a.967.967 0 0 0-.143.504c0 .549.462.994 1.032.994h9.81c.184 0 .364-.048.523-.137a.974.974 0 0 0 .366-1.361L8.889 3.479zM8 5a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zm0 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="work" xmlns="http://www.w3.org/2000/svg"><path d="M12 3h1a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h1V2a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1zM6 2v1h4V2H6zM3 5a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H3zm1.5 1a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm7 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/cluster_popover.svg b/app/assets/images/illustrations/cluster_popover.svg
new file mode 100644
index 00000000000..202231373f1
--- /dev/null
+++ b/app/assets/images/illustrations/cluster_popover.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="142" height="104" viewBox="0 0 142 104"><g fill="none" fill-rule="evenodd"><g transform="translate(112 4)"><path fill="#FFF" d="M8 4a4 4 0 0 0-4 4v14a4 4 0 0 0 4 4h14a4 4 0 0 0 4-4V8a4 4 0 0 0-4-4H8z"/><path fill="#FDC4A8" fill-rule="nonzero" d="M8 4a4 4 0 0 0-4 4v14a4 4 0 0 0 4 4h14a4 4 0 0 0 4-4V8a4 4 0 0 0-4-4H8zm0-4h14a8 8 0 0 1 8 8v14a8 8 0 0 1-8 8H8a8 8 0 0 1-8-8V8a8 8 0 0 1 8-8z"/><rect width="10" height="10" x="10" y="10" fill="#FC6D26" rx="5"/></g><g transform="translate(5 74)"><rect width="30" height="30" fill="#FFF" rx="8"/><path fill="#E1DBF1" fill-rule="nonzero" d="M8 4a4 4 0 0 0-4 4v14a4 4 0 0 0 4 4h14a4 4 0 0 0 4-4V8a4 4 0 0 0-4-4H8zm0-4h14a8 8 0 0 1 8 8v14a8 8 0 0 1-8 8H8a8 8 0 0 1-8-8V8a8 8 0 0 1 8-8z"/><rect width="10" height="10" x="10" y="10" fill="#6B4FBB" rx="5"/></g><path fill="#FFF" d="M6 4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H6z"/><path fill="#FDC4A8" fill-rule="nonzero" d="M6 4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H6zm0-4h12a6 6 0 0 1 6 6v12a6 6 0 0 1-6 6H6a6 6 0 0 1-6-6V6a6 6 0 0 1 6-6z"/><rect width="8" height="8" x="8" y="8" fill="#FC6D26" rx="4"/><g transform="translate(112 77)"><rect width="24" height="24" fill="#FFF" rx="6"/><path fill="#E1DBF1" fill-rule="nonzero" d="M6 4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H6zm0-4h12a6 6 0 0 1 6 6v12a6 6 0 0 1-6 6H6a6 6 0 0 1-6-6V6a6 6 0 0 1 6-6z"/><rect width="8" height="8" x="8" y="8" fill="#6B4FBB" rx="4"/></g><g transform="translate(46 29)"><rect width="46" height="46" y="2" fill="#E1DBF1" rx="10"/><rect width="46" height="46" fill="#E1DBF1" rx="10"/><path fill="#C3B8E3" fill-rule="nonzero" d="M10 4a6 6 0 0 0-6 6v26a6 6 0 0 0 6 6h26a6 6 0 0 0 6-6V10a6 6 0 0 0-6-6H10zm0-4h26c5.523 0 10 4.477 10 10v26c0 5.523-4.477 10-10 10H10C4.477 46 0 41.523 0 36V10C0 4.477 4.477 0 10 0z"/><rect width="14" height="14" x="16" y="16" fill="#6B4FBB" rx="2"/></g><path fill="#E1DBF1" fill-rule="nonzero" d="M98.413 35.682a2 2 0 1 1-2.826-2.83l2.122-2.12a2 2 0 1 1 2.827 2.83l-2.123 2.12z"/><path fill="#C3B8E3" d="M104.78 29.32a2 2 0 0 1-2.826-2.829l2.122-2.12a2 2 0 0 1 2.827 2.83l-2.122 2.12z"/><path fill="#C3B8E3" fill-rule="nonzero" d="M42.413 89.682a2 2 0 1 1-2.826-2.83l2.122-2.12a2 2 0 1 1 2.827 2.83l-2.123 2.12z"/><path fill="#E1DBF1" d="M48.78 83.32a2 2 0 1 1-2.826-2.829l2.122-2.12a2 2 0 1 1 2.827 2.83l-2.122 2.12z"/><path fill="#E1DBF1" fill-rule="nonzero" d="M27.713 26.531a2 2 0 1 1 2.574-3.062l2.296 1.93a2 2 0 1 1-2.573 3.062l-2.297-1.93z"/><path fill="#C3B8E3" d="M34.604 32.321a2 2 0 1 1 2.573-3.062l2.297 1.93A2 2 0 0 1 36.9 34.25l-2.297-1.93z"/><path fill="#C3B8E3" fill-rule="nonzero" d="M93.74 74.553a2 2 0 0 1 2.52-3.106l2.33 1.891a2 2 0 1 1-2.521 3.106l-2.33-1.891z"/><path fill="#E1DBF1" d="M100.727 80.225a2 2 0 1 1 2.521-3.105l2.33 1.89a2 2 0 1 1-2.522 3.106l-2.33-1.89z"/></g></svg> \ No newline at end of file
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index d9341837149..87109a802e5 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -1,7 +1,7 @@
/* eslint-disable class-methods-use-this */
import _ from 'underscore';
import Cookies from 'js-cookie';
-import { s__ } from './locale';
+import { __ } from './locale';
import { isInIssuePage, updateTooltipTitle } from './lib/utils/common_utils';
import flash from './flash';
import axios from './lib/utils/axios_utils';
@@ -451,7 +451,7 @@ class AwardsHandler {
callback();
}
})
- .catch(() => flash(s__('Something went wrong on our end.')));
+ .catch(() => flash(__('Something went wrong on our end.')));
}
}
diff --git a/app/assets/javascripts/behaviors/secret_values.js b/app/assets/javascripts/behaviors/secret_values.js
index 7f70fce913a..0d6e0dbefcc 100644
--- a/app/assets/javascripts/behaviors/secret_values.js
+++ b/app/assets/javascripts/behaviors/secret_values.js
@@ -15,10 +15,12 @@ export default class SecretValues {
init() {
this.revealButton = this.container.querySelector('.js-secret-value-reveal-button');
- const isRevealed = convertPermissionToBoolean(this.revealButton.dataset.secretRevealStatus);
- this.updateDom(isRevealed);
+ if (this.revealButton) {
+ const isRevealed = convertPermissionToBoolean(this.revealButton.dataset.secretRevealStatus);
+ this.updateDom(isRevealed);
- this.revealButton.addEventListener('click', this.onRevealButtonClicked.bind(this));
+ this.revealButton.addEventListener('click', this.onRevealButtonClicked.bind(this));
+ }
}
onRevealButtonClicked() {
diff --git a/app/assets/javascripts/blob/file_template_mediator.js b/app/assets/javascripts/blob/file_template_mediator.js
index 583e5faa506..37074301b51 100644
--- a/app/assets/javascripts/blob/file_template_mediator.js
+++ b/app/assets/javascripts/blob/file_template_mediator.js
@@ -235,7 +235,7 @@ export default class FileTemplateMediator {
}
setFilename(name) {
- this.$filenameInput.val(name);
+ this.$filenameInput.val(name).trigger('change');
}
getSelected() {
diff --git a/app/assets/javascripts/ci_variable_list/ci_variable_list.js b/app/assets/javascripts/ci_variable_list/ci_variable_list.js
new file mode 100644
index 00000000000..e46478ddb98
--- /dev/null
+++ b/app/assets/javascripts/ci_variable_list/ci_variable_list.js
@@ -0,0 +1,205 @@
+import $ from 'jquery';
+import { convertPermissionToBoolean } from '../lib/utils/common_utils';
+import { s__ } from '../locale';
+import setupToggleButtons from '../toggle_buttons';
+import CreateItemDropdown from '../create_item_dropdown';
+import SecretValues from '../behaviors/secret_values';
+
+const ALL_ENVIRONMENTS_STRING = s__('CiVariable|All environments');
+
+function createEnvironmentItem(value) {
+ return {
+ title: value === '*' ? ALL_ENVIRONMENTS_STRING : value,
+ id: value,
+ text: value,
+ };
+}
+
+export default class VariableList {
+ constructor({
+ container,
+ formField,
+ }) {
+ this.$container = $(container);
+ this.formField = formField;
+ this.environmentDropdownMap = new WeakMap();
+
+ this.inputMap = {
+ id: {
+ selector: '.js-ci-variable-input-id',
+ default: '',
+ },
+ key: {
+ selector: '.js-ci-variable-input-key',
+ default: '',
+ },
+ value: {
+ selector: '.js-ci-variable-input-value',
+ default: '',
+ },
+ protected: {
+ selector: '.js-ci-variable-input-protected',
+ default: 'true',
+ },
+ environment: {
+ // We can't use a `.js-` class here because
+ // gl_dropdown replaces the <input> and doesn't copy over the class
+ // See https://gitlab.com/gitlab-org/gitlab-ce/issues/42458
+ selector: `input[name="${this.formField}[variables_attributes][][environment]"]`,
+ default: '*',
+ },
+ _destroy: {
+ selector: '.js-ci-variable-input-destroy',
+ default: '',
+ },
+ };
+
+ this.secretValues = new SecretValues({
+ container: this.$container[0],
+ valueSelector: '.js-row:not(:last-child) .js-secret-value',
+ placeholderSelector: '.js-row:not(:last-child) .js-secret-value-placeholder',
+ });
+ }
+
+ init() {
+ this.bindEvents();
+ this.secretValues.init();
+ }
+
+ bindEvents() {
+ this.$container.find('.js-row').each((index, rowEl) => {
+ this.initRow(rowEl);
+ });
+
+ this.$container.on('click', '.js-row-remove-button', (e) => {
+ e.preventDefault();
+ this.removeRow($(e.currentTarget).closest('.js-row'));
+ });
+
+ const inputSelector = Object.keys(this.inputMap)
+ .map(name => this.inputMap[name].selector)
+ .join(',');
+
+ // Remove any empty rows except the last row
+ this.$container.on('blur', inputSelector, (e) => {
+ const $row = $(e.currentTarget).closest('.js-row');
+
+ if ($row.is(':not(:last-child)') && !this.checkIfRowTouched($row)) {
+ this.removeRow($row);
+ }
+ });
+
+ // Always make sure there is an empty last row
+ this.$container.on('input trigger-change', inputSelector, () => {
+ const $lastRow = this.$container.find('.js-row').last();
+
+ if (this.checkIfRowTouched($lastRow)) {
+ this.insertRow($lastRow);
+ }
+ });
+ }
+
+ initRow(rowEl) {
+ const $row = $(rowEl);
+
+ setupToggleButtons($row[0]);
+
+ const $environmentSelect = $row.find('.js-variable-environment-toggle');
+ if ($environmentSelect.length) {
+ const createItemDropdown = new CreateItemDropdown({
+ $dropdown: $environmentSelect,
+ defaultToggleLabel: ALL_ENVIRONMENTS_STRING,
+ fieldName: `${this.formField}[variables_attributes][][environment]`,
+ getData: (term, callback) => callback(this.getEnvironmentValues()),
+ createNewItemFromValue: createEnvironmentItem,
+ onSelect: () => {
+ // Refresh the other dropdowns in the variable list
+ // so they have the new value we just picked
+ this.refreshDropdownData();
+
+ $row.find(this.inputMap.environment.selector).trigger('trigger-change');
+ },
+ });
+
+ // Clear out any data that might have been left-over from the row clone
+ createItemDropdown.clearDropdown();
+
+ this.environmentDropdownMap.set($row[0], createItemDropdown);
+ }
+ }
+
+ insertRow($row) {
+ const $rowClone = $row.clone();
+ $rowClone.removeAttr('data-is-persisted');
+
+ // Reset the inputs to their defaults
+ Object.keys(this.inputMap).forEach((name) => {
+ const entry = this.inputMap[name];
+ $rowClone.find(entry.selector).val(entry.default);
+ });
+
+ this.initRow($rowClone);
+
+ $row.after($rowClone);
+ }
+
+ removeRow($row) {
+ const isPersisted = convertPermissionToBoolean($row.attr('data-is-persisted'));
+
+ if (isPersisted) {
+ $row.hide();
+ $row
+ // eslint-disable-next-line no-underscore-dangle
+ .find(this.inputMap._destroy.selector)
+ .val(true);
+ } else {
+ $row.remove();
+ }
+ }
+
+ checkIfRowTouched($row) {
+ return Object.keys(this.inputMap).some((name) => {
+ const entry = this.inputMap[name];
+ const $el = $row.find(entry.selector);
+ return $el.length && $el.val() !== entry.default;
+ });
+ }
+
+ getAllData() {
+ // Ignore the last empty row because we don't want to try persist
+ // a blank variable and run into validation problems.
+ const validRows = this.$container.find('.js-row').toArray().slice(0, -1);
+
+ return validRows.map((rowEl) => {
+ const resultant = {};
+ Object.keys(this.inputMap).forEach((name) => {
+ const entry = this.inputMap[name];
+ const $input = $(rowEl).find(entry.selector);
+ if ($input.length) {
+ resultant[name] = $input.val();
+ }
+ });
+
+ return resultant;
+ });
+ }
+
+ getEnvironmentValues() {
+ const valueMap = this.$container.find(this.inputMap.environment.selector).toArray()
+ .reduce((prevValueMap, envInput) => ({
+ ...prevValueMap,
+ [envInput.value]: envInput.value,
+ }), {});
+
+ return Object.keys(valueMap).map(createEnvironmentItem);
+ }
+
+ refreshDropdownData() {
+ this.$container.find('.js-row').each((index, rowEl) => {
+ const environmentDropdown = this.environmentDropdownMap.get(rowEl);
+ if (environmentDropdown) {
+ environmentDropdown.refreshData();
+ }
+ });
+ }
+}
diff --git a/app/assets/javascripts/ci_variable_list/native_form_variable_list.js b/app/assets/javascripts/ci_variable_list/native_form_variable_list.js
new file mode 100644
index 00000000000..d54ea7df1c3
--- /dev/null
+++ b/app/assets/javascripts/ci_variable_list/native_form_variable_list.js
@@ -0,0 +1,26 @@
+import VariableList from './ci_variable_list';
+
+// Used for the variable list on scheduled pipeline edit page
+export default function setupNativeFormVariableList({
+ container,
+ formField = 'variables',
+}) {
+ const $container = $(container);
+
+ const variableList = new VariableList({
+ container: $container,
+ formField,
+ });
+ variableList.init();
+
+ // Clear out the names in the empty last row so it
+ // doesn't get submitted and throw validation errors
+ $container.closest('form').on('submit trigger-submit', () => {
+ const $lastRow = $container.find('.js-row').last();
+
+ const isTouched = variableList.checkIfRowTouched($lastRow);
+ if (!isTouched) {
+ $lastRow.find('input, textarea').attr('name', '');
+ }
+ });
+}
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index df5a141ea57..b070a59cf15 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -36,10 +36,11 @@ export default class Clusters {
clusterStatus,
clusterStatusReason,
helpPath,
+ ingressHelpPath,
} = document.querySelector('.js-edit-cluster-form').dataset;
this.store = new ClustersStore();
- this.store.setHelpPath(helpPath);
+ this.store.setHelpPaths(helpPath, ingressHelpPath);
this.store.setManagePrometheusPath(managePrometheusPath);
this.store.updateStatus(clusterStatus);
this.store.updateStatusReason(clusterStatusReason);
@@ -95,6 +96,7 @@ export default class Clusters {
props: {
applications: this.state.applications,
helpPath: this.state.helpPath,
+ ingressHelpPath: this.state.ingressHelpPath,
managePrometheusPath: this.state.managePrometheusPath,
},
});
@@ -175,7 +177,7 @@ export default class Clusters {
.map(appId => newApplicationMap[appId].title);
if (appTitles.length > 0) {
- const text = sprintf(s__('ClusterIntegration|%{appList} was successfully installed on your cluster'), {
+ const text = sprintf(s__('ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster'), {
appList: appTitles.join(', '),
});
Flash(text, 'notice', this.successApplicationContainer);
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index 4c9766fb96f..978881a4831 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -18,6 +18,11 @@
required: false,
default: '',
},
+ ingressHelpPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
managePrometheusPath: {
type: String,
required: false,
@@ -28,7 +33,8 @@
generalApplicationDescription() {
return sprintf(
_.escape(s__(
- 'ClusterIntegration|Install applications on your cluster. Read more about %{helpLink}',
+ `ClusterIntegration|Install applications on your Kubernetes cluster.
+ Read more about %{helpLink}`,
)), {
helpLink: `<a href="${this.helpPath}">
${_.escape(s__('ClusterIntegration|installing applications'))}
@@ -39,7 +45,7 @@
},
helmTillerDescription() {
return _.escape(s__(
- `ClusterIntegration|Helm streamlines installing and managing Kubernets applications.
+ `ClusterIntegration|Helm streamlines installing and managing Kubernetes applications.
Tiller runs inside of your Kubernetes Cluster, and manages
releases of your charts.`,
));
@@ -54,7 +60,7 @@
_.escape(s__(
`ClusterIntegration|%{boldNotice} This will add some extra resources
like a load balancer, which may incur additional costs depending on
- the hosting provider Kubernetes is installed on. If you are using GKE,
+ the hosting provider your Kubernetes cluster is installed on. If you are using GKE,
you can %{pricingLink}.`,
)), {
boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`,
@@ -64,13 +70,28 @@
false,
);
+ const externalIpParagraph = sprintf(
+ _.escape(s__(
+ `ClusterIntegration|After installing Ingress, you will need to point your wildcard DNS
+ at the generated external IP address in order to view your app after it is deployed. %{ingressHelpLink}`,
+ )), {
+ ingressHelpLink: `<a href="${this.ingressHelpPath}">
+ ${_.escape(s__('ClusterIntegration|More information'))}
+ </a>`,
+ },
+ false,
+ );
+
return `
<p>
${descriptionParagraph}
</p>
- <p class="append-bottom-0">
+ <p>
${extraCostParagraph}
</p>
+ <p class="settings-message append-bottom-0">
+ ${externalIpParagraph}
+ </p>
`;
},
gitlabRunnerDescription() {
diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js
index 30d295a39fa..904ee5fd475 100644
--- a/app/assets/javascripts/clusters/stores/clusters_store.js
+++ b/app/assets/javascripts/clusters/stores/clusters_store.js
@@ -4,6 +4,7 @@ export default class ClusterStore {
constructor() {
this.state = {
helpPath: null,
+ ingressHelpPath: null,
status: null,
statusReason: null,
applications: {
@@ -39,8 +40,9 @@ export default class ClusterStore {
};
}
- setHelpPath(helpPath) {
+ setHelpPaths(helpPath, ingressHelpPath) {
this.state.helpPath = helpPath;
+ this.state.ingressHelpPath = ingressHelpPath;
}
setManagePrometheusPath(managePrometheusPath) {
diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js
index ff9e4485916..46232726510 100644
--- a/app/assets/javascripts/commons/polyfills.js
+++ b/app/assets/javascripts/commons/polyfills.js
@@ -8,6 +8,8 @@ import 'core-js/fn/promise';
import 'core-js/fn/string/code-point-at';
import 'core-js/fn/string/from-code-point';
import 'core-js/fn/symbol';
+import 'core-js/es6/map';
+import 'core-js/es6/weak-map';
// Browser polyfills
import 'classlist-polyfill';
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 262ed3783fb..ab28b7d8d44 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -12,9 +12,9 @@ import ShortcutsIssuable from './shortcuts_issuable';
import Diff from './diff';
import SearchAutocomplete from './search_autocomplete';
-(function() {
- var Dispatcher;
+var Dispatcher;
+(function() {
Dispatcher = (function() {
function Dispatcher() {
this.initSearch();
@@ -49,46 +49,16 @@ import SearchAutocomplete from './search_autocomplete';
});
switch (page) {
- case 'sessions:new':
- import('./pages/sessions/new')
- .then(callDefault)
- .catch(fail);
- break;
- case 'projects:boards:show':
- case 'projects:boards:index':
- import('./pages/projects/boards/index')
- .then(callDefault)
- .catch(fail);
- shortcut_handler = true;
- break;
case 'projects:environments:metrics':
import('./pages/projects/environments/metrics')
.then(callDefault)
.catch(fail);
break;
case 'projects:merge_requests:index':
- import('./pages/projects/merge_requests/index')
- .then(callDefault)
- .catch(fail);
- shortcut_handler = true;
- break;
case 'projects:issues:index':
- import('./pages/projects/issues/index')
- .then(callDefault)
- .catch(fail);
- shortcut_handler = true;
- break;
case 'projects:issues:show':
- import('./pages/projects/issues/show')
- .then(callDefault)
- .catch(fail);
shortcut_handler = true;
break;
- case 'dashboard:milestones:index':
- import('./pages/dashboard/milestones/index')
- .then(callDefault)
- .catch(fail);
- break;
case 'projects:milestones:index':
import('./pages/projects/milestones/index')
.then(callDefault)
@@ -318,9 +288,6 @@ import SearchAutocomplete from './search_autocomplete';
shortcut_handler = true;
break;
case 'projects:show':
- import('./pages/projects/show')
- .then(callDefault)
- .catch(fail);
shortcut_handler = true;
break;
case 'projects:edit':
@@ -352,9 +319,6 @@ import SearchAutocomplete from './search_autocomplete';
.catch(fail);
break;
case 'groups:show':
- import('./pages/groups/show')
- .then(callDefault)
- .catch(fail);
shortcut_handler = true;
break;
case 'groups:group_members:index':
@@ -363,7 +327,7 @@ import SearchAutocomplete from './search_autocomplete';
.catch(fail);
break;
case 'projects:project_members:index':
- import('./pages/projects/project_members/')
+ import('./pages/projects/project_members')
.then(callDefault)
.catch(fail);
break;
@@ -605,7 +569,7 @@ import SearchAutocomplete from './search_autocomplete';
}
break;
case 'profiles':
- import('./pages/profiles/index/')
+ import('./pages/profiles/index')
.then(callDefault)
.catch(fail);
break;
@@ -662,8 +626,8 @@ import SearchAutocomplete from './search_autocomplete';
return Dispatcher;
})();
+})();
- $(window).on('load', function() {
- new Dispatcher();
- });
-}).call(window);
+export default function initDispatcher() {
+ return new Dispatcher();
+}
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight.js b/app/assets/javascripts/feature_highlight/feature_highlight.js
new file mode 100644
index 00000000000..d65cc6d5d7d
--- /dev/null
+++ b/app/assets/javascripts/feature_highlight/feature_highlight.js
@@ -0,0 +1,65 @@
+import _ from 'underscore';
+import {
+ getSelector,
+ togglePopover,
+ inserted,
+ mouseenter,
+ mouseleave,
+} from './feature_highlight_helper';
+
+export function setupFeatureHighlightPopover(id, debounceTimeout = 300) {
+ const $selector = $(getSelector(id));
+ const $parent = $selector.parent();
+ const $popoverContent = $parent.siblings('.feature-highlight-popover-content');
+ const hideOnScroll = togglePopover.bind($selector, false);
+ const debouncedMouseleave = _.debounce(mouseleave, debounceTimeout);
+
+ $selector
+ // Setup popover
+ .data('content', $popoverContent.prop('outerHTML'))
+ .popover({
+ html: true,
+ // Override the existing template to add custom CSS classes
+ template: `
+ <div class="popover feature-highlight-popover" role="tooltip">
+ <div class="arrow"></div>
+ <div class="popover-content"></div>
+ </div>
+ `,
+ })
+ .on('mouseenter', mouseenter)
+ .on('mouseleave', debouncedMouseleave)
+ .on('inserted.bs.popover', inserted)
+ .on('show.bs.popover', () => {
+ window.addEventListener('scroll', hideOnScroll);
+ })
+ .on('hide.bs.popover', () => {
+ window.removeEventListener('scroll', hideOnScroll);
+ })
+ // Display feature highlight
+ .removeAttr('disabled');
+}
+
+export function findHighestPriorityFeature() {
+ let priorityFeature;
+
+ const sortedFeatureEls = [].slice.call(document.querySelectorAll('.js-feature-highlight')).sort((a, b) =>
+ (a.dataset.highlightPriority || 0) < (b.dataset.highlightPriority || 0));
+
+ const [priorityFeatureEl] = sortedFeatureEls;
+ if (priorityFeatureEl) {
+ priorityFeature = priorityFeatureEl.dataset.highlight;
+ }
+
+ return priorityFeature;
+}
+
+export function highlightFeatures() {
+ const priorityFeature = findHighestPriorityFeature();
+
+ if (priorityFeature) {
+ setupFeatureHighlightPopover(priorityFeature);
+ }
+
+ return priorityFeature;
+}
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js
new file mode 100644
index 00000000000..939d12237f3
--- /dev/null
+++ b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js
@@ -0,0 +1,59 @@
+import axios from '../lib/utils/axios_utils';
+import { __ } from '../locale';
+import Flash from '../flash';
+import LazyLoader from '../lazy_loader';
+
+export const getSelector = highlightId => `.js-feature-highlight[data-highlight=${highlightId}]`;
+
+export function togglePopover(show) {
+ const isAlreadyShown = this.hasClass('js-popover-show');
+ if ((show && isAlreadyShown) || (!show && !isAlreadyShown)) {
+ return false;
+ }
+ this.popover(show ? 'show' : 'hide');
+ this.toggleClass('disable-animation js-popover-show', show);
+
+ return true;
+}
+
+export function dismiss(highlightId) {
+ axios.post(this.attr('data-dismiss-endpoint'), {
+ feature_name: highlightId,
+ })
+ .catch(() => Flash(__('An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again.')));
+
+ togglePopover.call(this, false);
+ this.hide();
+}
+
+export function mouseleave() {
+ if (!$('.popover:hover').length > 0) {
+ const $featureHighlight = $(this);
+ togglePopover.call($featureHighlight, false);
+ }
+}
+
+export function mouseenter() {
+ const $featureHighlight = $(this);
+
+ const showedPopover = togglePopover.call($featureHighlight, true);
+ if (showedPopover) {
+ $('.popover')
+ .on('mouseleave', mouseleave.bind($featureHighlight));
+ }
+}
+
+export function inserted() {
+ const popoverId = this.getAttribute('aria-describedby');
+ const highlightId = this.dataset.highlight;
+ const $popover = $(this);
+ const dismissWrapper = dismiss.bind($popover, highlightId);
+
+ $(`#${popoverId} .dismiss-feature-highlight`)
+ .on('click', dismissWrapper);
+
+ const lazyImg = $(`#${popoverId} .feature-highlight-illustration`)[0];
+ if (lazyImg) {
+ LazyLoader.loadImage(lazyImg);
+ }
+}
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_options.js b/app/assets/javascripts/feature_highlight/feature_highlight_options.js
new file mode 100644
index 00000000000..212643b1e04
--- /dev/null
+++ b/app/assets/javascripts/feature_highlight/feature_highlight_options.js
@@ -0,0 +1,12 @@
+import { highlightFeatures } from './feature_highlight';
+import bp from '../breakpoints';
+
+export default function domContentLoaded() {
+ if (bp.getBreakpointSize() === 'lg') {
+ highlightFeatures();
+ return true;
+ }
+ return false;
+}
+
+document.addEventListener('DOMContentLoaded', domContentLoaded);
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index df20e1e9c88..57a1fa107e5 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -461,7 +461,7 @@ class GfmAutoComplete {
const accentAChar = decodeURI('%C3%80');
const accentYChar = decodeURI('%C3%BF');
- const regexp = new RegExp(`^(?:\\B|[^a-zA-Z0-9_${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`, 'gi');
+ const regexp = new RegExp(`^(?:\\B|[^a-zA-Z0-9_\`${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`, 'gi');
return regexp.exec(targetSubtext);
}
diff --git a/app/assets/javascripts/groups/transfer_dropdown.js b/app/assets/javascripts/groups/transfer_dropdown.js
new file mode 100644
index 00000000000..85b7b08db4d
--- /dev/null
+++ b/app/assets/javascripts/groups/transfer_dropdown.js
@@ -0,0 +1,34 @@
+export default class TransferDropdown {
+ constructor() {
+ this.groupDropdown = $('.js-groups-dropdown');
+ this.parentInput = $('#new_parent_group_id');
+ this.data = this.groupDropdown.data('data');
+ this.init();
+ }
+
+ init() {
+ this.buildDropdown();
+ }
+
+ buildDropdown() {
+ const extraOptions = [{ id: '', text: 'No parent group' }, 'divider'];
+
+ this.groupDropdown.glDropdown({
+ selectable: true,
+ filterable: true,
+ toggleLabel: item => item.text,
+ search: { fields: ['text'] },
+ data: extraOptions.concat(this.data),
+ text: item => item.text,
+ clicked: (options) => {
+ const { e } = options;
+ e.preventDefault();
+ this.assignSelected(options.selectedObj);
+ },
+ });
+ }
+
+ assignSelected(selected) {
+ this.parentInput.val(selected.id);
+ }
+}
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index 411c820cc43..ff65ea99e9a 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -1,7 +1,8 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */
import 'vendor/jquery.waitforimages';
+import axios from './lib/utils/axios_utils';
import { addDelimiter } from './lib/utils/text_utility';
-import Flash from './flash';
+import flash from './flash';
import TaskList from './task_list';
import CreateMergeRequestDropdown from './create_merge_request_dropdown';
import IssuablesHelper from './helpers/issuables_helper';
@@ -42,12 +43,8 @@ export default class Issue {
this.disableCloseReopenButton($button);
url = $button.attr('href');
- return $.ajax({
- type: 'PUT',
- url: url
- })
- .fail(() => new Flash(issueFailMessage))
- .done((data) => {
+ return axios.put(url)
+ .then(({ data }) => {
const isClosedBadge = $('div.status-box-issue-closed');
const isOpenBadge = $('div.status-box-open');
const projectIssuesCounter = $('.issue_counter');
@@ -74,9 +71,10 @@ export default class Issue {
}
}
} else {
- new Flash(issueFailMessage);
+ flash(issueFailMessage);
}
})
+ .catch(() => flash(issueFailMessage))
.then(() => {
this.disableCloseReopenButton($button, false);
});
@@ -115,24 +113,22 @@ export default class Issue {
static initMergeRequests() {
var $container;
$container = $('#merge-requests');
- return $.getJSON($container.data('url')).fail(function() {
- return new Flash('Failed to load referenced merge requests');
- }).done(function(data) {
- if ('html' in data) {
- return $container.html(data.html);
- }
- });
+ return axios.get($container.data('url'))
+ .then(({ data }) => {
+ if ('html' in data) {
+ $container.html(data.html);
+ }
+ }).catch(() => flash('Failed to load referenced merge requests'));
}
static initRelatedBranches() {
var $container;
$container = $('#related-branches');
- return $.getJSON($container.data('url')).fail(function() {
- return new Flash('Failed to load related branches');
- }).done(function(data) {
- if ('html' in data) {
- return $container.html(data.html);
- }
- });
+ return axios.get($container.data('url'))
+ .then(({ data }) => {
+ if ('html' in data) {
+ $container.html(data.html);
+ }
+ }).catch(() => flash('Failed to load related branches'));
}
}
diff --git a/app/assets/javascripts/job.js b/app/assets/javascripts/job.js
index 9b5092c5e3f..d0b7ea75082 100644
--- a/app/assets/javascripts/job.js
+++ b/app/assets/javascripts/job.js
@@ -1,4 +1,5 @@
import _ from 'underscore';
+import axios from './lib/utils/axios_utils';
import { visitUrl } from './lib/utils/url_utility';
import bp from './breakpoints';
import { numberToHumanSize } from './lib/utils/number_utils';
@@ -8,6 +9,7 @@ export default class Job {
constructor(options) {
this.timeout = null;
this.state = null;
+ this.fetchingStatusFavicon = false;
this.options = options || $('.js-build-options').data();
this.pagePath = this.options.pagePath;
@@ -171,12 +173,23 @@ export default class Job {
}
getBuildTrace() {
- return $.ajax({
- url: `${this.pagePath}/trace.json`,
- data: { state: this.state },
+ return axios.get(`${this.pagePath}/trace.json`, {
+ params: { state: this.state },
})
- .done((log) => {
- setCiStatusFavicon(`${this.pagePath}/status.json`);
+ .then((res) => {
+ const log = res.data;
+
+ if (!this.fetchingStatusFavicon) {
+ this.fetchingStatusFavicon = true;
+
+ setCiStatusFavicon(`${this.pagePath}/status.json`)
+ .then(() => {
+ this.fetchingStatusFavicon = false;
+ })
+ .catch(() => {
+ this.fetchingStatusFavicon = false;
+ });
+ }
if (log.state) {
this.state = log.state;
@@ -217,7 +230,7 @@ export default class Job {
visitUrl(this.pagePath);
}
})
- .fail(() => {
+ .catch(() => {
this.$buildRefreshAnimation.remove();
})
.then(() => {
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 664e793fc8e..5ecf81ad11d 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -2,9 +2,12 @@
/* global Issuable */
/* global ListLabel */
import _ from 'underscore';
+import { __ } from './locale';
+import axios from './lib/utils/axios_utils';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import DropdownUtils from './filtered_search/dropdown_utils';
import CreateLabelDropdown from './create_label';
+import flash from './flash';
export default class LabelsSelect {
constructor(els, options = {}) {
@@ -82,99 +85,96 @@ export default class LabelsSelect {
}
$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);
+ axios.put(issueUpdateURL, data)
+ .then(({ 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;
- });
+ if (data.labels.length) {
+ labelTitles = data.labels.map(function(label) {
+ return label.title;
+ });
- if (labelTitles.length > 5) {
- labelTitles = labelTitles.slice(0, 5);
- labelTitles.push('and ' + (data.labels.length - 5) + ' more');
- }
+ if (labelTitles.length > 5) {
+ labelTitles = labelTitles.slice(0, 5);
+ labelTitles.push('and ' + (data.labels.length - 5) + ' more');
+ }
- labelTooltipTitle = labelTitles.join(', ');
- }
- else {
- labelTooltipTitle = '';
- $sidebarLabelTooltip.tooltip('destroy');
- }
+ labelTooltipTitle = labelTitles.join(', ');
+ }
+ else {
+ labelTooltipTitle = '';
+ $sidebarLabelTooltip.tooltip('destroy');
+ }
- $sidebarLabelTooltip
- .attr('title', labelTooltipTitle)
- .tooltip('fixTitle');
+ $sidebarLabelTooltip
+ .attr('title', labelTooltipTitle)
+ .tooltip('fixTitle');
- $('.has-tooltip', $value).tooltip({
- container: 'body'
- });
- });
+ $('.has-tooltip', $value).tooltip({
+ container: 'body'
+ });
+ })
+ .catch(() => flash(__('Error saving label update.')));
};
$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'
+ axios.get(labelUrl)
+ .then((res) => {
+ let data = _.chain(res.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'
+ });
+ }
+ if (showAny) {
+ extraData.unshift({
+ isAny: true,
+ title: 'Any Label'
+ });
+ }
+ if (extraData.length) {
+ extraData.push('divider');
+ data = extraData.concat(data);
+ }
}
- 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();
- }
- });
+ callback(data);
+ if (showMenuAbove) {
+ $dropdown.data('glDropdown').positionMenuAbove();
+ }
+ })
+ .catch(() => flash(__('Error fetching labels.')));
},
renderRow: function(label, instance) {
var $a, $li, color, colorEl, indeterminate, removesAll, selectedClass, spacing, i, marked, dropdownName, dropdownValue;
diff --git a/app/assets/javascripts/lib/utils/ajax_cache.js b/app/assets/javascripts/lib/utils/ajax_cache.js
index 629d8f44e18..616d8952ada 100644
--- a/app/assets/javascripts/lib/utils/ajax_cache.js
+++ b/app/assets/javascripts/lib/utils/ajax_cache.js
@@ -1,3 +1,4 @@
+import axios from './axios_utils';
import Cache from './cache';
class AjaxCache extends Cache {
@@ -18,25 +19,18 @@ class AjaxCache extends Cache {
let pendingRequest = this.pendingRequests[endpoint];
if (!pendingRequest) {
- pendingRequest = new Promise((resolve, reject) => {
- // jQuery 2 is not Promises/A+ compatible (missing catch)
- $.ajax(endpoint) // eslint-disable-line promise/catch-or-return
- .then(data => resolve(data),
- (jqXHR, textStatus, errorThrown) => {
- const error = new Error(`${endpoint}: ${errorThrown}`);
- error.textStatus = textStatus;
- reject(error);
- },
- );
- })
- .then((data) => {
- this.internalStorage[endpoint] = data;
- delete this.pendingRequests[endpoint];
- })
- .catch((error) => {
- delete this.pendingRequests[endpoint];
- throw error;
- });
+ pendingRequest = axios.get(endpoint)
+ .then(({ data }) => {
+ this.internalStorage[endpoint] = data;
+ delete this.pendingRequests[endpoint];
+ })
+ .catch((e) => {
+ const error = new Error(`${endpoint}: ${e.message}`);
+ error.textStatus = e.message;
+
+ delete this.pendingRequests[endpoint];
+ throw error;
+ });
this.pendingRequests[endpoint] = pendingRequest;
}
diff --git a/app/assets/javascripts/lib/utils/axios_utils.js b/app/assets/javascripts/lib/utils/axios_utils.js
index 585214049c7..792871e2ecf 100644
--- a/app/assets/javascripts/lib/utils/axios_utils.js
+++ b/app/assets/javascripts/lib/utils/axios_utils.js
@@ -19,6 +19,10 @@ axios.interceptors.response.use((config) => {
window.activeVueResources -= 1;
return config;
+}, (e) => {
+ window.activeVueResources -= 1;
+
+ return Promise.reject(e);
});
export default axios;
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 8018ec411c1..5811d059e0b 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -1,5 +1,5 @@
-import { getLocationHash } from './url_utility';
import axios from './axios_utils';
+import { getLocationHash } from './url_utility';
export const getPagePath = (index = 0) => $('body').attr('data-page').split(':')[index];
@@ -28,16 +28,11 @@ export const isInIssuePage = () => {
return page === 'issues' && action === 'show';
};
-export const ajaxGet = url => $.ajax({
- type: 'GET',
- url,
- dataType: 'script',
-});
-
-export const ajaxPost = (url, data) => $.ajax({
- type: 'POST',
- url,
- data,
+export const ajaxGet = url => axios.get(url, {
+ params: { format: 'js' },
+ responseType: 'text',
+}).then(({ data }) => {
+ $.globalEval(data);
});
export const rstrip = (val) => {
@@ -412,7 +407,6 @@ window.gl.utils = {
getGroupSlug,
isInIssuePage,
ajaxGet,
- ajaxPost,
rstrip,
updateTooltipTitle,
disableButtonIfEmptyField,
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index d8b881a8fac..b99cb257ce3 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -26,6 +26,7 @@ import './gl_dropdown';
import initTodoToggle from './header';
import initImporterStatus from './importer_status';
import initLayoutNav from './layout_nav';
+import './feature_highlight/feature_highlight_options';
import LazyLoader from './lazy_loader';
import initLogoAnimation from './logo';
import './milestone_select';
@@ -33,7 +34,7 @@ import './projects_dropdown';
import './render_gfm';
import initBreadcrumbs from './breadcrumb';
-import './dispatcher';
+import initDispatcher from './dispatcher';
// eslint-disable-next-line global-require, import/no-commonjs
if (process.env.NODE_ENV !== 'production') require('./test_utils/');
@@ -265,4 +266,6 @@ $(() => {
removeFlashClickListener(flashEl);
});
}
+
+ initDispatcher();
});
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js
index c012b77e0bf..c68b47c9348 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js
@@ -1,4 +1,5 @@
/* eslint-disable no-param-reassign, comma-dangle */
+import axios from '../lib/utils/axios_utils';
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
@@ -10,20 +11,11 @@
}
fetchConflictsData() {
- return $.ajax({
- dataType: 'json',
- url: this.conflictsPath
- });
+ return axios.get(this.conflictsPath);
}
submitResolveConflicts(data) {
- return $.ajax({
- url: this.resolveConflictsPath,
- data: JSON.stringify(data),
- contentType: 'application/json',
- dataType: 'json',
- method: 'POST'
- });
+ return axios.post(this.resolveConflictsPath, data);
}
}
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
index 792b7523889..b4b3c15108d 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
@@ -38,24 +38,23 @@ $(() => {
showDiffViewTypeSwitcher() { return mergeConflictsStore.fileTextTypePresent(); }
},
created() {
- mergeConflictsService
- .fetchConflictsData()
- .done((data) => {
+ mergeConflictsService.fetchConflictsData()
+ .then(({ data }) => {
if (data.type === 'error') {
mergeConflictsStore.setFailedRequest(data.message);
} else {
mergeConflictsStore.setConflictsData(data);
}
- })
- .error(() => {
- mergeConflictsStore.setFailedRequest();
- })
- .always(() => {
+
mergeConflictsStore.setLoadingState(false);
this.$nextTick(() => {
syntaxHighlight($('.js-syntax-highlight'));
});
+ })
+ .catch(() => {
+ mergeConflictsStore.setLoadingState(false);
+ mergeConflictsStore.setFailedRequest();
});
},
methods: {
@@ -82,10 +81,10 @@ $(() => {
mergeConflictsService
.submitResolveConflicts(mergeConflictsStore.getCommitData())
- .done((data) => {
+ .then(({ data }) => {
window.location.href = data.redirect_to;
})
- .error(() => {
+ .catch(() => {
mergeConflictsStore.setSubmitState(false);
new Flash('Failed to save merge conflicts resolutions. Please try again!');
});
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index acfc62fe5cb..3e97a8c758d 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -1,7 +1,8 @@
/* eslint-disable no-new, class-methods-use-this */
import Cookies from 'js-cookie';
-import Flash from './flash';
+import axios from './lib/utils/axios_utils';
+import flash from './flash';
import BlobForkSuggestion from './blob/blob_fork_suggestion';
import initChangesDropdown from './init_changes_dropdown';
import bp from './breakpoints';
@@ -244,15 +245,22 @@ export default class MergeRequestTabs {
if (this.commitsLoaded) {
return;
}
- this.ajaxGet({
- url: `${source}.json`,
- success: (data) => {
+
+ this.toggleLoading(true);
+
+ axios.get(`${source}.json`)
+ .then(({ data }) => {
document.querySelector('div#commits').innerHTML = data.html;
localTimeAgo($('.js-timeago', 'div#commits'));
this.commitsLoaded = true;
this.scrollToElement('#commits');
- },
- });
+
+ this.toggleLoading(false);
+ })
+ .catch(() => {
+ this.toggleLoading(false);
+ flash('An error occurred while fetching this tab.');
+ });
}
mountPipelinesView() {
@@ -283,9 +291,10 @@ export default class MergeRequestTabs {
// some pages like MergeRequestsController#new has query parameters on that anchor
const urlPathname = parseUrlPathname(source);
- this.ajaxGet({
- url: `${urlPathname}.json${location.search}`,
- success: (data) => {
+ this.toggleLoading(true);
+
+ axios.get(`${urlPathname}.json${location.search}`)
+ .then(({ data }) => {
const $container = $('#diffs');
$container.html(data.html);
@@ -335,8 +344,13 @@ export default class MergeRequestTabs {
// (discussion and diff tabs) and `:target` only applies to the first
anchor.addClass('target');
}
- },
- });
+
+ this.toggleLoading(false);
+ })
+ .catch(() => {
+ this.toggleLoading(false);
+ flash('An error occurred while fetching this tab.');
+ });
}
// Show or hide the loading spinner
@@ -346,17 +360,6 @@ export default class MergeRequestTabs {
$('.mr-loading-status .loading').toggle(status);
}
- ajaxGet(options) {
- const defaults = {
- beforeSend: () => this.toggleLoading(true),
- error: () => new Flash('An error occurred while fetching this tab.', 'alert'),
- complete: () => this.toggleLoading(false),
- dataType: 'json',
- type: 'GET',
- };
- $.ajax($.extend({}, defaults, options));
- }
-
diffViewType() {
return $('.inline-parallel-buttons a.active').data('view-type');
}
diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js
index dd6c6b854bc..b1d74250dfd 100644
--- a/app/assets/javascripts/milestone.js
+++ b/app/assets/javascripts/milestone.js
@@ -1,4 +1,5 @@
-import Flash from './flash';
+import axios from './lib/utils/axios_utils';
+import flash from './flash';
export default class Milestone {
constructor() {
@@ -33,15 +34,12 @@ export default class Milestone {
const tabElId = $target.attr('href');
if (endpoint && !$target.hasClass('is-loaded')) {
- $.ajax({
- url: endpoint,
- dataType: 'JSON',
- })
- .fail(() => new Flash('Error loading milestone tab'))
- .done((data) => {
- $(tabElId).html(data.html);
- $target.addClass('is-loaded');
- });
+ axios.get(endpoint)
+ .then(({ data }) => {
+ $(tabElId).html(data.html);
+ $target.addClass('is-loaded');
+ })
+ .catch(() => flash('Error loading milestone tab'));
}
}
}
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 0e854295fe3..6581be606eb 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -2,6 +2,7 @@
/* global Issuable */
/* global ListMilestone */
import _ from 'underscore';
+import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility';
export default class MilestoneSelect {
@@ -52,48 +53,47 @@ export default class MilestoneSelect {
}
return $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
- data: (term, callback) => $.ajax({
- url: milestonesUrl
- }).done((data) => {
- const extraOptions = [];
- if (showAny) {
- extraOptions.push({
- id: 0,
- name: '',
- title: 'Any Milestone'
- });
- }
- if (showNo) {
- extraOptions.push({
- id: -1,
- name: 'No Milestone',
- title: 'No Milestone'
- });
- }
- if (showUpcoming) {
- extraOptions.push({
- id: -2,
- name: '#upcoming',
- title: 'Upcoming'
- });
- }
- if (showStarted) {
- extraOptions.push({
- id: -3,
- name: '#started',
- title: 'Started'
- });
- }
- if (extraOptions.length) {
- extraOptions.push('divider');
- }
+ data: (term, callback) => axios.get(milestonesUrl)
+ .then(({ data }) => {
+ const extraOptions = [];
+ if (showAny) {
+ extraOptions.push({
+ id: 0,
+ name: '',
+ title: 'Any Milestone'
+ });
+ }
+ if (showNo) {
+ extraOptions.push({
+ id: -1,
+ name: 'No Milestone',
+ title: 'No Milestone'
+ });
+ }
+ if (showUpcoming) {
+ extraOptions.push({
+ id: -2,
+ name: '#upcoming',
+ title: 'Upcoming'
+ });
+ }
+ if (showStarted) {
+ extraOptions.push({
+ id: -3,
+ name: '#started',
+ title: 'Started'
+ });
+ }
+ if (extraOptions.length) {
+ extraOptions.push('divider');
+ }
- callback(extraOptions.concat(data));
- if (showMenuAbove) {
- $dropdown.data('glDropdown').positionMenuAbove();
- }
- $(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active');
- }),
+ callback(extraOptions.concat(data));
+ if (showMenuAbove) {
+ $dropdown.data('glDropdown').positionMenuAbove();
+ }
+ $(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active');
+ }),
renderRow: milestone => `
<li data-milestone-id="${milestone.name}">
<a href='#' class='dropdown-menu-milestone-link'>
@@ -200,26 +200,23 @@ export default class MilestoneSelect {
data[abilityName].milestone_id = selected != null ? selected : null;
$loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown');
- return $.ajax({
- type: 'PUT',
- url: issueUpdateURL,
- data: data
- }).done((data) => {
- $dropdown.trigger('loaded.gl.dropdown');
- $loading.fadeOut();
- $selectBox.hide();
- $value.css('display', '');
- if (data.milestone != null) {
- data.milestone.full_path = this.currentProject.full_path;
- data.milestone.remaining = timeFor(data.milestone.due_date);
- data.milestone.name = data.milestone.title;
- $value.html(milestoneLinkTemplate(data.milestone));
- return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone));
- } else {
- $value.html(milestoneLinkNoneTemplate);
- return $sidebarCollapsedValue.find('span').text('No');
- }
- });
+ return axios.put(issueUpdateURL, data)
+ .then(({ data }) => {
+ $dropdown.trigger('loaded.gl.dropdown');
+ $loading.fadeOut();
+ $selectBox.hide();
+ $value.css('display', '');
+ if (data.milestone != null) {
+ data.milestone.full_path = this.currentProject.full_path;
+ data.milestone.remaining = timeFor(data.milestone.due_date);
+ data.milestone.name = data.milestone.title;
+ $value.html(milestoneLinkTemplate(data.milestone));
+ return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone));
+ } else {
+ $value.html(milestoneLinkNoneTemplate);
+ return $sidebarCollapsedValue.find('span').text('No');
+ }
+ });
}
}
});
diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js b/app/assets/javascripts/mini_pipeline_graph_dropdown.js
index ca3d271663b..c7bccd483ac 100644
--- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js
+++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js
@@ -1,5 +1,6 @@
/* eslint-disable no-new */
-import Flash from './flash';
+import flash from './flash';
+import axios from './lib/utils/axios_utils';
/**
* In each pipelines table we have a mini pipeline graph for each pipeline.
@@ -78,27 +79,22 @@ export default class MiniPipelineGraph {
const button = e.relatedTarget;
const endpoint = button.dataset.stageEndpoint;
- return $.ajax({
- dataType: 'json',
- type: 'GET',
- url: endpoint,
- beforeSend: () => {
- this.renderBuildsList(button, '');
- this.toggleLoading(button);
- },
- success: (data) => {
+ this.renderBuildsList(button, '');
+ this.toggleLoading(button);
+
+ axios.get(endpoint)
+ .then(({ data }) => {
this.toggleLoading(button);
this.renderBuildsList(button, data.html);
this.stopDropdownClickPropagation();
- },
- error: () => {
+ })
+ .catch(() => {
this.toggleLoading(button);
if ($(button).parent().hasClass('open')) {
$(button).dropdown('toggle');
}
- new Flash('An error occurred while fetching the builds.', 'alert');
- },
- });
+ flash('An error occurred while fetching the builds.', 'alert');
+ });
}
/**
diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js
index 5aad3908eb6..d3edcb724f1 100644
--- a/app/assets/javascripts/network/branch_graph.js
+++ b/app/assets/javascripts/network/branch_graph.js
@@ -1,5 +1,8 @@
/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-mixed-operators, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len */
+import { __ } from '../locale';
+import axios from '../lib/utils/axios_utils';
+import flash from '../flash';
import Raphael from './raphael';
export default (function() {
@@ -26,16 +29,13 @@ export default (function() {
}
BranchGraph.prototype.load = function() {
- return $.ajax({
- url: this.options.url,
- method: "get",
- dataType: "json",
- success: $.proxy(function(data) {
+ axios.get(this.options.url)
+ .then(({ data }) => {
$(".loading", this.element).hide();
this.prepareData(data.days, data.commits);
- return this.buildGraph();
- }, this)
- });
+ this.buildGraph();
+ })
+ .catch(() => __('Error fetching network graph.'));
};
BranchGraph.prototype.prepareData = function(days, commits) {
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index a2b8e6f6495..8efb8ac5320 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -16,6 +16,7 @@ import Autosize from 'autosize';
import 'vendor/jquery.caret'; // required by jquery.atwho
import 'vendor/jquery.atwho';
import AjaxCache from '~/lib/utils/ajax_cache';
+import axios from './lib/utils/axios_utils';
import { getLocationHash } from './lib/utils/url_utility';
import Flash from './flash';
import CommentTypeToggle from './comment_type_toggle';
@@ -23,7 +24,7 @@ import GLForm from './gl_form';
import loadAwardsHandler from './awards_handler';
import Autosave from './autosave';
import TaskList from './task_list';
-import { ajaxPost, isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils';
+import { isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils';
import imageDiffHelper from './image_diff/helpers/index';
import { localTimeAgo } from './lib/utils/datetime_utility';
@@ -252,26 +253,20 @@ export default class Notes {
return;
}
this.refreshing = true;
- return $.ajax({
- url: this.notes_url,
- headers: { 'X-Last-Fetched-At': this.last_fetched_at },
- dataType: 'json',
- success: (function(_this) {
- return function(data) {
- var notes;
- notes = data.notes;
- _this.last_fetched_at = data.last_fetched_at;
- _this.setPollingInterval(data.notes.length);
- return $.each(notes, function(i, note) {
- _this.renderNote(note);
- });
- };
- })(this)
- }).always((function(_this) {
- return function() {
- return _this.refreshing = false;
- };
- })(this));
+ axios.get(this.notes_url, {
+ headers: {
+ 'X-Last-Fetched-At': this.last_fetched_at,
+ },
+ }).then(({ data }) => {
+ const notes = data.notes;
+ this.last_fetched_at = data.last_fetched_at;
+ this.setPollingInterval(data.notes.length);
+ $.each(notes, (i, note) => this.renderNote(note));
+
+ this.refreshing = false;
+ }).catch(() => {
+ this.refreshing = false;
+ });
}
/**
@@ -1404,7 +1399,7 @@ export default class Notes {
* 2) Identify comment type; a) Main thread b) Discussion thread c) Discussion resolve
* 3) Build temporary placeholder element (using `createPlaceholderNote`)
* 4) Show placeholder note on UI
- * 5) Perform network request to submit the note using `ajaxPost`
+ * 5) Perform network request to submit the note using `axios.post`
* a) If request is successfully completed
* 1. Remove placeholder element
* 2. Show submitted Note element
@@ -1486,8 +1481,10 @@ export default class Notes {
/* eslint-disable promise/catch-or-return */
// Make request to submit comment on server
- ajaxPost(formAction, formData)
- .then((note) => {
+ axios.post(formAction, formData)
+ .then((res) => {
+ const note = res.data;
+
// Submission successful! remove placeholder
$notesContainer.find(`#${noteUniqueId}`).remove();
@@ -1560,7 +1557,7 @@ export default class Notes {
}
$form.trigger('ajax:success', [note]);
- }).fail(() => {
+ }).catch(() => {
// Submission failed, remove placeholder note and show Flash error message
$notesContainer.find(`#${noteUniqueId}`).remove();
@@ -1599,7 +1596,7 @@ export default class Notes {
*
* 1) Get Form metadata
* 2) Update note element with new content
- * 3) Perform network request to submit the updated note using `ajaxPost`
+ * 3) Perform network request to submit the updated note using `axios.post`
* a) If request is successfully completed
* 1. Show submitted Note element
* b) If request failed
@@ -1630,12 +1627,12 @@ export default class Notes {
/* eslint-disable promise/catch-or-return */
// Make request to update comment on server
- ajaxPost(formAction, formData)
- .then((note) => {
+ axios.post(formAction, formData)
+ .then(({ data }) => {
// Submission successful! render final note element
- this.updateNote(note, $editingNote);
+ this.updateNote(data, $editingNote);
})
- .fail(() => {
+ .catch(() => {
// Submission failed, revert back to original note
$noteBodyText.html(_.escape(cachedNoteBodyText));
$editingNote.removeClass('being-posted fade-in');
diff --git a/app/assets/javascripts/notifications_form.js b/app/assets/javascripts/notifications_form.js
index 4534360d577..4e0afe13590 100644
--- a/app/assets/javascripts/notifications_form.js
+++ b/app/assets/javascripts/notifications_form.js
@@ -1,3 +1,7 @@
+import { __ } from './locale';
+import axios from './lib/utils/axios_utils';
+import flash from './flash';
+
export default class NotificationsForm {
constructor() {
this.toggleCheckbox = this.toggleCheckbox.bind(this);
@@ -27,24 +31,20 @@ export default class NotificationsForm {
saveEvent($checkbox, $parent) {
const form = $parent.parents('form:first');
- return $.ajax({
- url: form.attr('action'),
- method: form.attr('method'),
- dataType: 'json',
- data: form.serialize(),
- beforeSend: () => {
- this.showCheckboxLoadingSpinner($parent);
- },
- }).done((data) => {
- $checkbox.enable();
- if (data.saved) {
- $parent.find('.custom-notification-event-loading').toggleClass('fa-spin fa-spinner fa-check is-done');
- setTimeout(() => {
- $parent.removeClass('is-loading')
- .find('.custom-notification-event-loading')
- .toggleClass('fa-spin fa-spinner fa-check is-done');
- }, 2000);
- }
- });
+ this.showCheckboxLoadingSpinner($parent);
+
+ axios[form.attr('method')](form.attr('action'), form.serialize())
+ .then(({ data }) => {
+ $checkbox.enable();
+ if (data.saved) {
+ $parent.find('.custom-notification-event-loading').toggleClass('fa-spin fa-spinner fa-check is-done');
+ setTimeout(() => {
+ $parent.removeClass('is-loading')
+ .find('.custom-notification-event-loading')
+ .toggleClass('fa-spin fa-spinner fa-check is-done');
+ }, 2000);
+ }
+ })
+ .catch(() => flash(__('There was an error saving your notification settings.')));
}
}
diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js
index 6552a88b606..fd3105b1960 100644
--- a/app/assets/javascripts/pager.js
+++ b/app/assets/javascripts/pager.js
@@ -1,4 +1,5 @@
import { getParameterByName } from '~/lib/utils/common_utils';
+import axios from './lib/utils/axios_utils';
import { removeParams } from './lib/utils/url_utility';
const ENDLESS_SCROLL_BOTTOM_PX = 400;
@@ -22,24 +23,22 @@ export default {
getOld() {
this.loading.show();
- $.ajax({
- type: 'GET',
- url: this.url,
- data: `limit=${this.limit}&offset=${this.offset}`,
- dataType: 'json',
- error: () => this.loading.hide(),
- success: (data) => {
- this.append(data.count, this.prepareData(data.html));
- this.callback();
-
- // keep loading until we've filled the viewport height
- if (!this.disable && !this.isScrollable()) {
- this.getOld();
- } else {
- this.loading.hide();
- }
+ axios.get(this.url, {
+ params: {
+ limit: this.limit,
+ offset: this.offset,
},
- });
+ }).then(({ data }) => {
+ this.append(data.count, this.prepareData(data.html));
+ this.callback();
+
+ // keep loading until we've filled the viewport height
+ if (!this.disable && !this.isScrollable()) {
+ this.getOld();
+ } else {
+ this.loading.hide();
+ }
+ }).catch(() => this.loading.hide());
},
append(count, html) {
diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js
index 857a6793fe3..885acfac6d0 100644
--- a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js
+++ b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js
@@ -1,4 +1,7 @@
import _ from 'underscore';
+import axios from '~/lib/utils/axios_utils';
+import flash from '~/flash';
+import { __ } from '~/locale';
export default function initBroadcastMessagesForm() {
$('input#broadcast_message_color').on('input', function onMessageColorInput() {
@@ -18,13 +21,15 @@ export default function initBroadcastMessagesForm() {
if (message === '') {
$('.js-broadcast-message-preview').text('Your message here');
} else {
- $.ajax({
- url: previewPath,
- type: 'POST',
- data: {
- broadcast_message: { message },
+ axios.post(previewPath, {
+ broadcast_message: {
+ message,
},
- });
+ })
+ .then(({ data }) => {
+ $('.js-broadcast-message-preview').html(data.message);
+ })
+ .catch(() => flash(__('An error occurred while rendering preview broadcast message')));
}
}, 250));
}
diff --git a/app/assets/javascripts/pages/admin/cohorts/usage_ping.js b/app/assets/javascripts/pages/admin/cohorts/usage_ping.js
index 2389056bd02..914a9661c27 100644
--- a/app/assets/javascripts/pages/admin/cohorts/usage_ping.js
+++ b/app/assets/javascripts/pages/admin/cohorts/usage_ping.js
@@ -1,12 +1,13 @@
+import axios from '../../../lib/utils/axios_utils';
+import { __ } from '../../../locale';
+import flash from '../../../flash';
+
export default function UsagePing() {
- const usageDataUrl = $('.usage-data').data('endpoint');
+ const el = document.querySelector('.usage-data');
- $.ajax({
- type: 'GET',
- url: usageDataUrl,
- dataType: 'html',
- success(html) {
- $('.usage-data').html(html);
- },
- });
+ axios.get(el.dataset.endpoint, {
+ responseType: 'text',
+ }).then(({ data }) => {
+ el.innerHTML = data;
+ }).catch(() => flash(__('Error fetching usage ping data.')));
}
diff --git a/app/assets/javascripts/pages/dashboard/milestones/index/index.js b/app/assets/javascripts/pages/dashboard/milestones/index/index.js
index 0f2f1bd4a25..38ddebe30d9 100644
--- a/app/assets/javascripts/pages/dashboard/milestones/index/index.js
+++ b/app/assets/javascripts/pages/dashboard/milestones/index/index.js
@@ -1,3 +1,3 @@
import projectSelect from '~/project_select';
-export default projectSelect;
+document.addEventListener('DOMContentLoaded', projectSelect);
diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
index e976a3d2f1d..b3f6a72fdcb 100644
--- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js
+++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
@@ -2,6 +2,9 @@
import { visitUrl } from '~/lib/utils/url_utility';
import UsersSelect from '~/users_select';
import { isMetaClick } from '~/lib/utils/common_utils';
+import { __ } from '../../../../locale';
+import flash from '../../../../flash';
+import axios from '../../../../lib/utils/axios_utils';
export default class Todos {
constructor() {
@@ -59,18 +62,12 @@ export default class Todos {
const target = e.target;
target.setAttribute('disabled', true);
target.classList.add('disabled');
- $.ajax({
- type: 'POST',
- url: target.dataset.href,
- dataType: 'json',
- data: {
- '_method': target.dataset.method,
- },
- success: (data) => {
+
+ axios[target.dataset.method](target.dataset.href)
+ .then(({ data }) => {
this.updateRowState(target);
- return this.updateBadges(data);
- },
- });
+ this.updateBadges(data);
+ }).catch(() => flash(__('Error updating todo status.')));
}
updateRowState(target) {
@@ -98,19 +95,15 @@ export default class Todos {
e.preventDefault();
const target = e.currentTarget;
- const requestData = { '_method': target.dataset.method, ids: this.todo_ids };
target.setAttribute('disabled', true);
target.classList.add('disabled');
- $.ajax({
- type: 'POST',
- url: target.dataset.href,
- dataType: 'json',
- data: requestData,
- success: (data) => {
- this.updateAllState(target, data);
- return this.updateBadges(data);
- },
- });
+
+ axios[target.dataset.method](target.dataset.href, {
+ ids: this.todo_ids,
+ }).then(({ data }) => {
+ this.updateAllState(target, data);
+ this.updateBadges(data);
+ }).catch(() => flash(__('Error updating status for all todos.')));
}
updateAllState(target, data) {
diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js
index 48e8c9550bf..1aeec55a4be 100644
--- a/app/assets/javascripts/pages/groups/edit/index.js
+++ b/app/assets/javascripts/pages/groups/edit/index.js
@@ -1,3 +1,7 @@
import groupAvatar from '~/group_avatar';
+import TransferDropdown from '~/groups/transfer_dropdown';
-export default groupAvatar;
+export default () => {
+ groupAvatar();
+ new TransferDropdown(); // eslint-disable-line no-new
+};
diff --git a/app/assets/javascripts/pages/groups/show/index.js b/app/assets/javascripts/pages/groups/show/index.js
index 6ed0f010f15..5c763986da3 100644
--- a/app/assets/javascripts/pages/groups/show/index.js
+++ b/app/assets/javascripts/pages/groups/show/index.js
@@ -7,7 +7,7 @@ import ProjectsList from '~/projects_list';
import ShortcutsNavigation from '~/shortcuts_navigation';
import initGroupsList from '../../../groups';
-export default () => {
+document.addEventListener('DOMContentLoaded', () => {
const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup');
new ShortcutsNavigation();
new NotificationsForm();
@@ -19,4 +19,4 @@ export default () => {
}
initGroupsList();
-};
+});
diff --git a/app/assets/javascripts/pages/projects/boards/index.js b/app/assets/javascripts/pages/projects/boards/index.js
index 42c9bb5ec99..3aeeedbb45d 100644
--- a/app/assets/javascripts/pages/projects/boards/index.js
+++ b/app/assets/javascripts/pages/projects/boards/index.js
@@ -1,7 +1,7 @@
import UsersSelect from '~/users_select';
import ShortcutsNavigation from '~/shortcuts_navigation';
-export default () => {
+document.addEventListener('DOMContentLoaded', () => {
new UsersSelect(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
-};
+});
diff --git a/app/assets/javascripts/pages/projects/issues/index/index.js b/app/assets/javascripts/pages/projects/issues/index/index.js
index 0d3f35f044d..39c043edc38 100644
--- a/app/assets/javascripts/pages/projects/issues/index/index.js
+++ b/app/assets/javascripts/pages/projects/issues/index/index.js
@@ -7,10 +7,10 @@ import initFilteredSearch from '~/pages/search/init_filtered_search';
import { FILTERED_SEARCH } from '~/pages/constants';
import { ISSUABLE_INDEX } from '~/pages/projects/constants';
-export default () => {
+document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch(FILTERED_SEARCH.ISSUES);
new IssuableIndex(ISSUABLE_INDEX.ISSUE);
new ShortcutsNavigation();
new UsersSelect();
-};
+});
diff --git a/app/assets/javascripts/pages/projects/issues/show/index.js b/app/assets/javascripts/pages/projects/issues/show/index.js
index 48ed8fb2243..da312c1f1b7 100644
--- a/app/assets/javascripts/pages/projects/issues/show/index.js
+++ b/app/assets/javascripts/pages/projects/issues/show/index.js
@@ -1,13 +1,13 @@
-
/* eslint-disable no-new */
+
import initIssuableSidebar from '~/init_issuable_sidebar';
import Issue from '~/issue';
import ShortcutsIssuable from '~/shortcuts_issuable';
import ZenMode from '~/zen_mode';
-export default () => {
+document.addEventListener('DOMContentLoaded', () => {
new Issue();
new ShortcutsIssuable();
new ZenMode();
initIssuableSidebar();
-};
+});
diff --git a/app/assets/javascripts/pages/projects/merge_requests/index/index.js b/app/assets/javascripts/pages/projects/merge_requests/index/index.js
index b386e8fb48d..adadbf28e49 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/index/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/index/index.js
@@ -5,9 +5,9 @@ import initFilteredSearch from '~/pages/search/init_filtered_search';
import { FILTERED_SEARCH } from '~/pages/constants';
import { ISSUABLE_INDEX } from '~/pages/projects/constants';
-export default () => {
+document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch(FILTERED_SEARCH.MERGE_REQUESTS);
new IssuableIndex(ISSUABLE_INDEX.MERGE_REQUEST); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
new UsersSelect(); // eslint-disable-line no-new
-};
+});
diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js
index e30d558726b..863dac0d20e 100644
--- a/app/assets/javascripts/pages/projects/project.js
+++ b/app/assets/javascripts/pages/projects/project.js
@@ -1,7 +1,10 @@
/* eslint-disable func-names, space-before-function-paren, no-var, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */
import Cookies from 'js-cookie';
-import { visitUrl } from '../../lib/utils/url_utility';
+import { __ } from '~/locale';
+import { visitUrl } from '~/lib/utils/url_utility';
+import axios from '~/lib/utils/axios_utils';
+import flash from '~/flash';
import projectSelect from '../../project_select';
export default class Project {
@@ -67,17 +70,15 @@ export default class Project {
$dropdown = $(this);
selected = $dropdown.data('selected');
return $dropdown.glDropdown({
- data: function(term, callback) {
- return $.ajax({
- url: $dropdown.data('refs-url'),
- data: {
+ data(term, callback) {
+ axios.get($dropdown.data('refs-url'), {
+ params: {
ref: $dropdown.data('ref'),
search: term,
},
- dataType: 'json',
- }).done(function(refs) {
- return callback(refs);
- });
+ })
+ .then(({ data }) => callback(data))
+ .catch(() => flash(__('An error occurred while getting projects')));
},
selectable: true,
filterable: true,
diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js
index 55154cdddcb..9b87f249f09 100644
--- a/app/assets/javascripts/pages/projects/show/index.js
+++ b/app/assets/javascripts/pages/projects/show/index.js
@@ -8,7 +8,7 @@ import { ajaxGet } from '~/lib/utils/common_utils';
import Star from '../../../star';
import notificationsDropdown from '../../../notifications_dropdown';
-export default () => {
+document.addEventListener('DOMContentLoaded', () => {
new Star(); // eslint-disable-line no-new
notificationsDropdown();
new ShortcutsNavigation(); // eslint-disable-line no-new
@@ -24,4 +24,4 @@ export default () => {
$('#tree-slider').waitForImages(() => {
ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath);
});
-};
+});
diff --git a/app/assets/javascripts/pages/projects/tree/show/index.js b/app/assets/javascripts/pages/projects/tree/show/index.js
index 28a0160f47d..c4b3356e478 100644
--- a/app/assets/javascripts/pages/projects/tree/show/index.js
+++ b/app/assets/javascripts/pages/projects/tree/show/index.js
@@ -1,3 +1,5 @@
+import Vue from 'vue';
+import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
import TreeView from '../../../../tree';
import ShortcutsNavigation from '../../../../shortcuts_navigation';
import BlobViewer from '../../../../blob/viewer';
@@ -11,5 +13,25 @@ export default () => {
new NewCommitForm($('.js-create-dir-form')); // eslint-disable-line no-new
$('#tree-slider').waitForImages(() =>
ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath));
+
+ const commitPipelineStatusEl = document.getElementById('commit-pipeline-status');
+ const statusLink = document.querySelector('.commit-actions .ci-status-link');
+ if (statusLink != null) {
+ statusLink.remove();
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: commitPipelineStatusEl,
+ components: {
+ commitPipelineStatus,
+ },
+ render(createElement) {
+ return createElement('commit-pipeline-status', {
+ props: {
+ endpoint: commitPipelineStatusEl.dataset.endpoint,
+ },
+ });
+ },
+ });
+ }
};
diff --git a/app/assets/javascripts/pages/sessions/new/index.js b/app/assets/javascripts/pages/sessions/new/index.js
index f163557babc..a0aa0499776 100644
--- a/app/assets/javascripts/pages/sessions/new/index.js
+++ b/app/assets/javascripts/pages/sessions/new/index.js
@@ -2,10 +2,10 @@ import UsernameValidator from './username_validator';
import SigninTabsMemoizer from './signin_tabs_memoizer';
import OAuthRememberMe from './oauth_remember_me';
-export default () => {
+document.addEventListener('DOMContentLoaded', () => {
new UsernameValidator(); // eslint-disable-line no-new
new SigninTabsMemoizer(); // eslint-disable-line no-new
new OAuthRememberMe({ // eslint-disable-line no-new
container: $('.omniauth-container'),
}).bindEvents();
-};
+});
diff --git a/app/assets/javascripts/pages/sessions/new/username_validator.js b/app/assets/javascripts/pages/sessions/new/username_validator.js
index bb34d5d2008..745543c22da 100644
--- a/app/assets/javascripts/pages/sessions/new/username_validator.js
+++ b/app/assets/javascripts/pages/sessions/new/username_validator.js
@@ -1,6 +1,9 @@
/* eslint-disable comma-dangle, consistent-return, class-methods-use-this, arrow-parens, no-param-reassign, max-len */
import _ from 'underscore';
+import axios from '~/lib/utils/axios_utils';
+import flash from '~/flash';
+import { __ } from '~/locale';
const debounceTimeoutDuration = 1000;
const invalidInputClass = 'gl-field-error-outline';
@@ -77,12 +80,9 @@ export default class UsernameValidator {
this.state.pending = true;
this.state.available = false;
this.renderState();
- return $.ajax({
- type: 'GET',
- url: `${gon.relative_url_root}/users/${username}/exists`,
- dataType: 'json',
- success: (res) => this.setAvailabilityState(res.exists)
- });
+ axios.get(`${gon.relative_url_root}/users/${username}/exists`)
+ .then(({ data }) => this.setAvailabilityState(data.exists))
+ .catch(() => flash(__('An error occurred while validating username')));
}
}
diff --git a/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js b/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js
index f1cf6e92ef5..0b1a81bae13 100644
--- a/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js
+++ b/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js
@@ -4,7 +4,7 @@ import GlFieldErrors from '../gl_field_errors';
import intervalPatternInput from './components/interval_pattern_input.vue';
import TimezoneDropdown from './components/timezone_dropdown';
import TargetBranchDropdown from './components/target_branch_dropdown';
-import { setupPipelineVariableList } from './setup_pipeline_variable_list';
+import setupNativeFormVariableList from '../ci_variable_list/native_form_variable_list';
Vue.use(Translate);
@@ -42,5 +42,8 @@ document.addEventListener('DOMContentLoaded', () => {
gl.targetBranchDropdown = new TargetBranchDropdown();
gl.pipelineScheduleFieldErrors = new GlFieldErrors(formElement);
- setupPipelineVariableList($('.js-pipeline-variable-list'));
+ setupNativeFormVariableList({
+ container: $('.js-ci-variable-list-section'),
+ formField: 'schedule',
+ });
});
diff --git a/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js b/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js
deleted file mode 100644
index 9e0e5cacb11..00000000000
--- a/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js
+++ /dev/null
@@ -1,73 +0,0 @@
-import { convertPermissionToBoolean } from '../lib/utils/common_utils';
-
-function insertRow($row) {
- const $rowClone = $row.clone();
- $rowClone.removeAttr('data-is-persisted');
- $rowClone.find('input, textarea').val('');
- $row.after($rowClone);
-}
-
-function removeRow($row) {
- const isPersisted = convertPermissionToBoolean($row.attr('data-is-persisted'));
-
- if (isPersisted) {
- $row.hide();
- $row
- .find('.js-destroy-input')
- .val(1);
- } else {
- $row.remove();
- }
-}
-
-function checkIfRowTouched($row) {
- return $row.find('.js-user-input').toArray().some(el => $(el).val().length > 0);
-}
-
-function setupPipelineVariableList(parent = document) {
- const $parent = $(parent);
-
- $parent.on('click', '.js-row-remove-button', (e) => {
- const $row = $(e.currentTarget).closest('.js-row');
- removeRow($row);
-
- e.preventDefault();
- });
-
- // Remove any empty rows except the last r
- $parent.on('blur', '.js-user-input', (e) => {
- const $row = $(e.currentTarget).closest('.js-row');
-
- const isTouched = checkIfRowTouched($row);
- if ($row.is(':not(:last-child)') && !isTouched) {
- removeRow($row);
- }
- });
-
- // Always make sure there is an empty last row
- $parent.on('input', '.js-user-input', () => {
- const $lastRow = $parent.find('.js-row').last();
-
- const isTouched = checkIfRowTouched($lastRow);
- if (isTouched) {
- insertRow($lastRow);
- }
- });
-
- // Clear out the empty last row so it
- // doesn't get submitted and throw validation errors
- $parent.closest('form').on('submit', () => {
- const $lastRow = $parent.find('.js-row').last();
-
- const isTouched = checkIfRowTouched($lastRow);
- if (!isTouched) {
- $lastRow.find('input, textarea').attr('name', '');
- }
- });
-}
-
-export {
- setupPipelineVariableList,
- insertRow,
- removeRow,
-};
diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js
index 86c7b56198d..464bfb351e7 100644
--- a/app/assets/javascripts/preview_markdown.js
+++ b/app/assets/javascripts/preview_markdown.js
@@ -7,6 +7,10 @@
// more than `x` users are referenced.
//
+import axios from '~/lib/utils/axios_utils';
+import flash from '~/flash';
+import { __ } from '~/locale';
+
var lastTextareaPreviewed;
var lastTextareaHeight = null;
var markdownPreview;
@@ -62,21 +66,17 @@ MarkdownPreview.prototype.fetchMarkdownPreview = function (text, url, success) {
success(this.ajaxCache.response);
return;
}
- $.ajax({
- type: 'POST',
- url: url,
- data: {
- text: text
- },
- dataType: 'json',
- success: (function (response) {
- this.ajaxCache = {
- text: text,
- response: response
- };
- success(response);
- }).bind(this)
- });
+ axios.post(url, {
+ text,
+ })
+ .then(({ data }) => {
+ this.ajaxCache = {
+ text: text,
+ response: data,
+ };
+ success(data);
+ })
+ .catch(() => flash(__('An error occurred while fetching markdown preview')));
};
MarkdownPreview.prototype.hideReferencedUsers = function ($form) {
diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js
index 0da32b4a3cc..586d188350f 100644
--- a/app/assets/javascripts/project_find_file.js
+++ b/app/assets/javascripts/project_find_file.js
@@ -1,6 +1,9 @@
/* 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 */
import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import axios from '~/lib/utils/axios_utils';
+import flash from '~/flash';
+import { __ } from '~/locale';
// highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> )
const highlighter = function(element, text, matches) {
@@ -72,19 +75,14 @@ export default class ProjectFindFile {
// files pathes load
load(url) {
- return $.ajax({
- url: url,
- method: "get",
- dataType: "json",
- success: (function(_this) {
- return function(data) {
- _this.element.find(".loading").hide();
- _this.filePaths = data;
- _this.findFile();
- return _this.element.find(".files-slider tr.tree-item").eq(0).addClass("selected").focus();
- };
- })(this)
- });
+ axios.get(url)
+ .then(({ data }) => {
+ this.element.find('.loading').hide();
+ this.filePaths = data;
+ this.findFile();
+ this.element.find('.files-slider tr.tree-item').eq(0).addClass('selected').focus();
+ })
+ .catch(() => flash(__('An error occurred while loading filenames')));
}
// render result
diff --git a/app/assets/javascripts/project_label_subscription.js b/app/assets/javascripts/project_label_subscription.js
index b65521b278f..64b7dd540f9 100644
--- a/app/assets/javascripts/project_label_subscription.js
+++ b/app/assets/javascripts/project_label_subscription.js
@@ -1,3 +1,7 @@
+import { __ } from './locale';
+import axios from './lib/utils/axios_utils';
+import flash from './flash';
+
export default class ProjectLabelSubscription {
constructor(container) {
this.$container = $(container);
@@ -17,10 +21,7 @@ export default class ProjectLabelSubscription {
$btn.addClass('disabled');
$span.toggleClass('hidden');
- $.ajax({
- type: 'POST',
- url,
- }).done(() => {
+ axios.post(url).then(() => {
let newStatus;
let newAction;
@@ -45,6 +46,6 @@ export default class ProjectLabelSubscription {
return button;
});
- });
+ }).catch(() => flash(__('There was an error subscribing to this label.')));
}
}
diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
new file mode 100644
index 00000000000..63f20a0041d
--- /dev/null
+++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
@@ -0,0 +1,120 @@
+<script>
+ import Visibility from 'visibilityjs';
+ import ciIcon from '~/vue_shared/components/ci_icon.vue';
+ import loadingIcon from '~/vue_shared/components/loading_icon.vue';
+ import Poll from '~/lib/utils/poll';
+ import Flash from '~/flash';
+ import { s__, sprintf } from '~/locale';
+ import tooltip from '~/vue_shared/directives/tooltip';
+ import CommitPipelineService from '../services/commit_pipeline_service';
+
+ export default {
+ directives: {
+ tooltip,
+ },
+ components: {
+ ciIcon,
+ loadingIcon,
+ },
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ /* This prop can be used to replace some of the `render_commit_status`
+ used across GitLab, this way we could use this vue component and add a
+ realtime status where it makes sense
+ realtime: {
+ type: Boolean,
+ required: false,
+ default: true,
+ }, */
+ },
+ data() {
+ return {
+ ciStatus: {},
+ isLoading: true,
+ };
+ },
+ computed: {
+ statusTitle() {
+ return sprintf(s__('Commits|Commit: %{commitText}'), { commitText: this.ciStatus.text });
+ },
+ },
+ mounted() {
+ this.service = new CommitPipelineService(this.endpoint);
+ this.initPolling();
+ },
+ methods: {
+ successCallback(res) {
+ const pipelines = res.data.pipelines;
+ if (pipelines.length > 0) {
+ // The pipeline entity always keeps the latest pipeline info on the `details.status`
+ this.ciStatus = pipelines[0].details.status;
+ }
+ this.isLoading = false;
+ },
+ errorCallback() {
+ this.ciStatus = {
+ text: 'not found',
+ icon: 'status_notfound',
+ group: 'notfound',
+ };
+ this.isLoading = false;
+ Flash(s__('Something went wrong on our end'));
+ },
+ initPolling() {
+ this.poll = new Poll({
+ resource: this.service,
+ method: 'fetchData',
+ successCallback: response => this.successCallback(response),
+ errorCallback: this.errorCallback,
+ });
+
+ if (!Visibility.hidden()) {
+ this.isLoading = true;
+ this.poll.makeRequest();
+ } else {
+ this.fetchPipelineCommitData();
+ }
+
+ Visibility.change(() => {
+ if (!Visibility.hidden()) {
+ this.poll.restart();
+ } else {
+ this.poll.stop();
+ }
+ });
+ },
+ fetchPipelineCommitData() {
+ this.service.fetchData()
+ .then(this.successCallback)
+ .catch(this.errorCallback);
+ },
+ },
+ destroy() {
+ this.poll.stop();
+ },
+ };
+</script>
+<template>
+ <div>
+ <loading-icon
+ label="Loading pipeline status"
+ size="3"
+ v-if="isLoading"
+ />
+ <a
+ v-else
+ :href="ciStatus.details_path"
+ >
+ <ci-icon
+ v-tooltip
+ :title="statusTitle"
+ :aria-label="statusTitle"
+ data-container="body"
+ :status="ciStatus"
+ />
+ </a>
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects/tree/services/commit_pipeline_service.js b/app/assets/javascripts/projects/tree/services/commit_pipeline_service.js
new file mode 100644
index 00000000000..4b4189bc2de
--- /dev/null
+++ b/app/assets/javascripts/projects/tree/services/commit_pipeline_service.js
@@ -0,0 +1,11 @@
+import axios from '~/lib/utils/axios_utils';
+
+export default class CommitPipelineService {
+ constructor(endpoint) {
+ this.endpoint = endpoint;
+ }
+
+ fetchData() {
+ return axios.get(this.endpoint);
+ }
+}
diff --git a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
index 55c93923cc8..59ad5b45855 100644
--- a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
+++ b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
@@ -1,3 +1,4 @@
+import axios from '../lib/utils/axios_utils';
import PANEL_STATE from './constants';
import { backOff } from '../lib/utils/common_utils';
@@ -81,24 +82,20 @@ export default class PrometheusMetrics {
loadActiveMetrics() {
this.showMonitoringMetricsPanelState(PANEL_STATE.LOADING);
backOff((next, stop) => {
- $.ajax({
- url: this.activeMetricsEndpoint,
- dataType: 'json',
- global: false,
- })
- .done((res) => {
- if (res && res.success) {
- stop(res);
+ axios.get(this.activeMetricsEndpoint)
+ .then(({ data }) => {
+ if (data && data.success) {
+ stop(data);
} else {
this.backOffRequestCounter = this.backOffRequestCounter += 1;
if (this.backOffRequestCounter < 3) {
next();
} else {
- stop(res);
+ stop(data);
}
}
})
- .fail(stop);
+ .catch(stop);
})
.then((res) => {
if (res && res.data && res.data.length) {
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js b/app/assets/javascripts/protected_branches/protected_branch_edit.js
index 632625da8e7..b51b3e9a6ff 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js
@@ -1,5 +1,5 @@
-/* eslint-disable no-new */
-import Flash from '../flash';
+import flash from '../flash';
+import axios from '../lib/utils/axios_utils';
import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown';
export default class ProtectedBranchEdit {
@@ -38,29 +38,25 @@ export default class ProtectedBranchEdit {
this.$allowedToMergeDropdown.disable();
this.$allowedToPushDropdown.disable();
- $.ajax({
- type: 'POST',
- url: this.$wrap.data('url'),
- dataType: 'json',
- data: {
- _method: 'PATCH',
- protected_branch: {
- merge_access_levels_attributes: [{
- id: this.$allowedToMergeDropdown.data('access-level-id'),
- access_level: $allowedToMergeInput.val(),
- }],
- push_access_levels_attributes: [{
- id: this.$allowedToPushDropdown.data('access-level-id'),
- access_level: $allowedToPushInput.val(),
- }],
- },
+ axios.patch(this.$wrap.data('url'), {
+ protected_branch: {
+ merge_access_levels_attributes: [{
+ id: this.$allowedToMergeDropdown.data('access-level-id'),
+ access_level: $allowedToMergeInput.val(),
+ }],
+ push_access_levels_attributes: [{
+ id: this.$allowedToPushDropdown.data('access-level-id'),
+ access_level: $allowedToPushInput.val(),
+ }],
},
- error() {
- new Flash('Failed to update branch!', 'alert', document.querySelector('.js-protected-branches-list'));
- },
- }).always(() => {
+ }).then(() => {
+ this.$allowedToMergeDropdown.enable();
+ this.$allowedToPushDropdown.enable();
+ }).catch(() => {
this.$allowedToMergeDropdown.enable();
this.$allowedToPushDropdown.enable();
+
+ flash('Failed to update branch!', 'alert', document.querySelector('.js-protected-branches-list'));
});
}
}
diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.js b/app/assets/javascripts/protected_tags/protected_tag_edit.js
index dad0ad25b65..21a258cf93c 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_edit.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_edit.js
@@ -1,5 +1,5 @@
-/* eslint-disable no-new */
-import Flash from '../flash';
+import flash from '../flash';
+import axios from '../lib/utils/axios_utils';
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
export default class ProtectedTagEdit {
@@ -28,24 +28,19 @@ export default class ProtectedTagEdit {
this.$allowedToCreateDropdownButton.disable();
- $.ajax({
- type: 'POST',
- url: this.$wrap.data('url'),
- dataType: 'json',
- data: {
- _method: 'PATCH',
- protected_tag: {
- create_access_levels_attributes: [{
- id: this.$allowedToCreateDropdownButton.data('access-level-id'),
- access_level: $allowedToCreateInput.val(),
- }],
- },
+ axios.patch(this.$wrap.data('url'), {
+ protected_tag: {
+ create_access_levels_attributes: [{
+ id: this.$allowedToCreateDropdownButton.data('access-level-id'),
+ access_level: $allowedToCreateInput.val(),
+ }],
},
- error() {
- new Flash('Failed to update tag!', 'alert', document.querySelector('.js-protected-tags-list'));
- },
- }).always(() => {
+ }).then(() => {
+ this.$allowedToCreateDropdownButton.enable();
+ }).catch(() => {
this.$allowedToCreateDropdownButton.enable();
+
+ flash('Failed to update tag!', 'alert', document.querySelector('.js-protected-tags-list'));
});
}
}
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index b830fcf7e80..01c3be5411f 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -2,6 +2,8 @@
import _ from 'underscore';
import Cookies from 'js-cookie';
+import flash from './flash';
+import axios from './lib/utils/axios_utils';
function Sidebar(currentUser) {
this.toggleTodo = this.toggleTodo.bind(this);
@@ -62,7 +64,7 @@ Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
Sidebar.prototype.toggleTodo = function(e) {
var $btnText, $this, $todoLoading, ajaxType, url;
$this = $(e.currentTarget);
- ajaxType = $this.attr('data-delete-path') ? 'DELETE' : 'POST';
+ ajaxType = $this.attr('data-delete-path') ? 'delete' : 'post';
if ($this.attr('data-delete-path')) {
url = "" + ($this.attr('data-delete-path'));
} else {
@@ -71,25 +73,14 @@ Sidebar.prototype.toggleTodo = function(e) {
$this.tooltip('hide');
- return $.ajax({
- url: url,
- type: ajaxType,
- dataType: 'json',
- data: {
- issuable_id: $this.data('issuable-id'),
- issuable_type: $this.data('issuable-type')
- },
- beforeSend: (function(_this) {
- return function() {
- $('.js-issuable-todo').disable()
- .addClass('is-loading');
- };
- })(this)
- }).done((function(_this) {
- return function(data) {
- return _this.todoUpdateDone(data);
- };
- })(this));
+ $('.js-issuable-todo').disable().addClass('is-loading');
+
+ axios[ajaxType](url, {
+ issuable_id: $this.data('issuable-id'),
+ issuable_type: $this.data('issuable-type'),
+ }).then(({ data }) => {
+ this.todoUpdateDone(data);
+ }).catch(() => flash(`There was an error ${ajaxType === 'post' ? 'adding a' : 'deleting the'} todo.`));
};
Sidebar.prototype.todoUpdateDone = function(data) {
diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js
index cd5ab53eace..c5dddd001bb 100644
--- a/app/assets/javascripts/shortcuts.js
+++ b/app/assets/javascripts/shortcuts.js
@@ -1,5 +1,6 @@
import Cookies from 'js-cookie';
import Mousetrap from 'mousetrap';
+import axios from './lib/utils/axios_utils';
import { refreshCurrentPage, visitUrl } from './lib/utils/url_utility';
import findAndFollowLink from './shortcuts_dashboard_navigation';
@@ -85,21 +86,21 @@ export default class Shortcuts {
$modal.modal('toggle');
}
- $.ajax({
- url: gon.shortcuts_path,
- dataType: 'script',
- success() {
- if (location && location.length > 0) {
- const results = [];
- for (let i = 0, len = location.length; i < len; i += 1) {
- results.push($(location[i]).show());
- }
- return results;
+ return axios.get(gon.shortcuts_path, {
+ responseType: 'text',
+ }).then(({ data }) => {
+ $.globalEval(data);
+
+ if (location && location.length > 0) {
+ const results = [];
+ for (let i = 0, len = location.length; i < len; i += 1) {
+ results.push($(location[i]).show());
}
+ return results;
+ }
- $('.hidden-shortcut').show();
- return $('.js-more-help-button').remove();
- },
+ $('.hidden-shortcut').show();
+ return $('.js-more-help-button').remove();
});
}
diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js
index dcbec40c79e..129a551cbcd 100644
--- a/app/assets/javascripts/task_list.js
+++ b/app/assets/javascripts/task_list.js
@@ -1,4 +1,5 @@
import 'deckar01-task_list';
+import axios from './lib/utils/axios_utils';
import Flash from './flash';
export default class TaskList {
@@ -7,11 +8,11 @@ export default class TaskList {
this.dataType = options.dataType;
this.fieldName = options.fieldName;
this.onSuccess = options.onSuccess || (() => {});
- this.onError = function showFlash(response) {
+ this.onError = function showFlash(e) {
let errorMessages = '';
- if (response.responseJSON) {
- errorMessages = response.responseJSON.errors.join(' ');
+ if (e.response.data && typeof e.response.data === 'object') {
+ errorMessages = e.response.data.errors.join(' ');
}
return new Flash(errorMessages || 'Update failed', 'alert');
@@ -38,12 +39,9 @@ export default class TaskList {
patchData[this.dataType] = {
[this.fieldName]: $target.val(),
};
- return $.ajax({
- type: 'PATCH',
- url: $target.data('update-url') || $('form.js-issuable-update').attr('action'),
- data: patchData,
- success: this.onSuccess,
- error: this.onError,
- });
+
+ return axios.patch($target.data('update-url') || $('form.js-issuable-update').attr('action'), patchData)
+ .then(({ data }) => this.onSuccess(data))
+ .catch(err => this.onError(err));
}
}
diff --git a/app/assets/javascripts/toggle_buttons.js b/app/assets/javascripts/toggle_buttons.js
index 974dc3ee052..199b14458ed 100644
--- a/app/assets/javascripts/toggle_buttons.js
+++ b/app/assets/javascripts/toggle_buttons.js
@@ -8,12 +8,12 @@ import { convertPermissionToBoolean } from './lib/utils/common_utils';
```
%button.js-project-feature-toggle.project-feature-toggle{ type: "button",
class: "#{'is-checked' if enabled?}",
- 'aria-label': _('Toggle Cluster') }
+ 'aria-label': _('Toggle Kubernetes Cluster') }
%input{ type: "hidden", class: 'js-project-feature-toggle-input', value: enabled? }
```
*/
-function updatetoggle(toggle, isOn) {
+function updateToggle(toggle, isOn) {
toggle.classList.toggle('is-checked', isOn);
}
@@ -21,7 +21,7 @@ function onToggleClicked(toggle, input, clickCallback) {
const previousIsOn = convertPermissionToBoolean(input.value);
// Visually change the toggle and start loading
- updatetoggle(toggle, !previousIsOn);
+ updateToggle(toggle, !previousIsOn);
toggle.setAttribute('disabled', true);
toggle.classList.toggle('is-loading', true);
@@ -32,7 +32,7 @@ function onToggleClicked(toggle, input, clickCallback) {
})
.catch(() => {
// Revert the visuals if something goes wrong
- updatetoggle(toggle, previousIsOn);
+ updateToggle(toggle, previousIsOn);
})
.then(() => {
// Remove the loading indicator in any case
@@ -54,7 +54,7 @@ export default function setupToggleButtons(container, clickCallback = () => {})
const isOn = convertPermissionToBoolean(input.value);
// Get the visible toggle in sync with the hidden input
- updatetoggle(toggle, isOn);
+ updateToggle(toggle, isOn);
toggle.addEventListener('click', onToggleClicked.bind(null, toggle, input, clickCallback));
});
diff --git a/app/assets/javascripts/users/activity_calendar.js b/app/assets/javascripts/users/activity_calendar.js
index 0ca54faa71c..57306322aa4 100644
--- a/app/assets/javascripts/users/activity_calendar.js
+++ b/app/assets/javascripts/users/activity_calendar.js
@@ -1,7 +1,10 @@
import _ from 'underscore';
import { scaleLinear, scaleThreshold } from 'd3-scale';
import { select } from 'd3-selection';
-import { getDayName, getDayDifference } from '../lib/utils/datetime_utility';
+import { getDayName, getDayDifference } from '~/lib/utils/datetime_utility';
+import axios from '~/lib/utils/axios_utils';
+import flash from '~/flash';
+import { __ } from '~/locale';
const d3 = { select, scaleLinear, scaleThreshold };
@@ -221,14 +224,16 @@ export default class ActivityCalendar {
this.currentSelectedDate.getDate(),
].join('-');
- $.ajax({
- url: this.calendarActivitiesPath,
- data: { date },
- cache: false,
- dataType: 'html',
- beforeSend: () => $('.user-calendar-activities').html(LOADING_HTML),
- success: data => $('.user-calendar-activities').html(data),
- });
+ $('.user-calendar-activities').html(LOADING_HTML);
+
+ axios.get(this.calendarActivitiesPath, {
+ params: {
+ date,
+ },
+ responseType: 'text',
+ })
+ .then(({ data }) => $('.user-calendar-activities').html(data))
+ .catch(() => flash(__('An error occurred while retrieving calendar activity')));
} else {
this.currentSelectedDate = '';
$('.user-calendar-activities').html('');
diff --git a/app/assets/javascripts/users/user_tabs.js b/app/assets/javascripts/users/user_tabs.js
index 992baa9a1ef..e13b9839a20 100644
--- a/app/assets/javascripts/users/user_tabs.js
+++ b/app/assets/javascripts/users/user_tabs.js
@@ -1,6 +1,9 @@
+import axios from '../lib/utils/axios_utils';
import Activities from '../activities';
import ActivityCalendar from './activity_calendar';
import { localTimeAgo } from '../lib/utils/datetime_utility';
+import { __ } from '../locale';
+import flash from '../flash';
/**
* UserTabs
@@ -131,18 +134,20 @@ export default class UserTabs {
}
loadTab(action, endpoint) {
- return $.ajax({
- beforeSend: () => this.toggleLoading(true),
- complete: () => this.toggleLoading(false),
- dataType: 'json',
- url: endpoint,
- success: (data) => {
+ this.toggleLoading(true);
+
+ return axios.get(endpoint)
+ .then(({ data }) => {
const tabSelector = `div#${action}`;
this.$parentEl.find(tabSelector).html(data.html);
this.loaded[action] = true;
localTimeAgo($('.js-timeago', tabSelector));
- },
- });
+
+ this.toggleLoading(false);
+ })
+ .catch(() => {
+ this.toggleLoading(false);
+ });
}
loadActivities() {
@@ -158,17 +163,15 @@ export default class UserTabs {
utcFormatted = `UTC${utcOffset > 0 ? '+' : ''}${(utcOffset / 3600)}`;
}
- $.ajax({
- dataType: 'json',
- url: calendarPath,
- success: (activityData) => {
+ axios.get(calendarPath)
+ .then(({ data }) => {
$calendarWrap.html(CALENDAR_TEMPLATE);
$calendarWrap.find('.calendar-hint').append(`(Timezone: ${utcFormatted})`);
// eslint-disable-next-line no-new
- new ActivityCalendar('.js-contrib-calendar', activityData, calendarActivitiesPath, utcOffset);
- },
- });
+ new ActivityCalendar('.js-contrib-calendar', data, calendarActivitiesPath, utcOffset);
+ })
+ .catch(() => flash(__('There was an error loading users activity calendar.')));
// eslint-disable-next-line no-new
new Activities();
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index ab108906732..eaed81cf79e 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -2,6 +2,7 @@
/* global Issuable */
/* global emitSidebarEvent */
import _ from 'underscore';
+import axios from './lib/utils/axios_utils';
// TODO: remove eventHub hack after code splitting refactor
window.emitSidebarEvent = window.emitSidebarEvent || $.noop;
@@ -177,32 +178,28 @@ function UsersSelect(currentUser, els, options = {}) {
$loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown');
- return $.ajax({
- type: 'PUT',
- dataType: 'json',
- url: issueURL,
- data: data
- }).done(function(data) {
- var user;
- $dropdown.trigger('loaded.gl.dropdown');
- $loading.fadeOut();
- if (data.assignee) {
- user = {
- name: data.assignee.name,
- username: data.assignee.username,
- avatar: data.assignee.avatar_url
- };
- } else {
- user = {
- name: 'Unassigned',
- username: '',
- avatar: ''
- };
- }
- $value.html(assigneeTemplate(user));
- $collapsedSidebar.attr('title', _.escape(user.name)).tooltip('fixTitle');
- return $collapsedSidebar.html(collapsedAssigneeTemplate(user));
- });
+ return axios.put(issueURL, data)
+ .then(({ data }) => {
+ var user;
+ $dropdown.trigger('loaded.gl.dropdown');
+ $loading.fadeOut();
+ if (data.assignee) {
+ user = {
+ name: data.assignee.name,
+ username: data.assignee.username,
+ avatar: data.assignee.avatar_url
+ };
+ } else {
+ user = {
+ name: 'Unassigned',
+ username: '',
+ avatar: ''
+ };
+ }
+ $value.html(assigneeTemplate(user));
+ $collapsedSidebar.attr('title', _.escape(user.name)).tooltip('fixTitle');
+ return $collapsedSidebar.html(collapsedAssigneeTemplate(user));
+ });
};
collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author_link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>');
assigneeTemplate = _.template('<% if (username) { %> <a class="author_link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>');
@@ -660,38 +657,33 @@ UsersSelect.prototype.user = function(user_id, callback) {
var url;
url = this.buildUrl(this.userPath);
url = url.replace(':id', user_id);
- return $.ajax({
- url: url,
- dataType: "json"
- }).done(function(user) {
- return callback(user);
- });
+ return axios.get(url)
+ .then(({ data }) => {
+ callback(data);
+ });
};
// Return users list. Filtered by query
// Only active users retrieved
UsersSelect.prototype.users = function(query, options, callback) {
- var url;
- url = this.buildUrl(this.usersPath);
- return $.ajax({
- url: url,
- data: {
- search: query,
- per_page: options.perPage || 20,
- active: true,
- project_id: options.projectId || null,
- group_id: options.groupId || null,
- skip_ldap: options.skipLdap || null,
- todo_filter: options.todoFilter || null,
- todo_state_filter: options.todoStateFilter || null,
- current_user: options.showCurrentUser || null,
- author_id: options.authorId || null,
- skip_users: options.skipUsers || null
- },
- dataType: "json"
- }).done(function(users) {
- return callback(users);
- });
+ const url = this.buildUrl(this.usersPath);
+ const params = {
+ search: query,
+ per_page: options.perPage || 20,
+ active: true,
+ project_id: options.projectId || null,
+ group_id: options.groupId || null,
+ skip_ldap: options.skipLdap || null,
+ todo_filter: options.todoFilter || null,
+ todo_state_filter: options.todoStateFilter || null,
+ current_user: options.showCurrentUser || null,
+ author_id: options.authorId || null,
+ skip_users: options.skipUsers || null
+ };
+ return axios.get(url, { params })
+ .then(({ data }) => {
+ callback(data);
+ });
};
UsersSelect.prototype.buildUrl = function(url) {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.js
deleted file mode 100644
index 982b5e8e373..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import tooltip from '../../vue_shared/directives/tooltip';
-
-export default {
- name: 'MRWidgetAuthor',
- props: {
- author: { type: Object, required: true },
- showAuthorName: { type: Boolean, required: false, default: true },
- showAuthorTooltip: { type: Boolean, required: false, default: false },
- },
- directives: {
- tooltip,
- },
- template: `
- <a
- :href="author.webUrl || author.web_url"
- class="author-link inline"
- :v-tooltip="showAuthorTooltip"
- :title="author.name">
- <img
- :src="author.avatarUrl || author.avatar_url"
- class="avatar avatar-inline s16" />
- <span
- v-if="showAuthorName"
- class="author">{{author.name}}
- </span>
- </a>
- `,
-};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue
new file mode 100644
index 00000000000..cb6e9858736
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue
@@ -0,0 +1,53 @@
+<script>
+ import tooltip from '../../vue_shared/directives/tooltip';
+
+ export default {
+ name: 'MRWidgetAuthor',
+ directives: {
+ tooltip,
+ },
+ props: {
+ author: {
+ type: Object,
+ required: true,
+ },
+ showAuthorName: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ showAuthorTooltip: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ authorUrl() {
+ return this.author.webUrl || this.author.web_url;
+ },
+ avatarUrl() {
+ return this.author.avatarUrl || this.author.avatar_url;
+ },
+ },
+ };
+</script>
+<template>
+ <a
+ :href="authorUrl"
+ class="author-link inline"
+ :v-tooltip="showAuthorTooltip"
+ :title="author.name"
+ >
+ <img
+ :src="avatarUrl"
+ class="avatar avatar-inline s16"
+ />
+ <span
+ class="author"
+ v-if="showAuthorName"
+ >
+ {{ author.name }}
+ </span>
+ </a>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.js
deleted file mode 100644
index 6d2ed5fda64..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import MRWidgetAuthor from './mr_widget_author';
-
-export default {
- name: 'MRWidgetAuthorTime',
- props: {
- actionText: { type: String, required: true },
- author: { type: Object, required: true },
- dateTitle: { type: String, required: true },
- dateReadable: { type: String, required: true },
- },
- components: {
- 'mr-widget-author': MRWidgetAuthor,
- },
- template: `
- <h4 class="js-mr-widget-author">
- {{actionText}}
- <mr-widget-author :author="author" />
- <time
- :title="dateTitle"
- data-toggle="tooltip"
- data-placement="top"
- data-container="body">
- {{dateReadable}}
- </time>
- </h4>
- `,
-};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue
new file mode 100644
index 00000000000..8f1fd809a81
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue
@@ -0,0 +1,42 @@
+<script>
+ import mrWidgetAuthor from './mr_widget_author.vue';
+
+ export default {
+ name: 'MRWidgetAuthorTime',
+ components: {
+ mrWidgetAuthor,
+ },
+ props: {
+ actionText: {
+ type: String,
+ required: true,
+ },
+ author: {
+ type: Object,
+ required: true,
+ },
+ dateTitle: {
+ type: String,
+ required: true,
+ },
+ dateReadable: {
+ type: String,
+ required: true,
+ },
+ },
+ };
+</script>
+<template>
+ <h4 class="js-mr-widget-author">
+ {{ actionText }}
+ <mr-widget-author :author="author" />
+ <time
+ :title="dateTitle"
+ data-toggle="tooltip"
+ data-placement="top"
+ data-container="body"
+ >
+ {{ dateReadable }}
+ </time>
+ </h4>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js
deleted file mode 100644
index de6e5149a87..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js
+++ /dev/null
@@ -1,117 +0,0 @@
-import tooltip from '../../vue_shared/directives/tooltip';
-import { pluralize } from '../../lib/utils/text_utility';
-import icon from '../../vue_shared/components/icon.vue';
-
-export default {
- name: 'MRWidgetHeader',
- props: {
- mr: { type: Object, required: true },
- },
- directives: {
- tooltip,
- },
- components: {
- icon,
- },
- computed: {
- shouldShowCommitsBehindText() {
- return this.mr.divergedCommitsCount > 0;
- },
- commitsText() {
- return pluralize('commit', this.mr.divergedCommitsCount);
- },
- branchNameClipboardData() {
- // This supports code in app/assets/javascripts/copy_to_clipboard.js that
- // works around ClipboardJS limitations to allow the context-specific
- // copy/pasting of plain text or GFM.
- return JSON.stringify({
- text: this.mr.sourceBranch,
- gfm: `\`${this.mr.sourceBranch}\``,
- });
- },
- },
- methods: {
- isBranchTitleLong(branchTitle) {
- return branchTitle.length > 32;
- },
- },
- template: `
- <div class="mr-source-target">
- <div class="normal">
- <strong>
- Request to merge
- <span
- class="label-branch"
- :class="{'label-truncated': isBranchTitleLong(mr.sourceBranch)}"
- :title="isBranchTitleLong(mr.sourceBranch) ? mr.sourceBranch : ''"
- data-placement="bottom"
- :v-tooltip="isBranchTitleLong(mr.sourceBranch)"
- v-html="mr.sourceBranchLink"></span>
- <button
- v-tooltip
- class="btn btn-transparent btn-clipboard"
- data-title="Copy branch name to clipboard"
- :data-clipboard-text="branchNameClipboardData">
- <i
- aria-hidden="true"
- class="fa fa-clipboard"></i>
- </button>
- into
- <span
- class="label-branch"
- :v-tooltip="isBranchTitleLong(mr.sourceBranch)"
- :class="{'label-truncatedtooltip': isBranchTitleLong(mr.targetBranch)}"
- :title="isBranchTitleLong(mr.targetBranch) ? mr.targetBranch : ''"
- data-placement="bottom">
- <a :href="mr.targetBranchTreePath">{{mr.targetBranch}}</a>
- </span>
- </strong>
- <span
- v-if="shouldShowCommitsBehindText"
- class="diverged-commits-count">
- (<a :href="mr.targetBranchPath">{{mr.divergedCommitsCount}} {{commitsText}} behind</a>)
- </span>
- </div>
- <div v-if="mr.isOpen">
- <a
- href="#modal_merge_info"
- data-toggle="modal"
- :disabled="mr.sourceBranchRemoved"
- class="btn btn-sm inline">
- Check out branch
- </a>
- <span class="dropdown prepend-left-10">
- <a
- class="btn btn-sm inline dropdown-toggle"
- data-toggle="dropdown"
- aria-label="Download as"
- role="button">
- <icon
- name="download">
- </icon>
- <i
- class="fa fa-caret-down"
- aria-hidden="true">
- </i>
- </a>
- <ul class="dropdown-menu dropdown-menu-align-right">
- <li>
- <a
- :href="mr.emailPatchesPath"
- download>
- Email patches
- </a>
- </li>
- <li>
- <a
- :href="mr.plainDiffPath"
- download>
- Plain diff
- </a>
- </li>
- </ul>
- </span>
- </div>
- </div>
- `,
-};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
new file mode 100644
index 00000000000..18a3787857d
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
@@ -0,0 +1,145 @@
+<script>
+ import tooltip from '~/vue_shared/directives/tooltip';
+ import { n__ } from '~/locale';
+ import icon from '~/vue_shared/components/icon.vue';
+ import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
+
+ export default {
+ name: 'MRWidgetHeader',
+ directives: {
+ tooltip,
+ },
+ components: {
+ icon,
+ clipboardButton,
+ },
+ props: {
+ mr: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ shouldShowCommitsBehindText() {
+ return this.mr.divergedCommitsCount > 0;
+ },
+ commitsText() {
+ return n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount);
+ },
+ branchNameClipboardData() {
+ // This supports code in app/assets/javascripts/copy_to_clipboard.js that
+ // works around ClipboardJS limitations to allow the context-specific
+ // copy/pasting of plain text or GFM.
+ return JSON.stringify({
+ text: this.mr.sourceBranch,
+ gfm: `\`${this.mr.sourceBranch}\``,
+ });
+ },
+ isSourceBranchLong() {
+ return this.isBranchTitleLong(this.mr.sourceBranch);
+ },
+ isTargetBranchLong() {
+ return this.isBranchTitleLong(this.mr.targetBranch);
+ },
+ },
+ methods: {
+ isBranchTitleLong(branchTitle) {
+ return branchTitle.length > 32;
+ },
+ },
+ };
+</script>
+<template>
+ <div class="mr-source-target">
+ <div class="normal">
+ <strong>
+ {{ s__("mrWidget|Request to merge") }}
+ <span
+ class="label-branch js-source-branch"
+ :class="{ 'label-truncated': isSourceBranchLong }"
+ :title="isSourceBranchLong ? mr.sourceBranch : ''"
+ data-placement="bottom"
+ :v-tooltip="isSourceBranchLong"
+ v-html="mr.sourceBranchLink"
+ >
+ </span>
+
+ <clipboard-button
+ :text="branchNameClipboardData"
+ :title="__('Copy branch name to clipboard')"
+ />
+
+ {{ s__("mrWidget|into") }}
+
+ <span
+ class="label-branch"
+ :v-tooltip="isTargetBranchLong"
+ :class="{ 'label-truncatedtooltip': isTargetBranchLong }"
+ :title="isTargetBranchLong ? mr.targetBranch : ''"
+ data-placement="bottom"
+ >
+ <a
+ :href="mr.targetBranchTreePath"
+ class="js-target-branch"
+ >
+ {{ mr.targetBranch }}
+ </a>
+ </span>
+ </strong>
+ <span
+ v-if="shouldShowCommitsBehindText"
+ class="diverged-commits-count"
+ >
+ (<a :href="mr.targetBranchPath">{{ commitsText }}</a>)
+ </span>
+ </div>
+
+ <div v-if="mr.isOpen">
+ <button
+ data-target="#modal_merge_info"
+ data-toggle="modal"
+ :disabled="mr.sourceBranchRemoved"
+ class="btn btn-sm btn-default inline js-check-out-branch"
+ type="button"
+ >
+ {{ s__("mrWidget|Check out branch") }}
+ </button>
+ <span class="dropdown prepend-left-10">
+ <button
+ type="button"
+ class="btn btn-sm inline dropdown-toggle"
+ data-toggle="dropdown"
+ aria-label="Download as"
+ aria-haspopup="true"
+ aria-expanded="false"
+ >
+ <icon name="download" />
+ <i
+ class="fa fa-caret-down"
+ aria-hidden="true">
+ </i>
+ </button>
+ <ul class="dropdown-menu dropdown-menu-align-right">
+ <li>
+ <a
+ class="js-download-email-patches"
+ :href="mr.emailPatchesPath"
+ download
+ >
+ {{ s__("mrWidget|Email patches") }}
+ </a>
+ </li>
+ <li>
+ <a
+ class="js-download-plain-diff"
+ :href="mr.plainDiffPath"
+ download
+ >
+ {{ s__("mrWidget|Plain diff") }}
+ </a>
+ </li>
+ </ul>
+ </span>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.js
deleted file mode 100644
index 1d9f9863dd9..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.js
+++ /dev/null
@@ -1,23 +0,0 @@
-export default {
- name: 'MRWidgetMergeHelp',
- props: {
- missingBranch: { type: String, required: false, default: '' },
- },
- template: `
- <section class="mr-widget-help">
- <template
- v-if="missingBranch">
- If the {{missingBranch}} branch exists in your local repository, you
- </template>
- <template v-else>
- You
- </template>
- can merge this merge request manually using the
- <a
- data-toggle="modal"
- href="#modal_merge_info">
- command line
- </a>
- </section>
- `,
-};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.vue
new file mode 100644
index 00000000000..62b61e1f41f
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.vue
@@ -0,0 +1,41 @@
+<script>
+ import { sprintf, s__ } from '~/locale';
+
+ export default {
+ name: 'MRWidgetMergeHelp',
+ props: {
+ missingBranch: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ missingBranchInfo() {
+ return sprintf(
+ s__('mrWidget|If the %{branch} branch exists in your local repository, you can merge this merge request manually using the'),
+ { branch: this.missingBranch },
+ );
+ },
+ },
+ };
+</script>
+<template>
+ <section class="mr-widget-help">
+ <template v-if="missingBranch">
+ {{ missingBranchInfo }}
+ </template>
+ <template v-else>
+ {{ s__("mrWidget|You can merge this merge request manually using the") }}
+ </template>
+
+ <button
+ type="button"
+ class="btn-link btn-blank js-open-modal-help"
+ data-toggle="modal"
+ data-target="#modal_merge_info"
+ >
+ {{ s__("mrWidget|command line") }}
+ </button>
+ </section>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue
index 71bfdaf801e..68b691fc914 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue
@@ -1,5 +1,5 @@
<script>
- import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
+ import mrWidgetAuthorTime from '../../components/mr_widget_author_time.vue';
import statusIcon from '../mr_widget_status_icon.vue';
export default {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue
index 72887528bd8..de98a77be6f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue
@@ -1,7 +1,7 @@
<script>
import Flash from '../../../flash';
import statusIcon from '../mr_widget_status_icon.vue';
- import mrWidgetAuthor from '../../components/mr_widget_author';
+ import mrWidgetAuthor from '../../components/mr_widget_author.vue';
import eventHub from '../../event_hub';
export default {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
index a92e0b3c124..c1618bc6ea0 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
@@ -3,7 +3,7 @@
import tooltip from '~/vue_shared/directives/tooltip';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import { s__, __ } from '~/locale';
- import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
+ import mrWidgetAuthorTime from '../../components/mr_widget_author_time.vue';
import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js
index 303877d6fbf..7733fb74afe 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js
@@ -1,6 +1,6 @@
import statusIcon from '../mr_widget_status_icon.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
-import mrWidgetMergeHelp from '../../components/mr_widget_merge_help';
+import mrWidgetMergeHelp from '../../components/mr_widget_merge_help.vue';
export default {
name: 'MRWidgetMissingBranch',
diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
index 515eb32f5d6..7ca15537719 100644
--- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js
+++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
@@ -11,8 +11,8 @@
export { default as Vue } from 'vue';
export { default as SmartInterval } from '~/smart_interval';
-export { default as WidgetHeader } from './components/mr_widget_header';
-export { default as WidgetMergeHelp } from './components/mr_widget_merge_help';
+export { default as WidgetHeader } from './components/mr_widget_header.vue';
+export { default as WidgetMergeHelp } from './components/mr_widget_merge_help.vue';
export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue';
export { default as WidgetDeployment } from './components/mr_widget_deployment';
export { default as WidgetRelatedLinks } from './components/mr_widget_related_links.vue';
diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue
index 6d1fe7ee8ca..97789636787 100644
--- a/app/assets/javascripts/vue_shared/components/commit.vue
+++ b/app/assets/javascripts/vue_shared/components/commit.vue
@@ -118,7 +118,7 @@
<template>
<div class="branch-commit">
<template v-if="hasCommitRef && showBranch">
- <div class="icon-container hidden-xs">
+ <div class="icon-container">
<i
v-if="tag"
class="fa fa-tag"
@@ -132,7 +132,7 @@
</div>
<a
- class="ref-name hidden-xs"
+ class="ref-name"
:href="commitRef.ref_url"
v-tooltip
data-container="body"
diff --git a/app/assets/javascripts/vue_shared/components/confirmation_input.vue b/app/assets/javascripts/vue_shared/components/confirmation_input.vue
new file mode 100644
index 00000000000..1aa03ea6317
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/confirmation_input.vue
@@ -0,0 +1,62 @@
+<script>
+ import _ from 'underscore';
+ import { __, sprintf } from '~/locale';
+
+ export default {
+ props: {
+ inputId: {
+ type: String,
+ required: true,
+ },
+ confirmationKey: {
+ type: String,
+ required: true,
+ },
+ confirmationValue: {
+ type: String,
+ required: true,
+ },
+ shouldEscapeConfirmationValue: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+ computed: {
+ inputLabel() {
+ let value = this.confirmationValue;
+ if (this.shouldEscapeConfirmationValue) {
+ value = _.escape(value);
+ }
+
+ return sprintf(
+ __('Type %{value} to confirm:'),
+ { value: `<code>${value}</code>` },
+ false,
+ );
+ },
+ },
+ methods: {
+ hasCorrectValue() {
+ return this.$refs.enteredValue.value === this.confirmationValue;
+ },
+ },
+ };
+</script>
+
+<template>
+ <div>
+ <label
+ v-html="inputLabel"
+ :for="inputId"
+ >
+ </label>
+ <input
+ :id="inputId"
+ :name="confirmationKey"
+ type="text"
+ ref="enteredValue"
+ class="form-control"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
index cb8e6072a9b..63d8329e495 100644
--- a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
+++ b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
@@ -48,7 +48,7 @@
};
</script>
<template>
- <ul class="nav-links scrolling-tabs">
+ <ul class="nav-links scrolling-tabs separator">
<li
v-for="(tab, i) in tabs"
:key="i"
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index cff47ea76ec..887879ab715 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -60,3 +60,5 @@
@import "framework/memory_graph";
@import "framework/responsive_tables";
@import "framework/stacked-progress-bar";
+@import "framework/ci_variable_list";
+@import "framework/feature_highlight";
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index d0b0c69b18f..c4b046a6d68 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -176,6 +176,11 @@
&.btn-remove {
@include btn-outline($white-light, $red-500, $red-500, $red-500, $white-light, $red-600, $red-600, $red-700);
}
+
+ &.btn-primary,
+ &.btn-info {
+ @include btn-outline($white-light, $blue-500, $blue-500, $blue-500, $white-light, $blue-600, $blue-600, $blue-700);
+ }
}
&.btn-gray {
diff --git a/app/assets/stylesheets/framework/ci_variable_list.scss b/app/assets/stylesheets/framework/ci_variable_list.scss
new file mode 100644
index 00000000000..8f654ab363c
--- /dev/null
+++ b/app/assets/stylesheets/framework/ci_variable_list.scss
@@ -0,0 +1,88 @@
+.ci-variable-list {
+ margin-left: 0;
+ margin-bottom: 0;
+ padding-left: 0;
+ list-style: none;
+ clear: both;
+}
+
+.ci-variable-row {
+ display: flex;
+ align-items: flex-end;
+
+ &:not(:last-child) {
+ margin-bottom: $gl-btn-padding;
+
+ @media (max-width: $screen-xs-max) {
+ margin-bottom: 3 * $gl-btn-padding;
+ }
+ }
+
+ &:last-child {
+ .ci-variable-body-item:last-child {
+ margin-right: $ci-variable-remove-button-width;
+
+ @media (max-width: $screen-xs-max) {
+ margin-right: 0;
+ }
+ }
+
+ .ci-variable-row-remove-button {
+ display: none;
+ }
+
+ @media (max-width: $screen-xs-max) {
+ .ci-variable-row-body {
+ margin-right: $ci-variable-remove-button-width;
+ }
+ }
+ }
+}
+
+.ci-variable-row-body {
+ display: flex;
+ width: 100%;
+
+ @media (max-width: $screen-xs-max) {
+ display: block;
+ }
+}
+
+.ci-variable-body-item {
+ flex: 1;
+
+ &:not(:last-child) {
+ margin-right: $gl-btn-padding;
+
+ @media (max-width: $screen-xs-max) {
+ margin-right: 0;
+ margin-bottom: $gl-btn-padding;
+ }
+ }
+}
+
+.ci-variable-protected-item {
+ flex: 0 1 auto;
+ display: flex;
+ align-items: center;
+}
+
+.ci-variable-row-remove-button {
+ @include transition(color);
+ flex-shrink: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: $ci-variable-remove-button-width;
+ height: $input-height;
+ padding: 0;
+ background: transparent;
+ border: 0;
+ color: $gl-text-color-secondary;
+
+ &:hover,
+ &:focus {
+ outline: none;
+ color: $gl-text-color;
+ }
+}
diff --git a/app/assets/stylesheets/framework/feature_highlight.scss b/app/assets/stylesheets/framework/feature_highlight.scss
new file mode 100644
index 00000000000..4f26cd015e4
--- /dev/null
+++ b/app/assets/stylesheets/framework/feature_highlight.scss
@@ -0,0 +1,103 @@
+.feature-highlight {
+ position: relative;
+ margin-left: $gl-padding;
+ width: 20px;
+ height: 20px;
+ cursor: pointer;
+
+ &::before {
+ content: '';
+ display: block;
+ position: absolute;
+ top: 6px;
+ left: 6px;
+ width: 8px;
+ height: 8px;
+ background-color: $blue-500;
+ border-radius: 50%;
+ box-shadow: 0 0 0 rgba($blue-500, 0.4);
+ animation: pulse-highlight 2s infinite;
+ }
+
+ &:hover::before,
+ &.disable-animation::before {
+ animation: none;
+ }
+
+ &[disabled]::before {
+ display: none;
+ }
+}
+
+.is-showing-fly-out {
+ .feature-highlight {
+ display: none;
+ }
+}
+
+.feature-highlight-popover-content {
+ display: none;
+
+ hr {
+ margin: $gl-padding * 0.5 0;
+ }
+
+ .btn-link {
+ svg {
+ @include btn-svg;
+
+ path {
+ fill: currentColor;
+ }
+ }
+ }
+
+ .feature-highlight-illustration {
+ width: 100%;
+ height: 100px;
+ padding-top: 12px;
+ padding-bottom: 12px;
+
+ background-color: $indigo-50;
+ border-top-left-radius: 2px;
+ border-top-right-radius: 2px;
+ border-bottom: 1px solid darken($gray-normal, 8%);
+ }
+}
+
+.popover .feature-highlight-popover-content {
+ display: block;
+}
+
+.feature-highlight-popover {
+ width: 240px;
+ padding: 0;
+ border: 1px solid $dropdown-border-color;
+ box-shadow: 0 2px 4px $dropdown-shadow-color;
+
+ &.right > .arrow {
+ border-right-color: $dropdown-border-color;
+ }
+
+ .popover-content {
+ padding: 0;
+ }
+}
+
+.feature-highlight-popover-sub-content {
+ padding: 9px 14px;
+}
+
+@include keyframes(pulse-highlight) {
+ 0% {
+ box-shadow: 0 0 0 0 rgba($blue-200, 0.4);
+ }
+
+ 70% {
+ box-shadow: 0 0 0 10px transparent;
+ }
+
+ 100% {
+ box-shadow: 0 0 0 0 transparent;
+ }
+}
diff --git a/app/assets/stylesheets/framework/gfm.scss b/app/assets/stylesheets/framework/gfm.scss
index 5621505996d..e378e84ca1b 100644
--- a/app/assets/stylesheets/framework/gfm.scss
+++ b/app/assets/stylesheets/framework/gfm.scss
@@ -16,3 +16,31 @@
background-color: $user-mention-bg-hover;
}
}
+
+.gfm-color_chip {
+ display: inline-block;
+ margin: 0 0 2px 4px;
+ vertical-align: middle;
+ border-radius: 3px;
+
+ $chip-size: 0.9em;
+ $bg-size: $chip-size / 0.9;
+ $bg-pos: $bg-size / 2;
+
+ width: $chip-size;
+ height: $chip-size;
+ background: $white-light;
+ background-image: linear-gradient(135deg, $gray-dark 25%, transparent 0%, transparent 75%, $gray-dark 0%),
+ linear-gradient(135deg, $gray-dark 25%, transparent 0%, transparent 75%, $gray-dark 0%);
+ background-size: $bg-size $bg-size;
+ background-position: 0 0, $bg-pos $bg-pos;
+
+ > span {
+ display: inline-block;
+ width: 100%;
+ height: 100%;
+ margin-bottom: 2px;
+ border-radius: 3px;
+ border: 1px solid $black-transparent;
+ }
+}
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index 32b9894ae04..a6b1bf9b099 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -4,6 +4,11 @@
.page-title {
margin-top: 0;
+
+ .color-label {
+ font-size: $gl-font-size;
+ padding: $gl-vert-padding $label-padding-modal;
+ }
}
}
diff --git a/app/assets/stylesheets/framework/secondary-navigation-elements.scss b/app/assets/stylesheets/framework/secondary-navigation-elements.scss
index 5f67126bafa..17c31d6b184 100644
--- a/app/assets/stylesheets/framework/secondary-navigation-elements.scss
+++ b/app/assets/stylesheets/framework/secondary-navigation-elements.scss
@@ -82,6 +82,10 @@
/* Small devices (phones, tablets, 768px and lower) */
@media (max-width: $screen-xs-max) {
width: 100%;
+
+ &.mobile-separator {
+ border-bottom: 1px solid $border-color;
+ }
}
}
@@ -168,9 +172,9 @@
display: inline-block;
}
- // Applies on /dashboard/issues
.project-item-select-holder {
margin: 0;
+ width: 100%;
}
}
}
@@ -340,7 +344,6 @@
.project-item-select-holder.btn-group {
display: flex;
- max-width: 350px;
overflow: hidden;
float: right;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index f76c6866463..0d21a9f5f77 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -558,6 +558,7 @@ $jq-ui-default-color: #777;
* Label
*/
$label-padding: 7px;
+$label-padding-modal: 10px;
$label-gray-bg: #f8fafc;
$label-inverse-bg: #333;
$label-remove-border: rgba(0, 0, 0, .1);
@@ -668,9 +669,9 @@ $pipeline-dropdown-line-height: 20px;
$pipeline-dropdown-status-icon-size: 18px;
/*
-Pipeline Schedules
+CI variable lists
*/
-$pipeline-variable-remove-button-width: calc(1em + #{2 * $gl-padding});
+$ci-variable-remove-button-width: calc(1em + #{2 * $gl-padding});
/*
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index aeaa33bd3bd..17801ed5910 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -195,6 +195,18 @@
.commit-actions {
@media (min-width: $screen-sm-min) {
font-size: 0;
+
+ div {
+ display: inline;
+ }
+
+ .fa-spinner {
+ font-size: 12px;
+ }
+
+ span {
+ font-size: 6px;
+ }
}
.ci-status-link {
@@ -219,6 +231,11 @@
font-size: 14px;
font-weight: $gl-font-weight-bold;
}
+
+ .ci-status-icon {
+ position: relative;
+ top: 1px;
+ }
}
.commit,
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index f9a761e85fe..6ee8b33bd39 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -224,3 +224,16 @@
border-radius: $label-border-radius;
font-weight: $gl-font-weight-normal;
}
+
+.js-groups-dropdown {
+ width: 100%;
+}
+
+.dropdown-group-transfer {
+ bottom: 100%;
+ top: initial;
+
+ .dropdown-content {
+ overflow-y: unset;
+ }
+}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index e8cd8a4905c..a72e654824e 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -58,13 +58,13 @@
@media (min-width: $screen-sm-min) {
width: 200px;
+ margin-left: $gl-padding * 2;
margin-bottom: 0;
}
.label {
overflow: hidden;
text-overflow: ellipsis;
- vertical-align: middle;
max-width: 100%;
}
}
@@ -79,26 +79,33 @@
width: 100px;
margin-left: 10px;
margin-bottom: 0;
- vertical-align: middle;
+ vertical-align: top;
}
}
.label-description {
display: block;
margin-bottom: 10px;
- margin-left: 50px;
+
+ .description-text {
+ margin-bottom: $gl-padding;
+ }
+
+ a {
+ color: $blue-600;
+ }
@media (min-width: $screen-sm-min) {
display: inline-block;
- width: 30%;
+ max-width: 50%;
margin-left: 10px;
margin-bottom: 0;
- vertical-align: middle;
+ vertical-align: top;
}
}
.label {
- padding: 8px 9px 9px;
+ padding: 8px 12px;
font-size: 14px;
}
}
@@ -116,6 +123,12 @@
}
.manage-labels-list {
+ @media(min-width: $screen-md-min) {
+ &.content-list li {
+ padding: $gl-padding 0;
+ }
+ }
+
> li:not(.empty-message):not(.is-not-draggable) {
background-color: $white-light;
cursor: move;
@@ -133,8 +146,6 @@
}
.btn-action {
- color: $gl-text-color;
-
.fa {
font-size: 18px;
vertical-align: middle;
@@ -155,10 +166,18 @@
float: right;
}
}
+
+ @media (max-width: $screen-xs-max) {
+ .dropdown-menu {
+ min-width: 100%;
+ }
+ }
}
.draggable-handler {
display: inline-block;
+ vertical-align: top;
+ margin: 5px 0;
opacity: 0;
transition: opacity .3s;
color: $gray-darkest;
@@ -188,7 +207,7 @@
.toggle-priority {
display: inline-block;
- vertical-align: middle;
+ vertical-align: top;
button {
border-color: transparent;
@@ -255,6 +274,11 @@
}
.label-subscribe-button {
+ @media(min-width: $screen-md-min) {
+ min-width: 105px;
+ margin-left: $gl-padding;
+ }
+
.label-subscribe-button-icon {
&[disabled] {
opacity: 0.5;
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index bf8eb4c1f06..4a528bc2bb1 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -410,7 +410,6 @@
width: 298px;
}
-
@media (max-width: $screen-xs-max) {
display: flex;
width: 100%;
diff --git a/app/assets/stylesheets/pages/pipeline_schedules.scss b/app/assets/stylesheets/pages/pipeline_schedules.scss
index b698a4f9afa..bc7fa8a26d9 100644
--- a/app/assets/stylesheets/pages/pipeline_schedules.scss
+++ b/app/assets/stylesheets/pages/pipeline_schedules.scss
@@ -78,84 +78,3 @@
margin-right: 3px;
}
}
-
-.pipeline-variable-list {
- margin-left: 0;
- margin-bottom: 0;
- padding-left: 0;
- list-style: none;
- clear: both;
-}
-
-.pipeline-variable-row {
- display: flex;
- align-items: flex-end;
-
- &:not(:last-child) {
- margin-bottom: $gl-btn-padding;
- }
-
- @media (max-width: $screen-sm-max) {
- padding-right: $gl-col-padding;
- }
-
- &:last-child {
- .pipeline-variable-row-remove-button {
- display: none;
- }
-
- @media (max-width: $screen-sm-max) {
- .pipeline-variable-value-input {
- margin-right: $pipeline-variable-remove-button-width;
- }
- }
-
- @media (max-width: $screen-xs-max) {
- .pipeline-variable-row-body {
- margin-right: $pipeline-variable-remove-button-width;
- }
- }
- }
-}
-
-.pipeline-variable-row-body {
- display: flex;
- width: calc(75% - #{$gl-col-padding});
- padding-left: $gl-col-padding;
-
- @media (max-width: $screen-sm-max) {
- width: 100%;
- }
-
- @media (max-width: $screen-xs-max) {
- display: block;
- }
-}
-
-.pipeline-variable-key-input {
- margin-right: $gl-btn-padding;
-
- @media (max-width: $screen-xs-max) {
- margin-bottom: $gl-btn-padding;
- }
-}
-
-.pipeline-variable-row-remove-button {
- @include transition(color);
- flex-shrink: 0;
- display: flex;
- justify-content: center;
- align-items: center;
- width: $pipeline-variable-remove-button-width;
- height: $input-height;
- padding: 0;
- background: transparent;
- border: 0;
- color: $gl-text-color-secondary;
-
- &:hover,
- &:focus {
- outline: none;
- color: $gl-text-color;
- }
-}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index db88d4a16b7..f10908c3630 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -121,7 +121,7 @@
.ref-name {
font-weight: $gl-font-weight-bold;
- max-width: 120px;
+ max-width: 100px;
overflow: hidden;
display: inline-block;
white-space: nowrap;
diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss
index d8fec583121..e70a57c2a67 100644
--- a/app/assets/stylesheets/pages/wiki.scss
+++ b/app/assets/stylesheets/pages/wiki.scss
@@ -6,6 +6,14 @@
}
}
+.wiki-form {
+ .edit-wiki-page-slug-tip {
+ display: inline-block;
+ max-width: 100%;
+ margin-top: 5px;
+ }
+}
+
.title .edit-wiki-header {
width: 780px;
margin-left: auto;
diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb
index c49b6459452..a9109a1d4d0 100644
--- a/app/controllers/admin/broadcast_messages_controller.rb
+++ b/app/controllers/admin/broadcast_messages_controller.rb
@@ -1,4 +1,6 @@
class Admin::BroadcastMessagesController < Admin::ApplicationController
+ include BroadcastMessagesHelper
+
before_action :finder, only: [:edit, :update, :destroy]
def index
@@ -37,7 +39,8 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController
end
def preview
- @broadcast_message = BroadcastMessage.new(broadcast_message_params)
+ broadcast_message = BroadcastMessage.new(broadcast_message_params)
+ render json: { message: render_broadcast_message(broadcast_message) }
end
protected
diff --git a/app/controllers/admin/cohorts_controller.rb b/app/controllers/admin/cohorts_controller.rb
index 9b77c554908..10d9d1b5345 100644
--- a/app/controllers/admin/cohorts_controller.rb
+++ b/app/controllers/admin/cohorts_controller.rb
@@ -1,6 +1,6 @@
class Admin::CohortsController < Admin::ApplicationController
def index
- if current_application_settings.usage_ping_enabled
+ if Gitlab::CurrentSettings.usage_ping_enabled
cohorts_results = Rails.cache.fetch('cohorts', expires_in: 1.day) do
CohortsService.new.execute
end
diff --git a/app/controllers/admin/services_controller.rb b/app/controllers/admin/services_controller.rb
index 4c3d336b3af..a7025b62ad7 100644
--- a/app/controllers/admin/services_controller.rb
+++ b/app/controllers/admin/services_controller.rb
@@ -1,6 +1,7 @@
class Admin::ServicesController < Admin::ApplicationController
include ServiceParams
+ before_action :whitelist_query_limiting, only: [:index]
before_action :service, only: [:edit, :update]
def index
@@ -37,4 +38,8 @@ class Admin::ServicesController < Admin::ApplicationController
def service
@service ||= Service.where(id: params[:id], template: true).first
end
+
+ def whitelist_query_limiting
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42430')
+ end
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 95ad38d9230..b04bfaf3e49 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -2,7 +2,6 @@ require 'gon'
require 'fogbugz'
class ApplicationController < ActionController::Base
- include Gitlab::CurrentSettings
include Gitlab::GonHelper
include GitlabRoutingHelper
include PageLayoutHelper
@@ -28,7 +27,7 @@ class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
- helper_method :can?, :current_application_settings
+ helper_method :can?
helper_method :import_sources_enabled?, :github_import_enabled?, :gitea_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled?
rescue_from Encoding::CompatibilityError do |exception|
@@ -120,7 +119,7 @@ class ApplicationController < ActionController::Base
end
def after_sign_out_path_for(resource)
- current_application_settings.after_sign_out_path.presence || new_user_session_path
+ Gitlab::CurrentSettings.after_sign_out_path.presence || new_user_session_path
end
def can?(object, action, subject = :global)
@@ -268,15 +267,15 @@ class ApplicationController < ActionController::Base
end
def import_sources_enabled?
- !current_application_settings.import_sources.empty?
+ !Gitlab::CurrentSettings.import_sources.empty?
end
def github_import_enabled?
- current_application_settings.import_sources.include?('github')
+ Gitlab::CurrentSettings.import_sources.include?('github')
end
def gitea_import_enabled?
- current_application_settings.import_sources.include?('gitea')
+ Gitlab::CurrentSettings.import_sources.include?('gitea')
end
def github_import_configured?
@@ -284,7 +283,7 @@ class ApplicationController < ActionController::Base
end
def gitlab_import_enabled?
- request.host != 'gitlab.com' && current_application_settings.import_sources.include?('gitlab')
+ request.host != 'gitlab.com' && Gitlab::CurrentSettings.import_sources.include?('gitlab')
end
def gitlab_import_configured?
@@ -292,7 +291,7 @@ class ApplicationController < ActionController::Base
end
def bitbucket_import_enabled?
- current_application_settings.import_sources.include?('bitbucket')
+ Gitlab::CurrentSettings.import_sources.include?('bitbucket')
end
def bitbucket_import_configured?
@@ -300,19 +299,19 @@ class ApplicationController < ActionController::Base
end
def google_code_import_enabled?
- current_application_settings.import_sources.include?('google_code')
+ Gitlab::CurrentSettings.import_sources.include?('google_code')
end
def fogbugz_import_enabled?
- current_application_settings.import_sources.include?('fogbugz')
+ Gitlab::CurrentSettings.import_sources.include?('fogbugz')
end
def git_import_enabled?
- current_application_settings.import_sources.include?('git')
+ Gitlab::CurrentSettings.import_sources.include?('git')
end
def gitlab_project_import_enabled?
- current_application_settings.import_sources.include?('gitlab_project')
+ Gitlab::CurrentSettings.import_sources.include?('gitlab_project')
end
# U2F (universal 2nd factor) devices need a unique identifier for the application
diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb
index f8049b20b9f..ee23ee0bcc3 100644
--- a/app/controllers/boards/issues_controller.rb
+++ b/app/controllers/boards/issues_controller.rb
@@ -2,6 +2,7 @@ module Boards
class IssuesController < Boards::ApplicationController
include BoardsResponses
+ before_action :whitelist_query_limiting, only: [:index, :update]
before_action :authorize_read_issue, only: [:index]
before_action :authorize_create_issue, only: [:create]
before_action :authorize_update_issue, only: [:update]
@@ -92,5 +93,10 @@ module Boards
}
)
end
+
+ def whitelist_query_limiting
+ # Also see https://gitlab.com/gitlab-org/gitlab-ce/issues/42439
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42428')
+ end
end
end
diff --git a/app/controllers/concerns/enforces_two_factor_authentication.rb b/app/controllers/concerns/enforces_two_factor_authentication.rb
index 688e8bd4a37..997af4ab9e9 100644
--- a/app/controllers/concerns/enforces_two_factor_authentication.rb
+++ b/app/controllers/concerns/enforces_two_factor_authentication.rb
@@ -20,13 +20,13 @@ module EnforcesTwoFactorAuthentication
end
def two_factor_authentication_required?
- current_application_settings.require_two_factor_authentication? ||
+ Gitlab::CurrentSettings.require_two_factor_authentication? ||
current_user.try(:require_two_factor_authentication_from_group?)
end
def two_factor_authentication_reason(global: -> {}, group: -> {})
if two_factor_authentication_required?
- if current_application_settings.require_two_factor_authentication?
+ if Gitlab::CurrentSettings.require_two_factor_authentication?
global.call
else
groups = current_user.expanded_groups_requiring_two_factor_authentication.reorder(name: :asc)
@@ -36,7 +36,7 @@ module EnforcesTwoFactorAuthentication
end
def two_factor_grace_period
- periods = [current_application_settings.two_factor_grace_period]
+ periods = [Gitlab::CurrentSettings.two_factor_grace_period]
periods << current_user.two_factor_grace_period if current_user.try(:require_two_factor_authentication_from_group?)
periods.min
end
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 2fa0f98e344..0d7ee06deb6 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -94,6 +94,7 @@ module IssuableCollections
@filter_params[:project_id] = @project.id
elsif @group
@filter_params[:group_id] = @group.id
+ @filter_params[:include_subgroups] = true
else
# TODO: this filter ignore issues/mr created in public or
# internal repos where you are not a member. Enable this filter
diff --git a/app/controllers/concerns/requires_whitelisted_monitoring_client.rb b/app/controllers/concerns/requires_whitelisted_monitoring_client.rb
index 0218ac83441..88d1b34bb06 100644
--- a/app/controllers/concerns/requires_whitelisted_monitoring_client.rb
+++ b/app/controllers/concerns/requires_whitelisted_monitoring_client.rb
@@ -1,8 +1,6 @@
module RequiresWhitelistedMonitoringClient
extend ActiveSupport::Concern
- include Gitlab::CurrentSettings
-
included do
before_action :validate_ip_whitelisted_or_valid_token!
end
@@ -26,7 +24,7 @@ module RequiresWhitelistedMonitoringClient
token.present? &&
ActiveSupport::SecurityUtils.variable_size_secure_compare(
token,
- current_application_settings.health_check_access_token
+ Gitlab::CurrentSettings.health_check_access_token
)
end
diff --git a/app/controllers/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb
index a6fb1f40001..7ad79a1e56c 100644
--- a/app/controllers/concerns/uploads_actions.rb
+++ b/app/controllers/concerns/uploads_actions.rb
@@ -1,6 +1,8 @@
module UploadsActions
include Gitlab::Utils::StrongMemoize
+ UPLOAD_MOUNTS = %w(avatar attachment file logo header_logo).freeze
+
def create
link_to_file = UploadService.new(model, params[:file], uploader_class).execute
@@ -17,34 +19,71 @@ module UploadsActions
end
end
+ # This should either
+ # - send the file directly
+ # - or redirect to its URL
+ #
def show
return render_404 unless uploader.exists?
- disposition = uploader.image_or_video? ? 'inline' : 'attachment'
-
- expires_in 0.seconds, must_revalidate: true, private: true
+ if uploader.file_storage?
+ disposition = uploader.image_or_video? ? 'inline' : 'attachment'
+ expires_in 0.seconds, must_revalidate: true, private: true
- send_file uploader.file.path, disposition: disposition
+ send_file uploader.file.path, disposition: disposition
+ else
+ redirect_to uploader.url
+ end
end
private
+ def uploader_class
+ raise NotImplementedError
+ end
+
+ def upload_mount
+ mounted_as = params[:mounted_as]
+ mounted_as if UPLOAD_MOUNTS.include?(mounted_as)
+ end
+
+ def uploader_mounted?
+ upload_model_class < CarrierWave::Mount::Extension && !upload_mount.nil?
+ end
+
def uploader
strong_memoize(:uploader) do
- return if show_model.nil?
+ if uploader_mounted?
+ model.public_send(upload_mount) # rubocop:disable GitlabSecurity/PublicSend
+ else
+ build_uploader_from_upload || build_uploader_from_params
+ end
+ end
+ end
- file_uploader = FileUploader.new(show_model, params[:secret])
- file_uploader.retrieve_from_store!(params[:filename])
+ def build_uploader_from_upload
+ return nil unless params[:secret] && params[:filename]
- file_uploader
- end
+ upload_path = uploader_class.upload_path(params[:secret], params[:filename])
+ upload = Upload.find_by(uploader: uploader_class.to_s, path: upload_path)
+ upload&.build_uploader
+ end
+
+ def build_uploader_from_params
+ uploader = uploader_class.new(model, secret: params[:secret])
+ uploader.retrieve_from_store!(params[:filename])
+ uploader
end
def image_or_video?
uploader && uploader.exists? && uploader.image_or_video?
end
- def uploader_class
- FileUploader
+ def find_model
+ nil
+ end
+
+ def model
+ strong_memoize(:model) { find_model }
end
end
diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb
index dda59262483..f3a9e591c3e 100644
--- a/app/controllers/groups/labels_controller.rb
+++ b/app/controllers/groups/labels_controller.rb
@@ -54,7 +54,7 @@ class Groups::LabelsController < Groups::ApplicationController
respond_to do |format|
format.html do
- redirect_to group_labels_path(@group), status: 302, notice: 'Label was removed'
+ redirect_to group_labels_path(@group), status: 302, notice: "#{@label.name} deleted permanently"
end
format.js
end
diff --git a/app/controllers/groups/uploads_controller.rb b/app/controllers/groups/uploads_controller.rb
index e6bd9806401..f1578f75e88 100644
--- a/app/controllers/groups/uploads_controller.rb
+++ b/app/controllers/groups/uploads_controller.rb
@@ -7,29 +7,23 @@ class Groups::UploadsController < Groups::ApplicationController
private
- def show_model
- strong_memoize(:show_model) do
- group_id = params[:group_id]
-
- Group.find_by_full_path(group_id)
- end
+ def upload_model_class
+ Group
end
- def authorize_upload_file!
- render_404 unless can?(current_user, :upload_file, group)
+ def uploader_class
+ NamespaceFileUploader
end
- def uploader
- strong_memoize(:uploader) do
- file_uploader = uploader_class.new(show_model, params[:secret])
- file_uploader.retrieve_from_store!(params[:filename])
- file_uploader
- end
- end
+ def find_model
+ return @group if @group
- def uploader_class
- NamespaceFileUploader
+ group_id = params[:group_id]
+
+ Group.find_by_full_path(group_id)
end
- alias_method :model, :group
+ def authorize_upload_file!
+ render_404 unless can?(current_user, :upload_file, group)
+ end
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index bb652832cb1..7d129c5dece 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -10,7 +10,7 @@ class GroupsController < Groups::ApplicationController
before_action :group, except: [:index, :new, :create]
# Authorize
- before_action :authorize_admin_group!, only: [:edit, :update, :destroy, :projects]
+ before_action :authorize_admin_group!, only: [:edit, :update, :destroy, :projects, :transfer]
before_action :authorize_create_group!, only: [:new]
before_action :group_projects, only: [:projects, :activity, :issues, :merge_requests]
@@ -94,6 +94,19 @@ class GroupsController < Groups::ApplicationController
redirect_to root_path, status: 302, alert: "Group '#{@group.name}' was scheduled for deletion."
end
+ def transfer
+ parent_group = Group.find_by(id: params[:new_parent_group_id])
+ service = ::Groups::TransferService.new(@group, current_user)
+
+ if service.execute(parent_group)
+ flash[:notice] = "Group '#{@group.name}' was successfully transferred."
+ redirect_to group_path(@group)
+ else
+ flash.now[:alert] = service.error
+ render :edit
+ end
+ end
+
protected
def authorize_create_group!
diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb
index 38f379dbf4f..a394521698c 100644
--- a/app/controllers/help_controller.rb
+++ b/app/controllers/help_controller.rb
@@ -5,7 +5,7 @@ class HelpController < ApplicationController
# Taken from Jekyll
# https://github.com/jekyll/jekyll/blob/3.5-stable/lib/jekyll/document.rb#L13
- YAML_FRONT_MATTER_REGEXP = %r!\A(---\s*\n.*?\n?)^((---|\.\.\.)\s*$\n?)!m
+ YAML_FRONT_MATTER_REGEXP = /\A(---\s*\n.*?\n?)^((---|\.\.\.)\s*$\n?)/m
def index
# Remove YAML frontmatter so that it doesn't look weird
diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb
index 567957ba2cb..f22df992fe9 100644
--- a/app/controllers/import/gitlab_projects_controller.rb
+++ b/app/controllers/import/gitlab_projects_controller.rb
@@ -1,4 +1,5 @@
class Import::GitlabProjectsController < Import::BaseController
+ before_action :whitelist_query_limiting, only: [:create]
before_action :verify_gitlab_project_import_enabled
def new
@@ -40,4 +41,8 @@ class Import::GitlabProjectsController < Import::BaseController
:path, :namespace_id, :file
)
end
+
+ def whitelist_query_limiting
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42437')
+ end
end
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
index 04b29aa2384..52430ea771f 100644
--- a/app/controllers/invites_controller.rb
+++ b/app/controllers/invites_controller.rb
@@ -51,7 +51,7 @@ class InvitesController < ApplicationController
return if current_user
notice = "To accept this invitation, sign in"
- notice << " or create an account" if current_application_settings.allow_signup?
+ notice << " or create an account" if Gitlab::CurrentSettings.allow_signup?
notice << "."
store_location_for :user, request.fullpath
diff --git a/app/controllers/koding_controller.rb b/app/controllers/koding_controller.rb
index 6b1e64ce819..745abf3c0f5 100644
--- a/app/controllers/koding_controller.rb
+++ b/app/controllers/koding_controller.rb
@@ -10,6 +10,6 @@ class KodingController < ApplicationController
private
def check_integration!
- render_404 unless current_application_settings.koding_enabled?
+ render_404 unless Gitlab::CurrentSettings.koding_enabled?
end
end
diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb
index 2443f529c7b..6a21a3f77ad 100644
--- a/app/controllers/oauth/applications_controller.rb
+++ b/app/controllers/oauth/applications_controller.rb
@@ -1,5 +1,4 @@
class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
- include Gitlab::CurrentSettings
include Gitlab::GonHelper
include PageLayoutHelper
include OauthApplications
@@ -31,7 +30,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
private
def verify_user_oauth_applications_enabled
- return if current_application_settings.user_oauth_applications?
+ return if Gitlab::CurrentSettings.user_oauth_applications?
redirect_to profile_path
end
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index d631d09f1b8..83c9a3f035e 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -145,7 +145,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
label = Gitlab::OAuth::Provider.label_for(oauth['provider'])
message = "Signing in using your #{label} account without a pre-existing GitLab account is not allowed."
- if current_application_settings.allow_signup?
+ if Gitlab::CurrentSettings.allow_signup?
message << " Create a GitLab account first, and then connect it to your #{label} account."
end
diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb
index 57761bfbe26..331583c49e6 100644
--- a/app/controllers/passwords_controller.rb
+++ b/app/controllers/passwords_controller.rb
@@ -1,6 +1,4 @@
class PasswordsController < Devise::PasswordsController
- include Gitlab::CurrentSettings
-
skip_before_action :require_no_authentication, only: [:edit, :update]
before_action :resource_from_email, only: [:create]
@@ -46,7 +44,7 @@ class PasswordsController < Devise::PasswordsController
if resource
return if resource.allow_password_authentication?
else
- return if current_application_settings.password_authentication_enabled?
+ return if Gitlab::CurrentSettings.password_authentication_enabled?
end
redirect_to after_sending_reset_password_instructions_path_for(resource_name),
diff --git a/app/controllers/projects/clusters/gcp_controller.rb b/app/controllers/projects/clusters/gcp_controller.rb
index 4fc515bd03e..94d33b91562 100644
--- a/app/controllers/projects/clusters/gcp_controller.rb
+++ b/app/controllers/projects/clusters/gcp_controller.rb
@@ -42,7 +42,7 @@ class Projects::Clusters::GcpController < Projects::ApplicationController
when 'true'
return
when 'false'
- flash[:alert] = _('Please <a href=%{link_to_billing} target="_blank" rel="noopener noreferrer">enable billing for one of your projects to be able to create a cluster</a>, then try again.').html_safe % { link_to_billing: "https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral" }
+ flash[:alert] = _('Please <a href=%{link_to_billing} target="_blank" rel="noopener noreferrer">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again.').html_safe % { link_to_billing: "https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral" }
else
flash[:alert] = _('We could not verify that one of your projects on GCP has billing enabled. Please try again.')
end
diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb
index 1dc7f1b3a7f..142e8b6e4bc 100644
--- a/app/controllers/projects/clusters_controller.rb
+++ b/app/controllers/projects/clusters_controller.rb
@@ -41,7 +41,7 @@ class Projects::ClustersController < Projects::ApplicationController
head :no_content
end
format.html do
- flash[:notice] = "Cluster was successfully updated."
+ flash[:notice] = _('Kubernetes cluster was successfully updated.')
redirect_to project_cluster_path(project, cluster)
end
end
@@ -55,10 +55,10 @@ class Projects::ClustersController < Projects::ApplicationController
def destroy
if cluster.destroy
- flash[:notice] = "Cluster integration was successfully removed."
+ flash[:notice] = _('Kubernetes cluster integration was successfully removed.')
redirect_to project_clusters_path(project), status: 302
else
- flash[:notice] = "Cluster integration was not removed."
+ flash[:notice] = _('Kubernetes cluster integration was not removed.')
render :show
end
end
diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb
index 0a40c67368f..1d910e461b1 100644
--- a/app/controllers/projects/commits_controller.rb
+++ b/app/controllers/projects/commits_controller.rb
@@ -4,6 +4,7 @@ class Projects::CommitsController < Projects::ApplicationController
include ExtractsPath
include RendersCommits
+ before_action :whitelist_query_limiting
before_action :require_non_empty_project
before_action :assign_ref_vars
before_action :authorize_download_code!
@@ -65,4 +66,8 @@ class Projects::CommitsController < Projects::ApplicationController
@commits = @commits.with_pipeline_status
@commits = prepare_commits_for_rendering(@commits)
end
+
+ def whitelist_query_limiting
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42330')
+ end
end
diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb
index 88ac3ad046b..d1b8fd80c4e 100644
--- a/app/controllers/projects/cycle_analytics_controller.rb
+++ b/app/controllers/projects/cycle_analytics_controller.rb
@@ -3,6 +3,7 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
include ActionView::Helpers::TextHelper
include CycleAnalyticsParams
+ before_action :whitelist_query_limiting, only: [:show]
before_action :authorize_read_cycle_analytics!
def show
@@ -31,4 +32,8 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
permissions: @cycle_analytics.permissions(user: current_user)
}
end
+
+ def whitelist_query_limiting
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42671')
+ end
end
diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb
index 68978f8fdd1..f43bba18d81 100644
--- a/app/controllers/projects/forks_controller.rb
+++ b/app/controllers/projects/forks_controller.rb
@@ -2,6 +2,7 @@ class Projects::ForksController < Projects::ApplicationController
include ContinueParams
# Authorize
+ before_action :whitelist_query_limiting, only: [:create]
before_action :require_non_empty_project
before_action :authorize_download_code!
before_action :authenticate_user!, only: [:new, :create]
@@ -54,4 +55,8 @@ class Projects::ForksController < Projects::ApplicationController
render :error
end
end
+
+ def whitelist_query_limiting
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42335')
+ end
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 384f18b316c..515cb08f1fc 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -8,6 +8,7 @@ class Projects::IssuesController < Projects::ApplicationController
prepend_before_action :authenticate_user!, only: [:new]
+ before_action :whitelist_query_limiting, only: [:create, :create_merge_request, :move, :bulk_update]
before_action :check_issues_available!
before_action :issue, except: [:index, :new, :create, :bulk_update]
before_action :set_issuables_index, only: [:index]
@@ -247,4 +248,13 @@ class Projects::IssuesController < Projects::ApplicationController
@finder_type = IssuesFinder
super
end
+
+ def whitelist_query_limiting
+ # Also see the following issues:
+ #
+ # 1. https://gitlab.com/gitlab-org/gitlab-ce/issues/42423
+ # 2. https://gitlab.com/gitlab-org/gitlab-ce/issues/42424
+ # 3. https://gitlab.com/gitlab-org/gitlab-ce/issues/42426
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42422')
+ end
end
diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb
index 293869345bd..941638db427 100644
--- a/app/controllers/projects/lfs_storage_controller.rb
+++ b/app/controllers/projects/lfs_storage_controller.rb
@@ -60,7 +60,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
def store_file(oid, size, tmp_file)
# Define tmp_file_path early because we use it in "ensure"
- tmp_file_path = File.join("#{Gitlab.config.lfs.storage_path}/tmp/upload", tmp_file)
+ tmp_file_path = File.join(LfsObjectUploader.workhorse_upload_path, tmp_file)
object = LfsObject.find_or_create_by(oid: oid, size: size)
file_exists = object.file.exists? || move_tmp_file_to_storage(object, tmp_file_path)
diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb
index 0df80fa700f..a5a2d54ba82 100644
--- a/app/controllers/projects/merge_requests/creations_controller.rb
+++ b/app/controllers/projects/merge_requests/creations_controller.rb
@@ -4,6 +4,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
include RendersCommits
skip_before_action :merge_request
+ before_action :whitelist_query_limiting, only: [:create]
before_action :authorize_create_merge_request!
before_action :apply_diff_view_cookie!, only: [:diffs, :diff_for_path]
before_action :build_merge_request, except: [:create]
@@ -125,4 +126,8 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
@project.forked_from_project
end
end
+
+ def whitelist_query_limiting
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42384')
+ end
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 2e8a738b6d9..8af4e379f0a 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -7,6 +7,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
include IssuableCollections
skip_before_action :merge_request, only: [:index, :bulk_update]
+ before_action :whitelist_query_limiting, only: [:assign_related_issues, :update]
before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort]
before_action :set_issuables_index, only: [:index]
before_action :authenticate_user!, only: [:assign_related_issues]
@@ -339,4 +340,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
access_denied! unless access_check
end
+
+ def whitelist_query_limiting
+ # Also see https://gitlab.com/gitlab-org/gitlab-ce/issues/42441
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42438')
+ end
end
diff --git a/app/controllers/projects/network_controller.rb b/app/controllers/projects/network_controller.rb
index fb68dd771a1..3b10a93e97f 100644
--- a/app/controllers/projects/network_controller.rb
+++ b/app/controllers/projects/network_controller.rb
@@ -2,6 +2,7 @@ class Projects::NetworkController < Projects::ApplicationController
include ExtractsPath
include ApplicationHelper
+ before_action :whitelist_query_limiting
before_action :require_non_empty_project
before_action :assign_ref_vars
before_action :authorize_download_code!
@@ -35,4 +36,8 @@ class Projects::NetworkController < Projects::ApplicationController
@options[:extended_sha1] = params[:extended_sha1]
@commit = @repo.commit(@options[:extended_sha1])
end
+
+ def whitelist_query_limiting
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42333')
+ end
end
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 5940fae8dd0..4f8978c93c3 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -2,6 +2,7 @@ class Projects::NotesController < Projects::ApplicationController
include NotesActions
include ToggleAwardEmoji
+ before_action :whitelist_query_limiting, only: [:create]
before_action :authorize_read_note!
before_action :authorize_create_note!, only: [:create]
before_action :authorize_resolve_note!, only: [:resolve, :unresolve]
@@ -79,4 +80,8 @@ class Projects::NotesController < Projects::ApplicationController
access_denied! unless can?(current_user, :create_note, noteable)
end
+
+ def whitelist_query_limiting
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42383')
+ end
end
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index e146d0d3cd5..78d109cf33e 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -1,4 +1,5 @@
class Projects::PipelinesController < Projects::ApplicationController
+ before_action :whitelist_query_limiting, only: [:create, :retry]
before_action :pipeline, except: [:index, :new, :create, :charts]
before_action :commit, only: [:show, :builds, :failures]
before_action :authorize_read_pipeline!
@@ -166,4 +167,9 @@ class Projects::PipelinesController < Projects::ApplicationController
def commit
@commit ||= @pipeline.commit
end
+
+ def whitelist_query_limiting
+ # Also see https://gitlab.com/gitlab-org/gitlab-ce/issues/42343
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42339')
+ end
end
diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb
index 4685bbe80b4..f5cf089ad98 100644
--- a/app/controllers/projects/uploads_controller.rb
+++ b/app/controllers/projects/uploads_controller.rb
@@ -1,6 +1,7 @@
class Projects::UploadsController < Projects::ApplicationController
include UploadsActions
+ # These will kick you out if you don't have access.
skip_before_action :project, :repository,
if: -> { action_name == 'show' && image_or_video? }
@@ -8,14 +9,20 @@ class Projects::UploadsController < Projects::ApplicationController
private
- def show_model
- strong_memoize(:show_model) do
- namespace = params[:namespace_id]
- id = params[:project_id]
+ def upload_model_class
+ Project
+ end
- Project.find_by_full_path("#{namespace}/#{id}")
- end
+ def uploader_class
+ FileUploader
end
- alias_method :model, :project
+ def find_model
+ return @project if @project
+
+ namespace = params[:namespace_id]
+ id = params[:project_id]
+
+ Project.find_by_full_path("#{namespace}/#{id}")
+ end
end
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 292e4158f8b..c4930d3d18d 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -54,8 +54,8 @@ class Projects::WikisController < Projects::ApplicationController
else
render 'edit'
end
- rescue WikiPage::PageChangedError
- @conflict = true
+ rescue WikiPage::PageChangedError, WikiPage::PageRenameError => e
+ @error = e
render 'edit'
end
@@ -76,9 +76,9 @@ class Projects::WikisController < Projects::ApplicationController
@page = @project_wiki.find_page(params[:id])
if @page
- @page_versions = Kaminari.paginate_array(@page.versions(page: params[:page]),
+ @page_versions = Kaminari.paginate_array(@page.versions(page: params[:page].to_i),
total_count: @page.count_versions)
- .page(params[:page])
+ .page(params[:page])
else
redirect_to(
project_wiki_path(@project, :home),
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index e6e2b219e6a..72573e0765d 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -3,6 +3,7 @@ class ProjectsController < Projects::ApplicationController
include ExtractsPath
include PreviewMarkdown
+ before_action :whitelist_query_limiting, only: [:create]
before_action :authenticate_user!, except: [:index, :show, :activity, :refs]
before_action :redirect_git_extension, only: [:show]
before_action :project, except: [:index, :new, :create]
@@ -394,7 +395,7 @@ class ProjectsController < Projects::ApplicationController
end
def project_export_enabled
- render_404 unless current_application_settings.project_export_enabled?
+ render_404 unless Gitlab::CurrentSettings.project_export_enabled?
end
def redirect_git_extension
@@ -403,6 +404,10 @@ class ProjectsController < Projects::ApplicationController
# to
# localhost/group/project
#
- redirect_to request.original_url.sub(/\.git\/?\Z/, '') if params[:format] == 'git'
+ redirect_to request.original_url.sub(%r{\.git/?\Z}, '') if params[:format] == 'git'
+ end
+
+ def whitelist_query_limiting
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42440')
end
end
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index d9142311b6f..1848c806c41 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -1,6 +1,8 @@
class RegistrationsController < Devise::RegistrationsController
include Recaptcha::Verify
+ before_action :whitelist_query_limiting, only: [:destroy]
+
def new
redirect_to(new_user_session_path)
end
@@ -83,4 +85,8 @@ class RegistrationsController < Devise::RegistrationsController
def devise_mapping
@devise_mapping ||= Devise.mappings[:user]
end
+
+ def whitelist_query_limiting
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42380')
+ end
end
diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb
index 19e38993038..8acefd58e77 100644
--- a/app/controllers/root_controller.rb
+++ b/app/controllers/root_controller.rb
@@ -23,7 +23,7 @@ class RootController < Dashboard::ProjectsController
def redirect_unlogged_user
if redirect_to_home_page_url?
- redirect_to(current_application_settings.home_page_url)
+ redirect_to(Gitlab::CurrentSettings.home_page_url)
else
redirect_to(new_user_session_path)
end
@@ -48,9 +48,9 @@ class RootController < Dashboard::ProjectsController
def redirect_to_home_page_url?
# If user is not signed-in and tries to access root_path - redirect him to landing page
# Don't redirect to the default URL to prevent endless redirections
- return false unless current_application_settings.home_page_url.present?
+ return false unless Gitlab::CurrentSettings.home_page_url.present?
- home_page_url = current_application_settings.home_page_url.chomp('/')
+ home_page_url = Gitlab::CurrentSettings.home_page_url.chomp('/')
root_urls = [Gitlab.config.gitlab['url'].chomp('/'), root_url.chomp('/')]
root_urls.exclude?(home_page_url)
diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb
index 16a74f82d3f..3d227b0a955 100644
--- a/app/controllers/uploads_controller.rb
+++ b/app/controllers/uploads_controller.rb
@@ -1,19 +1,34 @@
class UploadsController < ApplicationController
include UploadsActions
+ UnknownUploadModelError = Class.new(StandardError)
+
+ MODEL_CLASSES = {
+ "user" => User,
+ "project" => Project,
+ "note" => Note,
+ "group" => Group,
+ "appearance" => Appearance,
+ "personal_snippet" => PersonalSnippet,
+ nil => PersonalSnippet
+ }.freeze
+
+ rescue_from UnknownUploadModelError, with: :render_404
+
skip_before_action :authenticate_user!
+ before_action :upload_mount_satisfied?
before_action :find_model
before_action :authorize_access!, only: [:show]
before_action :authorize_create_access!, only: [:create]
- private
+ def uploader_class
+ PersonalFileUploader
+ end
def find_model
return nil unless params[:id]
- return render_404 unless upload_model && upload_mount
-
- @model = upload_model.find(params[:id])
+ upload_model_class.find(params[:id])
end
def authorize_access!
@@ -53,55 +68,17 @@ class UploadsController < ApplicationController
end
end
- def upload_model
- upload_models = {
- "user" => User,
- "project" => Project,
- "note" => Note,
- "group" => Group,
- "appearance" => Appearance,
- "personal_snippet" => PersonalSnippet
- }
-
- upload_models[params[:model]]
- end
-
- def upload_mount
- return true unless params[:mounted_as]
-
- upload_mounts = %w(avatar attachment file logo header_logo)
-
- if upload_mounts.include?(params[:mounted_as])
- params[:mounted_as]
- end
+ def upload_model_class
+ MODEL_CLASSES[params[:model]] || raise(UnknownUploadModelError)
end
- def uploader
- return @uploader if defined?(@uploader)
-
- case model
- when nil
- @uploader = PersonalFileUploader.new(nil, params[:secret])
-
- @uploader.retrieve_from_store!(params[:filename])
- when PersonalSnippet
- @uploader = PersonalFileUploader.new(model, params[:secret])
-
- @uploader.retrieve_from_store!(params[:filename])
- else
- @uploader = @model.public_send(upload_mount) # rubocop:disable GitlabSecurity/PublicSend
-
- redirect_to @uploader.url unless @uploader.file_storage?
- end
-
- @uploader
+ def upload_model_class_has_mounts?
+ upload_model_class < CarrierWave::Mount::Extension
end
- def uploader_class
- PersonalFileUploader
- end
+ def upload_mount_satisfied?
+ return true unless upload_model_class_has_mounts?
- def model
- @model ||= find_model
+ upload_model_class.uploader_options.has_key?(upload_mount)
end
end
diff --git a/app/controllers/user_callouts_controller.rb b/app/controllers/user_callouts_controller.rb
new file mode 100644
index 00000000000..18cde4a7b1a
--- /dev/null
+++ b/app/controllers/user_callouts_controller.rb
@@ -0,0 +1,23 @@
+class UserCalloutsController < ApplicationController
+ def create
+ if ensure_callout.persisted?
+ respond_to do |format|
+ format.json { head :ok }
+ end
+ else
+ respond_to do |format|
+ format.json { head :bad_request }
+ end
+ end
+ end
+
+ private
+
+ def ensure_callout
+ current_user.callouts.find_or_create_by(feature_name: UserCallout.feature_names[feature_name])
+ end
+
+ def feature_name
+ params.require(:feature_name)
+ end
+end
diff --git a/app/finders/group_projects_finder.rb b/app/finders/group_projects_finder.rb
index f2d3b90b8e2..f73cf8adb4d 100644
--- a/app/finders/group_projects_finder.rb
+++ b/app/finders/group_projects_finder.rb
@@ -87,8 +87,17 @@ class GroupProjectsFinder < ProjectsFinder
options.fetch(:only_shared, false)
end
+ # subgroups are supported only for owned projects not for shared
+ def include_subgroups?
+ options.fetch(:include_subgroups, false)
+ end
+
def owned_projects
- group.projects
+ if include_subgroups?
+ Project.where(namespace_id: group.self_and_descendants.select(:id))
+ else
+ group.projects
+ end
end
def shared_projects
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 493e7985d75..0fe3000ca01 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -43,6 +43,7 @@ class IssuableFinder
search
sort
state
+ include_subgroups
].freeze
ARRAY_PARAMS = { label_name: [], iids: [], assignee_username: [] }.freeze
@@ -148,7 +149,8 @@ class IssuableFinder
if current_user && params[:authorized_only].presence && !current_user_related?
current_user.authorized_projects
elsif group
- GroupProjectsFinder.new(group: group, current_user: current_user).execute
+ finder_options = { include_subgroups: params[:include_subgroups], only_owned: true }
+ GroupProjectsFinder.new(group: group, current_user: current_user, options: finder_options).execute
else
ProjectsFinder.new(current_user: current_user, project_ids_relation: item_project_ids(items)).execute
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 8ef561d90e6..7548bc30247 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -1,25 +1,23 @@
module ApplicationSettingsHelper
extend self
- include Gitlab::CurrentSettings
-
delegate :allow_signup?,
:gravatar_enabled?,
:password_authentication_enabled_for_web?,
:akismet_enabled?,
:koding_enabled?,
- to: :current_application_settings
+ to: :'Gitlab::CurrentSettings.current_application_settings'
def user_oauth_applications?
- current_application_settings.user_oauth_applications
+ Gitlab::CurrentSettings.user_oauth_applications
end
def allowed_protocols_present?
- current_application_settings.enabled_git_access_protocol.present?
+ Gitlab::CurrentSettings.enabled_git_access_protocol.present?
end
def enabled_protocol
- case current_application_settings.enabled_git_access_protocol
+ case Gitlab::CurrentSettings.enabled_git_access_protocol
when 'http'
gitlab_config.protocol
when 'ssh'
@@ -57,7 +55,7 @@ module ApplicationSettingsHelper
# toggle button effect.
def import_sources_checkboxes(help_block_id)
Gitlab::ImportSources.options.map do |name, source|
- checked = current_application_settings.import_sources.include?(source)
+ checked = Gitlab::CurrentSettings.import_sources.include?(source)
css_class = checked ? 'active' : ''
checkbox_name = 'application_setting[import_sources][]'
@@ -72,7 +70,7 @@ module ApplicationSettingsHelper
def oauth_providers_checkboxes
button_based_providers.map do |source|
- disabled = current_application_settings.disabled_oauth_sign_in_sources.include?(source.to_s)
+ disabled = Gitlab::CurrentSettings.disabled_oauth_sign_in_sources.include?(source.to_s)
css_class = 'btn'
css_class << ' active' unless disabled
checkbox_name = 'application_setting[enabled_oauth_sign_in_sources][]'
@@ -96,12 +94,12 @@ module ApplicationSettingsHelper
]
end
- def repository_storages_options_for_select
+ def repository_storages_options_for_select(selected)
options = Gitlab.config.repositories.storages.map do |name, storage|
["#{name} - #{storage['path']}", name]
end
- options_for_select(options, @application_setting.repository_storages)
+ options_for_select(options, selected)
end
def sidekiq_queue_options_for_select
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index 66dc0b1e6f7..f909f664034 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -1,6 +1,4 @@
module AuthHelper
- include Gitlab::CurrentSettings
-
PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq).freeze
FORM_BASED_PROVIDERS = [/\Aldap/, 'crowd'].freeze
@@ -41,7 +39,7 @@ module AuthHelper
end
def enabled_button_based_providers
- disabled_providers = current_application_settings.disabled_oauth_sign_in_sources || []
+ disabled_providers = Gitlab::CurrentSettings.disabled_oauth_sign_in_sources || []
button_based_providers.map(&:to_s) - disabled_providers
end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 676c1d1988b..23de3590b93 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -1,4 +1,8 @@
module GroupsHelper
+ def group_nav_link_paths
+ %w[groups#projects groups#edit ci_cd#show ldap_group_links#index hooks#index audit_events#index pipeline_quota#index]
+ end
+
def can_change_group_visibility_level?(group)
can?(current_user, :change_visibility_level, group)
end
@@ -88,6 +92,19 @@ module GroupsHelper
end
end
+ def parent_group_options(current_group)
+ groups = current_user.owned_groups.sort_by(&:human_name).map do |group|
+ { id: group.id, text: group.human_name }
+ end
+
+ groups.delete_if { |group| group[:id] == current_group.id }
+ groups.to_json
+ end
+
+ def supports_nested_groups?
+ Group.supports_nested_groups?
+ end
+
private
def group_title_link(group, hidable: false, show_avatar: false, for_dropdown: false)
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index f7bdcc6fd9c..6512617a02d 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -1,6 +1,4 @@
module ProjectsHelper
- include Gitlab::CurrentSettings
-
def link_to_project(project)
link_to [project.namespace.becomes(Namespace), project], title: h(project.name) do
title = content_tag(:span, project.name, class: 'project-name')
@@ -214,7 +212,7 @@ module ProjectsHelper
project.cache_key,
controller.controller_name,
controller.action_name,
- current_application_settings.cache_key,
+ Gitlab::CurrentSettings.cache_key,
'v2.5'
]
@@ -447,10 +445,10 @@ module ProjectsHelper
path = "#{import_path}?repo=#{repo}&branch=#{branch}&sha=#{sha}"
- return URI.join(current_application_settings.koding_url, path).to_s
+ return URI.join(Gitlab::CurrentSettings.koding_url, path).to_s
end
- current_application_settings.koding_url
+ Gitlab::CurrentSettings.koding_url
end
def contribution_guide_path(project)
@@ -559,7 +557,7 @@ module ProjectsHelper
def restricted_levels
return [] if current_user.admin?
- current_application_settings.restricted_visibility_levels || []
+ Gitlab::CurrentSettings.restricted_visibility_levels || []
end
def project_permissions_settings(project)
diff --git a/app/helpers/sidekiq_helper.rb b/app/helpers/sidekiq_helper.rb
index 55f4da0ef85..50aeb7f4b82 100644
--- a/app/helpers/sidekiq_helper.rb
+++ b/app/helpers/sidekiq_helper.rb
@@ -1,12 +1,12 @@
module SidekiqHelper
- SIDEKIQ_PS_REGEXP = /\A
+ SIDEKIQ_PS_REGEXP = %r{\A
(?<pid>\d+)\s+
(?<cpu>[\d\.,]+)\s+
(?<mem>[\d\.,]+)\s+
- (?<state>[DIEKNRSTVWXZNLpsl\+<>\/\d]+)\s+
+ (?<state>[DIEKNRSTVWXZNLpsl\+<>/\d]+)\s+
(?<start>.+?)\s+
(?<command>(?:ruby\d+:\s+)?sidekiq.*\].*)
- \z/x
+ \z}x
def parse_sidekiq_ps(line)
match = line.strip.match(SIDEKIQ_PS_REGEXP)
diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb
index 1db9ae3839c..9151543dfdc 100644
--- a/app/helpers/submodule_helper.rb
+++ b/app/helpers/submodule_helper.rb
@@ -11,7 +11,7 @@ module SubmoduleHelper
url = File.join(Gitlab.config.gitlab.url, @project.full_path)
end
- if url =~ /([^\/:]+)\/([^\/]+(?:\.git)?)\Z/
+ if url =~ %r{([^/:]+)/([^/]+(?:\.git)?)\Z}
namespace, project = $1, $2
gitlab_hosts = [Gitlab.config.gitlab.url,
Gitlab.config.gitlab_shell.ssh_path_prefix]
@@ -23,7 +23,7 @@ module SubmoduleHelper
end
end
- namespace.sub!(/\A\//, '')
+ namespace.sub!(%r{\A/}, '')
project.rstrip!
project.sub!(/\.git\z/, '')
@@ -47,11 +47,11 @@ module SubmoduleHelper
protected
def github_dot_com_url?(url)
- url =~ /github\.com[\/:][^\/]+\/[^\/]+\Z/
+ url =~ %r{github\.com[/:][^/]+/[^/]+\Z}
end
def gitlab_dot_com_url?(url)
- url =~ /gitlab\.com[\/:][^\/]+\/[^\/]+\Z/
+ url =~ %r{gitlab\.com[/:][^/]+/[^/]+\Z}
end
def self_url?(url, namespace, project)
@@ -65,7 +65,7 @@ module SubmoduleHelper
def relative_self_url?(url)
# (./)?(../repo.git) || (./)?(../../project/repo.git) )
- url =~ /\A((\.\/)?(\.\.\/))(?!(\.\.)|(.*\/)).*(\.git)?\z/ || url =~ /\A((\.\/)?(\.\.\/){2})(?!(\.\.))([^\/]*)\/(?!(\.\.)|(.*\/)).*(\.git)?\z/
+ url =~ %r{\A((\./)?(\.\./))(?!(\.\.)|(.*/)).*(\.git)?\z} || url =~ %r{\A((\./)?(\.\./){2})(?!(\.\.))([^/]*)/(?!(\.\.)|(.*/)).*(\.git)?\z}
end
def standard_links(host, namespace, project, commit)
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index 5b2ea38a03d..d39cac0f510 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -110,7 +110,7 @@ module TreeHelper
# returns the relative path of the first subdir that doesn't have only one directory descendant
def flatten_tree(root_path, tree)
- return tree.flat_path.sub(/\A#{root_path}\//, '') if tree.flat_path.present?
+ return tree.flat_path.sub(%r{\A#{root_path}/}, '') if tree.flat_path.present?
subtree = Gitlab::Git::Tree.where(@repository, @commit.id, tree.path)
if subtree.count == 1 && subtree.first.dir?
diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb
new file mode 100644
index 00000000000..36abfaf19a5
--- /dev/null
+++ b/app/helpers/user_callouts_helper.rb
@@ -0,0 +1,14 @@
+module UserCalloutsHelper
+ GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'.freeze
+
+ def show_gke_cluster_integration_callout?(project)
+ can?(current_user, :create_cluster, project) &&
+ !user_dismissed?(GKE_CLUSTER_INTEGRATION)
+ end
+
+ private
+
+ def user_dismissed?(feature_name)
+ current_user&.callouts&.find_by(feature_name: UserCallout.feature_names[feature_name])
+ end
+end
diff --git a/app/helpers/version_check_helper.rb b/app/helpers/version_check_helper.rb
index 456598b4c28..c20753ece72 100644
--- a/app/helpers/version_check_helper.rb
+++ b/app/helpers/version_check_helper.rb
@@ -1,6 +1,6 @@
module VersionCheckHelper
def version_status_badge
- if Rails.env.production? && current_application_settings.version_check_enabled
+ if Rails.env.production? && Gitlab::CurrentSettings.version_check_enabled
image_url = VersionCheck.new.url
image_tag image_url, class: 'js-version-status-badge'
end
diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb
index c3d5628f241..e395cda03d3 100644
--- a/app/helpers/visibility_level_helper.rb
+++ b/app/helpers/visibility_level_helper.rb
@@ -151,12 +151,12 @@ module VisibilityLevelHelper
def restricted_visibility_levels(show_all = false)
return [] if current_user.admin? && !show_all
- current_application_settings.restricted_visibility_levels || []
+ Gitlab::CurrentSettings.restricted_visibility_levels || []
end
delegate :default_project_visibility,
:default_group_visibility,
- to: :current_application_settings
+ to: :'Gitlab::CurrentSettings.current_application_settings'
def disallowed_visibility_level?(form_model, level)
return false unless form_model.respond_to?(:visibility_level_allowed?)
diff --git a/app/helpers/webpack_helper.rb b/app/helpers/webpack_helper.rb
index 77433acb92a..9d071f2d59a 100644
--- a/app/helpers/webpack_helper.rb
+++ b/app/helpers/webpack_helper.rb
@@ -5,6 +5,24 @@ module WebpackHelper
javascript_include_tag(*gitlab_webpack_asset_paths(bundle, force_same_domain: force_same_domain))
end
+ def webpack_controller_bundle_tags
+ bundles = []
+ segments = [*controller.controller_path.split('/'), controller.action_name].compact
+
+ until segments.empty?
+ begin
+ asset_paths = gitlab_webpack_asset_paths("pages.#{segments.join('.')}", extension: 'js')
+ bundles.unshift(*asset_paths)
+ rescue Webpack::Rails::Manifest::EntryPointMissingError
+ # no bundle exists for this path
+ end
+
+ segments.pop
+ end
+
+ javascript_include_tag(*bundles)
+ end
+
# override webpack-rails gem helper until changes can make it upstream
def gitlab_webpack_asset_paths(source, extension: nil, force_same_domain: false)
return "" unless source.present?
diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb
index 815fab9e061..41f9eedd4bd 100644
--- a/app/helpers/wiki_helper.rb
+++ b/app/helpers/wiki_helper.rb
@@ -21,4 +21,22 @@ module WikiHelper
add_to_breadcrumb_dropdown link_to(WikiPage.unhyphenize(dir_or_page).capitalize, project_wiki_path(@project, current_slug)), location: :after
end
end
+
+ def wiki_page_errors(error)
+ return unless error
+
+ content_tag(:div, class: 'alert alert-danger') do
+ case error
+ when WikiPage::PageChangedError
+ page_link = link_to s_("WikiPageConflictMessage|the page"), project_wiki_path(@project, @page), target: "_blank"
+ concat(
+ (s_("WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs.") % { page_link: page_link }).html_safe
+ )
+ when WikiPage::PageRenameError
+ s_("WikiEdit|There is already a page with the same title in that path.")
+ else
+ error.message
+ end
+ end
+ end
end
diff --git a/app/mailers/abuse_report_mailer.rb b/app/mailers/abuse_report_mailer.rb
index d0ce827a595..fe5f68ba3d5 100644
--- a/app/mailers/abuse_report_mailer.rb
+++ b/app/mailers/abuse_report_mailer.rb
@@ -1,13 +1,11 @@
class AbuseReportMailer < BaseMailer
- include Gitlab::CurrentSettings
-
def notify(abuse_report_id)
return unless deliverable?
@abuse_report = AbuseReport.find(abuse_report_id)
mail(
- to: current_application_settings.admin_notification_email,
+ to: Gitlab::CurrentSettings.admin_notification_email,
subject: "#{@abuse_report.user.name} (#{@abuse_report.user.username}) was reported for abuse"
)
end
@@ -15,6 +13,6 @@ class AbuseReportMailer < BaseMailer
private
def deliverable?
- current_application_settings.admin_notification_email.present?
+ Gitlab::CurrentSettings.admin_notification_email.present?
end
end
diff --git a/app/mailers/base_mailer.rb b/app/mailers/base_mailer.rb
index 8e99db444d6..654468bc7fe 100644
--- a/app/mailers/base_mailer.rb
+++ b/app/mailers/base_mailer.rb
@@ -1,13 +1,11 @@
class BaseMailer < ActionMailer::Base
- include Gitlab::CurrentSettings
-
around_action :render_with_default_locale
helper ApplicationHelper
helper MarkupHelper
attr_accessor :current_user
- helper_method :current_user, :can?, :current_application_settings
+ helper_method :current_user, :can?
default from: proc { default_sender_address.format }
default reply_to: proc { default_reply_to_address.format }
diff --git a/app/models/appearance.rb b/app/models/appearance.rb
index 76cfe28742a..dcd14c08f3c 100644
--- a/app/models/appearance.rb
+++ b/app/models/appearance.rb
@@ -11,6 +11,7 @@ class Appearance < ActiveRecord::Base
mount_uploader :logo, AttachmentUploader
mount_uploader :header_logo, AttachmentUploader
+
has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
CACHE_KEY = 'current_appearance'.freeze
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 6ced5fb0e24..20534b8eed0 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -21,6 +21,7 @@ module Ci
has_many :job_artifacts, class_name: 'Ci::JobArtifact', foreign_key: :job_id, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :job_artifacts_archive, -> { where(file_type: Ci::JobArtifact.file_types[:archive]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
has_one :job_artifacts_metadata, -> { where(file_type: Ci::JobArtifact.file_types[:metadata]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
+ has_one :job_artifacts_trace, -> { where(file_type: Ci::JobArtifact.file_types[:trace]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
# The "environment" field for builds is a String, and is the unexpanded name
def persisted_environment
@@ -292,7 +293,7 @@ module Ci
def repo_url
auth = "gitlab-ci-token:#{ensure_token!}@"
- project.http_url_to_repo.sub(/^https?:\/\//) do |prefix|
+ project.http_url_to_repo.sub(%r{^https?://}) do |prefix|
prefix + auth
end
end
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 84fc6863567..0a599f72bc7 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -9,9 +9,12 @@ module Ci
mount_uploader :file, JobArtifactUploader
+ delegate :open, :exists?, to: :file
+
enum file_type: {
archive: 1,
- metadata: 2
+ metadata: 2,
+ trace: 3
}
def self.artifacts_size_for(project)
diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb
index 9024f1df1cd..aa5cf97756f 100644
--- a/app/models/clusters/applications/ingress.rb
+++ b/app/models/clusters/applications/ingress.rb
@@ -17,8 +17,12 @@ module Clusters
'stable/nginx-ingress'
end
+ def chart_values_file
+ "#{Rails.root}/vendor/#{name}/values.yaml"
+ end
+
def install_command
- Gitlab::Kubernetes::Helm::InstallCommand.new(name, chart: chart)
+ Gitlab::Kubernetes::Helm::InstallCommand.new(name, chart: chart, chart_values_file: chart_values_file)
end
end
end
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
index 9160a169452..7ce8befeeeb 100644
--- a/app/models/clusters/platforms/kubernetes.rb
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -1,7 +1,6 @@
module Clusters
module Platforms
class Kubernetes < ActiveRecord::Base
- include Gitlab::CurrentSettings
include Gitlab::Kubernetes
include ReactiveCaching
@@ -169,7 +168,7 @@ module Clusters
{
token: token,
ca_pem: ca_pem,
- max_session_time: current_application_settings.terminal_max_session_time
+ max_session_time: Gitlab::CurrentSettings.terminal_max_session_time
}
end
@@ -181,7 +180,7 @@ module Clusters
return unless managed?
if api_url_changed? || token_changed? || ca_pem_changed?
- errors.add(:base, "cannot modify managed cluster")
+ errors.add(:base, _('Cannot modify managed Kubernetes cluster'))
return false
end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index c0263c0b4e2..3469d5d795c 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -141,7 +141,7 @@ class CommitStatus < ActiveRecord::Base
end
def group_name
- name.to_s.gsub(/\d+[\s:\/\\]+\d+\s*/, '').strip
+ name.to_s.gsub(%r{\d+[\s:/\\]+\d+\s*}, '').strip
end
def failed_but_allowed?
diff --git a/app/models/concerns/artifact_migratable.rb b/app/models/concerns/artifact_migratable.rb
index 0460439e9e6..ff52ca64459 100644
--- a/app/models/concerns/artifact_migratable.rb
+++ b/app/models/concerns/artifact_migratable.rb
@@ -39,7 +39,6 @@ module ArtifactMigratable
end
def artifacts_size
- read_attribute(:artifacts_size).to_i +
- job_artifacts_archive&.size.to_i + job_artifacts_metadata&.size.to_i
+ read_attribute(:artifacts_size).to_i + job_artifacts.sum(:size).to_i
end
end
diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb
index 10659030910..d35e37935fb 100644
--- a/app/models/concerns/avatarable.rb
+++ b/app/models/concerns/avatarable.rb
@@ -1,6 +1,30 @@
module Avatarable
extend ActiveSupport::Concern
+ included do
+ prepend ShadowMethods
+
+ validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
+ validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
+
+ mount_uploader :avatar, AvatarUploader
+ end
+
+ module ShadowMethods
+ def avatar_url(**args)
+ # We use avatar_path instead of overriding avatar_url because of carrierwave.
+ # See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11001/diffs#note_28659864
+
+ avatar_path(only_path: args.fetch(:only_path, true)) || super
+ end
+ end
+
+ def avatar_type
+ unless self.avatar.image?
+ self.errors.add :avatar, "only images allowed"
+ end
+ end
+
def avatar_path(only_path: true)
return unless self[:avatar].present?
diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb
index 99dbd4fbacf..67a988addbe 100644
--- a/app/models/concerns/storage/legacy_namespace.rb
+++ b/app/models/concerns/storage/legacy_namespace.rb
@@ -14,7 +14,11 @@ module Storage
# Ensure old directory exists before moving it
gitlab_shell.add_namespace(repository_storage_path, full_path_was)
+ # Ensure new directory exists before moving it (if there's a parent)
+ gitlab_shell.add_namespace(repository_storage_path, parent.full_path) if parent
+
unless gitlab_shell.mv_namespace(repository_storage_path, full_path_was, full_path)
+
Rails.logger.error "Exception moving path #{repository_storage_path} from #{full_path_was} to #{full_path}"
# if we cannot move namespace directory we should rollback
@@ -87,20 +91,10 @@ module Storage
remove_exports!
end
- def remove_exports!
- Gitlab::Popen.popen(%W(find #{export_path} -not -path #{export_path} -delete))
- end
-
- def export_path
- File.join(Gitlab::ImportExport.storage_path, full_path_was)
- end
+ def remove_legacy_exports!
+ legacy_export_path = File.join(Gitlab::ImportExport.storage_path, full_path_was)
- def full_path_was
- if parent
- parent.full_path + '/' + path_was
- else
- path_was
- end
+ FileUtils.rm_rf(legacy_export_path)
end
end
end
diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb
index d07041c2fdf..ccd6f0e0a7d 100644
--- a/app/models/concerns/taskable.rb
+++ b/app/models/concerns/taskable.rb
@@ -9,13 +9,13 @@ require 'task_list/filter'
module Taskable
COMPLETED = 'completed'.freeze
INCOMPLETE = 'incomplete'.freeze
- ITEM_PATTERN = /
+ ITEM_PATTERN = %r{
^
\s*(?:[-+*]|(?:\d+\.)) # list prefix required - task item has to be always in a list
\s+ # whitespace prefix has to be always presented for a list item
(\[\s\]|\[[xX]\]) # checkbox
(\s.+) # followed by whitespace and some text.
- /x
+ }x
def self.get_tasks(content)
content.to_s.scan(ITEM_PATTERN).map do |checkbox, label|
diff --git a/app/models/environment.rb b/app/models/environment.rb
index bf69b4c50f0..2f6eae605ee 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -115,7 +115,7 @@ class Environment < ActiveRecord::Base
def formatted_external_url
return nil unless external_url
- external_url.gsub(/\A.*?:\/\//, '')
+ external_url.gsub(%r{\A.*?://}, '')
end
def stop_action?
diff --git a/app/models/group.rb b/app/models/group.rb
index fddace03387..5b7f1b38612 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -29,18 +29,14 @@ class Group < Namespace
has_many :variables, class_name: 'Ci::GroupVariable'
has_many :custom_attributes, class_name: 'GroupCustomAttribute'
- validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
+ has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+
validate :visibility_level_allowed_by_projects
validate :visibility_level_allowed_by_sub_groups
validate :visibility_level_allowed_by_parent
- validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
-
validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 }
- mount_uploader :avatar, AvatarUploader
- has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
-
after_create :post_create_hook
after_destroy :post_destroy_hook
after_save :update_two_factor_requirement
@@ -116,12 +112,6 @@ class Group < Namespace
visibility_level_allowed_by_sub_groups?(level)
end
- def avatar_url(**args)
- # We use avatar_path instead of overriding avatar_url because of carrierwave.
- # See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11001/diffs#note_28659864
- avatar_path(args)
- end
-
def lfs_enabled?
return false unless Gitlab.config.lfs.enabled
return Gitlab.config.lfs.enabled if self[:lfs_enabled].nil?
@@ -284,12 +274,6 @@ 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
-
def group_member(user)
if group_members.loaded?
group_members.find { |gm| gm.user_id == user.id }
diff --git a/app/models/issue_assignee.rb b/app/models/issue_assignee.rb
index 06d760b6a89..326b9eb7ad5 100644
--- a/app/models/issue_assignee.rb
+++ b/app/models/issue_assignee.rb
@@ -1,6 +1,4 @@
class IssueAssignee < ActiveRecord::Base
- extend Gitlab::CurrentSettings
-
belongs_to :issue
belongs_to :assignee, class_name: "User", foreign_key: :user_id
end
diff --git a/app/models/key.rb b/app/models/key.rb
index a3f8a5d6dc7..ae5769c0627 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -1,7 +1,6 @@
require 'digest/md5'
class Key < ActiveRecord::Base
- include Gitlab::CurrentSettings
include AfterCommitQueue
include Sortable
@@ -34,9 +33,8 @@ class Key < ActiveRecord::Base
after_destroy :refresh_user_cache
def key=(value)
- value&.delete!("\n\r")
- value.strip! unless value.blank?
- write_attribute(:key, value)
+ write_attribute(:key, value.present? ? Gitlab::SSHPublicKey.sanitize(value) : nil)
+
@public_key = nil
end
@@ -98,13 +96,13 @@ class Key < ActiveRecord::Base
def generate_fingerprint
self.fingerprint = nil
- return unless self.key.present?
+ return unless public_key.valid?
self.fingerprint = public_key.fingerprint
end
def key_meets_restrictions
- restriction = current_application_settings.key_restriction_for(public_key.type)
+ restriction = Gitlab::CurrentSettings.key_restriction_for(public_key.type)
if restriction == ApplicationSetting::FORBIDDEN_KEY_VALUE
errors.add(:key, forbidden_key_type_message)
@@ -115,7 +113,7 @@ class Key < ActiveRecord::Base
def forbidden_key_type_message
allowed_types =
- current_application_settings
+ Gitlab::CurrentSettings
.allowed_key_types
.map(&:upcase)
.to_sentence(last_word_connector: ', or ', two_words_connector: ' or ')
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 37a7417cafc..44c5bb13a59 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -2,7 +2,6 @@ class Namespace < ActiveRecord::Base
include CacheMarkdownField
include Sortable
include Gitlab::ShellAdapter
- include Gitlab::CurrentSettings
include Gitlab::VisibilityLevel
include Routable
include AfterCommitQueue
@@ -21,6 +20,9 @@ class Namespace < ActiveRecord::Base
has_many :projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :project_statistics
+
+ # This should _not_ be `inverse_of: :namespace`, because that would also set
+ # `user.namespace` when this user creates a group with themselves as `owner`.
belongs_to :owner, class_name: "User"
belongs_to :parent, class_name: "Namespace"
@@ -41,7 +43,6 @@ class Namespace < ActiveRecord::Base
namespace_path: true
validate :nesting_level_allowed
- validate :allowed_path_by_redirects
delegate :name, to: :owner, allow_nil: true, prefix: true
@@ -53,7 +54,7 @@ class Namespace < ActiveRecord::Base
# Legacy Storage specific hooks
- after_update :move_dir, if: :path_changed?
+ after_update :move_dir, if: :path_or_parent_changed?
before_destroy(prepend: true) { prepare_for_destroy }
after_destroy :rm_dir
@@ -222,8 +223,33 @@ class Namespace < ActiveRecord::Base
has_parent?
end
+ def full_path_was
+ if parent_id_was.nil?
+ path_was
+ else
+ previous_parent = Group.find_by(id: parent_id_was)
+ previous_parent.full_path + '/' + path_was
+ end
+ end
+
+ # Exports belonging to projects with legacy storage are placed in a common
+ # subdirectory of the namespace, so a simple `rm -rf` is sufficient to remove
+ # them.
+ #
+ # Exports of projects using hashed storage are placed in a location defined
+ # only by the project ID, so each must be removed individually.
+ def remove_exports!
+ remove_legacy_exports!
+
+ all_projects.with_storage_feature(:repository).find_each(&:remove_exports)
+ end
+
private
+ def path_or_parent_changed?
+ path_changed? || parent_changed?
+ end
+
def refresh_access_of_projects_invited_groups
Group
.joins(project_group_links: :project)
@@ -254,16 +280,6 @@ class Namespace < ActiveRecord::Base
.update_all(share_with_group_lock: true)
end
- def allowed_path_by_redirects
- return if path.nil?
-
- errors.add(:path, "#{path} has been taken before. Please use another one") if namespace_previously_created_with_same_path?
- end
-
- def namespace_previously_created_with_same_path?
- RedirectRoute.permanent.exists?(path: path)
- end
-
def write_projects_repository_config
all_projects.find_each do |project|
project.expires_full_path_cache # we need to clear cache to validate renames correctly
diff --git a/app/models/note.rb b/app/models/note.rb
index 184fbd5f5ae..cac60845a49 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -3,7 +3,6 @@
# A note of this type is never resolvable.
class Note < ActiveRecord::Base
extend ActiveModel::Naming
- include Gitlab::CurrentSettings
include Participable
include Mentionable
include Awardable
@@ -61,7 +60,7 @@ class Note < ActiveRecord::Base
belongs_to :updated_by, class_name: "User"
belongs_to :last_edited_by, class_name: 'User'
- has_many :todos, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :todos
has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :system_note_metadata
@@ -88,6 +87,7 @@ class Note < ActiveRecord::Base
end
end
+ # @deprecated attachments are handler by the MarkdownUploader
mount_uploader :attachment, AttachmentUploader
# Scopes
@@ -195,7 +195,7 @@ class Note < ActiveRecord::Base
end
def max_attachment_size
- current_application_settings.max_attachment_size.megabytes.to_i
+ Gitlab::CurrentSettings.max_attachment_size.megabytes.to_i
end
def hook_attrs
diff --git a/app/models/project.rb b/app/models/project.rb
index d0d0fd6e093..33085151217 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -4,7 +4,6 @@ class Project < ActiveRecord::Base
include Gitlab::ConfigHelper
include Gitlab::ShellAdapter
include Gitlab::VisibilityLevel
- include Gitlab::CurrentSettings
include AccessRequestable
include Avatarable
include CacheMarkdownField
@@ -23,7 +22,6 @@ class Project < ActiveRecord::Base
include ::Gitlab::Utils::StrongMemoize
extend Gitlab::ConfigHelper
- extend Gitlab::CurrentSettings
BoardLimitExceeded = Class.new(StandardError)
@@ -51,8 +49,8 @@ class Project < ActiveRecord::Base
default_value_for :visibility_level, gitlab_config_features.visibility_level
default_value_for :resolve_outdated_diff_discussions, false
default_value_for :container_registry_enabled, gitlab_config_features.container_registry
- default_value_for(:repository_storage) { current_application_settings.pick_repository_storage }
- default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled }
+ default_value_for(:repository_storage) { Gitlab::CurrentSettings.pick_repository_storage }
+ default_value_for(:shared_runners_enabled) { Gitlab::CurrentSettings.shared_runners_enabled }
default_value_for :issues_enabled, gitlab_config_features.issues
default_value_for :merge_requests_enabled, gitlab_config_features.merge_requests
default_value_for :builds_enabled, gitlab_config_features.builds
@@ -71,6 +69,7 @@ class Project < ActiveRecord::Base
before_destroy :remove_private_deploy_keys
after_destroy -> { run_after_commit { remove_pages } }
+ after_destroy :remove_exports
after_validation :check_pending_delete
@@ -234,7 +233,7 @@ class Project < ActiveRecord::Base
validates :creator, presence: true, on: :create
validates :description, length: { maximum: 2000 }, allow_blank: true
validates :ci_config_path,
- format: { without: /(\.{2}|\A\/)/,
+ format: { without: %r{(\.{2}|\A/)},
message: 'cannot include leading slash or directory traversal.' },
length: { maximum: 255 },
allow_blank: true
@@ -256,9 +255,6 @@ class Project < ActiveRecord::Base
validates :star_count, numericality: { greater_than_or_equal_to: 0 }
validate :check_limit, on: :create
validate :check_repository_path_availability, on: :update, if: ->(project) { project.renamed? }
- validate :avatar_type,
- if: ->(project) { project.avatar.present? && project.avatar_changed? }
- validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
validate :visibility_level_allowed_by_group
validate :visibility_level_allowed_as_fork
validate :check_wiki_path_conflict
@@ -266,7 +262,6 @@ class Project < ActiveRecord::Base
presence: true,
inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } }
- mount_uploader :avatar, AvatarUploader
has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
# Scopes
@@ -289,7 +284,6 @@ class Project < ActiveRecord::Base
scope :non_archived, -> { where(archived: false) }
scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) }
-
scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
scope :with_statistics, -> { includes(:statistics) }
scope :with_shared_runners, -> { where(shared_runners_enabled: true) }
@@ -491,14 +485,14 @@ class Project < ActiveRecord::Base
def auto_devops_enabled?
if auto_devops&.enabled.nil?
- current_application_settings.auto_devops_enabled?
+ Gitlab::CurrentSettings.auto_devops_enabled?
else
auto_devops.enabled?
end
end
def has_auto_devops_implicitly_disabled?
- auto_devops&.enabled.nil? && !current_application_settings.auto_devops_enabled?
+ auto_devops&.enabled.nil? && !Gitlab::CurrentSettings.auto_devops_enabled?
end
def empty_repo?
@@ -517,10 +511,13 @@ class Project < ActiveRecord::Base
@repository ||= Repository.new(full_path, self, disk_path: disk_path)
end
- def reload_repository!
+ def cleanup
+ @repository&.cleanup
@repository = nil
end
+ alias_method :reload_repository!, :cleanup
+
def container_registry_url
if Gitlab.config.registry.enabled
"#{Gitlab.config.registry.host_port}/#{full_path.downcase}"
@@ -923,20 +920,12 @@ class Project < ActiveRecord::Base
issues_tracker.to_param == 'jira'
end
- def avatar_type
- unless self.avatar.image?
- self.errors.add :avatar, 'only images allowed'
- end
- end
-
def avatar_in_git
repository.avatar
end
def avatar_url(**args)
- # We use avatar_path instead of overriding avatar_url because of carrierwave.
- # See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11001/diffs#note_28659864
- avatar_path(args) || (Gitlab::Routing.url_helpers.project_avatar_url(self) if avatar_in_git)
+ Gitlab::Routing.url_helpers.project_avatar_url(self) if avatar_in_git
end
# For compatibility with old code
@@ -1338,7 +1327,7 @@ class Project < ActiveRecord::Base
host = "#{subdomain}.#{Settings.pages.host}".downcase
# The host in URL always needs to be downcased
- url = Gitlab.config.pages.url.sub(/^https?:\/\//) do |prefix|
+ url = Gitlab.config.pages.url.sub(%r{^https?://}) do |prefix|
"#{prefix}#{subdomain}."
end.downcase
@@ -1484,14 +1473,14 @@ class Project < ActiveRecord::Base
# Ensure HEAD points to the default branch in case it is not master
change_head(default_branch)
- if current_application_settings.default_branch_protection != Gitlab::Access::PROTECTION_NONE && !ProtectedBranch.protected?(self, default_branch)
+ if Gitlab::CurrentSettings.default_branch_protection != Gitlab::Access::PROTECTION_NONE && !ProtectedBranch.protected?(self, default_branch)
params = {
name: default_branch,
push_access_levels_attributes: [{
- access_level: current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
+ access_level: Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
}],
merge_access_levels_attributes: [{
- access_level: current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
+ access_level: Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
}]
}
@@ -1540,6 +1529,8 @@ class Project < ActiveRecord::Base
end
def export_path
+ return nil unless namespace.present? || hashed_storage?(:repository)
+
File.join(Gitlab::ImportExport.storage_path, disk_path)
end
@@ -1548,8 +1539,9 @@ class Project < ActiveRecord::Base
end
def remove_exports
- _, status = Gitlab::Popen.popen(%W(find #{export_path} -not -path #{export_path} -delete))
- status.zero?
+ return nil unless export_path.present?
+
+ FileUtils.rm_rf(export_path)
end
def full_path_slug
@@ -1786,7 +1778,7 @@ class Project < ActiveRecord::Base
end
def use_hashed_storage
- if self.new_record? && current_application_settings.hashed_storage_enabled
+ if self.new_record? && Gitlab::CurrentSettings.hashed_storage_enabled
self.storage_version = LATEST_STORAGE_VERSION
end
end
diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb
index 9ce2d1153a7..109258d1eb7 100644
--- a/app/models/project_services/asana_service.rb
+++ b/app/models/project_services/asana_service.rb
@@ -84,7 +84,7 @@ http://app.asana.com/-/account_api'
# - fix/ed/es/ing
# - close/s/d
# - closing
- issue_finder = /(fix\w*|clos[ei]\w*+)?\W*(?:https:\/\/app\.asana\.com\/\d+\/\d+\/(\d+)|#(\d+))/i
+ issue_finder = %r{(fix\w*|clos[ei]\w*+)?\W*(?:https://app\.asana\.com/\d+/\d+/(\d+)|#(\d+))}i
message.scan(issue_finder).each do |tuple|
# tuple will be
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index 31984c5d7ed..5fb15c383ca 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -10,9 +10,9 @@ class IssueTrackerService < Service
# overriden patterns. See ReferenceRegexes::EXTERNAL_PATTERN
def self.reference_pattern(only_long: false)
if only_long
- %r{(\b[A-Z][A-Z0-9_]+-)(?<issue>\d+)}
+ /(\b[A-Z][A-Z0-9_]+-)(?<issue>\d+)/
else
- %r{(\b[A-Z][A-Z0-9_]+-|#{Issue.reference_prefix})(?<issue>\d+)}
+ /(\b[A-Z][A-Z0-9_]+-|#{Issue.reference_prefix})(?<issue>\d+)/
end
end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 23147d7f666..30eafe31454 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -19,7 +19,7 @@ class JiraService < IssueTrackerService
# {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1
def self.reference_pattern(only_long: true)
- @reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)}
+ @reference_pattern ||= /(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)/
end
def initialize_properties
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
index c72b01b64af..ad4ad7903ad 100644
--- a/app/models/project_services/kubernetes_service.rb
+++ b/app/models/project_services/kubernetes_service.rb
@@ -4,7 +4,6 @@
# After we've migrated data, we'll remove KubernetesService. This would happen in a few months.
# If you're modyfiyng this class, please note that you should update the same change in Clusters::Platforms::Kubernetes.
class KubernetesService < DeploymentService
- include Gitlab::CurrentSettings
include Gitlab::Kubernetes
include ReactiveCaching
@@ -151,9 +150,10 @@ class KubernetesService < DeploymentService
end
def deprecation_message
- content = <<-MESSAGE.strip_heredoc
- Kubernetes service integration has been deprecated. #{deprecated_message_content} your clusters using the new <a href=\'#{Gitlab::Routing.url_helpers.project_clusters_path(project)}'/>Clusters</a> page
- MESSAGE
+ content = _("Kubernetes service integration has been deprecated. %{deprecated_message_content} your Kubernetes clusters using the new <a href=\"%{url}\"/>Kubernetes Clusters</a> page") % {
+ deprecated_message_content: deprecated_message_content,
+ url: Gitlab::Routing.url_helpers.project_clusters_path(project)
+ }
content.html_safe
end
@@ -231,7 +231,7 @@ class KubernetesService < DeploymentService
{
token: token,
ca_pem: ca_pem,
- max_session_time: current_application_settings.terminal_max_session_time
+ max_session_time: Gitlab::CurrentSettings.terminal_max_session_time
}
end
@@ -249,9 +249,9 @@ class KubernetesService < DeploymentService
def deprecated_message_content
if active?
- "Your cluster information on this page is still editable, but you are advised to disable and reconfigure"
+ _("Your Kubernetes cluster information on this page is still editable, but you are advised to disable and reconfigure")
else
- "Fields on this page are now uneditable, you can configure"
+ _("Fields on this page are now uneditable, you can configure")
end
end
end
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index 459d1673125..f6041da986c 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -119,6 +119,8 @@ class ProjectWiki
end
def delete_page(page, message = nil)
+ return unless page
+
wiki.delete_page(page.path, commit_details(:deleted, message, page.title))
update_project_activity
@@ -131,6 +133,8 @@ class ProjectWiki
end
def page_title_and_dir(title)
+ return unless title
+
title_array = title.split("/")
title = title_array.pop
[title, title_array.join("/")]
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index d28fed11ca8..609780c5587 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -2,8 +2,6 @@ class ProtectedBranch < ActiveRecord::Base
include Gitlab::ShellAdapter
include ProtectedRef
- extend Gitlab::CurrentSettings
-
protected_ref_access_levels :merge, :push
# Check if branch name is marked as protected in the system
@@ -16,7 +14,7 @@ class ProtectedBranch < ActiveRecord::Base
end
def self.default_branch_protected?
- current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL ||
- current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE
+ Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_FULL ||
+ Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE
end
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index edfb236a91a..3d6f8f0c305 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -93,6 +93,10 @@ class Repository
alias_method :raw, :raw_repository
+ def cleanup
+ @raw_repository&.cleanup
+ end
+
# Return absolute path to repository
def path_to_repo
@path_to_repo ||= File.expand_path(
@@ -173,15 +177,7 @@ class Repository
end
def find_branch(name, fresh_repo: true)
- # Since the Repository object may have in-memory index changes, invalidating the memoized Repository object may
- # cause unintended side effects. Because finding a branch is a read-only operation, we can safely instantiate
- # a new repo here to ensure a consistent state to avoid a libgit2 bug where concurrent access (e.g. via git gc)
- # may cause the branch to "disappear" erroneously or have the wrong SHA.
- #
- # See: https://github.com/libgit2/libgit2/issues/1534 and https://gitlab.com/gitlab-org/gitlab-ce/issues/15392
- raw_repo = fresh_repo ? initialize_raw_repository : raw_repository
-
- raw_repo.find_branch(name)
+ raw_repository.find_branch(name, fresh_repo)
end
def find_tag(name)
@@ -721,11 +717,11 @@ class Repository
end
def branch_names_contains(sha)
- refs_contains_sha('branch', sha)
+ raw_repository.branch_names_contains_sha(sha)
end
def tag_names_contains(sha)
- refs_contains_sha('tag', sha)
+ raw_repository.tag_names_contains_sha(sha)
end
def local_branches
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 05a16f11b59..7c8716f8c18 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -11,8 +11,6 @@ class Snippet < ActiveRecord::Base
include Editable
include Gitlab::SQL::Pattern
- extend Gitlab::CurrentSettings
-
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description
cache_markdown_field :content
@@ -28,7 +26,7 @@ class Snippet < ActiveRecord::Base
default_content_html_invalidator || file_name_changed?
end
- default_value_for(:visibility_level) { current_application_settings.default_snippet_visibility }
+ default_value_for(:visibility_level) { Gitlab::CurrentSettings.default_snippet_visibility }
belongs_to :author, class_name: 'User'
belongs_to :project
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 7af54b2beb2..bb5965e20eb 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -28,6 +28,7 @@ class Todo < ActiveRecord::Base
delegate :name, :email, to: :author, prefix: true, allow_nil: true
validates :action, :project, :target_type, :user, presence: true
+ validates :author, presence: true
validates :target_id, presence: true, unless: :for_commit?
validates :commit_id, presence: true, if: :for_commit?
diff --git a/app/models/upload.rb b/app/models/upload.rb
index f194d7bdb80..99ad37dc892 100644
--- a/app/models/upload.rb
+++ b/app/models/upload.rb
@@ -9,22 +9,15 @@ class Upload < ActiveRecord::Base
validates :model, presence: true
validates :uploader, presence: true
- before_save :calculate_checksum, if: :foreground_checksum?
- after_commit :schedule_checksum, unless: :foreground_checksum?
+ before_save :calculate_checksum!, if: :foreground_checksummable?
+ after_commit :schedule_checksum, if: :checksummable?
- def self.remove_path(path)
- where(path: path).destroy_all
- end
-
- def self.record(uploader)
- remove_path(uploader.relative_path)
+ # as the FileUploader is not mounted, the default CarrierWave ActiveRecord
+ # hooks are not executed and the file will not be deleted
+ after_destroy :delete_file!, if: -> { uploader_class <= FileUploader }
- create(
- size: uploader.file.size,
- path: uploader.relative_path,
- model: uploader.model,
- uploader: uploader.class.to_s
- )
+ def self.hexdigest(path)
+ Digest::SHA256.file(path).hexdigest
end
def absolute_path
@@ -33,20 +26,47 @@ class Upload < ActiveRecord::Base
uploader_class.absolute_path(self)
end
- def calculate_checksum
- return unless exist?
+ def calculate_checksum!
+ self.checksum = nil
+ return unless checksummable?
+
+ self.checksum = self.class.hexdigest(absolute_path)
+ end
- self.checksum = Digest::SHA256.file(absolute_path).hexdigest
+ def build_uploader
+ uploader_class.new(model, mount_point, **uploader_context).tap do |uploader|
+ uploader.upload = self
+ uploader.retrieve_from_store!(identifier)
+ end
end
def exist?
File.exist?(absolute_path)
end
+ def uploader_context
+ {
+ identifier: identifier,
+ secret: secret
+ }.compact
+ end
+
private
- def foreground_checksum?
- size <= CHECKSUM_THRESHOLD
+ def delete_file!
+ build_uploader.remove!
+ end
+
+ def checksummable?
+ checksum.nil? && local? && exist?
+ end
+
+ def local?
+ true
+ end
+
+ def foreground_checksummable?
+ checksummable? && size <= CHECKSUM_THRESHOLD
end
def schedule_checksum
@@ -60,4 +80,12 @@ class Upload < ActiveRecord::Base
def uploader_class
Object.const_get(uploader)
end
+
+ def identifier
+ File.basename(path)
+ end
+
+ def mount_point
+ super&.to_sym
+ end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 9403da98268..1df3772e63f 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -2,10 +2,8 @@ require 'carrierwave/orm/activerecord'
class User < ActiveRecord::Base
extend Gitlab::ConfigHelper
- extend Gitlab::CurrentSettings
include Gitlab::ConfigHelper
- include Gitlab::CurrentSettings
include Gitlab::SQL::Pattern
include AfterCommitQueue
include Avatarable
@@ -30,7 +28,7 @@ class User < ActiveRecord::Base
add_authentication_token_field :rss_token
default_value_for :admin, false
- default_value_for(:external) { current_application_settings.user_default_external }
+ default_value_for(:external) { Gitlab::CurrentSettings.user_default_external }
default_value_for :can_create_group, gitlab_config.default_can_create_group
default_value_for :can_create_team, false
default_value_for :hide_no_ssh_key, false
@@ -79,7 +77,7 @@ class User < ActiveRecord::Base
#
# Namespace for personal projects
- has_one :namespace, -> { where(type: nil) }, dependent: :destroy, foreign_key: :owner_id, autosave: true # rubocop:disable Cop/ActiveRecordDependent
+ has_one :namespace, -> { where(type: nil) }, dependent: :destroy, foreign_key: :owner_id, inverse_of: :owner, autosave: true # rubocop:disable Cop/ActiveRecordDependent
# Profile
has_many :keys, -> do
@@ -127,7 +125,7 @@ class User < ActiveRecord::Base
has_many :spam_logs, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :builds, dependent: :nullify, class_name: 'Ci::Build' # rubocop:disable Cop/ActiveRecordDependent
has_many :pipelines, dependent: :nullify, class_name: 'Ci::Pipeline' # rubocop:disable Cop/ActiveRecordDependent
- has_many :todos, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :todos
has_many :notification_settings, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :award_emoji, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :owner_id # rubocop:disable Cop/ActiveRecordDependent
@@ -137,6 +135,8 @@ class User < ActiveRecord::Base
has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" # rubocop:disable Cop/ActiveRecordDependent
has_many :custom_attributes, class_name: 'UserCustomAttribute'
+ has_many :callouts, class_name: 'UserCallout'
+ has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
#
# Validations
@@ -159,12 +159,10 @@ class User < ActiveRecord::Base
validate :namespace_uniq, if: :username_changed?
validate :namespace_move_dir_allowed, if: :username_changed?
- validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validate :unique_email, if: :email_changed?
validate :owns_notification_email, if: :notification_email_changed?
validate :owns_public_email, if: :public_email_changed?
validate :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id }
- validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
before_validation :sanitize_attrs
before_validation :set_notification_email, if: :email_changed?
@@ -173,7 +171,7 @@ class User < ActiveRecord::Base
before_save :ensure_user_rights_and_limits, if: ->(user) { user.new_record? || user.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
+ before_validation :ensure_namespace_correct
after_update :username_changed_hook, if: :username_changed?
after_destroy :post_destroy_hook
after_destroy :remove_key_cache
@@ -225,9 +223,6 @@ class User < ActiveRecord::Base
end
end
- mount_uploader :avatar, AvatarUploader
- has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
-
# Scopes
scope :admins, -> { where(admin: true) }
scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
@@ -235,8 +230,8 @@ class User < ActiveRecord::Base
scope :active, -> { with_state(:active).non_internal }
scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') }
scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
- scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('last_sign_in_at', 'DESC')) }
- scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('last_sign_in_at', 'ASC')) }
+ scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) }
+ scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) }
def self.with_two_factor
joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id")
@@ -527,12 +522,6 @@ class User < ActiveRecord::Base
end
end
- def avatar_type
- unless avatar.image?
- errors.add :avatar, "only images allowed"
- end
- end
-
def unique_email
if !emails.exists?(email: email) && Email.exists?(email: email)
errors.add(:email, 'has already been taken')
@@ -670,11 +659,11 @@ class User < ActiveRecord::Base
end
def allow_password_authentication_for_web?
- current_application_settings.password_authentication_enabled_for_web? && !ldap_user?
+ Gitlab::CurrentSettings.password_authentication_enabled_for_web? && !ldap_user?
end
def allow_password_authentication_for_git?
- current_application_settings.password_authentication_enabled_for_git? && !ldap_user?
+ Gitlab::CurrentSettings.password_authentication_enabled_for_git? && !ldap_user?
end
def can_change_username?
@@ -802,7 +791,7 @@ class User < ActiveRecord::Base
# without this safeguard!
return unless has_attribute?(:projects_limit) && projects_limit.nil?
- self.projects_limit = current_application_settings.default_projects_limit
+ self.projects_limit = Gitlab::CurrentSettings.default_projects_limit
end
def requires_ldap_check?
@@ -842,13 +831,13 @@ class User < ActiveRecord::Base
end
def full_website_url
- return "http://#{website_url}" if website_url !~ /\Ahttps?:\/\//
+ return "http://#{website_url}" if website_url !~ %r{\Ahttps?://}
website_url
end
def short_website_url
- website_url.sub(/\Ahttps?:\/\//, '')
+ website_url.sub(%r{\Ahttps?://}, '')
end
def all_ssh_keys
@@ -860,9 +849,7 @@ class User < ActiveRecord::Base
end
def avatar_url(size: nil, scale: 2, **args)
- # We use avatar_path instead of overriding avatar_url because of carrierwave.
- # See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11001/diffs#note_28659864
- avatar_path(args) || GravatarService.new.execute(email, size, scale, username: username)
+ GravatarService.new.execute(email, size, scale, username: username)
end
def primary_email_verified?
@@ -897,16 +884,10 @@ class User < ActiveRecord::Base
end
def ensure_namespace_correct
- # Ensure user has namespace
- create_namespace!(path: username, name: username) unless namespace
-
- if username_changed?
- unless namespace.update_attributes(path: username, name: username)
- namespace.errors.each do |attribute, message|
- self.errors.add(:"namespace_#{attribute}", message)
- end
- raise ActiveRecord::RecordInvalid.new(namespace)
- end
+ if namespace
+ namespace.path = namespace.name = username if username_changed?
+ else
+ build_namespace(path: username, name: username)
end
end
@@ -1227,7 +1208,7 @@ class User < ActiveRecord::Base
else
# Only revert these back to the default if they weren't specifically changed in this update.
self.can_create_group = gitlab_config.default_can_create_group unless can_create_group_changed?
- self.projects_limit = current_application_settings.default_projects_limit unless projects_limit_changed?
+ self.projects_limit = Gitlab::CurrentSettings.default_projects_limit unless projects_limit_changed?
end
end
@@ -1235,15 +1216,15 @@ class User < ActiveRecord::Base
valid = true
error = nil
- if current_application_settings.domain_blacklist_enabled?
- blocked_domains = current_application_settings.domain_blacklist
+ if Gitlab::CurrentSettings.domain_blacklist_enabled?
+ blocked_domains = Gitlab::CurrentSettings.domain_blacklist
if domain_matches?(blocked_domains, email)
error = 'is not from an allowed domain.'
valid = false
end
end
- allowed_domains = current_application_settings.domain_whitelist
+ allowed_domains = Gitlab::CurrentSettings.domain_whitelist
unless allowed_domains.blank?
if domain_matches?(allowed_domains, email)
valid = true
diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb
new file mode 100644
index 00000000000..e4b69382626
--- /dev/null
+++ b/app/models/user_callout.rb
@@ -0,0 +1,13 @@
+class UserCallout < ActiveRecord::Base
+ belongs_to :user
+
+ enum feature_name: {
+ gke_cluster_integration: 1
+ }
+
+ validates :user, presence: true
+ validates :feature_name,
+ presence: true,
+ uniqueness: { scope: :user_id },
+ inclusion: { in: UserCallout.feature_names.keys }
+end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index e6254183baf..0f5536415f7 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -1,5 +1,6 @@
class WikiPage
PageChangedError = Class.new(StandardError)
+ PageRenameError = Class.new(StandardError)
include ActiveModel::Validations
include ActiveModel::Conversion
@@ -102,7 +103,7 @@ class WikiPage
# The hierarchy of the directory this page is contained in.
def directory
- wiki.page_title_and_dir(slug).last
+ wiki.page_title_and_dir(slug)&.last.to_s
end
# The processed/formatted content of this page.
@@ -177,7 +178,7 @@ class WikiPage
# Creates a new Wiki Page.
#
# attr - Hash of attributes to set on the new page.
- # :title - The title for the new page.
+ # :title - The title (optionally including dir) for the new page.
# :content - The raw markup content.
# :format - Optional symbol representing the
# content format. Can be any type
@@ -189,7 +190,7 @@ class WikiPage
# Returns the String SHA1 of the newly created page
# or False if the save was unsuccessful.
def create(attrs = {})
- @attributes.merge!(attrs)
+ update_attributes(attrs)
save(page_details: title) do
wiki.create_page(title, content, format, message)
@@ -204,24 +205,29 @@ class WikiPage
# See ProjectWiki::MARKUPS Hash for available formats.
# :message - Optional commit message to set on the new version.
# :last_commit_sha - Optional last commit sha to validate the page unchanged.
- # :title - The Title to replace existing title
+ # :title - The Title (optionally including dir) to replace existing title
#
# Returns the String SHA1 of the newly created page
# or False if the save was unsuccessful.
def update(attrs = {})
last_commit_sha = attrs.delete(:last_commit_sha)
+
if last_commit_sha && last_commit_sha != self.last_commit_sha
- raise PageChangedError.new("You are attempting to update a page that has changed since you started editing it.")
+ raise PageChangedError
end
- attrs.slice!(:content, :format, :message, :title)
- @attributes.merge!(attrs)
- page_details =
- if title.present? && @page.title != title
- title
- else
- @page.url_path
+ update_attributes(attrs)
+
+ if title_changed?
+ page_details = title
+
+ if wiki.find_page(page_details).present?
+ @attributes[:title] = @page.url_path
+ raise PageRenameError
end
+ else
+ page_details = @page.url_path
+ end
save(page_details: page_details) do
wiki.update_page(
@@ -255,8 +261,44 @@ class WikiPage
page.version.to_s
end
+ def title_changed?
+ title.present? && self.class.unhyphenize(@page.url_path) != title
+ end
+
private
+ # Process and format the title based on the user input.
+ def process_title(title)
+ return if title.blank?
+
+ title = deep_title_squish(title)
+ current_dirname = File.dirname(title)
+
+ if @page.present?
+ return title[1..-1] if current_dirname == '/'
+ return File.join([directory.presence, title].compact) if current_dirname == '.'
+ end
+
+ title
+ end
+
+ # This method squishes all the filename
+ # i.e: ' foo / bar / page_name' => 'foo/bar/page_name'
+ def deep_title_squish(title)
+ components = title.split(File::SEPARATOR).map(&:squish)
+
+ File.join(components)
+ end
+
+ # Updates the current @attributes hash by merging a hash of params
+ def update_attributes(attrs)
+ attrs[:title] = process_title(attrs[:title]) if attrs[:title].present?
+
+ attrs.slice!(:content, :format, :message, :title)
+
+ @attributes.merge!(attrs)
+ end
+
def set_attributes
attributes[:slug] = @page.url_path
attributes[:title] = @page.title
diff --git a/app/policies/ci/pipeline_schedule_policy.rb b/app/policies/ci/pipeline_schedule_policy.rb
index abcf536b2f7..dc7a4aed577 100644
--- a/app/policies/ci/pipeline_schedule_policy.rb
+++ b/app/policies/ci/pipeline_schedule_policy.rb
@@ -10,6 +10,10 @@ module Ci
can?(:developer_access) && pipeline_schedule.owned_by?(@user)
end
+ condition(:non_owner_of_schedule) do
+ !pipeline_schedule.owned_by?(@user)
+ end
+
rule { can?(:developer_access) }.policy do
enable :play_pipeline_schedule
end
@@ -19,6 +23,10 @@ module Ci
enable :admin_pipeline_schedule
end
+ rule { can?(:master_access) & non_owner_of_schedule }.policy do
+ enable :take_ownership_pipeline_schedule
+ end
+
rule { protected_ref }.prevent :play_pipeline_schedule
end
end
diff --git a/app/services/akismet_service.rb b/app/services/akismet_service.rb
index aa6f0e841c9..0521393dd27 100644
--- a/app/services/akismet_service.rb
+++ b/app/services/akismet_service.rb
@@ -1,6 +1,4 @@
class AkismetService
- include Gitlab::CurrentSettings
-
attr_accessor :owner, :text, :options
def initialize(owner, text, options = {})
@@ -41,12 +39,12 @@ class AkismetService
private
def akismet_client
- @akismet_client ||= ::Akismet::Client.new(current_application_settings.akismet_api_key,
+ @akismet_client ||= ::Akismet::Client.new(Gitlab::CurrentSettings.akismet_api_key,
Gitlab.config.gitlab.url)
end
def akismet_enabled?
- current_application_settings.akismet_enabled
+ Gitlab::CurrentSettings.akismet_enabled
end
def submit(type)
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
index f40cd2b06c8..2b77f6be72a 100644
--- a/app/services/auth/container_registry_authentication_service.rb
+++ b/app/services/auth/container_registry_authentication_service.rb
@@ -1,7 +1,5 @@
module Auth
class ContainerRegistryAuthenticationService < BaseService
- extend Gitlab::CurrentSettings
-
AUDIENCE = 'container_registry'.freeze
def execute(authentication_abilities:)
@@ -32,7 +30,7 @@ module Auth
end
def self.token_expire_at
- Time.now + current_application_settings.container_registry_token_expire_delay.minutes
+ Time.now + Gitlab::CurrentSettings.container_registry_token_expire_delay.minutes
end
private
diff --git a/app/services/base_service.rb b/app/services/base_service.rb
index a0cb00dba58..6883ba36c71 100644
--- a/app/services/base_service.rb
+++ b/app/services/base_service.rb
@@ -1,6 +1,5 @@
class BaseService
include Gitlab::Allowable
- include Gitlab::CurrentSettings
attr_accessor :project, :current_user, :params
diff --git a/app/services/ci/create_trace_artifact_service.rb b/app/services/ci/create_trace_artifact_service.rb
new file mode 100644
index 00000000000..280a2c3afa4
--- /dev/null
+++ b/app/services/ci/create_trace_artifact_service.rb
@@ -0,0 +1,16 @@
+module Ci
+ class CreateTraceArtifactService < BaseService
+ def execute(job)
+ return if job.job_artifacts_trace
+
+ job.trace.read do |stream|
+ if stream.file?
+ job.create_job_artifacts_trace!(
+ project: job.project,
+ file_type: :trace,
+ file: stream)
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb
index f832b79ef21..e09b445636f 100644
--- a/app/services/ci/register_job_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -2,8 +2,6 @@ module Ci
# This class responsible for assigning
# proper pending build to runner on runner API request
class RegisterJobService
- include Gitlab::CurrentSettings
-
attr_reader :runner
Result = Struct.new(:build, :valid?)
diff --git a/app/services/clusters/create_service.rb b/app/services/clusters/create_service.rb
index 0471b0f17a2..418888e3293 100644
--- a/app/services/clusters/create_service.rb
+++ b/app/services/clusters/create_service.rb
@@ -5,7 +5,7 @@ module Clusters
def execute(access_token = nil)
@access_token = access_token
- raise ArgumentError.new('Instance does not support multiple clusters') unless can_create_cluster?
+ raise ArgumentError.new(_('Instance does not support multiple Kubernetes clusters')) unless can_create_cluster?
create_cluster.tap do |cluster|
ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted?
diff --git a/app/services/clusters/gcp/verify_provision_status_service.rb b/app/services/clusters/gcp/verify_provision_status_service.rb
index bc33756f27c..f994aacd086 100644
--- a/app/services/clusters/gcp/verify_provision_status_service.rb
+++ b/app/services/clusters/gcp/verify_provision_status_service.rb
@@ -28,7 +28,7 @@ module Clusters
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}")
+ provider.make_errored!(_('Kubernetes cluster creation time exceeds timeout; %{timeout}') % { timeout: TIMEOUT })
end
end
diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb
index 00a8dcf0934..46acdc5406c 100644
--- a/app/services/files/create_service.rb
+++ b/app/services/files/create_service.rb
@@ -1,10 +1,20 @@
module Files
class CreateService < Files::BaseService
def create_commit!
+ handler = Lfs::FileModificationHandler.new(project, @branch_name)
+
+ handler.new_file(@file_path, @file_content) do |content_or_lfs_pointer|
+ create_transformed_commit(content_or_lfs_pointer)
+ end
+ end
+
+ private
+
+ def create_transformed_commit(content_or_lfs_pointer)
repository.create_file(
current_user,
@file_path,
- @file_content,
+ content_or_lfs_pointer,
message: @commit_message,
branch_name: @branch_name,
author_email: @author_email,
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index e6fd193ffb3..c037141fcde 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -1,6 +1,5 @@
class GitPushService < BaseService
attr_accessor :push_data, :push_commits
- include Gitlab::CurrentSettings
include Gitlab::Access
# The N most recent commits to process in a single push payload.
diff --git a/app/services/gravatar_service.rb b/app/services/gravatar_service.rb
index e77e08aa380..c6e52c3bb91 100644
--- a/app/services/gravatar_service.rb
+++ b/app/services/gravatar_service.rb
@@ -1,8 +1,6 @@
class GravatarService
- include Gitlab::CurrentSettings
-
def execute(email, size = nil, scale = 2, username: nil)
- return unless current_application_settings.gravatar_enabled?
+ return unless Gitlab::CurrentSettings.gravatar_enabled?
identifier = email.presence || username.presence
return unless identifier
diff --git a/app/services/groups/transfer_service.rb b/app/services/groups/transfer_service.rb
new file mode 100644
index 00000000000..e591c820cff
--- /dev/null
+++ b/app/services/groups/transfer_service.rb
@@ -0,0 +1,96 @@
+module Groups
+ class TransferService < Groups::BaseService
+ ERROR_MESSAGES = {
+ database_not_supported: 'Database is not supported.',
+ namespace_with_same_path: 'The parent group already has a subgroup with the same path.',
+ group_is_already_root: 'Group is already a root group.',
+ same_parent_as_current: 'Group is already associated to the parent group.',
+ invalid_policies: "You don't have enough permissions."
+ }.freeze
+
+ TransferError = Class.new(StandardError)
+
+ attr_reader :error
+
+ def initialize(group, user, params = {})
+ super
+ @error = nil
+ end
+
+ def execute(new_parent_group)
+ @new_parent_group = new_parent_group
+ ensure_allowed_transfer
+ proceed_to_transfer
+
+ rescue TransferError, ActiveRecord::RecordInvalid, Gitlab::UpdatePathError => e
+ @group.errors.clear
+ @error = "Transfer failed: " + e.message
+ false
+ end
+
+ private
+
+ def proceed_to_transfer
+ Group.transaction do
+ update_group_attributes
+ end
+ end
+
+ def ensure_allowed_transfer
+ raise_transfer_error(:group_is_already_root) if group_is_already_root?
+ raise_transfer_error(:database_not_supported) unless Group.supports_nested_groups?
+ raise_transfer_error(:same_parent_as_current) if same_parent?
+ raise_transfer_error(:invalid_policies) unless valid_policies?
+ raise_transfer_error(:namespace_with_same_path) if namespace_with_same_path?
+ end
+
+ def group_is_already_root?
+ !@new_parent_group && !@group.has_parent?
+ end
+
+ def same_parent?
+ @new_parent_group && @new_parent_group.id == @group.parent_id
+ end
+
+ def valid_policies?
+ return false unless can?(current_user, :admin_group, @group)
+
+ if @new_parent_group
+ can?(current_user, :create_subgroup, @new_parent_group)
+ else
+ can?(current_user, :create_group)
+ end
+ end
+
+ def namespace_with_same_path?
+ Namespace.exists?(path: @group.path, parent: @new_parent_group)
+ end
+
+ def update_group_attributes
+ if @new_parent_group && @new_parent_group.visibility_level < @group.visibility_level
+ update_children_and_projects_visibility
+ @group.visibility_level = @new_parent_group.visibility_level
+ end
+
+ @group.parent = @new_parent_group
+ @group.save!
+ end
+
+ def update_children_and_projects_visibility
+ descendants = @group.descendants.where("visibility_level > ?", @new_parent_group.visibility_level)
+
+ Group
+ .where(id: descendants.select(:id))
+ .update_all(visibility_level: @new_parent_group.visibility_level)
+
+ @group
+ .all_projects
+ .where("visibility_level > ?", @new_parent_group.visibility_level)
+ .update_all(visibility_level: @new_parent_group.visibility_level)
+ end
+
+ def raise_transfer_error(message)
+ raise TransferError, ERROR_MESSAGES[message]
+ end
+ end
+end
diff --git a/app/services/lfs/file_modification_handler.rb b/app/services/lfs/file_modification_handler.rb
new file mode 100644
index 00000000000..fe9091a6e5d
--- /dev/null
+++ b/app/services/lfs/file_modification_handler.rb
@@ -0,0 +1,42 @@
+module Lfs
+ class FileModificationHandler
+ attr_reader :project, :branch_name
+
+ delegate :repository, to: :project
+
+ def initialize(project, branch_name)
+ @project = project
+ @branch_name = branch_name
+ end
+
+ def new_file(file_path, file_content)
+ if project.lfs_enabled? && lfs_file?(file_path)
+ lfs_pointer_file = Gitlab::Git::LfsPointerFile.new(file_content)
+ lfs_object = create_lfs_object!(lfs_pointer_file, file_content)
+ content = lfs_pointer_file.pointer
+
+ success = yield(content)
+
+ link_lfs_object!(lfs_object) if success
+ else
+ yield(file_content)
+ end
+ end
+
+ private
+
+ def lfs_file?(file_path)
+ repository.attributes_at(branch_name, file_path)['filter'] == 'lfs'
+ end
+
+ def create_lfs_object!(lfs_pointer_file, file_content)
+ LfsObject.find_or_create_by(oid: lfs_pointer_file.sha256, size: lfs_pointer_file.size) do |lfs_object|
+ lfs_object.file = CarrierWaveStringFile.new(file_content)
+ end
+ end
+
+ def link_lfs_object!(lfs_object)
+ project.lfs_objects << lfs_object
+ end
+ end
+end
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index 22b9b91a957..2ae855d078b 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -1,7 +1,9 @@
module MergeRequests
class BuildService < MergeRequests::BaseService
+ include Gitlab::Utils::StrongMemoize
+
def execute
- @issue_iid = params.delete(:issue_iid)
+ @params_issue_iid = params.delete(:issue_iid)
self.merge_request = MergeRequest.new(params)
merge_request.compare_commits = []
@@ -123,7 +125,7 @@ module MergeRequests
#
def assign_title_and_description
assign_title_and_description_from_single_commit
- assign_title_from_issue
+ assign_title_from_issue if target_project.issues_enabled? || target_project.external_issue_tracker
merge_request.title ||= source_branch.titleize.humanize
merge_request.title = wip_title if compare_commits.empty?
@@ -132,9 +134,9 @@ module MergeRequests
end
def append_closes_description
- return unless issue_iid
+ return unless issue
- closes_issue = "Closes ##{issue_iid}"
+ closes_issue = "Closes #{issue.to_reference}"
if description.present?
merge_request.description += closes_issue.prepend("\n\n")
@@ -154,13 +156,27 @@ module MergeRequests
end
def assign_title_from_issue
- return unless issue && issue.is_a?(Issue)
+ return unless issue
+
+ merge_request.title = "Resolve \"#{issue.title}\"" if issue.is_a?(Issue)
- merge_request.title = "Resolve \"#{issue.title}\""
+ unless merge_request.title
+ branch_title = source_branch.downcase.remove(issue_iid.downcase).titleize.humanize
+ merge_request.title = "Resolve #{issue_iid}"
+ merge_request.title += " \"#{branch_title}\"" unless branch_title.empty?
+ end
end
def issue_iid
- @issue_iid ||= source_branch.match(/\A(\d+)-/).try(:[], 1)
+ strong_memoize(:issue_iid) do
+ @params_issue_iid || begin
+ id = if target_project.external_issue_tracker
+ source_branch.match(target_project.external_issue_reference_pattern).try(:[], 0)
+ end
+
+ id || source_branch.match(/\A(\d+)-/).try(:[], 1)
+ end
+ end
end
def issue
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 262622f8bd0..18c40ce8992 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -90,6 +90,10 @@ module MergeRequests
merge_request.mark_as_unchecked
UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
end
+
+ # Upcoming method calls need the refreshed version of
+ # @source_merge_requests diffs (for MergeRequest#commit_shas for instance).
+ merge_requests_for_source_branch(reload: true)
end
def reset_merge_when_pipeline_succeeds
@@ -195,7 +199,8 @@ module MergeRequests
merge_requests.uniq.select(&:source_project)
end
- def merge_requests_for_source_branch
+ def merge_requests_for_source_branch(reload: false)
+ @source_merge_requests = nil if reload
@source_merge_requests ||= merge_requests_for(@branch_name)
end
diff --git a/app/services/projects/hashed_storage/migrate_attachments_service.rb b/app/services/projects/hashed_storage/migrate_attachments_service.rb
index f8aaec8a9c0..bc897d891d5 100644
--- a/app/services/projects/hashed_storage/migrate_attachments_service.rb
+++ b/app/services/projects/hashed_storage/migrate_attachments_service.rb
@@ -14,9 +14,9 @@ module Projects
@old_path = project.full_path
@new_path = project.disk_path
- origin = FileUploader.dynamic_path_segment(project)
+ origin = FileUploader.absolute_base_dir(project)
project.storage_version = ::Project::HASHED_STORAGE_FEATURES[:attachments]
- target = FileUploader.dynamic_path_segment(project)
+ target = FileUploader.absolute_base_dir(project)
result = move_folder!(origin, target)
project.save!
diff --git a/app/services/projects/housekeeping_service.rb b/app/services/projects/housekeeping_service.rb
index dcef8b66215..120d57a188d 100644
--- a/app/services/projects/housekeeping_service.rb
+++ b/app/services/projects/housekeeping_service.rb
@@ -7,8 +7,6 @@
#
module Projects
class HousekeepingService < BaseService
- include Gitlab::CurrentSettings
-
# Timeout set to 24h
LEASE_TIMEOUT = 86400
@@ -83,19 +81,19 @@ module Projects
end
def housekeeping_enabled?
- current_application_settings.housekeeping_enabled
+ Gitlab::CurrentSettings.housekeeping_enabled
end
def gc_period
- current_application_settings.housekeeping_gc_period
+ Gitlab::CurrentSettings.housekeeping_gc_period
end
def full_repack_period
- current_application_settings.housekeeping_full_repack_period
+ Gitlab::CurrentSettings.housekeeping_full_repack_period
end
def repack_period
- current_application_settings.housekeeping_incremental_repack_period
+ Gitlab::CurrentSettings.housekeeping_incremental_repack_period
end
end
end
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index a773222bf17..c760bd3b626 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -1,7 +1,5 @@
module Projects
class UpdatePagesService < BaseService
- include Gitlab::CurrentSettings
-
BLOCK_SIZE = 32.kilobytes
MAX_SIZE = 1.terabyte
SITE_PATH = 'public/'.freeze
@@ -134,7 +132,7 @@ module Projects
end
def max_size
- max_pages_size = current_application_settings.max_pages_size.megabytes
+ max_pages_size = Gitlab::CurrentSettings.max_pages_size.megabytes
return MAX_SIZE if max_pages_size.zero?
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index ff4c73c886e..0e235a6d2a0 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -34,7 +34,7 @@ module Projects
def run_auto_devops_pipeline?
return false if project.repository.gitlab_ci_yml || !project.auto_devops.previous_changes.include?('enabled')
- project.auto_devops.enabled? || (project.auto_devops.enabled.nil? && current_application_settings.auto_devops_enabled?)
+ project.auto_devops.enabled? || (project.auto_devops.enabled.nil? && Gitlab::CurrentSettings.auto_devops_enabled?)
end
private
diff --git a/app/services/submit_usage_ping_service.rb b/app/services/submit_usage_ping_service.rb
index 14171bce782..2623f253d98 100644
--- a/app/services/submit_usage_ping_service.rb
+++ b/app/services/submit_usage_ping_service.rb
@@ -11,10 +11,8 @@ class SubmitUsagePingService
percentage_projects_prometheus_active leader_service_desk_issues instance_service_desk_issues
percentage_service_desk_issues].freeze
- include Gitlab::CurrentSettings
-
def execute
- return false unless current_application_settings.usage_ping_enabled?
+ return false unless Gitlab::CurrentSettings.usage_ping_enabled?
response = HTTParty.post(
URL,
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 06b23cd7076..2253d638e93 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -22,8 +22,7 @@ module SystemNoteService
commits_text = "#{total_count} commit".pluralize(total_count)
body = "added #{commits_text}\n\n"
- body << existing_commit_summary(noteable, existing_commits, oldrev)
- body << new_commit_summary(new_commits).join("\n")
+ body << commits_list(noteable, new_commits, existing_commits, oldrev)
body << "\n\n[Compare with previous version](#{diff_comparison_url(noteable, project, oldrev)})"
create_note(NoteSummary.new(noteable, project, author, body, action: 'commit', commit_count: total_count))
@@ -481,7 +480,7 @@ module SystemNoteService
# Returns an Array of Strings
def new_commit_summary(new_commits)
new_commits.collect do |commit|
- "* #{commit.short_id} - #{escape_html(commit.title)}"
+ content_tag('li', "#{commit.short_id} - #{commit.title}")
end
end
@@ -604,6 +603,16 @@ module SystemNoteService
"#{cross_reference_note_prefix}#{gfm_reference}"
end
+ # Builds a list of existing and new commits according to existing_commits and
+ # new_commits methods.
+ # Returns a String wrapped in `ul` and `li` tags.
+ def commits_list(noteable, new_commits, existing_commits, oldrev)
+ existing_commit_summary = existing_commit_summary(noteable, existing_commits, oldrev)
+ new_commit_summary = new_commit_summary(new_commits).join
+
+ content_tag('ul', "#{existing_commit_summary}#{new_commit_summary}".html_safe)
+ end
+
# Build a single line summarizing existing commits being added in a merge
# request
#
@@ -640,11 +649,8 @@ module SystemNoteService
branch = noteable.target_branch
branch = "#{noteable.target_project_namespace}:#{branch}" if noteable.for_fork?
- "* #{commit_ids} - #{commits_text} from branch `#{branch}`\n"
- end
-
- def escape_html(text)
- Rack::Utils.escape_html(text)
+ branch_name = content_tag('code', branch)
+ content_tag('li', "#{commit_ids} - #{commits_text} from branch #{branch_name}".html_safe)
end
def url_helpers
@@ -661,4 +667,8 @@ module SystemNoteService
start_sha: oldrev
)
end
+
+ def content_tag(*args)
+ ActionController::Base.helpers.content_tag(*args)
+ end
end
diff --git a/app/services/upload_service.rb b/app/services/upload_service.rb
index 76700dfcdee..d5a9b344905 100644
--- a/app/services/upload_service.rb
+++ b/app/services/upload_service.rb
@@ -1,6 +1,4 @@
class UploadService
- include Gitlab::CurrentSettings
-
def initialize(model, file, uploader_class = FileUploader)
@model, @file, @uploader_class = model, file, uploader_class
end
@@ -17,6 +15,6 @@ class UploadService
private
def max_attachment_size
- current_application_settings.max_attachment_size.megabytes.to_i
+ Gitlab::CurrentSettings.max_attachment_size.megabytes.to_i
end
end
diff --git a/app/services/users/build_service.rb b/app/services/users/build_service.rb
index 61f1568f366..4fb6d221909 100644
--- a/app/services/users/build_service.rb
+++ b/app/services/users/build_service.rb
@@ -1,7 +1,5 @@
module Users
class BuildService < BaseService
- include Gitlab::CurrentSettings
-
def initialize(current_user, params = {})
@current_user = current_user
@params = params.dup
@@ -34,7 +32,7 @@ module Users
private
def can_create_user?
- (current_user.nil? && current_application_settings.allow_signup?) || current_user&.admin?
+ (current_user.nil? && Gitlab::CurrentSettings.allow_signup?) || current_user&.admin?
end
# Allowed params for creating a user (admins only)
@@ -102,7 +100,7 @@ module Users
end
def skip_user_confirmation_email_from_setting
- !current_application_settings.send_user_confirmation_email
+ !Gitlab::CurrentSettings.send_user_confirmation_email
end
end
end
diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb
index 109eb2fea0b..4930fb2fca7 100644
--- a/app/uploaders/attachment_uploader.rb
+++ b/app/uploaders/attachment_uploader.rb
@@ -1,10 +1,12 @@
class AttachmentUploader < GitlabUploader
- include RecordsUploads
include UploaderHelper
+ include RecordsUploads::Concern
storage :file
- def store_dir
- "#{base_dir}/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
+ private
+
+ def dynamic_segment
+ File.join(model.class.to_s.underscore, mounted_as.to_s, model.id.to_s)
end
end
diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb
index cbb79376d5f..5c8e1cea62e 100644
--- a/app/uploaders/avatar_uploader.rb
+++ b/app/uploaders/avatar_uploader.rb
@@ -1,25 +1,24 @@
class AvatarUploader < GitlabUploader
- include RecordsUploads
include UploaderHelper
+ include RecordsUploads::Concern
storage :file
- def store_dir
- "#{base_dir}/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
- end
-
def exists?
model.avatar.file && model.avatar.file.present?
end
- # We set move_to_store and move_to_cache to 'false' to prevent stealing
- # the avatar file from a project when forking it.
- # https://gitlab.com/gitlab-org/gitlab-ce/issues/26158
- def move_to_store
+ def move_to_cache
false
end
- def move_to_cache
+ def move_to_store
false
end
+
+ private
+
+ def dynamic_segment
+ File.join(model.class.to_s.underscore, mounted_as.to_s, model.id.to_s)
+ end
end
diff --git a/app/uploaders/file_mover.rb b/app/uploaders/file_mover.rb
index 00c2888d224..8f56f09c9f7 100644
--- a/app/uploaders/file_mover.rb
+++ b/app/uploaders/file_mover.rb
@@ -21,7 +21,8 @@ class FileMover
end
def update_markdown
- updated_text = model.read_attribute(update_field).gsub(temp_file_uploader.to_markdown, uploader.to_markdown)
+ updated_text = model.read_attribute(update_field)
+ .gsub(temp_file_uploader.markdown_link, uploader.markdown_link)
model.update_attribute(update_field, updated_text)
true
@@ -48,11 +49,11 @@ class FileMover
end
def uploader
- @uploader ||= PersonalFileUploader.new(model, secret)
+ @uploader ||= PersonalFileUploader.new(model, secret: secret)
end
def temp_file_uploader
- @temp_file_uploader ||= PersonalFileUploader.new(nil, secret)
+ @temp_file_uploader ||= PersonalFileUploader.new(nil, secret: secret)
end
def revert
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index 0b591e3bbbb..bde1161dfa8 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -1,23 +1,40 @@
+# This class breaks the actual CarrierWave concept.
+# Every uploader should use a base_dir that is model agnostic so we can build
+# back URLs from base_dir-relative paths saved in the `Upload` model.
+#
+# As the `.base_dir` is model dependent and **not** saved in the upload model (see #upload_path)
+# there is no way to build back the correct file path without the model, which defies
+# CarrierWave way of storing files.
+#
class FileUploader < GitlabUploader
- include RecordsUploads
include UploaderHelper
+ include RecordsUploads::Concern
MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)}
+ DYNAMIC_PATH_PATTERN = %r{(?<secret>\h{32})/(?<identifier>.*)}
storage :file
- def self.absolute_path(upload_record)
+ after :remove, :prune_store_dir
+
+ def self.root
+ File.join(options.storage_path, 'uploads')
+ end
+
+ def self.absolute_path(upload)
File.join(
- self.dynamic_path_segment(upload_record.model),
- upload_record.path
+ absolute_base_dir(upload.model),
+ upload.path # already contain the dynamic_segment, see #upload_path
)
end
- # Not using `GitlabUploader.base_dir` because all project namespaces are in
- # the `public/uploads` dir.
- #
- def self.base_dir
- root_dir
+ def self.base_dir(model)
+ model_path_segment(model)
+ end
+
+ # used in migrations and import/exports
+ def self.absolute_base_dir(model)
+ File.join(root, base_dir(model))
end
# Returns the part of `store_dir` that can change based on the model's current
@@ -29,63 +46,116 @@ class FileUploader < GitlabUploader
# model - Object that responds to `full_path` and `disk_path`
#
# Returns a String without a trailing slash
- def self.dynamic_path_segment(model)
+ def self.model_path_segment(model)
if model.hashed_storage?(:attachments)
- dynamic_path_builder(model.disk_path)
+ model.disk_path
else
- dynamic_path_builder(model.full_path)
+ model.full_path
end
end
- # Auxiliary method to build dynamic path segment when not using a project model
- #
- # Prefer to use the `.dynamic_path_segment` as it includes Hashed Storage specific logic
- def self.dynamic_path_builder(path)
- File.join(CarrierWave.root, base_dir, path)
+ def self.upload_path(secret, identifier)
+ File.join(secret, identifier)
+ end
+
+ def self.generate_secret
+ SecureRandom.hex
end
attr_accessor :model
- attr_reader :secret
- def initialize(model, secret = nil)
+ def initialize(model, mounted_as = nil, **uploader_context)
+ super(model, nil, **uploader_context)
+
@model = model
- @secret = secret || generate_secret
+ apply_context!(uploader_context)
end
- def store_dir
- File.join(dynamic_path_segment, @secret)
+ def base_dir
+ self.class.base_dir(@model)
end
- def relative_path
- self.file.path.sub("#{dynamic_path_segment}/", '')
+ # we don't need to know the actual path, an uploader instance should be
+ # able to yield the file content on demand, so we should build the digest
+ def absolute_path
+ self.class.absolute_path(@upload)
end
- def to_markdown
- to_h[:markdown]
+ def upload_path
+ self.class.upload_path(dynamic_segment, identifier)
end
- def to_h
- filename = image_or_video? ? self.file.basename : self.file.filename
- escaped_filename = filename.gsub("]", "\\]")
+ def model_path_segment
+ self.class.model_path_segment(@model)
+ end
+
+ def store_dir
+ File.join(base_dir, dynamic_segment)
+ end
- markdown = "[#{escaped_filename}](#{secure_url})"
+ def markdown_link
+ markdown = "[#{markdown_name}](#{secure_url})"
markdown.prepend("!") if image_or_video? || dangerous?
+ markdown
+ end
+ def to_h
{
- alt: filename,
+ alt: markdown_name,
url: secure_url,
- markdown: markdown
+ markdown: markdown_link
}
end
+ def filename
+ self.file.filename
+ end
+
+ def upload=(value)
+ super
+
+ return unless value
+ return if apply_context!(value.uploader_context)
+
+ # fallback to the regex based extraction
+ if matches = DYNAMIC_PATH_PATTERN.match(value.path)
+ @secret = matches[:secret]
+ @identifier = matches[:identifier]
+ end
+ end
+
+ def secret
+ @secret ||= self.class.generate_secret
+ end
+
private
- def dynamic_path_segment
- self.class.dynamic_path_segment(model)
+ def apply_context!(uploader_context)
+ @secret, @identifier = uploader_context.values_at(:secret, :identifier)
+
+ !!(@secret && @identifier)
end
- def generate_secret
- SecureRandom.hex
+ def build_upload
+ super.tap do |upload|
+ upload.secret = secret
+ end
+ end
+
+ def prune_store_dir
+ storage.delete_dir!(store_dir) # only remove when empty
+ end
+
+ def markdown_name
+ (image_or_video? ? File.basename(filename, File.extname(filename)) : filename).gsub("]", "\\]")
+ end
+
+ def identifier
+ @identifier ||= filename
+ end
+
+ def dynamic_segment
+ secret
end
def secure_url
diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb
index 7f72b3ce471..a9e5c028b03 100644
--- a/app/uploaders/gitlab_uploader.rb
+++ b/app/uploaders/gitlab_uploader.rb
@@ -1,64 +1,66 @@
class GitlabUploader < CarrierWave::Uploader::Base
- def self.absolute_path(upload_record)
- File.join(CarrierWave.root, upload_record.path)
- end
+ class_attribute :options
- def self.root_dir
- 'uploads'
- end
+ class << self
+ # DSL setter
+ def storage_options(options)
+ self.options = options
+ end
- # When object storage is used, keep the `root_dir` as `base_dir`.
- # The files aren't really in folders there, they just have a name.
- # The files that contain user input in their name, also contain a hash, so
- # the names are still unique
- #
- # This method is overridden in the `FileUploader`
- def self.base_dir
- return root_dir unless file_storage?
+ def root
+ options.storage_path
+ end
- File.join(root_dir, '-', 'system')
- end
+ # represent the directory namespacing at the class level
+ def base_dir
+ options.fetch('base_dir', '')
+ end
- def self.file_storage?
- self.storage == CarrierWave::Storage::File
+ def file_storage?
+ storage == CarrierWave::Storage::File
+ end
+
+ def absolute_path(upload_record)
+ File.join(root, upload_record.path)
+ end
end
+ storage_options Gitlab.config.uploads
+
delegate :base_dir, :file_storage?, to: :class
+ def initialize(model, mounted_as = nil, **uploader_context)
+ super(model, mounted_as)
+ end
+
def file_cache_storage?
cache_storage.is_a?(CarrierWave::Storage::File)
end
# Reduce disk IO
def move_to_cache
- true
+ file_storage?
end
# Reduce disk IO
def move_to_store
- true
- end
-
- # Designed to be overridden by child uploaders that have a dynamic path
- # segment -- that is, a path that changes based on mutable attributes of its
- # associated model
- #
- # For example, `FileUploader` builds the storage path based on the associated
- # project model's `path_with_namespace` value, which can change when the
- # project or its containing namespace is moved or renamed.
- def relative_path
- self.file.path.sub("#{root}/", '')
+ file_storage?
end
def exists?
file.present?
end
- # Override this if you don't want to save files by default to the Rails.root directory
+ def store_dir
+ File.join(base_dir, dynamic_segment)
+ end
+
+ def cache_dir
+ File.join(root, base_dir, 'tmp/cache')
+ end
+
def work_dir
- # Default path set by CarrierWave:
- # https://github.com/carrierwaveuploader/carrierwave/blob/v1.0.0/lib/carrierwave/uploader/cache.rb#L182
- CarrierWave.tmp_path
+ File.join(root, base_dir, 'tmp/work')
end
def filename
@@ -67,6 +69,13 @@ class GitlabUploader < CarrierWave::Uploader::Base
private
+ # Designed to be overridden by child uploaders that have a dynamic path
+ # segment -- that is, a path that changes based on mutable attributes of its
+ # associated model
+ def dynamic_segment
+ raise(NotImplementedError)
+ end
+
# To prevent files from moving across filesystems, override the default
# implementation:
# http://github.com/carrierwaveuploader/carrierwave/blob/v1.0.0/lib/carrierwave/uploader/cache.rb#L181-L183
@@ -74,6 +83,6 @@ class GitlabUploader < CarrierWave::Uploader::Base
# To be safe, keep this directory outside of the the cache directory
# because calling CarrierWave.clean_cache_files! will remove any files in
# the cache directory.
- File.join(work_dir, @cache_id, version_name.to_s, for_file)
+ File.join(work_dir, cache_id, version_name.to_s, for_file)
end
end
diff --git a/app/uploaders/job_artifact_uploader.rb b/app/uploaders/job_artifact_uploader.rb
index 15dfb5a5763..ad5385f45a4 100644
--- a/app/uploaders/job_artifact_uploader.rb
+++ b/app/uploaders/job_artifact_uploader.rb
@@ -1,13 +1,7 @@
class JobArtifactUploader < GitlabUploader
- storage :file
+ extend Workhorse::UploadPath
- def self.local_store_path
- Gitlab.config.artifacts.path
- end
-
- def self.artifacts_upload_path
- File.join(self.local_store_path, 'tmp/uploads/')
- end
+ storage_options Gitlab.config.artifacts
def size
return super if model.size.nil?
@@ -16,24 +10,18 @@ class JobArtifactUploader < GitlabUploader
end
def store_dir
- default_local_path
+ dynamic_segment
end
- def cache_dir
- File.join(self.class.local_store_path, 'tmp/cache')
- end
+ def open
+ raise 'Only File System is supported' unless file_storage?
- def work_dir
- File.join(self.class.local_store_path, 'tmp/work')
+ File.open(path, "rb") if path
end
private
- def default_local_path
- File.join(self.class.local_store_path, default_path)
- end
-
- def default_path
+ def dynamic_segment
creation_date = model.created_at.utc.strftime('%Y_%m_%d')
File.join(disk_hash[0..1], disk_hash[2..3], disk_hash,
diff --git a/app/uploaders/legacy_artifact_uploader.rb b/app/uploaders/legacy_artifact_uploader.rb
index 4f7f8a63108..28c458d3ff1 100644
--- a/app/uploaders/legacy_artifact_uploader.rb
+++ b/app/uploaders/legacy_artifact_uploader.rb
@@ -1,33 +1,15 @@
class LegacyArtifactUploader < GitlabUploader
- storage :file
+ extend Workhorse::UploadPath
- def self.local_store_path
- Gitlab.config.artifacts.path
- end
-
- def self.artifacts_upload_path
- File.join(self.local_store_path, 'tmp/uploads/')
- end
+ storage_options Gitlab.config.artifacts
def store_dir
- default_local_path
- end
-
- def cache_dir
- File.join(self.class.local_store_path, 'tmp/cache')
- end
-
- def work_dir
- File.join(self.class.local_store_path, 'tmp/work')
+ dynamic_segment
end
private
- def default_local_path
- File.join(self.class.local_store_path, default_path)
- end
-
- def default_path
+ def dynamic_segment
File.join(model.created_at.utc.strftime('%Y_%m'), model.project_id.to_s, model.id.to_s)
end
end
diff --git a/app/uploaders/lfs_object_uploader.rb b/app/uploaders/lfs_object_uploader.rb
index d11ebf0f9ca..e04c97ce179 100644
--- a/app/uploaders/lfs_object_uploader.rb
+++ b/app/uploaders/lfs_object_uploader.rb
@@ -1,19 +1,24 @@
class LfsObjectUploader < GitlabUploader
- storage :file
+ extend Workhorse::UploadPath
- def store_dir
- "#{Gitlab.config.lfs.storage_path}/#{model.oid[0, 2]}/#{model.oid[2, 2]}"
+ # LfsObject are in `tmp/upload` instead of `tmp/uploads`
+ def self.workhorse_upload_path
+ File.join(root, 'tmp/upload')
end
- def cache_dir
- "#{Gitlab.config.lfs.storage_path}/tmp/cache"
- end
+ storage_options Gitlab.config.lfs
def filename
model.oid[4..-1]
end
- def work_dir
- File.join(Gitlab.config.lfs.storage_path, 'tmp', 'work')
+ def store_dir
+ dynamic_segment
+ end
+
+ private
+
+ def dynamic_segment
+ File.join(model.oid[0, 2], model.oid[2, 2])
end
end
diff --git a/app/uploaders/namespace_file_uploader.rb b/app/uploaders/namespace_file_uploader.rb
index 672126e9ec2..993e85fbc13 100644
--- a/app/uploaders/namespace_file_uploader.rb
+++ b/app/uploaders/namespace_file_uploader.rb
@@ -1,15 +1,19 @@
class NamespaceFileUploader < FileUploader
- def self.base_dir
- File.join(root_dir, '-', 'system', 'namespace')
+ # Re-Override
+ def self.root
+ options.storage_path
end
- def self.dynamic_path_segment(model)
- dynamic_path_builder(model.id.to_s)
+ def self.base_dir(model)
+ File.join(options.base_dir, 'namespace', model_path_segment(model))
end
- private
+ def self.model_path_segment(model)
+ File.join(model.id.to_s)
+ end
- def secure_url
- File.join('/uploads', @secret, file.filename)
+ # Re-Override
+ def store_dir
+ File.join(base_dir, dynamic_segment)
end
end
diff --git a/app/uploaders/personal_file_uploader.rb b/app/uploaders/personal_file_uploader.rb
index 3298ad104ec..e7d9ecd3222 100644
--- a/app/uploaders/personal_file_uploader.rb
+++ b/app/uploaders/personal_file_uploader.rb
@@ -1,23 +1,27 @@
class PersonalFileUploader < FileUploader
- def self.dynamic_path_segment(model)
- File.join(CarrierWave.root, model_path(model))
+ # Re-Override
+ def self.root
+ options.storage_path
end
- def self.base_dir
- File.join(root_dir, '-', 'system')
+ def self.base_dir(model)
+ File.join(options.base_dir, model_path_segment(model))
end
- private
+ def self.model_path_segment(model)
+ return 'temp/' unless model
- def secure_url
- File.join(self.class.model_path(model), secret, file.filename)
+ File.join(model.class.to_s.underscore, model.id.to_s)
+ end
+
+ # Revert-Override
+ def store_dir
+ File.join(base_dir, dynamic_segment)
end
- def self.model_path(model)
- if model
- File.join("/#{base_dir}", model.class.to_s.underscore, model.id.to_s)
- else
- File.join("/#{base_dir}", 'temp')
- end
+ private
+
+ def secure_url
+ File.join('/', base_dir, secret, file.filename)
end
end
diff --git a/app/uploaders/records_uploads.rb b/app/uploaders/records_uploads.rb
index feb4f04d7b7..458928bc067 100644
--- a/app/uploaders/records_uploads.rb
+++ b/app/uploaders/records_uploads.rb
@@ -1,35 +1,62 @@
module RecordsUploads
- extend ActiveSupport::Concern
+ module Concern
+ extend ActiveSupport::Concern
- included do
- after :store, :record_upload
- before :remove, :destroy_upload
- end
+ attr_accessor :upload
- # After storing an attachment, create a corresponding Upload record
- #
- # NOTE: We're ignoring the argument passed to this callback because we want
- # the `SanitizedFile` object from `CarrierWave::Uploader::Base#file`, not the
- # `Tempfile` object the callback gets.
- #
- # Called `after :store`
- def record_upload(_tempfile = nil)
- return unless model
- return unless file_storage?
- return unless file.exists?
-
- Upload.record(self)
- end
+ included do
+ after :store, :record_upload
+ before :remove, :destroy_upload
+ end
+
+ # After storing an attachment, create a corresponding Upload record
+ #
+ # NOTE: We're ignoring the argument passed to this callback because we want
+ # the `SanitizedFile` object from `CarrierWave::Uploader::Base#file`, not the
+ # `Tempfile` object the callback gets.
+ #
+ # Called `after :store`
+ def record_upload(_tempfile = nil)
+ return unless model
+ return unless file && file.exists?
+
+ Upload.transaction do
+ uploads.where(path: upload_path).delete_all
+ upload.destroy! if upload
+
+ self.upload = build_upload
+ upload.save!
+ end
+ end
+
+ def upload_path
+ File.join(store_dir, filename.to_s)
+ end
+
+ private
+
+ def uploads
+ Upload.order(id: :desc).where(uploader: self.class.to_s)
+ end
- private
+ def build_upload
+ Upload.new(
+ uploader: self.class.to_s,
+ size: file.size,
+ path: upload_path,
+ model: model,
+ mount_point: mounted_as
+ )
+ end
- # Before removing an attachment, destroy any Upload records at the same path
- #
- # Called `before :remove`
- def destroy_upload(*args)
- return unless file_storage?
- return unless file
+ # Before removing an attachment, destroy any Upload records at the same path
+ #
+ # Called `before :remove`
+ def destroy_upload(*args)
+ return unless file && file.exists?
- Upload.remove_path(relative_path)
+ self.upload = nil
+ uploads.where(path: upload_path).delete_all
+ end
end
end
diff --git a/app/uploaders/uploader_helper.rb b/app/uploaders/uploader_helper.rb
index 7635c20ab3a..fd446d31092 100644
--- a/app/uploaders/uploader_helper.rb
+++ b/app/uploaders/uploader_helper.rb
@@ -32,14 +32,7 @@ module UploaderHelper
def extension_match?(extensions)
return false unless file
- extension =
- if file.respond_to?(:extension)
- file.extension
- else
- # Not all CarrierWave storages respond to :extension
- File.extname(file.path).delete('.')
- end
-
+ extension = file.try(:extension) || File.extname(file.path).delete('.')
extensions.include?(extension.downcase)
end
end
diff --git a/app/uploaders/workhorse.rb b/app/uploaders/workhorse.rb
new file mode 100644
index 00000000000..782032cf516
--- /dev/null
+++ b/app/uploaders/workhorse.rb
@@ -0,0 +1,7 @@
+module Workhorse
+ module UploadPath
+ def workhorse_upload_path
+ File.join(root, base_dir, 'tmp/uploads')
+ end
+ end
+end
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index ba4ca88a8a9..fb5e6f337a7 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -537,7 +537,8 @@
.form-group
= f.label :repository_storages, 'Storage paths for new projects', class: 'control-label col-sm-2'
.col-sm-10
- = f.select :repository_storages, repository_storages_options_for_select, {include_hidden: false}, multiple: true, class: 'form-control'
+ = f.select :repository_storages, repository_storages_options_for_select(@application_setting.repository_storages),
+ {include_hidden: false}, multiple: true, class: 'form-control'
.help-block
Manage repository storage paths. Learn more in the
= succeed "." do
diff --git a/app/views/admin/broadcast_messages/preview.js.haml b/app/views/admin/broadcast_messages/preview.js.haml
deleted file mode 100644
index c72e59640d7..00000000000
--- a/app/views/admin/broadcast_messages/preview.js.haml
+++ /dev/null
@@ -1 +0,0 @@
-$('.js-broadcast-message-preview').html("#{j(render_broadcast_message(@broadcast_message))}");
diff --git a/app/views/admin/conversational_development_index/show.html.haml b/app/views/admin/conversational_development_index/show.html.haml
index 30dd87f0463..ed40e7b4d00 100644
--- a/app/views/admin/conversational_development_index/show.html.haml
+++ b/app/views/admin/conversational_development_index/show.html.haml
@@ -6,7 +6,7 @@
= render 'callout'
.prepend-top-default
- - if !current_application_settings.usage_ping_enabled
+ - if !Gitlab::CurrentSettings.usage_ping_enabled
= render 'disabled'
- elsif @metric.blank?
= render 'no_data'
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index d251f75a8fd..e3711421b61 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -119,7 +119,7 @@
.well-segment.admin-well
%h4
Components
- - if current_application_settings.version_check_enabled
+ - if Gitlab::CurrentSettings.version_check_enabled
.pull-right
= version_status_badge
%p
diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml
index 10a3bed0a4f..e31fb58b205 100644
--- a/app/views/admin/health_check/show.html.haml
+++ b/app/views/admin/health_check/show.html.haml
@@ -8,7 +8,7 @@
.pull-left
%p
#{ s_('HealthCheck|Access token is') }
- %code#health-check-token= current_application_settings.health_check_access_token
+ %code#health-check-token= Gitlab::CurrentSettings.health_check_access_token
.prepend-top-10
= button_to _("Reset health check access token"), reset_health_check_token_admin_application_settings_path,
method: :put, class: 'btn btn-default',
@@ -18,11 +18,11 @@
= link_to s_('More information is available|here'), help_page_path('user/admin_area/monitoring/health_check')
%ul
%li
- %code= readiness_url(token: current_application_settings.health_check_access_token)
+ %code= readiness_url(token: Gitlab::CurrentSettings.health_check_access_token)
%li
- %code= liveness_url(token: current_application_settings.health_check_access_token)
+ %code= liveness_url(token: Gitlab::CurrentSettings.health_check_access_token)
%li
- %code= metrics_url(token: current_application_settings.health_check_access_token)
+ %code= metrics_url(token: Gitlab::CurrentSettings.health_check_access_token)
%hr
.panel.panel-default
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index 4f60be698e9..1e52646b1cc 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -36,7 +36,7 @@
data: { confirm: _("Are you sure you want to reset registration token?") }
= render partial: 'ci/runner/how_to_setup_runner',
- locals: { registration_token: current_application_settings.runners_registration_token,
+ locals: { registration_token: Gitlab::CurrentSettings.runners_registration_token,
type: 'shared' }
.append-bottom-20.clearfix
diff --git a/app/views/ci/variables/_variable_row.html.haml b/app/views/ci/variables/_variable_row.html.haml
new file mode 100644
index 00000000000..495a55660cb
--- /dev/null
+++ b/app/views/ci/variables/_variable_row.html.haml
@@ -0,0 +1,49 @@
+- form_field = local_assigns.fetch(:form_field, nil)
+- variable = local_assigns.fetch(:variable, nil)
+- only_key_value = local_assigns.fetch(:only_key_value, false)
+
+- id = variable&.id
+- key = variable&.key
+- value = variable&.value
+- is_protected = variable && !only_key_value ? variable.protected : true
+
+- id_input_name = "#{form_field}[variables_attributes][][id]"
+- destroy_input_name = "#{form_field}[variables_attributes][][_destroy]"
+- key_input_name = "#{form_field}[variables_attributes][][key]"
+- value_input_name = "#{form_field}[variables_attributes][][value]"
+- protected_input_name = "#{form_field}[variables_attributes][][protected]"
+
+%li.js-row.ci-variable-row{ data: { is_persisted: "#{!id.nil?}" } }
+ .ci-variable-row-body
+ %input.js-ci-variable-input-id{ type: "hidden", name: id_input_name, value: id }
+ %input.js-ci-variable-input-destroy{ type: "hidden", name: destroy_input_name }
+ %input.js-ci-variable-input-key.ci-variable-body-item.form-control{ type: "text",
+ name: key_input_name,
+ value: key,
+ placeholder: s_('CiVariables|Input variable key') }
+ .ci-variable-body-item
+ .form-control.js-secret-value-placeholder{ class: ('hide' unless id) }
+ = '*' * 20
+ %textarea.js-ci-variable-input-value.js-secret-value.form-control{ class: ('hide' if id),
+ rows: 1,
+ name: value_input_name,
+ placeholder: s_('CiVariables|Input variable value') }
+ = value
+ - unless only_key_value
+ .ci-variable-body-item.ci-variable-protected-item
+ .append-right-default
+ = s_("CiVariable|Protected")
+ %button{ type: 'button',
+ class: "js-project-feature-toggle project-feature-toggle #{'is-checked' if is_protected}",
+ "aria-label": s_("CiVariable|Toggle protected") }
+ %input{ type: "hidden",
+ class: 'js-ci-variable-input-protected js-project-feature-toggle-input',
+ name: protected_input_name,
+ value: is_protected }
+ %span.toggle-icon
+ = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked')
+ = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked')
+ -# EE-specific start
+ -# EE-specific end
+ %button.js-row-remove-button.ci-variable-row-remove-button{ type: 'button', 'aria-label': s_('CiVariables|Remove variable row') }
+ = icon('minus-circle')
diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml
index cebdbab4e74..617c20b9635 100644
--- a/app/views/dashboard/_groups_head.html.haml
+++ b/app/views/dashboard/_groups_head.html.haml
@@ -1,5 +1,5 @@
.top-area
- %ul.nav-links
+ %ul.nav-links.mobile-separator
= nav_link(page: dashboard_groups_path) do
= link_to dashboard_groups_path, title: _("Your groups") do
Your groups
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index 9038c4fbebd..449a2ce625e 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -4,7 +4,7 @@
.top-area.scrolling-tabs-container.inner-page-scroll-tabs
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
- %ul.nav-links.scrolling-tabs
+ %ul.nav-links.scrolling-tabs.mobile-separator
= nav_link(page: [dashboard_projects_path, root_path]) do
= link_to dashboard_projects_path, class: 'shortcuts-activity', data: {placement: 'right'} do
Your projects
diff --git a/app/views/dashboard/projects/_nav.html.haml b/app/views/dashboard/projects/_nav.html.haml
index c18077bc66f..97f854cc5f0 100644
--- a/app/views/dashboard/projects/_nav.html.haml
+++ b/app/views/dashboard/projects/_nav.html.haml
@@ -1,5 +1,5 @@
.nav-block
- %ul.nav-links
+ %ul.nav-links.mobile-separator
= nav_link(html_options: { class: ("active" unless params[:personal].present?) }) do
= link_to s_('DashboardProjects|All'), dashboard_projects_path
= nav_link(html_options: { class: ("active" if params[:personal].present?) }) do
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 20ca6ec969a..664966989db 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -4,7 +4,7 @@
- if current_user.todos.any?
.top-area
- %ul.nav-links
+ %ul.nav-links.mobile-separator
%li.todos-pending{ class: active_when(params[:state].blank? || params[:state] == 'pending') }>
= link_to todos_filter_path(state: 'pending') do
%span
diff --git a/app/views/devise/confirmations/almost_there.haml b/app/views/devise/confirmations/almost_there.haml
index fb70d158096..79826a364db 100644
--- a/app/views/devise/confirmations/almost_there.haml
+++ b/app/views/devise/confirmations/almost_there.haml
@@ -4,9 +4,9 @@
%p.lead.append-bottom-20
Please check your email to confirm your account
%hr
-- if current_application_settings.after_sign_up_text.present?
+- if Gitlab::CurrentSettings.after_sign_up_text.present?
.well-confirmation.text-center
- = markdown_field(current_application_settings, :after_sign_up_text)
+ = markdown_field(Gitlab::CurrentSettings, :after_sign_up_text)
%p.text-center
No confirmation email received? Please check your spam folder or
.append-bottom-20.prepend-top-20.text-center
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index 76a8099d7c0..86cd0759a2c 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -57,4 +57,20 @@
.form-actions
= button_to 'Remove group', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_group_message(@group) }
+- if supports_nested_groups?
+ .panel.panel-warning
+ .panel-heading Transfer group
+ .panel-body
+ = form_for @group, url: transfer_group_path(@group), method: :put do |f|
+ .form-group
+ = dropdown_tag('Select parent group', options: { toggle_class: 'js-groups-dropdown', title: 'Parent Group', filter: true, dropdown_class: 'dropdown-open-top dropdown-group-transfer', placeholder: "Search groups", data: { data: parent_group_options(@group) } })
+ = hidden_field_tag 'new_parent_group_id'
+
+ %ul
+ %li Be careful. Changing a group's parent can have unintended #{link_to 'side effects', 'https://docs.gitlab.com/ce/user/project/index.html#redirects-when-changing-repository-paths', target: 'blank'}.
+ %li You can only transfer the group to a group you manage.
+ %li You will need to update your local repositories to point to the new location.
+ %li If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility.
+ = f.submit 'Transfer group', class: "btn btn-warning"
+
= render 'shared/confirm_modal', phrase: @group.path
diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml
index fdd72ead2cb..63811ea1c81 100644
--- a/app/views/help/index.html.haml
+++ b/app/views/help/index.html.haml
@@ -1,8 +1,8 @@
= webpack_bundle_tag 'docs'
%div
-- if current_application_settings.help_page_text.present?
- = markdown_field(current_application_settings, :help_page_text)
+- if Gitlab::CurrentSettings.help_page_text.present?
+ = markdown_field(Gitlab::CurrentSettings.current_application_settings, :help_page_text)
%hr
%h1
@@ -14,7 +14,7 @@
= version_status_badge
%hr
-- unless current_application_settings.help_page_hide_commercial_content?
+- unless Gitlab::CurrentSettings.help_page_hide_commercial_content?
%p.slead
GitLab is open source software to collaborate on code.
%br
@@ -46,6 +46,6 @@
%li
%button.btn-blank.btn-link.js-trigger-shortcut{ type: 'button' }
Use shortcuts
- - unless current_application_settings.help_page_hide_commercial_content?
+ - unless Gitlab::CurrentSettings.help_page_hide_commercial_content?
%li= link_to 'Get a support subscription', 'https://about.gitlab.com/pricing/'
%li= link_to 'Compare GitLab editions', 'https://about.gitlab.com/features/#compare'
diff --git a/app/views/koding/index.html.haml b/app/views/koding/index.html.haml
index 04e2d4b63e6..bb7f9ba7ae4 100644
--- a/app/views/koding/index.html.haml
+++ b/app/views/koding/index.html.haml
@@ -3,4 +3,4 @@
= icon('circle', class: 'cgreen')
Integration is active for
= link_to koding_project_url, target: '_blank', rel: 'noopener noreferrer' do
- #{current_application_settings.koding_url}
+ #{Gitlab::CurrentSettings.koding_url}
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index ea13a5e6d62..0c979109b3f 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -41,12 +41,14 @@
= webpack_bundle_tag "webpack_runtime"
= webpack_bundle_tag "common"
= webpack_bundle_tag "main"
- = webpack_bundle_tag "raven" if current_application_settings.clientside_sentry_enabled
+ = webpack_bundle_tag "raven" if Gitlab::CurrentSettings.clientside_sentry_enabled
= webpack_bundle_tag "test" if Rails.env.test?
- if content_for?(:page_specific_javascripts)
= yield :page_specific_javascripts
+ = webpack_controller_bundle_tags
+
= yield :project_javascripts
= csrf_meta_tags
diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml
index 4e9ea33e675..257f7326409 100644
--- a/app/views/layouts/devise.html.haml
+++ b/app/views/layouts/devise.html.haml
@@ -15,7 +15,7 @@
.col-sm-7.brand-holder.pull-left
%h1
= brand_title
- = brand_image
+ = brand_image
- if brand_item&.description?
= brand_text
- else
@@ -26,8 +26,8 @@
Perform code reviews and enhance collaboration with merge requests.
Each project can also have an issue tracker and a wiki.
- - if current_application_settings.sign_in_text.present?
- = markdown_field(current_application_settings, :sign_in_text)
+ - if Gitlab::CurrentSettings.sign_in_text.present?
+ = markdown_field(Gitlab::CurrentSettings.current_application_settings, :sign_in_text)
%hr.footer-fixed
.container.footer-container
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index 96aae06a9df..09a43a2cac5 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -88,7 +88,7 @@
%strong.fly-out-top-item-name
#{ _('Members') }
- if current_user && can?(current_user, :admin_group, @group)
- = nav_link(path: %w[groups#projects groups#edit ci_cd#show]) do
+ = nav_link(path: group_nav_link_paths) do
= link_to edit_group_path(@group) do
.nav-icon-container
= sprite_icon('settings')
diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml
index a5a62a0695f..c878fcf2808 100644
--- a/app/views/layouts/nav/sidebar/_profile.html.haml
+++ b/app/views/layouts/nav/sidebar/_profile.html.haml
@@ -28,7 +28,7 @@
= link_to profile_account_path do
%strong.fly-out-top-item-name
#{ _('Account') }
- - if current_application_settings.user_oauth_applications?
+ - if Gitlab::CurrentSettings.user_oauth_applications?
= nav_link(controller: 'oauth/applications') do
= link_to applications_profile_path do
.nav-icon-container
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index abd07d71bcc..2b98cb9de99 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -184,10 +184,33 @@
Environments
- if project_nav_tab? :clusters
+ - show_cluster_hint = show_gke_cluster_integration_callout?(@project)
= nav_link(controller: [:clusters, :user, :gcp]) do
- = link_to project_clusters_path(@project), title: 'Cluster', class: 'shortcuts-cluster' do
+ = link_to project_clusters_path(@project), title: _('Kubernetes'), class: 'shortcuts-cluster' do
%span
- Clusters
+ = _('Kubernetes')
+ - if show_cluster_hint
+ .feature-highlight.js-feature-highlight{ disabled: true,
+ data: { trigger: 'manual',
+ container: 'body',
+ toggle: 'popover',
+ placement: 'right',
+ highlight: UserCalloutsHelper::GKE_CLUSTER_INTEGRATION,
+ highlight_priority: UserCallout.feature_names[:GKE_CLUSTER_INTEGRATION],
+ dismiss_endpoint: user_callouts_path } }
+ - if show_cluster_hint
+ .feature-highlight-popover-content
+ = image_tag 'illustrations/cluster_popover.svg', class: 'feature-highlight-illustration'
+ .feature-highlight-popover-sub-content
+ %p= _('Allows you to add and manage Kubernetes clusters.')
+ %p
+ = _('Protip:')
+ = link_to 'Auto DevOps', help_page_path('topics/autodevops/index.md')
+ %span= _('uses Kubernetes clusters to deploy your code!')
+ %hr
+ %button.btn.btn-create.btn-xs.dismiss-feature-highlight{ type: 'button' }
+ %span= _("Got it!")
+ = sprite_icon('thumb-up')
- if @project.feature_available?(:builds, current_user) && !@project.empty_repo?
= nav_link(path: 'pipelines#charts') do
diff --git a/app/views/notify/_note_email.html.haml b/app/views/notify/_note_email.html.haml
index 3e36da31ea3..94bd6f96dbc 100644
--- a/app/views/notify/_note_email.html.haml
+++ b/app/views/notify/_note_email.html.haml
@@ -22,7 +22,7 @@
- else
commented on a #{link_to 'discussion', @target_url}
-- elsif current_application_settings.email_author_in_body
+- elsif Gitlab::CurrentSettings.email_author_in_body
%p.details
#{link_to @note.author_name, user_url(@note.author)} commented:
diff --git a/app/views/notify/_note_email.text.erb b/app/views/notify/_note_email.text.erb
index cb2e7fab6d5..c319cb55e87 100644
--- a/app/views/notify/_note_email.text.erb
+++ b/app/views/notify/_note_email.text.erb
@@ -12,7 +12,7 @@
<%= ":" -%>
-<% elsif current_application_settings.email_author_in_body -%>
+<% elsif Gitlab::CurrentSettings.email_author_in_body -%>
<%= "#{@note.author_name} commented:" -%>
diff --git a/app/views/notify/new_issue_email.html.haml b/app/views/notify/new_issue_email.html.haml
index eb5157ccac9..e6cdaf85c0d 100644
--- a/app/views/notify/new_issue_email.html.haml
+++ b/app/views/notify/new_issue_email.html.haml
@@ -1,4 +1,4 @@
-- if current_application_settings.email_author_in_body
+- if Gitlab::CurrentSettings.email_author_in_body
%p.details
#{link_to @issue.author_name, user_url(@issue.author)} created an issue:
diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml
index 951c96bdb9c..0a9adc6f243 100644
--- a/app/views/notify/new_merge_request_email.html.haml
+++ b/app/views/notify/new_merge_request_email.html.haml
@@ -1,4 +1,4 @@
-- if current_application_settings.email_author_in_body
+- if Gitlab::CurrentSettings.email_author_in_body
%p.details
#{link_to @merge_request.author_name, user_url(@merge_request.author)} created a merge request:
diff --git a/app/views/notify/new_user_email.html.haml b/app/views/notify/new_user_email.html.haml
index 00e1b5faae3..db4424a01f9 100644
--- a/app/views/notify/new_user_email.html.haml
+++ b/app/views/notify/new_user_email.html.haml
@@ -1,7 +1,7 @@
%p
Hi #{@user['name']}!
%p
- - if current_application_settings.allow_signup?
+ - if Gitlab::CurrentSettings.allow_signup?
Your account has been created successfully.
- else
The Administrator created an account for you. Now you are a member of the company GitLab application.
diff --git a/app/views/projects/_export.html.haml b/app/views/projects/_export.html.haml
index e759c87bda7..5dfe973f33c 100644
--- a/app/views/projects/_export.html.haml
+++ b/app/views/projects/_export.html.haml
@@ -1,4 +1,4 @@
-- return unless current_application_settings.project_export_enabled?
+- return unless Gitlab::CurrentSettings.project_export_enabled?
- project = local_assigns.fetch(:project)
- expanded = Rails.env.test?
diff --git a/app/views/projects/clusters/_advanced_settings.html.haml b/app/views/projects/clusters/_advanced_settings.html.haml
index 8a13713ae02..14979bee714 100644
--- a/app/views/projects/clusters/_advanced_settings.html.haml
+++ b/app/views/projects/clusters/_advanced_settings.html.haml
@@ -5,11 +5,11 @@
= s_('ClusterIntegration|Google Kubernetes Engine')
%p
- link_gke = link_to(s_('ClusterIntegration|Google Kubernetes Engine'), @cluster.gke_cluster_url, target: '_blank', rel: 'noopener noreferrer')
- = s_('ClusterIntegration|Manage your cluster by visiting %{link_gke}').html_safe % { link_gke: link_gke }
+ = s_('ClusterIntegration|Manage your Kubernetes cluster by visiting %{link_gke}').html_safe % { link_gke: link_gke }
.well.form-group
%label.text-danger
- = s_('ClusterIntegration|Remove cluster integration')
+ = s_('ClusterIntegration|Remove Kubernetes cluster integration')
%p
- = s_("ClusterIntegration|Remove this cluster's configuration from this project. This will not delete your actual cluster.")
- = link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: s_("ClusterIntegration|Are you sure you want to remove this cluster's integration? This will not delete your actual cluster.")})
+ = s_("ClusterIntegration|Remove this Kubernetes cluster's configuration from this project. This will not delete your actual Kubernetes cluster.")
+ = link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: s_("ClusterIntegration|Are you sure you want to remove this Kubernetes cluster's integration? This will not delete your actual Kubernetes cluster.")})
diff --git a/app/views/projects/clusters/_banner.html.haml b/app/views/projects/clusters/_banner.html.haml
index 26ca3307a4a..f18caa3f4ac 100644
--- a/app/views/projects/clusters/_banner.html.haml
+++ b/app/views/projects/clusters/_banner.html.haml
@@ -1,14 +1,14 @@
-%h4= s_('ClusterIntegration|Cluster integration')
+%h4= s_('ClusterIntegration|Kubernetes cluster integration')
.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 Kubernetes Engine')
+ = s_('ClusterIntegration|Something went wrong while creating your Kubernetes cluster on Google Kubernetes Engine')
%p.js-error-reason
.hidden.js-cluster-creating.alert.alert-info.alert-block.append-bottom-10{ role: 'alert' }
- = s_('ClusterIntegration|Cluster is being created on Google Kubernetes Engine...')
+ = s_('ClusterIntegration|Kubernetes cluster is being created on Google Kubernetes Engine...')
.hidden.js-cluster-success.alert.alert-success.alert-block.append-bottom-10{ role: 'alert' }
- = s_('ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine. Refresh the page to see cluster\'s details')
+ = s_("ClusterIntegration|Kubernetes cluster was successfully created on Google Kubernetes Engine. Refresh the page to see Kubernetes cluster's details")
- %p= s_('ClusterIntegration|Control how your cluster integrates with GitLab')
+ %p= s_('ClusterIntegration|Control how your Kubernetes cluster integrates with GitLab')
diff --git a/app/views/projects/clusters/_cluster.html.haml b/app/views/projects/clusters/_cluster.html.haml
index 20ee8086f93..2d7f7c6b1fb 100644
--- a/app/views/projects/clusters/_cluster.html.haml
+++ b/app/views/projects/clusters/_cluster.html.haml
@@ -1,6 +1,6 @@
.gl-responsive-table-row
.table-section.section-30
- .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Cluster")
+ .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Kubernetes cluster")
.table-mobile-content
= link_to cluster.name, namespace_project_cluster_path(@project.namespace, @project, cluster)
.table-section.section-30
@@ -14,7 +14,7 @@
.table-mobile-content
%button.js-project-feature-toggle.project-feature-toggle{ type: "button",
class: "#{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_cluster?}",
- "aria-label": s_("ClusterIntegration|Toggle Cluster"),
+ "aria-label": s_("ClusterIntegration|Toggle Kubernetes Cluster"),
disabled: !cluster.can_toggle_cluster?,
data: { endpoint: namespace_project_cluster_path(@project.namespace, @project, cluster, format: :json) } }
%input.js-project-feature-toggle-input{ type: "hidden", value: cluster.enabled? }
diff --git a/app/views/projects/clusters/_dropdown.html.haml b/app/views/projects/clusters/_dropdown.html.haml
index e36dd900f8d..d55a9c60b64 100644
--- a/app/views/projects/clusters/_dropdown.html.haml
+++ b/app/views/projects/clusters/_dropdown.html.haml
@@ -1,4 +1,4 @@
-%h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up cluster integration')
+%h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up Kubernetes cluster integration')
.dropdown.clusters-dropdown
%button.dropdown-menu-toggle.dropdown-menu-full-width{ type: 'button', data: { toggle: 'dropdown' }, 'aria-haspopup': true, 'aria-expanded': false }
@@ -7,6 +7,6 @@
= icon('chevron-down')
%ul.dropdown-menu.clusters-dropdown-menu.dropdown-menu-full-width
%li
- = link_to(s_('ClusterIntegration|Create cluster on Google Kubernetes Engine'), gcp_new_namespace_project_clusters_path(@project.namespace, @project))
+ = link_to(s_('ClusterIntegration|Create Kubernetes cluster on Google Kubernetes Engine'), gcp_new_namespace_project_clusters_path(@project.namespace, @project))
%li
- = link_to(s_('ClusterIntegration|Add an existing cluster'), user_new_namespace_project_clusters_path(@project.namespace, @project))
+ = link_to(s_('ClusterIntegration|Add an existing Kubernetes cluster'), user_new_namespace_project_clusters_path(@project.namespace, @project))
diff --git a/app/views/projects/clusters/_empty_state.html.haml b/app/views/projects/clusters/_empty_state.html.haml
index b525f4efc83..600d679b60c 100644
--- a/app/views/projects/clusters/_empty_state.html.haml
+++ b/app/views/projects/clusters/_empty_state.html.haml
@@ -3,10 +3,9 @@
.svg-content= image_tag 'illustrations/clusters_empty.svg'
.col-xs-12
.text-content
- %h4.text-center= s_('ClusterIntegration|Integrate cluster automation')
- - link_to_help_page = link_to(s_('ClusterIntegration|Learn more about Clusters'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
- %p= s_('ClusterIntegration|Clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page}
+ %h4.text-center= s_('ClusterIntegration|Integrate Kubernetes cluster automation')
+ - link_to_help_page = link_to(s_('ClusterIntegration|Learn more about Kubernetes'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
+ %p= s_('ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page}
.text-center
- = link_to s_('ClusterIntegration|Add cluster'), new_project_cluster_path(@project), class: 'btn btn-success'
-
+ = link_to s_('ClusterIntegration|Add Kubernetes cluster'), new_project_cluster_path(@project), class: 'btn btn-success'
diff --git a/app/views/projects/clusters/_integration_form.html.haml b/app/views/projects/clusters/_integration_form.html.haml
index 0af6e6e0577..d4c0cd82ce3 100644
--- a/app/views/projects/clusters/_integration_form.html.haml
+++ b/app/views/projects/clusters/_integration_form.html.haml
@@ -5,15 +5,15 @@
%p
- if @cluster.enabled?
- if can?(current_user, :update_cluster, @cluster)
- = s_('ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab\'s connection to it.')
+ = s_('ClusterIntegration|Kubernetes cluster integration is enabled for this project. Disabling this integration will not affect your Kubernetes cluster, it will only temporarily turn off GitLab\'s connection to it.')
- else
- = s_('ClusterIntegration|Cluster integration is enabled for this project.')
+ = s_('ClusterIntegration|Kubernetes cluster integration is enabled for this project.')
- else
- = s_('ClusterIntegration|Cluster integration is disabled for this project.')
+ = s_('ClusterIntegration|Kubernetes cluster integration is disabled for this project.')
%label.append-bottom-10.js-cluster-enable-toggle-area
%button{ type: 'button',
class: "js-project-feature-toggle project-feature-toggle #{'is-checked' if @cluster.enabled?} #{'is-disabled' unless can?(current_user, :update_cluster, @cluster)}",
- "aria-label": s_("ClusterIntegration|Toggle Cluster"),
+ "aria-label": s_("ClusterIntegration|Toggle Kubernetes cluster"),
disabled: !can?(current_user, :update_cluster, @cluster) }
= field.hidden_field :enabled, { class: 'js-project-feature-toggle-input'}
%span.toggle-icon
@@ -23,7 +23,7 @@
.form-group
%h5= s_('ClusterIntegration|Environment scope')
%p
- = s_("ClusterIntegration|Choose which of your project's environments will use this cluster.")
+ = s_("ClusterIntegration|Choose which of your project's environments will use this Kubernetes cluster.")
= link_to s_("ClusterIntegration|Learn more about environments"), help_page_path('ci/environments')
= field.text_field :environment_scope, class: 'form-control js-select-on-focus', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope')
diff --git a/app/views/projects/clusters/_sidebar.html.haml b/app/views/projects/clusters/_sidebar.html.haml
index 761879db32b..73cd7c50922 100644
--- a/app/views/projects/clusters/_sidebar.html.haml
+++ b/app/views/projects/clusters/_sidebar.html.haml
@@ -1,7 +1,7 @@
%h4.prepend-top-0
- = s_('ClusterIntegration|Cluster integration')
+ = s_('ClusterIntegration|Kubernetes cluster integration')
%p
- = s_('ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way.')
+ = s_('ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way.')
%p
- - link = link_to(s_('ClusterIntegration|cluster'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
+ - link = link_to(_('Kubernetes'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Learn more about %{link_to_documentation}').html_safe % { link_to_documentation: link }
diff --git a/app/views/projects/clusters/gcp/_form.html.haml b/app/views/projects/clusters/gcp/_form.html.haml
index e384b60d8d9..5739a57dcfe 100644
--- a/app/views/projects/clusters/gcp/_form.html.haml
+++ b/app/views/projects/clusters/gcp/_form.html.haml
@@ -1,12 +1,12 @@
%p
- 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}
+ = s_('ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration.').html_safe % { link_to_help_page: link_to_help_page}
= form_for @cluster, html: { class: 'prepend-top-20' }, url: gcp_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field|
= form_errors(@cluster)
.form-group
- = field.label :name, s_('ClusterIntegration|Cluster name')
- = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Cluster name')
+ = field.label :name, s_('ClusterIntegration|Kubernetes cluster name')
+ = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
.form-group
= field.label :environment_scope, s_('ClusterIntegration|Environment scope')
= field.text_field :environment_scope, class: 'form-control', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope')
@@ -32,4 +32,4 @@
= 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-success'
+ = field.submit s_('ClusterIntegration|Create Kubernetes cluster'), class: 'btn btn-success'
diff --git a/app/views/projects/clusters/gcp/_header.html.haml b/app/views/projects/clusters/gcp/_header.html.haml
index bddb902115d..fa989943492 100644
--- a/app/views/projects/clusters/gcp/_header.html.haml
+++ b/app/views/projects/clusters/gcp/_header.html.haml
@@ -1,5 +1,5 @@
%h4.prepend-top-20
- = s_('ClusterIntegration|Enter the details for your cluster')
+ = s_('ClusterIntegration|Enter the details for your Kubernetes cluster')
%p
= s_('ClusterIntegration|Please make sure that your Google account meets the following requirements:')
%ul
@@ -8,7 +8,7 @@
= s_('ClusterIntegration|Your account must have %{link_to_kubernetes_engine}').html_safe % { link_to_kubernetes_engine: link_to_kubernetes_engine }
%li
- link_to_requirements = link_to(s_('ClusterIntegration|meets the requirements'), 'https://cloud.google.com/kubernetes-engine/docs/quickstart?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer')
- = s_('ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters').html_safe % { link_to_requirements: link_to_requirements }
+ = s_('ClusterIntegration|Make sure your account %{link_to_requirements} to create Kubernetes clusters').html_safe % { link_to_requirements: link_to_requirements }
%li
- link_to_container_project = link_to(s_('ClusterIntegration|Google Kubernetes Engine project'), 'https://console.cloud.google.com/home/dashboard?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer')
- = s_('ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below').html_safe % { link_to_container_project: link_to_container_project }
+ = s_('ClusterIntegration|This account must have permissions to create a Kubernetes cluster in the %{link_to_container_project} specified below').html_safe % { link_to_container_project: link_to_container_project }
diff --git a/app/views/projects/clusters/gcp/_show.html.haml b/app/views/projects/clusters/gcp/_show.html.haml
index f3122a1bf47..78cd687ef93 100644
--- a/app/views/projects/clusters/gcp/_show.html.haml
+++ b/app/views/projects/clusters/gcp/_show.html.haml
@@ -1,10 +1,10 @@
.form-group
%label.append-bottom-10{ for: 'cluster-name' }
- = s_('ClusterIntegration|Cluster name')
+ = s_('ClusterIntegration|Kubernetes cluster name')
.input-group
%input.form-control.cluster-name.js-select-on-focus{ value: @cluster.name, readonly: true }
%span.input-group-btn
- = clipboard_button(text: @cluster.name, title: s_('ClusterIntegration|Copy cluster name'), class: 'btn-default')
+ = clipboard_button(text: @cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'), class: 'btn-default')
= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
= form_errors(@cluster)
diff --git a/app/views/projects/clusters/gcp/login.html.haml b/app/views/projects/clusters/gcp/login.html.haml
index 878ebaded88..dada51f39da 100644
--- a/app/views/projects/clusters/gcp/login.html.haml
+++ b/app/views/projects/clusters/gcp/login.html.haml
@@ -1,11 +1,11 @@
-- breadcrumb_title "Cluster"
+- breadcrumb_title 'Kubernetes'
- page_title _("Login")
.row.prepend-top-default
.col-sm-4
= render 'projects/clusters/sidebar'
.col-sm-8
- = render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Create cluster on Google Kubernetes Engine')
+ = render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Create Kubernetes cluster on Google Kubernetes Engine')
= render 'header'
.row
.col-sm-8.col-sm-offset-4.signin-with-google
diff --git a/app/views/projects/clusters/gcp/new.html.haml b/app/views/projects/clusters/gcp/new.html.haml
index 8d92fb1e320..ea78d66d883 100644
--- a/app/views/projects/clusters/gcp/new.html.haml
+++ b/app/views/projects/clusters/gcp/new.html.haml
@@ -1,10 +1,10 @@
-- breadcrumb_title "Cluster"
-- page_title _("New Cluster")
+- breadcrumb_title 'Kubernetes'
+- page_title _("New Kubernetes Cluster")
.row.prepend-top-default
.col-sm-4
= render 'projects/clusters/sidebar'
.col-sm-8
- = render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Create cluster on Google Kubernetes Engine')
+ = render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Create Kubernetes cluster on Google Kubernetes Engine')
= render 'header'
= render 'form'
diff --git a/app/views/projects/clusters/index.html.haml b/app/views/projects/clusters/index.html.haml
index 74dbe859eea..17b244f4bf7 100644
--- a/app/views/projects/clusters/index.html.haml
+++ b/app/views/projects/clusters/index.html.haml
@@ -1,5 +1,5 @@
-- breadcrumb_title "Clusters"
-- page_title "Clusters"
+- breadcrumb_title 'Kubernetes'
+- page_title "Kubernetes Clusters"
.clusters-container
- if @clusters.empty?
@@ -7,11 +7,11 @@
- else
.top-area.adjust
.nav-text
- = s_("ClusterIntegration|Clusters can be used to deploy applications and to provide Review Apps for this project")
+ = s_("ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project")
.ci-table.js-clusters-list
.gl-responsive-table-row.table-row-header{ role: "row" }
.table-section.section-30{ role: "rowheader" }
- = s_("ClusterIntegration|Cluster")
+ = s_("ClusterIntegration|Kubernetes cluster")
.table-section.section-30{ role: "rowheader" }
= s_("ClusterIntegration|Environment scope")
.table-section.section-30{ role: "rowheader" }
diff --git a/app/views/projects/clusters/new.html.haml b/app/views/projects/clusters/new.html.haml
index ddd13f8ea96..ebb7d247125 100644
--- a/app/views/projects/clusters/new.html.haml
+++ b/app/views/projects/clusters/new.html.haml
@@ -1,13 +1,13 @@
-- breadcrumb_title "Cluster"
-- page_title _("Cluster")
+- breadcrumb_title 'Kubernetes'
+- page_title _("Kubernetes Cluster")
.row.prepend-top-default
.col-sm-4
= render 'sidebar'
.col-sm-8
- %h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up cluster integration')
+ %h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up Kubernetes cluster integration')
- %p= s_('ClusterIntegration|Create a new cluster on Google Kubernetes Engine right from GitLab')
+ %p= s_('ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab')
= link_to s_('ClusterIntegration|Create on GKE'), gcp_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
%p= s_('ClusterIntegration|Enter the details for an existing Kubernetes cluster')
- = link_to s_('ClusterIntegration|Add an existing cluster'), user_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
+ = link_to s_('ClusterIntegration|Add an existing Kubernetes cluster'), user_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml
index 74631f8b5ab..2b1b23ba198 100644
--- a/app/views/projects/clusters/show.html.haml
+++ b/app/views/projects/clusters/show.html.haml
@@ -1,7 +1,7 @@
- @content_class = "limit-container-width" unless fluid_layout
-- add_to_breadcrumbs "Clusters", project_clusters_path(@project)
+- add_to_breadcrumbs "Kubernetes Clusters", project_clusters_path(@project)
- breadcrumb_title @cluster.name
-- page_title _("Cluster")
+- page_title _("Kubernetes Cluster")
- expanded = Rails.env.test?
@@ -14,6 +14,7 @@
cluster_status: @cluster.status_name,
cluster_status_reason: @cluster.status_reason,
help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications'),
+ ingress_help_path: help_page_path('user/project/clusters/index.md', anchor: 'getting-the-external-ip-address'),
manage_prometheus_path: edit_project_service_path(@cluster.project, 'prometheus') } }
.js-cluster-application-notice
@@ -27,10 +28,10 @@
%section.settings#js-cluster-details{ class: ('expanded' if expanded) }
.settings-header
- %h4= s_('ClusterIntegration|Cluster details')
+ %h4= s_('ClusterIntegration|Kubernetes cluster details')
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
- %p= s_('ClusterIntegration|See and edit the details for your cluster')
+ %p= s_('ClusterIntegration|See and edit the details for your Kubernetes cluster')
.settings-content
- if @cluster.managed?
= render 'projects/clusters/gcp/show'
@@ -42,6 +43,6 @@
%h4= _('Advanced settings')
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
- %p= s_("ClusterIntegration|Advanced options on this cluster's integration")
+ %p= s_("ClusterIntegration|Advanced options on this Kubernetes cluster's integration")
.settings-content
= render 'advanced_settings'
diff --git a/app/views/projects/clusters/user/_form.html.haml b/app/views/projects/clusters/user/_form.html.haml
index babfca0c567..2e92524ce8f 100644
--- a/app/views/projects/clusters/user/_form.html.haml
+++ b/app/views/projects/clusters/user/_form.html.haml
@@ -1,8 +1,8 @@
= form_for @cluster, url: user_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field|
= form_errors(@cluster)
.form-group
- = field.label :name, s_('ClusterIntegration|Cluster name')
- = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Cluster name')
+ = field.label :name, s_('ClusterIntegration|Kubernetes cluster name')
+ = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
.form-group
= field.label :environment_scope, s_('ClusterIntegration|Environment scope')
= field.text_field :environment_scope, class: 'form-control', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope')
@@ -25,4 +25,4 @@
= platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
.form-group
- = field.submit s_('ClusterIntegration|Add cluster'), class: 'btn btn-success'
+ = field.submit s_('ClusterIntegration|Add Kubernetes cluster'), class: 'btn btn-success'
diff --git a/app/views/projects/clusters/user/_header.html.haml b/app/views/projects/clusters/user/_header.html.haml
index 06ac210a06d..04c7ce96a4b 100644
--- a/app/views/projects/clusters/user/_header.html.haml
+++ b/app/views/projects/clusters/user/_header.html.haml
@@ -1,5 +1,5 @@
%h4.prepend-top-20
- = s_('ClusterIntegration|Enter the details for your cluster')
+ = s_('ClusterIntegration|Enter the details for your Kubernetes cluster')
%p
- link_to_help_page = link_to(s_('ClusterIntegration|documentation'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
- = s_('ClusterIntegration|Please enter access information for your cluster. If you need help, you can read our %{link_to_help_page} on clusters').html_safe % { link_to_help_page: link_to_help_page }
+ = s_('ClusterIntegration|Please enter access information for your Kubernetes cluster. If you need help, you can read our %{link_to_help_page} on Kubernetes').html_safe % { link_to_help_page: link_to_help_page }
diff --git a/app/views/projects/clusters/user/_show.html.haml b/app/views/projects/clusters/user/_show.html.haml
index 5931e0b7f17..ebbf7e775c7 100644
--- a/app/views/projects/clusters/user/_show.html.haml
+++ b/app/views/projects/clusters/user/_show.html.haml
@@ -1,8 +1,8 @@
= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
= form_errors(@cluster)
.form-group
- = field.label :name, s_('ClusterIntegration|Cluster name')
- = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Cluster name')
+ = field.label :name, s_('ClusterIntegration|Kubernetes cluster name')
+ = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
= field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field|
.form-group
diff --git a/app/views/projects/clusters/user/new.html.haml b/app/views/projects/clusters/user/new.html.haml
index 68f38f83453..7fb75cd9cc7 100644
--- a/app/views/projects/clusters/user/new.html.haml
+++ b/app/views/projects/clusters/user/new.html.haml
@@ -1,11 +1,11 @@
-- breadcrumb_title "Cluster"
-- page_title _("New Cluster")
+- breadcrumb_title 'Kubernetes'
+- page_title _("New Kubernetes cluster")
.row.prepend-top-default
.col-sm-4
= render 'projects/clusters/sidebar'
.col-sm-8
- = render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Add an existing cluster')
+ = render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Add an existing Kubernetes cluster')
= render 'header'
.prepend-top-20
= render 'form'
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index d66066a6d0b..64259669c19 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -6,7 +6,7 @@
- link = commit_path(project, commit, merge_request: merge_request)
- cache_key = [project.full_path,
commit.id,
- current_application_settings,
+ Gitlab::CurrentSettings.current_application_settings,
@path.presence,
current_controller?(:commits),
merge_request&.iid,
@@ -51,6 +51,7 @@
- if commit.status(ref)
= render_commit_status(commit, ref: ref)
+ #commit-pipeline-status{ data: { endpoint: pipelines_project_commit_path(project, commit.id) } }
= link_to commit.short_id, link, class: "commit-sha btn btn-transparent btn-link"
= clipboard_button(text: commit.id, title: _("Copy commit SHA to clipboard"))
= link_to_browse_code(project, commit)
diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml
index 331d62cf247..37b00a14fc6 100644
--- a/app/views/projects/issues/_new_branch.html.haml
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -15,10 +15,10 @@
%span.text
Checking branch availability…
.btn-group.available.hide
- %button.btn.js-create-merge-request.btn-default{ type: 'button', data: { action: data_action } }
+ %button.btn.js-create-merge-request.btn-success.btn-inverted{ type: 'button', data: { action: data_action } }
= value
- %button.btn.create-merge-request-dropdown-toggle.dropdown-toggle.btn-default.js-dropdown-toggle{ type: 'button', data: { dropdown: { trigger: '#create-merge-request-dropdown' } } }
+ %button.btn.create-merge-request-dropdown-toggle.dropdown-toggle.btn-success.btn-inverted.js-dropdown-toggle{ type: 'button', data: { dropdown: { trigger: '#create-merge-request-dropdown' } } }
= icon('caret-down')
%ul#create-merge-request-dropdown.create-merge-request-dropdown-menu.dropdown-menu.dropdown-menu-align-right.gl-show-field-errors{ data: { dropdown: true } }
diff --git a/app/views/projects/pipeline_schedules/_form.html.haml b/app/views/projects/pipeline_schedules/_form.html.haml
index 857ae00d0ab..ff440e99042 100644
--- a/app/views/projects/pipeline_schedules/_form.html.haml
+++ b/app/views/projects/pipeline_schedules/_form.html.haml
@@ -22,14 +22,20 @@
= f.label :ref, _('Target Branch'), class: 'label-light'
= dropdown_tag(_("Select target branch"), options: { toggle_class: 'btn js-target-branch-dropdown', dropdown_class: 'git-revision-dropdown', title: _("Select target branch"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: @project.repository.branch_names, default_branch: @project.default_branch } } )
= f.text_field :ref, value: @schedule.ref, id: 'schedule_ref', class: 'hidden', name: 'schedule[ref]', required: true
- .form-group
+ .form-group.js-ci-variable-list-section
.col-md-9
%label.label-light
#{ s_('PipelineSchedules|Variables') }
- %ul.js-pipeline-variable-list.pipeline-variable-list
- - @schedule.variables.each do |variable|
- = render 'variable_row', id: variable.id, key: variable.key, value: variable.value
- = render 'variable_row'
+ %ul.ci-variable-list
+ - @schedule.variables.each do |variable|
+ = render 'ci/variables/variable_row', form_field: 'schedule', variable: variable, only_key_value: true
+ = render 'ci/variables/variable_row', form_field: 'schedule', only_key_value: true
+ - if @schedule.variables.size > 0
+ %button.btn.btn-info.btn-inverted.prepend-top-10.js-secret-value-reveal-button{ type: 'button', data: { secret_reveal_status: "#{@schedule.variables.size == 0}" } }
+ - if @schedule.variables.size == 0
+ = n_('Hide value', 'Hide values', @schedule.variables.size)
+ - else
+ = n_('Reveal value', 'Reveal values', @schedule.variables.size)
.form-group
.col-md-9
= f.label :active, s_('PipelineSchedules|Activated'), class: 'label-light'
diff --git a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
index 800e234275c..a8692b83b07 100644
--- a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
+++ b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
@@ -29,9 +29,10 @@
- if can?(current_user, :play_pipeline_schedule, pipeline_schedule)
= link_to play_pipeline_schedule_path(pipeline_schedule), method: :post, title: s_('Play'), class: 'btn' do
= icon('play')
- - if can?(current_user, :update_pipeline_schedule, pipeline_schedule)
+ - if can?(current_user, :take_ownership_pipeline_schedule, pipeline_schedule)
= link_to take_ownership_pipeline_schedule_path(pipeline_schedule), method: :post, title: s_('PipelineSchedules|Take ownership'), class: 'btn' do
= s_('PipelineSchedules|Take ownership')
+ - if can?(current_user, :update_pipeline_schedule, pipeline_schedule)
= link_to edit_pipeline_schedule_path(pipeline_schedule), title: _('Edit'), class: 'btn' do
= icon('pencil')
- if can?(current_user, :admin_pipeline_schedule, pipeline_schedule)
diff --git a/app/views/projects/pipeline_schedules/_tabs.html.haml b/app/views/projects/pipeline_schedules/_tabs.html.haml
index 7fcb624a9dd..8996c1b3e38 100644
--- a/app/views/projects/pipeline_schedules/_tabs.html.haml
+++ b/app/views/projects/pipeline_schedules/_tabs.html.haml
@@ -1,4 +1,4 @@
-%ul.nav-links
+%ul.nav-links.mobile-separator
%li{ class: active_when(scope.nil?) }>
= link_to schedule_path_proc.call(nil) do
= s_("PipelineSchedules|All")
diff --git a/app/views/projects/pipeline_schedules/_variable_row.html.haml b/app/views/projects/pipeline_schedules/_variable_row.html.haml
deleted file mode 100644
index 564cb5d1ca9..00000000000
--- a/app/views/projects/pipeline_schedules/_variable_row.html.haml
+++ /dev/null
@@ -1,17 +0,0 @@
-- id = local_assigns.fetch(:id, nil)
-- key = local_assigns.fetch(:key, "")
-- value = local_assigns.fetch(:value, "")
-%li.js-row.pipeline-variable-row{ data: { is_persisted: "#{!id.nil?}" } }
- .pipeline-variable-row-body
- %input{ type: "hidden", name: "schedule[variables_attributes][][id]", value: id }
- %input.js-destroy-input{ type: "hidden", name: "schedule[variables_attributes][][_destroy]" }
- %input.js-user-input.pipeline-variable-key-input.form-control{ type: "text",
- name: "schedule[variables_attributes][][key]",
- value: key,
- placeholder: s_('PipelineSchedules|Input variable key') }
- %textarea.js-user-input.pipeline-variable-value-input.form-control{ rows: 1,
- name: "schedule[variables_attributes][][value]",
- placeholder: s_('PipelineSchedules|Input variable value') }
- = value
- %button.js-row-remove-button.pipeline-variable-row-remove-button{ 'aria-label': s_('PipelineSchedules|Remove variable row') }
- %i.fa.fa-minus-circle{ 'aria-hidden': "true" }
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index 398a1c46746..5de17977d5a 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -1,7 +1,7 @@
- failed_builds = @pipeline.statuses.latest.failed
.tabs-holder
- %ul.pipelines-tabs.nav-links.no-top.no-bottom
+ %ul.pipelines-tabs.nav-links.no-top.no-bottom.mobile-separator
%li.js-pipeline-tab-link
= link_to project_pipeline_path(@project, @pipeline), data: { target: 'div#js-tab-pipeline', action: 'pipelines', toggle: 'tab' }, class: 'pipeline-tab' do
Pipeline
diff --git a/app/views/projects/pipelines_settings/_show.html.haml b/app/views/projects/pipelines_settings/_show.html.haml
index c5f9f5aa15b..646c01c0989 100644
--- a/app/views/projects/pipelines_settings/_show.html.haml
+++ b/app/views/projects/pipelines_settings/_show.html.haml
@@ -31,7 +31,7 @@
.radio
= form.label :enabled_ do
= form.radio_button :enabled, ''
- %strong Instance default (#{current_application_settings.auto_devops_enabled? ? 'enabled' : 'disabled'})
+ %strong Instance default (#{Gitlab::CurrentSettings.auto_devops_enabled? ? 'enabled' : 'disabled'})
%br
%span.descr
Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific <code>.gitlab-ci.yml</code>.
diff --git a/app/views/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml
index 67607e4e9c6..b037b57e78a 100644
--- a/app/views/projects/runners/_shared_runners.html.haml
+++ b/app/views/projects/runners/_shared_runners.html.haml
@@ -1,8 +1,8 @@
%h3 Shared Runners
.bs-callout.bs-callout-warning.shared-runners-description
- - if current_application_settings.shared_runners_text.present?
- = markdown_field(current_application_settings, :shared_runners_text)
+ - if Gitlab::CurrentSettings.shared_runners_text.present?
+ = markdown_field(Gitlab::CurrentSettings.current_application_settings, :shared_runners_text)
- else
GitLab Shared Runners execute code of different projects on the same Runner
unless you configure GitLab Runner Autoscale with MaxBuilds 1 (which it is
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index d3e867e124c..888d820b04e 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -47,7 +47,7 @@
- if @repository.gitlab_ci_yml
%li
- = link_to _('CI configuration'), ci_configuration_path(@project)
+ = link_to _('CI/CD configuration'), ci_configuration_path(@project)
- if current_user && can_push_branch?(@project, @project.default_branch)
- unless @repository.changelog
@@ -65,7 +65,7 @@
- unless @repository.gitlab_ci_yml
%li.missing
= link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do
- #{ _('Set up CI') }
+ #{ _('Set up CI/CD') }
- if koding_enabled? && @repository.koding_yml.blank?
%li.missing
= link_to _('Set up Koding'), add_koding_stack_path(@project)
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 55e45a5e954..3d5f92f9aaa 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -24,7 +24,7 @@
.wiki
= markdown_field(release, :description)
- .row-fixed-content.controls
+ .row-fixed-content.controls.flex-row
= render 'projects/buttons/download', project: @project, ref: tag.name, pipeline: @tags_pipelines[tag.name]
- if can?(current_user, :push_code, @project)
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index 4e265bf733a..d285251d06f 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -9,7 +9,13 @@
.form-group
.col-sm-12= f.label :title, class: 'control-label-full-width'
- .col-sm-12= f.text_field :title, class: 'form-control', value: @page.title
+ .col-sm-12
+ = f.text_field :title, class: 'form-control', value: @page.title
+ - if @page.persisted?
+ %span.edit-wiki-page-slug-tip
+ = icon('lightbulb-o')
+ = s_("WikiEditPageTip|Tip: You can move this page by adding the path to the beginning of the title.")
+ = link_to icon('question-circle'), help_page_path('user/project/wiki/index', anchor: 'moving-a-wiki-page'), target: '_blank'
.form-group
.col-sm-12= f.label :format, class: 'control-label-full-width'
.col-sm-12
diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml
index 0d77e5bd16d..9d3d4072027 100644
--- a/app/views/projects/wikis/edit.html.haml
+++ b/app/views/projects/wikis/edit.html.haml
@@ -1,10 +1,7 @@
- @content_class = "limit-container-width limit-container-width-sm" unless fluid_layout
- page_title _("Edit"), @page.title.capitalize, _("Wiki")
-- if @conflict
- .alert.alert-danger
- - page_link = link_to s_("WikiPageConflictMessage|the page"), project_wiki_path(@project, @page), target: "_blank"
- = (s_("WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs.") % { page_link: page_link }).html_safe
+= wiki_page_errors(@error)
.wiki-page-header.has-sidebar-toggle
%button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
diff --git a/app/views/shared/_delete_label_modal.html.haml b/app/views/shared/_delete_label_modal.html.haml
new file mode 100644
index 00000000000..01effefc34d
--- /dev/null
+++ b/app/views/shared/_delete_label_modal.html.haml
@@ -0,0 +1,20 @@
+.modal{ id: "modal-delete-label-#{label.id}", tabindex: -1 }
+ .modal-dialog
+ .modal-content
+ .modal-header
+ %button.close{ data: {dismiss: 'modal' } } &times;
+ %h3.page-title Delete #{render_colored_label(label, tooltip: false)} ?
+
+ .modal-body
+ %p
+ %strong= label.name
+ %span will be permanently deleted from #{label.is_a?(ProjectLabel)? label.project.name : label.group.name}. This cannot be undone.
+
+ .modal-footer
+ %a{ href: '#', data: { dismiss: 'modal' }, class: 'btn btn-default' } Cancel
+
+ = link_to 'Delete label',
+ destroy_label_path(label),
+ title: 'Delete',
+ method: :delete,
+ class: 'btn btn-remove'
diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml
index 151aad306a0..e7fa7477e0c 100644
--- a/app/views/shared/_event_filter.html.haml
+++ b/app/views/shared/_event_filter.html.haml
@@ -1,11 +1,14 @@
-%ul.nav-links.event-filter.scrolling-tabs
- = event_filter_link EventFilter.all, _('All'), s_('EventFilterBy|Filter by all')
- - if event_filter_visible(:repository)
- = event_filter_link EventFilter.push, _('Push events'), s_('EventFilterBy|Filter by push events')
- - if event_filter_visible(:merge_requests)
- = event_filter_link EventFilter.merged, _('Merge events'), s_('EventFilterBy|Filter by merge events')
- - if event_filter_visible(:issues)
- = event_filter_link EventFilter.issue, _('Issue events'), s_('EventFilterBy|Filter by issue events')
- - if comments_visible?
- = event_filter_link EventFilter.comments, _('Comments'), s_('EventFilterBy|Filter by comments')
- = event_filter_link EventFilter.team, _('Team'), s_('EventFilterBy|Filter by team')
+.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
+ .fade-left= icon('angle-left')
+ .fade-right= icon('angle-right')
+ %ul.nav-links.event-filter.scrolling-tabs
+ = event_filter_link EventFilter.all, _('All'), s_('EventFilterBy|Filter by all')
+ - if event_filter_visible(:repository)
+ = event_filter_link EventFilter.push, _('Push events'), s_('EventFilterBy|Filter by push events')
+ - if event_filter_visible(:merge_requests)
+ = event_filter_link EventFilter.merged, _('Merge events'), s_('EventFilterBy|Filter by merge events')
+ - if event_filter_visible(:issues)
+ = event_filter_link EventFilter.issue, _('Issue events'), s_('EventFilterBy|Filter by issue events')
+ - if comments_visible?
+ = event_filter_link EventFilter.comments, _('Comments'), s_('EventFilterBy|Filter by comments')
+ = event_filter_link EventFilter.team, _('Team'), s_('EventFilterBy|Filter by team')
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index 8e88cecaf9e..c0eebdfaddd 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -5,10 +5,10 @@
- show_label_merge_requests_link = show_label_issuables_link?(label, :merge_requests, project: @project)
- show_label_issues_link = show_label_issuables_link?(label, :issues, project: @project)
-%li{ id: label_css_id, data: { id: label.id } }
+%li.label-list-item{ id: label_css_id, data: { id: label.id } }
= render "shared/label_row", label: label
- .visible-xs.visible-sm-inline-block.visible-md-inline-block.dropdown
+ .visible-xs.visible-sm-inline-block.dropdown
%button.btn.btn-default.label-options-toggle{ type: 'button', data: { toggle: "dropdown" } }
Options
= icon('caret-down')
@@ -46,14 +46,19 @@
data: {confirm: 'Remove this label? Are you sure?'},
class: 'text-danger'
- .pull-right.hidden-xs.hidden-sm.hidden-md
- - if show_label_merge_requests_link
- = link_to_label(label, subject: subject, type: :merge_request, css_class: 'btn btn-transparent btn-action btn-link') do
- view merge requests
- - if show_label_issues_link
- = link_to_label(label, subject: subject, css_class: 'btn btn-transparent btn-action btn-link') do
- view open issues
-
+ .pull-right.hidden-xs.hidden-sm
+ - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group)
+ = link_to promote_project_label_path(label.project, label), title: "Promote to Group Label", class: 'btn btn-transparent btn-action', data: {confirm: "You are about to promote #{label.title} to a group level. This will make this milestone available to all projects inside #{label.project.group.name}. The existing project label will be merged into the group level. This action cannot be reversed.", toggle: "tooltip"}, method: :post do
+ %span.sr-only Promote to Group
+ = sprite_icon('level-up')
+ - if can?(current_user, :admin_label, label)
+ = link_to edit_label_path(label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do
+ %span.sr-only Edit
+ = sprite_icon('pencil')
+ %span{ data: { toggle: 'modal', target: "#modal-delete-label-#{label.id}" } }
+ = link_to "#", title: "Delete", class: 'btn btn-transparent btn-action remove-row', data: { toggle: "tooltip" } do
+ %span.sr-only Delete
+ = sprite_icon('remove')
- if current_user
.label-subscription.inline
- if can_subscribe_to_label_in_different_levels?(label)
@@ -76,14 +81,4 @@
%span= label_subscription_toggle_button_text(label, @project)
= icon('spinner spin', class: 'label-subscribe-button-loading')
- - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group)
- = link_to promote_project_label_path(label.project, label), title: "Promote to Group Label", class: 'btn btn-transparent btn-action', data: {confirm: "You are about to promote #{label.title} to a group level. This will make this milestone available to all projects inside #{label.project.group.name}. The existing project label will be merged into the group level. This action cannot be reversed.", toggle: "tooltip"}, method: :post do
- %span.sr-only Promote to Group
- = icon('level-up')
- - if can?(current_user, :admin_label, label)
- = link_to edit_label_path(label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do
- %span.sr-only Edit
- = icon('pencil-square-o')
- = link_to destroy_label_path(label), title: "Delete", class: 'btn btn-transparent btn-action remove-row', method: :delete, data: {confirm: label_deletion_confirm_text(label), toggle: "tooltip"} do
- %span.sr-only Delete
- = icon('trash-o')
+= render 'shared/delete_label_modal', label: label
diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml
index 7f58298c60f..bd4f191502e 100644
--- a/app/views/shared/_label_row.html.haml
+++ b/app/views/shared/_label_row.html.haml
@@ -1,3 +1,7 @@
+- subject = local_assigns[:subject]
+- show_label_issues_link = show_label_issuables_link?(label, :issues, project: @project)
+- show_label_merge_requests_link = show_label_issuables_link?(label, :merge_requests, project: @project)
+
%span.label-row
- if can?(current_user, :admin_label, @project)
.draggable-handler
@@ -13,6 +17,14 @@
- if defined?(@project) && @project.group.present?
%span.label-type
= label.model_name.human.titleize
- - if label.description
- %span.label-description
- = markdown_field(label, :description)
+
+ %span.label-description
+ - if label.description.present?
+ .description-text
+ = markdown_field(label, :description)
+ .hidden-xs.hidden-sm
+ - if show_label_issues_link
+ = link_to_label(label, subject: subject) { 'Issues' }
+ - if show_label_merge_requests_link
+ &middot;
+ = link_to_label(label, subject: subject, type: :merge_request) { 'Merge requests' }
diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml
index db2ac1e1d12..034b76b978f 100644
--- a/app/views/shared/_milestones_filter.html.haml
+++ b/app/views/shared/_milestones_filter.html.haml
@@ -1,4 +1,4 @@
-%ul.nav-links
+%ul.nav-links.mobile-separator
%li{ class: milestone_class_for_state(params[:state], 'opened', true) }>
= link_to milestones_filter_path(state: 'opened') do
Open
diff --git a/app/views/shared/builds/_tabs.html.haml b/app/views/shared/builds/_tabs.html.haml
index 639f28cc210..0b003125912 100644
--- a/app/views/shared/builds/_tabs.html.haml
+++ b/app/views/shared/builds/_tabs.html.haml
@@ -1,4 +1,4 @@
-%ul.nav-links
+%ul.nav-links.mobile-separator
%li{ class: active_when(scope.nil?) }>
= link_to build_path_proc.call(nil) do
All
diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml
index 6d8a4668cec..4d8109eb90c 100644
--- a/app/views/shared/issuable/_nav.html.haml
+++ b/app/views/shared/issuable/_nav.html.haml
@@ -1,7 +1,7 @@
- type = local_assigns.fetch(:type, :issues)
- page_context_word = type.to_s.humanize(capitalize: false)
-%ul.nav-links.issues-state-filters
+%ul.nav-links.issues-state-filters.mobile-separator
%li{ class: active_when(params[:state] == 'opened') }>
= link_to page_filter_path(state: 'opened', label: true), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened.", data: { state: 'opened' } do
#{issuables_state_counter_text(type, :opened)}
diff --git a/app/views/snippets/_snippets_scope_menu.html.haml b/app/views/snippets/_snippets_scope_menu.html.haml
index 8b6a98a054a..65aa4fbc757 100644
--- a/app/views/snippets/_snippets_scope_menu.html.haml
+++ b/app/views/snippets/_snippets_scope_menu.html.haml
@@ -1,7 +1,7 @@
- subject = local_assigns.fetch(:subject, current_user)
- include_private = local_assigns.fetch(:include_private, false)
-.nav-links.snippet-scope-menu
+.nav-links.snippet-scope-menu.mobile-separator
%li{ class: active_when(params[:scope].nil?) }
= link_to subject_snippets_path(subject) do
All
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 50e876b1d19..f2c20114534 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -43,6 +43,7 @@
- pipeline_creation:run_pipeline_schedule
- pipeline_default:build_coverage
- pipeline_default:build_trace_sections
+- pipeline_default:create_trace_artifact
- pipeline_default:pipeline_metrics
- pipeline_default:pipeline_notification
- pipeline_default:update_head_pipeline_for_merge_request
diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb
index 97d80305bec..b5ed8d607b3 100644
--- a/app/workers/build_finished_worker.rb
+++ b/app/workers/build_finished_worker.rb
@@ -6,9 +6,13 @@ class BuildFinishedWorker
def perform(build_id)
Ci::Build.find_by(id: build_id).try do |build|
- BuildTraceSectionsWorker.perform_async(build.id)
+ # We execute that in sync as this access the files in order to access local file, and reduce IO
+ BuildTraceSectionsWorker.new.perform(build.id)
BuildCoverageWorker.new.perform(build.id)
- BuildHooksWorker.new.perform(build.id)
+
+ # We execute that async as this are two indepentent operations that can be executed after TraceSections and Coverage
+ BuildHooksWorker.perform_async(build.id)
+ CreateTraceArtifactWorker.perform_async(build.id)
end
end
end
diff --git a/app/workers/create_trace_artifact_worker.rb b/app/workers/create_trace_artifact_worker.rb
new file mode 100644
index 00000000000..11cda58021e
--- /dev/null
+++ b/app/workers/create_trace_artifact_worker.rb
@@ -0,0 +1,10 @@
+class CreateTraceArtifactWorker
+ include ApplicationWorker
+ include PipelineQueue
+
+ def perform(job_id)
+ Ci::Build.preload(:project, :user).find_by(id: job_id).try do |job|
+ Ci::CreateTraceArtifactService.new(job.project, job.user).execute(job)
+ end
+ end
+end
diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb
index 8e26275669e..7ba224d74c8 100644
--- a/app/workers/git_garbage_collect_worker.rb
+++ b/app/workers/git_garbage_collect_worker.rb
@@ -1,6 +1,5 @@
class GitGarbageCollectWorker
include ApplicationWorker
- include Gitlab::CurrentSettings
sidekiq_options retry: false
@@ -102,7 +101,7 @@ class GitGarbageCollectWorker
end
def bitmaps_enabled?
- current_application_settings.housekeeping_bitmaps_enabled
+ Gitlab::CurrentSettings.housekeeping_bitmaps_enabled
end
def git(write_bitmaps:)
diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb
index f19bcbf946a..a993b4b2680 100644
--- a/app/workers/project_cache_worker.rb
+++ b/app/workers/project_cache_worker.rb
@@ -18,6 +18,8 @@ class ProjectCacheWorker
update_statistics(project, statistics.map(&:to_sym))
project.repository.refresh_method_caches(files.map(&:to_sym))
+
+ project.cleanup
end
def update_statistics(project, statistics = [])
diff --git a/app/workers/upload_checksum_worker.rb b/app/workers/upload_checksum_worker.rb
index 9222760c031..65d40336f18 100644
--- a/app/workers/upload_checksum_worker.rb
+++ b/app/workers/upload_checksum_worker.rb
@@ -3,7 +3,7 @@ class UploadChecksumWorker
def perform(upload_id)
upload = Upload.find(upload_id)
- upload.calculate_checksum
+ upload.calculate_checksum!
upload.save!
rescue ActiveRecord::RecordNotFound
Rails.logger.error("UploadChecksumWorker: couldn't find upload #{upload_id}, skipping")
diff --git a/changelogs/unreleased/14256-upload-destroy-removes-file.yml b/changelogs/unreleased/14256-upload-destroy-removes-file.yml
new file mode 100644
index 00000000000..d97188e23f1
--- /dev/null
+++ b/changelogs/unreleased/14256-upload-destroy-removes-file.yml
@@ -0,0 +1,5 @@
+---
+title: Deleting an upload will correctly clean up the filesystem.
+merge_request: 16799
+author:
+type: fixed
diff --git a/changelogs/unreleased/24167__color_label.yml b/changelogs/unreleased/24167__color_label.yml
new file mode 100644
index 00000000000..68c6c731163
--- /dev/null
+++ b/changelogs/unreleased/24167__color_label.yml
@@ -0,0 +1,5 @@
+---
+title: Add Colors to GitLab Flavored Markdown
+merge_request: 16095
+author: Tony Rom <thetonyrom@gmail.com>
+type: added
diff --git a/changelogs/unreleased/25327-coverage-badge-rounding.yml b/changelogs/unreleased/25327-coverage-badge-rounding.yml
new file mode 100644
index 00000000000..ea985689484
--- /dev/null
+++ b/changelogs/unreleased/25327-coverage-badge-rounding.yml
@@ -0,0 +1,5 @@
+---
+title: Show coverage to two decimal points in coverage badge
+merge_request: 10083
+author: Jeff Stubler
+type: changed
diff --git a/changelogs/unreleased/26468-fix-sort-by-recent-sign-in.yml b/changelogs/unreleased/26468-fix-sort-by-recent-sign-in.yml
new file mode 100644
index 00000000000..a2c81f6c995
--- /dev/null
+++ b/changelogs/unreleased/26468-fix-sort-by-recent-sign-in.yml
@@ -0,0 +1,4 @@
+---
+title: Fix Sort by Recent Sign-in in Admin Area
+merge_request: 13852
+author: Poornima M
diff --git a/changelogs/unreleased/30106-group-issues.yml b/changelogs/unreleased/30106-group-issues.yml
new file mode 100644
index 00000000000..d24996e6087
--- /dev/null
+++ b/changelogs/unreleased/30106-group-issues.yml
@@ -0,0 +1,5 @@
+---
+title: Include subgroup issues and merge requests on the group page
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/31885-ability-to-transfer-groups-to-another-group.yml b/changelogs/unreleased/31885-ability-to-transfer-groups-to-another-group.yml
new file mode 100644
index 00000000000..d2a5802af64
--- /dev/null
+++ b/changelogs/unreleased/31885-ability-to-transfer-groups-to-another-group.yml
@@ -0,0 +1,5 @@
+---
+title: Add ability to transfer a group into another group
+merge_request: 16302
+author:
+type: added
diff --git a/changelogs/unreleased/32282-add-foreign-keys-to-todos.yml b/changelogs/unreleased/32282-add-foreign-keys-to-todos.yml
new file mode 100644
index 00000000000..e74c2a8b9ff
--- /dev/null
+++ b/changelogs/unreleased/32282-add-foreign-keys-to-todos.yml
@@ -0,0 +1,5 @@
+---
+title: Add foreign key and NOT NULL constraints to todos table.
+merge_request: 16849
+author:
+type: other
diff --git a/changelogs/unreleased/32283-trending-projects-unique-constraint2.yml b/changelogs/unreleased/32283-trending-projects-unique-constraint2.yml
new file mode 100644
index 00000000000..4fd6b6fddc4
--- /dev/null
+++ b/changelogs/unreleased/32283-trending-projects-unique-constraint2.yml
@@ -0,0 +1,5 @@
+---
+title: Add unique constraint to trending_projects#project_id.
+merge_request: 16846
+author:
+type: other
diff --git a/changelogs/unreleased/35285-user-interface-bugs-for-schedule-pipelines.yml b/changelogs/unreleased/35285-user-interface-bugs-for-schedule-pipelines.yml
new file mode 100644
index 00000000000..f3a04469884
--- /dev/null
+++ b/changelogs/unreleased/35285-user-interface-bugs-for-schedule-pipelines.yml
@@ -0,0 +1,5 @@
+---
+title: Hide pipeline schedule take ownership for current owner
+merge_request: 12986
+author:
+type: fixed
diff --git a/changelogs/unreleased/35779-realtime-update-of-pipeline-status-in-files-view.yml b/changelogs/unreleased/35779-realtime-update-of-pipeline-status-in-files-view.yml
new file mode 100644
index 00000000000..82df00fe631
--- /dev/null
+++ b/changelogs/unreleased/35779-realtime-update-of-pipeline-status-in-files-view.yml
@@ -0,0 +1,5 @@
+---
+title: Add realtime ci status for the repository -> files view
+merge_request: 16523
+author:
+type: added
diff --git a/changelogs/unreleased/39985-enable-prometheus-metrics-for-deployed-ingresses.yml b/changelogs/unreleased/39985-enable-prometheus-metrics-for-deployed-ingresses.yml
new file mode 100644
index 00000000000..5c45d0db602
--- /dev/null
+++ b/changelogs/unreleased/39985-enable-prometheus-metrics-for-deployed-ingresses.yml
@@ -0,0 +1,5 @@
+---
+title: Enable Prometheus metrics for deployed Ingresses
+merge_request: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/16866
+author: joshlambert
+type: changed
diff --git a/changelogs/unreleased/40552-sanitize-extra-blank-spaces-used-when-uploading-a-ssh-key.yml b/changelogs/unreleased/40552-sanitize-extra-blank-spaces-used-when-uploading-a-ssh-key.yml
new file mode 100644
index 00000000000..9e4811ca308
--- /dev/null
+++ b/changelogs/unreleased/40552-sanitize-extra-blank-spaces-used-when-uploading-a-ssh-key.yml
@@ -0,0 +1,5 @@
+---
+title: Sanitize extra blank spaces used when uploading a SSH key
+merge_request: 40552
+author:
+type: fixed
diff --git a/changelogs/unreleased/40793-fix-mr-title-for-jira.yml b/changelogs/unreleased/40793-fix-mr-title-for-jira.yml
new file mode 100644
index 00000000000..69461510a87
--- /dev/null
+++ b/changelogs/unreleased/40793-fix-mr-title-for-jira.yml
@@ -0,0 +1,5 @@
+---
+title: Prevent JIRA issue identifier from being humanized.
+merge_request: 16491
+author: Andrew McCallum
+type: fixed
diff --git a/changelogs/unreleased/41672-emphasize-gke-cluster-to-new-users.yml b/changelogs/unreleased/41672-emphasize-gke-cluster-to-new-users.yml
new file mode 100644
index 00000000000..6b0d443e097
--- /dev/null
+++ b/changelogs/unreleased/41672-emphasize-gke-cluster-to-new-users.yml
@@ -0,0 +1,5 @@
+---
+title: Add blue dot feature highlight to make GKE Clusters more visible to users
+merge_request: 16379
+author:
+type: added
diff --git a/changelogs/unreleased/42270-fix-namespace-remove-exports-for-hashed-storage.yml b/changelogs/unreleased/42270-fix-namespace-remove-exports-for-hashed-storage.yml
new file mode 100644
index 00000000000..d7a8b6e6f81
--- /dev/null
+++ b/changelogs/unreleased/42270-fix-namespace-remove-exports-for-hashed-storage.yml
@@ -0,0 +1,6 @@
+---
+title: Fix export removal for hashed-storage projects within a renamed or deleted
+ namespace
+merge_request: 16658
+author:
+type: fixed
diff --git a/changelogs/unreleased/42497-rubocop-style-regexpliteral.yml b/changelogs/unreleased/42497-rubocop-style-regexpliteral.yml
new file mode 100644
index 00000000000..6053883bac4
--- /dev/null
+++ b/changelogs/unreleased/42497-rubocop-style-regexpliteral.yml
@@ -0,0 +1,5 @@
+---
+title: Enable RuboCop Style/RegexpLiteral
+merge_request: 16752
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/42547-upload-store-mount-point.yml b/changelogs/unreleased/42547-upload-store-mount-point.yml
new file mode 100644
index 00000000000..35ae022984e
--- /dev/null
+++ b/changelogs/unreleased/42547-upload-store-mount-point.yml
@@ -0,0 +1,5 @@
+---
+title: Added uploader metadata to the uploads.
+merge_request: 16779
+author:
+type: added
diff --git a/changelogs/unreleased/42584-fix-margins-in-tag-list.yml b/changelogs/unreleased/42584-fix-margins-in-tag-list.yml
new file mode 100644
index 00000000000..38b3dd85fd8
--- /dev/null
+++ b/changelogs/unreleased/42584-fix-margins-in-tag-list.yml
@@ -0,0 +1,5 @@
+---
+title: Fixes different margins between buttons in tag list
+merge_request: 16927
+author: Jacopo Beschi @jacopo-beschi
+type: fixed
diff --git a/changelogs/unreleased/42669-allow-order_by-users-in-gitlab-api.yml b/changelogs/unreleased/42669-allow-order_by-users-in-gitlab-api.yml
new file mode 100644
index 00000000000..24fcc38ee0e
--- /dev/null
+++ b/changelogs/unreleased/42669-allow-order_by-users-in-gitlab-api.yml
@@ -0,0 +1,5 @@
+---
+title: Add sorting options for /users API (admin only)
+merge_request: 16945
+author:
+type: added
diff --git a/changelogs/unreleased/42684-set-up-ci-set-up-ci-cd.yml b/changelogs/unreleased/42684-set-up-ci-set-up-ci-cd.yml
new file mode 100644
index 00000000000..0ef28e2ee01
--- /dev/null
+++ b/changelogs/unreleased/42684-set-up-ci-set-up-ci-cd.yml
@@ -0,0 +1,5 @@
+---
+title: Rename button to enable CI/CD configuration to "Set up CI/CD"
+merge_request: 16870
+author:
+type: changed
diff --git a/changelogs/unreleased/42693-42693-add-a-link-to-documentation-on-how-to-get-external-ip-in-the-kubernetes-cluster-details-page.yml b/changelogs/unreleased/42693-42693-add-a-link-to-documentation-on-how-to-get-external-ip-in-the-kubernetes-cluster-details-page.yml
new file mode 100644
index 00000000000..aeadf8ffc4a
--- /dev/null
+++ b/changelogs/unreleased/42693-42693-add-a-link-to-documentation-on-how-to-get-external-ip-in-the-kubernetes-cluster-details-page.yml
@@ -0,0 +1,5 @@
+---
+title: Add a link to documentation on how to get external ip in the Kubernetes cluster details page
+merge_request: 16937
+author:
+type: added
diff --git a/changelogs/unreleased/42696-gitlab-import-leaves-group_id-on-projectlabel.yml b/changelogs/unreleased/42696-gitlab-import-leaves-group_id-on-projectlabel.yml
new file mode 100644
index 00000000000..c46cc86c99b
--- /dev/null
+++ b/changelogs/unreleased/42696-gitlab-import-leaves-group_id-on-projectlabel.yml
@@ -0,0 +1,5 @@
+---
+title: Fix GitLab import leaving group_id on ProjectLabel
+merge_request: 16877
+author:
+type: fixed
diff --git a/changelogs/unreleased/42730-close-rugged-repository.yml b/changelogs/unreleased/42730-close-rugged-repository.yml
new file mode 100644
index 00000000000..a632f5030a5
--- /dev/null
+++ b/changelogs/unreleased/42730-close-rugged-repository.yml
@@ -0,0 +1,5 @@
+---
+title: Close low level rugged repository in project cache worker
+merge_request: 16930
+author: Bastian Blank
+type: fixed
diff --git a/changelogs/unreleased/add-confirmation-input-for-modals.yml b/changelogs/unreleased/add-confirmation-input-for-modals.yml
new file mode 100644
index 00000000000..ff1027bc55a
--- /dev/null
+++ b/changelogs/unreleased/add-confirmation-input-for-modals.yml
@@ -0,0 +1,5 @@
+---
+title: Add confirmation-input component
+merge_request: 16816
+author:
+type: other
diff --git a/changelogs/unreleased/bump-workhorse.yml b/changelogs/unreleased/bump-workhorse.yml
new file mode 100644
index 00000000000..37ee402dac7
--- /dev/null
+++ b/changelogs/unreleased/bump-workhorse.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade GitLab Workhorse to v3.6.0
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/bvl-fix-500-on-fork-without-restricted-visibility-levels.yml b/changelogs/unreleased/bvl-fix-500-on-fork-without-restricted-visibility-levels.yml
new file mode 100644
index 00000000000..378f0ef7ce9
--- /dev/null
+++ b/changelogs/unreleased/bvl-fix-500-on-fork-without-restricted-visibility-levels.yml
@@ -0,0 +1,5 @@
+---
+title: Fix forking projects when no restricted visibility levels are defined applicationwide
+merge_request: 16881
+author:
+type: fixed
diff --git a/changelogs/unreleased/dm-user-namespace-route-path-validation.yml b/changelogs/unreleased/dm-user-namespace-route-path-validation.yml
new file mode 100644
index 00000000000..36615e5b976
--- /dev/null
+++ b/changelogs/unreleased/dm-user-namespace-route-path-validation.yml
@@ -0,0 +1,5 @@
+---
+title: Validate user namespace before saving so that errors persist on model
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/feature-sm-artifacts-trace.yml b/changelogs/unreleased/feature-sm-artifacts-trace.yml
new file mode 100644
index 00000000000..7654ce58aeb
--- /dev/null
+++ b/changelogs/unreleased/feature-sm-artifacts-trace.yml
@@ -0,0 +1,5 @@
+---
+title: Save traces as artifacts
+merge_request: 16702
+author:
+type: changed
diff --git a/changelogs/unreleased/fix-adjust-button-group-width-on-mobile.yml b/changelogs/unreleased/fix-adjust-button-group-width-on-mobile.yml
new file mode 100644
index 00000000000..b79ec2944fd
--- /dev/null
+++ b/changelogs/unreleased/fix-adjust-button-group-width-on-mobile.yml
@@ -0,0 +1,5 @@
+---
+title: Change button group width on mobile
+merge_request: 16726
+author: George Tsiolis
+type: fixed
diff --git a/changelogs/unreleased/fix-improve-issue-note-dropdown.yml b/changelogs/unreleased/fix-improve-issue-note-dropdown.yml
new file mode 100644
index 00000000000..aaf4811c64a
--- /dev/null
+++ b/changelogs/unreleased/fix-improve-issue-note-dropdown.yml
@@ -0,0 +1,5 @@
+---
+title: Improve issue note dropdown and mr button
+merge_request: 16758
+author: George Tsiolis
+type: changed
diff --git a/changelogs/unreleased/fix-show-sidebar-sub-level-items-for-billing.yml b/changelogs/unreleased/fix-show-sidebar-sub-level-items-for-billing.yml
new file mode 100644
index 00000000000..883eecabe04
--- /dev/null
+++ b/changelogs/unreleased/fix-show-sidebar-sub-level-items-for-billing.yml
@@ -0,0 +1,5 @@
+---
+title: Override group sidebar links
+merge_request: 16942
+author: George Tsiolis
+type: fixed
diff --git a/changelogs/unreleased/fj-22607-lowercase-usernames-from-ldap.yml b/changelogs/unreleased/fj-22607-lowercase-usernames-from-ldap.yml
new file mode 100644
index 00000000000..77142528be2
--- /dev/null
+++ b/changelogs/unreleased/fj-22607-lowercase-usernames-from-ldap.yml
@@ -0,0 +1,5 @@
+---
+title: Added ldap config setting to lower case the username
+merge_request: 16791
+author:
+type: added
diff --git a/changelogs/unreleased/fj-37273-moving-wiki-pages-from-the-ui.yml b/changelogs/unreleased/fj-37273-moving-wiki-pages-from-the-ui.yml
new file mode 100644
index 00000000000..5b5310dcfef
--- /dev/null
+++ b/changelogs/unreleased/fj-37273-moving-wiki-pages-from-the-ui.yml
@@ -0,0 +1,5 @@
+---
+title: Allow moving wiki pages from the UI
+merge_request: 16313
+author:
+type: fixed
diff --git a/changelogs/unreleased/issue-42689-new-file-template.yml b/changelogs/unreleased/issue-42689-new-file-template.yml
new file mode 100644
index 00000000000..d6b77b87605
--- /dev/null
+++ b/changelogs/unreleased/issue-42689-new-file-template.yml
@@ -0,0 +1,5 @@
+---
+title: Trigger change event on filename input when file template is applied
+merge_request: 16911
+author: Sebastian Klingler
+type: fixed
diff --git a/changelogs/unreleased/issues-closed-at-steal.yml b/changelogs/unreleased/issues-closed-at-steal.yml
new file mode 100644
index 00000000000..a5f0898995f
--- /dev/null
+++ b/changelogs/unreleased/issues-closed-at-steal.yml
@@ -0,0 +1,5 @@
+---
+title: Finish any remaining jobs for issues.closed_at
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/jej-upload-file-tracks-lfs.yml b/changelogs/unreleased/jej-upload-file-tracks-lfs.yml
new file mode 100644
index 00000000000..a7cf6b6ba2c
--- /dev/null
+++ b/changelogs/unreleased/jej-upload-file-tracks-lfs.yml
@@ -0,0 +1,5 @@
+---
+title: File Upload UI can create LFS pointers based on .gitattributes
+merge_request: 16412
+author:
+type: fixed
diff --git a/changelogs/unreleased/osw-markdown-bypass-for-commit-messages.yml b/changelogs/unreleased/osw-markdown-bypass-for-commit-messages.yml
new file mode 100644
index 00000000000..b2c1cd9710a
--- /dev/null
+++ b/changelogs/unreleased/osw-markdown-bypass-for-commit-messages.yml
@@ -0,0 +1,5 @@
+---
+title: Bypass commits title markdown on notes
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/osw-system-notes-for-commits-regression.yml b/changelogs/unreleased/osw-system-notes-for-commits-regression.yml
new file mode 100644
index 00000000000..6d0943c7716
--- /dev/null
+++ b/changelogs/unreleased/osw-system-notes-for-commits-regression.yml
@@ -0,0 +1,5 @@
+---
+title: Reload MRs memoization after diffs creation
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/persistent-callouts.yml b/changelogs/unreleased/persistent-callouts.yml
new file mode 100644
index 00000000000..ca949a3b96c
--- /dev/null
+++ b/changelogs/unreleased/persistent-callouts.yml
@@ -0,0 +1,5 @@
+---
+title: Add backend for persistently dismissably callouts
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/query-counts.yml b/changelogs/unreleased/query-counts.yml
new file mode 100644
index 00000000000..e01ff8a4ad8
--- /dev/null
+++ b/changelogs/unreleased/query-counts.yml
@@ -0,0 +1,5 @@
+---
+title: Track and act upon the number of executed queries
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/refactor-ci-variable-list-for-future-usage-in-4110.yml b/changelogs/unreleased/refactor-ci-variable-list-for-future-usage-in-4110.yml
new file mode 100644
index 00000000000..d43675e175d
--- /dev/null
+++ b/changelogs/unreleased/refactor-ci-variable-list-for-future-usage-in-4110.yml
@@ -0,0 +1,5 @@
+---
+title: Hide variable values on pipeline schedule edit page
+merge_request: 16729
+author:
+type: changed
diff --git a/changelogs/unreleased/style-include-branch-in-mobile-view.yml b/changelogs/unreleased/style-include-branch-in-mobile-view.yml
new file mode 100644
index 00000000000..5c8ef86992d
--- /dev/null
+++ b/changelogs/unreleased/style-include-branch-in-mobile-view.yml
@@ -0,0 +1,5 @@
+---
+title: Include branch in mobile view for pipelines
+merge_request: 16910
+author: George Tsiolis
+type: other
diff --git a/changelogs/unreleased/winh-kubernetes-clusters.yml b/changelogs/unreleased/winh-kubernetes-clusters.yml
new file mode 100644
index 00000000000..387a719848d
--- /dev/null
+++ b/changelogs/unreleased/winh-kubernetes-clusters.yml
@@ -0,0 +1,5 @@
+---
+title: Replace "cluster" with "Kubernetes cluster"
+merge_request: 16778
+author:
+type: changed
diff --git a/changelogs/unreleased/zj-protobuf.yml b/changelogs/unreleased/zj-protobuf.yml
new file mode 100644
index 00000000000..830c2e82da9
--- /dev/null
+++ b/changelogs/unreleased/zj-protobuf.yml
@@ -0,0 +1,5 @@
+---
+title: Downgrade google-protobuf gem
+merge_request: 16941
+author:
+type: other
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 25f4085deb2..bbc2bcfb0cc 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -152,6 +152,12 @@ production: &base
# The location where LFS objects are stored (default: shared/lfs-objects).
# storage_path: shared/lfs-objects
+ ## Uploads (attachments, avatars, etc...)
+ uploads:
+ # The location where uploads objects are stored (default: public/).
+ # storage_path: public/
+ # base_dir: uploads/-/system
+
## GitLab Pages
pages:
enabled: false
@@ -364,6 +370,9 @@ production: &base
first_name: 'givenName'
last_name: 'sn'
+ # If lowercase_usernames is enabled, GitLab will lower case the username.
+ lowercase_usernames: false
+
# GitLab EE only: add more LDAP servers
# Choose an ID made of a-z and 0-9 . This ID will be stored in the database
# so that GitLab can remember which LDAP server a user belongs to.
@@ -644,6 +653,8 @@ test:
enabled: false
artifacts:
path: tmp/tests/artifacts
+ uploads:
+ storage_path: tmp/tests/public
gitlab:
host: localhost
port: 80
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 899e612ffbd..28e05bfc18d 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -110,7 +110,7 @@ class Settings < Settingslogic
url = "http://#{url}" unless url.start_with?('http')
# Get rid of the path so that we don't even have to encode it
- url_without_path = url.sub(%r{(https?://[^\/]+)/?.*}, '\1')
+ url_without_path = url.sub(%r{(https?://[^/]+)/?.*}, '\1')
URI.parse(url_without_path).host
end
@@ -151,6 +151,7 @@ if Settings.ldap['enabled'] || Rails.env.test?
server['allow_username_or_email_login'] = false if server['allow_username_or_email_login'].nil?
server['active_directory'] = true if server['active_directory'].nil?
server['attributes'] = {} if server['attributes'].nil?
+ server['lowercase_usernames'] = false if server['lowercase_usernames'].nil?
server['provider_name'] ||= "ldap#{key}".downcase
server['provider_class'] = OmniAuth::Utils.camelize(server['provider_name'])
@@ -300,8 +301,10 @@ Settings.incoming_email['enabled'] = false if Settings.incoming_email['enabled']
#
Settings['artifacts'] ||= Settingslogic.new({})
Settings.artifacts['enabled'] = true if Settings.artifacts['enabled'].nil?
-Settings.artifacts['path'] = Settings.absolute(Settings.artifacts['path'] || File.join(Settings.shared['path'], "artifacts"))
-Settings.artifacts['max_size'] ||= 100 # in megabytes
+Settings.artifacts['storage_path'] = Settings.absolute(Settings.artifacts.values_at('path', 'storage_path').compact.first || File.join(Settings.shared['path'], "artifacts"))
+# Settings.artifact['path'] is deprecated, use `storage_path` instead
+Settings.artifacts['path'] = Settings.artifacts['storage_path']
+Settings.artifacts['max_size'] ||= 100 # in megabytes
#
# Registry
@@ -339,6 +342,13 @@ Settings.lfs['enabled'] = true if Settings.lfs['enabled'].nil?
Settings.lfs['storage_path'] = Settings.absolute(Settings.lfs['storage_path'] || File.join(Settings.shared['path'], "lfs-objects"))
#
+# Uploads
+#
+Settings['uploads'] ||= Settingslogic.new({})
+Settings.uploads['storage_path'] = Settings.absolute(Settings.uploads['storage_path'] || 'public')
+Settings.uploads['base_dir'] = Settings.uploads['base_dir'] || 'uploads/-/system'
+
+#
# Mattermost
#
Settings['mattermost'] ||= Settingslogic.new({})
@@ -469,10 +479,10 @@ end
# repository_downloads_path value.
#
repositories_storages = Settings.repositories.storages.values
-repository_downloads_path = Settings.gitlab['repository_downloads_path'].to_s.gsub(/\/$/, '')
+repository_downloads_path = Settings.gitlab['repository_downloads_path'].to_s.gsub(%r{/$}, '')
repository_downloads_full_path = File.expand_path(repository_downloads_path, Settings.gitlab['user_home'])
-if repository_downloads_path.blank? || repositories_storages.any? { |rs| [repository_downloads_path, repository_downloads_full_path].include?(rs['path'].gsub(/\/$/, '')) }
+if repository_downloads_path.blank? || repositories_storages.any? { |rs| [repository_downloads_path, repository_downloads_full_path].include?(rs['path'].gsub(%r{/$}, '')) }
Settings.gitlab['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive')
end
diff --git a/config/initializers/active_record_data_types.rb b/config/initializers/active_record_data_types.rb
index 0359e14b232..fda13d0c4cb 100644
--- a/config/initializers/active_record_data_types.rb
+++ b/config/initializers/active_record_data_types.rb
@@ -54,7 +54,7 @@ elsif Gitlab::Database.mysql?
def initialize_type_map(mapping)
super mapping
- mapping.register_type(%r(timestamp)i) do |sql_type|
+ mapping.register_type(/timestamp/i) do |sql_type|
precision = extract_precision(sql_type)
ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::MysqlDateTimeWithTimeZone.new(precision: precision)
end
diff --git a/config/initializers/gollum.rb b/config/initializers/gollum.rb
index 0b86cac51a7..6dfaceb8427 100644
--- a/config/initializers/gollum.rb
+++ b/config/initializers/gollum.rb
@@ -35,6 +35,88 @@ module Gollum
[]
end
end
+
+ # Remove if https://github.com/gollum/gollum-lib/pull/292 has been merged
+ def update_page(page, name, format, data, commit = {})
+ name = name ? ::File.basename(name) : page.name
+ format ||= page.format
+ dir = ::File.dirname(page.path)
+ dir = '' if dir == '.'
+ filename = (rename = page.name != name) ? Gollum::Page.cname(name) : page.filename_stripped
+
+ multi_commit = !!commit[:committer]
+ committer = multi_commit ? commit[:committer] : Committer.new(self, commit)
+
+ if !rename && page.format == format
+ committer.add(page.path, normalize(data))
+ else
+ committer.delete(page.path)
+ committer.add_to_index(dir, filename, format, data)
+ end
+
+ committer.after_commit do |index, _sha|
+ @access.refresh
+ index.update_working_dir(dir, page.filename_stripped, page.format)
+ index.update_working_dir(dir, filename, format)
+ end
+
+ multi_commit ? committer : committer.commit
+ end
+
+ # Remove if https://github.com/gollum/gollum-lib/pull/292 has been merged
+ def rename_page(page, rename, commit = {})
+ return false if page.nil?
+ return false if rename.nil? || rename.empty?
+
+ (target_dir, target_name) = ::File.split(rename)
+ (source_dir, source_name) = ::File.split(page.path)
+ source_name = page.filename_stripped
+
+ # File.split gives us relative paths with ".", commiter.add_to_index doesn't like that.
+ target_dir = '' if target_dir == '.'
+ source_dir = '' if source_dir == '.'
+ target_dir = target_dir.gsub(/^\//, '') # rubocop:disable Style/RegexpLiteral
+
+ # if the rename is a NOOP, abort
+ if source_dir == target_dir && source_name == target_name
+ return false
+ end
+
+ multi_commit = !!commit[:committer]
+ committer = multi_commit ? commit[:committer] : Committer.new(self, commit)
+
+ # This piece only works for multi_commit
+ # If we are in a commit batch and one of the previous operations
+ # has updated the page, any information we ask to the page can be outdated.
+ # Therefore, we should ask first to the current committer tree to see if
+ # there is any updated change.
+ raw_data = raw_data_in_committer(committer, source_dir, page.filename) ||
+ raw_data_in_committer(committer, source_dir, "#{target_name}.#{Page.format_to_ext(page.format)}") ||
+ page.raw_data
+
+ committer.delete(page.path)
+ committer.add_to_index(target_dir, target_name, page.format, raw_data)
+
+ committer.after_commit do |index, _sha|
+ @access.refresh
+ index.update_working_dir(source_dir, source_name, page.format)
+ index.update_working_dir(target_dir, target_name, page.format)
+ end
+
+ multi_commit ? committer : committer.commit
+ end
+
+ # Remove if https://github.com/gollum/gollum-lib/pull/292 has been merged
+ def raw_data_in_committer(committer, dir, filename)
+ data = nil
+
+ [*dir.split(::File::SEPARATOR), filename].each do |key|
+ data = data ? data[key] : committer.tree[key]
+ break unless data
+ end
+
+ data
+ end
end
module Git
diff --git a/config/initializers/query_limiting.rb b/config/initializers/query_limiting.rb
new file mode 100644
index 00000000000..66864d1898e
--- /dev/null
+++ b/config/initializers/query_limiting.rb
@@ -0,0 +1,9 @@
+if Gitlab::QueryLimiting.enable?
+ require_dependency 'gitlab/query_limiting/active_support_subscriber'
+ require_dependency 'gitlab/query_limiting/transaction'
+ require_dependency 'gitlab/query_limiting/middleware'
+
+ Gitlab::Application.configure do |config|
+ config.middleware.use(Gitlab::QueryLimiting::Middleware)
+ end
+end
diff --git a/config/routes.rb b/config/routes.rb
index f162043dd5e..e72ea1881cd 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -60,6 +60,9 @@ Rails.application.routes.draw do
resources :issues, module: :boards, only: [:index, :update]
end
+
+ # UserCallouts
+ resources :user_callouts, only: [:create]
end
# Koding route
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index 57e401c9b89..3cca1210e39 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -1,5 +1,5 @@
namespace :admin do
- resources :users, constraints: { id: /[a-zA-Z.\/0-9_\-]+/ } do
+ resources :users, constraints: { id: %r{[a-zA-Z./0-9_\-]+} } do
resources :keys, only: [:show, :destroy]
resources :identities, except: [:show]
resources :impersonation_tokens, only: [:index, :create] do
diff --git a/config/routes/group.rb b/config/routes/group.rb
index 976837a246d..b17611d8623 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -14,6 +14,7 @@ constraints(GroupUrlConstrainer.new) do
get :merge_requests, as: :merge_requests_group
get :projects, as: :projects_group
get :activity, as: :activity_group
+ put :transfer, as: :transfer_group
end
get '/', action: :show, as: :group_canonical
@@ -35,7 +36,7 @@ constraints(GroupUrlConstrainer.new) do
post :toggle_subscription, on: :member
end
- resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :edit, :update, :new, :create] do
+ resources :milestones, constraints: { id: %r{[^/]+} }, only: [:index, :show, :edit, :update, :new, :create] do
member do
get :merge_requests
get :participants
@@ -52,7 +53,7 @@ constraints(GroupUrlConstrainer.new) do
resources :uploads, only: [:create] do
collection do
- get ":secret/:filename", action: :show, as: :show, constraints: { filename: /[^\/]+/ }
+ get ":secret/:filename", action: :show, as: :show, constraints: { filename: %r{[^/]+} }
end
end
end
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 0496bd85b4e..bcaa68c8ce5 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -40,7 +40,7 @@ constraints(ProjectUrlConstrainer.new) do
#
# Templates
#
- get '/templates/:template_type/:key' => 'templates#show', as: :template, constraints: { key: /[^\/]+/ }
+ get '/templates/:template_type/:key' => 'templates#show', as: :template, constraints: { key: %r{[^/]+} }
resource :avatar, only: [:show, :destroy]
resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do
@@ -55,7 +55,7 @@ constraints(ProjectUrlConstrainer.new) do
end
resource :pages, only: [:show, :destroy] do
- resources :domains, only: [:show, :new, :create, :destroy], controller: 'pages_domains', constraints: { id: /[^\/]+/ }
+ resources :domains, only: [:show, :new, :create, :destroy], controller: 'pages_domains', constraints: { id: %r{[^/]+} }
end
resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do
@@ -65,7 +65,7 @@ constraints(ProjectUrlConstrainer.new) do
end
end
- resources :services, constraints: { id: /[^\/]+/ }, only: [:index, :edit, :update] do
+ resources :services, constraints: { id: %r{[^/]+} }, only: [:index, :edit, :update] do
member do
put :test
end
@@ -346,7 +346,7 @@ constraints(ProjectUrlConstrainer.new) do
end
end
- resources :project_members, except: [:show, :new, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ }, concerns: :access_requestable do
+ resources :project_members, except: [:show, :new, :edit], constraints: { id: %r{[a-zA-Z./0-9_\-#%+]+} }, concerns: :access_requestable do
collection do
delete :leave
@@ -379,7 +379,7 @@ constraints(ProjectUrlConstrainer.new) do
resources :uploads, only: [:create] do
collection do
- get ":secret/:filename", action: :show, as: :show, constraints: { filename: /[^\/]+/ }
+ get ":secret/:filename", action: :show, as: :show, constraints: { filename: %r{[^/]+} }
end
end
diff --git a/config/routes/uploads.rb b/config/routes/uploads.rb
index d7bca8310e4..6370645bcb9 100644
--- a/config/routes/uploads.rb
+++ b/config/routes/uploads.rb
@@ -2,17 +2,17 @@ scope path: :uploads do
# Note attachments and User/Group/Project avatars
get "-/system/:model/:mounted_as/:id/:filename",
to: "uploads#show",
- constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /[^\/]+/ }
+ constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: %r{[^/]+} }
# show uploads for models, snippets (notes) available for now
get '-/system/:model/:id/:secret/:filename',
to: 'uploads#show',
- constraints: { model: /personal_snippet/, id: /\d+/, filename: /[^\/]+/ }
+ constraints: { model: /personal_snippet/, id: /\d+/, filename: %r{[^/]+} }
# show temporary uploads
get '-/system/temp/:secret/:filename',
to: 'uploads#show',
- constraints: { filename: /[^\/]+/ }
+ constraints: { filename: %r{[^/]+} }
# Appearance
get "-/system/:model/:mounted_as/:id/:filename",
@@ -22,7 +22,7 @@ scope path: :uploads do
# Project markdown uploads
get ":namespace_id/:project_id/:secret/:filename",
to: "projects/uploads#show",
- constraints: { namespace_id: /[a-zA-Z.0-9_\-]+/, project_id: /[a-zA-Z.0-9_\-]+/, filename: /[^\/]+/ }
+ constraints: { namespace_id: /[a-zA-Z.0-9_\-]+/, project_id: /[a-zA-Z.0-9_\-]+/, filename: %r{[^/]+} }
# create uploads for models, snippets (notes) available for now
post ':model',
@@ -34,4 +34,4 @@ end
# Redirect old note attachments path to new uploads path.
get "files/note/:id/:filename",
to: redirect("uploads/note/attachment/%{id}/%{filename}"),
- constraints: { filename: /[^\/]+/ }
+ constraints: { filename: %r{[^/]+} }
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 783677b5b8d..7f3fe551a03 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -3,6 +3,7 @@
var crypto = require('crypto');
var fs = require('fs');
var path = require('path');
+var glob = require('glob');
var webpack = require('webpack');
var StatsWriterPlugin = require('webpack-stats-plugin').StatsWriterPlugin;
var CopyWebpackPlugin = require('copy-webpack-plugin');
@@ -20,6 +21,26 @@ var DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false';
var WEBPACK_REPORT = process.env.WEBPACK_REPORT;
var NO_COMPRESSION = process.env.NO_COMPRESSION;
+// generate automatic entry points
+var autoEntries = {};
+var pageEntries = glob.sync('pages/**/index.js', { cwd: path.join(ROOT_PATH, 'app/assets/javascripts') });
+
+// filter out entries currently imported dynamically in dispatcher.js
+var dispatcher = fs.readFileSync(path.join(ROOT_PATH, 'app/assets/javascripts/dispatcher.js')).toString();
+var dispatcherChunks = dispatcher.match(/(?!import\('.\/)pages\/[^']+/g);
+
+pageEntries.forEach(( path ) => {
+ let chunkPath = path.replace(/\/index\.js$/, '');
+ if (!dispatcherChunks.includes(chunkPath)) {
+ let chunkName = chunkPath.replace(/\//g, '.');
+ autoEntries[chunkName] = './' + path;
+ }
+});
+
+// report our auto-generated bundle count
+var autoEntriesCount = Object.keys(autoEntries).length;
+console.log(`${autoEntriesCount} entries from '/pages' automatically added to webpack output.`);
+
var config = {
// because sqljs requires fs.
node: {
@@ -301,6 +322,8 @@ var config = {
}
}
+config.entry = Object.assign({}, autoEntries, config.entry);
+
if (IS_PRODUCTION) {
config.devtool = 'source-map';
config.plugins.push(
diff --git a/db/migrate/20180119135717_add_uploader_index_to_uploads.rb b/db/migrate/20180119135717_add_uploader_index_to_uploads.rb
new file mode 100644
index 00000000000..a678c3d049f
--- /dev/null
+++ b/db/migrate/20180119135717_add_uploader_index_to_uploads.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 AddUploaderIndexToUploads < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ remove_concurrent_index :uploads, :path
+ add_concurrent_index :uploads, [:uploader, :path], using: :btree
+ end
+
+ def down
+ remove_concurrent_index :uploads, [:uploader, :path]
+ add_concurrent_index :uploads, :path, using: :btree
+ end
+end
diff --git a/db/migrate/20180125214301_create_user_callouts.rb b/db/migrate/20180125214301_create_user_callouts.rb
new file mode 100644
index 00000000000..856eff36ae0
--- /dev/null
+++ b/db/migrate/20180125214301_create_user_callouts.rb
@@ -0,0 +1,16 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class CreateUserCallouts < ActiveRecord::Migration
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def change
+ create_table :user_callouts do |t|
+ t.integer :feature_name, null: false
+ t.references :user, index: true, foreign_key: { on_delete: :cascade }, null: false
+ end
+
+ add_index :user_callouts, [:user_id, :feature_name], unique: true
+ end
+end
diff --git a/db/migrate/20180129193323_add_uploads_builder_context.rb b/db/migrate/20180129193323_add_uploads_builder_context.rb
new file mode 100644
index 00000000000..b3909a770ca
--- /dev/null
+++ b/db/migrate/20180129193323_add_uploads_builder_context.rb
@@ -0,0 +1,14 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddUploadsBuilderContext < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def change
+ add_column :uploads, :mount_point, :string
+ add_column :uploads, :secret, :string
+ end
+end
diff --git a/db/migrate/20180201102129_add_unique_constraint_to_trending_projects_project_id.rb b/db/migrate/20180201102129_add_unique_constraint_to_trending_projects_project_id.rb
new file mode 100644
index 00000000000..02e53b8fa8a
--- /dev/null
+++ b/db/migrate/20180201102129_add_unique_constraint_to_trending_projects_project_id.rb
@@ -0,0 +1,19 @@
+class AddUniqueConstraintToTrendingProjectsProjectId < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :trending_projects, :project_id, unique: true, name: 'index_trending_projects_on_project_id_unique'
+ remove_concurrent_index_by_name :trending_projects, 'index_trending_projects_on_project_id'
+ rename_index :trending_projects, 'index_trending_projects_on_project_id_unique', 'index_trending_projects_on_project_id'
+ end
+
+ def down
+ rename_index :trending_projects, 'index_trending_projects_on_project_id', 'index_trending_projects_on_project_id_old'
+ add_concurrent_index :trending_projects, :project_id
+ remove_concurrent_index_by_name :trending_projects, 'index_trending_projects_on_project_id_old'
+ end
+end
diff --git a/db/migrate/20180201110056_add_foreign_keys_to_todos.rb b/db/migrate/20180201110056_add_foreign_keys_to_todos.rb
new file mode 100644
index 00000000000..b7c40f8c01a
--- /dev/null
+++ b/db/migrate/20180201110056_add_foreign_keys_to_todos.rb
@@ -0,0 +1,38 @@
+class AddForeignKeysToTodos < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ class Todo < ActiveRecord::Base
+ self.table_name = 'todos'
+ include EachBatch
+ end
+
+ BATCH_SIZE = 1000
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ Todo.where('NOT EXISTS ( SELECT true FROM users WHERE id=todos.user_id )').each_batch(of: BATCH_SIZE) do |batch|
+ batch.delete_all
+ end
+
+ Todo.where('NOT EXISTS ( SELECT true FROM users WHERE id=todos.author_id )').each_batch(of: BATCH_SIZE) do |batch|
+ batch.delete_all
+ end
+
+ Todo.where('note_id IS NOT NULL AND NOT EXISTS ( SELECT true FROM notes WHERE id=todos.note_id )').each_batch(of: BATCH_SIZE) do |batch|
+ batch.delete_all
+ end
+
+ add_concurrent_foreign_key :todos, :users, column: :user_id, on_delete: :cascade
+ add_concurrent_foreign_key :todos, :users, column: :author_id, on_delete: :cascade
+ add_concurrent_foreign_key :todos, :notes, column: :note_id, on_delete: :cascade
+ end
+
+ def down
+ remove_foreign_key :todos, :users
+ remove_foreign_key :todos, column: :author_id
+ remove_foreign_key :todos, :notes
+ end
+end
diff --git a/db/migrate/20180201145907_migrate_remaining_issues_closed_at.rb b/db/migrate/20180201145907_migrate_remaining_issues_closed_at.rb
new file mode 100644
index 00000000000..5a36dec6a9a
--- /dev/null
+++ b/db/migrate/20180201145907_migrate_remaining_issues_closed_at.rb
@@ -0,0 +1,55 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MigrateRemainingIssuesClosedAt < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class Issue < ActiveRecord::Base
+ self.table_name = 'issues'
+ include EachBatch
+ end
+
+ def up
+ Gitlab::BackgroundMigration.steal('CopyColumn')
+ Gitlab::BackgroundMigration.steal('CleanupConcurrentTypeChange')
+
+ if migrate_column_type?
+ if closed_at_for_type_change_exists?
+ migrate_remaining_rows
+ else
+ # Due to some EE merge problems some environments may not have the
+ # "closed_at_for_type_change" column. If this is the case we have no
+ # other option than to migrate the data _right now_.
+ change_column_type_concurrently(:issues, :closed_at, :datetime_with_timezone)
+ cleanup_concurrent_column_type_change(:issues, :closed_at)
+ end
+ end
+ end
+
+ def down
+ # Previous migrations already revert the changes made here.
+ end
+
+ def migrate_remaining_rows
+ Issue.where('closed_at_for_type_change IS NULL AND closed_at IS NOT NULL').each_batch do |batch|
+ batch.update_all('closed_at_for_type_change = closed_at')
+ end
+
+ cleanup_concurrent_column_type_change(:issues, :closed_at)
+ end
+
+ def migrate_column_type?
+ # Some environments may have already executed the previous version of this
+ # migration, thus we don't need to migrate those environments again.
+ column_for('issues', 'closed_at').type == :datetime # rubocop:disable Migration/Datetime
+ end
+
+ def closed_at_for_type_change_exists?
+ columns('issues').any? { |col| col.name == 'closed_at_for_type_change' }
+ end
+end
diff --git a/db/post_migrate/20171207150300_remove_project_labels_group_id_copy.rb b/db/post_migrate/20171207150300_remove_project_labels_group_id_copy.rb
new file mode 100644
index 00000000000..2f339172eeb
--- /dev/null
+++ b/db/post_migrate/20171207150300_remove_project_labels_group_id_copy.rb
@@ -0,0 +1,21 @@
+# Copy of 20180202111106 - this one should run before 20171207150343 to fix issues related to
+# the removal of groups with labels.
+
+class RemoveProjectLabelsGroupIdCopy < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ # rubocop:disable Migration/UpdateColumnInBatches
+ update_column_in_batches(:labels, :group_id, nil) do |table, query|
+ query.where(table[:type].eq('ProjectLabel').and(table[:group_id].not_eq(nil)))
+ end
+ # rubocop:enable Migration/UpdateColumnInBatches
+ end
+
+ def down
+ end
+end
diff --git a/db/post_migrate/20180202111106_remove_project_labels_group_id.rb b/db/post_migrate/20180202111106_remove_project_labels_group_id.rb
new file mode 100644
index 00000000000..db7fd0d167d
--- /dev/null
+++ b/db/post_migrate/20180202111106_remove_project_labels_group_id.rb
@@ -0,0 +1,19 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveProjectLabelsGroupId < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ update_column_in_batches(:labels, :group_id, nil) do |table, query|
+ query.where(table[:type].eq('ProjectLabel').and(table[:group_id].not_eq(nil)))
+ end
+ end
+
+ def down
+ end
+end
diff --git a/db/post_migrate/20180204200836_change_author_id_to_not_null_in_todos.rb b/db/post_migrate/20180204200836_change_author_id_to_not_null_in_todos.rb
new file mode 100644
index 00000000000..92c32feebf7
--- /dev/null
+++ b/db/post_migrate/20180204200836_change_author_id_to_not_null_in_todos.rb
@@ -0,0 +1,26 @@
+class ChangeAuthorIdToNotNullInTodos < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ class Todo < ActiveRecord::Base
+ self.table_name = 'todos'
+ include EachBatch
+ end
+
+ BATCH_SIZE = 1000
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ Todo.where(author_id: nil).each_batch(of: BATCH_SIZE) do |batch|
+ batch.delete_all
+ end
+
+ change_column_null :todos, :author_id, false
+ end
+
+ def down
+ change_column_null :todos, :author_id, true
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 4e82a688725..432eb095746 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: 20180115201419) do
+ActiveRecord::Schema.define(version: 20180204200836) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -1707,7 +1707,7 @@ ActiveRecord::Schema.define(version: 20180115201419) do
t.integer "project_id", null: false
t.integer "target_id"
t.string "target_type", null: false
- t.integer "author_id"
+ t.integer "author_id", null: false
t.integer "action", null: false
t.string "state", null: false
t.datetime "created_at"
@@ -1727,7 +1727,7 @@ ActiveRecord::Schema.define(version: 20180115201419) do
t.integer "project_id", null: false
end
- add_index "trending_projects", ["project_id"], name: "index_trending_projects_on_project_id", using: :btree
+ add_index "trending_projects", ["project_id"], name: "index_trending_projects_on_project_id", unique: true, using: :btree
create_table "u2f_registrations", force: :cascade do |t|
t.text "certificate"
@@ -1751,11 +1751,13 @@ ActiveRecord::Schema.define(version: 20180115201419) do
t.string "model_type"
t.string "uploader", null: false
t.datetime "created_at", null: false
+ t.string "mount_point"
+ t.string "secret"
end
add_index "uploads", ["checksum"], name: "index_uploads_on_checksum", using: :btree
add_index "uploads", ["model_id", "model_type"], name: "index_uploads_on_model_id_and_model_type", using: :btree
- add_index "uploads", ["path"], name: "index_uploads_on_path", using: :btree
+ add_index "uploads", ["uploader", "path"], name: "index_uploads_on_uploader_and_path", using: :btree
create_table "user_agent_details", force: :cascade do |t|
t.string "user_agent", null: false
@@ -1769,6 +1771,14 @@ ActiveRecord::Schema.define(version: 20180115201419) do
add_index "user_agent_details", ["subject_id", "subject_type"], name: "index_user_agent_details_on_subject_id_and_subject_type", using: :btree
+ create_table "user_callouts", force: :cascade do |t|
+ t.integer "feature_name", null: false
+ t.integer "user_id", null: false
+ end
+
+ add_index "user_callouts", ["user_id", "feature_name"], name: "index_user_callouts_on_user_id_and_feature_name", unique: true, using: :btree
+ add_index "user_callouts", ["user_id"], name: "index_user_callouts_on_user_id", using: :btree
+
create_table "user_custom_attributes", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
@@ -2037,9 +2047,13 @@ ActiveRecord::Schema.define(version: 20180115201419) do
add_foreign_key "system_note_metadata", "notes", name: "fk_d83a918cb1", on_delete: :cascade
add_foreign_key "timelogs", "issues", name: "fk_timelogs_issues_issue_id", on_delete: :cascade
add_foreign_key "timelogs", "merge_requests", name: "fk_timelogs_merge_requests_merge_request_id", on_delete: :cascade
+ add_foreign_key "todos", "notes", name: "fk_91d1f47b13", on_delete: :cascade
add_foreign_key "todos", "projects", name: "fk_45054f9c45", on_delete: :cascade
+ add_foreign_key "todos", "users", column: "author_id", name: "fk_ccf0373936", on_delete: :cascade
+ add_foreign_key "todos", "users", name: "fk_d94154aa95", on_delete: :cascade
add_foreign_key "trending_projects", "projects", on_delete: :cascade
add_foreign_key "u2f_registrations", "users"
+ add_foreign_key "user_callouts", "users", on_delete: :cascade
add_foreign_key "user_custom_attributes", "users", on_delete: :cascade
add_foreign_key "user_synced_attributes_metadata", "users", on_delete: :cascade
add_foreign_key "users_star_projects", "projects", name: "fk_22cd27ddfc", on_delete: :cascade
diff --git a/doc/README.md b/doc/README.md
index 330670587f5..c8b6b4f32b8 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -8,23 +8,13 @@ comments: false
Welcome to [GitLab](https://about.gitlab.com/), a Git-based fully featured
platform for software development!
-GitLab offers the most scalable Git-based fully integrated platform for software development, with flexible products and subscription plans:
+GitLab offers the most scalable Git-based fully integrated platform for software development, with flexible products and subscription plans.
-- **GitLab Community Edition (CE)** is an [open source product](https://gitlab.com/gitlab-org/gitlab-ce/),
-self-hosted, free to use. Every feature available in GitLab CE is also available on GitLab Enterprise Edition (Starter and Premium) and GitLab.com.
-- **GitLab Enterprise Edition (EE)** is an [open-core product](https://gitlab.com/gitlab-org/gitlab-ee/),
-self-hosted, fully featured solution of GitLab, available under distinct [subscriptions](https://about.gitlab.com/products/): **GitLab Enterprise Edition Starter (EES)**, **GitLab Enterprise Edition Premium (EEP)**, and **GitLab Enterprise Edition Ultimate (EEU)**.
-- **GitLab.com**: SaaS GitLab solution, with [free and paid subscriptions](https://about.gitlab.com/gitlab-com/). GitLab.com is hosted by GitLab, Inc., and administrated by GitLab (users don't have access to admin settings).
+With GitLab self-hosted, you deploy your own GitLab instance on-premises or on a private cloud of your choice. GitLab self-hosted is available for [free and with paid subscriptions](https://about.gitlab.com/products/): Libre, Starter, Premium, and Ultimate.
-> **GitLab EE** contains all features available in **GitLab CE**,
-plus premium features available in each version: **Enterprise Edition Starter**
-(**EES**), **Enterprise Edition Premium** (**EEP**), and **Enterprise Edition Ultimate**
-(**EEU**). Everything available in **EES** is also available in **EEP**. Every feature
-available in **EEP** is also available in **EEU**.
+GitLab.com is our SaaS offering. It's hosted, managed, and administered by GitLab, with [free and paid plans](https://about.gitlab.com/gitlab-com/) for individuals and teams: Free, Bronze, Silver, and Gold.
-----
-
-Shortcuts to GitLab's most visited docs:
+## Shortcuts to GitLab's most visited docs
| [GitLab CI/CD](ci/README.md) | Other |
| :----- | :----- |
@@ -134,14 +124,8 @@ Manage your [repositories](user/project/repository/index.md) from the UI (user i
## Administrator documentation
-[Administration documentation](administration/index.md) applies to admin users of GitLab
-self-hosted instances:
-
-- GitLab Community Edition
-- GitLab [Enterprise Editions](https://about.gitlab.com/gitlab-ee/)
- - Enterprise Edition Starter (EES)
- - Enterprise Edition Premium (EEP)
- - Enterprise Edition Ultimate (EEU)
+[Administration documentation](administration/index.md) applies to admin users of [GitLab
+self-hosted instances](#self-hosted-gitlab): Libre, Starter, Premium, Ultimate.
Learn how to install, configure, update, upgrade, integrate, and maintain your own instance.
Regular users don't have access to GitLab administration tools and settings.
diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md
index 881b6a827f4..63fbb24bac1 100644
--- a/doc/administration/auth/ldap.md
+++ b/doc/administration/auth/ldap.md
@@ -181,6 +181,10 @@ main: # 'main' is the GitLab 'provider ID' of this LDAP server
first_name: 'givenName'
last_name: 'sn'
+ # If lowercase_usernames is enabled, GitLab will lower case the username.
+ lowercase_usernames: false
+
+
## EE only
# Base where we can search for groups
@@ -290,6 +294,41 @@ In other words, if an existing GitLab user wants to enable LDAP sign-in for
themselves, they should check that their GitLab email address matches their
LDAP email address, and then sign into GitLab via their LDAP credentials.
+## Enabling LDAP username lowercase
+
+Some LDAP servers, depending on their configurations, can return uppercase usernames. This can lead to several confusing issues like, for example, creating links or namespaces with uppercase names.
+
+GitLab can automatically lowercase usernames provided by the LDAP server by enabling
+the configuration option `lowercase_usernames`. By default, this configuration option is `false`.
+
+**Omnibus configuration**
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_rails['ldap_servers'] = YAML.load <<-EOS
+ main:
+ # snip...
+ lowercase_usernames: true
+ EOS
+ ```
+
+2. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
+
+**Source configuration**
+
+1. Edit `config/gitlab.yaml`:
+
+ ```yaml
+ production:
+ ldap:
+ servers:
+ main:
+ # snip...
+ lowercase_usernames: true
+ ```
+2. [Restart GitLab](../restart_gitlab.md#installations-from-source) for the changes to take effect.
+
## Encryption
### TLS Server Authentication
diff --git a/doc/administration/environment_variables.md b/doc/administration/environment_variables.md
index 9bcd13a52f7..e6c8f59549f 100644
--- a/doc/administration/environment_variables.md
+++ b/doc/administration/environment_variables.md
@@ -13,7 +13,7 @@ override certain values.
Variable | Type | Description
-------- | ---- | -----------
-`GITLAB_CDN_HOST` | string | Sets the hostname for a CDN to serve static assets (e.g. `mycdnsubdomain.fictional-cdn.com`)
+`GITLAB_CDN_HOST` | string | Sets the base URL for a CDN to serve static assets (e.g. `//mycdnsubdomain.fictional-cdn.com`)
`GITLAB_ROOT_PASSWORD` | string | Sets the password for the `root` user on installation
`GITLAB_HOST` | string | The full URL of the GitLab server (including `http://` or `https://`)
`RAILS_ENV` | string | The Rails environment; can be one of `production`, `development`, `staging` or `test`
diff --git a/doc/api/users.md b/doc/api/users.md
index 1da6fcf297d..2082e45756a 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -51,6 +51,11 @@ GET /users?blocked=true
GET /users
```
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `order_by` | string | no | Return projects ordered by `id`, `name`, `username`, `created_at`, or `updated_at` fields. Default is `id` |
+| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
+
```json
[
{
diff --git a/doc/development/README.md b/doc/development/README.md
index 12cca9f84b7..45e9565f9a7 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -75,6 +75,7 @@ comments: false
- [Ordering table columns](ordering_table_columns.md)
- [Verifying database capabilities](verifying_database_capabilities.md)
- [Database Debugging and Troubleshooting](database_debugging.md)
+- [Query Count Limits](query_count_limits.md)
## Testing guides
diff --git a/doc/development/automatic_ce_ee_merge.md b/doc/development/automatic_ce_ee_merge.md
index 5a784b6de06..cf6314f9521 100644
--- a/doc/development/automatic_ce_ee_merge.md
+++ b/doc/development/automatic_ce_ee_merge.md
@@ -5,17 +5,32 @@ Enterprise Edition (look for the [`CE Upstream` merge requests]).
This merge is done automatically in a
[scheduled pipeline](https://gitlab.com/gitlab-org/release-tools/-/jobs/43201679).
-If a merge is already in progress, the job [doesn't create a new one](https://gitlab.com/gitlab-org/release-tools/-/jobs/43157687).
-**If you are pinged in a `CE Upstream` merge request to resolve a conflict,
-please resolve the conflict as soon as possible or ask someone else to do it!**
-
->**Note:**
-It's ok to resolve more conflicts than the one that you are asked to resolve. In
-that case, it's a good habit to ask for a double-check on your resolution by
-someone who is familiar with the code you touched.
+## What to do if you are pinged in a `CE Upstream` merge request to resolve a conflict?
+
+1. Please resolve the conflict as soon as possible or ask someone else to do it
+ - It's ok to resolve more conflicts than the one that you are asked to resolve.
+ In that case, it's a good habit to ask for a double-check on your resolution
+ by someone who is familiar with the code you touched.
+1. Once you have resolved your conflicts, push to the branch (no force-push)
+1. Assign the merge request to the next person that has to resolve a conflict
+1. If all conflicts are resolved after your resolution is pushed, keep the merge
+ request assigned to you: **you are now responsible for the merge request to be
+ green**
+1. If you need any help, you can ping the current [release managers], or ask in
+ the `#ce-to-ee` Slack channel
+
+A few notes about the automatic CE->EE merge job:
+
+- If a merge is already in progress, the job
+ [doesn't create a new one](https://gitlab.com/gitlab-org/release-tools/-/jobs/43157687).
+- If there is nothing to merge (i.e. EE is up-to-date with CE), the job doesn't
+ create a new one
+- The job posts messages to the `#ce-to-ee` Slack channel to inform what's the
+ current CE->EE merge status (e.g. "A new MR has been created", "A MR is still pending")
[`CE Upstream` merge requests]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests?label_name%5B%5D=CE+upstream
+[release managers]: https://about.gitlab.com/release-managers/
## Always merge EE merge requests before their CE counterparts
diff --git a/doc/development/file_storage.md b/doc/development/file_storage.md
index cf00e24e11a..34a02bd2c3c 100644
--- a/doc/development/file_storage.md
+++ b/doc/development/file_storage.md
@@ -14,9 +14,9 @@ There are many places where file uploading is used, according to contexts:
- User snippet attachments
* Project
- Project avatars
- - Issues/MR Markdown attachments
- - Issues/MR Legacy Markdown attachments
- - CI Build Artifacts
+ - Issues/MR/Notes Markdown attachments
+ - Issues/MR/Notes Legacy Markdown attachments
+ - CI Artifacts (archive, metadata, trace)
- LFS Objects
@@ -25,7 +25,7 @@ There are many places where file uploading is used, according to contexts:
GitLab started saving everything on local disk. While directory location changed from previous versions,
they are still not 100% standardized. You can see them below:
-| Description | In DB? | Relative path | Uploader class | model_type |
+| Description | In DB? | Relative path (from CarrierWave.root) | Uploader class | model_type |
| ------------------------------------- | ------ | ----------------------------------------------------------- | ---------------------- | ---------- |
| Instance logo | yes | uploads/-/system/appearance/logo/:id/:filename | `AttachmentUploader` | Appearance |
| Header logo | yes | uploads/-/system/appearance/header_logo/:id/:filename | `AttachmentUploader` | Appearance |
@@ -33,17 +33,107 @@ they are still not 100% standardized. You can see them below:
| User avatars | yes | uploads/-/system/user/avatar/:id/:filename | `AvatarUploader` | User |
| User snippet attachments | yes | uploads/-/system/personal_snippet/:id/:random_hex/:filename | `PersonalFileUploader` | Snippet |
| Project avatars | yes | uploads/-/system/project/avatar/:id/:filename | `AvatarUploader` | Project |
-| Issues/MR Markdown attachments | yes | uploads/:project_path_with_namespace/:random_hex/:filename | `FileUploader` | Project |
-| Issues/MR Legacy Markdown attachments | no | uploads/-/system/note/attachment/:id/:filename | `AttachmentUploader` | Note |
-| CI Artifacts (CE) | yes | shared/artifacts/:year_:month/:project_id/:id | `ArtifactUploader` | Ci::Build |
+| Issues/MR/Notes Markdown attachments | yes | uploads/:project_path_with_namespace/:random_hex/:filename | `FileUploader` | Project |
+| Issues/MR/Notes Legacy Markdown attachments | no | uploads/-/system/note/attachment/:id/:filename | `AttachmentUploader` | Note |
+| CI Artifacts (CE) | yes | shared/artifacts/:disk_hash[0..1]/:disk_hash[2..3]/:disk_hash/:year_:month_:date/:job_id/:job_artifact_id (:disk_hash is SHA256 digest of project_id) | `JobArtifactUploader` | Ci::JobArtifact |
| LFS Objects (CE) | yes | shared/lfs-objects/:hex/:hex/:object_hash | `LfsObjectUploader` | LfsObject |
CI Artifacts and LFS Objects behave differently in CE and EE. In CE they inherit the `GitlabUploader`
-while in EE they inherit the `ObjectStoreUploader` and store files in and S3 API compatible object store.
+while in EE they inherit the `ObjectStorage` and store files in and S3 API compatible object store.
-In the case of Issues/MR Markdown attachments, there is a different approach using the [Hashed Storage] layout,
+In the case of Issues/MR/Notes Markdown attachments, there is a different approach using the [Hashed Storage] layout,
instead of basing the path into a mutable variable `:project_path_with_namespace`, it's possible to use the
hash of the project ID instead, if project migrates to the new approach (introduced in 10.2).
+### Path segments
+
+Files are stored at multiple locations and use different path schemes.
+All the `GitlabUploader` derived classes should comply with this path segment schema:
+
+```
+| GitlabUploader
+| ----------------------- + ------------------------- + --------------------------------- + -------------------------------- |
+| `<gitlab_root>/public/` | `uploads/-/system/` | `user/avatar/:id/` | `:filename` |
+| ----------------------- + ------------------------- + --------------------------------- + -------------------------------- |
+| `CarrierWave.root` | `GitlabUploader.base_dir` | `GitlabUploader#dynamic_segment` | `CarrierWave::Uploader#filename` |
+| | `CarrierWave::Uploader#store_dir` | |
+
+| FileUploader
+| ----------------------- + ------------------------- + --------------------------------- + -------------------------------- |
+| `<gitlab_root>/shared/` | `artifacts/` | `:year_:month/:id` | `:filename` |
+| `<gitlab_root>/shared/` | `snippets/` | `:secret/` | `:filename` |
+| ----------------------- + ------------------------- + --------------------------------- + -------------------------------- |
+| `CarrierWave.root` | `GitlabUploader.base_dir` | `GitlabUploader#dynamic_segment` | `CarrierWave::Uploader#filename` |
+| | `CarrierWave::Uploader#store_dir` | |
+| | | `FileUploader#upload_path |
+
+| ObjectStore::Concern (store = remote)
+| ----------------------- + ------------------------- + ----------------------------------- + -------------------------------- |
+| `<bucket_name>` | <ignored> | `user/avatar/:id/` | `:filename` |
+| ----------------------- + ------------------------- + ----------------------------------- + -------------------------------- |
+| `#fog_dir` | `GitlabUploader.base_dir` | `GitlabUploader#dynamic_segment` | `CarrierWave::Uploader#filename` |
+| | | `ObjectStorage::Concern#store_dir` | |
+| | | `ObjectStorage::Concern#upload_path |
+```
+
+The `RecordsUploads::Concern` concern will create an `Upload` entry for every file stored by a `GitlabUploader` persisting the dynamic parts of the path using
+`GitlabUploader#dynamic_path`. You may then use the `Upload#build_uploader` method to manipulate the file.
+
+## Object Storage
+
+By including the `ObjectStorage::Concern` in the `GitlabUploader` derived class, you may enable the object storage for this uploader. To enable the object storage
+in your uploader, you need to either 1) include `RecordsUpload::Concern` and prepend `ObjectStorage::Extension::RecordsUploads` or 2) mount the uploader and create a new field named `<mount>_store`.
+
+The `CarrierWave::Uploader#store_dir` is overriden to
+
+ - `GitlabUploader.base_dir` + `GitlabUploader.dynamic_segment` when the store is LOCAL
+ - `GitlabUploader.dynamic_segment` when the store is REMOTE (the bucket name is used to namespace)
+
+### Using `ObjectStorage::Extension::RecordsUploads`
+
+> Note: this concern will automatically include `RecordsUploads::Concern` if not already included.
+
+The `ObjectStorage::Concern` uploader will search for the matching `Upload` to select the correct object store. The `Upload` is mapped using `#store_dirs + identifier` for each store (LOCAL/REMOTE).
+
+```ruby
+class SongUploader < GitlabUploader
+ include RecordsUploads::Concern
+ include ObjectStorage::Concern
+ prepend ObjectStorage::Extension::RecordsUploads
+
+ ...
+end
+
+class Thing < ActiveRecord::Base
+ mount :theme, SongUploader # we have a great theme song!
+
+ ...
+end
+```
+
+### Using a mounted uploader
+
+The `ObjectStorage::Concern` will query the `model.<mount>_store` attribute to select the correct object store.
+This column must be present in the model schema.
+
+```ruby
+class SongUploader < GitlabUploader
+ include ObjectStorage::Concern
+
+ ...
+end
+
+class Thing < ActiveRecord::Base
+ attr_reader :theme_store # this is an ActiveRecord attribute
+ mount :theme, SongUploader # we have a great theme song!
+
+ def theme_store
+ super || ObjectStorage::Store::LOCAL
+ end
+
+ ...
+end
+```
+
[CarrierWave]: https://github.com/carrierwaveuploader/carrierwave
[Hashed Storage]: ../administration/repository_storage_types.md
diff --git a/doc/development/query_count_limits.md b/doc/development/query_count_limits.md
new file mode 100644
index 00000000000..ebb6e0c2dac
--- /dev/null
+++ b/doc/development/query_count_limits.md
@@ -0,0 +1,65 @@
+# Query Count Limits
+
+Each controller or API endpoint is allowed to execute up to 100 SQL queries. In
+a production environment we'll only log an error in case this threshold is
+exceeded, but in a test environment we'll raise an error instead.
+
+## Solving Failing Tests
+
+When a test fails because it executes more than 100 SQL queries there are two
+solutions to this problem:
+
+1. Reduce the number of SQL queries that are executed.
+2. Whitelist the controller or API endpoint.
+
+You should only resort to whitelisting when an existing controller or endpoint
+is to blame as in this case reducing the number of SQL queries can take a lot of
+effort. Newly added controllers and endpoints are not allowed to execute more
+than 100 SQL queries and no exceptions will be made for this rule. _If_ a large
+number of SQL queries is necessary to perform certain work it's best to have
+this work performed by Sidekiq instead of doing this directly in a web request.
+
+## Whitelisting
+
+In the event that you _have_ to whitelist a controller you'll first need to
+create an issue. This issue should (preferably in the title) mention the
+controller or endpoint and include the appropriate labels (`database`,
+`performance`, and at least a team specific label such as `Discussion`).
+
+Once the issue has been created you can whitelist the code in question. For
+Rails controllers it's best to create a `before_action` hook that runs as early
+as possible. The called method in turn should call
+`Gitlab::QueryLimiting.whitelist('issue URL here')`. For example:
+
+```ruby
+class MyController < ApplicationController
+ before_action :whitelist_query_limiting, only: [:show]
+
+ def index
+ # ...
+ end
+
+ def show
+ # ...
+ end
+
+ def whitelist_query_limiting
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/...')
+ end
+end
+```
+
+By using a `before_action` you don't have to modify the controller method in
+question, reducing the likelihood of merge conflicts.
+
+For Grape API endpoints there unfortunately is not a reliable way of running a
+hook before a specific endpoint. This means that you have to add the whitelist
+call directly into the endpoint like so:
+
+```ruby
+get '/projects/:id/foo' do
+ Gitlab::QueryLimiting.whitelist('...')
+
+ # ...
+end
+```
diff --git a/doc/install/kubernetes/gitlab_runner_chart.md b/doc/install/kubernetes/gitlab_runner_chart.md
index ca9c95aeced..1f53e12d5f8 100644
--- a/doc/install/kubernetes/gitlab_runner_chart.md
+++ b/doc/install/kubernetes/gitlab_runner_chart.md
@@ -31,7 +31,7 @@ The default configuration can always be found in the [values.yaml](https://gitla
In order for GitLab Runner to function, your config file **must** specify the following:
- - `gitlabURL` - the GitLab Server URL (with protocol) to register the runner against
+ - `gitlabUrl` - the GitLab Server URL (with protocol) to register the runner against
- `runnerRegistrationToken` - The Registration Token for adding new Runners to the GitLab Server. This must be
retrieved from your GitLab Instance. See the [GitLab Runner Documentation](../../ci/runners/README.md#creating-and-registering-a-runner) for more information.
@@ -47,7 +47,7 @@ Here is a snippet of the important settings:
## The GitLab Server URL (with protocol) that want to register the runner against
## ref: https://docs.gitlab.com/runner/commands/README.html#gitlab-runner-register
##
-gitlabURL: http://gitlab.your-domain.com/
+gitlabUrl: http://gitlab.your-domain.com/
## The Registration Token for adding new Runners to the GitLab Server. This must
## be retreived from your GitLab Instance.
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 144cd4c26b0..0fab752afad 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -395,7 +395,7 @@ If you want to modify the CI/CD pipeline used by Auto DevOps, you can copy the
Assuming that your project is new or it doesn't have a `.gitlab-ci.yml` file
present:
-1. From your project home page, either click on the "Set up CI" button, or click
+1. From your project home page, either click on the "Set up CI/CD" button, or click
on the plus button and (`+`), then "New file"
1. Pick `.gitlab-ci.yml` as the template type
1. Select "Auto-DevOps" from the template dropdown
diff --git a/doc/user/feature_highlight.md b/doc/user/feature_highlight.md
new file mode 100644
index 00000000000..bd98ea00757
--- /dev/null
+++ b/doc/user/feature_highlight.md
@@ -0,0 +1,15 @@
+# Feature highlight
+
+> [Introduced][ce-16379] in GitLab 10.5
+
+Feature highlights are represented by a pulsing blue dot. Hovering over the dot
+will open up callout with more information.
+They are used to emphasize a certain feature and make something more visible to the user.
+
+You can dismiss any feature highlight permanently by clicking the "Got it" link
+at the bottom of the callout. There isn't a way to restore the feature highlight
+after it has been dismissed.
+
+![Clusters feature highlight](img/feature_highlight_example.png)
+
+[ce-16379]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/16379
diff --git a/doc/user/group/index.md b/doc/user/group/index.md
index 7f77a33aadc..f78e5089886 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -168,6 +168,20 @@ Alternatively, you can [lock the sharing with group feature](#share-with-group-l
In GitLab Enterprise Edition it is possible to manage GitLab group memberships using LDAP groups.
See [the GitLab Enterprise Edition documentation](../../integration/ldap.md) for more information.
+## Transfer groups to another group
+
+From 10.5 there are two different ways to transfer a group:
+
+- Either by transferring a group into another group (making it a subgroup of that group).
+- Or by converting a subgroup into a root group (a group with no parent).
+
+Please make sure to understand that:
+
+- Changing a group's parent can have unintended side effects. See [Redirects when changing repository paths](https://docs.gitlab.com/ce/user/project/index.html#redirects-when-changing-repository-paths)
+- You can only transfer the group to a group you manage.
+- You will need to update your local repositories to point to the new location.
+- If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility.
+
## Group settings
Once you have created a group, you can manage its settings by navigating to
diff --git a/doc/user/group/subgroups/img/create_subgroup_button.png b/doc/user/group/subgroups/img/create_subgroup_button.png
index 000b54c2855..d1355d4b2c3 100644
--- a/doc/user/group/subgroups/img/create_subgroup_button.png
+++ b/doc/user/group/subgroups/img/create_subgroup_button.png
Binary files differ
diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md
index 161a3af9903..2a982344e5f 100644
--- a/doc/user/group/subgroups/index.md
+++ b/doc/user/group/subgroups/index.md
@@ -90,7 +90,8 @@ structure.
To create a subgroup:
-1. In the group's dashboard go to the **Subgroups** page and click **New subgroup**.
+1. In the group's dashboard expand the **New project** split button, select
+ **New subgroup** and click the **New subgroup** button.
![Subgroups page](img/create_subgroup_button.png)
diff --git a/doc/user/img/feature_highlight_example.png b/doc/user/img/feature_highlight_example.png
new file mode 100644
index 00000000000..32ca05a6087
--- /dev/null
+++ b/doc/user/img/feature_highlight_example.png
Binary files differ
diff --git a/doc/user/index.md b/doc/user/index.md
index 01db8becc43..43b6fd53b91 100644
--- a/doc/user/index.md
+++ b/doc/user/index.md
@@ -107,6 +107,8 @@ personal access tokens, authorized applications, etc.
methods available in GitLab.
- [Permissions](permissions.md): Learn the different set of permissions levels for each
user type (guest, reporter, developer, master, owner).
+- [Feature highlight](feature_highlight.md): Learn more about the little blue dots
+around the app that explain certain features
## Groups
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index 552abac747b..b590dfa0d40 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -253,7 +253,7 @@ GFM will recognize the following:
| `@user_name` | specific user |
| `@group_name` | specific group |
| `@all` | entire team |
-| `#123` | issue |
+| `#12345` | issue |
| `!123` | merge request |
| `$123` | snippet |
| `~123` | label by ID |
@@ -379,6 +379,45 @@ _Be advised that KaTeX only supports a [subset][katex-subset] of LaTeX._
>**Note:**
This also works for the asciidoctor `:stem: latexmath`. For details see the [asciidoctor user manual][asciidoctor-manual].
+### Colors
+
+> If this is not rendered correctly, see
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#colors
+
+It is possible to have color written in HEX, RGB or HSL format rendered with a color indicator.
+
+Color written inside backticks will be followed by a color "chip".
+
+Examples:
+
+ `#F00`
+ `#F00A`
+ `#FF0000`
+ `#FF0000AA`
+ `RGB(0,255,0)`
+ `RGB(0%,100%,0%)`
+ `RGBA(0,255,0,0.7)`
+ `HSL(540,70%,50%)`
+ `HSLA(540,70%,50%,0.7)`
+
+Becomes:
+
+`#F00`
+`#F00A`
+`#FF0000`
+`#FF0000AA`
+`RGB(0,255,0)`
+`RGB(0%,100%,0%)`
+`RGBA(0,255,0,0.7)`
+`HSL(540,70%,50%)`
+`HSLA(540,70%,50%,0.7)`
+
+#### Supported formats:
+
+* HEX: `` `#RGB[A]` `` or `` `#RRGGBB[AA]` ``
+* RGB: `` `RGB[A](R, G, B[, A])` ``
+* HSL: `` `HSL[A](H, S, L[, A])` ``
+
### Mermaid
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15107) in
diff --git a/doc/user/project/import/github.md b/doc/user/project/import/github.md
index 72def9d1d1d..8c639bd5343 100644
--- a/doc/user/project/import/github.md
+++ b/doc/user/project/import/github.md
@@ -46,7 +46,7 @@ namespace that started the import process.
The importer will also import branches on forks of projects related to open pull
requests. These branches will be imported with a naming scheme similar to
-GH-SHA-Username/Pull-Request-number/fork-name/branch. This may lead to a discrepency
+GH-SHA-Username/Pull-Request-number/fork-name/branch. This may lead to a discrepancy
in branches compared to the GitHub Repository.
For a more technical description and an overview of the architecture you can
diff --git a/doc/user/project/index.md b/doc/user/project/index.md
index 4c772c62f8d..77eba8eda7c 100644
--- a/doc/user/project/index.md
+++ b/doc/user/project/index.md
@@ -128,8 +128,7 @@ and Git push/pull redirects.
Depending on the situation, different things apply.
-When [transferring a project](settings/index.md#transferring-an-existing-project-into-another-namespace),
-or [renaming a user](../profile/index.md#changing-your-username) or
+When [renaming a user](../profile/index.md#changing-your-username) or
[changing a group path](../group/index.md#changing-a-group-s-path):
- **The redirect to the new URL is permanent**, which means that the original
diff --git a/doc/user/project/pages/getting_started_part_two.md b/doc/user/project/pages/getting_started_part_two.md
index 64de0463dad..4a724dd5c1b 100644
--- a/doc/user/project/pages/getting_started_part_two.md
+++ b/doc/user/project/pages/getting_started_part_two.md
@@ -77,7 +77,7 @@ is useful for submitting merge requests to the upstream.
>
> 2. Why do I need to enable Shared Runners?
>
-> Shared Runners will run the script set by your GitLab CI
+> Shared Runners will run the script set by your GitLab CI/CD
configuration file. They're enabled by default to new projects,
but not to forks.
@@ -88,9 +88,9 @@ click **New project**, and name it considering the
[practical examples](getting_started_part_one.md#practical-examples).
1. Clone it to your local computer, add your website
files to your project, add, commit and push to GitLab.
-1. From the your **Project**'s page, click **Set up CI**:
+1. From the your **Project**'s page, click **Set up CI/CD**:
- ![setup GitLab CI](img/setup_ci.png)
+ ![setup GitLab CI/CD](img/setup_ci.png)
1. Choose one of the templates from the dropbox menu.
Pick up the template corresponding to the SSG you're using (or plain HTML).
@@ -98,7 +98,7 @@ Pick up the template corresponding to the SSG you're using (or plain HTML).
![gitlab-ci templates](img/choose_ci_template.png)
Once you have both site files and `.gitlab-ci.yml` in your project's
-root, GitLab CI will build your site and deploy it with Pages.
+root, GitLab CI/CD will build your site and deploy it with Pages.
Once the first build passes, you see your site is live by
navigating to your **Project**'s **Settings** > **Pages**,
where you'll find its default URL.
diff --git a/doc/user/project/repository/web_editor.md b/doc/user/project/repository/web_editor.md
index db0c3ed9d59..33c9a1a4d6b 100644
--- a/doc/user/project/repository/web_editor.md
+++ b/doc/user/project/repository/web_editor.md
@@ -45,7 +45,7 @@ has already been created, which creates a link to the license itself.
![New file button](img/web_editor_template_dropdown_buttons.png)
>**Note:**
-The **Set up CI** button will not appear on an empty repository. You have to at
+The **Set up CI/CD** button will not appear on an empty repository. You have to at
least add a file in order for the button to show up.
## Upload a file
diff --git a/doc/user/project/wiki/img/wiki_move_page_1.png b/doc/user/project/wiki/img/wiki_move_page_1.png
new file mode 100644
index 00000000000..0331c9d3a5c
--- /dev/null
+++ b/doc/user/project/wiki/img/wiki_move_page_1.png
Binary files differ
diff --git a/doc/user/project/wiki/img/wiki_move_page_2.png b/doc/user/project/wiki/img/wiki_move_page_2.png
new file mode 100644
index 00000000000..a8e0c055051
--- /dev/null
+++ b/doc/user/project/wiki/img/wiki_move_page_2.png
Binary files differ
diff --git a/doc/user/project/wiki/index.md b/doc/user/project/wiki/index.md
index c0b8a87f038..d084ee41d8a 100644
--- a/doc/user/project/wiki/index.md
+++ b/doc/user/project/wiki/index.md
@@ -64,6 +64,18 @@ effect.
You can find the **Delete** button only when editing a page. Click on it and
confirm you want the page to be deleted.
+## Moving a wiki page
+
+You can move a wiki page from one directory to another by specifying the full
+path in the wiki page title in the [edit](#editing-a-wiki-page) form.
+
+![Moving a page](img/wiki_move_page_1.png)
+
+![After moving a page](img/wiki_move_page_2.png)
+
+In order to move a wiki page to the root directory, the wiki page title must
+be preceded by the slash (`/`) character.
+
## Viewing a list of all created wiki pages
Every wiki has a sidebar from which a short list of the created pages can be
diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb
index bd3011b1cd8..959cf7d3e54 100644
--- a/features/steps/project/commits/commits.rb
+++ b/features/steps/project/commits/commits.rb
@@ -154,8 +154,8 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
step 'commit has ci status' do
@project.enable_ci
- pipeline = create :ci_pipeline, project: @project, sha: sample_commit.id
- create :ci_build, pipeline: pipeline
+ @pipeline = create(:ci_pipeline, project: @project, sha: sample_commit.id)
+ create(:ci_build, pipeline: @pipeline)
end
step 'repository contains ".gitlab-ci.yml" file' do
@@ -163,7 +163,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
end
step 'I see commit ci info' do
- expect(page).to have_content "Pipeline #1 pending"
+ expect(page).to have_content "Pipeline ##{@pipeline.id} pending"
end
step 'I search "submodules" commits' do
diff --git a/features/steps/project/issues/labels.rb b/features/steps/project/issues/labels.rb
index 196e0fff63a..4df96e081f9 100644
--- a/features/steps/project/issues/labels.rb
+++ b/features/steps/project/issues/labels.rb
@@ -15,8 +15,9 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps
step 'I delete all labels' do
page.within '.labels' do
- page.all('.remove-row').each do
- accept_confirm { first('.remove-row').click }
+ page.all('.label-list-item').each do
+ first('.remove-row').click
+ first(:link, 'Delete label').click
end
end
end
diff --git a/features/steps/project/source/markdown_render.rb b/features/steps/project/source/markdown_render.rb
index fc4ef26f6b6..db99c179439 100644
--- a/features/steps/project/source/markdown_render.rb
+++ b/features/steps/project/source/markdown_render.rb
@@ -193,7 +193,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
end
step 'The link with text "/ID" should have url "tree/markdownID"' do
- find('a', text: /^\/#id$/)['href'] == current_host + project_tree_path(@project, "markdown") + '#id'
+ find('a', text: %r{^/#id$})['href'] == current_host + project_tree_path(@project, "markdown") + '#id'
end
step 'The link with text "README.mdID" '\
@@ -203,7 +203,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
step 'The link with text "d/README.mdID" should have '\
'url "blob/markdown/d/README.mdID"' do
- find('a', text: /^d\/README.md#id$/)['href'] == current_host + project_blob_path(@project, "d/markdown/README.md") + '#id'
+ find('a', text: %r{^d/README.md#id$})['href'] == current_host + project_blob_path(@project, "d/markdown/README.md") + '#id'
end
step 'The link with text "ID" should have url "blob/markdown/README.mdID"' do
@@ -212,7 +212,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
end
step 'The link with text "/ID" should have url "blob/markdown/README.mdID"' do
- find('a', text: /^\/#id$/)['href'] == current_host + project_blob_path(@project, "markdown/README.md") + '#id'
+ find('a', text: %r{^/#id$})['href'] == current_host + project_blob_path(@project, "markdown/README.md") + '#id'
end
# Wiki
diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb
index 9aeebc34525..c2113551207 100644
--- a/lib/api/api_guard.rb
+++ b/lib/api/api_guard.rb
@@ -42,7 +42,7 @@ module API
include Gitlab::Auth::UserAuthFinders
def find_current_user!
- user = find_user_from_access_token || find_user_from_warden
+ user = find_user_from_sources
return unless user
forbidden!('User is blocked') unless Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api)
@@ -50,6 +50,10 @@ module API
user
end
+ def find_user_from_sources
+ find_user_from_access_token || find_user_from_warden
+ end
+
private
# An array of scopes that were registered (using `allow_access_with_scope`)
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index 0791a110c39..1794207e29b 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -29,6 +29,8 @@ module API
use :pagination
end
get ':id/repository/branches' do
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42329')
+
repository = user_project.repository
branches = ::Kaminari.paginate_array(repository.branches.sort_by(&:name))
merged_branch_names = repository.merged_branch_names(branches.map(&:name))
diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb
index 2cae53dba53..3d0d1287407 100644
--- a/lib/api/helpers/runner.rb
+++ b/lib/api/helpers/runner.rb
@@ -1,15 +1,13 @@
module API
module Helpers
module Runner
- include Gitlab::CurrentSettings
-
JOB_TOKEN_HEADER = 'HTTP_JOB_TOKEN'.freeze
JOB_TOKEN_PARAM = :token
UPDATE_RUNNER_EVERY = 10 * 60
def runner_registration_token_valid?
ActiveSupport::SecurityUtils.variable_size_secure_compare(params[:token],
- current_application_settings.runners_registration_token)
+ Gitlab::CurrentSettings.runners_registration_token)
end
def get_runner_version_from_params
@@ -70,7 +68,7 @@ module API
end
def max_artifacts_size
- current_application_settings.max_artifacts_size.megabytes.to_i
+ Gitlab::CurrentSettings.max_artifacts_size.megabytes.to_i
end
end
end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index c99fe3ab5b3..b6c278c89d0 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -161,6 +161,8 @@ module API
use :issue_params
end
post ':id/issues' do
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42320')
+
authorize! :create_issue, user_project
# Setting created_at time only allowed for admins and project owners
@@ -201,6 +203,8 @@ module API
:labels, :created_at, :due_date, :confidential, :state_event
end
put ':id/issues/:issue_iid' do
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42322')
+
issue = user_project.issues.find_by!(iid: params.delete(:issue_iid))
authorize! :update_issue, issue
@@ -234,6 +238,8 @@ module API
requires :to_project_id, type: Integer, desc: 'The ID of the new project'
end
post ':id/issues/:issue_iid/move' do
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42323')
+
issue = user_project.issues.find_by(iid: params[:issue_iid])
not_found!('Issue') unless issue
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 420aaf1c964..719afa09295 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -152,6 +152,8 @@ module API
use :optional_params
end
post ":id/merge_requests" do
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42316')
+
authorize! :create_merge_request, user_project
mr_params = declared_params(include_missing: false)
@@ -256,6 +258,8 @@ module API
at_least_one_of(*at_least_one_of_ce)
end
put ':id/merge_requests/:merge_request_iid' do
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42318')
+
merge_request = find_merge_request_with_access(params.delete(:merge_request_iid), :update_merge_request)
mr_params = declared_params(include_missing: false)
@@ -283,6 +287,8 @@ module API
optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch'
end
put ':id/merge_requests/:merge_request_iid/merge' do
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42317')
+
merge_request = find_project_merge_request(params[:merge_request_iid])
merge_when_pipeline_succeeds = to_boolean(params[:merge_when_pipeline_succeeds])
diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb
index 675c963bae2..d2b8b832e4e 100644
--- a/lib/api/pipelines.rb
+++ b/lib/api/pipelines.rb
@@ -42,6 +42,8 @@ module API
requires :ref, type: String, desc: 'Reference'
end
post ':id/pipeline' do
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42124')
+
authorize! :create_pipeline, user_project
new_pipeline = Ci::CreatePipelineService.new(user_project,
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 8b5e4f8edcc..5b481121a10 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -210,6 +210,8 @@ module API
optional :namespace, type: String, desc: 'The ID or name of the namespace that the project will be forked into'
end
post ':id/fork' do
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42284')
+
fork_params = declared_params(include_missing: false)
namespace_id = fork_params[:namespace]
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
index 80feb629d54..1f80646a2ea 100644
--- a/lib/api/runner.rb
+++ b/lib/api/runner.rb
@@ -215,9 +215,9 @@ module API
job = authenticate_job!
forbidden!('Job is not running!') unless job.running?
- artifacts_upload_path = JobArtifactUploader.artifacts_upload_path
- artifacts = uploaded_file(:file, artifacts_upload_path)
- metadata = uploaded_file(:metadata, artifacts_upload_path)
+ workhorse_upload_path = JobArtifactUploader.workhorse_upload_path
+ artifacts = uploaded_file(:file, workhorse_upload_path)
+ metadata = uploaded_file(:metadata, workhorse_upload_path)
bad_request!('Missing artifacts file!') unless artifacts
file_to_large! unless artifacts.size < max_artifacts_size
diff --git a/lib/api/templates.rb b/lib/api/templates.rb
index 6550b331fb8..41862768a3f 100644
--- a/lib/api/templates.rb
+++ b/lib/api/templates.rb
@@ -17,15 +17,15 @@ module API
}
}.freeze
PROJECT_TEMPLATE_REGEX =
- /[\<\{\[]
+ %r{[\<\{\[]
(project|description|
one\sline\s.+\swhat\sit\sdoes\.) # matching the start and end is enough here
- [\>\}\]]/xi.freeze
+ [\>\}\]]}xi.freeze
YEAR_TEMPLATE_REGEX = /[<{\[](year|yyyy)[>}\]]/i.freeze
FULLNAME_TEMPLATE_REGEX =
- /[\<\{\[]
+ %r{[\<\{\[]
(fullname|name\sof\s(author|copyright\sowner))
- [\>\}\]]/xi.freeze
+ [\>\}\]]}xi.freeze
helpers do
def parsed_license_template
diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb
index dd6801664b1..b3709455bc3 100644
--- a/lib/api/triggers.rb
+++ b/lib/api/triggers.rb
@@ -15,6 +15,8 @@ module API
optional :variables, type: Hash, desc: 'The list of variables to be injected into build'
end
post ":id/(ref/:ref/)trigger/pipeline", requirements: { ref: /.+/ } do
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42283')
+
# validate variables
params[:variables] = params[:variables].to_h
unless params[:variables].all? { |key, value| key.is_a?(String) && value.is_a?(String) }
diff --git a/lib/api/users.rb b/lib/api/users.rb
index e5de31ad51b..3cc12724b8a 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -18,6 +18,14 @@ module API
User.find_by(id: id) || not_found!('User')
end
+ def reorder_users(users)
+ if params[:order_by] && params[:sort]
+ users.reorder(params[:order_by] => params[:sort])
+ else
+ users
+ end
+ end
+
params :optional_attributes do
optional :skype, type: String, desc: 'The Skype username'
optional :linkedin, type: String, desc: 'The LinkedIn username'
@@ -35,6 +43,13 @@ module API
optional :avatar, type: File, desc: 'Avatar image for user'
all_or_none_of :extern_uid, :provider
end
+
+ params :sort_params do
+ optional :order_by, type: String, values: %w[id name username created_at updated_at],
+ default: 'id', desc: 'Return users ordered by a field'
+ optional :sort, type: String, values: %w[asc desc], default: 'desc',
+ desc: 'Return users sorted in ascending and descending order'
+ end
end
desc 'Get the list of users' do
@@ -53,16 +68,18 @@ module API
optional :created_before, type: DateTime, desc: 'Return users created before the specified time'
all_or_none_of :extern_uid, :provider
+ use :sort_params
use :pagination
end
get do
authenticated_as_admin! if params[:external].present? || (params[:extern_uid].present? && params[:provider].present?)
unless current_user&.admin?
- params.except!(:created_after, :created_before)
+ params.except!(:created_after, :created_before, :order_by, :sort)
end
users = UsersFinder.new(current_user, params).execute
+ users = reorder_users(users)
authorized = can?(current_user, :read_users_list)
@@ -383,6 +400,8 @@ module API
optional :hard_delete, type: Boolean, desc: "Whether to remove a user's contributions"
end
delete ":id" do
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42279')
+
authenticated_as_admin!
user = User.find_by(id: params[:id])
diff --git a/lib/api/v3/branches.rb b/lib/api/v3/branches.rb
index b201bf77667..25176c5b38e 100644
--- a/lib/api/v3/branches.rb
+++ b/lib/api/v3/branches.rb
@@ -14,6 +14,8 @@ module API
success ::API::Entities::Branch
end
get ":id/repository/branches" do
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42276')
+
repository = user_project.repository
branches = repository.branches.sort_by(&:name)
merged_branch_names = repository.merged_branch_names(branches.map(&:name))
diff --git a/lib/api/v3/issues.rb b/lib/api/v3/issues.rb
index cb371fdbab8..b59947d81d9 100644
--- a/lib/api/v3/issues.rb
+++ b/lib/api/v3/issues.rb
@@ -134,6 +134,8 @@ module API
use :issue_params
end
post ':id/issues' do
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42131')
+
# Setting created_at time only allowed for admins and project owners
unless current_user.admin? || user_project.owner == current_user
params.delete(:created_at)
@@ -169,6 +171,8 @@ module API
:labels, :created_at, :due_date, :confidential, :state_event
end
put ':id/issues/:issue_id' do
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42132')
+
issue = user_project.issues.find(params.delete(:issue_id))
authorize! :update_issue, issue
@@ -201,6 +205,8 @@ module API
requires :to_project_id, type: Integer, desc: 'The ID of the new project'
end
post ':id/issues/:issue_id/move' do
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42133')
+
issue = user_project.issues.find_by(id: params[:issue_id])
not_found!('Issue') unless issue
diff --git a/lib/api/v3/merge_requests.rb b/lib/api/v3/merge_requests.rb
index 0a24fea52a3..ce216497996 100644
--- a/lib/api/v3/merge_requests.rb
+++ b/lib/api/v3/merge_requests.rb
@@ -91,6 +91,8 @@ module API
use :optional_params
end
post ":id/merge_requests" do
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42126')
+
authorize! :create_merge_request, user_project
mr_params = declared_params(include_missing: false)
@@ -167,6 +169,8 @@ module API
:remove_source_branch
end
put path do
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42127')
+
merge_request = find_merge_request_with_access(params.delete(:merge_request_id), :update_merge_request)
mr_params = declared_params(include_missing: false)
diff --git a/lib/api/v3/pipelines.rb b/lib/api/v3/pipelines.rb
index c48cbd2b765..6d31c12f572 100644
--- a/lib/api/v3/pipelines.rb
+++ b/lib/api/v3/pipelines.rb
@@ -19,6 +19,8 @@ module API
desc: 'Either running, branches, or tags'
end
get ':id/pipelines' do
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42123')
+
authorize! :read_pipeline, user_project
pipelines = PipelinesFinder.new(user_project, scope: params[:scope]).execute
diff --git a/lib/api/v3/projects.rb b/lib/api/v3/projects.rb
index a7f0813bf74..c856ba99f09 100644
--- a/lib/api/v3/projects.rb
+++ b/lib/api/v3/projects.rb
@@ -173,7 +173,7 @@ module API
use :sort_params
use :pagination
end
- get "/search/:query", requirements: { query: /[^\/]+/ } do
+ get "/search/:query", requirements: { query: %r{[^/]+} } do
search_service = Search::GlobalService.new(current_user, search: params[:query]).execute
projects = search_service.objects('projects', params[:page], false)
projects = projects.reorder(params[:order_by] => params[:sort])
diff --git a/lib/api/v3/templates.rb b/lib/api/v3/templates.rb
index 7298203df10..b82b02b5f49 100644
--- a/lib/api/v3/templates.rb
+++ b/lib/api/v3/templates.rb
@@ -16,15 +16,15 @@ module API
}
}.freeze
PROJECT_TEMPLATE_REGEX =
- /[\<\{\[]
+ %r{[\<\{\[]
(project|description|
one\sline\s.+\swhat\sit\sdoes\.) # matching the start and end is enough here
- [\>\}\]]/xi.freeze
+ [\>\}\]]}xi.freeze
YEAR_TEMPLATE_REGEX = /[<{\[](year|yyyy)[>}\]]/i.freeze
FULLNAME_TEMPLATE_REGEX =
- /[\<\{\[]
+ %r{[\<\{\[]
(fullname|name\sof\s(author|copyright\sowner))
- [\>\}\]]/xi.freeze
+ [\>\}\]]}xi.freeze
DEPRECATION_MESSAGE = ' This endpoint is deprecated and has been removed in V4.'.freeze
helpers do
diff --git a/lib/api/v3/triggers.rb b/lib/api/v3/triggers.rb
index 534911fde5c..34f07dfb486 100644
--- a/lib/api/v3/triggers.rb
+++ b/lib/api/v3/triggers.rb
@@ -16,6 +16,8 @@ module API
optional :variables, type: Hash, desc: 'The list of variables to be injected into build'
end
post ":id/(ref/:ref/)trigger/builds", requirements: { ref: /.+/ } do
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42121')
+
# validate variables
params[:variables] = params[:variables].to_h
unless params[:variables].all? { |key, value| key.is_a?(String) && value.is_a?(String) }
diff --git a/lib/backup/artifacts.rb b/lib/backup/artifacts.rb
index 7a582a20056..4383124d150 100644
--- a/lib/backup/artifacts.rb
+++ b/lib/backup/artifacts.rb
@@ -3,7 +3,7 @@ require 'backup/files'
module Backup
class Artifacts < Files
def initialize
- super('artifacts', LegacyArtifactUploader.local_store_path)
+ super('artifacts', JobArtifactUploader.root)
end
def create_files_dir
diff --git a/lib/banzai/color_parser.rb b/lib/banzai/color_parser.rb
new file mode 100644
index 00000000000..355c364b07b
--- /dev/null
+++ b/lib/banzai/color_parser.rb
@@ -0,0 +1,44 @@
+module Banzai
+ module ColorParser
+ ALPHA = /0(?:\.\d+)?|\.\d+|1(?:\.0+)?/ # 0.0..1.0
+ PERCENTS = /(?:\d{1,2}|100)%/ # 00%..100%
+ ALPHA_CHANNEL = /(?:,\s*(?:#{ALPHA}|#{PERCENTS}))?/
+ BITS = /\d{1,2}|1\d\d|2(?:[0-4]\d|5[0-5])/ # 00..255
+ DEGS = /-?\d+(?:deg)?/i # [-]digits[deg]
+ RADS = /-?(?:\d+(?:\.\d+)?|\.\d+)rad/i # [-](digits[.digits] OR .digits)rad
+ HEX_FORMAT = /\#(?:\h{3}|\h{4}|\h{6}|\h{8})/
+ RGB_FORMAT = %r{
+ (?:rgba?
+ \(
+ (?:
+ (?:(?:#{BITS},\s*){2}#{BITS})
+ |
+ (?:(?:#{PERCENTS},\s*){2}#{PERCENTS})
+ )
+ #{ALPHA_CHANNEL}
+ \)
+ )
+ }xi
+ HSL_FORMAT = %r{
+ (?:hsla?
+ \(
+ (?:#{DEGS}|#{RADS}),\s*#{PERCENTS},\s*#{PERCENTS}
+ #{ALPHA_CHANNEL}
+ \)
+ )
+ }xi
+
+ FORMATS = [HEX_FORMAT, RGB_FORMAT, HSL_FORMAT].freeze
+
+ COLOR_FORMAT = /\A(#{Regexp.union(FORMATS)})\z/ix
+
+ # Public: Analyzes whether the String is a color code.
+ #
+ # text - The String to be parsed.
+ #
+ # Returns the recognized color String or nil if none was found.
+ def self.parse(text)
+ text if COLOR_FORMAT =~ text
+ end
+ end
+end
diff --git a/lib/banzai/filter/color_filter.rb b/lib/banzai/filter/color_filter.rb
new file mode 100644
index 00000000000..6ab29ac281f
--- /dev/null
+++ b/lib/banzai/filter/color_filter.rb
@@ -0,0 +1,31 @@
+module Banzai
+ module Filter
+ # HTML filter that renders `color` followed by a color "chip".
+ #
+ class ColorFilter < HTML::Pipeline::Filter
+ COLOR_CHIP_CLASS = 'gfm-color_chip'.freeze
+
+ def call
+ doc.css('code').each do |node|
+ color = ColorParser.parse(node.content)
+ node << color_chip(color) if color
+ end
+
+ doc
+ end
+
+ private
+
+ def color_chip(color)
+ checkerboard = doc.document.create_element('span', class: COLOR_CHIP_CLASS)
+ chip = doc.document.create_element('span', style: inline_styles(color: color))
+
+ checkerboard << chip
+ end
+
+ def inline_styles(color:)
+ "background-color: #{color};"
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/emoji_filter.rb b/lib/banzai/filter/emoji_filter.rb
index 6255a611dbe..b82c6ca6393 100644
--- a/lib/banzai/filter/emoji_filter.rb
+++ b/lib/banzai/filter/emoji_filter.rb
@@ -54,9 +54,9 @@ module Banzai
# Build a regexp that matches all valid :emoji: names.
def self.emoji_pattern
@emoji_pattern ||=
- /(?<=[^[:alnum:]:]|\n|^)
+ %r{(?<=[^[:alnum:]:]|\n|^)
:(#{Gitlab::Emoji.emojis_names.map { |name| Regexp.escape(name) }.join('|')}):
- (?=[^[:alnum:]:]|$)/x
+ (?=[^[:alnum:]:]|$)}x
end
# Build a regexp that matches all valid unicode emojis names.
diff --git a/lib/banzai/filter/gollum_tags_filter.rb b/lib/banzai/filter/gollum_tags_filter.rb
index 2e259904673..c2b42673376 100644
--- a/lib/banzai/filter/gollum_tags_filter.rb
+++ b/lib/banzai/filter/gollum_tags_filter.rb
@@ -51,10 +51,10 @@ module Banzai
# See https://github.com/gollum/gollum/wiki
#
# Rubular: http://rubular.com/r/7dQnE5CUCH
- TAGS_PATTERN = %r{\[\[(.+?)\]\]}.freeze
+ TAGS_PATTERN = /\[\[(.+?)\]\]/.freeze
# Pattern to match allowed image extensions
- ALLOWED_IMAGE_EXTENSIONS = %r{.+(jpg|png|gif|svg|bmp)\z}i.freeze
+ ALLOWED_IMAGE_EXTENSIONS = /.+(jpg|png|gif|svg|bmp)\z/i.freeze
def call
search_text_nodes(doc).each do |node|
diff --git a/lib/banzai/pipeline/broadcast_message_pipeline.rb b/lib/banzai/pipeline/broadcast_message_pipeline.rb
index adc09c8afbd..5dd572de3a1 100644
--- a/lib/banzai/pipeline/broadcast_message_pipeline.rb
+++ b/lib/banzai/pipeline/broadcast_message_pipeline.rb
@@ -7,6 +7,7 @@ module Banzai
Filter::SanitizationFilter,
Filter::EmojiFilter,
+ Filter::ColorFilter,
Filter::AutolinkFilter,
Filter::ExternalLinkFilter
]
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index c746f6f64e9..4001b8a85e3 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -14,6 +14,7 @@ module Banzai
Filter::SyntaxHighlightFilter,
Filter::MathFilter,
+ Filter::ColorFilter,
Filter::MermaidFilter,
Filter::VideoLinkFilter,
Filter::ImageLazyLoadFilter,
diff --git a/lib/carrier_wave_string_file.rb b/lib/carrier_wave_string_file.rb
new file mode 100644
index 00000000000..6c848902e4a
--- /dev/null
+++ b/lib/carrier_wave_string_file.rb
@@ -0,0 +1,5 @@
+class CarrierWaveStringFile < StringIO
+ def original_filename
+ ""
+ end
+end
diff --git a/lib/container_registry/registry.rb b/lib/container_registry/registry.rb
index 63bce655f57..f90d711474a 100644
--- a/lib/container_registry/registry.rb
+++ b/lib/container_registry/registry.rb
@@ -11,7 +11,7 @@ module ContainerRegistry
private
def default_path
- @uri.sub(/^https?:\/\//, '')
+ @uri.sub(%r{^https?://}, '')
end
end
end
diff --git a/lib/email_template_interceptor.rb b/lib/email_template_interceptor.rb
index f2bf3d0fb2b..3978a6d9fe4 100644
--- a/lib/email_template_interceptor.rb
+++ b/lib/email_template_interceptor.rb
@@ -1,10 +1,8 @@
# Read about interceptors in http://guides.rubyonrails.org/action_mailer_basics.html#intercepting-emails
class EmailTemplateInterceptor
- extend Gitlab::CurrentSettings
-
def self.delivering_email(message)
# Remove HTML part if HTML emails are disabled.
- unless current_application_settings.html_emails_enabled
+ unless Gitlab::CurrentSettings.html_emails_enabled
message.parts.delete_if do |part|
part.content_type.start_with?('text/html')
end
diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb
index d8aca3304c5..a9b04c183ad 100644
--- a/lib/extracts_path.rb
+++ b/lib/extracts_path.rb
@@ -56,7 +56,7 @@ module ExtractsPath
if valid_refs.length == 0
# No exact ref match, so just try our best
- pair = id.match(/([^\/]+)(.*)/).captures
+ pair = id.match(%r{([^/]+)(.*)}).captures
else
# There is a distinct possibility that multiple refs prefix the ID.
# Use the longest match to maximize the chance that we have the
@@ -68,7 +68,7 @@ module ExtractsPath
end
# Remove ending slashes from path
- pair[1].gsub!(/^\/|\/$/, '')
+ pair[1].gsub!(%r{^/|/$}, '')
pair
end
diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb
index cead1c7eacd..ee7f4be6b9f 100644
--- a/lib/gitlab/asciidoc.rb
+++ b/lib/gitlab/asciidoc.rb
@@ -6,8 +6,6 @@ module Gitlab
# Parser/renderer for the AsciiDoc format that uses Asciidoctor and filters
# the resulting HTML through HTML pipeline filters.
module Asciidoc
- extend Gitlab::CurrentSettings
-
DEFAULT_ADOC_ATTRS = [
'showtitle', 'idprefix=user-content-', 'idseparator=-', 'env=gitlab',
'env-gitlab', 'source-highlighter=html-pipeline', 'icons=font'
@@ -33,9 +31,9 @@ module Gitlab
def self.plantuml_setup
Asciidoctor::PlantUml.configure do |conf|
- conf.url = current_application_settings.plantuml_url
- conf.svg_enable = current_application_settings.plantuml_enabled
- conf.png_enable = current_application_settings.plantuml_enabled
+ conf.url = Gitlab::CurrentSettings.plantuml_url
+ conf.svg_enable = Gitlab::CurrentSettings.plantuml_enabled
+ conf.png_enable = Gitlab::CurrentSettings.plantuml_enabled
conf.txt_enable = false
end
end
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 65d7fd3ec70..05932378173 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -14,8 +14,6 @@ module Gitlab
DEFAULT_SCOPES = [:api].freeze
class << self
- include Gitlab::CurrentSettings
-
def find_for_git_client(login, password, project:, ip:)
raise "Must provide an IP for rate limiting" if ip.nil?
@@ -57,7 +55,7 @@ module Gitlab
if user.nil? || user.ldap_user?
# Second chance - try LDAP authentication
Gitlab::LDAP::Authentication.login(login, password)
- elsif current_application_settings.password_authentication_enabled_for_git?
+ elsif Gitlab::CurrentSettings.password_authentication_enabled_for_git?
user if user.active? && user.valid_password?(password)
end
end
@@ -87,7 +85,7 @@ module Gitlab
private
def authenticate_using_internal_or_ldap_password?
- current_application_settings.password_authentication_enabled_for_git? || Gitlab::LDAP::Config.enabled?
+ Gitlab::CurrentSettings.password_authentication_enabled_for_git? || Gitlab::LDAP::Config.enabled?
end
def service_request_check(login, password, project)
diff --git a/lib/gitlab/background_migration/populate_untracked_uploads.rb b/lib/gitlab/background_migration/populate_untracked_uploads.rb
index 81e95e5832d..8a8e770940e 100644
--- a/lib/gitlab/background_migration/populate_untracked_uploads.rb
+++ b/lib/gitlab/background_migration/populate_untracked_uploads.rb
@@ -12,7 +12,7 @@ module Gitlab
# Ends with /:random_hex/:filename
FILE_UPLOADER_PATH = %r{/\h+/[^/]+\z}
- FULL_PATH_CAPTURE = %r{\A(.+)#{FILE_UPLOADER_PATH}}
+ FULL_PATH_CAPTURE = /\A(.+)#{FILE_UPLOADER_PATH}/
# These regex patterns are tested against a relative path, relative to
# the upload directory.
@@ -143,7 +143,7 @@ module Gitlab
end
def absolute_path
- File.join(CarrierWave.root, path)
+ File.join(Gitlab.config.uploads.storage_path, path)
end
end
diff --git a/lib/gitlab/background_migration/prepare_untracked_uploads.rb b/lib/gitlab/background_migration/prepare_untracked_uploads.rb
index 4e0121ca34d..a7a1bbe1752 100644
--- a/lib/gitlab/background_migration/prepare_untracked_uploads.rb
+++ b/lib/gitlab/background_migration/prepare_untracked_uploads.rb
@@ -11,9 +11,12 @@ module Gitlab
FIND_BATCH_SIZE = 500
RELATIVE_UPLOAD_DIR = "uploads".freeze
- ABSOLUTE_UPLOAD_DIR = "#{CarrierWave.root}/#{RELATIVE_UPLOAD_DIR}".freeze
+ ABSOLUTE_UPLOAD_DIR = File.join(
+ Gitlab.config.uploads.storage_path,
+ RELATIVE_UPLOAD_DIR
+ )
FOLLOW_UP_MIGRATION = 'PopulateUntrackedUploads'.freeze
- START_WITH_CARRIERWAVE_ROOT_REGEX = %r{\A#{CarrierWave.root}/}
+ START_WITH_ROOT_REGEX = %r{\A#{Gitlab.config.uploads.storage_path}/}
EXCLUDED_HASHED_UPLOADS_PATH = "#{ABSOLUTE_UPLOAD_DIR}/@hashed/*".freeze
EXCLUDED_TMP_UPLOADS_PATH = "#{ABSOLUTE_UPLOAD_DIR}/tmp/*".freeze
@@ -81,7 +84,7 @@ module Gitlab
paths = []
stdout.each_line("\0") do |line|
- paths << line.chomp("\0").sub(START_WITH_CARRIERWAVE_ROOT_REGEX, '')
+ paths << line.chomp("\0").sub(START_WITH_ROOT_REGEX, '')
if paths.size >= batch_size
yield(paths)
diff --git a/lib/gitlab/badge/coverage/report.rb b/lib/gitlab/badge/coverage/report.rb
index 9a0482306b7..778d78185ff 100644
--- a/lib/gitlab/badge/coverage/report.rb
+++ b/lib/gitlab/badge/coverage/report.rb
@@ -23,7 +23,7 @@ module Gitlab
@coverage ||= raw_coverage
return unless @coverage
- @coverage.to_i
+ @coverage.to_f.round(2)
end
def metadata
diff --git a/lib/gitlab/badge/coverage/template.rb b/lib/gitlab/badge/coverage/template.rb
index fcecb1d9665..afbf9dd17e3 100644
--- a/lib/gitlab/badge/coverage/template.rb
+++ b/lib/gitlab/badge/coverage/template.rb
@@ -25,7 +25,7 @@ module Gitlab
end
def value_text
- @status ? "#{@status}%" : 'unknown'
+ @status ? ("%.2f%%" % @status) : 'unknown'
end
def key_width
@@ -33,7 +33,7 @@ module Gitlab
end
def value_width
- @status ? 36 : 58
+ @status ? 54 : 58
end
def value_color
diff --git a/lib/gitlab/checks/force_push.rb b/lib/gitlab/checks/force_push.rb
index dc5d285ea65..c9c3050cfc2 100644
--- a/lib/gitlab/checks/force_push.rb
+++ b/lib/gitlab/checks/force_push.rb
@@ -15,8 +15,8 @@ module Gitlab
.ancestor?(oldrev, newrev)
else
Gitlab::Git::RevList.new(
- path_to_repo: project.repository.path_to_repo,
- oldrev: oldrev, newrev: newrev).missed_ref.present?
+ project.repository.raw, oldrev: oldrev, newrev: newrev
+ ).missed_ref.present?
end
end
end
diff --git a/lib/gitlab/ci/build/artifacts/metadata/entry.rb b/lib/gitlab/ci/build/artifacts/metadata/entry.rb
index 5b2f09e03ea..428c0505808 100644
--- a/lib/gitlab/ci/build/artifacts/metadata/entry.rb
+++ b/lib/gitlab/ci/build/artifacts/metadata/entry.rb
@@ -97,7 +97,7 @@ module Gitlab
end
def total_size
- descendant_pattern = %r{^#{Regexp.escape(@path.to_s)}}
+ descendant_pattern = /^#{Regexp.escape(@path.to_s)}/
entries.sum do |path, entry|
(entry[:size] if path =~ descendant_pattern).to_i
end
diff --git a/lib/gitlab/ci/trace.rb b/lib/gitlab/ci/trace.rb
index baf55b1fa07..f2e5124c8a8 100644
--- a/lib/gitlab/ci/trace.rb
+++ b/lib/gitlab/ci/trace.rb
@@ -52,12 +52,14 @@ module Gitlab
end
def exist?
- current_path.present? || old_trace.present?
+ trace_artifact&.exists? || current_path.present? || old_trace.present?
end
def read
stream = Gitlab::Ci::Trace::Stream.new do
- if current_path
+ if trace_artifact
+ trace_artifact.open
+ elsif current_path
File.open(current_path, "rb")
elsif old_trace
StringIO.new(old_trace)
@@ -82,6 +84,8 @@ module Gitlab
end
def erase!
+ trace_artifact&.destroy
+
paths.each do |trace_path|
FileUtils.rm(trace_path, force: true)
end
@@ -137,6 +141,10 @@ module Gitlab
"#{job.id}.log"
) if job.project&.ci_id
end
+
+ def trace_artifact
+ job.job_artifacts_trace
+ end
end
end
end
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index 91fd9cc7631..b7c596a973d 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -1,73 +1,79 @@
module Gitlab
module CurrentSettings
- extend self
+ class << self
+ def current_application_settings
+ if RequestStore.active?
+ RequestStore.fetch(:current_application_settings) { ensure_application_settings! }
+ else
+ ensure_application_settings!
+ end
+ end
- def current_application_settings
- if RequestStore.active?
- RequestStore.fetch(:current_application_settings) { ensure_application_settings! }
- else
- ensure_application_settings!
+ def fake_application_settings(defaults = ::ApplicationSetting.defaults)
+ Gitlab::FakeApplicationSettings.new(defaults)
end
- end
- delegate :sidekiq_throttling_enabled?, to: :current_application_settings
+ def method_missing(name, *args, &block)
+ current_application_settings.send(name, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
+ end
- def fake_application_settings(defaults = ::ApplicationSetting.defaults)
- FakeApplicationSettings.new(defaults)
- end
+ def respond_to_missing?(name, include_private = false)
+ current_application_settings.respond_to?(name, include_private) || super
+ end
- private
+ private
- def ensure_application_settings!
- return in_memory_application_settings if ENV['IN_MEMORY_APPLICATION_SETTINGS'] == 'true'
+ def ensure_application_settings!
+ return in_memory_application_settings if ENV['IN_MEMORY_APPLICATION_SETTINGS'] == 'true'
- cached_application_settings || uncached_application_settings
- end
+ cached_application_settings || uncached_application_settings
+ end
- def cached_application_settings
- begin
- ::ApplicationSetting.cached
- rescue ::Redis::BaseError, ::Errno::ENOENT, ::Errno::EADDRNOTAVAIL
- # In case Redis isn't running or the Redis UNIX socket file is not available
+ def cached_application_settings
+ begin
+ ::ApplicationSetting.cached
+ rescue ::Redis::BaseError, ::Errno::ENOENT, ::Errno::EADDRNOTAVAIL
+ # In case Redis isn't running or the Redis UNIX socket file is not available
+ end
end
- end
- def uncached_application_settings
- return fake_application_settings unless connect_to_db?
+ def uncached_application_settings
+ return fake_application_settings unless connect_to_db?
- db_settings = ::ApplicationSetting.current
+ db_settings = ::ApplicationSetting.current
- # If there are pending migrations, it's possible there are columns that
- # need to be added to the application settings. To prevent Rake tasks
- # and other callers from failing, use any loaded settings and return
- # defaults for missing columns.
- if ActiveRecord::Migrator.needs_migration?
- defaults = ::ApplicationSetting.defaults
- defaults.merge!(db_settings.attributes.symbolize_keys) if db_settings.present?
- return fake_application_settings(defaults)
- end
+ # If there are pending migrations, it's possible there are columns that
+ # need to be added to the application settings. To prevent Rake tasks
+ # and other callers from failing, use any loaded settings and return
+ # defaults for missing columns.
+ if ActiveRecord::Migrator.needs_migration?
+ defaults = ::ApplicationSetting.defaults
+ defaults.merge!(db_settings.attributes.symbolize_keys) if db_settings.present?
+ return fake_application_settings(defaults)
+ end
- return db_settings if db_settings.present?
+ return db_settings if db_settings.present?
- ::ApplicationSetting.create_from_defaults || in_memory_application_settings
- end
+ ::ApplicationSetting.create_from_defaults || in_memory_application_settings
+ end
- def in_memory_application_settings
- @in_memory_application_settings ||= ::ApplicationSetting.new(::ApplicationSetting.defaults) # rubocop:disable Gitlab/ModuleWithInstanceVariables
- rescue ActiveRecord::StatementInvalid, ActiveRecord::UnknownAttributeError
- # In case migrations the application_settings table is not created yet,
- # we fallback to a simple OpenStruct
- fake_application_settings
- end
+ def in_memory_application_settings
+ @in_memory_application_settings ||= ::ApplicationSetting.new(::ApplicationSetting.defaults) # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ rescue ActiveRecord::StatementInvalid, ActiveRecord::UnknownAttributeError
+ # In case migrations the application_settings table is not created yet,
+ # we fallback to a simple OpenStruct
+ fake_application_settings
+ end
- def connect_to_db?
- # When the DBMS is not available, an exception (e.g. PG::ConnectionBad) is raised
- active_db_connection = ActiveRecord::Base.connection.active? rescue false
+ def connect_to_db?
+ # When the DBMS is not available, an exception (e.g. PG::ConnectionBad) is raised
+ active_db_connection = ActiveRecord::Base.connection.active? rescue false
- active_db_connection &&
- ActiveRecord::Base.connection.table_exists?('application_settings')
- rescue ActiveRecord::NoDatabaseError
- false
+ active_db_connection &&
+ ActiveRecord::Base.connection.table_exists?('application_settings')
+ rescue ActiveRecord::NoDatabaseError
+ false
+ end
end
end
end
diff --git a/lib/gitlab/dependency_linker/composer_json_linker.rb b/lib/gitlab/dependency_linker/composer_json_linker.rb
index 0245bf4077a..cfd4ec15125 100644
--- a/lib/gitlab/dependency_linker/composer_json_linker.rb
+++ b/lib/gitlab/dependency_linker/composer_json_linker.rb
@@ -11,7 +11,7 @@ module Gitlab
end
def package_url(name)
- "https://packagist.org/packages/#{name}" if name =~ %r{\A#{REPO_REGEX}\z}
+ "https://packagist.org/packages/#{name}" if name =~ /\A#{REPO_REGEX}\z/
end
end
end
diff --git a/lib/gitlab/dependency_linker/gemfile_linker.rb b/lib/gitlab/dependency_linker/gemfile_linker.rb
index d034ea67387..bfea836bcb2 100644
--- a/lib/gitlab/dependency_linker/gemfile_linker.rb
+++ b/lib/gitlab/dependency_linker/gemfile_linker.rb
@@ -15,7 +15,7 @@ module Gitlab
link_regex(/(github:|:github\s*=>)\s*['"](?<name>[^'"]+)['"]/, &method(:github_url))
# Link `git: "https://gitlab.example.com/user/repo"` to https://gitlab.example.com/user/repo
- link_regex(%r{(git:|:git\s*=>)\s*['"](?<name>#{URL_REGEX})['"]}, &:itself)
+ link_regex(/(git:|:git\s*=>)\s*['"](?<name>#{URL_REGEX})['"]/, &:itself)
# Link `source "https://rubygems.org"` to https://rubygems.org
link_method_call('source', URL_REGEX, &:itself)
diff --git a/lib/gitlab/dependency_linker/podspec_linker.rb b/lib/gitlab/dependency_linker/podspec_linker.rb
index a52c7a02439..924e55e9820 100644
--- a/lib/gitlab/dependency_linker/podspec_linker.rb
+++ b/lib/gitlab/dependency_linker/podspec_linker.rb
@@ -12,7 +12,7 @@ module Gitlab
def link_dependencies
link_method_call('homepage', URL_REGEX, &:itself)
- link_regex(%r{(git:|:git\s*=>)\s*['"](?<name>#{URL_REGEX})['"]}, &:itself)
+ link_regex(/(git:|:git\s*=>)\s*['"](?<name>#{URL_REGEX})['"]/, &:itself)
link_method_call('license', &method(:license_url))
link_regex(/license\s*=\s*\{\s*(type:|:type\s*=>)\s*#{STRING_REGEX}/, &method(:license_url))
diff --git a/lib/gitlab/email/reply_parser.rb b/lib/gitlab/email/reply_parser.rb
index 558df87f36d..01c28d051ee 100644
--- a/lib/gitlab/email/reply_parser.rb
+++ b/lib/gitlab/email/reply_parser.rb
@@ -43,7 +43,7 @@ module Gitlab
return "" unless decoded
# Certain trigger phrases that means we didn't parse correctly
- if decoded =~ /(Content\-Type\:|multipart\/alternative|text\/plain)/
+ if decoded =~ %r{(Content\-Type\:|multipart/alternative|text/plain)}
return ""
end
diff --git a/lib/gitlab/file_detector.rb b/lib/gitlab/file_detector.rb
index 0e9ef4f897c..cc2638172ec 100644
--- a/lib/gitlab/file_detector.rb
+++ b/lib/gitlab/file_detector.rb
@@ -6,14 +6,14 @@ module Gitlab
module FileDetector
PATTERNS = {
# Project files
- readme: /\Areadme[^\/]*\z/i,
- changelog: /\A(changelog|history|changes|news)[^\/]*\z/i,
- license: /\A(licen[sc]e|copying)(\.[^\/]+)?\z/i,
- contributing: /\Acontributing[^\/]*\z/i,
+ readme: %r{\Areadme[^/]*\z}i,
+ changelog: %r{\A(changelog|history|changes|news)[^/]*\z}i,
+ license: %r{\A(licen[sc]e|copying)(\.[^/]+)?\z}i,
+ contributing: %r{\Acontributing[^/]*\z}i,
version: 'version',
avatar: /\Alogo\.(png|jpg|gif)\z/,
- issue_template: /\A\.gitlab\/issue_templates\/[^\/]+\.md\z/,
- merge_request_template: /\A\.gitlab\/merge_request_templates\/[^\/]+\.md\z/,
+ issue_template: %r{\A\.gitlab/issue_templates/[^/]+\.md\z},
+ merge_request_template: %r{\A\.gitlab/merge_request_templates/[^/]+\.md\z},
# Configuration files
gitignore: '.gitignore',
@@ -22,17 +22,17 @@ module Gitlab
route_map: '.gitlab/route-map.yml',
# Dependency files
- cartfile: /\ACartfile[^\/]*\z/,
+ cartfile: %r{\ACartfile[^/]*\z},
composer_json: 'composer.json',
gemfile: /\A(Gemfile|gems\.rb)\z/,
gemfile_lock: 'Gemfile.lock',
- gemspec: /\A[^\/]*\.gemspec\z/,
+ gemspec: %r{\A[^/]*\.gemspec\z},
godeps_json: 'Godeps.json',
package_json: 'package.json',
podfile: 'Podfile',
- podspec_json: /\A[^\/]*\.podspec\.json\z/,
- podspec: /\A[^\/]*\.podspec\z/,
- requirements_txt: /\A[^\/]*requirements\.txt\z/,
+ podspec_json: %r{\A[^/]*\.podspec\.json\z},
+ podspec: %r{\A[^/]*\.podspec\z},
+ requirements_txt: %r{\A[^/]*requirements\.txt\z},
yarn_lock: 'yarn.lock'
}.freeze
diff --git a/lib/gitlab/gfm/uploads_rewriter.rb b/lib/gitlab/gfm/uploads_rewriter.rb
index 8fab5489616..1b74f735679 100644
--- a/lib/gitlab/gfm/uploads_rewriter.rb
+++ b/lib/gitlab/gfm/uploads_rewriter.rb
@@ -27,7 +27,7 @@ module Gitlab
with_link_in_tmp_dir(file.file) do |open_tmp_file|
new_uploader.store!(open_tmp_file)
end
- new_uploader.to_markdown
+ new_uploader.markdown_link
end
end
@@ -46,7 +46,7 @@ module Gitlab
private
def find_file(project, secret, file)
- uploader = FileUploader.new(project, secret)
+ uploader = FileUploader.new(project, secret: secret)
uploader.retrieve_from_store!(file)
uploader.file
end
diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb
index 71647099f83..d4e893b881c 100644
--- a/lib/gitlab/git.rb
+++ b/lib/gitlab/git.rb
@@ -6,12 +6,13 @@ module Gitlab
CommandError = Class.new(StandardError)
CommitError = Class.new(StandardError)
+ OSError = Class.new(StandardError)
class << self
include Gitlab::EncodingHelper
def ref_name(ref)
- encode!(ref).sub(/\Arefs\/(tags|heads|remotes)\//, '')
+ encode!(ref).sub(%r{\Arefs/(tags|heads|remotes)/}, '')
end
def branch_name(ref)
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index 13120120223..4828301dbb9 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -107,7 +107,7 @@ module Gitlab
def find_entry_by_path(repository, root_id, path)
root_tree = repository.lookup(root_id)
# Strip leading slashes
- path[/^\/*/] = ''
+ path[%r{^/*}] = ''
path_arr = path.split('/')
entry = root_tree.find do |entry|
@@ -140,7 +140,7 @@ module Gitlab
def find_by_gitaly(repository, sha, path, limit: MAX_DATA_DISPLAY_SIZE)
return unless path
- path = path.sub(/\A\/*/, '')
+ path = path.sub(%r{\A/*}, '')
path = '/' if path.empty?
name = File.basename(path)
diff --git a/lib/gitlab/git/branch.rb b/lib/gitlab/git/branch.rb
index 3487e099381..ae7e88f0503 100644
--- a/lib/gitlab/git/branch.rb
+++ b/lib/gitlab/git/branch.rb
@@ -1,5 +1,3 @@
-# Gitaly note: JV: no RPC's here.
-
module Gitlab
module Git
class Branch < Ref
diff --git a/lib/gitlab/git/lfs_changes.rb b/lib/gitlab/git/lfs_changes.rb
index 732dd5d998a..48434047fce 100644
--- a/lib/gitlab/git/lfs_changes.rb
+++ b/lib/gitlab/git/lfs_changes.rb
@@ -25,8 +25,7 @@ module Gitlab
private
def rev_list
- ::Gitlab::Git::RevList.new(path_to_repo: @repository.path_to_repo,
- newrev: @newrev)
+ Gitlab::Git::RevList.new(@repository, newrev: @newrev)
end
end
end
diff --git a/lib/gitlab/git/lfs_pointer_file.rb b/lib/gitlab/git/lfs_pointer_file.rb
new file mode 100644
index 00000000000..da12ed7d125
--- /dev/null
+++ b/lib/gitlab/git/lfs_pointer_file.rb
@@ -0,0 +1,25 @@
+module Gitlab
+ module Git
+ class LfsPointerFile
+ def initialize(data)
+ @data = data
+ end
+
+ def pointer
+ @pointer ||= <<~FILE
+ version https://git-lfs.github.com/spec/v1
+ oid sha256:#{sha256}
+ size #{size}
+ FILE
+ end
+
+ def size
+ @size ||= @data.bytesize
+ end
+
+ def sha256
+ @sha256 ||= Digest::SHA256.hexdigest(@data)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/path_helper.rb b/lib/gitlab/git/path_helper.rb
index 42c80aabd0a..155cf52f050 100644
--- a/lib/gitlab/git/path_helper.rb
+++ b/lib/gitlab/git/path_helper.rb
@@ -6,7 +6,7 @@ module Gitlab
class << self
def normalize_path(filename)
# Strip all leading slashes so that //foo -> foo
- filename[/^\/*/] = ''
+ filename[%r{^/*}] = ''
# Expand relative paths (e.g. foo/../bar)
filename = Pathname.new(filename)
diff --git a/lib/gitlab/git/popen.rb b/lib/gitlab/git/popen.rb
index e0bd2bbe47b..c1767046ff0 100644
--- a/lib/gitlab/git/popen.rb
+++ b/lib/gitlab/git/popen.rb
@@ -25,7 +25,7 @@ module Gitlab
stdin.close
if lazy_block
- return lazy_block.call(stdout.lazy)
+ return [lazy_block.call(stdout.lazy), 0]
else
cmd_output << stdout.read
end
diff --git a/lib/gitlab/git/ref.rb b/lib/gitlab/git/ref.rb
index a3ba9475ad0..fa71a4e7ea7 100644
--- a/lib/gitlab/git/ref.rb
+++ b/lib/gitlab/git/ref.rb
@@ -23,7 +23,7 @@ module Gitlab
# Ex.
# Ref.extract_branch_name('refs/heads/master') #=> 'master'
def self.extract_branch_name(str)
- str.gsub(/\Arefs\/heads\//, '')
+ str.gsub(%r{\Arefs/heads/}, '')
end
# Gitaly: this method will probably be migrated indirectly via its call sites.
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 8137c582c0f..d7510061def 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -128,6 +128,10 @@ module Gitlab
raise NoRepository.new('no repository for such path')
end
+ def cleanup
+ @rugged&.close
+ end
+
def circuit_breaker
@circuit_breaker ||= Gitlab::Git::Storage::CircuitBreaker.for_storage(storage)
end
@@ -462,7 +466,6 @@ module Gitlab
path: nil,
follow: false,
skip_merges: false,
- disable_walk: false,
after: nil,
before: nil
}
@@ -494,11 +497,7 @@ module Gitlab
return []
end
- if log_using_shell?(options)
- log_by_shell(sha, options)
- else
- log_by_walk(sha, options)
- end
+ log_by_shell(sha, options)
end
def count_commits(options)
@@ -1227,33 +1226,13 @@ module Gitlab
end
def squash(user, squash_id, branch:, start_sha:, end_sha:, author:, message:)
- squash_path = worktree_path(SQUASH_WORKTREE_PREFIX, squash_id)
- env = git_env_for_user(user).merge(
- 'GIT_AUTHOR_NAME' => author.name,
- 'GIT_AUTHOR_EMAIL' => author.email
- )
- diff_range = "#{start_sha}...#{end_sha}"
- diff_files = run_git!(
- %W(diff --name-only --diff-filter=a --binary #{diff_range})
- ).chomp
-
- with_worktree(squash_path, branch, sparse_checkout_files: diff_files, env: env) do
- # Apply diff of the `diff_range` to the worktree
- diff = run_git!(%W(diff --binary #{diff_range}))
- run_git!(%w(apply --index), chdir: squash_path, env: env) do |stdin|
- stdin.write(diff)
+ gitaly_migrate(:squash) do |is_enabled|
+ if is_enabled
+ gitaly_operation_client.user_squash(user, squash_id, branch,
+ start_sha, end_sha, author, message)
+ else
+ git_squash(user, squash_id, branch, start_sha, end_sha, author, message)
end
-
- # Commit the `diff_range` diff
- run_git!(%W(commit --no-verify --message #{message}), chdir: squash_path, env: env)
-
- # Return the squash sha. May print a warning for ambiguous refs, but
- # we can ignore that with `--quiet` and just take the SHA, if present.
- # HEAD here always refers to the current HEAD commit, even if there is
- # another ref called HEAD.
- run_git!(
- %w(rev-parse --quiet --verify HEAD), chdir: squash_path, env: env
- ).chomp
end
end
@@ -1311,7 +1290,15 @@ module Gitlab
# rubocop:enable Metrics/ParameterLists
def write_config(full_path:)
- rugged.config['gitlab.fullpath'] = full_path if full_path.present?
+ return unless full_path.present?
+
+ gitaly_migrate(:write_config) do |is_enabled|
+ if is_enabled
+ gitaly_repository_client.write_config(full_path: full_path)
+ else
+ rugged_write_config(full_path: full_path)
+ end
+ end
end
def gitaly_repository
@@ -1360,20 +1347,23 @@ module Gitlab
raise CommandError.new(e)
end
- def refs_contains_sha(ref_type, sha)
- args = %W(#{ref_type} --contains #{sha})
- names = run_git(args).first
-
- if names.respond_to?(:split)
- names = names.split("\n").map(&:strip)
-
- names.each do |name|
- name.slice! '* '
+ def branch_names_contains_sha(sha)
+ gitaly_migrate(:branch_names_contains_sha) do |is_enabled|
+ if is_enabled
+ gitaly_ref_client.branch_names_contains_sha(sha)
+ else
+ refs_contains_sha(:branch, sha)
end
+ end
+ end
- names
- else
- []
+ def tag_names_contains_sha(sha)
+ gitaly_migrate(:tag_names_contains_sha) do |is_enabled|
+ if is_enabled
+ gitaly_ref_client.tag_names_contains_sha(sha)
+ else
+ refs_contains_sha(:tag, sha)
+ end
end
end
@@ -1397,7 +1387,7 @@ module Gitlab
end
def search_files_by_name(query, ref)
- safe_query = Regexp.escape(query.sub(/^\/*/, ""))
+ safe_query = Regexp.escape(query.sub(%r{^/*}, ""))
return [] if empty? || safe_query.blank?
@@ -1441,6 +1431,26 @@ module Gitlab
end
end
+ def rev_list(including: [], excluding: [], objects: false, &block)
+ args = ['rev-list']
+
+ args.push(*rev_list_param(including))
+
+ exclude_param = *rev_list_param(excluding)
+ if exclude_param.any?
+ args.push('--not')
+ args.push(*exclude_param)
+ end
+
+ args.push('--objects') if objects
+
+ run_git!(args, lazy_block: block)
+ end
+
+ def missed_ref(oldrev, newrev)
+ run_git!(['rev-list', '--max-count=1', oldrev, "^#{newrev}"])
+ end
+
private
def local_write_ref(ref_path, ref, old_ref: nil, shell: true)
@@ -1451,6 +1461,25 @@ module Gitlab
end
end
+ def refs_contains_sha(ref_type, sha)
+ args = %W(#{ref_type} --contains #{sha})
+ names = run_git(args).first
+
+ return [] unless names.respond_to?(:split)
+
+ names = names.split("\n").map(&:strip)
+
+ names.each do |name|
+ name.slice! '* '
+ end
+
+ names
+ end
+
+ def rugged_write_config(full_path:)
+ rugged.config['gitlab.fullpath'] = full_path
+ end
+
def shell_write_ref(ref_path, ref, old_ref)
raise ArgumentError, "invalid ref_path #{ref_path.inspect}" if ref_path.include?(' ')
raise ArgumentError, "invalid ref #{ref.inspect}" if ref.include?("\x00")
@@ -1470,7 +1499,7 @@ module Gitlab
Rails.logger.error "Unable to create #{ref_path} reference for repository #{path}: #{ex}"
end
- def run_git(args, chdir: path, env: {}, nice: false, &block)
+ def run_git(args, chdir: path, env: {}, nice: false, lazy_block: nil, &block)
cmd = [Gitlab.config.git.bin_path, *args]
cmd.unshift("nice") if nice
@@ -1480,12 +1509,12 @@ module Gitlab
end
circuit_breaker.perform do
- popen(cmd, chdir, env, &block)
+ popen(cmd, chdir, env, lazy_block: lazy_block, &block)
end
end
- def run_git!(args, chdir: path, env: {}, nice: false, &block)
- output, status = run_git(args, chdir: chdir, env: env, nice: nice, &block)
+ def run_git!(args, chdir: path, env: {}, nice: false, lazy_block: nil, &block)
+ output, status = run_git(args, chdir: chdir, env: env, nice: nice, lazy_block: lazy_block, &block)
raise GitError, output unless status.zero?
@@ -1512,7 +1541,7 @@ module Gitlab
if sparse_checkout_files
# Create worktree without checking out
run_git!(base_args + ['--no-checkout', worktree_path], env: env)
- worktree_git_path = run_git!(%w(rev-parse --git-dir), chdir: worktree_path)
+ worktree_git_path = run_git!(%w(rev-parse --git-dir), chdir: worktree_path).chomp
configure_sparse_checkout(worktree_git_path, sparse_checkout_files)
@@ -1645,24 +1674,6 @@ module Gitlab
end
end
- def log_using_shell?(options)
- options[:path].present? ||
- options[:disable_walk] ||
- options[:skip_merges] ||
- options[:after] ||
- options[:before]
- end
-
- def log_by_walk(sha, options)
- walk_options = {
- show: sha,
- sort: Rugged::SORT_NONE,
- limit: options[:limit],
- offset: options[:offset]
- }
- Rugged::Walker.walk(rugged, walk_options).to_a
- end
-
# Gitaly note: JV: although #log_by_shell shells out to Git I think the
# complexity is such that we should migrate it as Ruby before trying to
# do it in Go.
@@ -2025,7 +2036,7 @@ module Gitlab
target_commit = Gitlab::Git::Commit.find(self, rugged_ref.target)
Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target, target_commit)
rescue Rugged::ReferenceError => e
- raise InvalidRef.new("Branch #{ref} already exists") if e.to_s =~ /'refs\/heads\/#{ref}'/
+ raise InvalidRef.new("Branch #{ref} already exists") if e.to_s =~ %r{'refs/heads/#{ref}'}
raise InvalidRef.new("Invalid reference #{start_point}")
end
@@ -2175,6 +2186,37 @@ module Gitlab
end
end
+ def git_squash(user, squash_id, branch, start_sha, end_sha, author, message)
+ squash_path = worktree_path(SQUASH_WORKTREE_PREFIX, squash_id)
+ env = git_env_for_user(user).merge(
+ 'GIT_AUTHOR_NAME' => author.name,
+ 'GIT_AUTHOR_EMAIL' => author.email
+ )
+ diff_range = "#{start_sha}...#{end_sha}"
+ diff_files = run_git!(
+ %W(diff --name-only --diff-filter=a --binary #{diff_range})
+ ).chomp
+
+ with_worktree(squash_path, branch, sparse_checkout_files: diff_files, env: env) do
+ # Apply diff of the `diff_range` to the worktree
+ diff = run_git!(%W(diff --binary #{diff_range}))
+ run_git!(%w(apply --index), chdir: squash_path, env: env) do |stdin|
+ stdin.write(diff)
+ end
+
+ # Commit the `diff_range` diff
+ run_git!(%W(commit --no-verify --message #{message}), chdir: squash_path, env: env)
+
+ # Return the squash sha. May print a warning for ambiguous refs, but
+ # we can ignore that with `--quiet` and just take the SHA, if present.
+ # HEAD here always refers to the current HEAD commit, even if there is
+ # another ref called HEAD.
+ run_git!(
+ %w(rev-parse --quiet --verify HEAD), chdir: squash_path, env: env
+ ).chomp
+ end
+ end
+
def local_fetch_ref(source_path, source_ref:, target_ref:)
args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
run_git(args)
@@ -2354,6 +2396,10 @@ module Gitlab
rescue Rugged::ReferenceError
0
end
+
+ def rev_list_param(spec)
+ spec == :all ? ['--all'] : spec
+ end
end
end
end
diff --git a/lib/gitlab/git/repository_mirroring.rb b/lib/gitlab/git/repository_mirroring.rb
index effb1f0ca19..dc424a433fb 100644
--- a/lib/gitlab/git/repository_mirroring.rb
+++ b/lib/gitlab/git/repository_mirroring.rb
@@ -43,7 +43,7 @@ module Gitlab
branches = []
rugged.references.each("refs/remotes/#{remote_name}/*").map do |ref|
- name = ref.name.sub(/\Arefs\/remotes\/#{remote_name}\//, '')
+ name = ref.name.sub(%r{\Arefs/remotes/#{remote_name}/}, '')
begin
target_commit = Gitlab::Git::Commit.find(self, ref.target)
diff --git a/lib/gitlab/git/rev_list.rb b/lib/gitlab/git/rev_list.rb
index f8b2e7e0e21..38c3a55f96f 100644
--- a/lib/gitlab/git/rev_list.rb
+++ b/lib/gitlab/git/rev_list.rb
@@ -5,17 +5,17 @@ module Gitlab
class RevList
include Gitlab::Git::Popen
- attr_reader :oldrev, :newrev, :path_to_repo
+ attr_reader :oldrev, :newrev, :repository
- def initialize(path_to_repo:, newrev:, oldrev: nil)
+ def initialize(repository, newrev:, oldrev: nil)
@oldrev = oldrev
@newrev = newrev
- @path_to_repo = path_to_repo
+ @repository = repository
end
# This method returns an array of new commit references
def new_refs
- execute([*base_args, newrev, '--not', '--all'])
+ repository.rev_list(including: newrev, excluding: :all).split("\n")
end
# Finds newly added objects
@@ -28,66 +28,39 @@ module Gitlab
# When given a block it will yield objects as a lazy enumerator so
# the caller can limit work done instead of processing megabytes of data
def new_objects(require_path: nil, not_in: nil, &lazy_block)
- args = [*base_args, newrev, *not_in_refs(not_in), '--objects']
+ opts = {
+ including: newrev,
+ excluding: not_in.nil? ? :all : not_in,
+ require_path: require_path
+ }
- get_objects(args, require_path: require_path, &lazy_block)
+ get_objects(opts, &lazy_block)
end
def all_objects(require_path: nil, &lazy_block)
- args = [*base_args, '--all', '--objects']
-
- get_objects(args, require_path: require_path, &lazy_block)
+ get_objects(including: :all, require_path: require_path, &lazy_block)
end
# This methods returns an array of missed references
#
# Should become obsolete after https://gitlab.com/gitlab-org/gitaly/issues/348.
def missed_ref
- execute([*base_args, '--max-count=1', oldrev, "^#{newrev}"])
+ repository.missed_ref(oldrev, newrev).split("\n")
end
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)
-
- unless status.zero?
- raise "Got a non-zero exit code while calling out `#{args.join(' ')}`: #{output}"
- end
-
- output.split("\n")
- end
-
- def lazy_execute(args, &lazy_block)
- popen(args, nil, Gitlab::Git::Env.to_env_hash, lazy_block: lazy_block)
- end
-
- def base_args
- [
- Gitlab.config.git.bin_path,
- "--git-dir=#{path_to_repo}",
- 'rev-list'
- ]
+ repository.rev_list(args).split("\n")
end
- def get_objects(args, require_path: nil)
- if block_given?
- lazy_execute(args) do |lazy_output|
- objects = objects_from_output(lazy_output, require_path: require_path)
+ def get_objects(including: [], excluding: [], require_path: nil)
+ opts = { including: including, excluding: excluding, objects: true }
- yield(objects)
- end
- else
- object_output = execute(args)
+ repository.rev_list(opts) do |lazy_output|
+ objects = objects_from_output(lazy_output, require_path: require_path)
- objects_from_output(object_output, require_path: require_path)
+ yield(objects)
end
end
diff --git a/lib/gitlab/git/tag.rb b/lib/gitlab/git/tag.rb
index bc4e160dce9..8a8f7b051ed 100644
--- a/lib/gitlab/git/tag.rb
+++ b/lib/gitlab/git/tag.rb
@@ -1,5 +1,3 @@
-# Gitaly note: JV: no RPC's here.
-#
module Gitlab
module Git
class Tag < Ref
diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb
index ccdb8975342..39040d56971 100644
--- a/lib/gitlab/git/wiki.rb
+++ b/lib/gitlab/git/wiki.rb
@@ -25,8 +25,9 @@ module Gitlab
@repository.exists?
end
+ # Disabled because of https://gitlab.com/gitlab-org/gitaly/merge_requests/539
def write_page(name, format, content, commit_details)
- @repository.gitaly_migrate(:wiki_write_page) do |is_enabled|
+ @repository.gitaly_migrate(:wiki_write_page, status: Gitlab::GitalyClient::MigrationStatus::DISABLED) do |is_enabled|
if is_enabled
gitaly_write_page(name, format, content, commit_details)
gollum_wiki.clear_cache
@@ -47,8 +48,9 @@ module Gitlab
end
end
+ # Disable because of https://gitlab.com/gitlab-org/gitlab-ce/issues/42094
def update_page(page_path, title, format, content, commit_details)
- @repository.gitaly_migrate(:wiki_update_page) do |is_enabled|
+ @repository.gitaly_migrate(:wiki_update_page, status: Gitlab::GitalyClient::MigrationStatus::DISABLED) do |is_enabled|
if is_enabled
gitaly_update_page(page_path, title, format, content, commit_details)
gollum_wiki.clear_cache
@@ -68,8 +70,9 @@ module Gitlab
end
end
+ # Disable because of https://gitlab.com/gitlab-org/gitlab-ce/issues/42039
def page(title:, version: nil, dir: nil)
- @repository.gitaly_migrate(:wiki_find_page) do |is_enabled|
+ @repository.gitaly_migrate(:wiki_find_page, status: Gitlab::GitalyClient::MigrationStatus::DISABLED) do |is_enabled|
if is_enabled
gitaly_find_page(title: title, version: version, dir: dir)
else
@@ -93,11 +96,23 @@ module Gitlab
# :per_page - The number of items per page.
# :limit - Total number of items to return.
def page_versions(page_path, options = {})
- current_page = gollum_page_by_path(page_path)
+ @repository.gitaly_migrate(:wiki_page_versions) do |is_enabled|
+ if is_enabled
+ versions = gitaly_wiki_client.page_versions(page_path, options)
+
+ # Gitaly uses gollum-lib to get the versions. Gollum defaults to 20
+ # per page, but also fetches 20 if `limit` or `per_page` < 20.
+ # Slicing returns an array with the expected number of items.
+ slice_bound = options[:limit] || options[:per_page] || Gollum::Page.per_page
+ versions[0..slice_bound]
+ else
+ current_page = gollum_page_by_path(page_path)
- commits_from_page(current_page, options).map do |gitlab_git_commit|
- gollum_page = gollum_wiki.page(current_page.title, gitlab_git_commit.id)
- Gitlab::Git::WikiPageVersion.new(gitlab_git_commit, gollum_page&.format)
+ commits_from_page(current_page, options).map do |gitlab_git_commit|
+ gollum_page = gollum_wiki.page(current_page.title, gitlab_git_commit.id)
+ Gitlab::Git::WikiPageVersion.new(gitlab_git_commit, gollum_page&.format)
+ end
+ end
end
end
@@ -192,7 +207,10 @@ module Gitlab
assert_type!(format, Symbol)
assert_type!(commit_details, CommitDetails)
- gollum_wiki.write_page(name, format, content, commit_details.to_h)
+ filename = File.basename(name)
+ dir = (tmp_dir = File.dirname(name)) == '.' ? '' : tmp_dir
+
+ gollum_wiki.write_page(filename, format, content, commit_details.to_h, dir)
nil
rescue Gollum::DuplicatePageError => e
@@ -210,7 +228,15 @@ module Gitlab
assert_type!(format, Symbol)
assert_type!(commit_details, CommitDetails)
- gollum_wiki.update_page(gollum_page_by_path(page_path), title, format, content, commit_details.to_h)
+ page = gollum_page_by_path(page_path)
+ committer = Gollum::Committer.new(page.wiki, commit_details.to_h)
+
+ # Instead of performing two renames if the title has changed,
+ # the update_page will only update the format and content and
+ # the rename_page will do anything related to moving/renaming
+ gollum_wiki.update_page(page, page.name, format, content, committer: committer)
+ gollum_wiki.rename_page(page, title, committer: committer)
+ committer.commit
nil
end
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index cadc7149301..5767f06b0ce 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -257,7 +257,7 @@ module Gitlab
offset: options[:offset],
follow: options[:follow],
skip_merges: options[:skip_merges],
- disable_walk: options[:disable_walk]
+ disable_walk: true # This option is deprecated. The 'walk' implementation is being removed.
)
request.after = GitalyClient.timestamp(options[:after]) if options[:after]
request.before = GitalyClient.timestamp(options[:before]) if options[:before]
diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb
index cd2734b5a07..831cfd1e014 100644
--- a/lib/gitlab/gitaly_client/operation_service.rb
+++ b/lib/gitlab/gitaly_client/operation_service.rb
@@ -183,6 +183,32 @@ module Gitlab
end
end
+ def user_squash(user, squash_id, branch, start_sha, end_sha, author, message)
+ request = Gitaly::UserSquashRequest.new(
+ repository: @gitaly_repo,
+ user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
+ squash_id: squash_id.to_s,
+ branch: encode_binary(branch),
+ start_sha: start_sha,
+ end_sha: end_sha,
+ author: Gitlab::Git::User.from_gitlab(author).to_gitaly,
+ commit_message: encode_binary(message)
+ )
+
+ response = GitalyClient.call(
+ @repository.storage,
+ :operation_service,
+ :user_squash,
+ request
+ )
+
+ if response.git_error.presence
+ raise Gitlab::Git::Repository::GitError, response.git_error
+ end
+
+ response.squash_sha
+ end
+
def user_commit_files(
user, branch_name, commit_message, actions, author_email, author_name,
start_branch_name, start_repository)
diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb
index 8b9a224b700..ba6b577fd17 100644
--- a/lib/gitlab/gitaly_client/ref_service.rb
+++ b/lib/gitlab/gitaly_client/ref_service.rb
@@ -145,6 +145,32 @@ module Gitlab
raise Gitlab::Git::Repository::GitError, response.git_error if response.git_error.present?
end
+ # Limit: 0 implies no limit, thus all tag names will be returned
+ def tag_names_contains_sha(sha, limit: 0)
+ request = Gitaly::ListTagNamesContainingCommitRequest.new(
+ repository: @gitaly_repo,
+ commit_id: sha,
+ limit: limit
+ )
+
+ stream = GitalyClient.call(@repository.storage, :ref_service, :list_tag_names_containing_commit, request)
+
+ consume_ref_contains_sha_response(stream, :tag_names)
+ end
+
+ # Limit: 0 implies no limit, thus all tag names will be returned
+ def branch_names_contains_sha(sha, limit: 0)
+ request = Gitaly::ListBranchNamesContainingCommitRequest.new(
+ repository: @gitaly_repo,
+ commit_id: sha,
+ limit: limit
+ )
+
+ stream = GitalyClient.call(@repository.storage, :ref_service, :list_branch_names_containing_commit, request)
+
+ consume_ref_contains_sha_response(stream, :branch_names)
+ end
+
private
def consume_refs_response(response)
@@ -215,6 +241,13 @@ module Gitlab
Gitlab::Git::Commit.decorate(@repository, hash)
end
+ def consume_ref_contains_sha_response(stream, collection_name)
+ stream.each_with_object([]) do |response, array|
+ encoded_names = response.send(collection_name).map { |b| Gitlab::Git.ref_name(b) } # rubocop:disable GitlabSecurity/PublicSend
+ array.concat(encoded_names)
+ end
+ end
+
def invalid_ref!(message)
raise Gitlab::Git::Repository::InvalidRef.new(message)
end
diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb
index 7adf32af209..60706b4f0d8 100644
--- a/lib/gitlab/gitaly_client/repository_service.rb
+++ b/lib/gitlab/gitaly_client/repository_service.rb
@@ -219,6 +219,19 @@ module Gitlab
true
end
+
+ def write_config(full_path:)
+ request = Gitaly::WriteConfigRequest.new(repository: @gitaly_repo, full_path: full_path)
+ response = GitalyClient.call(
+ @storage,
+ :repository_service,
+ :write_config,
+ request,
+ timeout: GitalyClient.fast_timeout
+ )
+
+ raise Gitlab::Git::OSError.new(response.error) unless response.error.empty?
+ end
end
end
end
diff --git a/lib/gitlab/gitaly_client/wiki_page.rb b/lib/gitlab/gitaly_client/wiki_page.rb
index 7339468e911..a02d15db5dd 100644
--- a/lib/gitlab/gitaly_client/wiki_page.rb
+++ b/lib/gitlab/gitaly_client/wiki_page.rb
@@ -4,6 +4,7 @@ module Gitlab
ATTRS = %i(title format url_path path name historical raw_data).freeze
include AttributesBag
+ include Gitlab::EncodingHelper
def initialize(params)
super
@@ -11,6 +12,10 @@ module Gitlab
# All gRPC strings in a response are frozen, so we get an unfrozen
# version here so appending to `raw_data` doesn't blow up.
@raw_data = @raw_data.dup
+
+ @title = encode_utf8(@title)
+ @path = encode_utf8(@path)
+ @name = encode_utf8(@name)
end
def historical?
diff --git a/lib/gitlab/gitaly_client/wiki_service.rb b/lib/gitlab/gitaly_client/wiki_service.rb
index 8e87a8cc36f..0d8dd5cb8f4 100644
--- a/lib/gitlab/gitaly_client/wiki_service.rb
+++ b/lib/gitlab/gitaly_client/wiki_service.rb
@@ -101,6 +101,30 @@ module Gitlab
pages
end
+ # options:
+ # :page - The Integer page number.
+ # :per_page - The number of items per page.
+ # :limit - Total number of items to return.
+ def page_versions(page_path, options)
+ request = Gitaly::WikiGetPageVersionsRequest.new(
+ repository: @gitaly_repo,
+ page_path: encode_binary(page_path),
+ page: options[:page] || 1,
+ per_page: options[:per_page] || Gollum::Page.per_page
+ )
+
+ stream = GitalyClient.call(@repository.storage, :wiki_service, :wiki_get_page_versions, request)
+
+ versions = []
+ stream.each do |message|
+ message.versions.each do |version|
+ versions << new_wiki_page_version(version)
+ end
+ end
+
+ versions
+ end
+
def find_file(name, revision)
request = Gitaly::WikiFindFileRequest.new(
repository: @gitaly_repo,
@@ -141,7 +165,7 @@ module Gitlab
private
- # If a block is given and the yielded value is true, iteration will be
+ # If a block is given and the yielded value is truthy, iteration will be
# stopped early at that point; else the iterator is consumed entirely.
# The iterator is traversed with `next` to allow resuming the iteration.
def wiki_page_from_iterator(iterator)
@@ -158,10 +182,7 @@ module Gitlab
else
wiki_page = GitalyClient::WikiPage.new(page.to_h)
- version = Gitlab::Git::WikiPageVersion.new(
- Gitlab::Git::Commit.decorate(@repository, page.version.commit),
- page.version.format
- )
+ version = new_wiki_page_version(page.version)
end
end
@@ -170,6 +191,13 @@ module Gitlab
[wiki_page, version]
end
+ def new_wiki_page_version(version)
+ Gitlab::Git::WikiPageVersion.new(
+ Gitlab::Git::Commit.decorate(@repository, version.commit),
+ version.format
+ )
+ end
+
def gitaly_commit_details(commit_details)
Gitaly::WikiCommitDetails.new(
name: encode_binary(commit_details.name),
diff --git a/lib/gitlab/github_import/representation/diff_note.rb b/lib/gitlab/github_import/representation/diff_note.rb
index bb7439a0641..be1334ca98a 100644
--- a/lib/gitlab/github_import/representation/diff_note.rb
+++ b/lib/gitlab/github_import/representation/diff_note.rb
@@ -13,7 +13,7 @@ module Gitlab
:diff_hunk, :author, :note, :created_at, :updated_at,
:github_id
- NOTEABLE_ID_REGEX = /\/pull\/(?<iid>\d+)/i
+ NOTEABLE_ID_REGEX = %r{/pull/(?<iid>\d+)}i
# Builds a diff note from a GitHub API response.
#
diff --git a/lib/gitlab/github_import/representation/note.rb b/lib/gitlab/github_import/representation/note.rb
index a68bc4c002f..070e3b2db8d 100644
--- a/lib/gitlab/github_import/representation/note.rb
+++ b/lib/gitlab/github_import/representation/note.rb
@@ -12,7 +12,7 @@ module Gitlab
expose_attribute :noteable_id, :noteable_type, :author, :note,
:created_at, :updated_at, :github_id
- NOTEABLE_TYPE_REGEX = /\/(?<type>(pull|issues))\/(?<iid>\d+)/i
+ NOTEABLE_TYPE_REGEX = %r{/(?<type>(pull|issues))/(?<iid>\d+)}i
# Builds a note from a GitHub API response.
#
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 9148d7571f2..86a90d57d9c 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -3,12 +3,11 @@
module Gitlab
module GonHelper
include WebpackHelper
- include Gitlab::CurrentSettings
def add_gon_variables
gon.api_version = 'v4'
gon.default_avatar_url = URI.join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s
- gon.max_file_size = current_application_settings.max_attachment_size
+ gon.max_file_size = Gitlab::CurrentSettings.max_attachment_size
gon.asset_host = ActionController::Base.asset_host
gon.webpack_public_path = webpack_public_path
gon.relative_url_root = Gitlab.config.gitlab.relative_url_root
@@ -16,7 +15,7 @@ module Gitlab
gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class
gon.katex_css_url = ActionController::Base.helpers.asset_path('katex.css')
gon.katex_js_url = ActionController::Base.helpers.asset_path('katex.js')
- gon.sentry_dsn = current_application_settings.clientside_sentry_dsn if current_application_settings.clientside_sentry_enabled
+ gon.sentry_dsn = Gitlab::CurrentSettings.clientside_sentry_dsn if Gitlab::CurrentSettings.clientside_sentry_enabled
gon.gitlab_url = Gitlab.config.gitlab.url
gon.revision = Gitlab::REVISION
gon.gitlab_logo = ActionController::Base.helpers.asset_path('gitlab_logo.png')
diff --git a/lib/gitlab/import_export/file_importer.rb b/lib/gitlab/import_export/file_importer.rb
index 5c971564a73..0f4c3498036 100644
--- a/lib/gitlab/import_export/file_importer.rb
+++ b/lib/gitlab/import_export/file_importer.rb
@@ -59,7 +59,7 @@ module Gitlab
end
def extracted_files
- Dir.glob("#{@shared.export_path}/**/*", File::FNM_DOTMATCH).reject { |f| f =~ /.*\/\.{1,2}$/ }
+ Dir.glob("#{@shared.export_path}/**/*", File::FNM_DOTMATCH).reject { |f| f =~ %r{.*/\.{1,2}$} }
end
end
end
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index cb711a83433..759833a5ee5 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -139,13 +139,12 @@ module Gitlab
end
def setup_label
- return unless @relation_hash['type'] == 'GroupLabel'
-
# If there's no group, move the label to a project label
- if @relation_hash['group_id']
+ if @relation_hash['type'] == 'GroupLabel' && @relation_hash['group_id']
@relation_hash['project_id'] = nil
@relation_name = :group_label
else
+ @relation_hash['group_id'] = nil
@relation_hash['type'] = 'ProjectLabel'
end
end
diff --git a/lib/gitlab/import_export/uploads_saver.rb b/lib/gitlab/import_export/uploads_saver.rb
index 627a487d577..2f08dda55fd 100644
--- a/lib/gitlab/import_export/uploads_saver.rb
+++ b/lib/gitlab/import_export/uploads_saver.rb
@@ -17,15 +17,13 @@ module Gitlab
false
end
- private
+ def uploads_path
+ FileUploader.absolute_base_dir(@project)
+ end
def uploads_export_path
File.join(@shared.export_path, 'uploads')
end
-
- def uploads_path
- FileUploader.dynamic_path_segment(@project)
- end
end
end
end
diff --git a/lib/gitlab/kubernetes/helm/pod.rb b/lib/gitlab/kubernetes/helm/pod.rb
index a3216759cae..ca5e06009fa 100644
--- a/lib/gitlab/kubernetes/helm/pod.rb
+++ b/lib/gitlab/kubernetes/helm/pod.rb
@@ -64,7 +64,7 @@ module Gitlab
{
name: 'configuration-volume',
configMap: {
- name: 'values-content-configuration',
+ name: "values-content-configuration-#{command.name}",
items: [{ key: 'values', path: 'values.yaml' }]
}
}
@@ -81,7 +81,11 @@ module Gitlab
def create_config_map
resource = ::Kubeclient::Resource.new
- resource.metadata = { name: 'values-content-configuration', namespace: namespace_name, labels: { name: 'values-content-configuration' } }
+ resource.metadata = {
+ name: "values-content-configuration-#{command.name}",
+ namespace: namespace_name,
+ labels: { name: "values-content-configuration-#{command.name}" }
+ }
resource.data = { values: File.read(command.chart_values_file) }
kubeclient.create_config_map(resource)
end
diff --git a/lib/gitlab/ldap/auth_hash.rb b/lib/gitlab/ldap/auth_hash.rb
index 1bd0965679a..96171dc26c4 100644
--- a/lib/gitlab/ldap/auth_hash.rb
+++ b/lib/gitlab/ldap/auth_hash.rb
@@ -7,6 +7,12 @@ module Gitlab
@uid ||= Gitlab::LDAP::Person.normalize_dn(super)
end
+ def username
+ super.tap do |username|
+ username.downcase! if ldap_config.lowercase_usernames
+ end
+ end
+
private
def get_info(key)
diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb
index cde60addcf7..47b3fce3e7a 100644
--- a/lib/gitlab/ldap/config.rb
+++ b/lib/gitlab/ldap/config.rb
@@ -139,6 +139,10 @@ module Gitlab
options['allow_username_or_email_login']
end
+ def lowercase_usernames
+ options['lowercase_usernames']
+ end
+
def name_proc
if allow_username_or_email_login
proc { |name| name.gsub(/@.*\z/, '') }
diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb
index e81cec6ba1a..b91757c2a4b 100644
--- a/lib/gitlab/ldap/person.rb
+++ b/lib/gitlab/ldap/person.rb
@@ -82,7 +82,9 @@ module Gitlab
# be returned. We need only one for username.
# Ex. `uid` returns only one value but `mail` may
# return an array of multiple email addresses.
- [username].flatten.first
+ [username].flatten.first.tap do |username|
+ username.downcase! if config.lowercase_usernames
+ end
end
def email
diff --git a/lib/gitlab/legacy_github_import/project_creator.rb b/lib/gitlab/legacy_github_import/project_creator.rb
index 41e7eac4d08..cbabe5454ca 100644
--- a/lib/gitlab/legacy_github_import/project_creator.rb
+++ b/lib/gitlab/legacy_github_import/project_creator.rb
@@ -1,8 +1,6 @@
module Gitlab
module LegacyGithubImport
class ProjectCreator
- include Gitlab::CurrentSettings
-
attr_reader :repo, :name, :namespace, :current_user, :session_data, :type
def initialize(repo, name, namespace, current_user, session_data, type: 'github')
@@ -36,7 +34,7 @@ module Gitlab
end
def visibility_level
- repo.private ? Gitlab::VisibilityLevel::PRIVATE : current_application_settings.default_project_visibility
+ repo.private ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::CurrentSettings.default_project_visibility
end
#
diff --git a/lib/gitlab/metrics/prometheus.rb b/lib/gitlab/metrics/prometheus.rb
index f07ea3560ff..d12ba0ec176 100644
--- a/lib/gitlab/metrics/prometheus.rb
+++ b/lib/gitlab/metrics/prometheus.rb
@@ -71,8 +71,7 @@ module Gitlab
end
def prometheus_metrics_enabled_unmemoized
- metrics_folder_present? &&
- Gitlab::CurrentSettings.current_application_settings[:prometheus_metrics_enabled] || false
+ metrics_folder_present? && Gitlab::CurrentSettings.prometheus_metrics_enabled || false
end
end
end
diff --git a/lib/gitlab/metrics/subscribers/action_view.rb b/lib/gitlab/metrics/subscribers/action_view.rb
index 274436ca2b4..b600e8a2a50 100644
--- a/lib/gitlab/metrics/subscribers/action_view.rb
+++ b/lib/gitlab/metrics/subscribers/action_view.rb
@@ -34,7 +34,7 @@ module Gitlab
end
def relative_path(path)
- path.gsub(/^#{Rails.root.to_s}\/?/, '')
+ path.gsub(%r{^#{Rails.root.to_s}/?}, '')
end
def values_for(event)
diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb
index c6a56277922..1a570f480c6 100644
--- a/lib/gitlab/middleware/go.rb
+++ b/lib/gitlab/middleware/go.rb
@@ -4,7 +4,6 @@ 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
@@ -42,7 +41,7 @@ module Gitlab
project_url = URI.join(config.gitlab.url, path)
import_prefix = strip_url(project_url.to_s)
- repository_url = if current_application_settings.enabled_git_access_protocol == 'ssh'
+ repository_url = if Gitlab::CurrentSettings.enabled_git_access_protocol == 'ssh'
shell = config.gitlab_shell
port = ":#{shell.ssh_port}" unless shell.ssh_port == 22
"ssh://#{shell.ssh_user}@#{shell.ssh_host}#{port}/#{path}.git"
@@ -56,12 +55,12 @@ module Gitlab
end
def strip_url(url)
- url.gsub(/\Ahttps?:\/\//, '')
+ url.gsub(%r{\Ahttps?://}, '')
end
def project_path(request)
path_info = request.env["PATH_INFO"]
- path_info.sub!(/^\//, '')
+ path_info.sub!(%r{^/}, '')
project_path_match = "#{path_info}/".match(PROJECT_PATH_REGEX)
return unless project_path_match
diff --git a/lib/gitlab/middleware/static.rb b/lib/gitlab/middleware/static.rb
index 85ffa8aca68..aa1e9dc0fdb 100644
--- a/lib/gitlab/middleware/static.rb
+++ b/lib/gitlab/middleware/static.rb
@@ -1,7 +1,7 @@
module Gitlab
module Middleware
class Static < ActionDispatch::Static
- UPLOADS_REGEX = /\A\/uploads(\/|\z)/.freeze
+ UPLOADS_REGEX = %r{\A/uploads(/|\z)}.freeze
def call(env)
return @app.call(env) if env['PATH_INFO'] =~ UPLOADS_REGEX
diff --git a/lib/gitlab/performance_bar.rb b/lib/gitlab/performance_bar.rb
index e29e168fc5a..6c2b2036074 100644
--- a/lib/gitlab/performance_bar.rb
+++ b/lib/gitlab/performance_bar.rb
@@ -1,7 +1,5 @@
module Gitlab
module PerformanceBar
- extend Gitlab::CurrentSettings
-
ALLOWED_USER_IDS_KEY = 'performance_bar_allowed_user_ids:v2'.freeze
EXPIRY_TIME = 5.minutes
@@ -13,7 +11,7 @@ module Gitlab
end
def self.allowed_group_id
- current_application_settings.performance_bar_allowed_group_id
+ Gitlab::CurrentSettings.performance_bar_allowed_group_id
end
def self.allowed_user_ids
diff --git a/lib/gitlab/polling_interval.rb b/lib/gitlab/polling_interval.rb
index 4780675a492..fe4bdfe3831 100644
--- a/lib/gitlab/polling_interval.rb
+++ b/lib/gitlab/polling_interval.rb
@@ -1,12 +1,10 @@
module Gitlab
class PollingInterval
- extend Gitlab::CurrentSettings
-
HEADER_NAME = 'Poll-Interval'.freeze
def self.set_header(response, interval:)
if polling_enabled?
- multiplier = current_application_settings.polling_interval_multiplier
+ multiplier = Gitlab::CurrentSettings.polling_interval_multiplier
value = (interval * multiplier).to_i
else
value = -1
@@ -16,7 +14,7 @@ module Gitlab
end
def self.polling_enabled?
- !current_application_settings.polling_interval_multiplier.zero?
+ !Gitlab::CurrentSettings.polling_interval_multiplier.zero?
end
end
end
diff --git a/lib/gitlab/protocol_access.rb b/lib/gitlab/protocol_access.rb
index 09fa14764e6..2819c7d062c 100644
--- a/lib/gitlab/protocol_access.rb
+++ b/lib/gitlab/protocol_access.rb
@@ -1,14 +1,12 @@
module Gitlab
module ProtocolAccess
- extend Gitlab::CurrentSettings
-
def self.allowed?(protocol)
if protocol == 'web'
true
- elsif current_application_settings.enabled_git_access_protocol.blank?
+ elsif Gitlab::CurrentSettings.enabled_git_access_protocol.blank?
true
else
- protocol == current_application_settings.enabled_git_access_protocol
+ protocol == Gitlab::CurrentSettings.enabled_git_access_protocol
end
end
end
diff --git a/lib/gitlab/query_limiting.rb b/lib/gitlab/query_limiting.rb
new file mode 100644
index 00000000000..f64f1757144
--- /dev/null
+++ b/lib/gitlab/query_limiting.rb
@@ -0,0 +1,36 @@
+module Gitlab
+ module QueryLimiting
+ # Returns true if we should enable tracking of query counts.
+ #
+ # This is only enabled in production/staging if we're running on GitLab.com.
+ # This ensures we don't produce any errors that users can't do anything
+ # about themselves.
+ def self.enable?
+ Gitlab.com? || Rails.env.development? || Rails.env.test?
+ end
+
+ # Allows the current request to execute any number of SQL queries.
+ #
+ # This method should _only_ be used when there's a corresponding issue to
+ # reduce the number of queries.
+ #
+ # The issue URL is only meant to push developers into creating an issue
+ # instead of blindly whitelisting offending blocks of code.
+ def self.whitelist(issue_url)
+ return unless enable_whitelist?
+
+ unless issue_url.start_with?('https://')
+ raise(
+ ArgumentError,
+ 'You must provide a valid issue URL in order to whitelist a block of code'
+ )
+ end
+
+ Transaction&.current&.whitelisted = true
+ end
+
+ def self.enable_whitelist?
+ Rails.env.development? || Rails.env.test?
+ end
+ end
+end
diff --git a/lib/gitlab/query_limiting/active_support_subscriber.rb b/lib/gitlab/query_limiting/active_support_subscriber.rb
new file mode 100644
index 00000000000..66049c94ec6
--- /dev/null
+++ b/lib/gitlab/query_limiting/active_support_subscriber.rb
@@ -0,0 +1,11 @@
+module Gitlab
+ module QueryLimiting
+ class ActiveSupportSubscriber < ActiveSupport::Subscriber
+ attach_to :active_record
+
+ def sql(*)
+ Transaction.current&.increment
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/query_limiting/middleware.rb b/lib/gitlab/query_limiting/middleware.rb
new file mode 100644
index 00000000000..949ae79a047
--- /dev/null
+++ b/lib/gitlab/query_limiting/middleware.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module QueryLimiting
+ # Middleware for reporting (or raising) when a request performs more than a
+ # certain amount of database queries.
+ class Middleware
+ CONTROLLER_KEY = 'action_controller.instance'.freeze
+ ENDPOINT_KEY = 'api.endpoint'.freeze
+
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ transaction, retval = Transaction.run do
+ @app.call(env)
+ end
+
+ transaction.action = action_name(env)
+ transaction.act_upon_results
+
+ retval
+ end
+
+ def action_name(env)
+ if env[CONTROLLER_KEY]
+ action_for_rails(env)
+ elsif env[ENDPOINT_KEY]
+ action_for_grape(env)
+ end
+ end
+
+ private
+
+ def action_for_rails(env)
+ controller = env[CONTROLLER_KEY]
+ action = "#{controller.class.name}##{controller.action_name}"
+
+ if controller.content_type == 'text/html'
+ action
+ else
+ "#{action} (#{controller.content_type})"
+ end
+ end
+
+ def action_for_grape(env)
+ endpoint = env[ENDPOINT_KEY]
+ route = endpoint.route rescue nil
+
+ "#{route.request_method} #{route.path}" if route
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/query_limiting/transaction.rb b/lib/gitlab/query_limiting/transaction.rb
new file mode 100644
index 00000000000..3cbafadb0d0
--- /dev/null
+++ b/lib/gitlab/query_limiting/transaction.rb
@@ -0,0 +1,83 @@
+module Gitlab
+ module QueryLimiting
+ class Transaction
+ THREAD_KEY = :__gitlab_query_counts_transaction
+
+ attr_accessor :count, :whitelisted
+
+ # The name of the action (e.g. `UsersController#show`) that is being
+ # executed.
+ attr_accessor :action
+
+ # The maximum number of SQL queries that can be executed in a request. For
+ # the sake of keeping things simple we hardcode this value here, it's not
+ # supposed to be changed very often anyway.
+ THRESHOLD = 100
+
+ # Error that is raised whenever exceeding the maximum number of queries.
+ ThresholdExceededError = Class.new(StandardError)
+
+ def self.current
+ Thread.current[THREAD_KEY]
+ end
+
+ # Starts a new transaction and returns it and the blocks' return value.
+ #
+ # Example:
+ #
+ # transaction, retval = Transaction.run do
+ # 10
+ # end
+ #
+ # retval # => 10
+ def self.run
+ transaction = new
+ Thread.current[THREAD_KEY] = transaction
+
+ [transaction, yield]
+ ensure
+ Thread.current[THREAD_KEY] = nil
+ end
+
+ def initialize
+ @action = nil
+ @count = 0
+ @whitelisted = false
+ end
+
+ # Sends a notification based on the number of executed SQL queries.
+ def act_upon_results
+ return unless threshold_exceeded?
+
+ error = ThresholdExceededError.new(error_message)
+
+ if raise_error?
+ raise(error)
+ else
+ # Raven automatically logs to the Rails log if disabled, thus we don't
+ # need to manually log anything in case Sentry support is not enabled.
+ Raven.capture_exception(error)
+ end
+ end
+
+ def increment
+ @count += 1 unless whitelisted
+ end
+
+ def raise_error?
+ Rails.env.test?
+ end
+
+ def threshold_exceeded?
+ count > THRESHOLD
+ end
+
+ def error_message
+ header = 'Too many SQL queries were executed'
+ header += " in #{action}" if action
+
+ "#{header}: a maximum of #{THRESHOLD} is allowed but #{count} SQL queries were executed"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/quick_actions/spend_time_and_date_separator.rb b/lib/gitlab/quick_actions/spend_time_and_date_separator.rb
index 3f52402b31f..7328c517a30 100644
--- a/lib/gitlab/quick_actions/spend_time_and_date_separator.rb
+++ b/lib/gitlab/quick_actions/spend_time_and_date_separator.rb
@@ -9,7 +9,7 @@ module Gitlab
# if date doesn't present return time with current date
# in other cases return nil
class SpendTimeAndDateSeparator
- DATE_REGEX = /(\d{2,4}[\/\-.]\d{1,2}[\/\-.]\d{1,2})/
+ DATE_REGEX = %r{(\d{2,4}[/\-.]\d{1,2}[/\-.]\d{1,2})}
def initialize(spend_command_arg)
@spend_arg = spend_command_arg
diff --git a/lib/gitlab/recaptcha.rb b/lib/gitlab/recaptcha.rb
index c463dd487a0..c9efa28d7e7 100644
--- a/lib/gitlab/recaptcha.rb
+++ b/lib/gitlab/recaptcha.rb
@@ -1,12 +1,10 @@
module Gitlab
module Recaptcha
- extend Gitlab::CurrentSettings
-
def self.load_configurations!
- if current_application_settings.recaptcha_enabled
+ if Gitlab::CurrentSettings.recaptcha_enabled
::Recaptcha.configure do |config|
- config.public_key = current_application_settings.recaptcha_site_key
- config.private_key = current_application_settings.recaptcha_private_key
+ config.public_key = Gitlab::CurrentSettings.recaptcha_site_key
+ config.private_key = Gitlab::CurrentSettings.recaptcha_private_key
end
true
@@ -14,7 +12,7 @@ module Gitlab
end
def self.enabled?
- current_application_settings.recaptcha_enabled
+ Gitlab::CurrentSettings.recaptcha_enabled
end
end
end
diff --git a/lib/gitlab/repo_path.rb b/lib/gitlab/repo_path.rb
index 3591fa9145e..79265cf952d 100644
--- a/lib/gitlab/repo_path.rb
+++ b/lib/gitlab/repo_path.rb
@@ -30,7 +30,7 @@ module Gitlab
raise NotFoundError.new("No known storage path matches #{repo_path.inspect}")
end
- result.sub(/\A\/*/, '')
+ result.sub(%r{\A/*}, '')
end
def self.find_project(project_path)
diff --git a/lib/gitlab/sentry.rb b/lib/gitlab/sentry.rb
index 159d0e7952e..4a22fc80f75 100644
--- a/lib/gitlab/sentry.rb
+++ b/lib/gitlab/sentry.rb
@@ -1,9 +1,7 @@
module Gitlab
module Sentry
- extend Gitlab::CurrentSettings
-
def self.enabled?
- Rails.env.production? && current_application_settings.sentry_enabled?
+ Rails.env.production? && Gitlab::CurrentSettings.sentry_enabled?
end
def self.context(current_user = nil)
diff --git a/lib/gitlab/setup_helper.rb b/lib/gitlab/setup_helper.rb
index d01213bb6e0..e90a90508a2 100644
--- a/lib/gitlab/setup_helper.rb
+++ b/lib/gitlab/setup_helper.rb
@@ -31,7 +31,7 @@ module Gitlab
storages << { name: 'test_second_storage', path: Rails.root.join('tmp', 'tests', 'second_storage').to_s }
end
- config = { socket_path: address.sub(%r{\Aunix:}, ''), storage: storages }
+ config = { socket_path: address.sub(/\Aunix:/, ''), storage: storages }
config[:auth] = { token: 'secret' } if Rails.env.test?
config[:'gitaly-ruby'] = { dir: File.join(gitaly_dir, 'ruby') } if gitaly_ruby
config[:'gitlab-shell'] = { dir: Gitlab.config.gitlab_shell.path }
diff --git a/lib/gitlab/sherlock/file_sample.rb b/lib/gitlab/sherlock/file_sample.rb
index 8a3e1a5e5bf..89072b01f2e 100644
--- a/lib/gitlab/sherlock/file_sample.rb
+++ b/lib/gitlab/sherlock/file_sample.rb
@@ -16,7 +16,7 @@ module Gitlab
end
def relative_path
- @relative_path ||= @file.gsub(/^#{Rails.root.to_s}\/?/, '')
+ @relative_path ||= @file.gsub(%r{^#{Rails.root.to_s}/?}, '')
end
def to_param
diff --git a/lib/gitlab/sherlock/middleware.rb b/lib/gitlab/sherlock/middleware.rb
index 687332fc5fc..4c88e33699a 100644
--- a/lib/gitlab/sherlock/middleware.rb
+++ b/lib/gitlab/sherlock/middleware.rb
@@ -2,7 +2,7 @@ module Gitlab
module Sherlock
# Rack middleware used for tracking request metrics.
class Middleware
- CONTENT_TYPES = /text\/html|application\/json/i
+ CONTENT_TYPES = %r{text/html|application/json}i
IGNORE_PATHS = %r{^/sherlock}
diff --git a/lib/gitlab/sherlock/query.rb b/lib/gitlab/sherlock/query.rb
index 948bf5e6528..02ddc3f47eb 100644
--- a/lib/gitlab/sherlock/query.rb
+++ b/lib/gitlab/sherlock/query.rb
@@ -4,7 +4,7 @@ module Gitlab
attr_reader :id, :query, :started_at, :finished_at, :backtrace
# SQL identifiers that should be prefixed with newlines.
- PREFIX_NEWLINE = /
+ PREFIX_NEWLINE = %r{
\s+(FROM
|(LEFT|RIGHT)?INNER\s+JOIN
|(LEFT|RIGHT)?OUTER\s+JOIN
@@ -13,7 +13,7 @@ module Gitlab
|GROUP\s+BY
|ORDER\s+BY
|LIMIT
- |OFFSET)\s+/ix # Vim indent breaks when this is on a newline :<
+ |OFFSET)\s+}ix # Vim indent breaks when this is on a newline :<
# Creates a new Query using a String and a separate Array of bindings.
#
diff --git a/lib/gitlab/sidekiq_middleware/memory_killer.rb b/lib/gitlab/sidekiq_middleware/memory_killer.rb
index 2bfb7caefd9..b89ae2505c9 100644
--- a/lib/gitlab/sidekiq_middleware/memory_killer.rb
+++ b/lib/gitlab/sidekiq_middleware/memory_killer.rb
@@ -45,7 +45,7 @@ module Gitlab
private
def get_rss
- output, status = Gitlab::Popen.popen(%W(ps -o rss= -p #{pid}))
+ output, status = Gitlab::Popen.popen(%W(ps -o rss= -p #{pid}), Rails.root.to_s)
return 0 unless status.zero?
output.to_i
diff --git a/lib/gitlab/ssh_public_key.rb b/lib/gitlab/ssh_public_key.rb
index 89ca1298120..545e7c74f7e 100644
--- a/lib/gitlab/ssh_public_key.rb
+++ b/lib/gitlab/ssh_public_key.rb
@@ -21,6 +21,22 @@ module Gitlab
technology(name)&.supported_sizes
end
+ def self.sanitize(key_content)
+ ssh_type, *parts = key_content.strip.split
+
+ return key_content if parts.empty?
+
+ parts.each_with_object("#{ssh_type} ").with_index do |(part, content), index|
+ content << part
+
+ if Gitlab::SSHPublicKey.new(content).valid?
+ break [content, parts[index + 1]].compact.join(' ') # Add the comment part if present
+ elsif parts.size == index + 1 # return original content if we've reached the last element
+ break key_content
+ end
+ end
+ end
+
attr_reader :key_text, :key
# Unqualified MD5 fingerprint for compatibility
@@ -37,23 +53,23 @@ module Gitlab
end
def valid?
- key.present?
+ key.present? && bits && technology.supported_sizes.include?(bits)
end
def type
- technology.name if valid?
+ technology.name if key.present?
end
def bits
- return unless valid?
+ return if key.blank?
case type
when :rsa
- key.n.num_bits
+ key.n&.num_bits
when :dsa
- key.p.num_bits
+ key.p&.num_bits
when :ecdsa
- key.group.order.num_bits
+ key.group.order&.num_bits
when :ed25519
256
else
diff --git a/lib/gitlab/upgrader.rb b/lib/gitlab/upgrader.rb
index d545f2f95f1..024be6aca44 100644
--- a/lib/gitlab/upgrader.rb
+++ b/lib/gitlab/upgrader.rb
@@ -49,7 +49,7 @@ module Gitlab
def fetch_git_tags
remote_tags, _ = Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} ls-remote --tags https://gitlab.com/gitlab-org/gitlab-ce.git))
- remote_tags.split("\n").grep(/tags\/v#{current_version.major}/)
+ remote_tags.split("\n").grep(%r{tags/v#{current_version.major}})
end
def update_commands
diff --git a/lib/gitlab/uploads_transfer.rb b/lib/gitlab/uploads_transfer.rb
index b5f41240529..7d7400bdabf 100644
--- a/lib/gitlab/uploads_transfer.rb
+++ b/lib/gitlab/uploads_transfer.rb
@@ -1,7 +1,7 @@
module Gitlab
class UploadsTransfer < ProjectTransfer
def root_dir
- File.join(CarrierWave.root, FileUploader.base_dir)
+ FileUploader.root
end
end
end
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 2adcc9809b3..9d13d1d781f 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -1,8 +1,6 @@
module Gitlab
class UsageData
class << self
- include Gitlab::CurrentSettings
-
def data(force_refresh: false)
Rails.cache.fetch('usage_data', force: force_refresh, expires_in: 2.weeks) { uncached_data }
end
@@ -19,7 +17,7 @@ module Gitlab
def license_usage_data
usage_data = {
- uuid: current_application_settings.uuid,
+ uuid: Gitlab::CurrentSettings.uuid,
hostname: Gitlab.config.gitlab.host,
version: Gitlab::VERSION,
active_user_count: User.active.count,
@@ -79,9 +77,9 @@ module Gitlab
def features_usage_data_ce
{
- signup: current_application_settings.allow_signup?,
+ signup: Gitlab::CurrentSettings.allow_signup?,
ldap: Gitlab.config.ldap.enabled,
- gravatar: current_application_settings.gravatar_enabled?,
+ gravatar: Gitlab::CurrentSettings.gravatar_enabled?,
omniauth: Gitlab.config.omniauth.enabled,
reply_by_email: Gitlab::IncomingEmail.enabled?,
container_registry: Gitlab.config.registry.enabled,
diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb
index 6ced06a863d..2612208a927 100644
--- a/lib/gitlab/visibility_level.rb
+++ b/lib/gitlab/visibility_level.rb
@@ -5,7 +5,6 @@
#
module Gitlab
module VisibilityLevel
- extend CurrentSettings
extend ActiveSupport::Concern
included do
@@ -58,9 +57,9 @@ module Gitlab
end
def allowed_levels
- restricted_levels = current_application_settings.restricted_visibility_levels
+ restricted_levels = Gitlab::CurrentSettings.restricted_visibility_levels
- self.values - restricted_levels
+ self.values - Array(restricted_levels)
end
def closest_allowed_level(target_level)
@@ -81,7 +80,7 @@ module Gitlab
end
def non_restricted_level?(level)
- restricted_levels = current_application_settings.restricted_visibility_levels
+ restricted_levels = Gitlab::CurrentSettings.restricted_visibility_levels
if restricted_levels.nil?
true
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index 633da44b22d..823df67ea39 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -55,14 +55,14 @@ module Gitlab
def lfs_upload_ok(oid, size)
{
- StoreLFSPath: "#{Gitlab.config.lfs.storage_path}/tmp/upload",
+ StoreLFSPath: LfsObjectUploader.workhorse_upload_path,
LfsOid: oid,
LfsSize: size
}
end
def artifact_upload_ok
- { TempPath: JobArtifactUploader.artifacts_upload_path }
+ { TempPath: JobArtifactUploader.workhorse_upload_path }
end
def send_git_blob(repository, blob)
@@ -147,8 +147,11 @@ module Gitlab
end
def send_artifacts_entry(build, entry)
+ file = build.artifacts_file
+ archive = file.file_storage? ? file.path : file.url
+
params = {
- 'Archive' => build.artifacts_file.path,
+ 'Archive' => archive,
'Entry' => Base64.encode64(entry.to_s)
}
@@ -158,6 +161,18 @@ module Gitlab
]
end
+ def send_url(url, allow_redirects: false)
+ params = {
+ 'URL' => url,
+ 'AllowRedirects' => allow_redirects
+ }
+
+ [
+ SEND_DATA_HEADER,
+ "send-url:#{encode(params)}"
+ ]
+ end
+
def terminal_websocket(terminal)
details = {
'Terminal' => {
diff --git a/lib/tasks/flay.rake b/lib/tasks/flay.rake
index b1e012e70c5..4b4881cecb8 100644
--- a/lib/tasks/flay.rake
+++ b/lib/tasks/flay.rake
@@ -2,7 +2,7 @@ desc 'Code duplication analyze via flay'
task :flay do
output = `bundle exec flay --mass 35 app/ lib/gitlab/ 2> #{File::NULL}`
- if output.include? "Similar code found"
+ if output.include?("Similar code found") || output.include?("IDENTICAL code found")
puts output
exit 1
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 9311a9674ec..4befe02bc8b 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-01-30 14:59+0100\n"
-"PO-Revision-Date: 2018-01-30 14:59+0100\n"
+"POT-Creation-Date: 2018-02-06 10:02+0100\n"
+"PO-Revision-Date: 2018-02-06 10:02+0100\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
@@ -23,6 +23,11 @@ msgid_plural "%d commits"
msgstr[0] ""
msgstr[1] ""
+msgid "%d commit behind"
+msgid_plural "%d commits behind"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d issue"
msgid_plural "%d issues"
msgstr[0] ""
@@ -148,18 +153,45 @@ msgstr ""
msgid "All"
msgstr ""
+msgid "Allows you to add and manage Kubernetes clusters."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
msgid "An error occurred when toggling the notification subscription"
msgstr ""
+msgid "An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again."
+msgstr ""
+
+msgid "An error occurred while fetching markdown preview"
+msgstr ""
+
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while getting projects"
+msgstr ""
+
+msgid "An error occurred while loading filenames"
+msgstr ""
+
+msgid "An error occurred while rendering KaTeX"
+msgstr ""
+
+msgid "An error occurred while rendering preview broadcast message"
+msgstr ""
+
+msgid "An error occurred while retrieving calendar activity"
+msgstr ""
+
msgid "An error occurred while retrieving diff"
msgstr ""
+msgid "An error occurred while validating username"
+msgstr ""
+
msgid "An error occurred. Please try again."
msgstr ""
@@ -363,7 +395,7 @@ msgstr ""
msgid "CI / CD"
msgstr ""
-msgid "CI configuration"
+msgid "CI/CD configuration"
msgstr ""
msgid "CICD|Jobs"
@@ -375,6 +407,9 @@ msgstr ""
msgid "Cancel edit"
msgstr ""
+msgid "Cannot modify managed Kubernetes cluster"
+msgstr ""
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr ""
@@ -480,6 +515,24 @@ msgstr ""
msgid "CiStatus|running"
msgstr ""
+msgid "CiVariables|Input variable key"
+msgstr ""
+
+msgid "CiVariables|Input variable value"
+msgstr ""
+
+msgid "CiVariables|Remove variable row"
+msgstr ""
+
+msgid "CiVariable|All environments"
+msgstr ""
+
+msgid "CiVariable|Protected"
+msgstr ""
+
+msgid "CiVariable|Toggle protected"
+msgstr ""
+
msgid "CircuitBreakerApiLink|circuitbreaker api"
msgstr ""
@@ -489,28 +542,25 @@ msgstr ""
msgid "Clone repository"
msgstr ""
-msgid "Cluster"
-msgstr ""
-
-msgid "ClusterIntegration|%{appList} was successfully installed on your cluster"
+msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
msgstr ""
msgid "ClusterIntegration|API URL"
msgstr ""
-msgid "ClusterIntegration|Add an existing cluster"
+msgid "ClusterIntegration|Add Kubernetes cluster"
msgstr ""
-msgid "ClusterIntegration|Add cluster"
+msgid "ClusterIntegration|Add an existing Kubernetes cluster"
msgstr ""
-msgid "ClusterIntegration|Advanced options on this cluster's integration"
+msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration"
msgstr ""
msgid "ClusterIntegration|Applications"
msgstr ""
-msgid "ClusterIntegration|Are you sure you want to remove this cluster's integration? This will not delete your actual cluster."
+msgid "ClusterIntegration|Are you sure you want to remove this Kubernetes cluster's integration? This will not delete your actual Kubernetes cluster."
msgstr ""
msgid "ClusterIntegration|CA Certificate"
@@ -519,121 +569,121 @@ msgstr ""
msgid "ClusterIntegration|Certificate Authority bundle (PEM format)"
msgstr ""
-msgid "ClusterIntegration|Choose how to set up cluster integration"
+msgid "ClusterIntegration|Choose how to set up Kubernetes cluster integration"
msgstr ""
-msgid "ClusterIntegration|Choose which of your project's environments will use this cluster."
+msgid "ClusterIntegration|Choose which of your project's environments will use this Kubernetes cluster."
msgstr ""
-msgid "ClusterIntegration|Cluster"
+msgid "ClusterIntegration|Control how your Kubernetes cluster integrates with GitLab"
msgstr ""
-msgid "ClusterIntegration|Cluster details"
+msgid "ClusterIntegration|Copy API URL"
msgstr ""
-msgid "ClusterIntegration|Cluster integration"
+msgid "ClusterIntegration|Copy CA Certificate"
msgstr ""
-msgid "ClusterIntegration|Cluster integration is disabled for this project."
+msgid "ClusterIntegration|Copy Kubernetes cluster name"
msgstr ""
-msgid "ClusterIntegration|Cluster integration is enabled for this project."
+msgid "ClusterIntegration|Copy Token"
msgstr ""
-msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
+msgid "ClusterIntegration|Create Kubernetes cluster"
msgstr ""
-msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..."
+msgid "ClusterIntegration|Create Kubernetes cluster on Google Kubernetes Engine"
msgstr ""
-msgid "ClusterIntegration|Cluster name"
+msgid "ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab"
msgstr ""
-msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine. Refresh the page to see cluster's details"
+msgid "ClusterIntegration|Create on GKE"
msgstr ""
-msgid "ClusterIntegration|Clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}"
+msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
msgstr ""
-msgid "ClusterIntegration|Clusters can be used to deploy applications and to provide Review Apps for this project"
+msgid "ClusterIntegration|Enter the details for your Kubernetes cluster"
msgstr ""
-msgid "ClusterIntegration|Control how your cluster integrates with GitLab"
+msgid "ClusterIntegration|Environment scope"
msgstr ""
-msgid "ClusterIntegration|Copy API URL"
+msgid "ClusterIntegration|GitLab Integration"
msgstr ""
-msgid "ClusterIntegration|Copy CA Certificate"
+msgid "ClusterIntegration|GitLab Runner"
msgstr ""
-msgid "ClusterIntegration|Copy Token"
+msgid "ClusterIntegration|Google Cloud Platform project ID"
msgstr ""
-msgid "ClusterIntegration|Copy cluster name"
+msgid "ClusterIntegration|Google Kubernetes Engine"
msgstr ""
-msgid "ClusterIntegration|Create a new cluster on Google Kubernetes Engine right from GitLab"
+msgid "ClusterIntegration|Google Kubernetes Engine project"
msgstr ""
-msgid "ClusterIntegration|Create cluster"
+msgid "ClusterIntegration|Helm Tiller"
msgstr ""
-msgid "ClusterIntegration|Create cluster on Google Kubernetes Engine"
+msgid "ClusterIntegration|Ingress"
msgstr ""
-msgid "ClusterIntegration|Create on GKE"
+msgid "ClusterIntegration|Install"
msgstr ""
-msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
+msgid "ClusterIntegration|Installed"
msgstr ""
-msgid "ClusterIntegration|Enter the details for your cluster"
+msgid "ClusterIntegration|Installing"
msgstr ""
-msgid "ClusterIntegration|Environment scope"
+msgid "ClusterIntegration|Integrate Kubernetes cluster automation"
msgstr ""
-msgid "ClusterIntegration|GitLab Integration"
+msgid "ClusterIntegration|Integration status"
msgstr ""
-msgid "ClusterIntegration|GitLab Runner"
+msgid "ClusterIntegration|Kubernetes cluster"
msgstr ""
-msgid "ClusterIntegration|Google Cloud Platform project ID"
+msgid "ClusterIntegration|Kubernetes cluster details"
msgstr ""
-msgid "ClusterIntegration|Google Kubernetes Engine"
+msgid "ClusterIntegration|Kubernetes cluster integration"
msgstr ""
-msgid "ClusterIntegration|Google Kubernetes Engine project"
+msgid "ClusterIntegration|Kubernetes cluster integration is disabled for this project."
msgstr ""
-msgid "ClusterIntegration|Helm Tiller"
+msgid "ClusterIntegration|Kubernetes cluster integration is enabled for this project."
msgstr ""
-msgid "ClusterIntegration|Ingress"
+msgid "ClusterIntegration|Kubernetes cluster integration is enabled for this project. Disabling this integration will not affect your Kubernetes cluster, it will only temporarily turn off GitLab's connection to it."
msgstr ""
-msgid "ClusterIntegration|Install"
+msgid "ClusterIntegration|Kubernetes cluster is being created on Google Kubernetes Engine..."
msgstr ""
-msgid "ClusterIntegration|Installed"
+msgid "ClusterIntegration|Kubernetes cluster name"
msgstr ""
-msgid "ClusterIntegration|Installing"
+msgid "ClusterIntegration|Kubernetes cluster was successfully created on Google Kubernetes Engine. Refresh the page to see Kubernetes cluster's details"
msgstr ""
-msgid "ClusterIntegration|Integrate cluster automation"
+msgid "ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}"
msgstr ""
-msgid "ClusterIntegration|Integration status"
+msgid "ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project"
msgstr ""
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr ""
-msgid "ClusterIntegration|Learn more about Clusters"
+msgid "ClusterIntegration|Learn more about Kubernetes"
msgstr ""
msgid "ClusterIntegration|Learn more about environments"
@@ -642,10 +692,10 @@ msgstr ""
msgid "ClusterIntegration|Machine type"
msgstr ""
-msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
+msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create Kubernetes clusters"
msgstr ""
-msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
+msgid "ClusterIntegration|Manage your Kubernetes cluster by visiting %{link_gke}"
msgstr ""
msgid "ClusterIntegration|Note:"
@@ -654,7 +704,7 @@ msgstr ""
msgid "ClusterIntegration|Number of nodes"
msgstr ""
-msgid "ClusterIntegration|Please enter access information for your cluster. If you need help, you can read our %{link_to_help_page} on clusters"
+msgid "ClusterIntegration|Please enter access information for your Kubernetes cluster. If you need help, you can read our %{link_to_help_page} on Kubernetes"
msgstr ""
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
@@ -672,16 +722,16 @@ msgstr ""
msgid "ClusterIntegration|Prometheus"
msgstr ""
-msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
+msgid "ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration."
msgstr ""
-msgid "ClusterIntegration|Remove cluster integration"
+msgid "ClusterIntegration|Remove Kubernetes cluster integration"
msgstr ""
msgid "ClusterIntegration|Remove integration"
msgstr ""
-msgid "ClusterIntegration|Remove this cluster's configuration from this project. This will not delete your actual cluster."
+msgid "ClusterIntegration|Remove this Kubernetes cluster's configuration from this project. This will not delete your actual Kubernetes cluster."
msgstr ""
msgid "ClusterIntegration|Request to begin installing failed"
@@ -690,7 +740,7 @@ msgstr ""
msgid "ClusterIntegration|Save changes"
msgstr ""
-msgid "ClusterIntegration|See and edit the details for your cluster"
+msgid "ClusterIntegration|See and edit the details for your Kubernetes cluster"
msgstr ""
msgid "ClusterIntegration|See machine types"
@@ -711,22 +761,25 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong on our end."
msgstr ""
-msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine"
+msgid "ClusterIntegration|Something went wrong while creating your Kubernetes cluster on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Something went wrong while installing %{title}"
msgstr ""
-msgid "ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below"
+msgid "ClusterIntegration|This account must have permissions to create a Kubernetes cluster in the %{link_to_container_project} specified below"
msgstr ""
-msgid "ClusterIntegration|Toggle Cluster"
+msgid "ClusterIntegration|Toggle Kubernetes Cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Toggle Kubernetes cluster"
msgstr ""
msgid "ClusterIntegration|Token"
msgstr ""
-msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
+msgid "ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}"
@@ -741,9 +794,6 @@ msgstr ""
msgid "ClusterIntegration|check the pricing here"
msgstr ""
-msgid "ClusterIntegration|cluster"
-msgstr ""
-
msgid "ClusterIntegration|documentation"
msgstr ""
@@ -791,6 +841,9 @@ msgstr ""
msgid "Commits|An error occurred while fetching merge requests data."
msgstr ""
+msgid "Commits|Commit: %{commitText}"
+msgstr ""
+
msgid "Commits|History"
msgstr ""
@@ -890,6 +943,9 @@ msgstr ""
msgid "Copy URL to clipboard"
msgstr ""
+msgid "Copy branch name to clipboard"
+msgstr ""
+
msgid "Copy commit SHA to clipboard"
msgstr ""
@@ -1099,12 +1155,33 @@ msgstr ""
msgid "Environments|You don't have any environments right now."
msgstr ""
+msgid "Error fetching contributors data."
+msgstr ""
+
+msgid "Error fetching labels."
+msgstr ""
+
+msgid "Error fetching network graph."
+msgstr ""
+
msgid "Error fetching refs"
msgstr ""
+msgid "Error fetching usage ping data."
+msgstr ""
+
msgid "Error occurred when toggling the notification subscription"
msgstr ""
+msgid "Error saving label update."
+msgstr ""
+
+msgid "Error updating status for all todos."
+msgstr ""
+
+msgid "Error updating todo status."
+msgstr ""
+
msgid "EventFilterBy|Filter by all"
msgstr ""
@@ -1150,6 +1227,9 @@ msgstr ""
msgid "February"
msgstr ""
+msgid "Fields on this page are now uneditable, you can configure"
+msgstr ""
+
msgid "File name"
msgstr ""
@@ -1218,6 +1298,9 @@ msgstr ""
msgid "Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service."
msgstr ""
+msgid "Got it!"
+msgstr ""
+
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
msgstr ""
@@ -1319,6 +1402,9 @@ msgstr ""
msgid "Install a Runner compatible with GitLab CI"
msgstr ""
+msgid "Instance does not support multiple Kubernetes clusters"
+msgstr ""
+
msgid "Interested parties can even contribute by pushing commits if they want to."
msgstr ""
@@ -1364,6 +1450,27 @@ msgstr ""
msgid "June"
msgstr ""
+msgid "Kubernetes"
+msgstr ""
+
+msgid "Kubernetes Cluster"
+msgstr ""
+
+msgid "Kubernetes cluster creation time exceeds timeout; %{timeout}"
+msgstr ""
+
+msgid "Kubernetes cluster integration was not removed."
+msgstr ""
+
+msgid "Kubernetes cluster integration was successfully removed."
+msgstr ""
+
+msgid "Kubernetes cluster was successfully updated."
+msgstr ""
+
+msgid "Kubernetes service integration has been deprecated. %{deprecated_message_content} your Kubernetes clusters using the new <a href=\"%{url}\"/>Kubernetes Clusters</a> page"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr ""
@@ -1489,14 +1596,17 @@ msgstr ""
msgid "More information is available|here"
msgstr ""
-msgid "New Cluster"
-msgstr ""
-
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] ""
msgstr[1] ""
+msgid "New Kubernetes Cluster"
+msgstr ""
+
+msgid "New Kubernetes cluster"
+msgstr ""
+
msgid "New Pipeline Schedule"
msgstr ""
@@ -1716,12 +1826,6 @@ msgstr ""
msgid "PipelineSchedules|Inactive"
msgstr ""
-msgid "PipelineSchedules|Input variable key"
-msgstr ""
-
-msgid "PipelineSchedules|Input variable value"
-msgstr ""
-
msgid "PipelineSchedules|Next Run"
msgstr ""
@@ -1731,9 +1835,6 @@ msgstr ""
msgid "PipelineSchedules|Provide a short description for this pipeline"
msgstr ""
-msgid "PipelineSchedules|Remove variable row"
-msgstr ""
-
msgid "PipelineSchedules|Take ownership"
msgstr ""
@@ -1782,7 +1883,7 @@ msgstr ""
msgid "Play"
msgstr ""
-msgid "Please <a href=%{link_to_billing} target=\"_blank\" rel=\"noopener noreferrer\">enable billing for one of your projects to be able to create a cluster</a>, then try again."
+msgid "Please <a href=%{link_to_billing} target=\"_blank\" rel=\"noopener noreferrer\">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again."
msgstr ""
msgid "Please solve the reCAPTCHA"
@@ -1956,6 +2057,9 @@ msgstr ""
msgid "PrometheusService|View environments"
msgstr ""
+msgid "Protip:"
+msgstr ""
+
msgid "Public - The group and any public projects can be viewed without any authentication."
msgstr ""
@@ -2087,7 +2191,7 @@ msgstr ""
msgid "Set a password on your account to pull or push via %{protocol}."
msgstr ""
-msgid "Set up CI"
+msgid "Set up CI/CD"
msgstr ""
msgid "Set up Koding"
@@ -2116,6 +2220,9 @@ msgstr[1] ""
msgid "Snippets"
msgstr ""
+msgid "Something went wrong on our end"
+msgstr ""
+
msgid "Something went wrong on our end."
msgstr ""
@@ -2424,6 +2531,24 @@ msgstr ""
msgid "There are problems accessing Git storage: "
msgstr ""
+msgid "There was an error loading users activity calendar."
+msgstr ""
+
+msgid "There was an error saving your notification settings."
+msgstr ""
+
+msgid "There was an error subscribing to this label."
+msgstr ""
+
+msgid "There was an error when reseting email token."
+msgstr ""
+
+msgid "There was an error when subscribing to this label."
+msgstr ""
+
+msgid "There was an error when unsubscribing from this label."
+msgstr ""
+
msgid "This directory"
msgstr ""
@@ -2635,6 +2760,9 @@ msgstr ""
msgid "Trigger this manual action"
msgstr ""
+msgid "Type %{value} to confirm:"
+msgstr ""
+
msgid "Unable to reset project cache."
msgstr ""
@@ -2719,6 +2847,12 @@ msgstr ""
msgid "WikiClone|Start Gollum and edit locally"
msgstr ""
+msgid "WikiEditPageTip|Tip: You can move this page by adding the path to the beginning of the title."
+msgstr ""
+
+msgid "WikiEdit|There is already a page with the same title in that path."
+msgstr ""
+
msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
msgstr ""
@@ -2869,6 +3003,9 @@ msgstr ""
msgid "You'll need to use different branch names to get a valid comparison."
msgstr ""
+msgid "Your Kubernetes cluster information on this page is still editable, but you are advised to disable and reconfigure"
+msgstr ""
+
msgid "Your comment will not be visible to the public."
msgstr ""
@@ -2897,6 +3034,9 @@ msgstr[1] ""
msgid "mrWidget|Cancel automatic merge"
msgstr ""
+msgid "mrWidget|Check out branch"
+msgstr ""
+
msgid "mrWidget|Checking ability to merge automatically"
msgstr ""
@@ -2906,9 +3046,27 @@ msgstr ""
msgid "mrWidget|Cherry-pick this merge request in a new merge request"
msgstr ""
+msgid "mrWidget|Closed"
+msgstr ""
+
msgid "mrWidget|Closed by"
msgstr ""
+msgid "mrWidget|Closes"
+msgstr ""
+
+msgid "mrWidget|Did not close"
+msgstr ""
+
+msgid "mrWidget|Email patches"
+msgstr ""
+
+msgid "mrWidget|If the %{branch} branch exists in your local repository, you can merge this merge request manually using the"
+msgstr ""
+
+msgid "mrWidget|Mentions"
+msgstr ""
+
msgid "mrWidget|Merge"
msgstr ""
@@ -2921,6 +3079,9 @@ msgstr ""
msgid "mrWidget|Merged by"
msgstr ""
+msgid "mrWidget|Plain diff"
+msgstr ""
+
msgid "mrWidget|Refresh"
msgstr ""
@@ -2936,6 +3097,9 @@ msgstr ""
msgid "mrWidget|Remove source branch"
msgstr ""
+msgid "mrWidget|Request to merge"
+msgstr ""
+
msgid "mrWidget|Resolve conflicts"
msgstr ""
@@ -2981,9 +3145,18 @@ msgstr ""
msgid "mrWidget|This project is archived, write access has been disabled"
msgstr ""
+msgid "mrWidget|You can merge this merge request manually using the"
+msgstr ""
+
msgid "mrWidget|You can remove source branch now"
msgstr ""
+msgid "mrWidget|command line"
+msgstr ""
+
+msgid "mrWidget|into"
+msgstr ""
+
msgid "mrWidget|to be merged automatically when the pipeline succeeds"
msgstr ""
@@ -3012,3 +3185,6 @@ msgstr ""
msgid "username"
msgstr ""
+
+msgid "uses Kubernetes clusters to deploy your code!"
+msgstr ""
diff --git a/package.json b/package.json
index c68cf648932..c508a6e9931 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"scripts": {
- "dev-server": "nodemon --watch config/webpack.config.js -- ./node_modules/.bin/webpack-dev-server --config config/webpack.config.js",
+ "dev-server": "nodemon -w 'config/webpack.config.js' -w 'app/assets/javascripts/dispatcher.js' -w 'app/assets/javascripts/pages/**/index.js' --exec 'webpack-dev-server --config config/webpack.config.js'",
"eslint": "eslint --max-warnings 0 --ext .js,.vue .",
"eslint-fix": "eslint --max-warnings 0 --ext .js,.vue --fix .",
"eslint-report": "eslint --max-warnings 0 --ext .js,.vue --format html --output-file ./eslint-report.html .",
@@ -47,6 +47,7 @@
"exports-loader": "^0.6.4",
"file-loader": "^0.11.1",
"fuzzaldrin-plus": "^0.5.0",
+ "glob": "^7.1.2",
"imports-loader": "^0.7.1",
"jed": "^1.1.1",
"jquery": "^2.2.4",
@@ -87,7 +88,7 @@
"worker-loader": "^1.1.0"
},
"devDependencies": {
- "@gitlab-org/gitlab-svgs": "^1.7.0",
+ "@gitlab-org/gitlab-svgs": "^1.8.0",
"axios-mock-adapter": "^1.10.0",
"babel-plugin-istanbul": "^4.1.5",
"eslint": "^3.18.0",
diff --git a/qa/qa/factory/resource/issue.rb b/qa/qa/factory/resource/issue.rb
index 06e7e8df56c..95f48e20b3e 100644
--- a/qa/qa/factory/resource/issue.rb
+++ b/qa/qa/factory/resource/issue.rb
@@ -1,5 +1,3 @@
-require 'securerandom'
-
module QA
module Factory
module Resource
diff --git a/qa/qa/page/component/dropzone.rb b/qa/qa/page/component/dropzone.rb
index 5e6fdff20eb..15bdc742fda 100644
--- a/qa/qa/page/component/dropzone.rb
+++ b/qa/qa/page/component/dropzone.rb
@@ -4,6 +4,8 @@ module QA
class Dropzone
attr_reader :page, :container
+ # page - A QA::Page::Base object
+ # container - CSS selector of the comment textarea's container
def initialize(page, container)
@page = page
@container = container
diff --git a/qa/qa/page/menu/side.rb b/qa/qa/page/menu/side.rb
index 5fdcea20029..7e028add2ef 100644
--- a/qa/qa/page/menu/side.rb
+++ b/qa/qa/page/menu/side.rb
@@ -7,7 +7,7 @@ module QA
element :settings_link, 'link_to edit_project_path'
element :repository_link, "title: 'Repository'"
element :pipelines_settings_link, "title: 'CI / CD'"
- element :issues_link, %r{link_to.*shortcuts-issues}
+ element :issues_link, /link_to.*shortcuts-issues/
element :issues_link_text, "Issues"
element :top_level_items, '.sidebar-top-level-items'
element :activity_link, "title: 'Activity'"
diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb
index 364a2c61665..5bc0598a524 100644
--- a/qa/qa/page/project/issue/show.rb
+++ b/qa/qa/page/project/issue/show.rb
@@ -27,7 +27,7 @@ module QA
fill_in(with: text, name: 'note[note]')
unless attachment.nil?
- QA::Page::Component::Dropzone.new(page, '.new-note')
+ QA::Page::Component::Dropzone.new(self, '.new-note')
.attach_file(attachment)
end
diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb
index 553d35f9579..9d2a84ea644 100644
--- a/qa/qa/page/project/show.rb
+++ b/qa/qa/page/project/show.rb
@@ -3,7 +3,6 @@ module QA
module Project
class Show < Page::Base
view 'app/views/shared/_clone_panel.html.haml' do
- element :clone_holder, '.git-clone-holder'
element :clone_dropdown
element :clone_options_dropdown, '.clone-options-dropdown'
element :project_repository_location, 'text_field_tag :project_clone'
@@ -31,7 +30,7 @@ module QA
end
# Ensure git clone textbox was updated to http URI
- page.has_css?('.git-clone-holder input#project_clone[value*="http"]')
+ repository_location.include?('http')
end
end
diff --git a/qa/qa/runtime/browser.rb b/qa/qa/runtime/browser.rb
index ce888b51ea5..a12d95683af 100644
--- a/qa/qa/runtime/browser.rb
+++ b/qa/qa/runtime/browser.rb
@@ -84,7 +84,7 @@ module QA
config.javascript_driver = :chrome
config.default_max_wait_time = 10
# https://github.com/mattheworiordan/capybara-screenshot/issues/164
- config.save_path = 'tmp'
+ config.save_path = File.expand_path('../../tmp', __dir__)
end
end
diff --git a/qa/qa/specs/features/project/create_issue_spec.rb b/qa/qa/specs/features/project/create_issue_spec.rb
new file mode 100644
index 00000000000..b73f108c2d9
--- /dev/null
+++ b/qa/qa/specs/features/project/create_issue_spec.rb
@@ -0,0 +1,18 @@
+module QA
+ feature 'creates issue', :core do
+ let(:issue_title) { 'issue title' }
+
+ scenario 'user creates issue' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.act { sign_in_using_credentials }
+
+ Factory::Resource::Issue.fabricate! do |issue|
+ issue.title = issue_title
+ end
+
+ Page::Menu::Side.act { click_issues }
+
+ expect(page).to have_content(issue_title)
+ end
+ end
+end
diff --git a/qa/spec/runtime/rsa_key.rb b/qa/spec/runtime/rsa_key.rb
index ff277b9077b..6d7ab4dcd2e 100644
--- a/qa/spec/runtime/rsa_key.rb
+++ b/qa/spec/runtime/rsa_key.rb
@@ -3,7 +3,7 @@ describe QA::Runtime::RSAKey do
subject { described_class.new.public_key }
it 'generates a public RSA key' do
- expect(subject).to match(/\Assh\-rsa AAAA[0-9A-Za-z+\/]+={0,3}\z/)
+ expect(subject).to match(%r{\Assh\-rsa AAAA[0-9A-Za-z+/]+={0,3}\z})
end
end
end
diff --git a/rubocop/cop/migration/update_column_in_batches.rb b/rubocop/cop/migration/update_column_in_batches.rb
index db2b5564297..5461abc5ee0 100644
--- a/rubocop/cop/migration/update_column_in_batches.rb
+++ b/rubocop/cop/migration/update_column_in_batches.rb
@@ -29,7 +29,7 @@ module RuboCop
path = Pathname.new(source_name).relative_path_from(rails_root)
dirname = File.dirname(path)
.sub(%r{\Adb/(migrate|post_migrate)}, 'spec/migrations')
- filename = File.basename(source_name, '.rb').sub(%r{\A\d+_}, '')
+ filename = File.basename(source_name, '.rb').sub(/\A\d+_/, '')
File.join(dirname, "#{filename}_spec.rb")
end
diff --git a/scripts/lint-rugged b/scripts/lint-rugged
index 03f780f880b..cabd083e9f9 100755
--- a/scripts/lint-rugged
+++ b/scripts/lint-rugged
@@ -1,7 +1,7 @@
#!/usr/bin/env ruby
ALLOWED = [
- # Can be deleted (?) once rugged is no longer used in production. Doesn't make Rugged calls.
+ # Can be fixed once Rugged is no longer used in production. Doesn't make Rugged calls.
'config/initializers/8_metrics.rb',
# Can be deleted once wiki's are fully (mandatory) migrated
@@ -13,9 +13,6 @@ ALLOWED = [
# Needs to be migrated, https://gitlab.com/gitlab-org/gitaly/issues/954
'lib/tasks/gitlab/cleanup.rake',
- # https://gitlab.com/gitlab-org/gitaly/issues/961
- 'app/models/repository.rb',
-
# The only place where Rugged code is still allowed in production
'lib/gitlab/git/'
].freeze
diff --git a/spec/controllers/groups/uploads_controller_spec.rb b/spec/controllers/groups/uploads_controller_spec.rb
index 67a11e56e94..6a1869d1a48 100644
--- a/spec/controllers/groups/uploads_controller_spec.rb
+++ b/spec/controllers/groups/uploads_controller_spec.rb
@@ -6,5 +6,7 @@ describe Groups::UploadsController do
{ group_id: model }
end
- it_behaves_like 'handle uploads'
+ it_behaves_like 'handle uploads' do
+ let(:uploader_class) { NamespaceFileUploader }
+ end
end
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index 492fed42d31..8688fb33f0d 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -496,4 +496,87 @@ describe GroupsController do
"Group '#{redirect_route.path}' was moved to '#{group.full_path}'. Please update any links and bookmarks that may still have the old path."
end
end
+
+ describe 'PUT transfer', :postgresql do
+ before do
+ sign_in(user)
+ end
+
+ context 'when transfering to a subgroup goes right' do
+ let(:new_parent_group) { create(:group, :public) }
+ let!(:group_member) { create(:group_member, :owner, group: group, user: user) }
+ let!(:new_parent_group_member) { create(:group_member, :owner, group: new_parent_group, user: user) }
+
+ before do
+ put :transfer,
+ id: group.to_param,
+ new_parent_group_id: new_parent_group.id
+ end
+
+ it 'should return a notice' do
+ expect(flash[:notice]).to eq("Group '#{group.name}' was successfully transferred.")
+ end
+
+ it 'should redirect to the new path' do
+ expect(response).to redirect_to("/#{new_parent_group.path}/#{group.path}")
+ end
+ end
+
+ context 'when converting to a root group goes right' do
+ let(:group) { create(:group, :public, :nested) }
+ let!(:group_member) { create(:group_member, :owner, group: group, user: user) }
+
+ before do
+ put :transfer,
+ id: group.to_param,
+ new_parent_group_id: ''
+ end
+
+ it 'should return a notice' do
+ expect(flash[:notice]).to eq("Group '#{group.name}' was successfully transferred.")
+ end
+
+ it 'should redirect to the new path' do
+ expect(response).to redirect_to("/#{group.path}")
+ end
+ end
+
+ context 'When the transfer goes wrong' do
+ let(:new_parent_group) { create(:group, :public) }
+ let!(:group_member) { create(:group_member, :owner, group: group, user: user) }
+ let!(:new_parent_group_member) { create(:group_member, :owner, group: new_parent_group, user: user) }
+
+ before do
+ allow_any_instance_of(::Groups::TransferService).to receive(:proceed_to_transfer).and_raise(Gitlab::UpdatePathError, 'namespace directory cannot be moved')
+
+ put :transfer,
+ id: group.to_param,
+ new_parent_group_id: new_parent_group.id
+ end
+
+ it 'should return an alert' do
+ expect(flash[:alert]).to eq "Transfer failed: namespace directory cannot be moved"
+ end
+
+ it 'should redirect to the current path' do
+ expect(response).to render_template(:edit)
+ end
+ end
+
+ context 'when the user is not allowed to transfer the group' do
+ let(:new_parent_group) { create(:group, :public) }
+ let!(:group_member) { create(:group_member, :guest, group: group, user: user) }
+ let!(:new_parent_group_member) { create(:group_member, :guest, group: new_parent_group, user: user) }
+
+ before do
+ put :transfer,
+ id: group.to_param,
+ new_parent_group_id: new_parent_group.id
+ end
+
+ it 'should be denied' do
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
end
diff --git a/spec/controllers/health_check_controller_spec.rb b/spec/controllers/health_check_controller_spec.rb
index 2cead1770c9..387ca46ef6f 100644
--- a/spec/controllers/health_check_controller_spec.rb
+++ b/spec/controllers/health_check_controller_spec.rb
@@ -5,7 +5,7 @@ describe HealthCheckController do
let(:json_response) { JSON.parse(response.body) }
let(:xml_response) { Hash.from_xml(response.body)['hash'] }
- let(:token) { current_application_settings.health_check_access_token }
+ let(:token) { Gitlab::CurrentSettings.health_check_access_token }
let(:whitelisted_ip) { '127.0.0.1' }
let(:not_whitelisted_ip) { '127.0.0.2' }
diff --git a/spec/controllers/health_controller_spec.rb b/spec/controllers/health_controller_spec.rb
index 95946def5f9..542eddc2d16 100644
--- a/spec/controllers/health_controller_spec.rb
+++ b/spec/controllers/health_controller_spec.rb
@@ -4,7 +4,7 @@ describe HealthController do
include StubENV
let(:json_response) { JSON.parse(response.body) }
- let(:token) { current_application_settings.health_check_access_token }
+ let(:token) { Gitlab::CurrentSettings.health_check_access_token }
let(:whitelisted_ip) { '127.0.0.1' }
let(:not_whitelisted_ip) { '127.0.0.2' }
diff --git a/spec/controllers/oauth/applications_controller_spec.rb b/spec/controllers/oauth/applications_controller_spec.rb
index b38652e7ab9..1195f44f37d 100644
--- a/spec/controllers/oauth/applications_controller_spec.rb
+++ b/spec/controllers/oauth/applications_controller_spec.rb
@@ -16,8 +16,7 @@ describe Oauth::ApplicationsController do
end
it 'redirects back to profile page if OAuth applications are disabled' do
- settings = double(user_oauth_applications?: false)
- allow_any_instance_of(Gitlab::CurrentSettings).to receive(:current_application_settings).and_return(settings)
+ allow(Gitlab::CurrentSettings.current_application_settings).to receive(:user_oauth_applications?).and_return(false)
get :index
diff --git a/spec/controllers/projects/artifacts_controller_spec.rb b/spec/controllers/projects/artifacts_controller_spec.rb
index 12cb7b2647f..25a2e13fe1a 100644
--- a/spec/controllers/projects/artifacts_controller_spec.rb
+++ b/spec/controllers/projects/artifacts_controller_spec.rb
@@ -145,8 +145,7 @@ describe Projects::ArtifactsController do
context 'when using local file storage' do
it_behaves_like 'a valid file' do
let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
- let(:store) { ObjectStoreUploader::LOCAL_STORE }
- let(:archive_path) { JobArtifactUploader.local_store_path }
+ let(:archive_path) { JobArtifactUploader.root }
end
end
end
diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb
index a3b13647c92..954fc79f57d 100644
--- a/spec/controllers/projects/clusters_controller_spec.rb
+++ b/spec/controllers/projects/clusters_controller_spec.rb
@@ -177,7 +177,7 @@ describe Projects::ClustersController do
cluster.reload
expect(response).to redirect_to(project_cluster_path(project, cluster))
- expect(flash[:notice]).to eq('Cluster was successfully updated.')
+ expect(flash[:notice]).to eq('Kubernetes cluster was successfully updated.')
expect(cluster.enabled).to be_falsey
end
@@ -276,7 +276,7 @@ describe Projects::ClustersController do
cluster.reload
expect(response).to redirect_to(project_cluster_path(project, cluster))
- expect(flash[:notice]).to eq('Cluster was successfully updated.')
+ expect(flash[:notice]).to eq('Kubernetes cluster was successfully updated.')
expect(cluster.enabled).to be_falsey
expect(cluster.name).to eq('my-new-cluster-name')
expect(cluster.platform_kubernetes.namespace).to eq('my-namespace')
@@ -336,7 +336,7 @@ describe Projects::ClustersController do
.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.')
+ expect(flash[:notice]).to eq('Kubernetes cluster integration was successfully removed.')
end
end
@@ -349,7 +349,7 @@ describe Projects::ClustersController do
.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.')
+ expect(flash[:notice]).to eq('Kubernetes cluster integration was successfully removed.')
end
end
end
@@ -364,7 +364,7 @@ describe Projects::ClustersController do
.and change { Clusters::Providers::Gcp.count }.by(0)
expect(response).to redirect_to(project_clusters_path(project))
- expect(flash[:notice]).to eq('Cluster integration was successfully removed.')
+ expect(flash[:notice]).to eq('Kubernetes cluster integration was successfully removed.')
end
end
end
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index e6a4e7c8257..f3e303bb0fe 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -137,8 +137,8 @@ describe Projects::JobsController do
it 'exposes needed information' do
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['raw_path']).to match(/jobs\/\d+\/raw\z/)
- expect(json_response.dig('merge_request', 'path')).to match(/merge_requests\/\d+\z/)
+ expect(json_response['raw_path']).to match(%r{jobs/\d+/raw\z})
+ expect(json_response.dig('merge_request', 'path')).to match(%r{merge_requests/\d+\z})
expect(json_response['new_issue_path'])
.to include('/issues/new')
end
@@ -159,8 +159,19 @@ describe Projects::JobsController do
get_trace
end
+ context 'when job has a trace artifact' do
+ let(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) }
+
+ it 'returns a trace' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['id']).to eq job.id
+ expect(json_response['status']).to eq job.status
+ expect(json_response['html']).to eq(job.trace.html)
+ end
+ end
+
context 'when job has a trace' do
- let(:job) { create(:ci_build, :trace, pipeline: pipeline) }
+ let(:job) { create(:ci_build, :trace_live, pipeline: pipeline) }
it 'returns a trace' do
expect(response).to have_gitlab_http_status(:ok)
@@ -182,7 +193,7 @@ describe Projects::JobsController do
end
context 'when job has a trace with ANSI sequence and Unicode' do
- let(:job) { create(:ci_build, :unicode_trace, pipeline: pipeline) }
+ let(:job) { create(:ci_build, :unicode_trace_live, pipeline: pipeline) }
it 'returns a trace with Unicode' do
expect(response).to have_gitlab_http_status(:ok)
@@ -381,7 +392,7 @@ describe Projects::JobsController do
end
context 'when job is erasable' do
- let(:job) { create(:ci_build, :erasable, :trace, pipeline: pipeline) }
+ let(:job) { create(:ci_build, :erasable, :trace_artifact, pipeline: pipeline) }
it 'redirects to the erased job page' do
expect(response).to have_gitlab_http_status(:found)
@@ -408,7 +419,7 @@ describe Projects::JobsController do
context 'when user is developer' do
let(:role) { :developer }
- let(:job) { create(:ci_build, :erasable, :trace, pipeline: pipeline, user: triggered_by) }
+ let(:job) { create(:ci_build, :erasable, :trace_artifact, pipeline: pipeline, user: triggered_by) }
context 'when triggered by same user' do
let(:triggered_by) { user }
@@ -439,8 +450,18 @@ describe Projects::JobsController do
get_raw
end
+ context 'when job has a trace artifact' do
+ let(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) }
+
+ it 'returns a trace' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.content_type).to eq 'text/plain; charset=utf-8'
+ expect(response.body).to eq job.job_artifacts_trace.open.read
+ end
+ end
+
context 'when job has a trace file' do
- let(:job) { create(:ci_build, :trace, pipeline: pipeline) }
+ let(:job) { create(:ci_build, :trace_live, pipeline: pipeline) }
it 'send a trace file' do
expect(response).to have_gitlab_http_status(:ok)
diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb
index 3a0c3faa7b4..b7df42168e0 100644
--- a/spec/controllers/projects/raw_controller_spec.rb
+++ b/spec/controllers/projects/raw_controller_spec.rb
@@ -53,7 +53,7 @@ describe Projects::RawController do
end
it 'serves the file' do
- expect(controller).to receive(:send_file).with("#{Gitlab.config.shared.path}/lfs-objects/91/ef/f75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897", filename: 'lfs_object.iso', disposition: 'attachment')
+ expect(controller).to receive(:send_file).with("#{LfsObjectUploader.root}/91/ef/f75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897", filename: 'lfs_object.iso', disposition: 'attachment')
get(:show,
namespace_id: public_project.namespace.to_param,
project_id: public_project,
diff --git a/spec/controllers/projects/todos_controller_spec.rb b/spec/controllers/projects/todos_controller_spec.rb
index e2524be7724..1ce7e84bef9 100644
--- a/spec/controllers/projects/todos_controller_spec.rb
+++ b/spec/controllers/projects/todos_controller_spec.rb
@@ -36,7 +36,7 @@ describe Projects::TodosController do
expect(response).to have_gitlab_http_status(200)
expect(json_response['count']).to eq 1
- expect(json_response['delete_path']).to match(/\/dashboard\/todos\/\d{1}/)
+ expect(json_response['delete_path']).to match(%r{/dashboard/todos/\d{1}})
end
end
@@ -104,7 +104,7 @@ describe Projects::TodosController do
expect(response).to have_gitlab_http_status(200)
expect(json_response['count']).to eq 1
- expect(json_response['delete_path']).to match(/\/dashboard\/todos\/\d{1}/)
+ expect(json_response['delete_path']).to match(%r{/dashboard/todos/\d{1}})
end
end
diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb
index b1f601a19e5..376b229ffc9 100644
--- a/spec/controllers/uploads_controller_spec.rb
+++ b/spec/controllers/uploads_controller_spec.rb
@@ -180,6 +180,7 @@ describe UploadsController do
it_behaves_like 'content not cached without revalidation' do
subject do
get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'image.png'
+
response
end
end
@@ -196,6 +197,7 @@ describe UploadsController do
it_behaves_like 'content not cached without revalidation' do
subject do
get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'image.png'
+
response
end
end
@@ -220,6 +222,7 @@ describe UploadsController do
it_behaves_like 'content not cached without revalidation' do
subject do
get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'image.png'
+
response
end
end
@@ -239,6 +242,7 @@ describe UploadsController do
it_behaves_like 'content not cached without revalidation' do
subject do
get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'image.png'
+
response
end
end
@@ -291,6 +295,7 @@ describe UploadsController do
it_behaves_like 'content not cached without revalidation' do
subject do
get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'image.png'
+
response
end
end
@@ -322,6 +327,7 @@ describe UploadsController do
it_behaves_like 'content not cached without revalidation' do
subject do
get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'image.png'
+
response
end
end
@@ -341,6 +347,7 @@ describe UploadsController do
it_behaves_like 'content not cached without revalidation' do
subject do
get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'image.png'
+
response
end
end
@@ -384,6 +391,7 @@ describe UploadsController do
it_behaves_like 'content not cached without revalidation' do
subject do
get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'image.png'
+
response
end
end
@@ -420,6 +428,7 @@ describe UploadsController do
it_behaves_like 'content not cached without revalidation' do
subject do
get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png'
+
response
end
end
@@ -439,6 +448,7 @@ describe UploadsController do
it_behaves_like 'content not cached without revalidation' do
subject do
get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png'
+
response
end
end
@@ -491,6 +501,7 @@ describe UploadsController do
it_behaves_like 'content not cached without revalidation' do
subject do
get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png'
+
response
end
end
@@ -522,6 +533,7 @@ describe UploadsController do
it_behaves_like 'content not cached without revalidation' do
subject do
get :show, model: 'appearance', mounted_as: 'header_logo', id: appearance.id, filename: 'dk.png'
+
response
end
end
@@ -541,6 +553,7 @@ describe UploadsController do
it_behaves_like 'content not cached without revalidation' do
subject do
get :show, model: 'appearance', mounted_as: 'logo', id: appearance.id, filename: 'dk.png'
+
response
end
end
diff --git a/spec/controllers/user_callouts_controller_spec.rb b/spec/controllers/user_callouts_controller_spec.rb
new file mode 100644
index 00000000000..48e2ff75cac
--- /dev/null
+++ b/spec/controllers/user_callouts_controller_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe UserCalloutsController do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ describe "POST #create" do
+ subject { post :create, feature_name: feature_name, format: :json }
+
+ context 'with valid feature name' do
+ let(:feature_name) { UserCallout.feature_names.keys.first }
+
+ context 'when callout entry does not exist' do
+ it 'should create a callout entry with dismissed state' do
+ expect { subject }.to change { UserCallout.count }.by(1)
+ end
+
+ it 'should return success' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when callout entry already exists' do
+ let!(:callout) { create(:user_callout, feature_name: UserCallout.feature_names.keys.first, user: user) }
+
+ it 'should return success' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+
+ context 'with invalid feature name' do
+ let(:feature_name) { 'bogus_feature_name' }
+
+ it 'should return bad request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+ end
+end
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index 6f66468570f..6ba599cdf83 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -135,13 +135,19 @@ FactoryBot.define do
coverage_regex '/(d+)/'
end
- trait :trace do
+ trait :trace_live do
after(:create) do |build, evaluator|
build.trace.set('BUILD TRACE')
end
end
- trait :unicode_trace do
+ trait :trace_artifact do
+ after(:create) do |build, evaluator|
+ create(:ci_job_artifact, :trace, job: build)
+ end
+ end
+
+ trait :unicode_trace_live do
after(:create) do |build, evaluator|
trace = File.binread(
File.expand_path(
diff --git a/spec/factories/ci/job_artifacts.rb b/spec/factories/ci/job_artifacts.rb
index 46afba2953c..7ee379ca2ec 100644
--- a/spec/factories/ci/job_artifacts.rb
+++ b/spec/factories/ci/job_artifacts.rb
@@ -26,5 +26,14 @@ FactoryBot.define do
Rails.root.join('spec/fixtures/ci_build_artifacts_metadata.gz'), 'application/x-gzip')
end
end
+
+ trait :trace do
+ file_type :trace
+
+ after(:build) do |artifact, evaluator|
+ artifact.file = fixture_file_upload(
+ Rails.root.join('spec/fixtures/trace/sample_trace'), 'text/plain')
+ end
+ end
end
end
diff --git a/spec/factories/commits.rb b/spec/factories/commits.rb
index 84a8bc56640..d5d819d862a 100644
--- a/spec/factories/commits.rb
+++ b/spec/factories/commits.rb
@@ -23,7 +23,7 @@ FactoryBot.define do
end
after(:build) do |commit, evaluator|
- allow(commit).to receive(:author).and_return(evaluator.author || build(:author))
+ allow(commit).to receive(:author).and_return(evaluator.author || build_stubbed(:author))
end
trait :without_author do
diff --git a/spec/factories/deployments.rb b/spec/factories/deployments.rb
index 9d7d5e56611..cac56695319 100644
--- a/spec/factories/deployments.rb
+++ b/spec/factories/deployments.rb
@@ -3,13 +3,14 @@ FactoryBot.define do
sha '97de212e80737a608d939f648d959671fb0a0142'
ref 'master'
tag false
- user
+ user nil
project nil
deployable factory: :ci_build
environment factory: :environment
after(:build) do |deployment, evaluator|
deployment.project ||= deployment.environment.project
+ deployment.user ||= deployment.project.creator
unless deployment.project.repository_exists?
allow(deployment.project.repository).to receive(:create_ref)
diff --git a/spec/factories/events.rb b/spec/factories/events.rb
index ed275243ac9..5798b81ecad 100644
--- a/spec/factories/events.rb
+++ b/spec/factories/events.rb
@@ -1,7 +1,7 @@
FactoryBot.define do
factory :event do
project
- author factory: :user
+ author(factory: :user) { project.creator }
action Event::JOINED
trait(:created) { action Event::CREATED }
diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb
index 1512f5a0e58..8c531cf5909 100644
--- a/spec/factories/groups.rb
+++ b/spec/factories/groups.rb
@@ -18,7 +18,7 @@ FactoryBot.define do
end
trait :with_avatar do
- avatar { File.open(Rails.root.join('spec/fixtures/dk.png')) }
+ avatar { fixture_file_upload('spec/fixtures/dk.png') }
end
trait :access_requestable do
diff --git a/spec/factories/issues.rb b/spec/factories/issues.rb
index 71dc169c6a2..998080a3dd5 100644
--- a/spec/factories/issues.rb
+++ b/spec/factories/issues.rb
@@ -1,8 +1,8 @@
FactoryBot.define do
factory :issue do
title { generate(:title) }
- author
project
+ author { project.creator }
trait :confidential do
confidential true
diff --git a/spec/factories/keys.rb b/spec/factories/keys.rb
index f0c43f3d6f5..23a98a899f1 100644
--- a/spec/factories/keys.rb
+++ b/spec/factories/keys.rb
@@ -5,6 +5,10 @@ FactoryBot.define do
title
key { Spec::Support::Helpers::KeyGeneratorHelper.new(1024).generate + ' dummy@gitlab.com' }
+ factory :key_without_comment do
+ key { Spec::Support::Helpers::KeyGeneratorHelper.new(1024).generate }
+ end
+
factory :deploy_key, class: 'DeployKey'
factory :personal_key do
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index 40558c88d15..d26cb0c3417 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -1,9 +1,9 @@
FactoryBot.define do
factory :merge_request do
title { generate(:title) }
- author
association :source_project, :repository, factory: :project
target_project { source_project }
+ author { source_project.creator }
# $ git log --pretty=oneline feature..master
# 5937ac0a7beb003549fc5fd26fc247adbce4a52e Add submodule from gitlab.com
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index 707ecbd6be5..3f4e408b3a6 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -6,7 +6,7 @@ FactoryBot.define do
factory :note do
project
note { generate(:title) }
- author
+ author { project&.creator || create(:user) }
on_issue
factory :note_on_commit, traits: [:on_commit]
@@ -122,11 +122,11 @@ FactoryBot.define do
end
trait :with_attachment do
- attachment { fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png") }
+ attachment { fixture_file_upload(Rails.root.join( "spec/fixtures/dk.png"), "image/png") }
end
trait :with_svg_attachment do
- attachment { fixture_file_upload(Rails.root + "spec/fixtures/unsanitized.svg", "image/svg+xml") }
+ attachment { fixture_file_upload(Rails.root.join("spec/fixtures/unsanitized.svg"), "image/svg+xml") }
end
transient do
diff --git a/spec/factories/project_wikis.rb b/spec/factories/project_wikis.rb
index 89d8248f9f4..db2eb4fc863 100644
--- a/spec/factories/project_wikis.rb
+++ b/spec/factories/project_wikis.rb
@@ -3,7 +3,7 @@ FactoryBot.define do
skip_create
project
- user factory: :user
+ user { project.creator }
initialize_with { new(project, user) }
end
end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 1dec4eb6c04..9eef923c30d 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -90,7 +90,13 @@ FactoryBot.define do
end
trait :with_avatar do
- avatar { File.open(Rails.root.join('spec/fixtures/dk.png')) }
+ avatar { fixture_file_upload('spec/fixtures/dk.png') }
+ end
+
+ trait :with_export do
+ after(:create) do |project, evaluator|
+ ProjectExportWorker.new.perform(project.creator.id, project.id)
+ end
end
trait :broken_storage do
diff --git a/spec/factories/sent_notifications.rb b/spec/factories/sent_notifications.rb
index 80872067233..b0174dd06b7 100644
--- a/spec/factories/sent_notifications.rb
+++ b/spec/factories/sent_notifications.rb
@@ -1,7 +1,7 @@
FactoryBot.define do
factory :sent_notification do
project
- recipient factory: :user
+ recipient { project.creator }
noteable { create(:issue, project: project) }
reply_key { SentNotification.reply_key }
end
diff --git a/spec/factories/snippets.rb b/spec/factories/snippets.rb
index 2ab9a56d255..dc12b562108 100644
--- a/spec/factories/snippets.rb
+++ b/spec/factories/snippets.rb
@@ -21,6 +21,7 @@ FactoryBot.define do
factory :project_snippet, parent: :snippet, class: :ProjectSnippet do
project
+ author { project.creator }
end
factory :personal_snippet, parent: :snippet, class: :PersonalSnippet do
diff --git a/spec/factories/subscriptions.rb b/spec/factories/subscriptions.rb
index a4bc4e87b0a..8f7ab74ec70 100644
--- a/spec/factories/subscriptions.rb
+++ b/spec/factories/subscriptions.rb
@@ -1,7 +1,7 @@
FactoryBot.define do
factory :subscription do
- user
project
+ user { project.creator }
subscribable factory: :issue
end
end
diff --git a/spec/factories/timelogs.rb b/spec/factories/timelogs.rb
index af34b0681e2..b45f06b9a0a 100644
--- a/spec/factories/timelogs.rb
+++ b/spec/factories/timelogs.rb
@@ -3,7 +3,7 @@
FactoryBot.define do
factory :timelog do
time_spent 3600
- user
issue
+ user { issue.project.creator }
end
end
diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb
index 6a6de665dd1..94f8caedfa6 100644
--- a/spec/factories/todos.rb
+++ b/spec/factories/todos.rb
@@ -1,8 +1,8 @@
FactoryBot.define do
factory :todo do
project
- author
- user
+ author { project.creator }
+ user { project.creator }
target factory: :issue
action { Todo::ASSIGNED }
diff --git a/spec/factories/uploads.rb b/spec/factories/uploads.rb
index c39500faea1..ff3a2a76acc 100644
--- a/spec/factories/uploads.rb
+++ b/spec/factories/uploads.rb
@@ -1,24 +1,46 @@
FactoryBot.define do
factory :upload do
model { build(:project) }
- path { "uploads/-/system/project/avatar/avatar.jpg" }
size 100.kilobytes
uploader "AvatarUploader"
+ mount_point :avatar
+ secret nil
- trait :personal_snippet do
- model { build(:personal_snippet) }
+ # we should build a mount agnostic upload by default
+ transient do
+ filename 'myfile.jpg'
+ end
+
+ # this needs to comply with RecordsUpload::Concern#upload_path
+ path { File.join("uploads/-/system", model.class.to_s.underscore, mount_point.to_s, 'avatar.jpg') }
+
+ trait :personal_snippet_upload do
uploader "PersonalFileUploader"
+ path { File.join(secret, filename) }
+ model { build(:personal_snippet) }
+ secret SecureRandom.hex
end
trait :issuable_upload do
- path { "#{SecureRandom.hex}/myfile.jpg" }
uploader "FileUploader"
+ path { File.join(secret, filename) }
+ secret SecureRandom.hex
end
trait :namespace_upload do
- path { "#{SecureRandom.hex}/myfile.jpg" }
model { build(:group) }
+ path { File.join(secret, filename) }
uploader "NamespaceFileUploader"
+ secret SecureRandom.hex
+ end
+
+ trait :attachment_upload do
+ transient do
+ mount_point :attachment
+ end
+
+ model { build(:note) }
+ uploader "AttachmentUploader"
end
end
end
diff --git a/spec/factories/user_callouts.rb b/spec/factories/user_callouts.rb
new file mode 100644
index 00000000000..528e442c14b
--- /dev/null
+++ b/spec/factories/user_callouts.rb
@@ -0,0 +1,7 @@
+FactoryBot.define do
+ factory :user_callout do
+ feature_name :gke_cluster_integration
+
+ user
+ end
+end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index e62e0b263ca..769fd656e7a 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -38,7 +38,7 @@ FactoryBot.define do
end
trait :with_avatar do
- avatar { File.open(Rails.root.join('spec/fixtures/dk.png')) }
+ avatar { fixture_file_upload('spec/fixtures/dk.png') }
end
trait :two_factor_via_otp do
diff --git a/spec/features/admin/admin_health_check_spec.rb b/spec/features/admin/admin_health_check_spec.rb
index ac3392b49f9..3693e5882f9 100644
--- a/spec/features/admin/admin_health_check_spec.rb
+++ b/spec/features/admin/admin_health_check_spec.rb
@@ -17,7 +17,7 @@ feature "Admin Health Check", :feature do
page.has_text? 'Health Check'
page.has_text? 'Health information can be retrieved'
- token = current_application_settings.health_check_access_token
+ token = Gitlab::CurrentSettings.health_check_access_token
expect(page).to have_content("Access token is #{token}")
expect(page).to have_selector('#health-check-token', text: token)
@@ -25,7 +25,7 @@ feature "Admin Health Check", :feature do
describe 'reload access token' do
it 'changes the access token' do
- orig_token = current_application_settings.health_check_access_token
+ orig_token = Gitlab::CurrentSettings.health_check_access_token
click_button 'Reset health check access token'
expect(page).to have_content('New health check access token has been generated!')
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index c1c54177167..a01c129defd 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -156,7 +156,7 @@ describe "Admin Runners" do
end
describe 'runners registration token' do
- let!(:token) { current_application_settings.runners_registration_token }
+ let!(:token) { Gitlab::CurrentSettings.runners_registration_token }
before do
visit admin_runners_path
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index 1218ea52227..cc0849d1cc6 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -38,12 +38,12 @@ feature 'Admin updates settings' do
uncheck 'Project export enabled'
click_button 'Save'
- expect(current_application_settings.gravatar_enabled).to be_falsey
- expect(current_application_settings.home_page_url).to eq "https://about.gitlab.com/"
- expect(current_application_settings.help_page_text).to eq "Example text"
- expect(current_application_settings.help_page_hide_commercial_content).to be_truthy
- expect(current_application_settings.help_page_support_url).to eq "http://example.com/help"
- expect(current_application_settings.project_export_enabled).to be_falsey
+ expect(Gitlab::CurrentSettings.gravatar_enabled).to be_falsey
+ expect(Gitlab::CurrentSettings.home_page_url).to eq "https://about.gitlab.com/"
+ expect(Gitlab::CurrentSettings.help_page_text).to eq "Example text"
+ expect(Gitlab::CurrentSettings.help_page_hide_commercial_content).to be_truthy
+ expect(Gitlab::CurrentSettings.help_page_support_url).to eq "http://example.com/help"
+ expect(Gitlab::CurrentSettings.project_export_enabled).to be_falsey
expect(page).to have_content "Application settings saved successfully"
end
diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb
index 782f42aab04..2d074c115dd 100644
--- a/spec/features/atom/users_spec.rb
+++ b/spec/features/atom/users_spec.rb
@@ -64,7 +64,7 @@ describe "User Feed" do
end
it 'has XHTML summaries in issue descriptions' do
- expect(body).to match /<hr ?\/>/
+ expect(body).to match %r{<hr ?/>}
end
it 'has XHTML summaries in notes' do
@@ -72,7 +72,7 @@ describe "User Feed" do
end
it 'has XHTML summaries in merge request descriptions' do
- expect(body).to match /Here is the fix: <a[^>]*><img[^>]*\/><\/a>/
+ expect(body).to match %r{Here is the fix: <a[^>]*><img[^>]*/></a>}
end
it 'has push event commit ID' do
diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb
index 991d360ccaf..744041ac425 100644
--- a/spec/features/dashboard/merge_requests_spec.rb
+++ b/spec/features/dashboard/merge_requests_spec.rb
@@ -44,36 +44,38 @@ feature 'Dashboard Merge Requests' do
context 'merge requests exist' do
let!(:assigned_merge_request) do
- create(:merge_request, assignee: current_user, target_project: project, source_project: project)
+ create(:merge_request,
+ assignee: current_user,
+ source_project: project,
+ author: create(:user))
end
let!(:assigned_merge_request_from_fork) do
create(:merge_request,
source_branch: 'markdown', assignee: current_user,
- target_project: public_project, source_project: forked_project
- )
+ target_project: public_project, source_project: forked_project,
+ author: create(:user))
end
let!(:authored_merge_request) do
create(:merge_request,
- source_branch: 'markdown', author: current_user,
- target_project: project, source_project: project
- )
+ source_branch: 'markdown',
+ source_project: project,
+ author: current_user)
end
let!(:authored_merge_request_from_fork) do
create(:merge_request,
source_branch: 'feature_conflict',
author: current_user,
- target_project: public_project, source_project: forked_project
- )
+ target_project: public_project, source_project: forked_project)
end
let!(:other_merge_request) do
create(:merge_request,
source_branch: 'fix',
- target_project: project, source_project: project
- )
+ source_project: project,
+ author: create(:user))
end
before do
diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb
index cdf7aceb13c..450bc0ff8cf 100644
--- a/spec/features/groups/issues_spec.rb
+++ b/spec/features/groups/issues_spec.rb
@@ -3,40 +3,61 @@ require 'spec_helper'
feature 'Group issues page' do
include FilteredSearchHelpers
- let(:path) { issues_group_path(group) }
- let(:issuable) { create(:issue, project: project, title: "this is my created issuable")}
+ context 'with shared examples' do
+ let(:path) { issues_group_path(group) }
+ let(:issuable) { create(:issue, project: project, title: "this is my created issuable")}
- include_examples 'project features apply to issuables', Issue
+ include_examples 'project features apply to issuables', Issue
- context 'rss feed' do
- let(:access_level) { ProjectFeature::ENABLED }
+ context 'rss feed' do
+ let(:access_level) { ProjectFeature::ENABLED }
- context 'when signed in' do
- let(:user) { user_in_group }
+ context 'when signed in' do
+ let(:user) { user_in_group }
- it_behaves_like "it has an RSS button with current_user's RSS token"
- it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
- end
+ it_behaves_like "it has an RSS button with current_user's RSS token"
+ it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
+ end
- context 'when signed out' do
- let(:user) { nil }
+ context 'when signed out' do
+ let(:user) { nil }
- it_behaves_like "it has an RSS button without an RSS token"
- it_behaves_like "an autodiscoverable RSS feed without an RSS token"
+ it_behaves_like "it has an RSS button without an RSS token"
+ it_behaves_like "an autodiscoverable RSS feed without an RSS token"
+ end
end
- end
- context 'assignee', :js do
- let(:access_level) { ProjectFeature::ENABLED }
- let(:user) { user_in_group }
- let(:user2) { user_outside_group }
- let(:path) { issues_group_path(group) }
+ context 'assignee', :js do
+ let(:access_level) { ProjectFeature::ENABLED }
+ let(:user) { user_in_group }
+ let(:user2) { user_outside_group }
+ let(:path) { issues_group_path(group) }
+
+ it 'filters by only group users' do
+ filtered_search.set('assignee:')
- it 'filters by only group users' do
- filtered_search.set('assignee:')
+ expect(find('#js-dropdown-assignee .filter-dropdown')).to have_content(user.name)
+ expect(find('#js-dropdown-assignee .filter-dropdown')).not_to have_content(user2.name)
+ end
+ end
+ end
- expect(find('#js-dropdown-assignee .filter-dropdown')).to have_content(user.name)
- expect(find('#js-dropdown-assignee .filter-dropdown')).not_to have_content(user2.name)
+ context 'issues list', :nested_groups do
+ let(:group) { create(:group)}
+ let(:subgroup) { create(:group, parent: group) }
+ let(:project) { create(:project, :public, group: group)}
+ let(:subgroup_project) { create(:project, :public, group: subgroup)}
+ let!(:issue) { create(:issue, project: project, title: 'root group issue') }
+ let!(:subgroup_issue) { create(:issue, project: subgroup_project, title: 'subgroup issue') }
+
+ it 'returns all group and subgroup issues' do
+ visit issues_group_path(group)
+
+ page.within('.issuable-list') do
+ expect(page).to have_selector('li.issue', count: 2)
+ expect(page).to have_content('root group issue')
+ expect(page).to have_content('subgroup issue')
+ end
end
end
end
diff --git a/spec/features/issues/spam_issues_spec.rb b/spec/features/issues/spam_issues_spec.rb
index c7cfd01f588..a75ca1d42b3 100644
--- a/spec/features/issues/spam_issues_spec.rb
+++ b/spec/features/issues/spam_issues_spec.rb
@@ -9,7 +9,7 @@ describe 'New issue', :js do
before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
- current_application_settings.update!(
+ Gitlab::CurrentSettings.update!(
akismet_enabled: true,
akismet_api_key: 'testkey',
recaptcha_enabled: true,
diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb
index a2b78a5e021..f13d78d24e3 100644
--- a/spec/features/markdown_spec.rb
+++ b/spec/features/markdown_spec.rb
@@ -259,6 +259,10 @@ describe 'GitLab Markdown' do
it 'includes VideoLinkFilter' do
expect(doc).to parse_video_links
end
+
+ it 'includes ColorFilter' do
+ expect(doc).to parse_colors
+ end
end
context 'wiki pipeline' do
@@ -320,6 +324,10 @@ describe 'GitLab Markdown' do
it 'includes VideoLinkFilter' do
expect(doc).to parse_video_links
end
+
+ it 'includes ColorFilter' do
+ expect(doc).to parse_colors
+ end
end
# Fake a `current_user` helper
diff --git a/spec/features/merge_request/user_awards_emoji_spec.rb b/spec/features/merge_request/user_awards_emoji_spec.rb
index 15a0878fb16..2f24cfbd9e3 100644
--- a/spec/features/merge_request/user_awards_emoji_spec.rb
+++ b/spec/features/merge_request/user_awards_emoji_spec.rb
@@ -3,7 +3,7 @@ require 'rails_helper'
describe 'Merge request > User awards emoji', :js do
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
- let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:merge_request) { create(:merge_request, source_project: project, author: create(:user)) }
describe 'logged in' do
before do
diff --git a/spec/features/merge_request/user_resolves_conflicts_spec.rb b/spec/features/merge_request/user_resolves_conflicts_spec.rb
index 61861d33952..19995559fae 100644
--- a/spec/features/merge_request/user_resolves_conflicts_spec.rb
+++ b/spec/features/merge_request/user_resolves_conflicts_spec.rb
@@ -100,12 +100,12 @@ describe 'Merge request > User resolves conflicts', :js do
end
it 'shows a link to the conflict resolution page' do
- expect(page).to have_link('conflicts', href: /\/conflicts\Z/)
+ expect(page).to have_link('conflicts', href: %r{/conflicts\Z})
end
context 'in Inline view mode' do
before do
- click_link('conflicts', href: /\/conflicts\Z/)
+ click_link('conflicts', href: %r{/conflicts\Z})
end
include_examples "conflicts are resolved in Interactive mode"
@@ -114,7 +114,7 @@ describe 'Merge request > User resolves conflicts', :js do
context 'in Parallel view mode' do
before do
- click_link('conflicts', href: /\/conflicts\Z/)
+ click_link('conflicts', href: %r{/conflicts\Z})
click_button 'Side-by-side'
end
@@ -128,7 +128,7 @@ describe 'Merge request > User resolves conflicts', :js do
before do
visit project_merge_request_path(project, merge_request)
- click_link('conflicts', href: /\/conflicts\Z/)
+ click_link('conflicts', href: %r{/conflicts\Z})
end
it 'conflicts can not be resolved in Interactive mode' do
@@ -181,7 +181,7 @@ describe 'Merge request > User resolves conflicts', :js do
end
it 'does not show a link to the conflict resolution page' do
- expect(page).not_to have_link('conflicts', href: /\/conflicts\Z/)
+ expect(page).not_to have_link('conflicts', href: %r{/conflicts\Z})
end
it 'shows an error if the conflicts page is visited directly' do
diff --git a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
index fb73ab05f87..dbca279569a 100644
--- a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
+++ b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
@@ -61,7 +61,7 @@ describe 'Merge request > User selects branches for new MR', :js do
fill_in "merge_request_title", with: "Orphaned MR test"
click_button "Submit merge request"
- click_link "Check out branch"
+ click_button "Check out branch"
expect(page).to have_content 'git checkout -b orphaned-branch origin/orphaned-branch'
end
diff --git a/spec/features/projects/badges/coverage_spec.rb b/spec/features/projects/badges/coverage_spec.rb
index 821ce88a402..f51001edcd7 100644
--- a/spec/features/projects/badges/coverage_spec.rb
+++ b/spec/features/projects/badges/coverage_spec.rb
@@ -18,7 +18,7 @@ feature 'test coverage badge' do
show_test_coverage_badge
- expect_coverage_badge('95%')
+ expect_coverage_badge('95.00%')
end
scenario 'user requests coverage badge for specific job' do
@@ -30,7 +30,7 @@ feature 'test coverage badge' do
show_test_coverage_badge(job: 'coverage')
- expect_coverage_badge('85%')
+ expect_coverage_badge('85.00%')
end
scenario 'user requests coverage badge for pipeline without coverage' do
diff --git a/spec/features/projects/clusters/applications_spec.rb b/spec/features/projects/clusters/applications_spec.rb
index 9c4abec115f..8d1e10b7191 100644
--- a/spec/features/projects/clusters/applications_spec.rb
+++ b/spec/features/projects/clusters/applications_spec.rb
@@ -64,7 +64,7 @@ feature 'Clusters Applications', :js do
expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installed')
end
- expect(page).to have_content('Helm Tiller was successfully installed on your cluster')
+ expect(page).to have_content('Helm Tiller was successfully installed on your Kubernetes cluster')
end
end
@@ -98,7 +98,7 @@ feature 'Clusters Applications', :js do
expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installed')
end
- expect(page).to have_content('Ingress was successfully installed on your cluster')
+ expect(page).to have_content('Ingress was successfully installed on your Kubernetes cluster')
end
end
end
diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb
index 94bde723e2f..02dbd3380b3 100644
--- a/spec/features/projects/clusters/gcp_spec.rb
+++ b/spec/features/projects/clusters/gcp_spec.rb
@@ -32,7 +32,7 @@ feature 'Gcp Cluster', :js do
before do
visit project_clusters_path(project)
- click_link 'Add cluster'
+ click_link 'Add Kubernetes cluster'
click_link 'Create on GKE'
end
@@ -50,19 +50,19 @@ feature 'Gcp Cluster', :js do
fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123'
fill_in 'cluster_name', with: 'dev-cluster'
- click_button 'Create cluster'
+ click_button 'Create Kubernetes cluster'
end
it 'user sees a cluster details page and creation status' do
- expect(page).to have_content('Cluster is being created on Google Kubernetes Engine...')
+ expect(page).to have_content('Kubernetes cluster is being created on Google Kubernetes Engine...')
Clusters::Cluster.last.provider.make_created!
- expect(page).to have_content('Cluster was successfully created on Google Kubernetes Engine')
+ expect(page).to have_content('Kubernetes cluster was successfully created on Google Kubernetes Engine')
end
it 'user sees a error if something worng during creation' do
- expect(page).to have_content('Cluster is being created on Google Kubernetes Engine...')
+ expect(page).to have_content('Kubernetes cluster is being created on Google Kubernetes Engine...')
Clusters::Cluster.last.provider.make_errored!('Something wrong!')
@@ -72,7 +72,7 @@ feature 'Gcp Cluster', :js do
context 'when user filled form with invalid parameters' do
before do
- click_button 'Create cluster'
+ click_button 'Create Kubernetes cluster'
end
it 'user sees a validation error' do
@@ -100,7 +100,7 @@ feature 'Gcp Cluster', :js do
end
it 'user sees the successful message' do
- expect(page).to have_content('Cluster was successfully updated.')
+ expect(page).to have_content('Kubernetes cluster was successfully updated.')
end
end
@@ -111,7 +111,7 @@ feature 'Gcp Cluster', :js do
end
it 'user sees the successful message' do
- expect(page).to have_content('Cluster was successfully updated.')
+ expect(page).to have_content('Kubernetes cluster was successfully updated.')
expect(cluster.reload.platform_kubernetes.namespace).to eq('my-namespace')
end
end
@@ -124,8 +124,8 @@ feature 'Gcp Cluster', :js do
end
it 'user sees creation form with the successful message' do
- expect(page).to have_content('Cluster integration was successfully removed.')
- expect(page).to have_link('Add cluster')
+ expect(page).to have_content('Kubernetes cluster integration was successfully removed.')
+ expect(page).to have_link('Add Kubernetes cluster')
end
end
end
@@ -138,16 +138,16 @@ feature 'Gcp Cluster', :js do
visit project_clusters_path(project)
- click_link 'Add cluster'
+ click_link 'Add Kubernetes cluster'
click_link 'Create on GKE'
fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123'
fill_in 'cluster_name', with: 'dev-cluster'
- click_button 'Create cluster'
+ click_button 'Create Kubernetes cluster'
end
it 'user sees form with error' do
- expect(page).to have_content('Please enable billing for one of your projects to be able to create a cluster, then try again.')
+ expect(page).to have_content('Please enable billing for one of your projects to be able to create a Kubernetes cluster, then try again.')
end
end
@@ -158,12 +158,12 @@ feature 'Gcp Cluster', :js do
visit project_clusters_path(project)
- click_link 'Add cluster'
+ click_link 'Add Kubernetes cluster'
click_link 'Create on GKE'
fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123'
fill_in 'cluster_name', with: 'dev-cluster'
- click_button 'Create cluster'
+ click_button 'Create Kubernetes cluster'
end
it 'user sees form with error' do
@@ -176,7 +176,7 @@ feature 'Gcp Cluster', :js do
before do
visit project_clusters_path(project)
- click_link 'Add cluster'
+ click_link 'Add Kubernetes cluster'
click_link 'Create on GKE'
end
diff --git a/spec/features/projects/clusters/user_spec.rb b/spec/features/projects/clusters/user_spec.rb
index b9ab434c259..698b64a659c 100644
--- a/spec/features/projects/clusters/user_spec.rb
+++ b/spec/features/projects/clusters/user_spec.rb
@@ -16,8 +16,8 @@ feature 'User Cluster', :js do
before do
visit project_clusters_path(project)
- click_link 'Add cluster'
- click_link 'Add an existing cluster'
+ click_link 'Add Kubernetes cluster'
+ click_link 'Add an existing Kubernetes cluster'
end
context 'when user filled form with valid parameters' do
@@ -25,11 +25,11 @@ feature 'User Cluster', :js do
fill_in 'cluster_name', with: 'dev-cluster'
fill_in 'cluster_platform_kubernetes_attributes_api_url', with: 'http://example.com'
fill_in 'cluster_platform_kubernetes_attributes_token', with: 'my-token'
- click_button 'Add cluster'
+ click_button 'Add Kubernetes cluster'
end
it 'user sees a cluster details page' do
- expect(page).to have_content('Cluster integration')
+ expect(page).to have_content('Kubernetes cluster integration')
expect(page.find_field('cluster[name]').value).to eq('dev-cluster')
expect(page.find_field('cluster[platform_kubernetes_attributes][api_url]').value)
.to have_content('http://example.com')
@@ -40,7 +40,7 @@ feature 'User Cluster', :js do
context 'when user filled form with invalid parameters' do
before do
- click_button 'Add cluster'
+ click_button 'Add Kubernetes cluster'
end
it 'user sees a validation error' do
@@ -68,7 +68,7 @@ feature 'User Cluster', :js do
end
it 'user sees the successful message' do
- expect(page).to have_content('Cluster was successfully updated.')
+ expect(page).to have_content('Kubernetes cluster was successfully updated.')
end
end
@@ -80,7 +80,7 @@ feature 'User Cluster', :js do
end
it 'user sees the successful message' do
- expect(page).to have_content('Cluster was successfully updated.')
+ expect(page).to have_content('Kubernetes cluster was successfully updated.')
expect(cluster.reload.name).to eq('my-dev-cluster')
expect(cluster.reload.platform_kubernetes.namespace).to eq('my-namespace')
end
@@ -94,8 +94,8 @@ feature 'User Cluster', :js do
end
it 'user sees creation form with the successful message' do
- expect(page).to have_content('Cluster integration was successfully removed.')
- expect(page).to have_link('Add cluster')
+ expect(page).to have_content('Kubernetes cluster integration was successfully removed.')
+ expect(page).to have_link('Add Kubernetes cluster')
end
end
end
diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb
index 497a50bebe4..bd9f7745cf8 100644
--- a/spec/features/projects/clusters_spec.rb
+++ b/spec/features/projects/clusters_spec.rb
@@ -17,7 +17,7 @@ feature 'Clusters', :js do
end
it 'sees empty state' do
- expect(page).to have_link('Add cluster')
+ expect(page).to have_link('Add Kubernetes cluster')
expect(page).to have_selector('.empty-state')
end
end
@@ -82,7 +82,7 @@ feature 'Clusters', :js do
before do
visit project_clusters_path(project)
- click_link 'Add cluster'
+ click_link 'Add Kubernetes cluster'
click_link 'Create on GKE'
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 e76bc6f1220..c1fccf4a40b 100644
--- a/spec/features/projects/import_export/namespace_export_file_spec.rb
+++ b/spec/features/projects/import_export/namespace_export_file_spec.rb
@@ -1,44 +1,37 @@
require 'spec_helper'
feature 'Import/Export - Namespace export file cleanup', :js do
- let(:export_path) { "#{Dir.tmpdir}/import_file_spec" }
- let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys }
+ let(:export_path) { Dir.mktmpdir('namespace_export_file_spec') }
- let(:project) { create(:project) }
-
- background do
- allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+ before do
+ allow(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
end
after do
FileUtils.rm_rf(export_path, secure: true)
end
- context 'admin user' do
+ shared_examples_for 'handling project exports on namespace change' do
+ let!(:old_export_path) { project.export_path }
+
before do
sign_in(create(:admin))
+
+ setup_export_project
end
context 'moving the namespace' do
- scenario 'removes the export file' do
- setup_export_project
-
- old_export_path = project.export_path.dup
-
+ it 'removes the export file' do
expect(File).to exist(old_export_path)
- project.namespace.update(path: 'new_path')
+ project.namespace.update!(path: build(:namespace).path)
expect(File).not_to exist(old_export_path)
end
end
context 'deleting the namespace' do
- scenario 'removes the export file' do
- setup_export_project
-
- old_export_path = project.export_path.dup
-
+ it 'removes the export file' do
expect(File).to exist(old_export_path)
project.namespace.destroy
@@ -46,17 +39,29 @@ feature 'Import/Export - Namespace export file cleanup', :js do
expect(File).not_to exist(old_export_path)
end
end
+ end
- def setup_export_project
- visit edit_project_path(project)
+ describe 'legacy storage' do
+ let(:project) { create(:project) }
- expect(page).to have_content('Export project')
+ it_behaves_like 'handling project exports on namespace change'
+ end
+
+ describe 'hashed storage' do
+ let(:project) { create(:project, :hashed) }
- find(:link, 'Export project').send_keys(:return)
+ it_behaves_like 'handling project exports on namespace change'
+ end
- visit edit_project_path(project)
+ def setup_export_project
+ visit edit_project_path(project)
- expect(page).to have_content('Download export')
- end
+ expect(page).to have_content('Export project')
+
+ find(:link, 'Export project').send_keys(:return)
+
+ visit edit_project_path(project)
+
+ expect(page).to have_content('Download export')
end
end
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index e661db1809a..5d311f2dde3 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -7,7 +7,7 @@ feature 'Jobs' do
let(:project) { create(:project, :repository) }
let(:pipeline) { create(:ci_pipeline, project: project) }
- let(:job) { create(:ci_build, :trace, pipeline: pipeline) }
+ let(:job) { create(:ci_build, :trace_live, pipeline: pipeline) }
let(:job2) { create(:ci_build) }
let(:artifacts_file) do
@@ -490,18 +490,34 @@ feature 'Jobs' do
describe 'GET /:project/jobs/:id/raw', :js do
context 'access source' do
context 'job from project' do
- before do
- job.run!
- end
+ context 'when job is running' do
+ before do
+ job.run!
+ end
- it 'sends the right headers' do
- requests = inspect_requests(inject_headers: { 'X-Sendfile-Type' => 'X-Sendfile' }) do
- visit raw_project_job_path(project, job)
+ it 'sends the right headers' do
+ requests = inspect_requests(inject_headers: { 'X-Sendfile-Type' => 'X-Sendfile' }) do
+ visit raw_project_job_path(project, job)
+ end
+
+ expect(requests.first.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
- 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))
+ context 'when job is complete' do
+ let(:job) { create(:ci_build, :success, :trace_artifact, pipeline: pipeline) }
+
+ it 'sends the right headers' do
+ requests = inspect_requests(inject_headers: { 'X-Sendfile-Type' => 'X-Sendfile' }) do
+ visit raw_project_job_path(project, job)
+ end
+
+ expect(requests.first.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.job_artifacts_trace.file.path)
+ end
end
end
diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb
index 85bd776932b..ae8b1364ec7 100644
--- a/spec/features/projects/labels/update_prioritization_spec.rb
+++ b/spec/features/projects/labels/update_prioritization_spec.rb
@@ -99,7 +99,7 @@ feature 'Prioritize labels' do
expect(page).to have_content 'wontfix'
# Sort labels
- drag_to(selector: '.js-prioritized-labels', from_index: 1, to_index: 2)
+ drag_to(selector: '.label-list-item', from_index: 1, to_index: 2)
page.within('.prioritized-labels') do
expect(first('li')).to have_content('feature')
diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb
index fa2f7a1fd78..65e24862d43 100644
--- a/spec/features/projects/pipeline_schedules_spec.rb
+++ b/spec/features/projects/pipeline_schedules_spec.rb
@@ -168,11 +168,11 @@ feature 'Pipeline Schedules', :js do
scenario 'user sees the new variable in edit window' do
find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
- page.within('.pipeline-variable-list') do
- expect(find(".pipeline-variable-row:nth-child(1) .pipeline-variable-key-input").value).to eq('AAA')
- expect(find(".pipeline-variable-row:nth-child(1) .pipeline-variable-value-input").value).to eq('AAA123')
- expect(find(".pipeline-variable-row:nth-child(2) .pipeline-variable-key-input").value).to eq('BBB')
- expect(find(".pipeline-variable-row:nth-child(2) .pipeline-variable-value-input").value).to eq('BBB123')
+ page.within('.ci-variable-list') do
+ expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-key").value).to eq('AAA')
+ expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-value", visible: false).value).to eq('AAA123')
+ expect(find(".ci-variable-row:nth-child(2) .js-ci-variable-input-key").value).to eq('BBB')
+ expect(find(".ci-variable-row:nth-child(2) .js-ci-variable-input-value", visible: false).value).to eq('BBB123')
end
end
end
@@ -185,16 +185,18 @@ feature 'Pipeline Schedules', :js do
visit_pipelines_schedules
find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
- all('[name="schedule[variables_attributes][][key]"]')[0].set('foo')
- all('[name="schedule[variables_attributes][][value]"]')[0].set('bar')
+
+ find('.js-ci-variable-list-section .js-secret-value-reveal-button').click
+ first('.js-ci-variable-input-key').set('foo')
+ first('.js-ci-variable-input-value').set('bar')
click_button 'Save pipeline schedule'
end
scenario 'user sees the updated variable in edit window' do
find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
- page.within('.pipeline-variable-list') do
- expect(find(".pipeline-variable-row:nth-child(1) .pipeline-variable-key-input").value).to eq('foo')
- expect(find(".pipeline-variable-row:nth-child(1) .pipeline-variable-value-input").value).to eq('bar')
+ page.within('.ci-variable-list') do
+ expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-key").value).to eq('foo')
+ expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-value", visible: false).value).to eq('bar')
end
end
end
@@ -207,15 +209,15 @@ feature 'Pipeline Schedules', :js do
visit_pipelines_schedules
find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
- find('.pipeline-variable-list .pipeline-variable-row-remove-button').click
+ find('.ci-variable-list .ci-variable-row-remove-button').click
click_button 'Save pipeline schedule'
end
scenario 'user does not see the removed variable in edit window' do
find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
- page.within('.pipeline-variable-list') do
- expect(find(".pipeline-variable-row:nth-child(1) .pipeline-variable-key-input").value).to eq('')
- expect(find(".pipeline-variable-row:nth-child(1) .pipeline-variable-value-input").value).to eq('')
+ page.within('.ci-variable-list') do
+ expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-key").value).to eq('')
+ expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-value", visible: false).value).to eq('')
end
end
end
diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
index 949d90a50ff..4d2a08afecc 100644
--- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
-describe 'User updates wiki page' do
+# Remove skip_gitaly_mock flag when gitaly_update_page implements moving pages
+describe 'User updates wiki page', :skip_gitaly_mock do
let(:user) { create(:user) }
before do
@@ -143,6 +144,7 @@ describe 'User updates wiki page' do
expect(page).to have_field('wiki[message]', with: 'Update home')
fill_in(:wiki_content, with: 'My awesome wiki!')
+
click_button('Save changes')
expect(page).to have_content('Home')
@@ -151,4 +153,74 @@ describe 'User updates wiki page' do
end
end
end
+
+ context 'when the page is in a subdir' do
+ let!(:project) { create(:project, namespace: user.namespace) }
+ let(:project_wiki) { create(:project_wiki, project: project, user: project.creator) }
+ let(:page_name) { 'page_name' }
+ let(:page_dir) { "foo/bar/#{page_name}" }
+ let!(:wiki_page) { create(:wiki_page, wiki: project_wiki, attrs: { title: page_dir, content: 'Home page' }) }
+
+ before do
+ visit(project_wiki_edit_path(project, wiki_page))
+ end
+
+ it 'moves the page to the root folder' do
+ fill_in(:wiki_title, with: "/#{page_name}")
+
+ click_button('Save changes')
+
+ expect(current_path).to eq(project_wiki_path(project, page_name))
+ end
+
+ it 'moves the page to other dir' do
+ new_page_dir = "foo1/bar1/#{page_name}"
+
+ fill_in(:wiki_title, with: new_page_dir)
+
+ click_button('Save changes')
+
+ expect(current_path).to eq(project_wiki_path(project, new_page_dir))
+ end
+
+ it 'remains in the same place if title has not changed' do
+ original_path = project_wiki_path(project, wiki_page)
+
+ fill_in(:wiki_title, with: page_name)
+
+ click_button('Save changes')
+
+ expect(current_path).to eq(original_path)
+ end
+
+ it 'can be moved to a different dir with a different name' do
+ new_page_dir = "foo1/bar1/new_page_name"
+
+ fill_in(:wiki_title, with: new_page_dir)
+
+ click_button('Save changes')
+
+ expect(current_path).to eq(project_wiki_path(project, new_page_dir))
+ end
+
+ it 'can be renamed and moved to the root folder' do
+ new_name = 'new_page_name'
+
+ fill_in(:wiki_title, with: "/#{new_name}")
+
+ click_button('Save changes')
+
+ expect(current_path).to eq(project_wiki_path(project, new_name))
+ end
+
+ it 'squishes the title before creating the page' do
+ new_page_dir = " foo1 / bar1 / #{page_name} "
+
+ fill_in(:wiki_title, with: new_page_dir)
+
+ click_button('Save changes')
+
+ expect(current_path).to eq(project_wiki_path(project, "foo1/bar1/#{page_name}"))
+ end
+ end
end
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 ff325aeadd3..e37436838fd 100644
--- a/spec/features/projects/wiki/user_views_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_views_wiki_page_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
-describe 'User views a wiki page' do
+# Remove skip_gitaly_mock flag when gitaly_update_page implements moving pages
+describe 'User views a wiki page', :skip_gitaly_mock do
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
let(:wiki_page) do
diff --git a/spec/finders/group_projects_finder_spec.rb b/spec/finders/group_projects_finder_spec.rb
index 27a09d7c6f5..be80ee7d767 100644
--- a/spec/finders/group_projects_finder_spec.rb
+++ b/spec/finders/group_projects_finder_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe GroupProjectsFinder do
let(:group) { create(:group) }
+ let(:subgroup) { create(:group, parent: group) }
let(:current_user) { create(:user) }
let(:options) { {} }
@@ -12,6 +13,8 @@ describe GroupProjectsFinder do
let!(:shared_project_1) { create(:project, :public, path: '3') }
let!(:shared_project_2) { create(:project, :private, path: '4') }
let!(:shared_project_3) { create(:project, :internal, path: '5') }
+ let!(:subgroup_project) { create(:project, :public, path: '6', group: subgroup) }
+ let!(:subgroup_private_project) { create(:project, :private, path: '7', group: subgroup) }
before do
shared_project_1.project_group_links.create(group_access: Gitlab::Access::MASTER, group: group)
@@ -35,11 +38,31 @@ describe GroupProjectsFinder do
context "only owned" do
let(:options) { { only_owned: true } }
- it { is_expected.to match_array([private_project, public_project]) }
+ context 'with subgroups projects', :nested_groups do
+ before do
+ options[:include_subgroups] = true
+ end
+
+ it { is_expected.to match_array([private_project, public_project, subgroup_project, subgroup_private_project]) }
+ end
+
+ context 'without subgroups projects' do
+ it { is_expected.to match_array([private_project, public_project]) }
+ end
end
context "all" do
- it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1, private_project, public_project]) }
+ context 'with subgroups projects', :nested_groups do
+ before do
+ options[:include_subgroups] = true
+ end
+
+ it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1, private_project, public_project, subgroup_project, subgroup_private_project]) }
+ end
+
+ context 'without subgroups projects' do
+ it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1, private_project, public_project]) }
+ end
end
end
@@ -71,9 +94,20 @@ describe GroupProjectsFinder do
context "without external user" do
before do
private_project.add_master(current_user)
+ subgroup_private_project.add_master(current_user)
end
- it { is_expected.to match_array([private_project, public_project]) }
+ context 'with subgroups projects', :nested_groups do
+ before do
+ options[:include_subgroups] = true
+ end
+
+ it { is_expected.to match_array([private_project, public_project, subgroup_project, subgroup_private_project]) }
+ end
+
+ context 'without subgroups projects' do
+ it { is_expected.to match_array([private_project, public_project]) }
+ end
end
context "with external user" do
@@ -81,12 +115,32 @@ describe GroupProjectsFinder do
current_user.update_attributes(external: true)
end
- it { is_expected.to eq([public_project]) }
+ context 'with subgroups projects', :nested_groups do
+ before do
+ options[:include_subgroups] = true
+ end
+
+ it { is_expected.to match_array([public_project, subgroup_project]) }
+ end
+
+ context 'without subgroups projects' do
+ it { is_expected.to eq([public_project]) }
+ end
end
end
context "all" do
- it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1, public_project]) }
+ context 'with subgroups projects', :nested_groups do
+ before do
+ options[:include_subgroups] = true
+ end
+
+ it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1, public_project, subgroup_project]) }
+ end
+
+ context 'without subgroups projects' do
+ it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1, public_project]) }
+ end
end
end
@@ -100,7 +154,17 @@ describe GroupProjectsFinder do
context "only owned" do
let(:options) { { only_owned: true } }
- it { is_expected.to eq([public_project]) }
+ context 'with subgroups projects', :nested_groups do
+ before do
+ options[:include_subgroups] = true
+ end
+
+ it { is_expected.to match_array([public_project, subgroup_project]) }
+ end
+
+ context 'without subgroups projects' do
+ it { is_expected.to eq([public_project]) }
+ end
end
end
end
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index 47fd98234f9..abb7631d7d7 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -3,13 +3,17 @@ require 'spec_helper'
describe IssuesFinder do
set(:user) { create(:user) }
set(:user2) { create(:user) }
- set(:project1) { create(:project) }
+ set(:group) { create(:group) }
+ set(:subgroup) { create(:group, parent: group) }
+ set(:project1) { create(:project, group: group) }
set(:project2) { create(:project) }
+ set(:project3) { create(:project, group: subgroup) }
set(:milestone) { create(:milestone, project: project1) }
set(:label) { create(:label, project: project2) }
set(:issue1) { create(:issue, author: user, assignees: [user], project: project1, milestone: milestone, title: 'gitlab', created_at: 1.week.ago) }
set(:issue2) { create(:issue, author: user, assignees: [user], project: project2, description: 'gitlab') }
set(:issue3) { create(:issue, author: user2, assignees: [user2], project: project2, title: 'tanuki', description: 'tanuki', created_at: 1.week.from_now) }
+ set(:issue4) { create(:issue, project: project3) }
set(:award_emoji1) { create(:award_emoji, name: 'thumbsup', user: user, awardable: issue1) }
set(:award_emoji2) { create(:award_emoji, name: 'thumbsup', user: user2, awardable: issue2) }
set(:award_emoji3) { create(:award_emoji, name: 'thumbsdown', user: user, awardable: issue3) }
@@ -25,10 +29,12 @@ describe IssuesFinder do
project1.add_master(user)
project2.add_developer(user)
project2.add_developer(user2)
+ project3.add_developer(user)
issue1
issue2
issue3
+ issue4
award_emoji1
award_emoji2
@@ -39,7 +45,7 @@ describe IssuesFinder do
let(:scope) { 'all' }
it 'returns all issues' do
- expect(issues).to contain_exactly(issue1, issue2, issue3)
+ expect(issues).to contain_exactly(issue1, issue2, issue3, issue4)
end
context 'filtering by assignee ID' do
@@ -50,6 +56,26 @@ describe IssuesFinder do
end
end
+ context 'filtering by group_id' do
+ let(:params) { { group_id: group.id } }
+
+ context 'when include_subgroup param not set' do
+ it 'returns all group issues' do
+ expect(issues).to contain_exactly(issue1)
+ end
+ end
+
+ context 'when include_subgroup param is true', :nested_groups do
+ before do
+ params[:include_subgroups] = true
+ end
+
+ it 'returns all group and subgroup issues' do
+ expect(issues).to contain_exactly(issue1, issue4)
+ end
+ end
+ end
+
context 'filtering by author ID' do
let(:params) { { author_id: user2.id } }
@@ -87,7 +113,7 @@ describe IssuesFinder do
let(:params) { { milestone_title: Milestone::None.title } }
it 'returns issues with no milestone' do
- expect(issues).to contain_exactly(issue2, issue3)
+ expect(issues).to contain_exactly(issue2, issue3, issue4)
end
end
@@ -185,7 +211,7 @@ describe IssuesFinder do
let(:params) { { label_name: Label::None.title } }
it 'returns issues with no labels' do
- expect(issues).to contain_exactly(issue1, issue3)
+ expect(issues).to contain_exactly(issue1, issue3, issue4)
end
end
@@ -210,7 +236,7 @@ describe IssuesFinder do
let(:params) { { state: 'opened' } }
it 'returns only opened issues' do
- expect(issues).to contain_exactly(issue1, issue2, issue3)
+ expect(issues).to contain_exactly(issue1, issue2, issue3, issue4)
end
end
@@ -226,7 +252,7 @@ describe IssuesFinder do
let(:params) { { state: 'all' } }
it 'returns all issues' do
- expect(issues).to contain_exactly(issue1, issue2, issue3, closed_issue)
+ expect(issues).to contain_exactly(issue1, issue2, issue3, closed_issue, issue4)
end
end
@@ -234,7 +260,7 @@ describe IssuesFinder do
let(:params) { { state: 'invalid_state' } }
it 'returns all issues' do
- expect(issues).to contain_exactly(issue1, issue2, issue3, closed_issue)
+ expect(issues).to contain_exactly(issue1, issue2, issue3, closed_issue, issue4)
end
end
end
@@ -338,7 +364,7 @@ describe IssuesFinder do
end
it "doesn't return issues if feature disabled" do
- [project1, project2].each do |project|
+ [project1, project2, project3].each do |project|
project.project_feature.update!(issues_access_level: ProjectFeature::DISABLED)
end
@@ -351,7 +377,7 @@ describe IssuesFinder do
it 'returns the number of rows for the default state' do
finder = described_class.new(user)
- expect(finder.row_count).to eq(3)
+ expect(finder.row_count).to eq(4)
end
it 'returns the number of rows for a given state' do
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index 687ffaec7cc..9385c892c9e 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -6,31 +6,36 @@ describe MergeRequestsFinder do
let(:user) { create :user }
let(:user2) { create :user }
- let(:project1) { create(:project, :public) }
+ let(:group) { create(:group) }
+ let(:subgroup) { create(:group, parent: group) }
+ let(:project1) { create(:project, :public, group: group) }
let(:project2) { fork_project(project1, user) }
let(:project3) do
p = fork_project(project1, user)
p.update!(archived: true)
p
end
+ let(:project4) { create(:project, :public, group: subgroup) }
let!(:merge_request1) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1) }
let!(:merge_request2) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1, state: 'closed') }
let!(:merge_request3) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project2) }
let!(:merge_request4) { create(:merge_request, :simple, author: user, source_project: project3, target_project: project3) }
+ let!(:merge_request5) { create(:merge_request, :simple, author: user, source_project: project4, target_project: project4) }
before do
project1.add_master(user)
project2.add_developer(user)
project3.add_developer(user)
project2.add_developer(user2)
+ project4.add_developer(user)
end
describe "#execute" do
it 'filters by scope' do
params = { scope: 'authored', state: 'opened' }
merge_requests = described_class.new(user, params).execute
- expect(merge_requests.size).to eq(3)
+ expect(merge_requests.size).to eq(4)
end
it 'filters by project' do
@@ -39,10 +44,26 @@ describe MergeRequestsFinder do
expect(merge_requests.size).to eq(1)
end
+ it 'filters by group' do
+ params = { group_id: group.id }
+
+ merge_requests = described_class.new(user, params).execute
+
+ expect(merge_requests.size).to eq(2)
+ end
+
+ it 'filters by group including subgroups', :nested_groups do
+ params = { group_id: group.id, include_subgroups: true }
+
+ merge_requests = described_class.new(user, params).execute
+
+ expect(merge_requests.size).to eq(3)
+ end
+
it 'filters by non_archived' do
params = { non_archived: true }
merge_requests = described_class.new(user, params).execute
- expect(merge_requests.size).to eq(3)
+ expect(merge_requests.size).to eq(4)
end
it 'filters by iid' do
@@ -73,14 +94,14 @@ describe MergeRequestsFinder do
end
context 'with created_after and created_before params' do
- let(:project4) { create(:project, forked_from_project: project1) }
+ let(:new_project) { create(:project, forked_from_project: project1) }
let!(:new_merge_request) do
create(:merge_request,
:simple,
author: user,
created_at: 1.week.from_now,
- source_project: project4,
+ source_project: new_project,
target_project: project1)
end
@@ -89,12 +110,12 @@ describe MergeRequestsFinder do
:simple,
author: user,
created_at: 1.week.ago,
- source_project: project4,
- target_project: project4)
+ source_project: new_project,
+ target_project: new_project)
end
before do
- project4.add_master(user)
+ new_project.add_master(user)
end
it 'filters by created_after' do
@@ -106,7 +127,7 @@ describe MergeRequestsFinder do
end
it 'filters by created_before' do
- params = { project_id: project4.id, created_before: old_merge_request.created_at + 1.second }
+ params = { project_id: new_project.id, created_before: old_merge_request.created_at + 1.second }
merge_requests = described_class.new(user, params).execute
@@ -119,7 +140,7 @@ describe MergeRequestsFinder do
it 'returns the number of rows for the default state' do
finder = described_class.new(user)
- expect(finder.row_count).to eq(3)
+ expect(finder.row_count).to eq(4)
end
it 'returns the number of rows for a given state' do
diff --git a/spec/fixtures/api/schemas/public_api/v4/user/basic.json b/spec/fixtures/api/schemas/public_api/v4/user/basic.json
index bf330d8278c..2d815be32a6 100644
--- a/spec/fixtures/api/schemas/public_api/v4/user/basic.json
+++ b/spec/fixtures/api/schemas/public_api/v4/user/basic.json
@@ -2,12 +2,16 @@
"type": ["object", "null"],
"required": [
"id",
+ "name",
+ "username",
"state",
"avatar_url",
"web_url"
],
"properties": {
"id": { "type": "integer" },
+ "name": { "type": "string" },
+ "username": { "type": "string" },
"state": { "type": "string" },
"avatar_url": { "type": "string" },
"web_url": { "type": "string" }
diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb
index 71abb6da607..da32a46675f 100644
--- a/spec/fixtures/markdown.md.erb
+++ b/spec/fixtures/markdown.md.erb
@@ -280,6 +280,18 @@ However the wrapping tags cannot be mixed as such:
![My Video](/assets/videos/gitlab-demo.mp4)
+### Colors
+
+`#F00`
+`#F00A`
+`#FF0000`
+`#FF0000AA`
+`RGB(0,255,0)`
+`RGB(0%,100%,0%)`
+`RGBA(0,255,0,0.7)`
+`HSL(540,70%,50%)`
+`HSLA(540,70%,50%,0.7)`
+
### Mermaid
> If this is not rendered correctly, see
diff --git a/spec/fixtures/trace/sample_trace b/spec/fixtures/trace/sample_trace
new file mode 100644
index 00000000000..55fcb9d2756
--- /dev/null
+++ b/spec/fixtures/trace/sample_trace
@@ -0,0 +1,1185 @@
+Running with gitlab-runner 10.4.0 (857480b6)
+ on docker-auto-scale-com (9a6801bd)
+Using Docker executor with image dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git-2.14-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6 ...
+Starting service postgres:9.2 ...
+Pulling docker image postgres:9.2 ...
+Using docker image postgres:9.2 ID=sha256:18cdbca56093c841d28e629eb8acd4224afe0aa4c57c839351fc181888b8a470 for postgres service...
+Starting service redis:alpine ...
+Pulling docker image redis:alpine ...
+Using docker image redis:alpine ID=sha256:cb1ec54b370d4a91dff57d00f91fd880dc710160a58440adaa133e0f84ae999d for redis service...
+Waiting for services to be up and running...
+Using docker image sha256:3006a02a5a6f0a116358a13bbc46ee46fb2471175efd5b7f9b1c22345ec2a8e9 for predefined container...
+Pulling docker image dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git-2.14-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6 ...
+Using docker image dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git-2.14-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6 ID=sha256:1f59be408f12738509ffe4177d65e9de6391f32461de83d9d45f58517b30af99 for build container...
+section_start:1517486886:prepare_script
+Running on runner-9a6801bd-project-13083-concurrent-0 via runner-9a6801bd-gsrm-1517484168-a8449153...
+section_end:1517486887:prepare_script
+section_start:1517486887:get_sources
+Fetching changes for 42624-gitaly-bundle-isolation-not-working-in-ci with git depth set to 20...
+Removing .gitlab_shell_secret
+Removing .gitlab_workhorse_secret
+Removing .yarn-cache/
+Removing config/database.yml
+Removing config/gitlab.yml
+Removing config/redis.cache.yml
+Removing config/redis.queues.yml
+Removing config/redis.shared_state.yml
+Removing config/resque.yml
+Removing config/secrets.yml
+Removing coverage/
+Removing knapsack/
+Removing log/api_json.log
+Removing log/application.log
+Removing log/gitaly-test.log
+Removing log/githost.log
+Removing log/grpc.log
+Removing log/test_json.log
+Removing node_modules/
+Removing public/assets/
+Removing rspec_flaky/
+Removing shared/tmp/
+Removing tmp/tests/
+Removing vendor/ruby/
+HEAD is now at 4cea24f Converted todos.js to axios
+From https://gitlab.com/gitlab-org/gitlab-ce
+ * [new branch] 42624-gitaly-bundle-isolation-not-working-in-ci -> origin/42624-gitaly-bundle-isolation-not-working-in-ci
+Checking out f42a5e24 as 42624-gitaly-bundle-isolation-not-working-in-ci...
+Skipping Git submodules setup
+section_end:1517486896:get_sources
+section_start:1517486896:restore_cache
+Checking cache for ruby-2.3.6-with-yarn...
+Downloading cache.zip from http://runners-cache-5-internal.gitlab.com:444/runner/project/13083/ruby-2.3.6-with-yarn
+Successfully extracted cache
+section_end:1517486919:restore_cache
+section_start:1517486919:download_artifacts
+Downloading artifacts for retrieve-tests-metadata (50551658)...
+Downloading artifacts from coordinator... ok  id=50551658 responseStatus=200 OK token=HhF7y_1X
+Downloading artifacts for compile-assets (50551659)...
+Downloading artifacts from coordinator... ok  id=50551659 responseStatus=200 OK token=wTz6JrCP
+Downloading artifacts for setup-test-env (50551660)...
+Downloading artifacts from coordinator... ok  id=50551660 responseStatus=200 OK token=DTGgeVF5
+WARNING: tmp/tests/gitlab-shell/.gitlab_shell_secret: chmod tmp/tests/gitlab-shell/.gitlab_shell_secret: no such file or directory (suppressing repeats)
+section_end:1517486934:download_artifacts
+section_start:1517486934:build_script
+$ bundle --version
+Bundler version 1.16.1
+$ source scripts/utils.sh
+$ source scripts/prepare_build.sh
+The Gemfile's dependencies are satisfied
+Successfully installed knapsack-1.15.0
+1 gem installed
+NOTICE: database "gitlabhq_test" does not exist, skipping
+DROP DATABASE
+CREATE DATABASE
+CREATE ROLE
+GRANT
+-- enable_extension("plpgsql")
+ -> 0.0156s
+-- enable_extension("pg_trgm")
+ -> 0.0156s
+-- create_table("abuse_reports", {:force=>:cascade})
+ -> 0.0119s
+-- create_table("appearances", {:force=>:cascade})
+ -> 0.0065s
+-- create_table("application_settings", {:force=>:cascade})
+ -> 0.0382s
+-- create_table("audit_events", {:force=>:cascade})
+ -> 0.0056s
+-- add_index("audit_events", ["entity_id", "entity_type"], {:name=>"index_audit_events_on_entity_id_and_entity_type", :using=>:btree})
+ -> 0.0040s
+-- create_table("award_emoji", {:force=>:cascade})
+ -> 0.0058s
+-- add_index("award_emoji", ["awardable_type", "awardable_id"], {:name=>"index_award_emoji_on_awardable_type_and_awardable_id", :using=>:btree})
+ -> 0.0068s
+-- add_index("award_emoji", ["user_id", "name"], {:name=>"index_award_emoji_on_user_id_and_name", :using=>:btree})
+ -> 0.0043s
+-- create_table("boards", {:force=>:cascade})
+ -> 0.0049s
+-- add_index("boards", ["project_id"], {:name=>"index_boards_on_project_id", :using=>:btree})
+ -> 0.0056s
+-- create_table("broadcast_messages", {:force=>:cascade})
+ -> 0.0056s
+-- add_index("broadcast_messages", ["starts_at", "ends_at", "id"], {:name=>"index_broadcast_messages_on_starts_at_and_ends_at_and_id", :using=>:btree})
+ -> 0.0041s
+-- create_table("chat_names", {:force=>:cascade})
+ -> 0.0056s
+-- add_index("chat_names", ["service_id", "team_id", "chat_id"], {:name=>"index_chat_names_on_service_id_and_team_id_and_chat_id", :unique=>true, :using=>:btree})
+ -> 0.0039s
+-- add_index("chat_names", ["user_id", "service_id"], {:name=>"index_chat_names_on_user_id_and_service_id", :unique=>true, :using=>:btree})
+ -> 0.0036s
+-- create_table("chat_teams", {:force=>:cascade})
+ -> 0.0068s
+-- add_index("chat_teams", ["namespace_id"], {:name=>"index_chat_teams_on_namespace_id", :unique=>true, :using=>:btree})
+ -> 0.0098s
+-- create_table("ci_build_trace_section_names", {:force=>:cascade})
+ -> 0.0048s
+-- add_index("ci_build_trace_section_names", ["project_id", "name"], {:name=>"index_ci_build_trace_section_names_on_project_id_and_name", :unique=>true, :using=>:btree})
+ -> 0.0035s
+-- create_table("ci_build_trace_sections", {:force=>:cascade})
+ -> 0.0040s
+-- add_index("ci_build_trace_sections", ["build_id", "section_name_id"], {:name=>"index_ci_build_trace_sections_on_build_id_and_section_name_id", :unique=>true, :using=>:btree})
+ -> 0.0035s
+-- add_index("ci_build_trace_sections", ["project_id"], {:name=>"index_ci_build_trace_sections_on_project_id", :using=>:btree})
+ -> 0.0033s
+-- create_table("ci_builds", {:force=>:cascade})
+ -> 0.0062s
+-- add_index("ci_builds", ["auto_canceled_by_id"], {:name=>"index_ci_builds_on_auto_canceled_by_id", :using=>:btree})
+ -> 0.0035s
+-- add_index("ci_builds", ["commit_id", "stage_idx", "created_at"], {:name=>"index_ci_builds_on_commit_id_and_stage_idx_and_created_at", :using=>:btree})
+ -> 0.0032s
+-- add_index("ci_builds", ["commit_id", "status", "type"], {:name=>"index_ci_builds_on_commit_id_and_status_and_type", :using=>:btree})
+ -> 0.0032s
+-- add_index("ci_builds", ["commit_id", "type", "name", "ref"], {:name=>"index_ci_builds_on_commit_id_and_type_and_name_and_ref", :using=>:btree})
+ -> 0.0035s
+-- add_index("ci_builds", ["commit_id", "type", "ref"], {:name=>"index_ci_builds_on_commit_id_and_type_and_ref", :using=>:btree})
+ -> 0.0042s
+-- add_index("ci_builds", ["project_id", "id"], {:name=>"index_ci_builds_on_project_id_and_id", :using=>:btree})
+ -> 0.0031s
+-- add_index("ci_builds", ["protected"], {:name=>"index_ci_builds_on_protected", :using=>:btree})
+ -> 0.0031s
+-- add_index("ci_builds", ["runner_id"], {:name=>"index_ci_builds_on_runner_id", :using=>:btree})
+ -> 0.0033s
+-- add_index("ci_builds", ["stage_id"], {:name=>"index_ci_builds_on_stage_id", :using=>:btree})
+ -> 0.0035s
+-- add_index("ci_builds", ["status", "type", "runner_id"], {:name=>"index_ci_builds_on_status_and_type_and_runner_id", :using=>:btree})
+ -> 0.0031s
+-- add_index("ci_builds", ["status"], {:name=>"index_ci_builds_on_status", :using=>:btree})
+ -> 0.0032s
+-- add_index("ci_builds", ["token"], {:name=>"index_ci_builds_on_token", :unique=>true, :using=>:btree})
+ -> 0.0028s
+-- add_index("ci_builds", ["updated_at"], {:name=>"index_ci_builds_on_updated_at", :using=>:btree})
+ -> 0.0047s
+-- add_index("ci_builds", ["user_id"], {:name=>"index_ci_builds_on_user_id", :using=>:btree})
+ -> 0.0029s
+-- create_table("ci_group_variables", {:force=>:cascade})
+ -> 0.0055s
+-- add_index("ci_group_variables", ["group_id", "key"], {:name=>"index_ci_group_variables_on_group_id_and_key", :unique=>true, :using=>:btree})
+ -> 0.0028s
+-- create_table("ci_job_artifacts", {:force=>:cascade})
+ -> 0.0048s
+-- add_index("ci_job_artifacts", ["job_id", "file_type"], {:name=>"index_ci_job_artifacts_on_job_id_and_file_type", :unique=>true, :using=>:btree})
+ -> 0.0027s
+-- add_index("ci_job_artifacts", ["project_id"], {:name=>"index_ci_job_artifacts_on_project_id", :using=>:btree})
+ -> 0.0028s
+-- create_table("ci_pipeline_schedule_variables", {:force=>:cascade})
+ -> 0.0044s
+-- add_index("ci_pipeline_schedule_variables", ["pipeline_schedule_id", "key"], {:name=>"index_ci_pipeline_schedule_variables_on_schedule_id_and_key", :unique=>true, :using=>:btree})
+ -> 0.0032s
+-- create_table("ci_pipeline_schedules", {:force=>:cascade})
+ -> 0.0047s
+-- add_index("ci_pipeline_schedules", ["next_run_at", "active"], {:name=>"index_ci_pipeline_schedules_on_next_run_at_and_active", :using=>:btree})
+ -> 0.0029s
+-- add_index("ci_pipeline_schedules", ["project_id"], {:name=>"index_ci_pipeline_schedules_on_project_id", :using=>:btree})
+ -> 0.0028s
+-- create_table("ci_pipeline_variables", {:force=>:cascade})
+ -> 0.0045s
+-- add_index("ci_pipeline_variables", ["pipeline_id", "key"], {:name=>"index_ci_pipeline_variables_on_pipeline_id_and_key", :unique=>true, :using=>:btree})
+ -> 0.0030s
+-- create_table("ci_pipelines", {:force=>:cascade})
+ -> 0.0057s
+-- add_index("ci_pipelines", ["auto_canceled_by_id"], {:name=>"index_ci_pipelines_on_auto_canceled_by_id", :using=>:btree})
+ -> 0.0030s
+-- add_index("ci_pipelines", ["pipeline_schedule_id"], {:name=>"index_ci_pipelines_on_pipeline_schedule_id", :using=>:btree})
+ -> 0.0031s
+-- add_index("ci_pipelines", ["project_id", "ref", "status", "id"], {:name=>"index_ci_pipelines_on_project_id_and_ref_and_status_and_id", :using=>:btree})
+ -> 0.0032s
+-- add_index("ci_pipelines", ["project_id", "sha"], {:name=>"index_ci_pipelines_on_project_id_and_sha", :using=>:btree})
+ -> 0.0032s
+-- add_index("ci_pipelines", ["project_id"], {:name=>"index_ci_pipelines_on_project_id", :using=>:btree})
+ -> 0.0035s
+-- add_index("ci_pipelines", ["status"], {:name=>"index_ci_pipelines_on_status", :using=>:btree})
+ -> 0.0032s
+-- add_index("ci_pipelines", ["user_id"], {:name=>"index_ci_pipelines_on_user_id", :using=>:btree})
+ -> 0.0029s
+-- create_table("ci_runner_projects", {:force=>:cascade})
+ -> 0.0035s
+-- add_index("ci_runner_projects", ["project_id"], {:name=>"index_ci_runner_projects_on_project_id", :using=>:btree})
+ -> 0.0029s
+-- add_index("ci_runner_projects", ["runner_id"], {:name=>"index_ci_runner_projects_on_runner_id", :using=>:btree})
+ -> 0.0028s
+-- create_table("ci_runners", {:force=>:cascade})
+ -> 0.0059s
+-- add_index("ci_runners", ["contacted_at"], {:name=>"index_ci_runners_on_contacted_at", :using=>:btree})
+ -> 0.0030s
+-- add_index("ci_runners", ["is_shared"], {:name=>"index_ci_runners_on_is_shared", :using=>:btree})
+ -> 0.0030s
+-- add_index("ci_runners", ["locked"], {:name=>"index_ci_runners_on_locked", :using=>:btree})
+ -> 0.0030s
+-- add_index("ci_runners", ["token"], {:name=>"index_ci_runners_on_token", :using=>:btree})
+ -> 0.0029s
+-- create_table("ci_stages", {:force=>:cascade})
+ -> 0.0046s
+-- add_index("ci_stages", ["pipeline_id", "name"], {:name=>"index_ci_stages_on_pipeline_id_and_name", :using=>:btree})
+ -> 0.0031s
+-- add_index("ci_stages", ["pipeline_id"], {:name=>"index_ci_stages_on_pipeline_id", :using=>:btree})
+ -> 0.0030s
+-- add_index("ci_stages", ["project_id"], {:name=>"index_ci_stages_on_project_id", :using=>:btree})
+ -> 0.0028s
+-- create_table("ci_trigger_requests", {:force=>:cascade})
+ -> 0.0058s
+-- add_index("ci_trigger_requests", ["commit_id"], {:name=>"index_ci_trigger_requests_on_commit_id", :using=>:btree})
+ -> 0.0031s
+-- create_table("ci_triggers", {:force=>:cascade})
+ -> 0.0043s
+-- add_index("ci_triggers", ["project_id"], {:name=>"index_ci_triggers_on_project_id", :using=>:btree})
+ -> 0.0033s
+-- create_table("ci_variables", {:force=>:cascade})
+ -> 0.0059s
+-- 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})
+ -> 0.0031s
+-- create_table("cluster_platforms_kubernetes", {:force=>:cascade})
+ -> 0.0053s
+-- add_index("cluster_platforms_kubernetes", ["cluster_id"], {:name=>"index_cluster_platforms_kubernetes_on_cluster_id", :unique=>true, :using=>:btree})
+ -> 0.0028s
+-- create_table("cluster_projects", {:force=>:cascade})
+ -> 0.0032s
+-- add_index("cluster_projects", ["cluster_id"], {:name=>"index_cluster_projects_on_cluster_id", :using=>:btree})
+ -> 0.0035s
+-- add_index("cluster_projects", ["project_id"], {:name=>"index_cluster_projects_on_project_id", :using=>:btree})
+ -> 0.0030s
+-- create_table("cluster_providers_gcp", {:force=>:cascade})
+ -> 0.0051s
+-- add_index("cluster_providers_gcp", ["cluster_id"], {:name=>"index_cluster_providers_gcp_on_cluster_id", :unique=>true, :using=>:btree})
+ -> 0.0034s
+-- create_table("clusters", {:force=>:cascade})
+ -> 0.0052s
+-- add_index("clusters", ["enabled"], {:name=>"index_clusters_on_enabled", :using=>:btree})
+ -> 0.0031s
+-- add_index("clusters", ["user_id"], {:name=>"index_clusters_on_user_id", :using=>:btree})
+ -> 0.0028s
+-- create_table("clusters_applications_helm", {:force=>:cascade})
+ -> 0.0045s
+-- create_table("clusters_applications_ingress", {:force=>:cascade})
+ -> 0.0044s
+-- create_table("clusters_applications_prometheus", {:force=>:cascade})
+ -> 0.0047s
+-- create_table("container_repositories", {:force=>:cascade})
+ -> 0.0050s
+-- add_index("container_repositories", ["project_id", "name"], {:name=>"index_container_repositories_on_project_id_and_name", :unique=>true, :using=>:btree})
+ -> 0.0032s
+-- add_index("container_repositories", ["project_id"], {:name=>"index_container_repositories_on_project_id", :using=>:btree})
+ -> 0.0032s
+-- create_table("conversational_development_index_metrics", {:force=>:cascade})
+ -> 0.0076s
+-- create_table("deploy_keys_projects", {:force=>:cascade})
+ -> 0.0037s
+-- add_index("deploy_keys_projects", ["project_id"], {:name=>"index_deploy_keys_projects_on_project_id", :using=>:btree})
+ -> 0.0032s
+-- create_table("deployments", {:force=>:cascade})
+ -> 0.0049s
+-- add_index("deployments", ["created_at"], {:name=>"index_deployments_on_created_at", :using=>:btree})
+ -> 0.0034s
+-- add_index("deployments", ["environment_id", "id"], {:name=>"index_deployments_on_environment_id_and_id", :using=>:btree})
+ -> 0.0028s
+-- add_index("deployments", ["environment_id", "iid", "project_id"], {:name=>"index_deployments_on_environment_id_and_iid_and_project_id", :using=>:btree})
+ -> 0.0029s
+-- add_index("deployments", ["project_id", "iid"], {:name=>"index_deployments_on_project_id_and_iid", :unique=>true, :using=>:btree})
+ -> 0.0032s
+-- create_table("emails", {:force=>:cascade})
+ -> 0.0046s
+-- add_index("emails", ["confirmation_token"], {:name=>"index_emails_on_confirmation_token", :unique=>true, :using=>:btree})
+ -> 0.0030s
+-- add_index("emails", ["email"], {:name=>"index_emails_on_email", :unique=>true, :using=>:btree})
+ -> 0.0035s
+-- add_index("emails", ["user_id"], {:name=>"index_emails_on_user_id", :using=>:btree})
+ -> 0.0028s
+-- create_table("environments", {:force=>:cascade})
+ -> 0.0052s
+-- add_index("environments", ["project_id", "name"], {:name=>"index_environments_on_project_id_and_name", :unique=>true, :using=>:btree})
+ -> 0.0031s
+-- add_index("environments", ["project_id", "slug"], {:name=>"index_environments_on_project_id_and_slug", :unique=>true, :using=>:btree})
+ -> 0.0028s
+-- create_table("events", {:force=>:cascade})
+ -> 0.0046s
+-- add_index("events", ["action"], {:name=>"index_events_on_action", :using=>:btree})
+ -> 0.0032s
+-- add_index("events", ["author_id"], {:name=>"index_events_on_author_id", :using=>:btree})
+ -> 0.0027s
+-- add_index("events", ["project_id", "id"], {:name=>"index_events_on_project_id_and_id", :using=>:btree})
+ -> 0.0027s
+-- add_index("events", ["target_type", "target_id"], {:name=>"index_events_on_target_type_and_target_id", :using=>:btree})
+ -> 0.0027s
+-- create_table("feature_gates", {:force=>:cascade})
+ -> 0.0046s
+-- add_index("feature_gates", ["feature_key", "key", "value"], {:name=>"index_feature_gates_on_feature_key_and_key_and_value", :unique=>true, :using=>:btree})
+ -> 0.0031s
+-- create_table("features", {:force=>:cascade})
+ -> 0.0041s
+-- add_index("features", ["key"], {:name=>"index_features_on_key", :unique=>true, :using=>:btree})
+ -> 0.0030s
+-- create_table("fork_network_members", {:force=>:cascade})
+ -> 0.0033s
+-- add_index("fork_network_members", ["fork_network_id"], {:name=>"index_fork_network_members_on_fork_network_id", :using=>:btree})
+ -> 0.0033s
+-- add_index("fork_network_members", ["project_id"], {:name=>"index_fork_network_members_on_project_id", :unique=>true, :using=>:btree})
+ -> 0.0029s
+-- create_table("fork_networks", {:force=>:cascade})
+ -> 0.0049s
+-- add_index("fork_networks", ["root_project_id"], {:name=>"index_fork_networks_on_root_project_id", :unique=>true, :using=>:btree})
+ -> 0.0029s
+-- create_table("forked_project_links", {:force=>:cascade})
+ -> 0.0032s
+-- add_index("forked_project_links", ["forked_to_project_id"], {:name=>"index_forked_project_links_on_forked_to_project_id", :unique=>true, :using=>:btree})
+ -> 0.0030s
+-- create_table("gcp_clusters", {:force=>:cascade})
+ -> 0.0074s
+-- add_index("gcp_clusters", ["project_id"], {:name=>"index_gcp_clusters_on_project_id", :unique=>true, :using=>:btree})
+ -> 0.0030s
+-- create_table("gpg_key_subkeys", {:force=>:cascade})
+ -> 0.0042s
+-- add_index("gpg_key_subkeys", ["fingerprint"], {:name=>"index_gpg_key_subkeys_on_fingerprint", :unique=>true, :using=>:btree})
+ -> 0.0029s
+-- add_index("gpg_key_subkeys", ["gpg_key_id"], {:name=>"index_gpg_key_subkeys_on_gpg_key_id", :using=>:btree})
+ -> 0.0032s
+-- add_index("gpg_key_subkeys", ["keyid"], {:name=>"index_gpg_key_subkeys_on_keyid", :unique=>true, :using=>:btree})
+ -> 0.0027s
+-- create_table("gpg_keys", {:force=>:cascade})
+ -> 0.0042s
+-- add_index("gpg_keys", ["fingerprint"], {:name=>"index_gpg_keys_on_fingerprint", :unique=>true, :using=>:btree})
+ -> 0.0032s
+-- add_index("gpg_keys", ["primary_keyid"], {:name=>"index_gpg_keys_on_primary_keyid", :unique=>true, :using=>:btree})
+ -> 0.0026s
+-- add_index("gpg_keys", ["user_id"], {:name=>"index_gpg_keys_on_user_id", :using=>:btree})
+ -> 0.0028s
+-- create_table("gpg_signatures", {:force=>:cascade})
+ -> 0.0054s
+-- add_index("gpg_signatures", ["commit_sha"], {:name=>"index_gpg_signatures_on_commit_sha", :unique=>true, :using=>:btree})
+ -> 0.0029s
+-- add_index("gpg_signatures", ["gpg_key_id"], {:name=>"index_gpg_signatures_on_gpg_key_id", :using=>:btree})
+ -> 0.0026s
+-- add_index("gpg_signatures", ["gpg_key_primary_keyid"], {:name=>"index_gpg_signatures_on_gpg_key_primary_keyid", :using=>:btree})
+ -> 0.0029s
+-- add_index("gpg_signatures", ["gpg_key_subkey_id"], {:name=>"index_gpg_signatures_on_gpg_key_subkey_id", :using=>:btree})
+ -> 0.0032s
+-- add_index("gpg_signatures", ["project_id"], {:name=>"index_gpg_signatures_on_project_id", :using=>:btree})
+ -> 0.0028s
+-- create_table("group_custom_attributes", {:force=>:cascade})
+ -> 0.0044s
+-- add_index("group_custom_attributes", ["group_id", "key"], {:name=>"index_group_custom_attributes_on_group_id_and_key", :unique=>true, :using=>:btree})
+ -> 0.0032s
+-- add_index("group_custom_attributes", ["key", "value"], {:name=>"index_group_custom_attributes_on_key_and_value", :using=>:btree})
+ -> 0.0028s
+-- create_table("identities", {:force=>:cascade})
+ -> 0.0043s
+-- add_index("identities", ["user_id"], {:name=>"index_identities_on_user_id", :using=>:btree})
+ -> 0.0034s
+-- create_table("issue_assignees", {:id=>false, :force=>:cascade})
+ -> 0.0013s
+-- add_index("issue_assignees", ["issue_id", "user_id"], {:name=>"index_issue_assignees_on_issue_id_and_user_id", :unique=>true, :using=>:btree})
+ -> 0.0028s
+-- add_index("issue_assignees", ["user_id"], {:name=>"index_issue_assignees_on_user_id", :using=>:btree})
+ -> 0.0029s
+-- create_table("issue_metrics", {:force=>:cascade})
+ -> 0.0032s
+-- add_index("issue_metrics", ["issue_id"], {:name=>"index_issue_metrics", :using=>:btree})
+ -> 0.0029s
+-- create_table("issues", {:force=>:cascade})
+ -> 0.0051s
+-- add_index("issues", ["author_id"], {:name=>"index_issues_on_author_id", :using=>:btree})
+ -> 0.0028s
+-- add_index("issues", ["confidential"], {:name=>"index_issues_on_confidential", :using=>:btree})
+ -> 0.0029s
+-- add_index("issues", ["description"], {:name=>"index_issues_on_description_trigram", :using=>:gin, :opclasses=>{"description"=>"gin_trgm_ops"}})
+ -> 0.0022s
+-- add_index("issues", ["milestone_id"], {:name=>"index_issues_on_milestone_id", :using=>:btree})
+ -> 0.0027s
+-- add_index("issues", ["moved_to_id"], {:name=>"index_issues_on_moved_to_id", :where=>"(moved_to_id IS NOT NULL)", :using=>:btree})
+ -> 0.0030s
+-- add_index("issues", ["project_id", "created_at", "id", "state"], {:name=>"index_issues_on_project_id_and_created_at_and_id_and_state", :using=>:btree})
+ -> 0.0039s
+-- add_index("issues", ["project_id", "due_date", "id", "state"], {:name=>"idx_issues_on_project_id_and_due_date_and_id_and_state_partial", :where=>"(due_date IS NOT NULL)", :using=>:btree})
+ -> 0.0031s
+-- add_index("issues", ["project_id", "iid"], {:name=>"index_issues_on_project_id_and_iid", :unique=>true, :using=>:btree})
+ -> 0.0032s
+-- add_index("issues", ["project_id", "updated_at", "id", "state"], {:name=>"index_issues_on_project_id_and_updated_at_and_id_and_state", :using=>:btree})
+ -> 0.0035s
+-- add_index("issues", ["relative_position"], {:name=>"index_issues_on_relative_position", :using=>:btree})
+ -> 0.0030s
+-- add_index("issues", ["state"], {:name=>"index_issues_on_state", :using=>:btree})
+ -> 0.0027s
+-- add_index("issues", ["title"], {:name=>"index_issues_on_title_trigram", :using=>:gin, :opclasses=>{"title"=>"gin_trgm_ops"}})
+ -> 0.0021s
+-- add_index("issues", ["updated_at"], {:name=>"index_issues_on_updated_at", :using=>:btree})
+ -> 0.0030s
+-- add_index("issues", ["updated_by_id"], {:name=>"index_issues_on_updated_by_id", :where=>"(updated_by_id IS NOT NULL)", :using=>:btree})
+ -> 0.0028s
+-- create_table("keys", {:force=>:cascade})
+ -> 0.0048s
+-- add_index("keys", ["fingerprint"], {:name=>"index_keys_on_fingerprint", :unique=>true, :using=>:btree})
+ -> 0.0028s
+-- add_index("keys", ["user_id"], {:name=>"index_keys_on_user_id", :using=>:btree})
+ -> 0.0029s
+-- create_table("label_links", {:force=>:cascade})
+ -> 0.0041s
+-- add_index("label_links", ["label_id"], {:name=>"index_label_links_on_label_id", :using=>:btree})
+ -> 0.0027s
+-- add_index("label_links", ["target_id", "target_type"], {:name=>"index_label_links_on_target_id_and_target_type", :using=>:btree})
+ -> 0.0028s
+-- create_table("label_priorities", {:force=>:cascade})
+ -> 0.0031s
+-- add_index("label_priorities", ["priority"], {:name=>"index_label_priorities_on_priority", :using=>:btree})
+ -> 0.0028s
+-- add_index("label_priorities", ["project_id", "label_id"], {:name=>"index_label_priorities_on_project_id_and_label_id", :unique=>true, :using=>:btree})
+ -> 0.0027s
+-- create_table("labels", {:force=>:cascade})
+ -> 0.0046s
+-- add_index("labels", ["group_id", "project_id", "title"], {:name=>"index_labels_on_group_id_and_project_id_and_title", :unique=>true, :using=>:btree})
+ -> 0.0028s
+-- add_index("labels", ["project_id"], {:name=>"index_labels_on_project_id", :using=>:btree})
+ -> 0.0032s
+-- add_index("labels", ["template"], {:name=>"index_labels_on_template", :where=>"template", :using=>:btree})
+ -> 0.0027s
+-- add_index("labels", ["title"], {:name=>"index_labels_on_title", :using=>:btree})
+ -> 0.0030s
+-- add_index("labels", ["type", "project_id"], {:name=>"index_labels_on_type_and_project_id", :using=>:btree})
+ -> 0.0028s
+-- create_table("lfs_objects", {:force=>:cascade})
+ -> 0.0040s
+-- add_index("lfs_objects", ["oid"], {:name=>"index_lfs_objects_on_oid", :unique=>true, :using=>:btree})
+ -> 0.0032s
+-- create_table("lfs_objects_projects", {:force=>:cascade})
+ -> 0.0035s
+-- add_index("lfs_objects_projects", ["project_id"], {:name=>"index_lfs_objects_projects_on_project_id", :using=>:btree})
+ -> 0.0025s
+-- create_table("lists", {:force=>:cascade})
+ -> 0.0033s
+-- add_index("lists", ["board_id", "label_id"], {:name=>"index_lists_on_board_id_and_label_id", :unique=>true, :using=>:btree})
+ -> 0.0026s
+-- add_index("lists", ["label_id"], {:name=>"index_lists_on_label_id", :using=>:btree})
+ -> 0.0026s
+-- create_table("members", {:force=>:cascade})
+ -> 0.0046s
+-- add_index("members", ["access_level"], {:name=>"index_members_on_access_level", :using=>:btree})
+ -> 0.0028s
+-- add_index("members", ["invite_token"], {:name=>"index_members_on_invite_token", :unique=>true, :using=>:btree})
+ -> 0.0027s
+-- add_index("members", ["requested_at"], {:name=>"index_members_on_requested_at", :using=>:btree})
+ -> 0.0025s
+-- add_index("members", ["source_id", "source_type"], {:name=>"index_members_on_source_id_and_source_type", :using=>:btree})
+ -> 0.0027s
+-- add_index("members", ["user_id"], {:name=>"index_members_on_user_id", :using=>:btree})
+ -> 0.0026s
+-- create_table("merge_request_diff_commits", {:id=>false, :force=>:cascade})
+ -> 0.0027s
+-- add_index("merge_request_diff_commits", ["merge_request_diff_id", "relative_order"], {:name=>"index_merge_request_diff_commits_on_mr_diff_id_and_order", :unique=>true, :using=>:btree})
+ -> 0.0032s
+-- add_index("merge_request_diff_commits", ["sha"], {:name=>"index_merge_request_diff_commits_on_sha", :using=>:btree})
+ -> 0.0029s
+-- create_table("merge_request_diff_files", {:id=>false, :force=>:cascade})
+ -> 0.0027s
+-- add_index("merge_request_diff_files", ["merge_request_diff_id", "relative_order"], {:name=>"index_merge_request_diff_files_on_mr_diff_id_and_order", :unique=>true, :using=>:btree})
+ -> 0.0027s
+-- create_table("merge_request_diffs", {:force=>:cascade})
+ -> 0.0042s
+-- add_index("merge_request_diffs", ["merge_request_id", "id"], {:name=>"index_merge_request_diffs_on_merge_request_id_and_id", :using=>:btree})
+ -> 0.0030s
+-- create_table("merge_request_metrics", {:force=>:cascade})
+ -> 0.0034s
+-- add_index("merge_request_metrics", ["first_deployed_to_production_at"], {:name=>"index_merge_request_metrics_on_first_deployed_to_production_at", :using=>:btree})
+ -> 0.0028s
+-- add_index("merge_request_metrics", ["merge_request_id"], {:name=>"index_merge_request_metrics", :using=>:btree})
+ -> 0.0025s
+-- add_index("merge_request_metrics", ["pipeline_id"], {:name=>"index_merge_request_metrics_on_pipeline_id", :using=>:btree})
+ -> 0.0026s
+-- create_table("merge_requests", {:force=>:cascade})
+ -> 0.0066s
+-- add_index("merge_requests", ["assignee_id"], {:name=>"index_merge_requests_on_assignee_id", :using=>:btree})
+ -> 0.0029s
+-- add_index("merge_requests", ["author_id"], {:name=>"index_merge_requests_on_author_id", :using=>:btree})
+ -> 0.0026s
+-- add_index("merge_requests", ["created_at"], {:name=>"index_merge_requests_on_created_at", :using=>:btree})
+ -> 0.0026s
+-- add_index("merge_requests", ["description"], {:name=>"index_merge_requests_on_description_trigram", :using=>:gin, :opclasses=>{"description"=>"gin_trgm_ops"}})
+ -> 0.0020s
+-- add_index("merge_requests", ["head_pipeline_id"], {:name=>"index_merge_requests_on_head_pipeline_id", :using=>:btree})
+ -> 0.0027s
+-- add_index("merge_requests", ["latest_merge_request_diff_id"], {:name=>"index_merge_requests_on_latest_merge_request_diff_id", :using=>:btree})
+ -> 0.0025s
+-- add_index("merge_requests", ["merge_user_id"], {:name=>"index_merge_requests_on_merge_user_id", :where=>"(merge_user_id IS NOT NULL)", :using=>:btree})
+ -> 0.0029s
+-- add_index("merge_requests", ["milestone_id"], {:name=>"index_merge_requests_on_milestone_id", :using=>:btree})
+ -> 0.0030s
+-- add_index("merge_requests", ["source_branch"], {:name=>"index_merge_requests_on_source_branch", :using=>:btree})
+ -> 0.0026s
+-- add_index("merge_requests", ["source_project_id", "source_branch"], {:name=>"index_merge_requests_on_source_project_and_branch_state_opened", :where=>"((state)::text = 'opened'::text)", :using=>:btree})
+ -> 0.0029s
+-- add_index("merge_requests", ["source_project_id", "source_branch"], {:name=>"index_merge_requests_on_source_project_id_and_source_branch", :using=>:btree})
+ -> 0.0031s
+-- add_index("merge_requests", ["target_branch"], {:name=>"index_merge_requests_on_target_branch", :using=>:btree})
+ -> 0.0028s
+-- add_index("merge_requests", ["target_project_id", "iid"], {:name=>"index_merge_requests_on_target_project_id_and_iid", :unique=>true, :using=>:btree})
+ -> 0.0027s
+-- add_index("merge_requests", ["target_project_id", "merge_commit_sha", "id"], {:name=>"index_merge_requests_on_tp_id_and_merge_commit_sha_and_id", :using=>:btree})
+ -> 0.0029s
+-- add_index("merge_requests", ["title"], {:name=>"index_merge_requests_on_title", :using=>:btree})
+ -> 0.0026s
+-- add_index("merge_requests", ["title"], {:name=>"index_merge_requests_on_title_trigram", :using=>:gin, :opclasses=>{"title"=>"gin_trgm_ops"}})
+ -> 0.0020s
+-- add_index("merge_requests", ["updated_by_id"], {:name=>"index_merge_requests_on_updated_by_id", :where=>"(updated_by_id IS NOT NULL)", :using=>:btree})
+ -> 0.0029s
+-- create_table("merge_requests_closing_issues", {:force=>:cascade})
+ -> 0.0031s
+-- add_index("merge_requests_closing_issues", ["issue_id"], {:name=>"index_merge_requests_closing_issues_on_issue_id", :using=>:btree})
+ -> 0.0026s
+-- add_index("merge_requests_closing_issues", ["merge_request_id"], {:name=>"index_merge_requests_closing_issues_on_merge_request_id", :using=>:btree})
+ -> 0.0028s
+-- create_table("milestones", {:force=>:cascade})
+ -> 0.0044s
+-- add_index("milestones", ["description"], {:name=>"index_milestones_on_description_trigram", :using=>:gin, :opclasses=>{"description"=>"gin_trgm_ops"}})
+ -> 0.0022s
+-- add_index("milestones", ["due_date"], {:name=>"index_milestones_on_due_date", :using=>:btree})
+ -> 0.0033s
+-- add_index("milestones", ["group_id"], {:name=>"index_milestones_on_group_id", :using=>:btree})
+ -> 0.0028s
+-- add_index("milestones", ["project_id", "iid"], {:name=>"index_milestones_on_project_id_and_iid", :unique=>true, :using=>:btree})
+ -> 0.0028s
+-- add_index("milestones", ["title"], {:name=>"index_milestones_on_title", :using=>:btree})
+ -> 0.0026s
+-- add_index("milestones", ["title"], {:name=>"index_milestones_on_title_trigram", :using=>:gin, :opclasses=>{"title"=>"gin_trgm_ops"}})
+ -> 0.0021s
+-- create_table("namespaces", {:force=>:cascade})
+ -> 0.0068s
+-- add_index("namespaces", ["created_at"], {:name=>"index_namespaces_on_created_at", :using=>:btree})
+ -> 0.0030s
+-- add_index("namespaces", ["name", "parent_id"], {:name=>"index_namespaces_on_name_and_parent_id", :unique=>true, :using=>:btree})
+ -> 0.0030s
+-- add_index("namespaces", ["name"], {:name=>"index_namespaces_on_name_trigram", :using=>:gin, :opclasses=>{"name"=>"gin_trgm_ops"}})
+ -> 0.0020s
+-- add_index("namespaces", ["owner_id"], {:name=>"index_namespaces_on_owner_id", :using=>:btree})
+ -> 0.0028s
+-- add_index("namespaces", ["parent_id", "id"], {:name=>"index_namespaces_on_parent_id_and_id", :unique=>true, :using=>:btree})
+ -> 0.0032s
+-- add_index("namespaces", ["path"], {:name=>"index_namespaces_on_path", :using=>:btree})
+ -> 0.0031s
+-- add_index("namespaces", ["path"], {:name=>"index_namespaces_on_path_trigram", :using=>:gin, :opclasses=>{"path"=>"gin_trgm_ops"}})
+ -> 0.0019s
+-- add_index("namespaces", ["require_two_factor_authentication"], {:name=>"index_namespaces_on_require_two_factor_authentication", :using=>:btree})
+ -> 0.0029s
+-- add_index("namespaces", ["type"], {:name=>"index_namespaces_on_type", :using=>:btree})
+ -> 0.0032s
+-- create_table("notes", {:force=>:cascade})
+ -> 0.0055s
+-- add_index("notes", ["author_id"], {:name=>"index_notes_on_author_id", :using=>:btree})
+ -> 0.0029s
+-- add_index("notes", ["commit_id"], {:name=>"index_notes_on_commit_id", :using=>:btree})
+ -> 0.0028s
+-- add_index("notes", ["created_at"], {:name=>"index_notes_on_created_at", :using=>:btree})
+ -> 0.0029s
+-- add_index("notes", ["discussion_id"], {:name=>"index_notes_on_discussion_id", :using=>:btree})
+ -> 0.0029s
+-- add_index("notes", ["line_code"], {:name=>"index_notes_on_line_code", :using=>:btree})
+ -> 0.0029s
+-- add_index("notes", ["note"], {:name=>"index_notes_on_note_trigram", :using=>:gin, :opclasses=>{"note"=>"gin_trgm_ops"}})
+ -> 0.0024s
+-- add_index("notes", ["noteable_id", "noteable_type"], {:name=>"index_notes_on_noteable_id_and_noteable_type", :using=>:btree})
+ -> 0.0029s
+-- add_index("notes", ["noteable_type"], {:name=>"index_notes_on_noteable_type", :using=>:btree})
+ -> 0.0030s
+-- add_index("notes", ["project_id", "noteable_type"], {:name=>"index_notes_on_project_id_and_noteable_type", :using=>:btree})
+ -> 0.0027s
+-- add_index("notes", ["updated_at"], {:name=>"index_notes_on_updated_at", :using=>:btree})
+ -> 0.0026s
+-- create_table("notification_settings", {:force=>:cascade})
+ -> 0.0053s
+-- add_index("notification_settings", ["source_id", "source_type"], {:name=>"index_notification_settings_on_source_id_and_source_type", :using=>:btree})
+ -> 0.0028s
+-- add_index("notification_settings", ["user_id", "source_id", "source_type"], {:name=>"index_notifications_on_user_id_and_source_id_and_source_type", :unique=>true, :using=>:btree})
+ -> 0.0030s
+-- add_index("notification_settings", ["user_id"], {:name=>"index_notification_settings_on_user_id", :using=>:btree})
+ -> 0.0031s
+-- create_table("oauth_access_grants", {:force=>:cascade})
+ -> 0.0042s
+-- add_index("oauth_access_grants", ["token"], {:name=>"index_oauth_access_grants_on_token", :unique=>true, :using=>:btree})
+ -> 0.0031s
+-- create_table("oauth_access_tokens", {:force=>:cascade})
+ -> 0.0051s
+-- add_index("oauth_access_tokens", ["refresh_token"], {:name=>"index_oauth_access_tokens_on_refresh_token", :unique=>true, :using=>:btree})
+ -> 0.0030s
+-- add_index("oauth_access_tokens", ["resource_owner_id"], {:name=>"index_oauth_access_tokens_on_resource_owner_id", :using=>:btree})
+ -> 0.0025s
+-- add_index("oauth_access_tokens", ["token"], {:name=>"index_oauth_access_tokens_on_token", :unique=>true, :using=>:btree})
+ -> 0.0026s
+-- create_table("oauth_applications", {:force=>:cascade})
+ -> 0.0049s
+-- add_index("oauth_applications", ["owner_id", "owner_type"], {:name=>"index_oauth_applications_on_owner_id_and_owner_type", :using=>:btree})
+ -> 0.0030s
+-- add_index("oauth_applications", ["uid"], {:name=>"index_oauth_applications_on_uid", :unique=>true, :using=>:btree})
+ -> 0.0032s
+-- create_table("oauth_openid_requests", {:force=>:cascade})
+ -> 0.0048s
+-- create_table("pages_domains", {:force=>:cascade})
+ -> 0.0052s
+-- add_index("pages_domains", ["domain"], {:name=>"index_pages_domains_on_domain", :unique=>true, :using=>:btree})
+ -> 0.0027s
+-- add_index("pages_domains", ["project_id"], {:name=>"index_pages_domains_on_project_id", :using=>:btree})
+ -> 0.0030s
+-- create_table("personal_access_tokens", {:force=>:cascade})
+ -> 0.0056s
+-- add_index("personal_access_tokens", ["token"], {:name=>"index_personal_access_tokens_on_token", :unique=>true, :using=>:btree})
+ -> 0.0032s
+-- add_index("personal_access_tokens", ["user_id"], {:name=>"index_personal_access_tokens_on_user_id", :using=>:btree})
+ -> 0.0028s
+-- create_table("project_authorizations", {:id=>false, :force=>:cascade})
+ -> 0.0018s
+-- add_index("project_authorizations", ["project_id"], {:name=>"index_project_authorizations_on_project_id", :using=>:btree})
+ -> 0.0033s
+-- add_index("project_authorizations", ["user_id", "project_id", "access_level"], {:name=>"index_project_authorizations_on_user_id_project_id_access_level", :unique=>true, :using=>:btree})
+ -> 0.0029s
+-- create_table("project_auto_devops", {:force=>:cascade})
+ -> 0.0043s
+-- add_index("project_auto_devops", ["project_id"], {:name=>"index_project_auto_devops_on_project_id", :unique=>true, :using=>:btree})
+ -> 0.0029s
+-- create_table("project_custom_attributes", {:force=>:cascade})
+ -> 0.0047s
+-- add_index("project_custom_attributes", ["key", "value"], {:name=>"index_project_custom_attributes_on_key_and_value", :using=>:btree})
+ -> 0.0030s
+-- add_index("project_custom_attributes", ["project_id", "key"], {:name=>"index_project_custom_attributes_on_project_id_and_key", :unique=>true, :using=>:btree})
+ -> 0.0028s
+-- create_table("project_features", {:force=>:cascade})
+ -> 0.0038s
+-- add_index("project_features", ["project_id"], {:name=>"index_project_features_on_project_id", :using=>:btree})
+ -> 0.0029s
+-- create_table("project_group_links", {:force=>:cascade})
+ -> 0.0036s
+-- add_index("project_group_links", ["group_id"], {:name=>"index_project_group_links_on_group_id", :using=>:btree})
+ -> 0.0028s
+-- add_index("project_group_links", ["project_id"], {:name=>"index_project_group_links_on_project_id", :using=>:btree})
+ -> 0.0030s
+-- create_table("project_import_data", {:force=>:cascade})
+ -> 0.0049s
+-- add_index("project_import_data", ["project_id"], {:name=>"index_project_import_data_on_project_id", :using=>:btree})
+ -> 0.0027s
+-- create_table("project_statistics", {:force=>:cascade})
+ -> 0.0046s
+-- add_index("project_statistics", ["namespace_id"], {:name=>"index_project_statistics_on_namespace_id", :using=>:btree})
+ -> 0.0027s
+-- add_index("project_statistics", ["project_id"], {:name=>"index_project_statistics_on_project_id", :unique=>true, :using=>:btree})
+ -> 0.0029s
+-- create_table("projects", {:force=>:cascade})
+ -> 0.0090s
+-- add_index("projects", ["ci_id"], {:name=>"index_projects_on_ci_id", :using=>:btree})
+ -> 0.0033s
+-- add_index("projects", ["created_at"], {:name=>"index_projects_on_created_at", :using=>:btree})
+ -> 0.0030s
+-- add_index("projects", ["creator_id"], {:name=>"index_projects_on_creator_id", :using=>:btree})
+ -> 0.0028s
+-- add_index("projects", ["description"], {:name=>"index_projects_on_description_trigram", :using=>:gin, :opclasses=>{"description"=>"gin_trgm_ops"}})
+ -> 0.0022s
+-- add_index("projects", ["last_activity_at"], {:name=>"index_projects_on_last_activity_at", :using=>:btree})
+ -> 0.0032s
+-- add_index("projects", ["last_repository_check_failed"], {:name=>"index_projects_on_last_repository_check_failed", :using=>:btree})
+ -> 0.0030s
+-- add_index("projects", ["last_repository_updated_at"], {:name=>"index_projects_on_last_repository_updated_at", :using=>:btree})
+ -> 0.0031s
+-- add_index("projects", ["name"], {:name=>"index_projects_on_name_trigram", :using=>:gin, :opclasses=>{"name"=>"gin_trgm_ops"}})
+ -> 0.0022s
+-- add_index("projects", ["namespace_id"], {:name=>"index_projects_on_namespace_id", :using=>:btree})
+ -> 0.0028s
+-- add_index("projects", ["path"], {:name=>"index_projects_on_path", :using=>:btree})
+ -> 0.0028s
+-- add_index("projects", ["path"], {:name=>"index_projects_on_path_trigram", :using=>:gin, :opclasses=>{"path"=>"gin_trgm_ops"}})
+ -> 0.0023s
+-- add_index("projects", ["pending_delete"], {:name=>"index_projects_on_pending_delete", :using=>:btree})
+ -> 0.0029s
+-- add_index("projects", ["repository_storage"], {:name=>"index_projects_on_repository_storage", :using=>:btree})
+ -> 0.0026s
+-- add_index("projects", ["runners_token"], {:name=>"index_projects_on_runners_token", :using=>:btree})
+ -> 0.0034s
+-- add_index("projects", ["star_count"], {:name=>"index_projects_on_star_count", :using=>:btree})
+ -> 0.0028s
+-- add_index("projects", ["visibility_level"], {:name=>"index_projects_on_visibility_level", :using=>:btree})
+ -> 0.0027s
+-- create_table("protected_branch_merge_access_levels", {:force=>:cascade})
+ -> 0.0042s
+-- add_index("protected_branch_merge_access_levels", ["protected_branch_id"], {:name=>"index_protected_branch_merge_access", :using=>:btree})
+ -> 0.0029s
+-- create_table("protected_branch_push_access_levels", {:force=>:cascade})
+ -> 0.0037s
+-- add_index("protected_branch_push_access_levels", ["protected_branch_id"], {:name=>"index_protected_branch_push_access", :using=>:btree})
+ -> 0.0030s
+-- create_table("protected_branches", {:force=>:cascade})
+ -> 0.0048s
+-- add_index("protected_branches", ["project_id"], {:name=>"index_protected_branches_on_project_id", :using=>:btree})
+ -> 0.0030s
+-- create_table("protected_tag_create_access_levels", {:force=>:cascade})
+ -> 0.0037s
+-- add_index("protected_tag_create_access_levels", ["protected_tag_id"], {:name=>"index_protected_tag_create_access", :using=>:btree})
+ -> 0.0029s
+-- add_index("protected_tag_create_access_levels", ["user_id"], {:name=>"index_protected_tag_create_access_levels_on_user_id", :using=>:btree})
+ -> 0.0029s
+-- create_table("protected_tags", {:force=>:cascade})
+ -> 0.0051s
+-- add_index("protected_tags", ["project_id"], {:name=>"index_protected_tags_on_project_id", :using=>:btree})
+ -> 0.0034s
+-- create_table("push_event_payloads", {:id=>false, :force=>:cascade})
+ -> 0.0030s
+-- add_index("push_event_payloads", ["event_id"], {:name=>"index_push_event_payloads_on_event_id", :unique=>true, :using=>:btree})
+ -> 0.0029s
+-- create_table("redirect_routes", {:force=>:cascade})
+ -> 0.0049s
+-- add_index("redirect_routes", ["path"], {:name=>"index_redirect_routes_on_path", :unique=>true, :using=>:btree})
+ -> 0.0031s
+-- add_index("redirect_routes", ["source_type", "source_id"], {:name=>"index_redirect_routes_on_source_type_and_source_id", :using=>:btree})
+ -> 0.0034s
+-- create_table("releases", {:force=>:cascade})
+ -> 0.0043s
+-- add_index("releases", ["project_id", "tag"], {:name=>"index_releases_on_project_id_and_tag", :using=>:btree})
+ -> 0.0032s
+-- add_index("releases", ["project_id"], {:name=>"index_releases_on_project_id", :using=>:btree})
+ -> 0.0030s
+-- create_table("routes", {:force=>:cascade})
+ -> 0.0055s
+-- add_index("routes", ["path"], {:name=>"index_routes_on_path", :unique=>true, :using=>:btree})
+ -> 0.0028s
+-- add_index("routes", ["path"], {:name=>"index_routes_on_path_text_pattern_ops", :using=>:btree, :opclasses=>{"path"=>"varchar_pattern_ops"}})
+ -> 0.0026s
+-- add_index("routes", ["source_type", "source_id"], {:name=>"index_routes_on_source_type_and_source_id", :unique=>true, :using=>:btree})
+ -> 0.0029s
+-- create_table("sent_notifications", {:force=>:cascade})
+ -> 0.0048s
+-- add_index("sent_notifications", ["reply_key"], {:name=>"index_sent_notifications_on_reply_key", :unique=>true, :using=>:btree})
+ -> 0.0029s
+-- create_table("services", {:force=>:cascade})
+ -> 0.0091s
+-- add_index("services", ["project_id"], {:name=>"index_services_on_project_id", :using=>:btree})
+ -> 0.0028s
+-- add_index("services", ["template"], {:name=>"index_services_on_template", :using=>:btree})
+ -> 0.0031s
+-- create_table("snippets", {:force=>:cascade})
+ -> 0.0050s
+-- add_index("snippets", ["author_id"], {:name=>"index_snippets_on_author_id", :using=>:btree})
+ -> 0.0030s
+-- add_index("snippets", ["file_name"], {:name=>"index_snippets_on_file_name_trigram", :using=>:gin, :opclasses=>{"file_name"=>"gin_trgm_ops"}})
+ -> 0.0020s
+-- add_index("snippets", ["project_id"], {:name=>"index_snippets_on_project_id", :using=>:btree})
+ -> 0.0028s
+-- add_index("snippets", ["title"], {:name=>"index_snippets_on_title_trigram", :using=>:gin, :opclasses=>{"title"=>"gin_trgm_ops"}})
+ -> 0.0020s
+-- add_index("snippets", ["updated_at"], {:name=>"index_snippets_on_updated_at", :using=>:btree})
+ -> 0.0026s
+-- add_index("snippets", ["visibility_level"], {:name=>"index_snippets_on_visibility_level", :using=>:btree})
+ -> 0.0026s
+-- create_table("spam_logs", {:force=>:cascade})
+ -> 0.0048s
+-- create_table("subscriptions", {:force=>:cascade})
+ -> 0.0041s
+-- add_index("subscriptions", ["subscribable_id", "subscribable_type", "user_id", "project_id"], {:name=>"index_subscriptions_on_subscribable_and_user_id_and_project_id", :unique=>true, :using=>:btree})
+ -> 0.0030s
+-- create_table("system_note_metadata", {:force=>:cascade})
+ -> 0.0040s
+-- add_index("system_note_metadata", ["note_id"], {:name=>"index_system_note_metadata_on_note_id", :unique=>true, :using=>:btree})
+ -> 0.0029s
+-- create_table("taggings", {:force=>:cascade})
+ -> 0.0047s
+-- add_index("taggings", ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], {:name=>"taggings_idx", :unique=>true, :using=>:btree})
+ -> 0.0030s
+-- add_index("taggings", ["taggable_id", "taggable_type", "context"], {:name=>"index_taggings_on_taggable_id_and_taggable_type_and_context", :using=>:btree})
+ -> 0.0025s
+-- create_table("tags", {:force=>:cascade})
+ -> 0.0044s
+-- add_index("tags", ["name"], {:name=>"index_tags_on_name", :unique=>true, :using=>:btree})
+ -> 0.0026s
+-- create_table("timelogs", {:force=>:cascade})
+ -> 0.0033s
+-- add_index("timelogs", ["issue_id"], {:name=>"index_timelogs_on_issue_id", :using=>:btree})
+ -> 0.0027s
+-- add_index("timelogs", ["merge_request_id"], {:name=>"index_timelogs_on_merge_request_id", :using=>:btree})
+ -> 0.0033s
+-- add_index("timelogs", ["user_id"], {:name=>"index_timelogs_on_user_id", :using=>:btree})
+ -> 0.0028s
+-- create_table("todos", {:force=>:cascade})
+ -> 0.0043s
+-- add_index("todos", ["author_id"], {:name=>"index_todos_on_author_id", :using=>:btree})
+ -> 0.0027s
+-- add_index("todos", ["commit_id"], {:name=>"index_todos_on_commit_id", :using=>:btree})
+ -> 0.0028s
+-- add_index("todos", ["note_id"], {:name=>"index_todos_on_note_id", :using=>:btree})
+ -> 0.0028s
+-- add_index("todos", ["project_id"], {:name=>"index_todos_on_project_id", :using=>:btree})
+ -> 0.0027s
+-- add_index("todos", ["target_type", "target_id"], {:name=>"index_todos_on_target_type_and_target_id", :using=>:btree})
+ -> 0.0028s
+-- add_index("todos", ["user_id"], {:name=>"index_todos_on_user_id", :using=>:btree})
+ -> 0.0026s
+-- create_table("trending_projects", {:force=>:cascade})
+ -> 0.0030s
+-- add_index("trending_projects", ["project_id"], {:name=>"index_trending_projects_on_project_id", :using=>:btree})
+ -> 0.0027s
+-- create_table("u2f_registrations", {:force=>:cascade})
+ -> 0.0048s
+-- add_index("u2f_registrations", ["key_handle"], {:name=>"index_u2f_registrations_on_key_handle", :using=>:btree})
+ -> 0.0029s
+-- add_index("u2f_registrations", ["user_id"], {:name=>"index_u2f_registrations_on_user_id", :using=>:btree})
+ -> 0.0028s
+-- create_table("uploads", {:force=>:cascade})
+ -> 0.0044s
+-- add_index("uploads", ["checksum"], {:name=>"index_uploads_on_checksum", :using=>:btree})
+ -> 0.0028s
+-- add_index("uploads", ["model_id", "model_type"], {:name=>"index_uploads_on_model_id_and_model_type", :using=>:btree})
+ -> 0.0027s
+-- add_index("uploads", ["path"], {:name=>"index_uploads_on_path", :using=>:btree})
+ -> 0.0028s
+-- create_table("user_agent_details", {:force=>:cascade})
+ -> 0.0051s
+-- add_index("user_agent_details", ["subject_id", "subject_type"], {:name=>"index_user_agent_details_on_subject_id_and_subject_type", :using=>:btree})
+ -> 0.0028s
+-- create_table("user_custom_attributes", {:force=>:cascade})
+ -> 0.0044s
+-- add_index("user_custom_attributes", ["key", "value"], {:name=>"index_user_custom_attributes_on_key_and_value", :using=>:btree})
+ -> 0.0027s
+-- add_index("user_custom_attributes", ["user_id", "key"], {:name=>"index_user_custom_attributes_on_user_id_and_key", :unique=>true, :using=>:btree})
+ -> 0.0026s
+-- create_table("user_synced_attributes_metadata", {:force=>:cascade})
+ -> 0.0056s
+-- add_index("user_synced_attributes_metadata", ["user_id"], {:name=>"index_user_synced_attributes_metadata_on_user_id", :unique=>true, :using=>:btree})
+ -> 0.0027s
+-- create_table("users", {:force=>:cascade})
+ -> 0.0134s
+-- add_index("users", ["admin"], {:name=>"index_users_on_admin", :using=>:btree})
+ -> 0.0030s
+-- add_index("users", ["confirmation_token"], {:name=>"index_users_on_confirmation_token", :unique=>true, :using=>:btree})
+ -> 0.0029s
+-- add_index("users", ["created_at"], {:name=>"index_users_on_created_at", :using=>:btree})
+ -> 0.0034s
+-- add_index("users", ["email"], {:name=>"index_users_on_email", :unique=>true, :using=>:btree})
+ -> 0.0030s
+-- add_index("users", ["email"], {:name=>"index_users_on_email_trigram", :using=>:gin, :opclasses=>{"email"=>"gin_trgm_ops"}})
+ -> 0.0431s
+-- add_index("users", ["ghost"], {:name=>"index_users_on_ghost", :using=>:btree})
+ -> 0.0051s
+-- add_index("users", ["incoming_email_token"], {:name=>"index_users_on_incoming_email_token", :using=>:btree})
+ -> 0.0044s
+-- add_index("users", ["name"], {:name=>"index_users_on_name", :using=>:btree})
+ -> 0.0044s
+-- add_index("users", ["name"], {:name=>"index_users_on_name_trigram", :using=>:gin, :opclasses=>{"name"=>"gin_trgm_ops"}})
+ -> 0.0034s
+-- add_index("users", ["reset_password_token"], {:name=>"index_users_on_reset_password_token", :unique=>true, :using=>:btree})
+ -> 0.0044s
+-- add_index("users", ["rss_token"], {:name=>"index_users_on_rss_token", :using=>:btree})
+ -> 0.0046s
+-- add_index("users", ["state"], {:name=>"index_users_on_state", :using=>:btree})
+ -> 0.0040s
+-- add_index("users", ["username"], {:name=>"index_users_on_username", :using=>:btree})
+ -> 0.0046s
+-- add_index("users", ["username"], {:name=>"index_users_on_username_trigram", :using=>:gin, :opclasses=>{"username"=>"gin_trgm_ops"}})
+ -> 0.0044s
+-- create_table("users_star_projects", {:force=>:cascade})
+ -> 0.0055s
+-- add_index("users_star_projects", ["project_id"], {:name=>"index_users_star_projects_on_project_id", :using=>:btree})
+ -> 0.0037s
+-- add_index("users_star_projects", ["user_id", "project_id"], {:name=>"index_users_star_projects_on_user_id_and_project_id", :unique=>true, :using=>:btree})
+ -> 0.0044s
+-- create_table("web_hook_logs", {:force=>:cascade})
+ -> 0.0060s
+-- add_index("web_hook_logs", ["web_hook_id"], {:name=>"index_web_hook_logs_on_web_hook_id", :using=>:btree})
+ -> 0.0034s
+-- create_table("web_hooks", {:force=>:cascade})
+ -> 0.0120s
+-- add_index("web_hooks", ["project_id"], {:name=>"index_web_hooks_on_project_id", :using=>:btree})
+ -> 0.0038s
+-- add_index("web_hooks", ["type"], {:name=>"index_web_hooks_on_type", :using=>:btree})
+ -> 0.0036s
+-- add_foreign_key("boards", "projects", {:name=>"fk_f15266b5f9", :on_delete=>:cascade})
+ -> 0.0030s
+-- add_foreign_key("chat_teams", "namespaces", {:on_delete=>:cascade})
+ -> 0.0021s
+-- add_foreign_key("ci_build_trace_section_names", "projects", {:on_delete=>:cascade})
+ -> 0.0022s
+-- add_foreign_key("ci_build_trace_sections", "ci_build_trace_section_names", {:column=>"section_name_id", :name=>"fk_264e112c66", :on_delete=>:cascade})
+ -> 0.0018s
+-- add_foreign_key("ci_build_trace_sections", "ci_builds", {:column=>"build_id", :name=>"fk_4ebe41f502", :on_delete=>:cascade})
+ -> 0.0024s
+-- add_foreign_key("ci_build_trace_sections", "projects", {:on_delete=>:cascade})
+ -> 0.0019s
+-- add_foreign_key("ci_builds", "ci_pipelines", {:column=>"auto_canceled_by_id", :name=>"fk_a2141b1522", :on_delete=>:nullify})
+ -> 0.0023s
+-- add_foreign_key("ci_builds", "ci_stages", {:column=>"stage_id", :name=>"fk_3a9eaa254d", :on_delete=>:cascade})
+ -> 0.0020s
+-- add_foreign_key("ci_builds", "projects", {:name=>"fk_befce0568a", :on_delete=>:cascade})
+ -> 0.0024s
+-- add_foreign_key("ci_group_variables", "namespaces", {:column=>"group_id", :name=>"fk_33ae4d58d8", :on_delete=>:cascade})
+ -> 0.0024s
+-- add_foreign_key("ci_job_artifacts", "ci_builds", {:column=>"job_id", :on_delete=>:cascade})
+ -> 0.0019s
+-- add_foreign_key("ci_job_artifacts", "projects", {:on_delete=>:cascade})
+ -> 0.0020s
+-- add_foreign_key("ci_pipeline_schedule_variables", "ci_pipeline_schedules", {:column=>"pipeline_schedule_id", :name=>"fk_41c35fda51", :on_delete=>:cascade})
+ -> 0.0027s
+-- add_foreign_key("ci_pipeline_schedules", "projects", {:name=>"fk_8ead60fcc4", :on_delete=>:cascade})
+ -> 0.0022s
+-- add_foreign_key("ci_pipeline_schedules", "users", {:column=>"owner_id", :name=>"fk_9ea99f58d2", :on_delete=>:nullify})
+ -> 0.0025s
+-- add_foreign_key("ci_pipeline_variables", "ci_pipelines", {:column=>"pipeline_id", :name=>"fk_f29c5f4380", :on_delete=>:cascade})
+ -> 0.0018s
+-- add_foreign_key("ci_pipelines", "ci_pipeline_schedules", {:column=>"pipeline_schedule_id", :name=>"fk_3d34ab2e06", :on_delete=>:nullify})
+ -> 0.0019s
+-- add_foreign_key("ci_pipelines", "ci_pipelines", {:column=>"auto_canceled_by_id", :name=>"fk_262d4c2d19", :on_delete=>:nullify})
+ -> 0.0029s
+-- add_foreign_key("ci_pipelines", "projects", {:name=>"fk_86635dbd80", :on_delete=>:cascade})
+ -> 0.0023s
+-- add_foreign_key("ci_runner_projects", "projects", {:name=>"fk_4478a6f1e4", :on_delete=>:cascade})
+ -> 0.0036s
+-- add_foreign_key("ci_stages", "ci_pipelines", {:column=>"pipeline_id", :name=>"fk_fb57e6cc56", :on_delete=>:cascade})
+ -> 0.0017s
+-- add_foreign_key("ci_stages", "projects", {:name=>"fk_2360681d1d", :on_delete=>:cascade})
+ -> 0.0020s
+-- add_foreign_key("ci_trigger_requests", "ci_triggers", {:column=>"trigger_id", :name=>"fk_b8ec8b7245", :on_delete=>:cascade})
+ -> 0.0016s
+-- add_foreign_key("ci_triggers", "projects", {:name=>"fk_e3e63f966e", :on_delete=>:cascade})
+ -> 0.0021s
+-- add_foreign_key("ci_triggers", "users", {:column=>"owner_id", :name=>"fk_e8e10d1964", :on_delete=>:cascade})
+ -> 0.0019s
+-- add_foreign_key("ci_variables", "projects", {:name=>"fk_ada5eb64b3", :on_delete=>:cascade})
+ -> 0.0021s
+-- add_foreign_key("cluster_platforms_kubernetes", "clusters", {:on_delete=>:cascade})
+ -> 0.0019s
+-- add_foreign_key("cluster_projects", "clusters", {:on_delete=>:cascade})
+ -> 0.0018s
+-- add_foreign_key("cluster_projects", "projects", {:on_delete=>:cascade})
+ -> 0.0020s
+-- add_foreign_key("cluster_providers_gcp", "clusters", {:on_delete=>:cascade})
+ -> 0.0017s
+-- add_foreign_key("clusters", "users", {:on_delete=>:nullify})
+ -> 0.0018s
+-- add_foreign_key("clusters_applications_helm", "clusters", {:on_delete=>:cascade})
+ -> 0.0019s
+-- add_foreign_key("container_repositories", "projects")
+ -> 0.0020s
+-- add_foreign_key("deploy_keys_projects", "projects", {:name=>"fk_58a901ca7e", :on_delete=>:cascade})
+ -> 0.0019s
+-- add_foreign_key("deployments", "projects", {:name=>"fk_b9a3851b82", :on_delete=>:cascade})
+ -> 0.0021s
+-- add_foreign_key("environments", "projects", {:name=>"fk_d1c8c1da6a", :on_delete=>:cascade})
+ -> 0.0019s
+-- add_foreign_key("events", "projects", {:on_delete=>:cascade})
+ -> 0.0020s
+-- add_foreign_key("events", "users", {:column=>"author_id", :name=>"fk_edfd187b6f", :on_delete=>:cascade})
+ -> 0.0020s
+-- add_foreign_key("fork_network_members", "fork_networks", {:on_delete=>:cascade})
+ -> 0.0016s
+-- add_foreign_key("fork_network_members", "projects", {:column=>"forked_from_project_id", :name=>"fk_b01280dae4", :on_delete=>:nullify})
+ -> 0.0019s
+-- add_foreign_key("fork_network_members", "projects", {:on_delete=>:cascade})
+ -> 0.0018s
+-- add_foreign_key("fork_networks", "projects", {:column=>"root_project_id", :name=>"fk_e7b436b2b5", :on_delete=>:nullify})
+ -> 0.0018s
+-- add_foreign_key("forked_project_links", "projects", {:column=>"forked_to_project_id", :name=>"fk_434510edb0", :on_delete=>:cascade})
+ -> 0.0018s
+-- add_foreign_key("gcp_clusters", "projects", {:on_delete=>:cascade})
+ -> 0.0029s
+-- add_foreign_key("gcp_clusters", "services", {:on_delete=>:nullify})
+ -> 0.0022s
+-- add_foreign_key("gcp_clusters", "users", {:on_delete=>:nullify})
+ -> 0.0019s
+-- add_foreign_key("gpg_key_subkeys", "gpg_keys", {:on_delete=>:cascade})
+ -> 0.0017s
+-- add_foreign_key("gpg_keys", "users", {:on_delete=>:cascade})
+ -> 0.0019s
+-- add_foreign_key("gpg_signatures", "gpg_key_subkeys", {:on_delete=>:nullify})
+ -> 0.0016s
+-- add_foreign_key("gpg_signatures", "gpg_keys", {:on_delete=>:nullify})
+ -> 0.0016s
+-- add_foreign_key("gpg_signatures", "projects", {:on_delete=>:cascade})
+ -> 0.0016s
+-- add_foreign_key("group_custom_attributes", "namespaces", {:column=>"group_id", :on_delete=>:cascade})
+ -> 0.0014s
+-- add_foreign_key("issue_assignees", "issues", {:name=>"fk_b7d881734a", :on_delete=>:cascade})
+ -> 0.0019s
+-- add_foreign_key("issue_assignees", "users", {:name=>"fk_5e0c8d9154", :on_delete=>:cascade})
+ -> 0.0015s
+-- add_foreign_key("issue_metrics", "issues", {:on_delete=>:cascade})
+ -> 0.0016s
+-- add_foreign_key("issues", "issues", {:column=>"moved_to_id", :name=>"fk_a194299be1", :on_delete=>:nullify})
+ -> 0.0014s
+-- add_foreign_key("issues", "milestones", {:name=>"fk_96b1dd429c", :on_delete=>:nullify})
+ -> 0.0016s
+-- add_foreign_key("issues", "projects", {:name=>"fk_899c8f3231", :on_delete=>:cascade})
+ -> 0.0016s
+-- add_foreign_key("issues", "users", {:column=>"author_id", :name=>"fk_05f1e72feb", :on_delete=>:nullify})
+ -> 0.0015s
+-- add_foreign_key("issues", "users", {:column=>"updated_by_id", :name=>"fk_ffed080f01", :on_delete=>:nullify})
+ -> 0.0017s
+-- add_foreign_key("label_priorities", "labels", {:on_delete=>:cascade})
+ -> 0.0015s
+-- add_foreign_key("label_priorities", "projects", {:on_delete=>:cascade})
+ -> 0.0015s
+-- add_foreign_key("labels", "namespaces", {:column=>"group_id", :on_delete=>:cascade})
+ -> 0.0015s
+-- add_foreign_key("labels", "projects", {:name=>"fk_7de4989a69", :on_delete=>:cascade})
+ -> 0.0016s
+-- add_foreign_key("lists", "boards", {:name=>"fk_0d3f677137", :on_delete=>:cascade})
+ -> 0.0015s
+-- add_foreign_key("lists", "labels", {:name=>"fk_7a5553d60f", :on_delete=>:cascade})
+ -> 0.0014s
+-- add_foreign_key("members", "users", {:name=>"fk_2e88fb7ce9", :on_delete=>:cascade})
+ -> 0.0016s
+-- add_foreign_key("merge_request_diff_commits", "merge_request_diffs", {:on_delete=>:cascade})
+ -> 0.0014s
+-- add_foreign_key("merge_request_diff_files", "merge_request_diffs", {:on_delete=>:cascade})
+ -> 0.0014s
+-- add_foreign_key("merge_request_diffs", "merge_requests", {:name=>"fk_8483f3258f", :on_delete=>:cascade})
+ -> 0.0019s
+-- add_foreign_key("merge_request_metrics", "ci_pipelines", {:column=>"pipeline_id", :on_delete=>:cascade})
+ -> 0.0017s
+-- add_foreign_key("merge_request_metrics", "merge_requests", {:on_delete=>:cascade})
+ -> 0.0016s
+-- add_foreign_key("merge_request_metrics", "users", {:column=>"latest_closed_by_id", :name=>"fk_ae440388cc", :on_delete=>:nullify})
+ -> 0.0015s
+-- add_foreign_key("merge_request_metrics", "users", {:column=>"merged_by_id", :name=>"fk_7f28d925f3", :on_delete=>:nullify})
+ -> 0.0015s
+-- add_foreign_key("merge_requests", "ci_pipelines", {:column=>"head_pipeline_id", :name=>"fk_fd82eae0b9", :on_delete=>:nullify})
+ -> 0.0014s
+-- add_foreign_key("merge_requests", "merge_request_diffs", {:column=>"latest_merge_request_diff_id", :name=>"fk_06067f5644", :on_delete=>:nullify})
+ -> 0.0014s
+-- add_foreign_key("merge_requests", "milestones", {:name=>"fk_6a5165a692", :on_delete=>:nullify})
+ -> 0.0015s
+-- add_foreign_key("merge_requests", "projects", {:column=>"source_project_id", :name=>"fk_3308fe130c", :on_delete=>:nullify})
+ -> 0.0017s
+-- add_foreign_key("merge_requests", "projects", {:column=>"target_project_id", :name=>"fk_a6963e8447", :on_delete=>:cascade})
+ -> 0.0016s
+-- add_foreign_key("merge_requests", "users", {:column=>"assignee_id", :name=>"fk_6149611a04", :on_delete=>:nullify})
+ -> 0.0016s
+-- add_foreign_key("merge_requests", "users", {:column=>"author_id", :name=>"fk_e719a85f8a", :on_delete=>:nullify})
+ -> 0.0017s
+-- add_foreign_key("merge_requests", "users", {:column=>"merge_user_id", :name=>"fk_ad525e1f87", :on_delete=>:nullify})
+ -> 0.0018s
+-- add_foreign_key("merge_requests", "users", {:column=>"updated_by_id", :name=>"fk_641731faff", :on_delete=>:nullify})
+ -> 0.0017s
+-- add_foreign_key("merge_requests_closing_issues", "issues", {:on_delete=>:cascade})
+ -> 0.0016s
+-- add_foreign_key("merge_requests_closing_issues", "merge_requests", {:on_delete=>:cascade})
+ -> 0.0014s
+-- add_foreign_key("milestones", "namespaces", {:column=>"group_id", :name=>"fk_95650a40d4", :on_delete=>:cascade})
+ -> 0.0014s
+-- add_foreign_key("milestones", "projects", {:name=>"fk_9bd0a0c791", :on_delete=>:cascade})
+ -> 0.0017s
+-- add_foreign_key("notes", "projects", {:name=>"fk_99e097b079", :on_delete=>:cascade})
+ -> 0.0019s
+-- add_foreign_key("oauth_openid_requests", "oauth_access_grants", {:column=>"access_grant_id", :name=>"fk_oauth_openid_requests_oauth_access_grants_access_grant_id"})
+ -> 0.0014s
+-- add_foreign_key("pages_domains", "projects", {:name=>"fk_ea2f6dfc6f", :on_delete=>:cascade})
+ -> 0.0021s
+-- add_foreign_key("personal_access_tokens", "users")
+ -> 0.0016s
+-- add_foreign_key("project_authorizations", "projects", {:on_delete=>:cascade})
+ -> 0.0016s
+-- add_foreign_key("project_authorizations", "users", {:on_delete=>:cascade})
+ -> 0.0016s
+-- add_foreign_key("project_auto_devops", "projects", {:on_delete=>:cascade})
+ -> 0.0026s
+-- add_foreign_key("project_custom_attributes", "projects", {:on_delete=>:cascade})
+ -> 0.0016s
+-- add_foreign_key("project_features", "projects", {:name=>"fk_18513d9b92", :on_delete=>:cascade})
+ -> 0.0020s
+-- add_foreign_key("project_group_links", "projects", {:name=>"fk_daa8cee94c", :on_delete=>:cascade})
+ -> 0.0016s
+-- add_foreign_key("project_import_data", "projects", {:name=>"fk_ffb9ee3a10", :on_delete=>:cascade})
+ -> 0.0016s
+-- add_foreign_key("project_statistics", "projects", {:on_delete=>:cascade})
+ -> 0.0021s
+-- add_foreign_key("protected_branch_merge_access_levels", "protected_branches", {:name=>"fk_8a3072ccb3", :on_delete=>:cascade})
+ -> 0.0014s
+-- add_foreign_key("protected_branch_push_access_levels", "protected_branches", {:name=>"fk_9ffc86a3d9", :on_delete=>:cascade})
+ -> 0.0014s
+-- add_foreign_key("protected_branches", "projects", {:name=>"fk_7a9c6d93e7", :on_delete=>:cascade})
+ -> 0.0016s
+-- add_foreign_key("protected_tag_create_access_levels", "namespaces", {:column=>"group_id"})
+ -> 0.0016s
+-- add_foreign_key("protected_tag_create_access_levels", "protected_tags", {:name=>"fk_f7dfda8c51", :on_delete=>:cascade})
+ -> 0.0013s
+-- add_foreign_key("protected_tag_create_access_levels", "users")
+ -> 0.0018s
+-- add_foreign_key("protected_tags", "projects", {:name=>"fk_8e4af87648", :on_delete=>:cascade})
+ -> 0.0015s
+-- add_foreign_key("push_event_payloads", "events", {:name=>"fk_36c74129da", :on_delete=>:cascade})
+ -> 0.0013s
+-- add_foreign_key("releases", "projects", {:name=>"fk_47fe2a0596", :on_delete=>:cascade})
+ -> 0.0015s
+-- add_foreign_key("services", "projects", {:name=>"fk_71cce407f9", :on_delete=>:cascade})
+ -> 0.0015s
+-- add_foreign_key("snippets", "projects", {:name=>"fk_be41fd4bb7", :on_delete=>:cascade})
+ -> 0.0017s
+-- add_foreign_key("subscriptions", "projects", {:on_delete=>:cascade})
+ -> 0.0018s
+-- add_foreign_key("system_note_metadata", "notes", {:name=>"fk_d83a918cb1", :on_delete=>:cascade})
+ -> 0.0015s
+-- add_foreign_key("timelogs", "issues", {:name=>"fk_timelogs_issues_issue_id", :on_delete=>:cascade})
+ -> 0.0015s
+-- add_foreign_key("timelogs", "merge_requests", {:name=>"fk_timelogs_merge_requests_merge_request_id", :on_delete=>:cascade})
+ -> 0.0016s
+-- add_foreign_key("todos", "projects", {:name=>"fk_45054f9c45", :on_delete=>:cascade})
+ -> 0.0018s
+-- add_foreign_key("trending_projects", "projects", {:on_delete=>:cascade})
+ -> 0.0015s
+-- add_foreign_key("u2f_registrations", "users")
+ -> 0.0017s
+-- add_foreign_key("user_custom_attributes", "users", {:on_delete=>:cascade})
+ -> 0.0019s
+-- add_foreign_key("user_synced_attributes_metadata", "users", {:on_delete=>:cascade})
+ -> 0.0016s
+-- add_foreign_key("users_star_projects", "projects", {:name=>"fk_22cd27ddfc", :on_delete=>:cascade})
+ -> 0.0016s
+-- add_foreign_key("web_hook_logs", "web_hooks", {:on_delete=>:cascade})
+ -> 0.0014s
+-- add_foreign_key("web_hooks", "projects", {:name=>"fk_0c8ca6d9d1", :on_delete=>:cascade})
+ -> 0.0017s
+-- initialize_schema_migrations_table()
+ -> 0.0112s
+$ JOB_NAME=( $CI_JOB_NAME )
+$ export CI_NODE_INDEX=${JOB_NAME[-2]}
+$ export CI_NODE_TOTAL=${JOB_NAME[-1]}
+$ export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
+$ export KNAPSACK_GENERATE_REPORT=true
+$ export CACHE_CLASSES=true
+$ cp ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
+$ scripts/gitaly-test-spawn
+Gem.path: ["/root/.gem/ruby/2.3.0", "/usr/local/lib/ruby/gems/2.3.0", "/usr/local/bundle"]
+ENV['BUNDLE_GEMFILE']: nil
+ENV['RUBYOPT']: nil
+bundle config in /builds/gitlab-org/gitlab-ce
+scripts/gitaly-test-spawn:10:in `<main>': undefined local variable or method `gitaly_dir' for main:Object (NameError)
+Did you mean? gitaly_dir
+Settings are listed in order of priority. The top value will be used.
+retry
+Set for your local app (/usr/local/bundle/config): 3
+
+path
+Set for your local app (/usr/local/bundle/config): "vendor"
+Set via BUNDLE_PATH: "/usr/local/bundle"
+
+jobs
+Set for your local app (/usr/local/bundle/config): "2"
+
+clean
+Set for your local app (/usr/local/bundle/config): "true"
+
+without
+Set for your local app (/usr/local/bundle/config): [:production]
+
+silence_root_warning
+Set via BUNDLE_SILENCE_ROOT_WARNING: true
+
+app_config
+Set via BUNDLE_APP_CONFIG: "/usr/local/bundle"
+
+install_flags
+Set via BUNDLE_INSTALL_FLAGS: "--without=production --jobs=2 --path=vendor --retry=3 --quiet"
+
+bin
+Set via BUNDLE_BIN: "/usr/local/bundle/bin"
+
+gemfile
+Set via BUNDLE_GEMFILE: "/builds/gitlab-org/gitlab-ce/Gemfile"
+
+section_end:1517486961:build_script
+section_start:1517486961:after_script
+section_end:1517486962:after_script
+section_start:1517486962:upload_artifacts
+Uploading artifacts...
+WARNING: coverage/: no matching files 
+knapsack/: found 5 matching files 
+WARNING: tmp/capybara/: no matching files 
+Uploading artifacts to coordinator... ok  id=50551722 responseStatus=201 Created token=XkN753rp
+section_end:1517486963:upload_artifacts
+ERROR: Job failed: exit code 1
+ \ No newline at end of file
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 32432ee1e81..5f608fe18d9 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -105,7 +105,7 @@ describe GroupsHelper do
it 'outputs the groups in the correct order' do
expect(helper.group_title(very_deep_nested_group))
- .to match(/<li style="text-indent: 16px;"><a.*>#{deep_nested_group.name}.*<\/li>.*<a.*>#{very_deep_nested_group.name}<\/a>/m)
+ .to match(%r{<li style="text-indent: 16px;"><a.*>#{deep_nested_group.name}.*</li>.*<a.*>#{very_deep_nested_group.name}</a>}m)
end
end
@@ -120,7 +120,7 @@ describe GroupsHelper do
let(:possible_help_texts) do
{
default_help: "This setting will be applied to all subgroups unless overridden by a group owner",
- ancestor_locked_but_you_can_override: /This setting is applied on <a .+>.+<\/a>\. You can override the setting or .+/,
+ ancestor_locked_but_you_can_override: %r{This setting is applied on <a .+>.+</a>\. You can override the setting or .+},
ancestor_locked_so_ask_the_owner: /This setting is applied on .+\. To share projects in this group with another group, ask the owner to override the setting or remove the share with group lock from .+/,
ancestor_locked_and_has_been_overridden: /This setting is applied on .+ and has been overridden on this subgroup/
}
diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb
index 0286d36952c..619baa78bfa 100644
--- a/spec/helpers/labels_helper_spec.rb
+++ b/spec/helpers/labels_helper_spec.rb
@@ -104,7 +104,7 @@ describe LabelsHelper do
context 'with a tooltip argument' do
context 'set to false' do
it 'does not include the has-tooltip class' do
- expect(link_to_label(label, tooltip: false)).not_to match %r{has-tooltip}
+ expect(link_to_label(label, tooltip: false)).not_to match /has-tooltip/
end
end
end
diff --git a/spec/helpers/user_callouts_helper_spec.rb b/spec/helpers/user_callouts_helper_spec.rb
new file mode 100644
index 00000000000..27455705d23
--- /dev/null
+++ b/spec/helpers/user_callouts_helper_spec.rb
@@ -0,0 +1,47 @@
+require "spec_helper"
+
+describe UserCalloutsHelper do
+ let(:user) { create(:user) }
+
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ end
+
+ describe '.show_gke_cluster_integration_callout?' do
+ let(:project) { create(:project) }
+
+ subject { helper.show_gke_cluster_integration_callout?(project) }
+
+ context 'when user can create a cluster' do
+ before do
+ allow(helper).to receive(:can?).with(anything, :create_cluster, anything)
+ .and_return(true)
+ end
+
+ context 'when user has not dismissed' do
+ before do
+ allow(helper).to receive(:user_dismissed?).and_return(false)
+ end
+
+ it { is_expected.to be true }
+ end
+
+ context 'when user dismissed' do
+ before do
+ allow(helper).to receive(:user_dismissed?).and_return(true)
+ end
+
+ it { is_expected.to be false }
+ end
+ end
+
+ context 'when user can not create a cluster' do
+ before do
+ allow(helper).to receive(:can?).with(anything, :create_cluster, anything)
+ .and_return(false)
+ end
+
+ it { is_expected.to be false }
+ end
+ end
+end
diff --git a/spec/helpers/version_check_helper_spec.rb b/spec/helpers/version_check_helper_spec.rb
index fa8cfda3b86..9d4e34abef5 100644
--- a/spec/helpers/version_check_helper_spec.rb
+++ b/spec/helpers/version_check_helper_spec.rb
@@ -4,7 +4,7 @@ describe VersionCheckHelper do
describe '#version_status_badge' do
it 'should return nil if not dev environment and not enabled' do
allow(Rails.env).to receive(:production?) { false }
- allow(helper.current_application_settings).to receive(:version_check_enabled) { false }
+ allow(Gitlab::CurrentSettings.current_application_settings).to receive(:version_check_enabled) { false }
expect(helper.version_status_badge).to be(nil)
end
@@ -12,7 +12,7 @@ describe VersionCheckHelper do
context 'when production and enabled' do
before do
allow(Rails.env).to receive(:production?) { true }
- allow(helper.current_application_settings).to receive(:version_check_enabled) { true }
+ allow(Gitlab::CurrentSettings.current_application_settings).to receive(:version_check_enabled) { true }
allow_any_instance_of(VersionCheck).to receive(:url) { 'https://version.host.com/check.svg?gitlab_info=xxx' }
@image_tag = helper.version_status_badge
@@ -27,7 +27,7 @@ describe VersionCheckHelper do
end
it 'should have a VersionCheck url as the src' do
- expect(@image_tag).to match(/src="https:\/\/version\.host\.com\/check\.svg\?gitlab_info=xxx"/)
+ expect(@image_tag).to match(%r{src="https://version\.host\.com/check\.svg\?gitlab_info=xxx"})
end
end
end
diff --git a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js
new file mode 100644
index 00000000000..0170ab458d4
--- /dev/null
+++ b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js
@@ -0,0 +1,163 @@
+import VariableList from '~/ci_variable_list/ci_variable_list';
+import getSetTimeoutPromise from '../helpers/set_timeout_promise_helper';
+
+describe('VariableList', () => {
+ preloadFixtures('pipeline_schedules/edit.html.raw');
+ preloadFixtures('pipeline_schedules/edit_with_variables.html.raw');
+
+ let $wrapper;
+ let variableList;
+
+ describe('with only key/value inputs', () => {
+ describe('with no variables', () => {
+ beforeEach(() => {
+ loadFixtures('pipeline_schedules/edit.html.raw');
+ $wrapper = $('.js-ci-variable-list-section');
+
+ variableList = new VariableList({
+ container: $wrapper,
+ formField: 'schedule',
+ });
+ variableList.init();
+ });
+
+ it('should remove the row when clicking the remove button', () => {
+ $wrapper.find('.js-row-remove-button').trigger('click');
+
+ expect($wrapper.find('.js-row').length).toBe(0);
+ });
+
+ it('should add another row when editing the last rows key input', () => {
+ const $row = $wrapper.find('.js-row');
+ $row.find('.js-ci-variable-input-key')
+ .val('foo')
+ .trigger('input');
+
+ expect($wrapper.find('.js-row').length).toBe(2);
+
+ // Check for the correct default in the new row
+ const $keyInput = $wrapper.find('.js-row:last-child').find('.js-ci-variable-input-key');
+ expect($keyInput.val()).toBe('');
+ });
+
+ it('should add another row when editing the last rows value textarea', () => {
+ const $row = $wrapper.find('.js-row');
+ $row.find('.js-ci-variable-input-value')
+ .val('foo')
+ .trigger('input');
+
+ expect($wrapper.find('.js-row').length).toBe(2);
+
+ // Check for the correct default in the new row
+ const $valueInput = $wrapper.find('.js-row:last-child').find('.js-ci-variable-input-key');
+ expect($valueInput.val()).toBe('');
+ });
+
+ it('should remove empty row after blurring', () => {
+ const $row = $wrapper.find('.js-row');
+ $row.find('.js-ci-variable-input-key')
+ .val('foo')
+ .trigger('input');
+
+ expect($wrapper.find('.js-row').length).toBe(2);
+
+ $row.find('.js-ci-variable-input-key')
+ .val('')
+ .trigger('input')
+ .trigger('blur');
+
+ expect($wrapper.find('.js-row').length).toBe(1);
+ });
+ });
+
+ describe('with persisted variables', () => {
+ beforeEach(() => {
+ loadFixtures('pipeline_schedules/edit_with_variables.html.raw');
+ $wrapper = $('.js-ci-variable-list-section');
+
+ variableList = new VariableList({
+ container: $wrapper,
+ formField: 'schedule',
+ });
+ variableList.init();
+ });
+
+ it('should have "Reveal values" button initially when there are already variables', () => {
+ expect($wrapper.find('.js-secret-value-reveal-button').text()).toBe('Reveal values');
+ });
+
+ it('should reveal hidden values', () => {
+ const $row = $wrapper.find('.js-row:first-child');
+ const $inputValue = $row.find('.js-ci-variable-input-value');
+ const $placeholder = $row.find('.js-secret-value-placeholder');
+
+ expect($placeholder.hasClass('hide')).toBe(false);
+ expect($inputValue.hasClass('hide')).toBe(true);
+
+ // Reveal values
+ $wrapper.find('.js-secret-value-reveal-button').click();
+
+ expect($placeholder.hasClass('hide')).toBe(true);
+ expect($inputValue.hasClass('hide')).toBe(false);
+ });
+ });
+ });
+
+ describe('with all inputs(key, value, protected)', () => {
+ beforeEach(() => {
+ // This markup will be replaced with a fixture when we can render the
+ // CI/CD settings page with the new dynamic variable list in https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/4110
+ $wrapper = $(`<form class="js-variable-list">
+ <ul>
+ <li class="js-row">
+ <div class="ci-variable-body-item">
+ <input class="js-ci-variable-input-key" name="variables[variables_attributes][][key]">
+ </div>
+
+ <div class="ci-variable-body-item">
+ <textarea class="js-ci-variable-input-value" name="variables[variables_attributes][][value]"></textarea>
+ </div>
+
+ <div class="ci-variable-body-item ci-variable-protected-item">
+ <button type="button" class="js-project-feature-toggle project-feature-toggle">
+ <input
+ type="hidden"
+ class="js-ci-variable-input-protected js-project-feature-toggle-input"
+ name="variables[variables_attributes][][protected]"
+ value="true"
+ />
+ </button>
+ </div>
+
+ <button type="button" class="js-row-remove-button"></button>
+ </li>
+ </ul>
+ <button type="button" class="js-secret-value-reveal-button">
+ Reveal values
+ </button>
+ </form>`);
+
+ variableList = new VariableList({
+ container: $wrapper,
+ formField: 'variables',
+ });
+ variableList.init();
+ });
+
+ it('should add another row when editing the last rows protected checkbox', (done) => {
+ const $row = $wrapper.find('.js-row:last-child');
+ $row.find('.ci-variable-protected-item .js-project-feature-toggle').click();
+
+ getSetTimeoutPromise()
+ .then(() => {
+ expect($wrapper.find('.js-row').length).toBe(2);
+
+ // Check for the correct default in the new row
+ const $protectedInput = $wrapper.find('.js-row:last-child').find('.js-ci-variable-input-protected');
+ expect($protectedInput.val()).toBe('true');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/ci_variable_list/native_form_variable_list_spec.js b/spec/javascripts/ci_variable_list/native_form_variable_list_spec.js
new file mode 100644
index 00000000000..eb508a7f059
--- /dev/null
+++ b/spec/javascripts/ci_variable_list/native_form_variable_list_spec.js
@@ -0,0 +1,30 @@
+import setupNativeFormVariableList from '~/ci_variable_list/native_form_variable_list';
+
+describe('NativeFormVariableList', () => {
+ preloadFixtures('pipeline_schedules/edit.html.raw');
+
+ let $wrapper;
+
+ beforeEach(() => {
+ loadFixtures('pipeline_schedules/edit.html.raw');
+ $wrapper = $('.js-ci-variable-list-section');
+
+ setupNativeFormVariableList({
+ container: $wrapper,
+ formField: 'schedule',
+ });
+ });
+
+ describe('onFormSubmit', () => {
+ it('should clear out the `name` attribute on the inputs for the last empty row on form submission (avoid BE validation)', () => {
+ const $row = $wrapper.find('.js-row');
+ expect($row.find('.js-ci-variable-input-key').attr('name')).toBe('schedule[variables_attributes][][key]');
+ expect($row.find('.js-ci-variable-input-value').attr('name')).toBe('schedule[variables_attributes][][value]');
+
+ $wrapper.closest('form').trigger('trigger-submit');
+
+ expect($row.find('.js-ci-variable-input-key').attr('name')).toBe('');
+ expect($row.find('.js-ci-variable-input-value').attr('name')).toBe('');
+ });
+ });
+});
diff --git a/spec/javascripts/clusters/clusters_bundle_spec.js b/spec/javascripts/clusters/clusters_bundle_spec.js
index 7b38f6b7855..a9e244e523d 100644
--- a/spec/javascripts/clusters/clusters_bundle_spec.js
+++ b/spec/javascripts/clusters/clusters_bundle_spec.js
@@ -71,7 +71,8 @@ describe('Clusters', () => {
helm: { status: APPLICATION_INSTALLABLE, title: 'Helm Tiller' },
});
- expect(document.querySelector('.js-cluster-application-notice .flash-text')).toBeNull();
+ const flashMessage = document.querySelector('.js-cluster-application-notice .flash-text');
+ expect(flashMessage).toBeNull();
});
it('shows an alert when something gets newly installed', () => {
@@ -83,8 +84,9 @@ describe('Clusters', () => {
helm: { status: APPLICATION_INSTALLED, title: 'Helm Tiller' },
});
- expect(document.querySelector('.js-cluster-application-notice .flash-text')).toBeDefined();
- expect(document.querySelector('.js-cluster-application-notice .flash-text').textContent.trim()).toEqual('Helm Tiller was successfully installed on your cluster');
+ const flashMessage = document.querySelector('.js-cluster-application-notice .flash-text');
+ expect(flashMessage).not.toBeNull();
+ expect(flashMessage.textContent.trim()).toEqual('Helm Tiller was successfully installed on your Kubernetes cluster');
});
it('shows an alert when multiple things gets newly installed', () => {
@@ -98,8 +100,9 @@ describe('Clusters', () => {
ingress: { status: APPLICATION_INSTALLED, title: 'Ingress' },
});
- expect(document.querySelector('.js-cluster-application-notice .flash-text')).toBeDefined();
- expect(document.querySelector('.js-cluster-application-notice .flash-text').textContent.trim()).toEqual('Helm Tiller, Ingress was successfully installed on your cluster');
+ const flashMessage = document.querySelector('.js-cluster-application-notice .flash-text');
+ expect(flashMessage).not.toBeNull();
+ expect(flashMessage.textContent.trim()).toEqual('Helm Tiller, Ingress was successfully installed on your Kubernetes cluster');
});
});
diff --git a/spec/javascripts/clusters/stores/clusters_store_spec.js b/spec/javascripts/clusters/stores/clusters_store_spec.js
index ec2889355e6..726a4ed30de 100644
--- a/spec/javascripts/clusters/stores/clusters_store_spec.js
+++ b/spec/javascripts/clusters/stores/clusters_store_spec.js
@@ -58,6 +58,7 @@ describe('Clusters Store', () => {
expect(store.state).toEqual({
helpPath: null,
+ ingressHelpPath: null,
status: mockResponseData.status,
statusReason: mockResponseData.status_reason,
applications: {
diff --git a/spec/javascripts/collapsed_sidebar_todo_spec.js b/spec/javascripts/collapsed_sidebar_todo_spec.js
index 5026eaafaca..2abf52a1676 100644
--- a/spec/javascripts/collapsed_sidebar_todo_spec.js
+++ b/spec/javascripts/collapsed_sidebar_todo_spec.js
@@ -1,10 +1,14 @@
/* eslint-disable no-new */
import _ from 'underscore';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import Sidebar from '~/right_sidebar';
+import timeoutPromise from './helpers/set_timeout_promise_helper';
describe('Issuable right sidebar collapsed todo toggle', () => {
const fixtureName = 'issues/open-issue.html.raw';
const jsonFixtureName = 'todos/todos.json';
+ let mock;
preloadFixtures(fixtureName);
preloadFixtures(jsonFixtureName);
@@ -19,19 +23,26 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
document.querySelector('.js-right-sidebar')
.classList.toggle('right-sidebar-collapsed');
- spyOn(jQuery, 'ajax').and.callFake((res) => {
- const d = $.Deferred();
+ mock = new MockAdapter(axios);
+
+ mock.onPost(`${gl.TEST_HOST}/frontend-fixtures/issues-project/todos`).reply(() => {
const response = _.clone(todoData);
- if (res.type === 'DELETE') {
- delete response.delete_path;
- }
+ return [200, response];
+ });
- d.resolve(response);
- return d.promise();
+ mock.onDelete(/(.*)\/dashboard\/todos\/\d+$/).reply(() => {
+ const response = _.clone(todoData);
+ delete response.delete_path;
+
+ return [200, response];
});
});
+ afterEach(() => {
+ mock.restore();
+ });
+
it('shows add todo button', () => {
expect(
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon'),
@@ -52,71 +63,101 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
).toBe('Add todo');
});
- it('toggle todo state', () => {
+ it('toggle todo state', (done) => {
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
- expect(
- document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'),
- ).not.toBeNull();
+ setTimeout(() => {
+ expect(
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'),
+ ).not.toBeNull();
- expect(
- document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .fa-check-square'),
- ).not.toBeNull();
- });
+ expect(
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .fa-check-square'),
+ ).not.toBeNull();
- it('toggle todo state of expanded todo toggle', () => {
- document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
-
- expect(
- document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(),
- ).toBe('Mark done');
+ done();
+ });
});
- it('toggles todo button tooltip', () => {
+ it('toggle todo state of expanded todo toggle', (done) => {
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
- expect(
- document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('data-original-title'),
- ).toBe('Mark done');
- });
-
- it('marks todo as done', () => {
- document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
+ setTimeout(() => {
+ expect(
+ document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(),
+ ).toBe('Mark done');
- expect(
- document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'),
- ).not.toBeNull();
+ done();
+ });
+ });
+ it('toggles todo button tooltip', (done) => {
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
- expect(
- document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'),
- ).toBeNull();
+ setTimeout(() => {
+ expect(
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('data-original-title'),
+ ).toBe('Mark done');
- expect(
- document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(),
- ).toBe('Add todo');
+ done();
+ });
});
- it('updates aria-label to mark done', () => {
+ it('marks todo as done', (done) => {
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
- expect(
- document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'),
- ).toBe('Mark done');
+ timeoutPromise()
+ .then(() => {
+ expect(
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'),
+ ).not.toBeNull();
+
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
+ })
+ .then(timeoutPromise)
+ .then(() => {
+ expect(
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'),
+ ).toBeNull();
+
+ expect(
+ document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(),
+ ).toBe('Add todo');
+ })
+ .then(done)
+ .catch(done.fail);
});
- it('updates aria-label to add todo', () => {
+ it('updates aria-label to mark done', (done) => {
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
- expect(
- document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'),
- ).toBe('Mark done');
+ setTimeout(() => {
+ expect(
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'),
+ ).toBe('Mark done');
+ done();
+ });
+ });
+
+ it('updates aria-label to add todo', (done) => {
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
- expect(
- document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'),
- ).toBe('Add todo');
+ timeoutPromise()
+ .then(() => {
+ expect(
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'),
+ ).toBe('Mark done');
+
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
+ })
+ .then(timeoutPromise)
+ .then(() => {
+ expect(
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'),
+ ).toBe('Add todo');
+ })
+ .then(done)
+ .catch(done.fail);
});
});
diff --git a/spec/javascripts/commit/commit_pipeline_status_component_spec.js b/spec/javascripts/commit/commit_pipeline_status_component_spec.js
new file mode 100644
index 00000000000..90f290e845e
--- /dev/null
+++ b/spec/javascripts/commit/commit_pipeline_status_component_spec.js
@@ -0,0 +1,104 @@
+import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
+import mountComponent from '../helpers/vue_mount_component_helper';
+
+describe('Commit pipeline status component', () => {
+ let vm;
+ let Component;
+ let mock;
+ const mockCiStatus = {
+ details_path: '/root/hello-world/pipelines/1',
+ favicon: 'canceled.ico',
+ group: 'canceled',
+ has_details: true,
+ icon: 'status_canceled',
+ label: 'canceled',
+ text: 'canceled',
+ };
+
+ beforeEach(() => {
+ Component = Vue.extend(commitPipelineStatus);
+ });
+
+ describe('While polling pipeline data succesfully', () => {
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mock.onGet('/dummy/endpoint').reply(() => {
+ const res = Promise.resolve([200, {
+ pipelines: [
+ {
+ details: {
+ status: mockCiStatus,
+ },
+ },
+ ],
+ }]);
+ return res;
+ });
+ vm = mountComponent(Component, {
+ endpoint: '/dummy/endpoint',
+ });
+ });
+
+ afterEach(() => {
+ vm.poll.stop();
+ vm.$destroy();
+ mock.restore();
+ });
+
+ it('shows the loading icon when polling is starting', (done) => {
+ expect(vm.$el.querySelector('.loading-container')).not.toBe(null);
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.loading-container')).toBe(null);
+ done();
+ });
+ });
+
+ it('contains a ciStatus when the polling is succesful ', (done) => {
+ setTimeout(() => {
+ expect(vm.ciStatus).toEqual(mockCiStatus);
+ done();
+ });
+ });
+
+ it('contains a ci-status icon when polling is succesful', (done) => {
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.ci-status-icon')).not.toBe(null);
+ expect(vm.$el.querySelector('.ci-status-icon').classList).toContain(`ci-status-icon-${mockCiStatus.group}`);
+ done();
+ });
+ });
+ });
+
+ describe('When polling data was not succesful', () => {
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mock.onGet('/dummy/endpoint').reply(() => {
+ const res = Promise.reject([502, { }]);
+ return res;
+ });
+ vm = new Component({
+ props: {
+ endpoint: '/dummy/endpoint',
+ },
+ });
+ });
+
+ afterEach(() => {
+ vm.poll.stop();
+ vm.$destroy();
+ mock.restore();
+ });
+
+ it('calls an errorCallback', (done) => {
+ spyOn(vm, 'errorCallback').and.callThrough();
+ vm.$mount();
+ setTimeout(() => {
+ expect(vm.errorCallback.calls.count()).toEqual(1);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js b/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js
new file mode 100644
index 00000000000..34ffc7b1016
--- /dev/null
+++ b/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js
@@ -0,0 +1,231 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import {
+ getSelector,
+ togglePopover,
+ dismiss,
+ mouseleave,
+ mouseenter,
+ inserted,
+} from '~/feature_highlight/feature_highlight_helper';
+import getSetTimeoutPromise from '../helpers/set_timeout_promise_helper';
+
+describe('feature highlight helper', () => {
+ describe('getSelector', () => {
+ it('returns js-feature-highlight selector', () => {
+ const highlightId = 'highlightId';
+ expect(getSelector(highlightId)).toEqual(`.js-feature-highlight[data-highlight=${highlightId}]`);
+ });
+ });
+
+ describe('togglePopover', () => {
+ describe('togglePopover(true)', () => {
+ it('returns true when popover is shown', () => {
+ const context = {
+ hasClass: () => false,
+ popover: () => {},
+ toggleClass: () => {},
+ };
+
+ expect(togglePopover.call(context, true)).toEqual(true);
+ });
+
+ it('returns false when popover is already shown', () => {
+ const context = {
+ hasClass: () => true,
+ };
+
+ expect(togglePopover.call(context, true)).toEqual(false);
+ });
+
+ it('shows popover', (done) => {
+ const context = {
+ hasClass: () => false,
+ popover: () => {},
+ toggleClass: () => {},
+ };
+
+ spyOn(context, 'popover').and.callFake((method) => {
+ expect(method).toEqual('show');
+ done();
+ });
+
+ togglePopover.call(context, true);
+ });
+
+ it('adds disable-animation and js-popover-show class', (done) => {
+ const context = {
+ hasClass: () => false,
+ popover: () => {},
+ toggleClass: () => {},
+ };
+
+ spyOn(context, 'toggleClass').and.callFake((classNames, show) => {
+ expect(classNames).toEqual('disable-animation js-popover-show');
+ expect(show).toEqual(true);
+ done();
+ });
+
+ togglePopover.call(context, true);
+ });
+ });
+
+ describe('togglePopover(false)', () => {
+ it('returns true when popover is hidden', () => {
+ const context = {
+ hasClass: () => true,
+ popover: () => {},
+ toggleClass: () => {},
+ };
+
+ expect(togglePopover.call(context, false)).toEqual(true);
+ });
+
+ it('returns false when popover is already hidden', () => {
+ const context = {
+ hasClass: () => false,
+ };
+
+ expect(togglePopover.call(context, false)).toEqual(false);
+ });
+
+ it('hides popover', (done) => {
+ const context = {
+ hasClass: () => true,
+ popover: () => {},
+ toggleClass: () => {},
+ };
+
+ spyOn(context, 'popover').and.callFake((method) => {
+ expect(method).toEqual('hide');
+ done();
+ });
+
+ togglePopover.call(context, false);
+ });
+
+ it('removes disable-animation and js-popover-show class', (done) => {
+ const context = {
+ hasClass: () => true,
+ popover: () => {},
+ toggleClass: () => {},
+ };
+
+ spyOn(context, 'toggleClass').and.callFake((classNames, show) => {
+ expect(classNames).toEqual('disable-animation js-popover-show');
+ expect(show).toEqual(false);
+ done();
+ });
+
+ togglePopover.call(context, false);
+ });
+ });
+ });
+
+ describe('dismiss', () => {
+ let mock;
+ const context = {
+ hide: () => {},
+ attr: () => '/-/callouts/dismiss',
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+
+ spyOn(togglePopover, 'call').and.callFake(() => {});
+ spyOn(context, 'hide').and.callFake(() => {});
+ dismiss.call(context);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('calls persistent dismissal endpoint', (done) => {
+ const spy = jasmine.createSpy('dismiss-endpoint-hit');
+ mock.onPost('/-/callouts/dismiss').reply(spy);
+
+ getSetTimeoutPromise()
+ .then(() => {
+ expect(spy).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('calls hide popover', () => {
+ expect(togglePopover.call).toHaveBeenCalledWith(context, false);
+ });
+
+ it('calls hide', () => {
+ expect(context.hide).toHaveBeenCalled();
+ });
+ });
+
+ describe('mouseleave', () => {
+ it('calls hide popover if .popover:hover is false', () => {
+ const fakeJquery = {
+ length: 0,
+ };
+
+ spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn));
+ spyOn(togglePopover, 'call');
+ mouseleave();
+ expect(togglePopover.call).toHaveBeenCalledWith(jasmine.any(Object), false);
+ });
+
+ it('does not call hide popover if .popover:hover is true', () => {
+ const fakeJquery = {
+ length: 1,
+ };
+
+ spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn));
+ spyOn(togglePopover, 'call');
+ mouseleave();
+ expect(togglePopover.call).not.toHaveBeenCalledWith(false);
+ });
+ });
+
+ describe('mouseenter', () => {
+ const context = {};
+
+ it('shows popover', () => {
+ spyOn(togglePopover, 'call').and.returnValue(false);
+ mouseenter.call(context);
+ expect(togglePopover.call).toHaveBeenCalledWith(jasmine.any(Object), true);
+ });
+
+ it('registers mouseleave event if popover is showed', (done) => {
+ spyOn(togglePopover, 'call').and.returnValue(true);
+ spyOn($.fn, 'on').and.callFake((eventName) => {
+ expect(eventName).toEqual('mouseleave');
+ done();
+ });
+ mouseenter.call(context);
+ });
+
+ it('does not register mouseleave event if popover is not showed', () => {
+ spyOn(togglePopover, 'call').and.returnValue(false);
+ const spy = spyOn($.fn, 'on').and.callFake(() => {});
+ mouseenter.call(context);
+ expect(spy).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('inserted', () => {
+ it('registers click event callback', (done) => {
+ const context = {
+ getAttribute: () => 'popoverId',
+ dataset: {
+ highlight: 'some-feature',
+ },
+ };
+
+ spyOn($.fn, 'on').and.callFake((event) => {
+ expect(event).toEqual('click');
+ done();
+ });
+ inserted.call(context);
+ });
+ });
+});
diff --git a/spec/javascripts/feature_highlight/feature_highlight_options_spec.js b/spec/javascripts/feature_highlight/feature_highlight_options_spec.js
new file mode 100644
index 00000000000..7f9425d8abe
--- /dev/null
+++ b/spec/javascripts/feature_highlight/feature_highlight_options_spec.js
@@ -0,0 +1,30 @@
+import domContentLoaded from '~/feature_highlight/feature_highlight_options';
+import bp from '~/breakpoints';
+
+describe('feature highlight options', () => {
+ describe('domContentLoaded', () => {
+ it('should not call highlightFeatures when breakpoint is xs', () => {
+ spyOn(bp, 'getBreakpointSize').and.returnValue('xs');
+
+ expect(domContentLoaded()).toBe(false);
+ });
+
+ it('should not call highlightFeatures when breakpoint is sm', () => {
+ spyOn(bp, 'getBreakpointSize').and.returnValue('sm');
+
+ expect(domContentLoaded()).toBe(false);
+ });
+
+ it('should not call highlightFeatures when breakpoint is md', () => {
+ spyOn(bp, 'getBreakpointSize').and.returnValue('md');
+
+ expect(domContentLoaded()).toBe(false);
+ });
+
+ it('should call highlightFeatures when breakpoint is lg', () => {
+ spyOn(bp, 'getBreakpointSize').and.returnValue('lg');
+
+ expect(domContentLoaded()).toBe(true);
+ });
+ });
+});
diff --git a/spec/javascripts/feature_highlight/feature_highlight_spec.js b/spec/javascripts/feature_highlight/feature_highlight_spec.js
new file mode 100644
index 00000000000..6e1b0429ab7
--- /dev/null
+++ b/spec/javascripts/feature_highlight/feature_highlight_spec.js
@@ -0,0 +1,131 @@
+import * as featureHighlightHelper from '~/feature_highlight/feature_highlight_helper';
+import * as featureHighlight from '~/feature_highlight/feature_highlight';
+
+describe('feature highlight', () => {
+ beforeEach(() => {
+ setFixtures(`
+ <div>
+ <div class="js-feature-highlight" data-highlight="test" data-highlight-priority="10" disabled>
+ Trigger
+ </div>
+ </div>
+ <div class="feature-highlight-popover-content">
+ Content
+ <div class="dismiss-feature-highlight">
+ Dismiss
+ </div>
+ </div>
+ `);
+ });
+
+ describe('setupFeatureHighlightPopover', () => {
+ const selector = '.js-feature-highlight[data-highlight=test]';
+ beforeEach(() => {
+ spyOn(window, 'addEventListener');
+ spyOn(window, 'removeEventListener');
+ featureHighlight.setupFeatureHighlightPopover('test', 0);
+ });
+
+ it('setup popover content', () => {
+ const $popoverContent = $('.feature-highlight-popover-content');
+ const outerHTML = $popoverContent.prop('outerHTML');
+
+ expect($(selector).data('content')).toEqual(outerHTML);
+ });
+
+ it('setup mouseenter', () => {
+ const toggleSpy = spyOn(featureHighlightHelper.togglePopover, 'call');
+ $(selector).trigger('mouseenter');
+
+ expect(toggleSpy).toHaveBeenCalledWith(jasmine.any(Object), true);
+ });
+
+ it('setup debounced mouseleave', (done) => {
+ const toggleSpy = spyOn(featureHighlightHelper.togglePopover, 'call');
+ $(selector).trigger('mouseleave');
+
+ // Even though we've set the debounce to 0ms, setTimeout is needed for the debounce
+ setTimeout(() => {
+ expect(toggleSpy).toHaveBeenCalledWith(jasmine.any(Object), false);
+ done();
+ }, 0);
+ });
+
+ it('setup inserted.bs.popover', () => {
+ $(selector).trigger('mouseenter');
+ const popoverId = $(selector).attr('aria-describedby');
+ const spyEvent = spyOnEvent(`#${popoverId} .dismiss-feature-highlight`, 'click');
+
+ $(`#${popoverId} .dismiss-feature-highlight`).click();
+ expect(spyEvent).toHaveBeenTriggered();
+ });
+
+ it('setup show.bs.popover', () => {
+ $(selector).trigger('show.bs.popover');
+ expect(window.addEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function));
+ });
+
+ it('setup hide.bs.popover', () => {
+ $(selector).trigger('hide.bs.popover');
+ expect(window.removeEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function));
+ });
+
+ it('removes disabled attribute', () => {
+ expect($('.js-feature-highlight').is(':disabled')).toEqual(false);
+ });
+
+ it('displays popover', () => {
+ expect($(selector).attr('aria-describedby')).toBeFalsy();
+ $(selector).trigger('mouseenter');
+ expect($(selector).attr('aria-describedby')).toBeTruthy();
+ });
+ });
+
+ describe('findHighestPriorityFeature', () => {
+ beforeEach(() => {
+ setFixtures(`
+ <div class="js-feature-highlight" data-highlight="test" data-highlight-priority="10" disabled></div>
+ <div class="js-feature-highlight" data-highlight="test-high-priority" data-highlight-priority="20" disabled></div>
+ <div class="js-feature-highlight" data-highlight="test-low-priority" data-highlight-priority="0" disabled></div>
+ `);
+ });
+
+ it('should pick the highest priority feature highlight', () => {
+ setFixtures(`
+ <div class="js-feature-highlight" data-highlight="test" data-highlight-priority="10" disabled></div>
+ <div class="js-feature-highlight" data-highlight="test-high-priority" data-highlight-priority="20" disabled></div>
+ <div class="js-feature-highlight" data-highlight="test-low-priority" data-highlight-priority="0" disabled></div>
+ `);
+
+ expect($('.js-feature-highlight').length).toBeGreaterThan(1);
+ expect(featureHighlight.findHighestPriorityFeature()).toEqual('test-high-priority');
+ });
+
+ it('should work when no priority is set', () => {
+ setFixtures(`
+ <div class="js-feature-highlight" data-highlight="test" disabled></div>
+ `);
+
+ expect(featureHighlight.findHighestPriorityFeature()).toEqual('test');
+ });
+
+ it('should pick the highest priority feature highlight when some have no priority set', () => {
+ setFixtures(`
+ <div class="js-feature-highlight" data-highlight="test-no-priority1" disabled></div>
+ <div class="js-feature-highlight" data-highlight="test" data-highlight-priority="10" disabled></div>
+ <div class="js-feature-highlight" data-highlight="test-no-priority2" disabled></div>
+ <div class="js-feature-highlight" data-highlight="test-high-priority" data-highlight-priority="20" disabled></div>
+ <div class="js-feature-highlight" data-highlight="test-low-priority" data-highlight-priority="0" disabled></div>
+ `);
+
+ expect($('.js-feature-highlight').length).toBeGreaterThan(1);
+ expect(featureHighlight.findHighestPriorityFeature()).toEqual('test-high-priority');
+ });
+ });
+
+ describe('highlightFeatures', () => {
+ it('calls setupFeatureHighlightPopover', () => {
+ expect(featureHighlight.highlightFeatures()).toEqual('test');
+ });
+ });
+});
diff --git a/spec/javascripts/fixtures/jobs.rb b/spec/javascripts/fixtures/jobs.rb
index 87d131dfe28..6d5c6d5334f 100644
--- a/spec/javascripts/fixtures/jobs.rb
+++ b/spec/javascripts/fixtures/jobs.rb
@@ -7,7 +7,7 @@ describe Projects::JobsController, '(JavaScript fixtures)', type: :controller do
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project_empty_repo, namespace: namespace, path: 'builds-project') }
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
- let!(:build_with_artifacts) { create(:ci_build, :success, :artifacts, :trace, pipeline: pipeline, stage: 'test', artifacts_expire_at: Time.now + 18.months) }
+ let!(:build_with_artifacts) { create(:ci_build, :success, :artifacts, :trace_artifact, pipeline: pipeline, stage: 'test', artifacts_expire_at: Time.now + 18.months) }
let!(:failed_build) { create(:ci_build, :failed, pipeline: pipeline, stage: 'build') }
let!(:pending_build) { create(:ci_build, :pending, pipeline: pipeline, stage: 'deploy') }
diff --git a/spec/javascripts/fixtures/pipeline_schedules.rb b/spec/javascripts/fixtures/pipeline_schedules.rb
new file mode 100644
index 00000000000..56f27ea7df1
--- /dev/null
+++ b/spec/javascripts/fixtures/pipeline_schedules.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe Projects::PipelineSchedulesController, '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:admin) { create(:admin) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:project) { create(:project, :public, :repository) }
+ let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: admin) }
+ let!(:pipeline_schedule_populated) { create(:ci_pipeline_schedule, project: project, owner: admin) }
+ let!(:pipeline_schedule_variable1) { create(:ci_pipeline_schedule_variable, key: 'foo', value: 'foovalue', pipeline_schedule: pipeline_schedule_populated) }
+ let!(:pipeline_schedule_variable2) { create(:ci_pipeline_schedule_variable, key: 'bar', value: 'barvalue', pipeline_schedule: pipeline_schedule_populated) }
+
+ render_views
+
+ before(:all) do
+ clean_frontend_fixtures('pipeline_schedules/')
+ end
+
+ before do
+ sign_in(admin)
+ end
+
+ it 'pipeline_schedules/edit.html.raw' do |example|
+ get :edit,
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: pipeline_schedule.id
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+
+ it 'pipeline_schedules/edit_with_variables.html.raw' do |example|
+ get :edit,
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: pipeline_schedule_populated.id
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+end
diff --git a/spec/javascripts/gfm_auto_complete_spec.js b/spec/javascripts/gfm_auto_complete_spec.js
index 6f357306ec7..50a587ef351 100644
--- a/spec/javascripts/gfm_auto_complete_spec.js
+++ b/spec/javascripts/gfm_auto_complete_spec.js
@@ -130,16 +130,25 @@ describe('GfmAutoComplete', function () {
});
describe('should not match special sequences', () => {
- const ShouldNotBeFollowedBy = flags.concat(['\x00', '\x10', '\x3f', '\n', ' ']);
+ const shouldNotBeFollowedBy = flags.concat(['\x00', '\x10', '\x3f', '\n', ' ']);
+ const shouldNotBePrependedBy = ['`'];
flagsUseDefaultMatcher.forEach((atSign) => {
- ShouldNotBeFollowedBy.forEach((followedSymbol) => {
+ shouldNotBeFollowedBy.forEach((followedSymbol) => {
const seq = atSign + followedSymbol;
it(`should not match "${seq}"`, () => {
expect(defaultMatcher(atwhoInstance, atSign, seq)).toBe(null);
});
});
+
+ shouldNotBePrependedBy.forEach((prependedSymbol) => {
+ const seq = prependedSymbol + atSign;
+
+ it(`should not match "${seq}"`, () => {
+ expect(defaultMatcher(atwhoInstance, atSign, seq)).toBe(null);
+ });
+ });
});
});
});
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index 2cd2e63b15d..177962ecf82 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -1,4 +1,6 @@
/* eslint-disable space-before-function-paren, one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle, max-len */
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import Issue from '~/issue';
import '~/lib/utils/text_utility';
@@ -68,40 +70,27 @@ describe('Issue', function() {
expect($btn).toHaveText(isIssueInitiallyOpen ? 'Close issue' : 'Reopen issue');
}
- describe('task lists', function() {
- beforeEach(function() {
- loadFixtures('issues/issue-with-task-list.html.raw');
- this.issue = new Issue();
- });
-
- it('submits an ajax request on tasklist:changed', function() {
- spyOn(jQuery, 'ajax').and.callFake(function(req) {
- expect(req.type).toBe('PATCH');
- expect(req.url).toBe(gl.TEST_HOST + '/frontend-fixtures/issues-project/issues/1.json'); // eslint-disable-line prefer-template
- expect(req.data.issue.description).not.toBe(null);
- });
-
- $('.js-task-list-field').trigger('tasklist:changed');
- });
- });
-
[true, false].forEach((isIssueInitiallyOpen) => {
describe(`with ${isIssueInitiallyOpen ? 'open' : 'closed'} issue`, function() {
const action = isIssueInitiallyOpen ? 'close' : 'reopen';
+ let mock;
- function ajaxSpy(req) {
- if (req.url === this.$triggeredButton.attr('href')) {
- expect(req.type).toBe('PUT');
- expectNewBranchButtonState(true, false);
- return this.issueStateDeferred;
- } else if (req.url === Issue.createMrDropdownWrap.dataset.canCreatePath) {
- expect(req.type).toBe('GET');
+ function mockCloseButtonResponseSuccess(url, response) {
+ mock.onPut(url).reply(() => {
expectNewBranchButtonState(true, false);
- return this.canCreateBranchDeferred;
- }
- expect(req.url).toBe('unexpected');
- return null;
+ return [200, response];
+ });
+ }
+
+ function mockCloseButtonResponseError(url) {
+ mock.onPut(url).networkError();
+ }
+
+ function mockCanCreateBranch(canCreateBranch) {
+ mock.onGet(/(.*)\/can_create_branch$/).reply(200, {
+ can_create_branch: canCreateBranch,
+ });
}
beforeEach(function() {
@@ -111,6 +100,11 @@ describe('Issue', function() {
loadFixtures('issues/closed-issue.html.raw');
}
+ mock = new MockAdapter(axios);
+
+ mock.onGet(/(.*)\/related_branches$/).reply(200, {});
+ mock.onGet(/(.*)\/referenced_merge_requests$/).reply(200, {});
+
findElements(isIssueInitiallyOpen);
this.issue = new Issue();
expectIssueState(isIssueInitiallyOpen);
@@ -120,71 +114,89 @@ describe('Issue', function() {
this.$projectIssuesCounter = $('.issue_counter').first();
this.$projectIssuesCounter.text('1,001');
- this.issueStateDeferred = new jQuery.Deferred();
- this.canCreateBranchDeferred = new jQuery.Deferred();
+ spyOn(axios, 'get').and.callThrough();
+ });
- spyOn(jQuery, 'ajax').and.callFake(ajaxSpy.bind(this));
+ afterEach(() => {
+ mock.restore();
+ $('div.flash-alert').remove();
});
- it(`${action}s the issue`, function() {
- this.$triggeredButton.trigger('click');
- this.issueStateDeferred.resolve({
+ it(`${action}s the issue`, function(done) {
+ mockCloseButtonResponseSuccess(this.$triggeredButton.attr('href'), {
id: 34
});
- this.canCreateBranchDeferred.resolve({
- can_create_branch: !isIssueInitiallyOpen
- });
+ mockCanCreateBranch(!isIssueInitiallyOpen);
- expectIssueState(!isIssueInitiallyOpen);
- expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
- expect(this.$projectIssuesCounter.text()).toBe(isIssueInitiallyOpen ? '1,000' : '1,002');
- expectNewBranchButtonState(false, !isIssueInitiallyOpen);
+ this.$triggeredButton.trigger('click');
+
+ setTimeout(() => {
+ expectIssueState(!isIssueInitiallyOpen);
+ expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
+ expect(this.$projectIssuesCounter.text()).toBe(isIssueInitiallyOpen ? '1,000' : '1,002');
+ expectNewBranchButtonState(false, !isIssueInitiallyOpen);
+
+ done();
+ });
});
- it(`fails to ${action} the issue if saved:false`, function() {
- this.$triggeredButton.trigger('click');
- this.issueStateDeferred.resolve({
+ it(`fails to ${action} the issue if saved:false`, function(done) {
+ mockCloseButtonResponseSuccess(this.$triggeredButton.attr('href'), {
saved: false
});
- this.canCreateBranchDeferred.resolve({
- can_create_branch: isIssueInitiallyOpen
- });
+ mockCanCreateBranch(isIssueInitiallyOpen);
- expectIssueState(isIssueInitiallyOpen);
- expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
- expectErrorMessage();
- expect(this.$projectIssuesCounter.text()).toBe('1,001');
- expectNewBranchButtonState(false, isIssueInitiallyOpen);
+ this.$triggeredButton.trigger('click');
+
+ setTimeout(() => {
+ expectIssueState(isIssueInitiallyOpen);
+ expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
+ expectErrorMessage();
+ expect(this.$projectIssuesCounter.text()).toBe('1,001');
+ expectNewBranchButtonState(false, isIssueInitiallyOpen);
+
+ done();
+ });
});
- it(`fails to ${action} the issue if HTTP error occurs`, function() {
+ it(`fails to ${action} the issue if HTTP error occurs`, function(done) {
+ mockCloseButtonResponseError(this.$triggeredButton.attr('href'));
+ mockCanCreateBranch(isIssueInitiallyOpen);
+
this.$triggeredButton.trigger('click');
- this.issueStateDeferred.reject();
- this.canCreateBranchDeferred.resolve({
- can_create_branch: isIssueInitiallyOpen
- });
- expectIssueState(isIssueInitiallyOpen);
- expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
- expectErrorMessage();
- expect(this.$projectIssuesCounter.text()).toBe('1,001');
- expectNewBranchButtonState(false, isIssueInitiallyOpen);
+ setTimeout(() => {
+ expectIssueState(isIssueInitiallyOpen);
+ expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
+ expectErrorMessage();
+ expect(this.$projectIssuesCounter.text()).toBe('1,001');
+ expectNewBranchButtonState(false, isIssueInitiallyOpen);
+
+ done();
+ });
});
it('disables the new branch button if Ajax call fails', function() {
+ mockCloseButtonResponseError(this.$triggeredButton.attr('href'));
+ mock.onGet(/(.*)\/can_create_branch$/).networkError();
+
this.$triggeredButton.trigger('click');
- this.issueStateDeferred.reject();
- this.canCreateBranchDeferred.reject();
expectNewBranchButtonState(false, false);
});
- it('does not trigger Ajax call if new branch button is missing', function() {
+ it('does not trigger Ajax call if new branch button is missing', function(done) {
+ mockCloseButtonResponseError(this.$triggeredButton.attr('href'));
Issue.$btnNewBranch = $();
this.canCreateBranchDeferred = null;
this.$triggeredButton.trigger('click');
- this.issueStateDeferred.reject();
+
+ setTimeout(() => {
+ expect(axios.get).not.toHaveBeenCalled();
+
+ done();
+ });
});
});
});
diff --git a/spec/javascripts/job_spec.js b/spec/javascripts/job_spec.js
index 0452934ea9e..03b58e9c1d0 100644
--- a/spec/javascripts/job_spec.js
+++ b/spec/javascripts/job_spec.js
@@ -1,3 +1,5 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import * as urlUtils from '~/lib/utils/url_utility';
import '~/lib/utils/datetime_utility';
@@ -6,11 +8,29 @@ import '~/breakpoints';
describe('Job', () => {
const JOB_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1`;
+ let mock;
+ let response;
+
+ function waitForPromise() {
+ return new Promise(resolve => requestAnimationFrame(resolve));
+ }
preloadFixtures('builds/build-with-artifacts.html.raw');
beforeEach(() => {
loadFixtures('builds/build-with-artifacts.html.raw');
+
+ spyOn(urlUtils, 'visitUrl');
+
+ mock = new MockAdapter(axios);
+
+ mock.onGet(new RegExp(`${JOB_URL}/trace.json?(.*)`)).reply(() => [200, response]);
+ });
+
+ afterEach(() => {
+ mock.restore();
+
+ response = {};
});
describe('class constructor', () => {
@@ -55,161 +75,159 @@ describe('Job', () => {
});
describe('running build', () => {
- it('updates the build trace on an interval', function () {
- const deferred1 = $.Deferred();
- const deferred2 = $.Deferred();
- spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise());
- spyOn(urlUtils, 'visitUrl');
-
- deferred1.resolve({
+ it('updates the build trace on an interval', function (done) {
+ response = {
html: '<span>Update<span>',
status: 'running',
state: 'newstate',
append: true,
complete: false,
- });
-
- deferred2.resolve({
- html: '<span>More</span>',
- status: 'running',
- state: 'finalstate',
- append: true,
- complete: true,
- });
+ };
this.job = new Job();
- expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
- expect(this.job.state).toBe('newstate');
-
- jasmine.clock().tick(4001);
-
- expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/);
- expect(this.job.state).toBe('finalstate');
+ waitForPromise()
+ .then(() => {
+ expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
+ expect(this.job.state).toBe('newstate');
+
+ response = {
+ html: '<span>More</span>',
+ status: 'running',
+ state: 'finalstate',
+ append: true,
+ complete: true,
+ };
+ })
+ .then(() => jasmine.clock().tick(4001))
+ .then(waitForPromise)
+ .then(() => {
+ expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/);
+ expect(this.job.state).toBe('finalstate');
+ })
+ .then(done)
+ .catch(done.fail);
});
- it('replaces the entire build trace', () => {
- const deferred1 = $.Deferred();
- const deferred2 = $.Deferred();
-
- spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise());
-
- spyOn(urlUtils, 'visitUrl');
-
- deferred1.resolve({
+ it('replaces the entire build trace', (done) => {
+ response = {
html: '<span>Update<span>',
status: 'running',
append: false,
complete: false,
- });
-
- deferred2.resolve({
- html: '<span>Different</span>',
- status: 'running',
- append: false,
- });
+ };
this.job = new Job();
- expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
-
- jasmine.clock().tick(4001);
-
- expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/);
- expect($('#build-trace .js-build-output').text()).toMatch(/Different/);
+ waitForPromise()
+ .then(() => {
+ expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
+
+ response = {
+ html: '<span>Different</span>',
+ status: 'running',
+ append: false,
+ };
+ })
+ .then(() => jasmine.clock().tick(4001))
+ .then(waitForPromise)
+ .then(() => {
+ expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/);
+ expect($('#build-trace .js-build-output').text()).toMatch(/Different/);
+ })
+ .then(done)
+ .catch(done.fail);
});
});
describe('truncated information', () => {
describe('when size is less than total', () => {
- it('shows information about truncated log', () => {
- spyOn(urlUtils, 'visitUrl');
- const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
-
- deferred.resolve({
+ it('shows information about truncated log', (done) => {
+ response = {
html: '<span>Update</span>',
status: 'success',
append: false,
size: 50,
total: 100,
- });
+ };
this.job = new Job();
- expect(document.querySelector('.js-truncated-info').classList).not.toContain('hidden');
+ waitForPromise()
+ .then(() => {
+ expect(document.querySelector('.js-truncated-info').classList).not.toContain('hidden');
+ })
+ .then(done)
+ .catch(done.fail);
});
- it('shows the size in KiB', () => {
+ it('shows the size in KiB', (done) => {
const size = 50;
- spyOn(urlUtils, 'visitUrl');
- const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
- deferred.resolve({
+ response = {
html: '<span>Update</span>',
status: 'success',
append: false,
size,
total: 100,
- });
+ };
this.job = new Job();
- expect(
- document.querySelector('.js-truncated-info-size').textContent.trim(),
- ).toEqual(`${numberToHumanSize(size)}`);
+ waitForPromise()
+ .then(() => {
+ expect(
+ document.querySelector('.js-truncated-info-size').textContent.trim(),
+ ).toEqual(`${numberToHumanSize(size)}`);
+ })
+ .then(done)
+ .catch(done.fail);
});
- it('shows incremented size', () => {
- const deferred1 = $.Deferred();
- const deferred2 = $.Deferred();
-
- spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise());
-
- spyOn(urlUtils, 'visitUrl');
-
- deferred1.resolve({
+ it('shows incremented size', (done) => {
+ response = {
html: '<span>Update</span>',
status: 'success',
append: false,
size: 50,
total: 100,
- });
+ };
this.job = new Job();
- expect(
- document.querySelector('.js-truncated-info-size').textContent.trim(),
- ).toEqual(`${numberToHumanSize(50)}`);
-
- jasmine.clock().tick(4001);
-
- deferred2.resolve({
- html: '<span>Update</span>',
- status: 'success',
- append: true,
- size: 10,
- total: 100,
- });
-
- expect(
- document.querySelector('.js-truncated-info-size').textContent.trim(),
- ).toEqual(`${numberToHumanSize(60)}`);
+ waitForPromise()
+ .then(() => {
+ expect(
+ document.querySelector('.js-truncated-info-size').textContent.trim(),
+ ).toEqual(`${numberToHumanSize(50)}`);
+
+ response = {
+ html: '<span>Update</span>',
+ status: 'success',
+ append: true,
+ size: 10,
+ total: 100,
+ };
+ })
+ .then(() => jasmine.clock().tick(4001))
+ .then(waitForPromise)
+ .then(() => {
+ expect(
+ document.querySelector('.js-truncated-info-size').textContent.trim(),
+ ).toEqual(`${numberToHumanSize(60)}`);
+ })
+ .then(done)
+ .catch(done.fail);
});
it('renders the raw link', () => {
- const deferred = $.Deferred();
- spyOn(urlUtils, 'visitUrl');
-
- spyOn($, 'ajax').and.returnValue(deferred.promise());
- deferred.resolve({
+ response = {
html: '<span>Update</span>',
status: 'success',
append: false,
size: 50,
total: 100,
- });
+ };
this.job = new Job();
@@ -220,50 +238,50 @@ describe('Job', () => {
});
describe('when size is equal than total', () => {
- it('does not show the trunctated information', () => {
- const deferred = $.Deferred();
- spyOn(urlUtils, 'visitUrl');
-
- spyOn($, 'ajax').and.returnValue(deferred.promise());
- deferred.resolve({
+ it('does not show the trunctated information', (done) => {
+ response = {
html: '<span>Update</span>',
status: 'success',
append: false,
size: 100,
total: 100,
- });
+ };
this.job = new Job();
- expect(document.querySelector('.js-truncated-info').classList).toContain('hidden');
+ waitForPromise()
+ .then(() => {
+ expect(document.querySelector('.js-truncated-info').classList).toContain('hidden');
+ })
+ .then(done)
+ .catch(done.fail);
});
});
});
describe('output trace', () => {
- beforeEach(() => {
- const deferred = $.Deferred();
- spyOn(urlUtils, 'visitUrl');
-
- spyOn($, 'ajax').and.returnValue(deferred.promise());
- deferred.resolve({
+ beforeEach((done) => {
+ response = {
html: '<span>Update</span>',
status: 'success',
append: false,
size: 50,
total: 100,
- });
+ };
this.job = new Job();
+
+ waitForPromise()
+ .then(done)
+ .catch(done.fail);
});
it('should render trace controls', () => {
const controllers = document.querySelector('.controllers');
- expect(controllers.querySelector('.js-raw-link-controller')).toBeDefined();
- expect(controllers.querySelector('.js-erase-link')).toBeDefined();
- expect(controllers.querySelector('.js-scroll-up')).toBeDefined();
- expect(controllers.querySelector('.js-scroll-down')).toBeDefined();
+ expect(controllers.querySelector('.js-raw-link-controller')).not.toBeNull();
+ expect(controllers.querySelector('.js-scroll-up')).not.toBeNull();
+ expect(controllers.querySelector('.js-scroll-down')).not.toBeNull();
});
it('should render received output', () => {
@@ -276,13 +294,13 @@ describe('Job', () => {
describe('getBuildTrace', () => {
it('should request build trace with state parameter', (done) => {
- spyOn(jQuery, 'ajax').and.callThrough();
+ spyOn(axios, 'get').and.callThrough();
// eslint-disable-next-line no-new
new Job();
setTimeout(() => {
- expect(jQuery.ajax).toHaveBeenCalledWith(
- { url: `${JOB_URL}/trace.json`, data: { state: '' } },
+ expect(axios.get).toHaveBeenCalledWith(
+ `${JOB_URL}/trace.json`, { params: { state: '' } },
);
done();
}, 0);
diff --git a/spec/javascripts/labels_issue_sidebar_spec.js b/spec/javascripts/labels_issue_sidebar_spec.js
index a197b35f6fb..7d992f62f64 100644
--- a/spec/javascripts/labels_issue_sidebar_spec.js
+++ b/spec/javascripts/labels_issue_sidebar_spec.js
@@ -1,4 +1,6 @@
/* eslint-disable no-new */
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import IssuableContext from '~/issuable_context';
import LabelsSelect from '~/labels_select';
@@ -10,35 +12,44 @@ import '~/users_select';
(() => {
let saveLabelCount = 0;
+ let mock;
+
describe('Issue dropdown sidebar', () => {
preloadFixtures('static/issue_sidebar_label.html.raw');
beforeEach(() => {
loadFixtures('static/issue_sidebar_label.html.raw');
+
+ mock = new MockAdapter(axios);
+
new IssuableContext('{"id":1,"name":"Administrator","username":"root"}');
new LabelsSelect();
- spyOn(jQuery, 'ajax').and.callFake((req) => {
- const d = $.Deferred();
- let LABELS_DATA = [];
+ mock.onGet('/root/test/labels.json').reply(() => {
+ const labels = Array(10).fill().map((_, i) => ({
+ id: i,
+ title: `test ${i}`,
+ color: '#5CB85C',
+ }));
- if (req.url === '/root/test/labels.json') {
- for (let i = 0; i < 10; i += 1) {
- LABELS_DATA.push({ id: i, title: `test ${i}`, color: '#5CB85C' });
- }
- } else if (req.url === '/root/test/issues/2.json') {
- const tmp = [];
- for (let i = 0; i < saveLabelCount; i += 1) {
- tmp.push({ id: i, title: `test ${i}`, color: '#5CB85C' });
- }
- LABELS_DATA = { labels: tmp };
- }
+ return [200, labels];
+ });
+
+ mock.onPut('/root/test/issues/2.json').reply(() => {
+ const labels = Array(saveLabelCount).fill().map((_, i) => ({
+ id: i,
+ title: `test ${i}`,
+ color: '#5CB85C',
+ }));
- d.resolve(LABELS_DATA);
- return d.promise();
+ return [200, { labels }];
});
});
+ afterEach(() => {
+ mock.restore();
+ });
+
it('changes collapsed tooltip when changing labels when less than 5', (done) => {
saveLabelCount = 5;
$('.edit-link').get(0).click();
diff --git a/spec/javascripts/lib/utils/ajax_cache_spec.js b/spec/javascripts/lib/utils/ajax_cache_spec.js
index 49971bd91e2..7603400b55e 100644
--- a/spec/javascripts/lib/utils/ajax_cache_spec.js
+++ b/spec/javascripts/lib/utils/ajax_cache_spec.js
@@ -1,3 +1,5 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import AjaxCache from '~/lib/utils/ajax_cache';
describe('AjaxCache', () => {
@@ -87,66 +89,53 @@ describe('AjaxCache', () => {
});
describe('retrieve', () => {
- let ajaxSpy;
+ let mock;
beforeEach(() => {
- spyOn(jQuery, 'ajax').and.callFake(url => ajaxSpy(url));
+ mock = new MockAdapter(axios);
+
+ spyOn(axios, 'get').and.callThrough();
+ });
+
+ afterEach(() => {
+ mock.restore();
});
it('stores and returns data from Ajax call if cache is empty', (done) => {
- ajaxSpy = (url) => {
- expect(url).toBe(dummyEndpoint);
- const deferred = $.Deferred();
- deferred.resolve(dummyResponse);
- return deferred.promise();
- };
+ mock.onGet(dummyEndpoint).reply(200, dummyResponse);
AjaxCache.retrieve(dummyEndpoint)
.then((data) => {
- expect(data).toBe(dummyResponse);
- expect(AjaxCache.internalStorage[dummyEndpoint]).toBe(dummyResponse);
+ expect(data).toEqual(dummyResponse);
+ expect(AjaxCache.internalStorage[dummyEndpoint]).toEqual(dummyResponse);
})
.then(done)
.catch(fail);
});
- it('makes no Ajax call if request is pending', () => {
- const responseDeferred = $.Deferred();
-
- ajaxSpy = (url) => {
- expect(url).toBe(dummyEndpoint);
- // neither reject nor resolve to keep request pending
- return responseDeferred.promise();
- };
-
- const unexpectedResponse = data => fail(`Did not expect response: ${data}`);
+ it('makes no Ajax call if request is pending', (done) => {
+ mock.onGet(dummyEndpoint).reply(200, dummyResponse);
AjaxCache.retrieve(dummyEndpoint)
- .then(unexpectedResponse)
+ .then(done)
.catch(fail);
AjaxCache.retrieve(dummyEndpoint)
- .then(unexpectedResponse)
+ .then(done)
.catch(fail);
- expect($.ajax.calls.count()).toBe(1);
+ expect(axios.get.calls.count()).toBe(1);
});
it('returns undefined if Ajax call fails and cache is empty', (done) => {
- const dummyStatusText = 'exploded';
- const dummyErrorMessage = 'server exploded';
- ajaxSpy = (url) => {
- expect(url).toBe(dummyEndpoint);
- const deferred = $.Deferred();
- deferred.reject(null, dummyStatusText, dummyErrorMessage);
- return deferred.promise();
- };
+ const errorMessage = 'Network Error';
+ mock.onGet(dummyEndpoint).networkError();
AjaxCache.retrieve(dummyEndpoint)
.then(data => fail(`Received unexpected data: ${JSON.stringify(data)}`))
.catch((error) => {
- expect(error.message).toBe(`${dummyEndpoint}: ${dummyErrorMessage}`);
- expect(error.textStatus).toBe(dummyStatusText);
+ expect(error.message).toBe(`${dummyEndpoint}: ${errorMessage}`);
+ expect(error.textStatus).toBe(errorMessage);
done();
})
.catch(fail);
@@ -154,7 +143,9 @@ describe('AjaxCache', () => {
it('makes no Ajax call if matching data exists', (done) => {
AjaxCache.internalStorage[dummyEndpoint] = dummyResponse;
- ajaxSpy = () => fail(new Error('expected no Ajax call!'));
+ mock.onGet(dummyEndpoint).reply(() => {
+ fail(new Error('expected no Ajax call!'));
+ });
AjaxCache.retrieve(dummyEndpoint)
.then((data) => {
@@ -171,12 +162,7 @@ describe('AjaxCache', () => {
AjaxCache.internalStorage[dummyEndpoint] = oldDummyResponse;
- ajaxSpy = (url) => {
- expect(url).toBe(dummyEndpoint);
- const deferred = $.Deferred();
- deferred.resolve(dummyResponse);
- return deferred.promise();
- };
+ mock.onGet(dummyEndpoint).reply(200, dummyResponse);
// Call without forceRetrieve param
AjaxCache.retrieve(dummyEndpoint)
@@ -189,7 +175,7 @@ describe('AjaxCache', () => {
// Call with forceRetrieve param
AjaxCache.retrieve(dummyEndpoint, true)
.then((data) => {
- expect(data).toBe(dummyResponse);
+ expect(data).toEqual(dummyResponse);
})
.then(done)
.catch(fail);
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js
index 1052b4e7c20..80430011aed 100644
--- a/spec/javascripts/lib/utils/common_utils_spec.js
+++ b/spec/javascripts/lib/utils/common_utils_spec.js
@@ -1,7 +1,6 @@
/* eslint-disable promise/catch-or-return */
-
-import * as commonUtils from '~/lib/utils/common_utils';
import axios from '~/lib/utils/axios_utils';
+import * as commonUtils from '~/lib/utils/common_utils';
import MockAdapter from 'axios-mock-adapter';
describe('common_utils', () => {
@@ -460,17 +459,6 @@ describe('common_utils', () => {
});
});
- describe('ajaxPost', () => {
- it('should perform `$.ajax` call and do `POST` request', () => {
- const requestURL = '/some/random/api';
- const data = { keyname: 'value' };
- const ajaxSpy = spyOn($, 'ajax').and.callFake(() => {});
-
- commonUtils.ajaxPost(requestURL, data);
- expect(ajaxSpy.calls.allArgs()[0][0].type).toEqual('POST');
- });
- });
-
describe('spriteIcon', () => {
let beforeGon;
diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js
index bae3219b043..bdfd16ac995 100644
--- a/spec/javascripts/merge_request_spec.js
+++ b/spec/javascripts/merge_request_spec.js
@@ -1,5 +1,6 @@
/* eslint-disable space-before-function-paren, no-return-assign */
-
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import MergeRequest from '~/merge_request';
import CloseReopenReportToggle from '~/close_reopen_report_toggle';
import IssuablesHelper from '~/helpers/issuables_helper';
@@ -7,11 +8,24 @@ import IssuablesHelper from '~/helpers/issuables_helper';
(function() {
describe('MergeRequest', function() {
describe('task lists', function() {
+ let mock;
+
preloadFixtures('merge_requests/merge_request_with_task_list.html.raw');
beforeEach(function() {
loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
+
+ spyOn(axios, 'patch').and.callThrough();
+ mock = new MockAdapter(axios);
+
+ mock.onPatch(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`).reply(200, {});
+
return this.merge = new MergeRequest();
});
+
+ afterEach(() => {
+ mock.restore();
+ });
+
it('modifies the Markdown field', function() {
spyOn(jQuery, 'ajax').and.stub();
const changeEvent = document.createEvent('HTMLEvents');
@@ -21,14 +35,14 @@ import IssuablesHelper from '~/helpers/issuables_helper';
});
it('submits an ajax request on tasklist:changed', (done) => {
- spyOn(jQuery, 'ajax').and.callFake((req) => {
- expect(req.type).toBe('PATCH');
- expect(req.url).toBe(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`);
- expect(req.data.merge_request.description).not.toBe(null);
+ $('.js-task-list-field').trigger('tasklist:changed');
+
+ setTimeout(() => {
+ expect(axios.patch).toHaveBeenCalledWith(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`, {
+ merge_request: { description: '- [ ] Task List Item' },
+ });
done();
});
-
- $('.js-task-list-field').trigger('tasklist:changed');
});
});
diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js
index a6be474805b..fda24db98b4 100644
--- a/spec/javascripts/merge_request_tabs_spec.js
+++ b/spec/javascripts/merge_request_tabs_spec.js
@@ -1,5 +1,6 @@
/* eslint-disable no-var, comma-dangle, object-shorthand */
-
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import * as urlUtils from '~/lib/utils/url_utility';
import MergeRequestTabs from '~/merge_request_tabs';
import '~/commit/pipelines/pipelines_bundle';
@@ -46,7 +47,7 @@ import 'vendor/jquery.scrollTo';
describe('activateTab', function () {
beforeEach(function () {
- spyOn($, 'ajax').and.callFake(function () {});
+ spyOn(axios, 'get').and.returnValue(Promise.resolve({ data: {} }));
loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
this.subject = this.class.activateTab;
});
@@ -148,7 +149,7 @@ import 'vendor/jquery.scrollTo';
describe('setCurrentAction', function () {
beforeEach(function () {
- spyOn($, 'ajax').and.callFake(function () {});
+ spyOn(axios, 'get').and.returnValue(Promise.resolve({ data: {} }));
this.subject = this.class.setCurrentAction;
});
@@ -214,13 +215,21 @@ import 'vendor/jquery.scrollTo';
});
describe('tabShown', () => {
+ let mock;
+
beforeEach(function () {
- spyOn($, 'ajax').and.callFake(function (options) {
- options.success({ html: '' });
+ mock = new MockAdapter(axios);
+ mock.onGet(/(.*)\/diffs\.json/).reply(200, {
+ data: { html: '' },
});
+
loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
});
+ afterEach(() => {
+ mock.restore();
+ });
+
describe('with "Side-by-side"/parallel diff view', () => {
beforeEach(function () {
this.class.diffViewType = () => 'parallel';
@@ -292,16 +301,20 @@ import 'vendor/jquery.scrollTo';
it('triggers Ajax request to JSON endpoint', function (done) {
const url = '/foo/bar/merge_requests/1/diffs';
- spyOn(this.class, 'ajaxGet').and.callFake((options) => {
- expect(options.url).toEqual(`${url}.json`);
+
+ spyOn(axios, 'get').and.callFake((reqUrl) => {
+ expect(reqUrl).toBe(`${url}.json`);
+
done();
+
+ return Promise.resolve({ data: {} });
});
this.class.loadDiff(url);
});
it('triggers scroll event when diff already loaded', function (done) {
- spyOn(this.class, 'ajaxGet').and.callFake(() => done.fail());
+ spyOn(axios, 'get').and.callFake(done.fail);
spyOn(document, 'dispatchEvent');
this.class.diffsLoaded = true;
@@ -316,6 +329,7 @@ import 'vendor/jquery.scrollTo';
describe('with inline diff', () => {
let noteId;
let noteLineNumId;
+ let mock;
beforeEach(() => {
const diffsResponse = getJSONFixture(inlineChangesTabJsonFixture);
@@ -330,29 +344,40 @@ import 'vendor/jquery.scrollTo';
.attr('href')
.replace('#', '');
- spyOn($, 'ajax').and.callFake(function (options) {
- options.success(diffsResponse);
- });
+ mock = new MockAdapter(axios);
+ mock.onGet(/(.*)\/diffs\.json/).reply(200, diffsResponse);
+ });
+
+ afterEach(() => {
+ mock.restore();
});
describe('with note fragment hash', () => {
- it('should expand and scroll to linked fragment hash #note_xxx', function () {
+ it('should expand and scroll to linked fragment hash #note_xxx', function (done) {
spyOn(urlUtils, 'getLocationHash').and.returnValue(noteId);
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
- expect(noteId.length).toBeGreaterThan(0);
- expect(Notes.instance.toggleDiffNote).toHaveBeenCalledWith({
- target: jasmine.any(Object),
- lineType: 'old',
- forceShow: true,
+ setTimeout(() => {
+ expect(noteId.length).toBeGreaterThan(0);
+ expect(Notes.instance.toggleDiffNote).toHaveBeenCalledWith({
+ target: jasmine.any(Object),
+ lineType: 'old',
+ forceShow: true,
+ });
+
+ done();
});
});
- it('should gracefully ignore non-existant fragment hash', function () {
+ it('should gracefully ignore non-existant fragment hash', function (done) {
spyOn(urlUtils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist');
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
- expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled();
+ setTimeout(() => {
+ expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled();
+
+ done();
+ });
});
});
@@ -370,6 +395,7 @@ import 'vendor/jquery.scrollTo';
describe('with parallel diff', () => {
let noteId;
let noteLineNumId;
+ let mock;
beforeEach(() => {
const diffsResponse = getJSONFixture(parallelChangesTabJsonFixture);
@@ -384,30 +410,40 @@ import 'vendor/jquery.scrollTo';
.attr('href')
.replace('#', '');
- spyOn($, 'ajax').and.callFake(function (options) {
- options.success(diffsResponse);
- });
+ mock = new MockAdapter(axios);
+ mock.onGet(/(.*)\/diffs\.json/).reply(200, diffsResponse);
+ });
+
+ afterEach(() => {
+ mock.restore();
});
describe('with note fragment hash', () => {
- it('should expand and scroll to linked fragment hash #note_xxx', function () {
+ it('should expand and scroll to linked fragment hash #note_xxx', function (done) {
spyOn(urlUtils, 'getLocationHash').and.returnValue(noteId);
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
- expect(noteId.length).toBeGreaterThan(0);
- expect(Notes.instance.toggleDiffNote).toHaveBeenCalledWith({
- target: jasmine.any(Object),
- lineType: 'new',
- forceShow: true,
+ setTimeout(() => {
+ expect(noteId.length).toBeGreaterThan(0);
+ expect(Notes.instance.toggleDiffNote).toHaveBeenCalledWith({
+ target: jasmine.any(Object),
+ lineType: 'new',
+ forceShow: true,
+ });
+
+ done();
});
});
- it('should gracefully ignore non-existant fragment hash', function () {
+ it('should gracefully ignore non-existant fragment hash', function (done) {
spyOn(urlUtils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist');
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
- expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled();
+ setTimeout(() => {
+ expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled();
+ done();
+ });
});
});
diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js
index 481b46c3ac6..6fa6f44f953 100644
--- a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js
+++ b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js
@@ -1,7 +1,9 @@
/* eslint-disable no-new */
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown';
-import '~/flash';
+import timeoutPromise from './helpers/set_timeout_promise_helper';
describe('Mini Pipeline Graph Dropdown', () => {
preloadFixtures('static/mini_dropdown_graph.html.raw');
@@ -27,6 +29,16 @@ describe('Mini Pipeline Graph Dropdown', () => {
});
describe('When dropdown is clicked', () => {
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
it('should call getBuildsList', () => {
const getBuildsListSpy = spyOn(
MiniPipelineGraph.prototype,
@@ -41,46 +53,55 @@ describe('Mini Pipeline Graph Dropdown', () => {
});
it('should make a request to the endpoint provided in the html', () => {
- const ajaxSpy = spyOn($, 'ajax').and.callFake(function () {});
+ const ajaxSpy = spyOn(axios, 'get').and.callThrough();
+
+ mock.onGet('foobar').reply(200, {
+ html: '',
+ });
new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
document.querySelector('.js-builds-dropdown-button').click();
- expect(ajaxSpy.calls.allArgs()[0][0].url).toEqual('foobar');
+ expect(ajaxSpy.calls.allArgs()[0][0]).toEqual('foobar');
});
- it('should not close when user uses cmd/ctrl + click', () => {
- spyOn($, 'ajax').and.callFake(function (params) {
- params.success({
- html: `<li>
- <a class="mini-pipeline-graph-dropdown-item" href="#">
- <span class="ci-status-icon ci-status-icon-failed"></span>
- <span class="ci-build-text">build</span>
- </a>
- <a class="ci-action-icon-wrapper js-ci-action-icon" href="#"></a>
- </li>`,
- });
+ it('should not close when user uses cmd/ctrl + click', (done) => {
+ mock.onGet('foobar').reply(200, {
+ html: `<li>
+ <a class="mini-pipeline-graph-dropdown-item" href="#">
+ <span class="ci-status-icon ci-status-icon-failed"></span>
+ <span class="ci-build-text">build</span>
+ </a>
+ <a class="ci-action-icon-wrapper js-ci-action-icon" href="#"></a>
+ </li>`,
});
new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
document.querySelector('.js-builds-dropdown-button').click();
- document.querySelector('a.mini-pipeline-graph-dropdown-item').click();
-
- expect($('.js-builds-dropdown-list').is(':visible')).toEqual(true);
+ timeoutPromise()
+ .then(() => {
+ document.querySelector('a.mini-pipeline-graph-dropdown-item').click();
+ })
+ .then(timeoutPromise)
+ .then(() => {
+ expect($('.js-builds-dropdown-list').is(':visible')).toEqual(true);
+ })
+ .then(done)
+ .catch(done.fail);
});
- });
- it('should close the dropdown when request returns an error', (done) => {
- spyOn($, 'ajax').and.callFake(options => options.error());
+ it('should close the dropdown when request returns an error', (done) => {
+ mock.onGet('foobar').networkError();
- new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
+ new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
- document.querySelector('.js-builds-dropdown-button').click();
+ document.querySelector('.js-builds-dropdown-button').click();
- setTimeout(() => {
- expect($('.js-builds-dropdown-tests .dropdown').hasClass('open')).toEqual(false);
- done();
- }, 0);
+ setTimeout(() => {
+ expect($('.js-builds-dropdown-tests .dropdown').hasClass('open')).toEqual(false);
+ done();
+ });
+ });
});
});
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index a40821a5693..274d7591c71 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -1,11 +1,14 @@
/* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, max-len */
import _ from 'underscore';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import * as urlUtils from '~/lib/utils/url_utility';
import 'autosize';
import '~/gl_form';
import '~/lib/utils/text_utility';
import '~/render_gfm';
import Notes from '~/notes';
+import timeoutPromise from './helpers/set_timeout_promise_helper';
(function() {
window.gon || (window.gon = {});
@@ -47,13 +50,24 @@ import Notes from '~/notes';
});
describe('task lists', function() {
+ let mock;
+
beforeEach(function() {
+ spyOn(axios, 'patch').and.callThrough();
+ mock = new MockAdapter(axios);
+
+ mock.onPatch(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`).reply(200, {});
+
$('.js-comment-button').on('click', function(e) {
e.preventDefault();
});
this.notes = new Notes('', []);
});
+ afterEach(() => {
+ mock.restore();
+ });
+
it('modifies the Markdown field', function() {
const changeEvent = document.createEvent('HTMLEvents');
changeEvent.initEvent('change', true, true);
@@ -62,14 +76,15 @@ import Notes from '~/notes';
expect($('.js-task-list-field.original-task-list').val()).toBe('- [x] Task List Item');
});
- it('submits an ajax request on tasklist:changed', function() {
- spyOn(jQuery, 'ajax').and.callFake(function(req) {
- expect(req.type).toBe('PATCH');
- expect(req.url).toBe('http://test.host/frontend-fixtures/merge-requests-project/merge_requests/1.json');
- return expect(req.data.note).not.toBe(null);
- });
+ it('submits an ajax request on tasklist:changed', function(done) {
+ $('.js-task-list-container').trigger('tasklist:changed');
- $('.js-task-list-field.js-note-text').trigger('tasklist:changed');
+ setTimeout(() => {
+ expect(axios.patch).toHaveBeenCalledWith(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`, {
+ note: { note: '' },
+ });
+ done();
+ });
});
});
@@ -119,6 +134,7 @@ import Notes from '~/notes';
let noteEntity;
let $form;
let $notesContainer;
+ let mock;
beforeEach(() => {
this.notes = new Notes('', []);
@@ -136,24 +152,32 @@ import Notes from '~/notes';
$form = $('form.js-main-target-form');
$notesContainer = $('ul.main-notes-list');
$form.find('textarea.js-note-text').val(sampleComment);
+
+ mock = new MockAdapter(axios);
+ mock.onPost(/(.*)\/notes$/).reply(200, noteEntity);
});
- it('updates note and resets edit form', () => {
- const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('updates note and resets edit form', (done) => {
spyOn(this.notes, 'revertNoteEditForm');
spyOn(this.notes, 'setupNewNote');
$('.js-comment-button').click();
- deferred.resolve(noteEntity);
- const $targetNote = $notesContainer.find(`#note_${noteEntity.id}`);
- const updatedNote = Object.assign({}, noteEntity);
- updatedNote.note = 'bar';
- this.notes.updateNote(updatedNote, $targetNote);
+ setTimeout(() => {
+ const $targetNote = $notesContainer.find(`#note_${noteEntity.id}`);
+ const updatedNote = Object.assign({}, noteEntity);
+ updatedNote.note = 'bar';
+ this.notes.updateNote(updatedNote, $targetNote);
+
+ expect(this.notes.revertNoteEditForm).toHaveBeenCalledWith($targetNote);
+ expect(this.notes.setupNewNote).toHaveBeenCalled();
- expect(this.notes.revertNoteEditForm).toHaveBeenCalledWith($targetNote);
- expect(this.notes.setupNewNote).toHaveBeenCalled();
+ done();
+ });
});
});
@@ -479,8 +503,19 @@ import Notes from '~/notes';
};
let $form;
let $notesContainer;
+ let mock;
+
+ function mockNotesPost() {
+ mock.onPost(/(.*)\/notes$/).reply(200, note);
+ }
+
+ function mockNotesPostError() {
+ mock.onPost(/(.*)\/notes$/).networkError();
+ }
beforeEach(() => {
+ mock = new MockAdapter(axios);
+
this.notes = new Notes('', []);
window.gon.current_username = 'root';
window.gon.current_user_fullname = 'Administrator';
@@ -489,63 +524,92 @@ import Notes from '~/notes';
$form.find('textarea.js-note-text').val(sampleComment);
});
+ afterEach(() => {
+ mock.restore();
+ });
+
it('should show placeholder note while new comment is being posted', () => {
+ mockNotesPost();
+
$('.js-comment-button').click();
expect($notesContainer.find('.note.being-posted').length > 0).toEqual(true);
});
- it('should remove placeholder note when new comment is done posting', () => {
- const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
+ it('should remove placeholder note when new comment is done posting', (done) => {
+ mockNotesPost();
+
$('.js-comment-button').click();
- deferred.resolve(note);
- expect($notesContainer.find('.note.being-posted').length).toEqual(0);
+ setTimeout(() => {
+ expect($notesContainer.find('.note.being-posted').length).toEqual(0);
+
+ done();
+ });
});
- it('should show actual note element when new comment is done posting', () => {
- const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
+ it('should show actual note element when new comment is done posting', (done) => {
+ mockNotesPost();
+
$('.js-comment-button').click();
- deferred.resolve(note);
- expect($notesContainer.find(`#note_${note.id}`).length > 0).toEqual(true);
+ setTimeout(() => {
+ expect($notesContainer.find(`#note_${note.id}`).length > 0).toEqual(true);
+
+ done();
+ });
});
- it('should reset Form when new comment is done posting', () => {
- const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
+ it('should reset Form when new comment is done posting', (done) => {
+ mockNotesPost();
+
$('.js-comment-button').click();
- deferred.resolve(note);
- expect($form.find('textarea.js-note-text').val()).toEqual('');
+ setTimeout(() => {
+ expect($form.find('textarea.js-note-text').val()).toEqual('');
+
+ done();
+ });
});
- it('should show flash error message when new comment failed to be posted', () => {
- const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
+ it('should show flash error message when new comment failed to be posted', (done) => {
+ mockNotesPostError();
+
$('.js-comment-button').click();
- deferred.reject();
- expect($notesContainer.parent().find('.flash-container .flash-text').is(':visible')).toEqual(true);
+ setTimeout(() => {
+ expect($notesContainer.parent().find('.flash-container .flash-text').is(':visible')).toEqual(true);
+
+ done();
+ });
});
- it('should show flash error message when comment failed to be updated', () => {
- const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
+ it('should show flash error message when comment failed to be updated', (done) => {
+ mockNotesPost();
+
$('.js-comment-button').click();
- deferred.resolve(note);
- const $noteEl = $notesContainer.find(`#note_${note.id}`);
- $noteEl.find('.js-note-edit').click();
- $noteEl.find('textarea.js-note-text').val(updatedComment);
- $noteEl.find('.js-comment-save-button').click();
+ timeoutPromise()
+ .then(() => {
+ const $noteEl = $notesContainer.find(`#note_${note.id}`);
+ $noteEl.find('.js-note-edit').click();
+ $noteEl.find('textarea.js-note-text').val(updatedComment);
- deferred.reject();
- const $updatedNoteEl = $notesContainer.find(`#note_${note.id}`);
- expect($updatedNoteEl.hasClass('.being-posted')).toEqual(false); // Remove being-posted visuals
- expect($updatedNoteEl.find('.note-text').text().trim()).toEqual(sampleComment); // See if comment reverted back to original
- expect($('.flash-container').is(':visible')).toEqual(true); // Flash error message shown
+ mock.restore();
+
+ mockNotesPostError();
+
+ $noteEl.find('.js-comment-save-button').click();
+ })
+ .then(timeoutPromise)
+ .then(() => {
+ const $updatedNoteEl = $notesContainer.find(`#note_${note.id}`);
+ expect($updatedNoteEl.hasClass('.being-posted')).toEqual(false); // Remove being-posted visuals
+ expect($updatedNoteEl.find('.note-text').text().trim()).toEqual(sampleComment); // See if comment reverted back to original
+ expect($('.flash-container').is(':visible')).toEqual(true); // Flash error message shown
+
+ done();
+ })
+ .catch(done.fail);
});
});
@@ -563,8 +627,12 @@ import Notes from '~/notes';
};
let $form;
let $notesContainer;
+ let mock;
beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mock.onPost(/(.*)\/notes$/).reply(200, note);
+
this.notes = new Notes('', []);
window.gon.current_username = 'root';
window.gon.current_user_fullname = 'Administrator';
@@ -582,15 +650,20 @@ import Notes from '~/notes';
$form.find('textarea.js-note-text').val(sampleComment);
});
- it('should remove slash command placeholder when comment with slash commands is done posting', () => {
- const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('should remove slash command placeholder when comment with slash commands is done posting', (done) => {
spyOn(gl.awardsHandler, 'addAwardToEmojiBar').and.callThrough();
$('.js-comment-button').click();
expect($notesContainer.find('.system-note.being-posted').length).toEqual(1); // Placeholder shown
- deferred.resolve(note);
- expect($notesContainer.find('.system-note.being-posted').length).toEqual(0); // Placeholder removed
+
+ setTimeout(() => {
+ expect($notesContainer.find('.system-note.being-posted').length).toEqual(0); // Placeholder removed
+ done();
+ });
});
});
@@ -607,8 +680,12 @@ import Notes from '~/notes';
};
let $form;
let $notesContainer;
+ let mock;
beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mock.onPost(/(.*)\/notes$/).reply(200, note);
+
this.notes = new Notes('', []);
window.gon.current_username = 'root';
window.gon.current_user_fullname = 'Administrator';
@@ -617,19 +694,24 @@ import Notes from '~/notes';
$form.find('textarea.js-note-text').html(sampleComment);
});
- it('should not render a script tag', () => {
- const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('should not render a script tag', (done) => {
$('.js-comment-button').click();
- deferred.resolve(note);
- const $noteEl = $notesContainer.find(`#note_${note.id}`);
- $noteEl.find('.js-note-edit').click();
- $noteEl.find('textarea.js-note-text').html(updatedComment);
- $noteEl.find('.js-comment-save-button').click();
+ setTimeout(() => {
+ const $noteEl = $notesContainer.find(`#note_${note.id}`);
+ $noteEl.find('.js-note-edit').click();
+ $noteEl.find('textarea.js-note-text').html(updatedComment);
+ $noteEl.find('.js-comment-save-button').click();
+
+ const $updatedNoteEl = $notesContainer.find(`#note_${note.id}`).find('.js-task-list-container');
+ expect($updatedNoteEl.find('.note-text').text().trim()).toEqual('');
- const $updatedNoteEl = $notesContainer.find(`#note_${note.id}`).find('.js-task-list-container');
- expect($updatedNoteEl.find('.note-text').text().trim()).toEqual('');
+ done();
+ });
});
});
diff --git a/spec/javascripts/pager_spec.js b/spec/javascripts/pager_spec.js
index 2fd87754238..b09494f0b77 100644
--- a/spec/javascripts/pager_spec.js
+++ b/spec/javascripts/pager_spec.js
@@ -1,5 +1,6 @@
/* global fixture */
-
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import * as utils from '~/lib/utils/url_utility';
import Pager from '~/pager';
@@ -9,7 +10,6 @@ describe('pager', () => {
beforeEach(() => {
setFixtures('<div class="content_list"></div><div class="loading"></div>');
- spyOn($, 'ajax');
});
afterEach(() => {
@@ -47,39 +47,90 @@ describe('pager', () => {
});
describe('getOld', () => {
+ const urlRegex = /(.*)some_list(.*)$/;
+ let mock;
+
+ function mockSuccess() {
+ mock.onGet(urlRegex).reply(200, {
+ count: 0,
+ html: '',
+ });
+ }
+
+ function mockError() {
+ mock.onGet(urlRegex).networkError();
+ }
+
beforeEach(() => {
setFixtures('<div class="content_list" data-href="/some_list"></div><div class="loading"></div>');
+ spyOn(axios, 'get').and.callThrough();
+
+ mock = new MockAdapter(axios);
+
Pager.init();
});
- it('shows loader while loading next page', () => {
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('shows loader while loading next page', (done) => {
+ mockSuccess();
+
spyOn(Pager.loading, 'show');
Pager.getOld();
- expect(Pager.loading.show).toHaveBeenCalled();
+
+ setTimeout(() => {
+ expect(Pager.loading.show).toHaveBeenCalled();
+
+ done();
+ });
});
- it('hides loader on success', () => {
- spyOn($, 'ajax').and.callFake(options => options.success({}));
+ it('hides loader on success', (done) => {
+ mockSuccess();
+
spyOn(Pager.loading, 'hide');
Pager.getOld();
- expect(Pager.loading.hide).toHaveBeenCalled();
+
+ setTimeout(() => {
+ expect(Pager.loading.hide).toHaveBeenCalled();
+
+ done();
+ });
});
- it('hides loader on error', () => {
- spyOn($, 'ajax').and.callFake(options => options.error());
+ it('hides loader on error', (done) => {
+ mockError();
+
spyOn(Pager.loading, 'hide');
Pager.getOld();
- expect(Pager.loading.hide).toHaveBeenCalled();
+
+ setTimeout(() => {
+ expect(Pager.loading.hide).toHaveBeenCalled();
+
+ done();
+ });
});
- it('sends request to url with offset and limit params', () => {
- spyOn($, 'ajax');
+ it('sends request to url with offset and limit params', (done) => {
Pager.offset = 100;
Pager.limit = 20;
Pager.getOld();
- const [{ data, url }] = $.ajax.calls.argsFor(0);
- expect(data).toBe('limit=20&offset=100');
- expect(url).toBe('/some_list');
+
+ setTimeout(() => {
+ const [url, params] = axios.get.calls.argsFor(0);
+
+ expect(params).toEqual({
+ params: {
+ limit: 20,
+ offset: 100,
+ },
+ });
+ expect(url).toBe('/some_list');
+
+ done();
+ });
});
});
});
diff --git a/spec/javascripts/pipeline_schedules/setup_pipeline_variable_list_spec.js b/spec/javascripts/pipeline_schedules/setup_pipeline_variable_list_spec.js
deleted file mode 100644
index 5b316b319a5..00000000000
--- a/spec/javascripts/pipeline_schedules/setup_pipeline_variable_list_spec.js
+++ /dev/null
@@ -1,145 +0,0 @@
-import {
- setupPipelineVariableList,
- insertRow,
- removeRow,
-} from '~/pipeline_schedules/setup_pipeline_variable_list';
-
-describe('Pipeline Variable List', () => {
- let $markup;
-
- describe('insertRow', () => {
- it('should insert another row', () => {
- $markup = $(`<div>
- <li class="js-row">
- <input>
- <textarea></textarea>
- </li>
- </div>`);
-
- insertRow($markup.find('.js-row'));
-
- expect($markup.find('.js-row').length).toBe(2);
- });
-
- it('should clear `data-is-persisted` on cloned row', () => {
- $markup = $(`<div>
- <li class="js-row" data-is-persisted="true"></li>
- </div>`);
-
- insertRow($markup.find('.js-row'));
-
- const $lastRow = $markup.find('.js-row').last();
- expect($lastRow.attr('data-is-persisted')).toBe(undefined);
- });
-
- it('should clear inputs on cloned row', () => {
- $markup = $(`<div>
- <li class="js-row">
- <input value="foo">
- <textarea>bar</textarea>
- </li>
- </div>`);
-
- insertRow($markup.find('.js-row'));
-
- const $lastRow = $markup.find('.js-row').last();
- expect($lastRow.find('input').val()).toBe('');
- expect($lastRow.find('textarea').val()).toBe('');
- });
- });
-
- describe('removeRow', () => {
- it('should remove dynamic row', () => {
- $markup = $(`<div>
- <li class="js-row">
- <input>
- <textarea></textarea>
- </li>
- </div>`);
-
- removeRow($markup.find('.js-row'));
-
- expect($markup.find('.js-row').length).toBe(0);
- });
-
- it('should hide and mark to destroy with already persisted rows', () => {
- $markup = $(`<div>
- <li class="js-row" data-is-persisted="true">
- <input class="js-destroy-input">
- </li>
- </div>`);
-
- const $row = $markup.find('.js-row');
- removeRow($row);
-
- expect($row.find('.js-destroy-input').val()).toBe('1');
- expect($markup.find('.js-row').length).toBe(1);
- });
- });
-
- describe('setupPipelineVariableList', () => {
- beforeEach(() => {
- $markup = $(`<form>
- <li class="js-row">
- <input class="js-user-input" name="schedule[variables_attributes][][key]">
- <textarea class="js-user-input" name="schedule[variables_attributes][][value]"></textarea>
- <button class="js-row-remove-button"></button>
- <button class="js-row-add-button"></button>
- </li>
- </form>`);
-
- setupPipelineVariableList($markup);
- });
-
- it('should remove the row when clicking the remove button', () => {
- $markup.find('.js-row-remove-button').trigger('click');
-
- expect($markup.find('.js-row').length).toBe(0);
- });
-
- it('should add another row when editing the last rows key input', () => {
- const $row = $markup.find('.js-row');
- $row.find('input.js-user-input')
- .val('foo')
- .trigger('input');
-
- expect($markup.find('.js-row').length).toBe(2);
- });
-
- it('should add another row when editing the last rows value textarea', () => {
- const $row = $markup.find('.js-row');
- $row.find('textarea.js-user-input')
- .val('foo')
- .trigger('input');
-
- expect($markup.find('.js-row').length).toBe(2);
- });
-
- it('should remove empty row after blurring', () => {
- const $row = $markup.find('.js-row');
- $row.find('input.js-user-input')
- .val('foo')
- .trigger('input');
-
- expect($markup.find('.js-row').length).toBe(2);
-
- $row.find('input.js-user-input')
- .val('')
- .trigger('input')
- .trigger('blur');
-
- expect($markup.find('.js-row').length).toBe(1);
- });
-
- it('should clear out the `name` attribute on the inputs for the last empty row on form submission (avoid BE validation)', () => {
- const $row = $markup.find('.js-row');
- expect($row.find('input').attr('name')).toBe('schedule[variables_attributes][][key]');
- expect($row.find('textarea').attr('name')).toBe('schedule[variables_attributes][][value]');
-
- $markup.filter('form').submit();
-
- expect($row.find('input').attr('name')).toBe('');
- expect($row.find('textarea').attr('name')).toBe('');
- });
- });
-});
diff --git a/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js b/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js
index b24567ffc0c..f6c0f51cf62 100644
--- a/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js
+++ b/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js
@@ -1,3 +1,5 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics';
import PANEL_STATE from '~/prometheus_metrics/constants';
import { metrics, missingVarMetrics } from './mock_data';
@@ -102,25 +104,38 @@ describe('PrometheusMetrics', () => {
describe('loadActiveMetrics', () => {
let prometheusMetrics;
+ let mock;
+
+ function mockSuccess() {
+ mock.onGet(prometheusMetrics.activeMetricsEndpoint).reply(200, {
+ data: metrics,
+ success: true,
+ });
+ }
+
+ function mockError() {
+ mock.onGet(prometheusMetrics.activeMetricsEndpoint).networkError();
+ }
beforeEach(() => {
+ spyOn(axios, 'get').and.callThrough();
+
prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring');
+
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
});
it('should show loader animation while response is being loaded and hide it when request is complete', (done) => {
- const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
+ mockSuccess();
prometheusMetrics.loadActiveMetrics();
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeFalsy();
- expect($.ajax).toHaveBeenCalledWith({
- url: prometheusMetrics.activeMetricsEndpoint,
- dataType: 'json',
- global: false,
- });
-
- deferred.resolve({ data: metrics, success: true });
+ expect(axios.get).toHaveBeenCalledWith(prometheusMetrics.activeMetricsEndpoint);
setTimeout(() => {
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
@@ -129,14 +144,10 @@ describe('PrometheusMetrics', () => {
});
it('should show empty state if response failed to load', (done) => {
- const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
- spyOn(prometheusMetrics, 'populateActiveMetrics');
+ mockError();
prometheusMetrics.loadActiveMetrics();
- deferred.reject();
-
setTimeout(() => {
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeFalsy();
@@ -145,14 +156,11 @@ describe('PrometheusMetrics', () => {
});
it('should populate metrics list once response is loaded', (done) => {
- const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
spyOn(prometheusMetrics, 'populateActiveMetrics');
+ mockSuccess();
prometheusMetrics.loadActiveMetrics();
- deferred.resolve({ data: metrics, success: true });
-
setTimeout(() => {
expect(prometheusMetrics.populateActiveMetrics).toHaveBeenCalledWith(metrics);
done();
diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js
index 3267e29585b..35bb630bf5d 100644
--- a/spec/javascripts/right_sidebar_spec.js
+++ b/spec/javascripts/right_sidebar_spec.js
@@ -1,6 +1,8 @@
/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-return-assign, new-cap, vars-on-top, max-len */
+import MockAdapter from 'axios-mock-adapter';
import '~/commons/bootstrap';
+import axios from '~/lib/utils/axios_utils';
import Sidebar from '~/right_sidebar';
(function() {
@@ -35,16 +37,23 @@ import Sidebar from '~/right_sidebar';
var fixtureName = 'issues/open-issue.html.raw';
preloadFixtures(fixtureName);
loadJSONFixtures('todos/todos.json');
+ let mock;
beforeEach(function() {
loadFixtures(fixtureName);
- this.sidebar = new Sidebar;
+ mock = new MockAdapter(axios);
+ this.sidebar = new Sidebar();
$aside = $('.right-sidebar');
$page = $('.layout-page');
$icon = $aside.find('i');
$toggle = $aside.find('.js-sidebar-toggle');
return $labelsIcon = $aside.find('.sidebar-collapsed-icon');
});
+
+ afterEach(() => {
+ mock.restore();
+ });
+
it('should expand/collapse the sidebar when arrow is clicked', function() {
assertSidebarState('expanded');
$toggle.click();
@@ -63,20 +72,19 @@ import Sidebar from '~/right_sidebar';
return assertSidebarState('collapsed');
});
- it('should broadcast todo:toggle event when add todo clicked', function() {
+ it('should broadcast todo:toggle event when add todo clicked', function(done) {
var todos = getJSONFixture('todos/todos.json');
- spyOn(jQuery, 'ajax').and.callFake(function() {
- var d = $.Deferred();
- var response = todos;
- d.resolve(response);
- return d.promise();
- });
+ mock.onPost(/(.*)\/todos$/).reply(200, todos);
var todoToggleSpy = spyOnEvent(document, 'todo:toggle');
$('.issuable-sidebar-header .js-issuable-todo').click();
- expect(todoToggleSpy.calls.count()).toEqual(1);
+ setTimeout(() => {
+ expect(todoToggleSpy.calls.count()).toEqual(1);
+
+ done();
+ });
});
it('should not hide collapsed icons', () => {
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js
index a750bc78f36..f14d5f6f76c 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js
@@ -1,39 +1,39 @@
import Vue from 'vue';
-import authorComponent from '~/vue_merge_request_widget/components/mr_widget_author';
-
-const author = {
- webUrl: 'http://foo.bar',
- avatarUrl: 'http://gravatar.com/foo',
- name: 'fatihacet',
-};
-const createComponent = () => {
- const Component = Vue.extend(authorComponent);
-
- return new Component({
- el: document.createElement('div'),
- propsData: { author },
- });
-};
+import authorComponent from '~/vue_merge_request_widget/components/mr_widget_author.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
describe('MRWidgetAuthor', () => {
- describe('props', () => {
- it('should have props', () => {
- const authorProp = authorComponent.props.author;
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(authorComponent);
+
+ vm = mountComponent(Component, {
+ author: {
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://localhost:3000/root',
+ avatarUrl: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ },
- expect(authorProp).toBeDefined();
- expect(authorProp.type instanceof Object).toBeTruthy();
- expect(authorProp.required).toBeTruthy();
});
});
- describe('template', () => {
- it('should have correct elements', () => {
- const el = createComponent().$el;
+ afterEach(() => {
+ vm.$destroy();
+ });
- expect(el.tagName).toEqual('A');
- expect(el.getAttribute('href')).toEqual(author.webUrl);
- expect(el.querySelector('img').getAttribute('src')).toEqual(author.avatarUrl);
- expect(el.querySelector('.author').innerText.trim()).toEqual(author.name);
- });
+ it('renders link with the author web url', () => {
+ expect(vm.$el.getAttribute('href')).toEqual('http://localhost:3000/root');
+ });
+
+ it('renders image with avatar url', () => {
+ expect(
+ vm.$el.querySelector('img').getAttribute('src'),
+ ).toEqual('http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon');
+ });
+
+ it('renders author name', () => {
+ expect(vm.$el.textContent.trim()).toEqual('Administrator');
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js
index 515ddcbb875..8c55622b15e 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js
@@ -1,61 +1,40 @@
import Vue from 'vue';
-import authorTimeComponent from '~/vue_merge_request_widget/components/mr_widget_author_time';
-
-const props = {
- actionText: 'Merged by',
- author: {
- webUrl: 'http://foo.bar',
- avatarUrl: 'http://gravatar.com/foo',
- name: 'fatihacet',
- },
- dateTitle: '2017-03-23T23:02:00.807Z',
- dateReadable: '12 hours ago',
-};
-const createComponent = () => {
- const Component = Vue.extend(authorTimeComponent);
-
- return new Component({
- el: document.createElement('div'),
- propsData: props,
- });
-};
+import authorTimeComponent from '~/vue_merge_request_widget/components/mr_widget_author_time.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
describe('MRWidgetAuthorTime', () => {
- describe('props', () => {
- it('should have props', () => {
- const { actionText, author, dateTitle, dateReadable } = authorTimeComponent.props;
- const ActionTextClass = actionText.type;
- const DateTitleClass = dateTitle.type;
- const DateReadableClass = dateReadable.type;
-
- expect(new ActionTextClass() instanceof String).toBeTruthy();
- expect(actionText.required).toBeTruthy();
-
- expect(author.type instanceof Object).toBeTruthy();
- expect(author.required).toBeTruthy();
-
- expect(new DateTitleClass() instanceof String).toBeTruthy();
- expect(dateTitle.required).toBeTruthy();
-
- expect(new DateReadableClass() instanceof String).toBeTruthy();
- expect(dateReadable.required).toBeTruthy();
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(authorTimeComponent);
+
+ vm = mountComponent(Component, {
+ actionText: 'Merged by',
+ author: {
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://localhost:3000/root',
+ avatarUrl: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ },
+ dateTitle: '2017-03-23T23:02:00.807Z',
+ dateReadable: '12 hours ago',
});
});
- describe('components', () => {
- it('should have components', () => {
- expect(authorTimeComponent.components['mr-widget-author']).toBeDefined();
- });
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders provided action text', () => {
+ expect(vm.$el.textContent).toContain('Merged by');
});
- describe('template', () => {
- it('should have correct elements', () => {
- const el = createComponent().$el;
+ it('renders author', () => {
+ expect(vm.$el.textContent).toContain('Administrator');
+ });
- expect(el.tagName).toEqual('H4');
- expect(el.querySelector('a').getAttribute('href')).toEqual(props.author.webUrl);
- expect(el.querySelector('time').innerText).toContain(props.dateReadable);
- expect(el.querySelector('time').getAttribute('title')).toEqual(props.dateTitle);
- });
+ it('renders provided time', () => {
+ expect(vm.$el.querySelector('time').getAttribute('title')).toEqual('2017-03-23T23:02:00.807Z');
+ expect(vm.$el.querySelector('time').textContent.trim()).toEqual('12 hours ago');
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
index 93bb83ca8bd..13e5595bbfc 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
@@ -1,121 +1,220 @@
import Vue from 'vue';
-import headerComponent from '~/vue_merge_request_widget/components/mr_widget_header';
-
-const createComponent = (mr) => {
- const Component = Vue.extend(headerComponent);
- return new Component({
- el: document.createElement('div'),
- propsData: { mr },
- });
-};
+import headerComponent from '~/vue_merge_request_widget/components/mr_widget_header.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
describe('MRWidgetHeader', () => {
- describe('props', () => {
- it('should have props', () => {
- const { mr } = headerComponent.props;
+ let vm;
+ let Component;
- expect(mr.type instanceof Object).toBeTruthy();
- expect(mr.required).toBeTruthy();
- });
+ beforeEach(() => {
+ Component = Vue.extend(headerComponent);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
});
describe('computed', () => {
- let vm;
- beforeEach(() => {
- vm = createComponent({
- divergedCommitsCount: 12,
- sourceBranch: 'mr-widget-refactor',
- sourceBranchLink: '/foo/bar/mr-widget-refactor',
- targetBranch: 'master',
+ describe('shouldShowCommitsBehindText', () => {
+ it('return true when there are divergedCommitsCount', () => {
+ vm = mountComponent(Component, { mr: {
+ divergedCommitsCount: 12,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
+ targetBranch: 'master',
+ } });
+
+ expect(vm.shouldShowCommitsBehindText).toEqual(true);
+ });
+
+ it('returns false where there are no divergedComits count', () => {
+ vm = mountComponent(Component, { mr: {
+ divergedCommitsCount: 0,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
+ targetBranch: 'master',
+ } });
+ expect(vm.shouldShowCommitsBehindText).toEqual(false);
});
});
- it('shouldShowCommitsBehindText', () => {
- expect(vm.shouldShowCommitsBehindText).toBeTruthy();
+ describe('commitsText', () => {
+ it('returns singular when there is one commit', () => {
+ vm = mountComponent(Component, { mr: {
+ divergedCommitsCount: 1,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
+ targetBranch: 'master',
+ } });
- vm.mr.divergedCommitsCount = 0;
- expect(vm.shouldShowCommitsBehindText).toBeFalsy();
- });
+ expect(vm.commitsText).toEqual('1 commit behind');
+ });
- it('commitsText', () => {
- expect(vm.commitsText).toEqual('commits');
+ it('returns plural when there is more than one commit', () => {
+ vm = mountComponent(Component, { mr: {
+ divergedCommitsCount: 2,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
+ targetBranch: 'master',
+ } });
- vm.mr.divergedCommitsCount = 1;
- expect(vm.commitsText).toEqual('commit');
+ expect(vm.commitsText).toEqual('2 commits behind');
+ });
});
});
describe('template', () => {
- let vm;
- let el;
- let mr;
- const sourceBranchPath = '/foo/bar/mr-widget-refactor';
-
- beforeEach(() => {
- mr = {
- divergedCommitsCount: 12,
- sourceBranch: 'mr-widget-refactor',
- sourceBranchLink: `<a href="${sourceBranchPath}">mr-widget-refactor</a>`,
- sourceBranchRemoved: false,
- targetBranchPath: 'foo/bar/commits-path',
- targetBranchTreePath: 'foo/bar/tree/path',
- targetBranch: 'master',
- isOpen: true,
- emailPatchesPath: '/mr/email-patches',
- plainDiffPath: '/mr/plainDiffPath',
- };
-
- vm = createComponent(mr);
- el = vm.$el;
+ describe('common elements', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, { mr: {
+ divergedCommitsCount: 12,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+ sourceBranchRemoved: false,
+ targetBranchPath: 'foo/bar/commits-path',
+ targetBranchTreePath: 'foo/bar/tree/path',
+ targetBranch: 'master',
+ isOpen: true,
+ emailPatchesPath: '/mr/email-patches',
+ plainDiffPath: '/mr/plainDiffPath',
+ } });
+ });
+
+ it('renders source branch link', () => {
+ expect(
+ vm.$el.querySelector('.js-source-branch').innerHTML,
+ ).toEqual('<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>');
+ });
+
+ it('renders clipboard button', () => {
+ expect(vm.$el.querySelector('.btn-clipboard')).not.toEqual(null);
+ });
+
+ it('renders target branch', () => {
+ expect(vm.$el.querySelector('.js-target-branch').textContent.trim()).toEqual('master');
+ });
});
- it('should render template elements correctly', () => {
- expect(el.classList.contains('mr-source-target')).toBeTruthy();
- const sourceBranchLink = el.querySelectorAll('.label-branch')[0];
- const targetBranchLink = el.querySelectorAll('.label-branch')[1];
- const commitsCount = el.querySelector('.diverged-commits-count');
+ describe('with an open merge request', () => {
+ afterEach(() => {
+ vm.$destroy();
+ });
- expect(sourceBranchLink.textContent).toContain(mr.sourceBranch);
- expect(targetBranchLink.textContent).toContain(mr.targetBranch);
- expect(sourceBranchLink.querySelector('a').getAttribute('href')).toEqual(sourceBranchPath);
- expect(targetBranchLink.querySelector('a').getAttribute('href')).toEqual(mr.targetBranchTreePath);
- expect(commitsCount.textContent).toContain('12 commits behind');
- expect(commitsCount.querySelector('a').getAttribute('href')).toEqual(mr.targetBranchPath);
+ beforeEach(() => {
+ vm = mountComponent(Component, { mr: {
+ divergedCommitsCount: 12,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+ sourceBranchRemoved: false,
+ targetBranchPath: 'foo/bar/commits-path',
+ targetBranchTreePath: 'foo/bar/tree/path',
+ targetBranch: 'master',
+ isOpen: true,
+ emailPatchesPath: '/mr/email-patches',
+ plainDiffPath: '/mr/plainDiffPath',
+ } });
+ });
+
+ it('renders checkout branch button with modal trigger', () => {
+ const button = vm.$el.querySelector('.js-check-out-branch');
+
+ expect(button.textContent.trim()).toEqual('Check out branch');
+ expect(button.getAttribute('data-target')).toEqual('#modal_merge_info');
+ expect(button.getAttribute('data-toggle')).toEqual('modal');
+ });
+
+ it('renders download dropdown with links', () => {
+ expect(
+ vm.$el.querySelector('.js-download-email-patches').textContent.trim(),
+ ).toEqual('Email patches');
- expect(el.textContent).toContain('Check out branch');
- expect(el.querySelectorAll('.dropdown li a')[0].getAttribute('href')).toEqual(mr.emailPatchesPath);
- expect(el.querySelectorAll('.dropdown li a')[1].getAttribute('href')).toEqual(mr.plainDiffPath);
+ expect(
+ vm.$el.querySelector('.js-download-email-patches').getAttribute('href'),
+ ).toEqual('/mr/email-patches');
- expect(el.querySelector('a[href="#modal_merge_info"]').getAttribute('disabled')).toBeNull();
+ expect(
+ vm.$el.querySelector('.js-download-plain-diff').textContent.trim(),
+ ).toEqual('Plain diff');
+
+ expect(
+ vm.$el.querySelector('.js-download-plain-diff').getAttribute('href'),
+ ).toEqual('/mr/plainDiffPath');
+ });
});
- it('should not have right action links if the MR state is not open', (done) => {
- vm.mr.isOpen = false;
- Vue.nextTick(() => {
- expect(el.textContent).not.toContain('Check out branch');
- expect(el.querySelectorAll('.dropdown li a').length).toEqual(0);
- done();
+ describe('with a closed merge request', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, { mr: {
+ divergedCommitsCount: 12,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+ sourceBranchRemoved: false,
+ targetBranchPath: 'foo/bar/commits-path',
+ targetBranchTreePath: 'foo/bar/tree/path',
+ targetBranch: 'master',
+ isOpen: false,
+ emailPatchesPath: '/mr/email-patches',
+ plainDiffPath: '/mr/plainDiffPath',
+ } });
+ });
+
+ it('does not render checkout branch button with modal trigger', () => {
+ const button = vm.$el.querySelector('.js-check-out-branch');
+
+ expect(button).toEqual(null);
+ });
+
+ it('does not render download dropdown with links', () => {
+ expect(
+ vm.$el.querySelector('.js-download-email-patches'),
+ ).toEqual(null);
+
+ expect(
+ vm.$el.querySelector('.js-download-plain-diff'),
+ ).toEqual(null);
});
});
- it('should not render diverged commits count if the MR has no diverged commits', (done) => {
- vm.mr.divergedCommitsCount = null;
- Vue.nextTick(() => {
- expect(el.textContent).not.toContain('commits behind');
- expect(el.querySelectorAll('.diverged-commits-count').length).toEqual(0);
- done();
+ describe('without diverged commits', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, { mr: {
+ divergedCommitsCount: 0,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+ sourceBranchRemoved: false,
+ targetBranchPath: 'foo/bar/commits-path',
+ targetBranchTreePath: 'foo/bar/tree/path',
+ targetBranch: 'master',
+ isOpen: true,
+ emailPatchesPath: '/mr/email-patches',
+ plainDiffPath: '/mr/plainDiffPath',
+ } });
+ });
+
+ it('does not render diverged commits info', () => {
+ expect(vm.$el.querySelector('.diverged-commits-count')).toEqual(null);
});
});
- it('should disable check out branch button if source branch has been removed', (done) => {
- vm.mr.sourceBranchRemoved = true;
+ describe('with diverged commits', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, { mr: {
+ divergedCommitsCount: 12,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+ sourceBranchRemoved: false,
+ targetBranchPath: 'foo/bar/commits-path',
+ targetBranchTreePath: 'foo/bar/tree/path',
+ targetBranch: 'master',
+ isOpen: true,
+ emailPatchesPath: '/mr/email-patches',
+ plainDiffPath: '/mr/plainDiffPath',
+ } });
+ });
- Vue.nextTick()
- .then(() => {
- expect(el.querySelector('a[href="#modal_merge_info"]').getAttribute('disabled')).toBe('disabled');
- done();
- })
- .catch(done.fail);
+ it('renders diverged commits info', () => {
+ expect(vm.$el.querySelector('.diverged-commits-count').textContent.trim()).toEqual('(12 commits behind)');
+ });
});
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js
index 4da4fc82c26..cc43639f576 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js
@@ -1,51 +1,56 @@
import Vue from 'vue';
-import mergeHelpComponent from '~/vue_merge_request_widget/components/mr_widget_merge_help';
-
-const props = {
- missingBranch: 'this-is-not-the-branch-you-are-looking-for',
-};
-const text = `If the ${props.missingBranch} branch exists in your local repository`;
-
-const createComponent = () => {
- const Component = Vue.extend(mergeHelpComponent);
- return new Component({
- el: document.createElement('div'),
- propsData: props,
- });
-};
+import mergeHelpComponent from '~/vue_merge_request_widget/components/mr_widget_merge_help.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
describe('MRWidgetMergeHelp', () => {
- describe('props', () => {
- it('should have props', () => {
- const { missingBranch } = mergeHelpComponent.props;
- const MissingBranchTypeClass = missingBranch.type;
-
- expect(new MissingBranchTypeClass() instanceof String).toBeTruthy();
- expect(missingBranch.required).toBeFalsy();
- expect(missingBranch.default).toEqual('');
- });
+ let vm;
+ let Component;
+
+ beforeEach(() => {
+ Component = Vue.extend(mergeHelpComponent);
});
- describe('template', () => {
- let vm;
- let el;
+ afterEach(() => {
+ vm.$destroy();
+ });
+ describe('with missing branch', () => {
beforeEach(() => {
- vm = createComponent();
- el = vm.$el;
+ vm = mountComponent(Component, {
+ missingBranch: 'this-is-not-the-branch-you-are-looking-for',
+ });
});
- it('should have the correct elements', () => {
- expect(el.classList.contains('mr-widget-help')).toBeTruthy();
- expect(el.textContent).toContain(text);
+ it('renders missing branch information', () => {
+ expect(
+ vm.$el.textContent.trim().replace(/[\r\n]+/g, ' ').replace(/\s\s+/g, ' '),
+ ).toEqual(
+ 'If the this-is-not-the-branch-you-are-looking-for branch exists in your local repository, you can merge this merge request manually using the command line',
+ );
});
- it('should not show missing branch name if missingBranch props is not provided', (done) => {
- vm.missingBranch = null;
- Vue.nextTick(() => {
- expect(el.textContent).not.toContain(text);
- done();
- });
+ it('renders button to open help modal', () => {
+ expect(vm.$el.querySelector('.js-open-modal-help').getAttribute('data-target')).toEqual('#modal_merge_info');
+ expect(vm.$el.querySelector('.js-open-modal-help').getAttribute('data-toggle')).toEqual('modal');
+ });
+ });
+
+ describe('without missing branch', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component);
+ });
+
+ it('renders information about how to merge manually', () => {
+ expect(
+ vm.$el.textContent.trim().replace(/[\r\n]+/g, ' ').replace(/\s\s+/g, ' '),
+ ).toEqual(
+ 'You can merge this merge request manually using the command line',
+ );
+ });
+
+ it('renders element to open a modal', () => {
+ expect(vm.$el.querySelector('.js-open-modal-help').getAttribute('data-target')).toEqual('#modal_merge_info');
+ expect(vm.$el.querySelector('.js-open-modal-help').getAttribute('data-toggle')).toEqual('modal');
});
});
});
diff --git a/spec/javascripts/vue_shared/components/confirmation_input_spec.js b/spec/javascripts/vue_shared/components/confirmation_input_spec.js
new file mode 100644
index 00000000000..a6a12614e77
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/confirmation_input_spec.js
@@ -0,0 +1,63 @@
+import Vue from 'vue';
+import confirmationInput from '~/vue_shared/components/confirmation_input.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('Confirmation input component', () => {
+ const Component = Vue.extend(confirmationInput);
+ const props = {
+ inputId: 'dummy-id',
+ confirmationKey: 'confirmation-key',
+ confirmationValue: 'confirmation-value',
+ };
+ let vm;
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('props', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, props);
+ });
+
+ it('sets id of the input field to inputId', () => {
+ expect(vm.$refs.enteredValue.id).toBe(props.inputId);
+ });
+
+ it('sets name of the input field to confirmationKey', () => {
+ expect(vm.$refs.enteredValue.name).toBe(props.confirmationKey);
+ });
+ });
+
+ describe('computed', () => {
+ describe('inputLabel', () => {
+ it('escapes confirmationValue by default', () => {
+ vm = mountComponent(Component, { ...props, confirmationValue: 'n<e></e>ds escap"ng' });
+ expect(vm.inputLabel).toBe('Type <code>n&lt;e&gt;&lt;/e&gt;ds escap&quot;ng</code> to confirm:');
+ });
+
+ it('does not escape confirmationValue if escapeValue is false', () => {
+ vm = mountComponent(Component, { ...props, confirmationValue: 'n<e></e>ds escap"ng', shouldEscapeConfirmationValue: false });
+ expect(vm.inputLabel).toBe('Type <code>n<e></e>ds escap"ng</code> to confirm:');
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('hasCorrectValue', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, props);
+ });
+
+ it('returns false if entered value is incorrect', () => {
+ vm.$refs.enteredValue.value = 'incorrect';
+ expect(vm.hasCorrectValue()).toBe(false);
+ });
+
+ it('returns true if entered value is correct', () => {
+ vm.$refs.enteredValue.value = props.confirmationValue;
+ expect(vm.hasCorrectValue()).toBe(true);
+ });
+ });
+ });
+});
diff --git a/spec/lib/banzai/color_parser_spec.rb b/spec/lib/banzai/color_parser_spec.rb
new file mode 100644
index 00000000000..a1cb0c07b06
--- /dev/null
+++ b/spec/lib/banzai/color_parser_spec.rb
@@ -0,0 +1,90 @@
+require 'spec_helper'
+
+describe Banzai::ColorParser do
+ describe '.parse' do
+ context 'HEX format' do
+ [
+ '#abc', '#ABC',
+ '#d2d2d2', '#D2D2D2',
+ '#123a', '#123A',
+ '#123456aa', '#123456AA'
+ ].each do |color|
+ it "parses the valid hex color #{color}" do
+ expect(subject.parse(color)).to eq(color)
+ end
+ end
+
+ [
+ '#', '#1', '#12', '#12g', '#12G',
+ '#12345', '#r2r2r2', '#R2R2R2', '#1234567',
+ '# 123', '# 1234', '# 123456', '# 12345678',
+ '#1 2 3', '#123 4', '#12 34 56', '#123456 78'
+ ].each do |color|
+ it "does not parse the invalid hex color #{color}" do
+ expect(subject.parse(color)).to be_nil
+ end
+ end
+ end
+
+ context 'RGB format' do
+ [
+ 'rgb(0,0,0)', 'rgb(255,255,255)',
+ 'rgb(0, 0, 0)', 'RGB(0,0,0)',
+ 'rgb(0,0,0,0)', 'rgb(0,0,0,0.0)', 'rgb(0,0,0,.0)',
+ 'rgb(0,0,0, 0)', 'rgb(0,0,0, 0.0)', 'rgb(0,0,0, .0)',
+ 'rgb(0,0,0,1)', 'rgb(0,0,0,1.0)',
+ 'rgba(0,0,0)', 'rgba(0,0,0,0)', 'RGBA(0,0,0)',
+ 'rgb(0%,0%,0%)', 'rgba(0%,0%,0%,0%)'
+ ].each do |color|
+ it "parses the valid rgb color #{color}" do
+ expect(subject.parse(color)).to eq(color)
+ end
+ end
+
+ [
+ 'FOOrgb(0,0,0)', 'rgb(0,0,0)BAR',
+ 'rgb(0,0,-1)', 'rgb(0,0,-0)', 'rgb(0,0,256)',
+ 'rgb(0,0,0,-0.1)', 'rgb(0,0,0,-0.0)', 'rgb(0,0,0,-.1)',
+ 'rgb(0,0,0,1.1)', 'rgb(0,0,0,2)',
+ 'rgba(0,0,0,)', 'rgba(0,0,0,0.)', 'rgba(0,0,0,1.)',
+ 'rgb(0,0,0%)', 'rgb(101%,0%,0%)'
+ ].each do |color|
+ it "does not parse the invalid rgb color #{color}" do
+ expect(subject.parse(color)).to be_nil
+ end
+ end
+ end
+
+ context 'HSL format' do
+ [
+ 'hsl(0,0%,0%)', 'hsl(0,100%,100%)',
+ 'hsl(540,0%,0%)', 'hsl(-720,0%,0%)',
+ 'hsl(0deg,0%,0%)', 'hsl(0DEG,0%,0%)',
+ 'hsl(0, 0%, 0%)', 'HSL(0,0%,0%)',
+ 'hsl(0,0%,0%,0)', 'hsl(0,0%,0%,0.0)', 'hsl(0,0%,0%,.0)',
+ 'hsl(0,0%,0%, 0)', 'hsl(0,0%,0%, 0.0)', 'hsl(0,0%,0%, .0)',
+ 'hsl(0,0%,0%,1)', 'hsl(0,0%,0%,1.0)',
+ 'hsla(0,0%,0%)', 'hsla(0,0%,0%,0)', 'HSLA(0,0%,0%)',
+ 'hsl(1rad,0%,0%)', 'hsl(1.1rad,0%,0%)', 'hsl(.1rad,0%,0%)',
+ 'hsl(-1rad,0%,0%)', 'hsl(1RAD,0%,0%)'
+ ].each do |color|
+ it "parses the valid hsl color #{color}" do
+ expect(subject.parse(color)).to eq(color)
+ end
+ end
+
+ [
+ 'hsl(+0,0%,0%)', 'hsl(0,0,0%)', 'hsl(0,0%,0)', 'hsl(0 deg,0%,0%)',
+ 'hsl(0,-0%,0%)', 'hsl(0,101%,0%)', 'hsl(0,-1%,0%)',
+ 'hsl(0,0%,0%,-0.1)', 'hsl(0,0%,0%,-.1)',
+ 'hsl(0,0%,0%,1.1)', 'hsl(0,0%,0%,2)',
+ 'hsl(0,0%,0%,)', 'hsl(0,0%,0%,0.)', 'hsl(0,0%,0%,1.)',
+ 'hsl(deg,0%,0%)', 'hsl(rad,0%,0%)'
+ ].each do |color|
+ it "does not parse the invalid hsl color #{color}" do
+ expect(subject.parse(color)).to be_nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/banzai/filter/color_filter_spec.rb b/spec/lib/banzai/filter/color_filter_spec.rb
new file mode 100644
index 00000000000..a098b037510
--- /dev/null
+++ b/spec/lib/banzai/filter/color_filter_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+
+describe Banzai::Filter::ColorFilter, lib: true do
+ include FilterSpecHelper
+
+ let(:color) { '#F00' }
+ let(:color_chip_selector) { 'code > span.gfm-color_chip > span' }
+
+ ['#123', '#1234', '#123456', '#12345678',
+ 'rgb(0,0,0)', 'RGB(0, 0, 0)', 'rgba(0,0,0,1)', 'RGBA(0,0,0,0.7)',
+ 'hsl(270,30%,50%)', 'HSLA(270, 30%, 50%, .7)'].each do |color|
+ it "inserts color chip for supported color format #{color}" do
+ content = code_tag(color)
+ doc = filter(content)
+ color_chip = doc.at_css(color_chip_selector)
+
+ expect(color_chip.content).to be_empty
+ expect(color_chip.parent[:class]).to eq 'gfm-color_chip'
+ expect(color_chip[:style]).to eq "background-color: #{color};"
+ end
+ end
+
+ it 'ignores valid color code without backticks(code tags)' do
+ doc = filter(color)
+
+ expect(doc.css('span.gfm-color_chip').size).to be_zero
+ end
+
+ it 'ignores valid color code with prepended space' do
+ content = code_tag(' ' + color)
+ doc = filter(content)
+
+ expect(doc.css(color_chip_selector).size).to be_zero
+ end
+
+ it 'ignores valid color code with appended space' do
+ content = code_tag(color + ' ')
+ doc = filter(content)
+
+ expect(doc.css(color_chip_selector).size).to be_zero
+ end
+
+ it 'ignores valid color code surrounded by spaces' do
+ content = code_tag(' ' + color + ' ')
+ doc = filter(content)
+
+ expect(doc.css(color_chip_selector).size).to be_zero
+ end
+
+ it 'ignores invalid color code' do
+ invalid_color = '#BAR'
+ content = code_tag(invalid_color)
+ doc = filter(content)
+
+ expect(doc.css(color_chip_selector).size).to be_zero
+ end
+
+ def code_tag(string)
+ "<code>#{string}</code>"
+ end
+end
diff --git a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb
index 935146c17fc..a41a28a56f1 100644
--- a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb
@@ -53,7 +53,7 @@ describe Banzai::Filter::CommitRangeReferenceFilter do
doc = reference_filter("See (#{reference}.)")
exp = Regexp.escape(range.reference_link_text)
- expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/)
+ expect(doc.to_html).to match(%r{\(<a.+>#{exp}</a>\.\)})
end
it 'ignores invalid commit IDs' do
@@ -222,7 +222,7 @@ describe Banzai::Filter::CommitRangeReferenceFilter do
doc = reference_filter("Fixed (#{reference}.)")
exp = Regexp.escape(range.reference_link_text(project))
- expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/)
+ expect(doc.to_html).to match(%r{\(<a.+>#{exp}</a>\.\)})
end
it 'ignores invalid commit IDs on the referenced project' do
diff --git a/spec/lib/banzai/filter/commit_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_reference_filter_spec.rb
index 080a5f57da9..35f8792ff35 100644
--- a/spec/lib/banzai/filter/commit_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/commit_reference_filter_spec.rb
@@ -42,7 +42,7 @@ describe Banzai::Filter::CommitReferenceFilter do
it 'links with adjacent text' do
doc = reference_filter("See (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>#{commit.short_id}<\/a>\.\)/)
+ expect(doc.to_html).to match(%r{\(<a.+>#{commit.short_id}</a>\.\)})
end
it 'ignores invalid commit IDs' do
@@ -199,12 +199,12 @@ describe Banzai::Filter::CommitReferenceFilter do
it 'links with adjacent text' do
doc = reference_filter("Fixed (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>#{commit.reference_link_text(project)}<\/a>\.\)/)
+ expect(doc.to_html).to match(%r{\(<a.+>#{commit.reference_link_text(project)}</a>\.\)})
end
it 'ignores invalid commit IDs on the referenced project' do
act = "Committed #{invalidate_reference(reference)}"
- expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/)
+ expect(reference_filter(act).to_html).to match(%r{<a.+>#{Regexp.escape(invalidate_reference(reference))}</a>})
end
end
end
diff --git a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
index a0d391d981c..d9018a7e4fe 100644
--- a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
@@ -49,7 +49,7 @@ describe Banzai::Filter::ExternalIssueReferenceFilter do
it 'links with adjacent text' do
doc = filter("Issue (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>#{reference}<\/a>\.\)/)
+ expect(doc.to_html).to match(%r{\(<a.+>#{reference}</a>\.\)})
end
it 'includes a title attribute' do
diff --git a/spec/lib/banzai/filter/image_link_filter_spec.rb b/spec/lib/banzai/filter/image_link_filter_spec.rb
index 51920869545..c84b98eb225 100644
--- a/spec/lib/banzai/filter/image_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/image_link_filter_spec.rb
@@ -14,7 +14,7 @@ describe Banzai::Filter::ImageLinkFilter do
it 'does not wrap a duplicate link' do
doc = filter(%Q(<a href="/whatever">#{image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')}</a>))
- expect(doc.to_html).to match /^<a href="\/whatever"><img[^>]*><\/a>$/
+ expect(doc.to_html).to match %r{^<a href="/whatever"><img[^>]*></a>$}
end
it 'works with external images' do
@@ -24,6 +24,6 @@ describe Banzai::Filter::ImageLinkFilter do
it 'works with inline images' do
doc = filter(%Q(<p>test #{image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')} inline</p>))
- expect(doc.to_html).to match /^<p>test <a[^>]*><img[^>]*><\/a> inline<\/p>$/
+ expect(doc.to_html).to match %r{^<p>test <a[^>]*><img[^>]*></a> inline</p>$}
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 3a5f52ea23f..905fbb9434b 100644
--- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
@@ -288,7 +288,7 @@ describe Banzai::Filter::IssueReferenceFilter do
it 'links with adjacent text' do
doc = reference_filter("Fixed (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(issue.to_reference(project))} \(comment 123\)<\/a>\.\)/)
+ expect(doc.to_html).to match(%r{\(<a.+>#{Regexp.escape(issue.to_reference(project))} \(comment 123\)</a>\.\)})
end
it 'includes default classes' do
@@ -317,7 +317,7 @@ describe Banzai::Filter::IssueReferenceFilter do
it 'links with adjacent text' do
doc = reference_filter("Fixed (#{reference_link}.)")
- expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/)
+ expect(doc.to_html).to match(%r{\(<a.+>Reference</a>\.\)})
end
it 'includes default classes' do
@@ -346,7 +346,7 @@ describe Banzai::Filter::IssueReferenceFilter do
it 'links with adjacent text' do
doc = reference_filter("Fixed (#{reference_link}.)")
- expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/)
+ expect(doc.to_html).to match(%r{\(<a.+>Reference</a>\.\)})
end
it 'includes default classes' do
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 158844e25ae..eeb82822f68 100644
--- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
@@ -42,7 +42,7 @@ describe Banzai::Filter::MergeRequestReferenceFilter do
it 'links with adjacent text' do
doc = reference_filter("Merge (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
+ expect(doc.to_html).to match(%r{\(<a.+>#{Regexp.escape(reference)}</a>\.\)})
end
it 'ignores invalid merge IDs' do
@@ -211,7 +211,7 @@ describe Banzai::Filter::MergeRequestReferenceFilter do
it 'links with adjacent text' do
doc = reference_filter("Merge (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(merge.to_reference(project))} \(diffs, comment 123\)<\/a>\.\)/)
+ expect(doc.to_html).to match(%r{\(<a.+>#{Regexp.escape(merge.to_reference(project))} \(diffs, comment 123\)</a>\.\)})
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 3a07a6dc179..e068e02d4fc 100644
--- a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
@@ -28,7 +28,7 @@ describe Banzai::Filter::SnippetReferenceFilter do
it 'links with adjacent text' do
doc = reference_filter("Snippet (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
+ expect(doc.to_html).to match(%r{\(<a.+>#{Regexp.escape(reference)}</a>\.\)})
end
it 'ignores invalid snippet IDs' do
@@ -192,13 +192,13 @@ describe Banzai::Filter::SnippetReferenceFilter do
it 'links with adjacent text' do
doc = reference_filter("See (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(snippet.to_reference(project))}<\/a>\.\)/)
+ expect(doc.to_html).to match(%r{\(<a.+>#{Regexp.escape(snippet.to_reference(project))}</a>\.\)})
end
it 'ignores invalid snippet IDs on the referenced project' do
act = "See #{invalidate_reference(reference)}"
- expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/)
+ expect(reference_filter(act).to_html).to match(%r{<a.+>#{Regexp.escape(invalidate_reference(reference))}</a>})
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 c76adc262fc..2f86a046d28 100644
--- a/spec/lib/banzai/filter/user_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb
@@ -146,7 +146,7 @@ describe Banzai::Filter::UserReferenceFilter do
it 'links with adjacent text' do
doc = reference_filter("Mention me (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>#{reference}<\/a>\.\)/)
+ expect(doc.to_html).to match(%r{\(<a.+>#{reference}</a>\.\)})
end
it 'includes default classes' do
@@ -172,7 +172,7 @@ describe Banzai::Filter::UserReferenceFilter do
it 'links with adjacent text' do
doc = reference_filter("Mention me (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>User<\/a>\.\)/)
+ expect(doc.to_html).to match(%r{\(<a.+>User</a>\.\)})
end
it 'includes a data-user attribute' do
diff --git a/spec/lib/file_size_validator_spec.rb b/spec/lib/file_size_validator_spec.rb
index c44bc1840df..ebd907ecb7f 100644
--- a/spec/lib/file_size_validator_spec.rb
+++ b/spec/lib/file_size_validator_spec.rb
@@ -2,8 +2,8 @@ require 'spec_helper'
describe FileSizeValidator do
let(:validator) { described_class.new(options) }
- let(:attachment) { AttachmentUploader.new }
let(:note) { create(:note) }
+ let(:attachment) { AttachmentUploader.new(note) }
describe 'options uses an integer' do
let(:options) { { maximum: 10, attributes: { attachment: attachment } } }
diff --git a/spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_spec.rb b/spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_spec.rb
index 4cdb679c97f..2b69e718e08 100644
--- a/spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_spec.rb
+++ b/spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_spec.rb
@@ -8,7 +8,7 @@ describe Gitlab::BackgroundMigration::PopulateMergeRequestMetricsWithEventsData,
end
after do
- [MergeRequest, MergeRequestDiff].each(&:reset_column_information)
+ [Project, MergeRequest, MergeRequestDiff].each(&:reset_column_information)
end
describe '#perform' do
diff --git a/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb b/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb
index 8bb9ebe0419..370c2490b97 100644
--- a/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb
+++ b/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb
@@ -23,6 +23,27 @@ describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq do
end
end
+ # E.g. The installation is in use at the time of migration, and someone has
+ # just uploaded a file
+ shared_examples 'does not add files in /uploads/tmp' do
+ let(:tmp_file) { Rails.root.join(described_class::ABSOLUTE_UPLOAD_DIR, 'tmp', 'some_file.jpg') }
+
+ before do
+ FileUtils.mkdir(File.dirname(tmp_file))
+ FileUtils.touch(tmp_file)
+ end
+
+ after do
+ FileUtils.rm(tmp_file)
+ end
+
+ it 'does not add files from /uploads/tmp' do
+ described_class.new.perform
+
+ expect(untracked_files_for_uploads.count).to eq(5)
+ end
+ end
+
it 'ensures the untracked_files_for_uploads table exists' do
expect do
described_class.new.perform
@@ -109,24 +130,8 @@ describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq do
end
end
- # E.g. The installation is in use at the time of migration, and someone has
- # just uploaded a file
context 'when there are files in /uploads/tmp' do
- let(:tmp_file) { Rails.root.join(described_class::ABSOLUTE_UPLOAD_DIR, 'tmp', 'some_file.jpg') }
-
- before do
- FileUtils.touch(tmp_file)
- end
-
- after do
- FileUtils.rm(tmp_file)
- end
-
- it 'does not add files from /uploads/tmp' do
- described_class.new.perform
-
- expect(untracked_files_for_uploads.count).to eq(5)
- end
+ it_behaves_like 'does not add files in /uploads/tmp'
end
end
end
@@ -197,24 +202,8 @@ describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq do
end
end
- # E.g. The installation is in use at the time of migration, and someone has
- # just uploaded a file
context 'when there are files in /uploads/tmp' do
- let(:tmp_file) { Rails.root.join(described_class::ABSOLUTE_UPLOAD_DIR, 'tmp', 'some_file.jpg') }
-
- before do
- FileUtils.touch(tmp_file)
- end
-
- after do
- FileUtils.rm(tmp_file)
- end
-
- it 'does not add files from /uploads/tmp' do
- described_class.new.perform
-
- expect(untracked_files_for_uploads.count).to eq(5)
- end
+ it_behaves_like 'does not add files in /uploads/tmp'
end
end
end
diff --git a/spec/lib/gitlab/badge/coverage/template_spec.rb b/spec/lib/gitlab/badge/coverage/template_spec.rb
index 383bae6e087..d9c21a22590 100644
--- a/spec/lib/gitlab/badge/coverage/template_spec.rb
+++ b/spec/lib/gitlab/badge/coverage/template_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::Badge::Coverage::Template do
- let(:badge) { double(entity: 'coverage', status: 90) }
+ let(:badge) { double(entity: 'coverage', status: 90.00) }
let(:template) { described_class.new(badge) }
describe '#key_text' do
@@ -13,7 +13,17 @@ describe Gitlab::Badge::Coverage::Template do
describe '#value_text' do
context 'when coverage is known' do
it 'returns coverage percentage' do
- expect(template.value_text).to eq '90%'
+ expect(template.value_text).to eq '90.00%'
+ end
+ end
+
+ context 'when coverage is known to many digits' do
+ before do
+ allow(badge).to receive(:status).and_return(92.349)
+ end
+
+ it 'returns rounded coverage percentage' do
+ expect(template.value_text).to eq '92.35%'
end
end
@@ -37,7 +47,7 @@ describe Gitlab::Badge::Coverage::Template do
describe '#value_width' do
context 'when coverage is known' do
it 'is narrower when coverage is known' do
- expect(template.value_width).to eq 36
+ expect(template.value_width).to eq 54
end
end
@@ -113,7 +123,7 @@ describe Gitlab::Badge::Coverage::Template do
describe '#width' do
context 'when coverage is known' do
it 'returns the key width plus value width' do
- expect(template.width).to eq 98
+ expect(template.width).to eq 116
end
end
diff --git a/spec/lib/gitlab/checks/force_push_spec.rb b/spec/lib/gitlab/checks/force_push_spec.rb
index 633e319f46d..a65012d2314 100644
--- a/spec/lib/gitlab/checks/force_push_spec.rb
+++ b/spec/lib/gitlab/checks/force_push_spec.rb
@@ -2,18 +2,20 @@ require 'spec_helper'
describe Gitlab::Checks::ForcePush do
let(:project) { create(:project, :repository) }
+ let(:repository) { project.repository.raw }
context "exit code checking", :skip_gitaly_mock do
it "does not raise a runtime error if the `popen` call to git returns a zero exit code" do
- allow_any_instance_of(Gitlab::Git::RevList).to receive(:popen).and_return(['normal output', 0])
+ allow(repository).to receive(:popen).and_return(['normal output', 0])
expect { described_class.force_push?(project, 'oldrev', 'newrev') }.not_to raise_error
end
- it "raises a runtime error if the `popen` call to git returns a non-zero exit code" do
- allow_any_instance_of(Gitlab::Git::RevList).to receive(:popen).and_return(['error', 1])
+ it "raises a GitError error if the `popen` call to git returns a non-zero exit code" do
+ allow(repository).to receive(:popen).and_return(['error', 1])
- expect { described_class.force_push?(project, 'oldrev', 'newrev') }.to raise_error(RuntimeError)
+ expect { described_class.force_push?(project, 'oldrev', 'newrev') }
+ .to raise_error(Gitlab::Git::Repository::GitError)
end
end
end
diff --git a/spec/lib/gitlab/ci/trace_spec.rb b/spec/lib/gitlab/ci/trace_spec.rb
index 3546532b9b4..91c9625ba06 100644
--- a/spec/lib/gitlab/ci/trace_spec.rb
+++ b/spec/lib/gitlab/ci/trace_spec.rb
@@ -238,11 +238,98 @@ describe Gitlab::Ci::Trace do
end
end
+ describe '#read' do
+ shared_examples 'read successfully with IO' do
+ it 'yields with source' do
+ trace.read do |stream|
+ expect(stream).to be_a(Gitlab::Ci::Trace::Stream)
+ expect(stream.stream).to be_a(IO)
+ end
+ end
+ end
+
+ shared_examples 'read successfully with StringIO' do
+ it 'yields with source' do
+ trace.read do |stream|
+ expect(stream).to be_a(Gitlab::Ci::Trace::Stream)
+ expect(stream.stream).to be_a(StringIO)
+ end
+ end
+ end
+
+ shared_examples 'failed to read' do
+ it 'yields without source' do
+ trace.read do |stream|
+ expect(stream).to be_a(Gitlab::Ci::Trace::Stream)
+ expect(stream.stream).to be_nil
+ end
+ end
+ end
+
+ context 'when trace artifact exists' do
+ before do
+ create(:ci_job_artifact, :trace, job: build)
+ end
+
+ it_behaves_like 'read successfully with IO'
+ end
+
+ context 'when current_path (with project_id) exists' do
+ before do
+ expect(trace).to receive(:default_path) { expand_fixture_path('trace/sample_trace') }
+ end
+
+ it_behaves_like 'read successfully with IO'
+ end
+
+ context 'when current_path (with project_ci_id) exists' do
+ before do
+ expect(trace).to receive(:deprecated_path) { expand_fixture_path('trace/sample_trace') }
+ end
+
+ it_behaves_like 'read successfully with IO'
+ end
+
+ context 'when db trace exists' do
+ before do
+ build.send(:write_attribute, :trace, "data")
+ end
+
+ it_behaves_like 'read successfully with StringIO'
+ end
+
+ context 'when no sources exist' do
+ it_behaves_like 'failed to read'
+ end
+ end
+
describe 'trace handling' do
+ subject { trace.exist? }
+
context 'trace does not exist' do
it { expect(trace.exist?).to be(false) }
end
+ context 'when trace artifact exists' do
+ before do
+ create(:ci_job_artifact, :trace, job: build)
+ end
+
+ it { is_expected.to be_truthy }
+
+ context 'when the trace artifact has been erased' do
+ before do
+ trace.erase!
+ end
+
+ it { is_expected.to be_falsy }
+
+ it 'removes associations' do
+ expect(Ci::JobArtifact.exists?(job_id: build.id, file_type: :trace)).to be_falsy
+ end
+ end
+ end
+
context 'new trace path is used' do
before do
trace.send(:ensure_directory)
diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb
index 8c79ef54c6c..28c679af12a 100644
--- a/spec/lib/gitlab/closing_issue_extractor_spec.rb
+++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::ClosingIssueExtractor do
let(:project) { create(:project) }
let(:project2) { create(:project) }
- let(:forked_project) { Projects::ForkService.new(project, project.creator).execute }
+ let(:forked_project) { Projects::ForkService.new(project, project2.creator).execute }
let(:issue) { create(:issue, project: project) }
let(:issue2) { create(:issue, project: project2) }
let(:reference) { issue.to_reference }
@@ -14,6 +14,7 @@ describe Gitlab::ClosingIssueExtractor do
before do
project.add_developer(project.creator)
+ project.add_developer(project2.creator)
project2.add_master(project.creator)
end
diff --git a/spec/lib/gitlab/current_settings_spec.rb b/spec/lib/gitlab/current_settings_spec.rb
index 492659a82b0..4ddcbd7eb66 100644
--- a/spec/lib/gitlab/current_settings_spec.rb
+++ b/spec/lib/gitlab/current_settings_spec.rb
@@ -8,22 +8,37 @@ describe Gitlab::CurrentSettings do
end
describe '#current_application_settings' do
+ it 'allows keys to be called directly' do
+ db_settings = create(:application_setting,
+ home_page_url: 'http://mydomain.com',
+ signup_enabled: false)
+
+ expect(described_class.home_page_url).to eq(db_settings.home_page_url)
+ expect(described_class.signup_enabled?).to be_falsey
+ expect(described_class.signup_enabled).to be_falsey
+ expect(described_class.metrics_sample_interval).to be(15)
+ end
+
context 'with DB available' do
before do
- allow_any_instance_of(described_class).to receive(:connect_to_db?).and_return(true)
+ # For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(true)` causes issues
+ # during the initialization phase of the test suite, so instead let's mock the internals of it
+ allow(ActiveRecord::Base.connection).to receive(:active?).and_return(true)
+ allow(ActiveRecord::Base.connection).to receive(:table_exists?).and_call_original
+ allow(ActiveRecord::Base.connection).to receive(:table_exists?).with('application_settings').and_return(true)
end
it 'attempts to use cached values first' do
expect(ApplicationSetting).to receive(:cached)
- expect(current_application_settings).to be_a(ApplicationSetting)
+ expect(described_class.current_application_settings).to be_a(ApplicationSetting)
end
it 'falls back to DB if Redis returns an empty value' do
expect(ApplicationSetting).to receive(:cached).and_return(nil)
expect(ApplicationSetting).to receive(:last).and_call_original.twice
- expect(current_application_settings).to be_a(ApplicationSetting)
+ expect(described_class.current_application_settings).to be_a(ApplicationSetting)
end
it 'falls back to DB if Redis fails' do
@@ -32,14 +47,14 @@ describe Gitlab::CurrentSettings do
expect(ApplicationSetting).to receive(:cached).and_raise(::Redis::BaseError)
expect(Rails.cache).to receive(:fetch).with(ApplicationSetting::CACHE_KEY).and_raise(Redis::BaseError)
- expect(current_application_settings).to eq(db_settings)
+ expect(described_class.current_application_settings).to eq(db_settings)
end
it 'creates default ApplicationSettings if none are present' do
expect(ApplicationSetting).to receive(:cached).and_raise(::Redis::BaseError)
expect(Rails.cache).to receive(:fetch).with(ApplicationSetting::CACHE_KEY).and_raise(Redis::BaseError)
- settings = current_application_settings
+ settings = described_class.current_application_settings
expect(settings).to be_a(ApplicationSetting)
expect(settings).to be_persisted
@@ -52,7 +67,7 @@ describe Gitlab::CurrentSettings do
end
it 'returns an in-memory ApplicationSetting object' do
- settings = current_application_settings
+ settings = described_class.current_application_settings
expect(settings).to be_a(OpenStruct)
expect(settings.sign_in_enabled?).to eq(settings.sign_in_enabled)
@@ -63,7 +78,7 @@ describe Gitlab::CurrentSettings do
db_settings = create(:application_setting,
home_page_url: 'http://mydomain.com',
signup_enabled: false)
- settings = current_application_settings
+ settings = described_class.current_application_settings
app_defaults = ApplicationSetting.last
expect(settings).to be_a(OpenStruct)
@@ -80,15 +95,16 @@ describe Gitlab::CurrentSettings do
context 'with DB unavailable' do
before do
- allow_any_instance_of(described_class).to receive(:connect_to_db?).and_return(false)
- allow_any_instance_of(described_class).to receive(:retrieve_settings_from_database_cache?).and_return(nil)
+ # For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(false)` causes issues
+ # during the initialization phase of the test suite, so instead let's mock the internals of it
+ allow(ActiveRecord::Base.connection).to receive(:active?).and_return(false)
end
it 'returns an in-memory ApplicationSetting object' do
expect(ApplicationSetting).not_to receive(:current)
expect(ApplicationSetting).not_to receive(:last)
- expect(current_application_settings).to be_a(OpenStruct)
+ expect(described_class.current_application_settings).to be_a(OpenStruct)
end
end
@@ -101,8 +117,8 @@ describe Gitlab::CurrentSettings do
expect(ApplicationSetting).not_to receive(:current)
expect(ApplicationSetting).not_to receive(:last)
- expect(current_application_settings).to be_a(ApplicationSetting)
- expect(current_application_settings).not_to be_persisted
+ expect(described_class.current_application_settings).to be_a(ApplicationSetting)
+ expect(described_class.current_application_settings).not_to be_persisted
end
end
end
diff --git a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb
index 39e3b875c49..326ed2f2ecf 100644
--- a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb
+++ b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb
@@ -17,7 +17,7 @@ describe Gitlab::Gfm::UploadsRewriter do
end
let(:text) do
- "Text and #{image_uploader.to_markdown} and #{zip_uploader.to_markdown}"
+ "Text and #{image_uploader.markdown_link} and #{zip_uploader.markdown_link}"
end
describe '#rewrite' do
diff --git a/spec/lib/gitlab/git/lfs_pointer_file_spec.rb b/spec/lib/gitlab/git/lfs_pointer_file_spec.rb
new file mode 100644
index 00000000000..d7f76737f3f
--- /dev/null
+++ b/spec/lib/gitlab/git/lfs_pointer_file_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Gitlab::Git::LfsPointerFile do
+ let(:data) { "1234\n" }
+
+ subject { described_class.new(data) }
+
+ describe '#size' do
+ it 'counts the bytes' do
+ expect(subject.size).to eq 5
+ end
+
+ it 'handles non ascii data' do
+ expect(described_class.new("ääää").size).to eq 8
+ end
+ end
+
+ describe '#sha256' do
+ it 'hashes the content correctly' do
+ expect(subject.sha256).to eq 'a883dafc480d466ee04e0d6da986bd78eb1fdd2178d04693723da3a8f95d42f4'
+ end
+ end
+
+ describe '#pointer' do
+ it 'starts with the LFS version' do
+ expect(subject.pointer).to start_with('version https://git-lfs.github.com/spec/v1')
+ end
+
+ it 'includes sha256' do
+ expect(subject.pointer).to match(/^oid sha256:[0-9a-fA-F]{64}/)
+ end
+
+ it 'ends with the size' do
+ expect(subject.pointer).to end_with("\nsize 5\n")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index bf01e6ef8e8..ec1c7a96f92 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -20,6 +20,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
let(:storage_path) { TestEnv.repos_path }
+ let(:user) { build(:user) }
describe '.create_hooks' do
let(:repo_path) { File.join(storage_path, 'hook-test.git') }
@@ -249,7 +250,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
shared_examples 'archive check' do |extenstion|
- it { expect(metadata['ArchivePath']).to match(/tmp\/gitlab-git-test.git\/gitlab-git-test-master-#{SeedRepo::LastCommit::ID}/) }
+ it { expect(metadata['ArchivePath']).to match(%r{tmp/gitlab-git-test.git/gitlab-git-test-master-#{SeedRepo::LastCommit::ID}}) }
it { expect(metadata['ArchivePath']).to end_with extenstion }
end
@@ -693,7 +694,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
describe '#remote_tags' do
let(:remote_name) { 'upstream' }
let(:target_commit_id) { SeedRepo::Commit::ID }
- let(:user) { create(:user) }
let(:tag_name) { 'v0.0.1' }
let(:tag_message) { 'My tag' }
let(:remote_repository) do
@@ -905,44 +905,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
- context "compare results between log_by_walk and log_by_shell" do
- let(:options) { { ref: "master" } }
- let(:commits_by_walk) { repository.log(options).map(&:id) }
- let(:commits_by_shell) { repository.log(options.merge({ disable_walk: true })).map(&:id) }
-
- it { expect(commits_by_walk).to eq(commits_by_shell) }
-
- context "with limit" do
- let(:options) { { ref: "master", limit: 1 } }
-
- it { expect(commits_by_walk).to eq(commits_by_shell) }
- end
-
- context "with offset" do
- let(:options) { { ref: "master", offset: 1 } }
-
- it { expect(commits_by_walk).to eq(commits_by_shell) }
- end
-
- context "with skip_merges" do
- let(:options) { { ref: "master", skip_merges: true } }
-
- it { expect(commits_by_walk).to eq(commits_by_shell) }
- end
-
- context "with path" do
- let(:options) { { ref: "master", path: "encoding" } }
-
- it { expect(commits_by_walk).to eq(commits_by_shell) }
-
- context "with follow" do
- let(:options) { { ref: "master", path: "encoding", follow: true } }
-
- it { expect(commits_by_walk).to eq(commits_by_shell) }
- end
- end
- end
-
context "where provides 'after' timestamp" do
options = { after: Time.iso8601('2014-03-03T20:15:01+00:00') }
@@ -1200,14 +1162,27 @@ describe Gitlab::Git::Repository, seed_helper: true do
context 'when Gitaly find_branch feature is disabled', :skip_gitaly_mock do
it_behaves_like 'finding a branch'
- it 'should reload Rugged::Repository and return master' do
- expect(Rugged::Repository).to receive(:new).twice.and_call_original
+ context 'force_reload is true' do
+ it 'should reload Rugged::Repository' do
+ expect(Rugged::Repository).to receive(:new).twice.and_call_original
- repository.find_branch('master')
- branch = repository.find_branch('master', force_reload: true)
+ repository.find_branch('master')
+ branch = repository.find_branch('master', force_reload: true)
- expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
- expect(branch.name).to eq('master')
+ expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
+ expect(branch.name).to eq('master')
+ end
+ end
+
+ context 'force_reload is false' do
+ it 'should not reload Rugged::Repository' do
+ expect(Rugged::Repository).to receive(:new).once.and_call_original
+
+ branch = repository.find_branch('master', force_reload: false)
+
+ expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
+ expect(branch.name).to eq('master')
+ end
end
end
end
@@ -1749,7 +1724,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
shared_examples "user deleting a branch" do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository.raw }
- let(:user) { create(:user) }
let(:branch_name) { "to-be-deleted-soon" }
before do
@@ -1790,12 +1764,49 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
+ describe '#write_config' do
+ before do
+ repository.rugged.config["gitlab.fullpath"] = repository.path
+ end
+
+ shared_examples 'writing repo config' do
+ context 'is given a path' do
+ it 'writes it to disk' do
+ repository.write_config(full_path: "not-the/real-path.git")
+
+ config = File.read(File.join(repository.path, "config"))
+
+ expect(config).to include("[gitlab]")
+ expect(config).to include("fullpath = not-the/real-path.git")
+ end
+ end
+
+ context 'it is given an empty path' do
+ it 'does not write it to disk' do
+ repository.write_config(full_path: "")
+
+ config = File.read(File.join(repository.path, "config"))
+
+ expect(config).to include("[gitlab]")
+ expect(config).to include("fullpath = #{repository.path}")
+ end
+ end
+ end
+
+ context "when gitaly_write_config is enabled" do
+ it_behaves_like "writing repo config"
+ end
+
+ context "when gitaly_write_config is disabled", :disable_gitaly do
+ it_behaves_like "writing repo config"
+ end
+ end
+
describe '#merge' do
let(:repository) do
Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
end
let(:source_sha) { '913c66a37b4a45b9769037c55c2d238bd0942d2e' }
- let(:user) { build(:user) }
let(:target_branch) { 'test-merge-target-branch' }
before do
@@ -1848,7 +1859,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
let(:branch_head) { '6d394385cf567f80a8fd85055db1ab4c5295806f' }
let(:source_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' }
- let(:user) { build(:user) }
let(:target_branch) { 'test-ff-target-branch' }
before do
@@ -2167,6 +2177,47 @@ describe Gitlab::Git::Repository, seed_helper: true do
expect { subject }.to raise_error(Gitlab::Git::CommandError, 'error')
end
end
+
+ describe '#squash' do
+ let(:squash_id) { '1' }
+ let(:branch_name) { 'fix' }
+ let(:start_sha) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' }
+ let(:end_sha) { '12d65c8dd2b2676fa3ac47d955accc085a37a9c1' }
+
+ subject do
+ opts = {
+ branch: branch_name,
+ start_sha: start_sha,
+ end_sha: end_sha,
+ author: user,
+ message: 'Squash commit message'
+ }
+
+ repository.squash(user, squash_id, opts)
+ end
+
+ context 'sparse checkout', :skip_gitaly_mock do
+ let(:expected_files) { %w(files files/js files/js/application.js) }
+
+ before do
+ allow(repository).to receive(:with_worktree).and_wrap_original do |m, *args|
+ m.call(*args) do
+ worktree_path = args[0]
+ files_pattern = File.join(worktree_path, '**', '*')
+ expected = expected_files.map do |path|
+ File.expand_path(path, worktree_path)
+ end
+
+ expect(Dir[files_pattern]).to eq(expected)
+ end
+ end
+ end
+
+ it 'checkouts only the files in the diff' do
+ subject
+ end
+ end
+ end
end
def create_remote_branch(repository, remote_name, branch_name, source_branch_name)
diff --git a/spec/lib/gitlab/git/rev_list_spec.rb b/spec/lib/gitlab/git/rev_list_spec.rb
index 90fbef9d248..4e0ee206219 100644
--- a/spec/lib/gitlab/git/rev_list_spec.rb
+++ b/spec/lib/gitlab/git/rev_list_spec.rb
@@ -1,51 +1,42 @@
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) }
+ let(:repository) { create(:project, :repository).repository.raw }
+ let(:rev_list) { described_class.new(repository, newrev: 'newrev') }
let(:env_hash) do
{
'GIT_OBJECT_DIRECTORY' => 'foo',
'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar'
}
end
+ let(:command_env) { { 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'foo:bar' } }
before do
- allow(Gitlab::Git::Env).to receive(:all).and_return(env_hash.symbolize_keys)
+ allow(Gitlab::Git::Env).to receive(:all).and_return(env_hash)
end
def args_for_popen(args_list)
- [
- Gitlab.config.git.bin_path,
- "--git-dir=#{project.repository.path_to_repo}",
- 'rev-list',
- *args_list
- ]
- end
-
- def stub_popen_rev_list(*additional_args, output:)
- args = args_for_popen(additional_args)
-
- expect(rev_list).to receive(:popen).with(args, nil, env_hash)
- .and_return([output, 0])
+ [Gitlab.config.git.bin_path, 'rev-list', *args_list]
end
- def stub_lazy_popen_rev_list(*additional_args, output:)
+ def stub_popen_rev_list(*additional_args, with_lazy_block: true, output:)
params = [
args_for_popen(additional_args),
- nil,
- env_hash,
- hash_including(lazy_block: anything)
+ repository.path,
+ command_env,
+ hash_including(lazy_block: with_lazy_block ? anything : nil)
]
- expect(rev_list).to receive(:popen).with(*params) do |*_, lazy_block:|
- lazy_block.call(output.lines.lazy.map(&:chomp))
+ expect(repository).to receive(:popen).with(*params) do |*_, lazy_block:|
+ output = lazy_block.call(output.lines.lazy.map(&:chomp)) if with_lazy_block
+
+ [output, 0]
end
end
context "#new_refs" do
it 'calls out to `popen`' do
- stub_popen_rev_list('newrev', '--not', '--all', output: "sha1\nsha2")
+ stub_popen_rev_list('newrev', '--not', '--all', with_lazy_block: false, output: "sha1\nsha2")
expect(rev_list.new_refs).to eq(%w[sha1 sha2])
end
@@ -55,18 +46,18 @@ describe Gitlab::Git::RevList 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])
+ expect { |b| rev_list.new_objects(&b) }.to yield_with_args(%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])
+ expect { |b| rev_list.new_objects(require_path: true, &b) }.to yield_with_args(%w[sha2])
end
it 'can handle non utf-8 paths' do
non_utf_char = [0x89].pack("c*").force_encoding("UTF-8")
- stub_lazy_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha2 πå†h/†ø/ƒîlé#{non_utf_char}\nsha1")
+ stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha2 πå†h/†ø/ƒîlé#{non_utf_char}\nsha1")
rev_list.new_objects(require_path: true) do |object_ids|
expect(object_ids.force).to eq(%w[sha2])
@@ -74,7 +65,7 @@ describe Gitlab::Git::RevList do
end
it 'can yield a lazy enumerator' do
- stub_lazy_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2")
+ stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2")
rev_list.new_objects do |object_ids|
expect(object_ids).to be_a Enumerator::Lazy
@@ -82,7 +73,7 @@ describe Gitlab::Git::RevList do
end
it 'returns the result of the block when given' do
- stub_lazy_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2")
+ stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2")
objects = rev_list.new_objects do |object_ids|
object_ids.first
@@ -94,13 +85,13 @@ describe Gitlab::Git::RevList do
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])
+ expect { |b| rev_list.new_objects(not_in: ['master'], &b) }.to yield_with_args(%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])
+ expect { |b| rev_list.new_objects(not_in: [], &b) }.to yield_with_args(%w[sha1 sha2])
end
end
@@ -108,15 +99,15 @@ describe Gitlab::Git::RevList 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).to eq(%w[sha1 sha2])
+ expect { |b| rev_list.all_objects(&b) }.to yield_with_args(%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) }
+ let(:rev_list) { described_class.new(repository, oldrev: 'oldrev', newrev: 'newrev') }
it 'calls out to `popen`' do
- stub_popen_rev_list('--max-count=1', 'oldrev', '^newrev', output: "sha1\nsha2")
+ stub_popen_rev_list('--max-count=1', 'oldrev', '^newrev', with_lazy_block: false, output: "sha1\nsha2")
expect(rev_list.missed_ref).to eq(%w[sha1 sha2])
end
diff --git a/spec/lib/gitlab/git/wiki_spec.rb b/spec/lib/gitlab/git/wiki_spec.rb
new file mode 100644
index 00000000000..bd8dbf07fa7
--- /dev/null
+++ b/spec/lib/gitlab/git/wiki_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe Gitlab::Git::Wiki do
+ let(:project) { create(:project) }
+ let(:user) { project.owner }
+ let(:wiki) { ProjectWiki.new(project, user) }
+ let(:gollum_wiki) { wiki.wiki }
+
+ # Remove skip_gitaly_mock flag when gitaly_find_page when
+ # https://gitlab.com/gitlab-org/gitaly/merge_requests/539 gets merged
+ describe '#page', :skip_gitaly_mock do
+ it 'returns the right page' do
+ create_page('page1', 'content')
+ create_page('foo/page1', 'content')
+
+ expect(gollum_wiki.page(title: 'page1', dir: '').url_path).to eq 'page1'
+ expect(gollum_wiki.page(title: 'page1', dir: 'foo').url_path).to eq 'foo/page1'
+
+ destroy_page('page1')
+ destroy_page('page1', 'foo')
+ end
+ end
+
+ def create_page(name, content)
+ gollum_wiki.write_page(name, :markdown, content, commit_details)
+ end
+
+ def commit_details
+ Gitlab::Git::Wiki::CommitDetails.new(user.name, user.email, "test commit")
+ end
+
+ def destroy_page(title, dir = '')
+ page = gollum_wiki.page(title: title, dir: dir)
+ wiki.delete_page(page, "test commit")
+ end
+end
diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
index d9ec28ab02e..9fbdd73ee0e 100644
--- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
@@ -123,4 +123,53 @@ describe Gitlab::GitalyClient::OperationService do
expect(subject.branch_created).to be(false)
end
end
+
+ describe '#user_squash' do
+ let(:branch_name) { 'my-branch' }
+ let(:squash_id) { '1' }
+ let(:start_sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
+ let(:end_sha) { '54cec5282aa9f21856362fe321c800c236a61615' }
+ let(:commit_message) { 'Squash message' }
+ let(:request) do
+ Gitaly::UserSquashRequest.new(
+ repository: repository.gitaly_repository,
+ user: gitaly_user,
+ squash_id: squash_id.to_s,
+ branch: branch_name,
+ start_sha: start_sha,
+ end_sha: end_sha,
+ author: gitaly_user,
+ commit_message: commit_message
+ )
+ end
+ let(:squash_sha) { 'f00' }
+ let(:response) { Gitaly::UserSquashResponse.new(squash_sha: squash_sha) }
+
+ subject do
+ client.user_squash(user, squash_id, branch_name, start_sha, end_sha, user, commit_message)
+ end
+
+ it 'sends a user_squash message and returns the squash sha' do
+ expect_any_instance_of(Gitaly::OperationService::Stub)
+ .to receive(:user_squash).with(request, kind_of(Hash))
+ .and_return(response)
+
+ expect(subject).to eq(squash_sha)
+ end
+
+ context "when git_error is present" do
+ let(:response) do
+ Gitaly::UserSquashResponse.new(git_error: "something failed")
+ end
+
+ it "throws a PreReceive exception" do
+ expect_any_instance_of(Gitaly::OperationService::Stub)
+ .to receive(:user_squash).with(request, kind_of(Hash))
+ .and_return(response)
+
+ expect { subject }.to raise_error(
+ Gitlab::Git::Repository::GitError, "something failed")
+ end
+ end
+ end
end
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 9dfd879a1bc..d076007e4bc 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -236,12 +236,14 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
labels = project.issues.first.labels
expect(labels.where(type: "ProjectLabel").count).to eq(results.fetch(:first_issue_labels, 0))
+ expect(labels.where(type: "ProjectLabel").where.not(group_id: nil).count).to eq(0)
end
end
shared_examples 'restores group correctly' do |**results|
it 'has group label' do
expect(project.group.labels.size).to eq(results.fetch(:labels, 0))
+ expect(project.group.labels.where(type: "GroupLabel").where.not(project_id: nil).count).to eq(0)
end
it 'has group milestone' do
diff --git a/spec/lib/gitlab/import_export/uploads_restorer_spec.rb b/spec/lib/gitlab/import_export/uploads_restorer_spec.rb
index 63992ea8ab8..a685521cbf0 100644
--- a/spec/lib/gitlab/import_export/uploads_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/uploads_restorer_spec.rb
@@ -4,7 +4,6 @@ describe Gitlab::ImportExport::UploadsRestorer do
describe 'bundle a project Git repo' do
let(:export_path) { "#{Dir.tmpdir}/uploads_saver_spec" }
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.full_path) }
- let(:uploads_path) { FileUploader.dynamic_path_segment(project) }
before do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
@@ -26,9 +25,9 @@ describe Gitlab::ImportExport::UploadsRestorer do
end
it 'copies the uploads to the project path' do
- restorer.restore
+ subject.restore
- uploads = Dir.glob(File.join(uploads_path, '**/*')).map { |file| File.basename(file) }
+ uploads = Dir.glob(File.join(subject.uploads_path, '**/*')).map { |file| File.basename(file) }
expect(uploads).to include('dummy.txt')
end
@@ -44,9 +43,9 @@ describe Gitlab::ImportExport::UploadsRestorer do
end
it 'copies the uploads to the project path' do
- restorer.restore
+ subject.restore
- uploads = Dir.glob(File.join(uploads_path, '**/*')).map { |file| File.basename(file) }
+ uploads = Dir.glob(File.join(subject.uploads_path, '**/*')).map { |file| File.basename(file) }
expect(uploads).to include('dummy.txt')
end
diff --git a/spec/lib/gitlab/import_export/uploads_saver_spec.rb b/spec/lib/gitlab/import_export/uploads_saver_spec.rb
index e8948de1f3a..959779523f4 100644
--- a/spec/lib/gitlab/import_export/uploads_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/uploads_saver_spec.rb
@@ -30,7 +30,7 @@ describe Gitlab::ImportExport::UploadsSaver do
it 'copies the uploads to the export path' do
saver.save
- uploads = Dir.glob(File.join(shared.export_path, 'uploads', '**/*')).map { |file| File.basename(file) }
+ uploads = Dir.glob(File.join(saver.uploads_export_path, '**/*')).map { |file| File.basename(file) }
expect(uploads).to include('banana_sample.gif')
end
@@ -52,7 +52,7 @@ describe Gitlab::ImportExport::UploadsSaver do
it 'copies the uploads to the export path' do
saver.save
- uploads = Dir.glob(File.join(shared.export_path, 'uploads', '**/*')).map { |file| File.basename(file) }
+ uploads = Dir.glob(File.join(saver.uploads_export_path, '**/*')).map { |file| File.basename(file) }
expect(uploads).to include('banana_sample.gif')
end
diff --git a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
index 0b8e97b8948..ebb6033f71e 100644
--- a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
@@ -63,14 +63,14 @@ describe Gitlab::Kubernetes::Helm::Pod do
it 'should mount configMap specification in the volume' do
spec = subject.generate.spec
- expect(spec.volumes.first.configMap['name']).to eq('values-content-configuration')
+ expect(spec.volumes.first.configMap['name']).to eq("values-content-configuration-#{app.name}")
expect(spec.volumes.first.configMap['items'].first['key']).to eq('values')
expect(spec.volumes.first.configMap['items'].first['path']).to eq('values.yaml')
end
end
context 'without a configuration file' do
- let(:app) { create(:clusters_applications_ingress, cluster: cluster) }
+ let(:app) { create(:clusters_applications_helm, cluster: cluster) }
it_behaves_like 'helm pod'
diff --git a/spec/lib/gitlab/ldap/auth_hash_spec.rb b/spec/lib/gitlab/ldap/auth_hash_spec.rb
index 1785094af10..9c30ddd7fe2 100644
--- a/spec/lib/gitlab/ldap/auth_hash_spec.rb
+++ b/spec/lib/gitlab/ldap/auth_hash_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Gitlab::LDAP::AuthHash do
+ include LdapHelpers
+
let(:auth_hash) do
described_class.new(
OmniAuth::AuthHash.new(
@@ -83,4 +85,26 @@ describe Gitlab::LDAP::AuthHash do
end
end
end
+
+ describe '#username' do
+ context 'if lowercase_usernames setting is' do
+ let(:given_uid) { 'uid=John Smith,ou=People,dc=example,dc=com' }
+
+ before do
+ raw_info[:uid] = ['JOHN']
+ end
+
+ it 'enabled the username attribute is lower cased' do
+ stub_ldap_config(lowercase_usernames: true)
+
+ expect(auth_hash.username).to eq 'john'
+ end
+
+ it 'disabled the username attribute is not lower cased' do
+ stub_ldap_config(lowercase_usernames: false)
+
+ expect(auth_hash.username).to eq 'JOHN'
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ldap/person_spec.rb b/spec/lib/gitlab/ldap/person_spec.rb
index ff29d9aa5be..b54d4000b53 100644
--- a/spec/lib/gitlab/ldap/person_spec.rb
+++ b/spec/lib/gitlab/ldap/person_spec.rb
@@ -139,6 +139,27 @@ describe Gitlab::LDAP::Person do
expect(person.username).to eq(attr_value)
end
end
+
+ context 'if lowercase_usernames setting is' do
+ let(:username_attribute) { 'uid' }
+
+ before do
+ entry[username_attribute] = 'JOHN'
+ @person = described_class.new(entry, 'ldapmain')
+ end
+
+ it 'enabled the username attribute is lower cased' do
+ stub_ldap_config(lowercase_usernames: true)
+
+ expect(@person.username).to eq 'john'
+ end
+
+ it 'disabled the username attribute is not lower cased' do
+ stub_ldap_config(lowercase_usernames: false)
+
+ expect(@person.username).to eq 'JOHN'
+ end
+ end
end
def assert_generic_test(test_description, got, expected)
diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb
index 9e405e9f736..03c185ddc07 100644
--- a/spec/lib/gitlab/metrics_spec.rb
+++ b/spec/lib/gitlab/metrics_spec.rb
@@ -20,7 +20,7 @@ describe Gitlab::Metrics do
context 'prometheus metrics enabled in config' do
before do
- allow(Gitlab::CurrentSettings).to receive(:current_application_settings).and_return(prometheus_metrics_enabled: true)
+ allow(Gitlab::CurrentSettings).to receive(:prometheus_metrics_enabled).and_return(true)
end
context 'when metrics folder is present' do
diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb
index 0ae90069b7f..85991c38363 100644
--- a/spec/lib/gitlab/path_regex_spec.rb
+++ b/spec/lib/gitlab/path_regex_spec.rb
@@ -121,7 +121,7 @@ describe Gitlab::PathRegex do
STARTING_WITH_NAMESPACE = %r{^/\*namespace_id/:(project_)?id}
NON_PARAM_PARTS = %r{[^:*][a-z\-_/]*}
ANY_OTHER_PATH_PART = %r{[a-z\-_/:]*}
- WILDCARD_SEGMENT = %r{\*}
+ WILDCARD_SEGMENT = /\*/
let(:namespaced_wildcard_routes) do
routes_without_format.select do |p|
p =~ %r{#{STARTING_WITH_NAMESPACE}/#{NON_PARAM_PARTS}/#{ANY_OTHER_PATH_PART}#{WILDCARD_SEGMENT}}
diff --git a/spec/lib/gitlab/query_limiting/active_support_subscriber_spec.rb b/spec/lib/gitlab/query_limiting/active_support_subscriber_spec.rb
new file mode 100644
index 00000000000..b49bc5c328c
--- /dev/null
+++ b/spec/lib/gitlab/query_limiting/active_support_subscriber_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe Gitlab::QueryLimiting::ActiveSupportSubscriber do
+ describe '#sql' do
+ it 'increments the number of executed SQL queries' do
+ transaction = double(:transaction)
+
+ allow(Gitlab::QueryLimiting::Transaction)
+ .to receive(:current)
+ .and_return(transaction)
+
+ expect(transaction)
+ .to receive(:increment)
+ .at_least(:once)
+
+ User.count
+ end
+ end
+end
diff --git a/spec/lib/gitlab/query_limiting/middleware_spec.rb b/spec/lib/gitlab/query_limiting/middleware_spec.rb
new file mode 100644
index 00000000000..a04bcdecb4b
--- /dev/null
+++ b/spec/lib/gitlab/query_limiting/middleware_spec.rb
@@ -0,0 +1,72 @@
+require 'spec_helper'
+
+describe Gitlab::QueryLimiting::Middleware do
+ describe '#call' do
+ it 'runs the application with query limiting in place' do
+ middleware = described_class.new(-> (env) { env })
+
+ expect_any_instance_of(Gitlab::QueryLimiting::Transaction)
+ .to receive(:act_upon_results)
+
+ expect(middleware.call({ number: 10 }))
+ .to eq({ number: 10 })
+ end
+ end
+
+ describe '#action_name' do
+ let(:middleware) { described_class.new(-> (env) { env }) }
+
+ context 'using a Rails request' do
+ it 'returns the name of the controller and action' do
+ env = {
+ described_class::CONTROLLER_KEY => double(
+ :controller,
+ action_name: 'show',
+ class: double(:class, name: 'UsersController'),
+ content_type: 'text/html'
+ )
+ }
+
+ expect(middleware.action_name(env)).to eq('UsersController#show')
+ end
+
+ it 'includes the content type if this is not text/html' do
+ env = {
+ described_class::CONTROLLER_KEY => double(
+ :controller,
+ action_name: 'show',
+ class: double(:class, name: 'UsersController'),
+ content_type: 'application/json'
+ )
+ }
+
+ expect(middleware.action_name(env))
+ .to eq('UsersController#show (application/json)')
+ end
+ end
+
+ context 'using a Grape API request' do
+ it 'returns the name of the request method and endpoint path' do
+ env = {
+ described_class::ENDPOINT_KEY => double(
+ :endpoint,
+ route: double(:route, request_method: 'GET', path: '/foo')
+ )
+ }
+
+ expect(middleware.action_name(env)).to eq('GET /foo')
+ end
+
+ it 'returns nil if the route can not be retrieved' do
+ endpoint = double(:endpoint)
+ env = { described_class::ENDPOINT_KEY => endpoint }
+
+ allow(endpoint)
+ .to receive(:route)
+ .and_raise(RuntimeError)
+
+ expect(middleware.action_name(env)).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/query_limiting/transaction_spec.rb b/spec/lib/gitlab/query_limiting/transaction_spec.rb
new file mode 100644
index 00000000000..b4231fcd0fa
--- /dev/null
+++ b/spec/lib/gitlab/query_limiting/transaction_spec.rb
@@ -0,0 +1,144 @@
+require 'spec_helper'
+
+describe Gitlab::QueryLimiting::Transaction do
+ after do
+ Thread.current[described_class::THREAD_KEY] = nil
+ end
+
+ describe '.current' do
+ it 'returns nil when there is no transaction' do
+ expect(described_class.current).to be_nil
+ end
+
+ it 'returns the transaction when present' do
+ Thread.current[described_class::THREAD_KEY] = described_class.new
+
+ expect(described_class.current).to be_an_instance_of(described_class)
+ end
+ end
+
+ describe '.run' do
+ it 'runs a transaction and returns it and its return value' do
+ trans, ret = described_class.run do
+ 10
+ end
+
+ expect(trans).to be_an_instance_of(described_class)
+ expect(ret).to eq(10)
+ end
+
+ it 'removes the transaction from the current thread upon completion' do
+ described_class.run do
+ 10
+ end
+
+ expect(Thread.current[described_class::THREAD_KEY]).to be_nil
+ end
+ end
+
+ describe '#act_upon_results' do
+ context 'when the query threshold is not exceeded' do
+ it 'does nothing' do
+ trans = described_class.new
+
+ expect(trans).not_to receive(:raise)
+
+ trans.act_upon_results
+ end
+ end
+
+ context 'when the query threshold is exceeded' do
+ let(:transaction) do
+ trans = described_class.new
+ trans.count = described_class::THRESHOLD + 1
+
+ trans
+ end
+
+ it 'raises an error when this is enabled' do
+ expect { transaction.act_upon_results }
+ .to raise_error(described_class::ThresholdExceededError)
+ end
+
+ it 'reports the error in Sentry if raising an error is disabled' do
+ expect(transaction)
+ .to receive(:raise_error?)
+ .and_return(false)
+
+ expect(Raven)
+ .to receive(:capture_exception)
+ .with(an_instance_of(described_class::ThresholdExceededError))
+
+ transaction.act_upon_results
+ end
+ end
+ end
+
+ describe '#increment' do
+ it 'increments the number of executed queries' do
+ transaction = described_class.new
+
+ expect(transaction.count).to be_zero
+
+ transaction.increment
+
+ expect(transaction.count).to eq(1)
+ end
+ end
+
+ describe '#raise_error?' do
+ it 'returns true in a test environment' do
+ transaction = described_class.new
+
+ expect(transaction.raise_error?).to eq(true)
+ end
+
+ it 'returns false in a production environment' do
+ transaction = described_class.new
+
+ expect(Rails.env)
+ .to receive(:test?)
+ .and_return(false)
+
+ expect(transaction.raise_error?).to eq(false)
+ end
+ end
+
+ describe '#threshold_exceeded?' do
+ it 'returns false when the threshold is not exceeded' do
+ transaction = described_class.new
+
+ expect(transaction.threshold_exceeded?).to eq(false)
+ end
+
+ it 'returns true when the threshold is exceeded' do
+ transaction = described_class.new
+ transaction.count = described_class::THRESHOLD + 1
+
+ expect(transaction.threshold_exceeded?).to eq(true)
+ end
+ end
+
+ describe '#error_message' do
+ it 'returns the error message to display when the threshold is exceeded' do
+ transaction = described_class.new
+ transaction.count = max = described_class::THRESHOLD
+
+ expect(transaction.error_message).to eq(
+ "Too many SQL queries were executed: a maximum of #{max} " \
+ "is allowed but #{max} SQL queries were executed"
+ )
+ end
+
+ it 'includes the action name in the error message when present' do
+ transaction = described_class.new
+ transaction.count = max = described_class::THRESHOLD
+ transaction.action = 'UsersController#show'
+
+ expect(transaction.error_message).to eq(
+ "Too many SQL queries were executed in UsersController#show: " \
+ "a maximum of #{max} is allowed but #{max} SQL queries were executed"
+ )
+ end
+ end
+end
diff --git a/spec/lib/gitlab/query_limiting_spec.rb b/spec/lib/gitlab/query_limiting_spec.rb
new file mode 100644
index 00000000000..2eddab0b8c3
--- /dev/null
+++ b/spec/lib/gitlab/query_limiting_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+
+describe Gitlab::QueryLimiting do
+ describe '.enable?' do
+ it 'returns true in a test environment' do
+ expect(described_class.enable?).to eq(true)
+ end
+
+ it 'returns true in a development environment' do
+ allow(Rails.env).to receive(:development?).and_return(true)
+
+ expect(described_class.enable?).to eq(true)
+ end
+
+ it 'returns true on GitLab.com' do
+ allow(Gitlab).to receive(:com?).and_return(true)
+
+ expect(described_class.enable?).to eq(true)
+ end
+
+ it 'returns true in a non GitLab.com' do
+ expect(Gitlab).to receive(:com?).and_return(false)
+ expect(Rails.env).to receive(:development?).and_return(false)
+ expect(Rails.env).to receive(:test?).and_return(false)
+
+ expect(described_class.enable?).to eq(false)
+ end
+ end
+
+ describe '.whitelist' do
+ it 'raises ArgumentError when an invalid issue URL is given' do
+ expect { described_class.whitelist('foo') }
+ .to raise_error(ArgumentError)
+ end
+
+ context 'without a transaction' do
+ it 'does nothing' do
+ expect { described_class.whitelist('https://example.com') }
+ .not_to raise_error
+ end
+ end
+
+ context 'with a transaction' do
+ let(:transaction) { Gitlab::QueryLimiting::Transaction.new }
+
+ before do
+ allow(Gitlab::QueryLimiting::Transaction)
+ .to receive(:current)
+ .and_return(transaction)
+ end
+
+ it 'does not increment the number of SQL queries executed in the block' do
+ before = transaction.count
+
+ described_class.whitelist('https://example.com')
+
+ 2.times do
+ User.count
+ end
+
+ expect(transaction.count).to eq(before)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/slash_commands/issue_search_spec.rb b/spec/lib/gitlab/slash_commands/issue_search_spec.rb
index e41e5254dde..35d01efc1bd 100644
--- a/spec/lib/gitlab/slash_commands/issue_search_spec.rb
+++ b/spec/lib/gitlab/slash_commands/issue_search_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::SlashCommands::IssueSearch do
let!(:issue) { create(:issue, project: project, title: 'find me') }
let!(:confidential) { create(:issue, :confidential, project: project, title: 'mepmep find') }
let(:project) { create(:project) }
- let(:user) { issue.author }
+ let(:user) { create(:user) }
let(:regex_match) { described_class.match("issue search find") }
subject do
diff --git a/spec/lib/gitlab/ssh_public_key_spec.rb b/spec/lib/gitlab/ssh_public_key_spec.rb
index 93d538141ce..c15e29774b6 100644
--- a/spec/lib/gitlab/ssh_public_key_spec.rb
+++ b/spec/lib/gitlab/ssh_public_key_spec.rb
@@ -37,6 +37,41 @@ describe Gitlab::SSHPublicKey, lib: true do
end
end
+ describe '.sanitize(key_content)' do
+ let(:content) { build(:key).key }
+
+ context 'when key has blank space characters' do
+ it 'removes the extra blank space characters' do
+ unsanitized = content.insert(100, "\n")
+ .insert(40, "\r\n")
+ .insert(30, ' ')
+
+ sanitized = described_class.sanitize(unsanitized)
+ _, body = sanitized.split
+
+ expect(sanitized).not_to eq(unsanitized)
+ expect(body).not_to match(/\s/)
+ end
+ end
+
+ context "when key doesn't have blank space characters" do
+ it "doesn't modify the content" do
+ sanitized = described_class.sanitize(content)
+
+ expect(sanitized).to eq(content)
+ end
+ end
+
+ context "when key is invalid" do
+ it 'returns the original content' do
+ unsanitized = "ssh-foo any content=="
+ sanitized = described_class.sanitize(unsanitized)
+
+ expect(sanitized).to eq(unsanitized)
+ end
+ end
+ end
+
describe '#valid?' do
subject { public_key }
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index b5f2a15ada3..0e9ecff25a6 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -103,9 +103,9 @@ describe Gitlab::UsageData do
subject { described_class.features_usage_data_ce }
it 'gathers feature usage data' do
- expect(subject[:signup]).to eq(current_application_settings.allow_signup?)
+ expect(subject[:signup]).to eq(Gitlab::CurrentSettings.allow_signup?)
expect(subject[:ldap]).to eq(Gitlab.config.ldap.enabled)
- expect(subject[:gravatar]).to eq(current_application_settings.gravatar_enabled?)
+ expect(subject[:gravatar]).to eq(Gitlab::CurrentSettings.gravatar_enabled?)
expect(subject[:omniauth]).to eq(Gitlab.config.omniauth.enabled)
expect(subject[:reply_by_email]).to eq(Gitlab::IncomingEmail.enabled?)
expect(subject[:container_registry]).to eq(Gitlab.config.registry.enabled)
@@ -129,7 +129,7 @@ describe Gitlab::UsageData do
subject { described_class.license_usage_data }
it "gathers license data" do
- expect(subject[:uuid]).to eq(current_application_settings.uuid)
+ expect(subject[:uuid]).to eq(Gitlab::CurrentSettings.uuid)
expect(subject[:version]).to eq(Gitlab::VERSION)
expect(subject[:active_user_count]).to eq(User.active.count)
expect(subject[:recorded_at]).to be_a(Time)
diff --git a/spec/lib/gitlab/visibility_level_spec.rb b/spec/lib/gitlab/visibility_level_spec.rb
index d85dac630b4..2c1146ceff5 100644
--- a/spec/lib/gitlab/visibility_level_spec.rb
+++ b/spec/lib/gitlab/visibility_level_spec.rb
@@ -57,6 +57,15 @@ describe Gitlab::VisibilityLevel do
expect(described_class.allowed_levels)
.to contain_exactly(described_class::PRIVATE, described_class::PUBLIC)
end
+
+ it 'returns all levels when no visibility level was set' do
+ allow(described_class)
+ .to receive_message_chain('current_application_settings.restricted_visibility_levels')
+ .and_return(nil)
+
+ expect(described_class.allowed_levels)
+ .to contain_exactly(described_class::PRIVATE, described_class::INTERNAL, described_class::PUBLIC)
+ end
end
describe '.closest_allowed_level' do
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index 2e7a0265a0b..dc2bb5b9747 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -465,4 +465,21 @@ describe Gitlab::Workhorse do
end
end
end
+
+ describe '.send_url' do
+ let(:url) { 'http://example.com' }
+
+ subject { described_class.send_url(url) }
+
+ it 'sets the header correctly' do
+ key, command, params = decode_workhorse_header(subject)
+
+ expect(key).to eq("Gitlab-Workhorse-Send-Data")
+ expect(command).to eq("send-url")
+ expect(params).to eq({
+ 'URL' => url,
+ 'AllowRedirects' => false
+ }.deep_stringify_keys)
+ end
+ end
end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 7a8e798e3c9..59eda025108 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -1357,7 +1357,7 @@ describe Notify do
matcher :have_part_with do |expected|
match do |actual|
- actual.body.parts.any? { |part| part.content_type.try(:match, %r(#{expected})) }
+ actual.body.parts.any? { |part| part.content_type.try(:match, /#{expected}/) }
end
end
end
diff --git a/spec/migrations/add_foreign_keys_to_todos_spec.rb b/spec/migrations/add_foreign_keys_to_todos_spec.rb
new file mode 100644
index 00000000000..4a22bd6f342
--- /dev/null
+++ b/spec/migrations/add_foreign_keys_to_todos_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20180201110056_add_foreign_keys_to_todos.rb')
+
+describe AddForeignKeysToTodos, :migration do
+ let(:todos) { table(:todos) }
+
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ context 'add foreign key on user_id' do
+ let!(:todo_with_user) { create_todo(user_id: user.id) }
+ let!(:todo_without_user) { create_todo(user_id: 4711) }
+
+ it 'removes orphaned todos without corresponding user' do
+ expect { migrate! }.to change { Todo.count }.from(2).to(1)
+ end
+
+ it 'does not remove entries with valid user_id' do
+ expect { migrate! }.not_to change { todo_with_user.reload }
+ end
+ end
+
+ context 'add foreign key on author_id' do
+ let!(:todo_with_author) { create_todo(author_id: user.id) }
+ let!(:todo_with_invalid_author) { create_todo(author_id: 4711) }
+
+ it 'removes orphaned todos by author_id' do
+ expect { migrate! }.to change { Todo.count }.from(2).to(1)
+ end
+
+ it 'does not touch author_id for valid entries' do
+ expect { migrate! }.not_to change { todo_with_author.reload }
+ end
+ end
+
+ context 'add foreign key on note_id' do
+ let(:note) { create(:note) }
+ let!(:todo_with_note) { create_todo(note_id: note.id) }
+ let!(:todo_with_invalid_note) { create_todo(note_id: 4711) }
+ let!(:todo_without_note) { create_todo(note_id: nil) }
+
+ it 'deletes todo if note_id is set but does not exist in notes table' do
+ expect { migrate! }.to change { Todo.count }.from(3).to(2)
+ end
+
+ it 'does not touch entry if note_id is nil' do
+ expect { migrate! }.not_to change { todo_without_note.reload }
+ end
+
+ it 'does not touch note_id for valid entries' do
+ expect { migrate! }.not_to change { todo_with_note.reload }
+ end
+ end
+
+ def create_todo(**opts)
+ todos.create!(
+ project_id: project.id,
+ user_id: user.id,
+ author_id: user.id,
+ target_type: '',
+ action: 0,
+ state: '', **opts
+ )
+ end
+end
diff --git a/spec/migrations/remove_project_labels_group_id_spec.rb b/spec/migrations/remove_project_labels_group_id_spec.rb
new file mode 100644
index 00000000000..d80d61af20b
--- /dev/null
+++ b/spec/migrations/remove_project_labels_group_id_spec.rb
@@ -0,0 +1,21 @@
+# encoding: utf-8
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20180202111106_remove_project_labels_group_id.rb')
+
+describe RemoveProjectLabelsGroupId, :delete do
+ let(:migration) { described_class.new }
+ let(:group) { create(:group) }
+ let!(:project_label) { create(:label, group_id: group.id) }
+ let!(:group_label) { create(:group_label) }
+
+ describe '#up' do
+ it 'updates the project labels group ID' do
+ expect { migration.up }.to change { project_label.reload.group_id }.to(nil)
+ end
+
+ it 'keeps the group labels group ID' do
+ expect { migration.up }.not_to change { group_label.reload.group_id }
+ end
+ end
+end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index f5b3b4a9fc5..0b3d5c6a0bd 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -675,7 +675,7 @@ describe Ci::Build do
context 'build is erasable' do
context 'new artifacts' do
- let!(:build) { create(:ci_build, :trace, :success, :artifacts) }
+ let!(:build) { create(:ci_build, :trace_artifact, :success, :artifacts) }
describe '#erase' do
before do
@@ -709,7 +709,7 @@ describe Ci::Build do
end
describe '#erased?' do
- let!(:build) { create(:ci_build, :trace, :success, :artifacts) }
+ let!(:build) { create(:ci_build, :trace_artifact, :success, :artifacts) }
subject { build.erased? }
context 'job has not been erased' do
@@ -744,7 +744,7 @@ describe Ci::Build do
context 'old artifacts' do
context 'build is erasable' do
context 'new artifacts' do
- let!(:build) { create(:ci_build, :trace, :success, :legacy_artifacts) }
+ let!(:build) { create(:ci_build, :trace_artifact, :success, :legacy_artifacts) }
describe '#erase' do
before do
@@ -778,7 +778,7 @@ describe Ci::Build do
end
describe '#erased?' do
- let!(:build) { create(:ci_build, :trace, :success, :legacy_artifacts) }
+ let!(:build) { create(:ci_build, :trace_artifact, :success, :legacy_artifacts) }
subject { build.erased? }
context 'job has not been erased' do
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index 0e18a326c68..a2bd36537e6 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -12,6 +12,9 @@ describe Ci::JobArtifact do
it { is_expected.to respond_to(:created_at) }
it { is_expected.to respond_to(:updated_at) }
+ it { is_expected.to delegate_method(:open).to(:file) }
+ it { is_expected.to delegate_method(:exists?).to(:file) }
+
describe '#set_size' do
it 'sets the size' do
expect(artifact.size).to eq(106365)
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 5e82a2988ce..5ea4acb6687 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -582,4 +582,20 @@ describe Group do
end
end
end
+
+ describe '#has_parent?' do
+ context 'when the group has a parent' do
+ it 'should be truthy' do
+ group = create(:group, :nested)
+ expect(group.has_parent?).to be_truthy
+ end
+ end
+
+ context 'when the group has no parent' do
+ it 'should be falsy' do
+ group = create(:group, parent: nil)
+ expect(group.has_parent?).to be_falsy
+ end
+ end
+ end
end
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index 4cd9e3f4f1d..bf5703ac986 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -1,13 +1,6 @@
require 'spec_helper'
describe Key, :mailer do
- include Gitlab::CurrentSettings
-
- describe 'modules' do
- subject { described_class }
- it { is_expected.to include_module(Gitlab::CurrentSettings) }
- end
-
describe "Associations" do
it { is_expected.to belong_to(:user) }
end
@@ -79,16 +72,53 @@ describe Key, :mailer do
expect(build(:key)).to be_valid
end
- it 'accepts a key with newline charecters after stripping them' do
- key = build(:key)
- key.key = key.key.insert(100, "\n")
- key.key = key.key.insert(40, "\r\n")
- expect(key).to be_valid
- end
-
it 'rejects the unfingerprintable key (not a key)' do
expect(build(:key, key: 'ssh-rsa an-invalid-key==')).not_to be_valid
end
+
+ where(:factory, :chars, :expected_sections) do
+ [
+ [:key, ["\n", "\r\n"], 3],
+ [:key, [' ', ' '], 3],
+ [:key_without_comment, [' ', ' '], 2]
+ ]
+ end
+
+ with_them do
+ let!(:key) { create(factory) }
+ let!(:original_fingerprint) { key.fingerprint }
+
+ it 'accepts a key with blank space characters after stripping them' do
+ modified_key = key.key.insert(100, chars.first).insert(40, chars.last)
+ _, content = modified_key.split
+
+ key.update!(key: modified_key)
+
+ expect(key).to be_valid
+ expect(key.key.split.size).to eq(expected_sections)
+
+ expect(content).not_to match(/\s/)
+ expect(original_fingerprint).to eq(key.fingerprint)
+ end
+ end
+ end
+
+ context 'validate size' do
+ where(:key_content, :result) do
+ [
+ [Spec::Support::Helpers::KeyGeneratorHelper.new(512).generate, false],
+ [Spec::Support::Helpers::KeyGeneratorHelper.new(8192).generate, false],
+ [Spec::Support::Helpers::KeyGeneratorHelper.new(1024).generate, true]
+ ]
+ end
+
+ with_them do
+ it 'validates the size of the key' do
+ key = build(:key, key: key_content)
+
+ expect(key.valid?).to eq(result)
+ end
+ end
end
context 'validate it meets key restrictions' do
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index c3673a0e2a3..5e126bc4bea 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -204,7 +204,7 @@ describe Namespace do
let(:parent) { create(:group, name: 'parent', path: 'parent') }
let(:child) { create(:group, name: 'child', path: 'child', parent: parent) }
let!(:project) { create(:project_empty_repo, path: 'the-project', namespace: child, skip_disk_validation: true) }
- let(:uploads_dir) { File.join(CarrierWave.root, FileUploader.base_dir) }
+ let(:uploads_dir) { FileUploader.root }
let(:pages_dir) { File.join(TestEnv.pages_path) }
before do
@@ -567,32 +567,62 @@ describe Namespace do
end
end
- describe "#allowed_path_by_redirects" do
- let(:namespace1) { create(:namespace, path: 'foo') }
+ describe '#remove_exports' do
+ let(:legacy_project) { create(:project, :with_export, namespace: namespace) }
+ let(:hashed_project) { create(:project, :with_export, :hashed, namespace: namespace) }
+ let(:export_path) { Dir.mktmpdir('namespace_remove_exports_spec') }
+ let(:legacy_export) { legacy_project.export_project_path }
+ let(:hashed_export) { hashed_project.export_project_path }
- context "when the path has been taken before" do
- before do
- namespace1.path = 'bar'
- namespace1.save!
+ it 'removes exports for legacy and hashed projects' do
+ allow(Gitlab::ImportExport).to receive(:storage_path) { export_path }
+
+ expect(File.exist?(legacy_export)).to be_truthy
+ expect(File.exist?(hashed_export)).to be_truthy
+
+ namespace.remove_exports!
+
+ expect(File.exist?(legacy_export)).to be_falsy
+ expect(File.exist?(hashed_export)).to be_falsy
+ end
+ end
+
+ describe '#full_path_was' do
+ context 'when the group has no parent' do
+ it 'should return the path was' do
+ group = create(:group, parent: nil)
+ expect(group.full_path_was).to eq(group.path_was)
end
+ end
+
+ context 'when a parent is assigned to a group with no previous parent' do
+ it 'should return the path was' do
+ group = create(:group, parent: nil)
- it 'should be invalid' do
- namespace2 = build(:group, path: 'foo')
- expect(namespace2).to be_invalid
+ parent = create(:group)
+ group.parent = parent
+
+ expect(group.full_path_was).to eq("#{group.path_was}")
end
+ end
+
+ context 'when a parent is removed from the group' do
+ it 'should return the parent full path' do
+ parent = create(:group)
+ group = create(:group, parent: parent)
+ group.parent = nil
- it 'should return an error on path' do
- namespace2 = build(:group, path: 'foo')
- namespace2.valid?
- expect(namespace2.errors.messages[:path].first).to eq('foo has been taken before. Please use another one')
+ expect(group.full_path_was).to eq("#{parent.full_path}/#{group.path}")
end
end
- context "when the path has not been taken before" do
- it 'should be valid' do
- expect(RedirectRoute.count).to eq(0)
- namespace = build(:namespace)
- expect(namespace).to be_valid
+ context 'when changing parents' do
+ it 'should return the previous parent full path' do
+ parent = create(:group)
+ group = create(:group, parent: parent)
+ new_parent = create(:group)
+ group.parent = new_parent
+ expect(group.full_path_was).to eq("#{parent.full_path}/#{group.path}")
end
end
end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 3d030927036..c853f707e6d 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -8,7 +8,7 @@ describe Note do
it { is_expected.to belong_to(:noteable).touch(false) }
it { is_expected.to belong_to(:author).class_name('User') }
- it { is_expected.to have_many(:todos).dependent(:destroy) }
+ it { is_expected.to have_many(:todos) }
end
describe 'modules' do
@@ -17,8 +17,6 @@ describe Note do
it { is_expected.to include_module(Participable) }
it { is_expected.to include_module(Mentionable) }
it { is_expected.to include_module(Awardable) }
-
- it { is_expected.to include_module(Gitlab::CurrentSettings) }
end
describe 'validation' do
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 1eaaadf56c5..748c366efca 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -205,7 +205,7 @@ describe JiraService do
@jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project))
expect(WebMock).to have_requested(:post, @comment_url).with(
- body: /#{custom_base_url}\/#{project.full_path}\/commit\/#{merge_request.diff_head_sha}/
+ body: %r{#{custom_base_url}/#{project.full_path}/commit/#{merge_request.diff_head_sha}}
).once
end
@@ -220,7 +220,7 @@ describe JiraService do
@jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project))
expect(WebMock).to have_requested(:post, @comment_url).with(
- body: /#{Gitlab.config.gitlab.url}\/#{project.full_path}\/commit\/#{merge_request.diff_head_sha}/
+ body: %r{#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/#{merge_request.diff_head_sha}}
).once
end
diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb
index 6980ba335b8..622d8844a72 100644
--- a/spec/models/project_services/kubernetes_service_spec.rb
+++ b/spec/models/project_services/kubernetes_service_spec.rb
@@ -408,7 +408,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
context 'if the services is active' do
it 'should return a message' do
- expect(kubernetes_service.deprecation_message).to match(/Your cluster information on this page is still editable/)
+ expect(kubernetes_service.deprecation_message).to match(/Your Kubernetes cluster information on this page is still editable/)
end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 31dcb543cbd..da940571bc1 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -117,7 +117,6 @@ describe Project do
it { is_expected.to include_module(Gitlab::ConfigHelper) }
it { is_expected.to include_module(Gitlab::ShellAdapter) }
it { is_expected.to include_module(Gitlab::VisibilityLevel) }
- it { is_expected.to include_module(Gitlab::CurrentSettings) }
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Sortable) }
end
@@ -2504,6 +2503,37 @@ describe Project do
end
end
+ describe '#remove_exports' do
+ let(:project) { create(:project, :with_export) }
+
+ it 'removes the exports directory for the project' do
+ expect(File.exist?(project.export_path)).to be_truthy
+
+ allow(FileUtils).to receive(:rm_rf).and_call_original
+ expect(FileUtils).to receive(:rm_rf).with(project.export_path).and_call_original
+ project.remove_exports
+
+ expect(File.exist?(project.export_path)).to be_falsy
+ end
+
+ it 'is a no-op when there is no namespace' do
+ export_path = project.export_path
+ project.update_column(:namespace_id, nil)
+
+ expect(FileUtils).not_to receive(:rm_rf).with(export_path)
+
+ project.remove_exports
+
+ expect(File.exist?(export_path)).to be_truthy
+ end
+
+ it 'is run when the project is destroyed' do
+ expect(project).to receive(:remove_exports).and_call_original
+
+ project.destroy
+ end
+ end
+
describe '#forks_count' do
it 'returns the number of forks' do
project = build(:project)
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index 929086305ba..1e7671476f1 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -127,7 +127,7 @@ describe ProjectWiki do
end
after do
- destroy_page(subject.pages.first.page)
+ subject.pages.each { |page| destroy_page(page.page) }
end
it "returns the latest version of the page if it exists" do
@@ -148,6 +148,17 @@ describe ProjectWiki do
page = subject.find_page("index page")
expect(page).to be_a WikiPage
end
+
+ context 'pages with multibyte-character title' do
+ before do
+ create_page("autre pagé", "C'est un génial Gollum Wiki")
+ end
+
+ it "can find a page by slug" do
+ page = subject.find_page("autre pagé")
+ expect(page.title).to eq("autre pagé")
+ end
+ end
end
context 'when Gitaly wiki_find_page is enabled' do
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 1102b1c9006..02a5ee54262 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -36,26 +36,49 @@ describe Repository do
end
describe '#branch_names_contains' do
- subject { repository.branch_names_contains(sample_commit.id) }
+ shared_examples '#branch_names_contains' do
+ set(:project) { create(:project, :repository) }
+ let(:repository) { project.repository }
- it { is_expected.to include('master') }
- it { is_expected.not_to include('feature') }
- it { is_expected.not_to include('fix') }
+ subject { repository.branch_names_contains(sample_commit.id) }
- describe 'when storage is broken', :broken_storage do
- it 'should raise a storage error' do
- expect_to_raise_storage_error do
- broken_repository.branch_names_contains(sample_commit.id)
+ it { is_expected.to include('master') }
+ it { is_expected.not_to include('feature') }
+ it { is_expected.not_to include('fix') }
+
+ describe 'when storage is broken', :broken_storage do
+ it 'should raise a storage error' do
+ expect_to_raise_storage_error do
+ broken_repository.branch_names_contains(sample_commit.id)
+ end
end
end
end
+
+ context 'when gitaly is enabled' do
+ it_behaves_like '#branch_names_contains'
+ end
+
+ context 'when gitaly is disabled', :skip_gitaly_mock do
+ it_behaves_like '#branch_names_contains'
+ end
end
describe '#tag_names_contains' do
- subject { repository.tag_names_contains(sample_commit.id) }
+ shared_examples '#tag_names_contains' do
+ subject { repository.tag_names_contains(sample_commit.id) }
- it { is_expected.to include('v1.1.0') }
- it { is_expected.not_to include('v1.0.0') }
+ it { is_expected.to include('v1.1.0') }
+ it { is_expected.not_to include('v1.0.0') }
+ end
+
+ context 'when gitaly is enabled' do
+ it_behaves_like '#tag_names_contains'
+ end
+
+ context 'when gitaly is enabled', :skip_gitaly_mock do
+ it_behaves_like '#tag_names_contains'
+ end
end
describe 'tags_sorted_by' do
@@ -959,19 +982,19 @@ describe Repository do
end
describe '#find_branch' do
- it 'loads a branch with a fresh repo' do
- expect(Gitlab::Git::Repository).to receive(:new).twice.and_call_original
+ context 'fresh_repo is true' do
+ it 'delegates the call to raw_repository' do
+ expect(repository.raw_repository).to receive(:find_branch).with('master', true)
- 2.times do
- expect(repository.find_branch('feature')).not_to be_nil
+ repository.find_branch('master', fresh_repo: true)
end
end
- it 'loads a branch with a cached repo' do
- expect(Gitlab::Git::Repository).to receive(:new).once.and_call_original
+ context 'fresh_repo is false' do
+ it 'delegates the call to raw_repository' do
+ expect(repository.raw_repository).to receive(:find_branch).with('master', false)
- 2.times do
- expect(repository.find_branch('feature', fresh_repo: false)).not_to be_nil
+ repository.find_branch('master', fresh_repo: false)
end
end
end
diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb
index 3e8f3848eca..bd498269798 100644
--- a/spec/models/todo_spec.rb
+++ b/spec/models/todo_spec.rb
@@ -20,6 +20,7 @@ describe Todo do
it { is_expected.to validate_presence_of(:action) }
it { is_expected.to validate_presence_of(:target_type) }
it { is_expected.to validate_presence_of(:user) }
+ it { is_expected.to validate_presence_of(:author) }
context 'for commits' do
subject { described_class.new(target_type: 'Commit') }
diff --git a/spec/models/upload_spec.rb b/spec/models/upload_spec.rb
index 345382ea8c7..36b8e5d304f 100644
--- a/spec/models/upload_spec.rb
+++ b/spec/models/upload_spec.rb
@@ -43,49 +43,16 @@ describe Upload do
.to(a_string_matching(/\A\h{64}\z/))
end
end
- end
-
- describe '.remove_path' do
- it 'removes all records at the given path' do
- described_class.create!(
- size: File.size(__FILE__),
- path: __FILE__,
- model: build_stubbed(:user),
- uploader: 'AvatarUploader'
- )
-
- expect { described_class.remove_path(__FILE__) }
- .to change { described_class.count }.from(1).to(0)
- end
- end
- describe '.record' do
- let(:fake_uploader) do
- double(
- file: double(size: 12_345),
- relative_path: 'foo/bar.jpg',
- model: build_stubbed(:user),
- class: 'AvatarUploader'
- )
- end
-
- it 'removes existing paths before creation' do
- expect(described_class).to receive(:remove_path)
- .with(fake_uploader.relative_path)
-
- described_class.record(fake_uploader)
- end
+ describe 'after_destroy' do
+ context 'uploader is FileUploader-based' do
+ subject { create(:upload, :issuable_upload) }
- it 'creates a new record and assigns size, path, model, and uploader' do
- upload = described_class.record(fake_uploader)
+ it 'calls delete_file!' do
+ is_expected.to receive(:delete_file!)
- aggregate_failures do
- expect(upload).to be_persisted
- expect(upload.size).to eq fake_uploader.file.size
- expect(upload.path).to eq fake_uploader.relative_path
- expect(upload.model_id).to eq fake_uploader.model.id
- expect(upload.model_type).to eq fake_uploader.model.class.to_s
- expect(upload.uploader).to eq fake_uploader.class
+ subject.destroy
+ end
end
end
end
@@ -111,27 +78,27 @@ describe Upload do
end
end
- describe '#calculate_checksum' do
- it 'calculates the SHA256 sum' do
- upload = described_class.new(
- path: __FILE__,
- size: described_class::CHECKSUM_THRESHOLD - 1.megabyte
- )
+ describe '#calculate_checksum!' do
+ let(:upload) do
+ described_class.new(path: __FILE__,
+ size: described_class::CHECKSUM_THRESHOLD - 1.megabyte)
+ end
+
+ it 'sets `checksum` to SHA256 sum of the file' do
expected = Digest::SHA256.file(__FILE__).hexdigest
- expect { upload.calculate_checksum }
+ expect { upload.calculate_checksum! }
.to change { upload.checksum }.from(nil).to(expected)
end
- it 'returns nil for a non-existant file' do
- upload = described_class.new(
- path: __FILE__,
- size: described_class::CHECKSUM_THRESHOLD - 1.megabyte
- )
-
+ it 'sets `checksum` to nil for a non-existant file' do
expect(upload).to receive(:exist?).and_return(false)
- expect(upload.calculate_checksum).to be_nil
+ checksum = Digest::SHA256.file(__FILE__).hexdigest
+ upload.checksum = checksum
+
+ expect { upload.calculate_checksum! }
+ .to change { upload.checksum }.from(checksum).to(nil)
end
end
@@ -148,4 +115,10 @@ describe Upload do
expect(upload).not_to exist
end
end
+
+ describe "#uploader_context" do
+ subject { create(:upload, :issuable_upload, secret: 'secret', filename: 'file.txt') }
+
+ it { expect(subject.uploader_context).to match(a_hash_including(secret: 'secret', identifier: 'file.txt')) }
+ end
end
diff --git a/spec/models/user_callout_spec.rb b/spec/models/user_callout_spec.rb
new file mode 100644
index 00000000000..64ba17c81fe
--- /dev/null
+++ b/spec/models/user_callout_spec.rb
@@ -0,0 +1,16 @@
+require 'rails_helper'
+
+describe UserCallout do
+ let!(:callout) { create(:user_callout) }
+
+ describe 'relationships' do
+ it { is_expected.to belong_to(:user) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:user) }
+
+ it { is_expected.to validate_presence_of(:feature_name) }
+ it { is_expected.to validate_uniqueness_of(:feature_name).scoped_to(:user_id).ignoring_case_sensitivity }
+ end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 594f23718da..24d4d8f1741 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -1,14 +1,12 @@
require 'spec_helper'
describe User do
- include Gitlab::CurrentSettings
include ProjectForksHelper
describe 'modules' do
subject { described_class }
it { is_expected.to include_module(Gitlab::ConfigHelper) }
- it { is_expected.to include_module(Gitlab::CurrentSettings) }
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Sortable) }
it { is_expected.to include_module(TokenAuthenticatable) }
@@ -35,7 +33,7 @@ describe User do
it { is_expected.to have_many(:merge_requests).dependent(:destroy) }
it { is_expected.to have_many(:identities).dependent(:destroy) }
it { is_expected.to have_many(:spam_logs).dependent(:destroy) }
- it { is_expected.to have_many(:todos).dependent(:destroy) }
+ it { is_expected.to have_many(:todos) }
it { is_expected.to have_many(:award_emoji).dependent(:destroy) }
it { is_expected.to have_many(:triggers).dependent(:destroy) }
it { is_expected.to have_many(:builds).dependent(:nullify) }
@@ -103,7 +101,7 @@ describe User do
user = build(:user, username: 'dashboard')
expect(user).not_to be_valid
- expect(user.errors.values).to eq [['dashboard is a reserved name']]
+ expect(user.errors.messages[:username]).to eq ['dashboard is a reserved name']
end
it 'allows child names' do
@@ -134,6 +132,23 @@ describe User do
expect(user.errors.messages[:username].first).to match('cannot be changed if a personal project has container registry tags')
end
end
+
+ context 'when the username was used by another user before' do
+ let(:username) { 'foo' }
+ let!(:other_user) { create(:user, username: username) }
+
+ before do
+ other_user.username = 'bar'
+ other_user.save!
+ end
+
+ it 'is invalid' do
+ user = build(:user, username: username)
+
+ expect(user).not_to be_valid
+ expect(user.errors.messages[:"namespace.route.path"].first).to eq('foo has been taken before. Please use another one')
+ end
+ end
end
it 'has a DB-level NOT NULL constraint on projects_limit' do
@@ -560,7 +575,7 @@ describe User do
stub_config_setting(default_can_create_group: true)
expect { user.update_attributes(external: false) }.to change { user.can_create_group }.to(true)
- .and change { user.projects_limit }.to(current_application_settings.default_projects_limit)
+ .and change { user.projects_limit }.to(Gitlab::CurrentSettings.default_projects_limit)
end
end
@@ -826,7 +841,7 @@ describe User do
end
end
- context 'when current_application_settings.user_default_external is true' do
+ context 'when Gitlab::CurrentSettings.user_default_external is true' do
before do
stub_application_setting(user_default_external: true)
end
@@ -1435,28 +1450,34 @@ describe User do
describe '#sort' do
before do
described_class.delete_all
- @user = create :user, created_at: Date.today, last_sign_in_at: Date.today, name: 'Alpha'
- @user1 = create :user, created_at: Date.today - 1, last_sign_in_at: Date.today - 1, name: 'Omega'
- @user2 = create :user, created_at: Date.today - 2, last_sign_in_at: nil, name: 'Beta'
+ @user = create :user, created_at: Date.today, current_sign_in_at: Date.today, name: 'Alpha'
+ @user1 = create :user, created_at: Date.today - 1, current_sign_in_at: Date.today - 1, name: 'Omega'
+ @user2 = create :user, created_at: Date.today - 2, name: 'Beta'
end
context 'when sort by recent_sign_in' do
- it 'sorts users by the recent sign-in time' do
- expect(described_class.sort('recent_sign_in').first).to eq(@user)
+ let(:users) { described_class.sort('recent_sign_in') }
+
+ it 'sorts users by recent sign-in time' do
+ expect(users.first).to eq(@user)
+ expect(users.second).to eq(@user1)
end
it 'pushes users who never signed in to the end' do
- expect(described_class.sort('recent_sign_in').third).to eq(@user2)
+ expect(users.third).to eq(@user2)
end
end
context 'when sort by oldest_sign_in' do
+ let(:users) { described_class.sort('oldest_sign_in') }
+
it 'sorts users by the oldest sign-in time' do
- expect(described_class.sort('oldest_sign_in').first).to eq(@user1)
+ expect(users.first).to eq(@user1)
+ expect(users.second).to eq(@user)
end
it 'pushes users who never signed in to the end' do
- expect(described_class.sort('oldest_sign_in').third).to eq(@user2)
+ expect(users.third).to eq(@user2)
end
end
@@ -2619,7 +2640,7 @@ describe User do
it 'should raise an ActiveRecord::RecordInvalid exception' do
user2 = build(:user, username: 'foo')
- expect { user2.save! }.to raise_error(ActiveRecord::RecordInvalid, /Path foo has been taken before/)
+ expect { user2.save! }.to raise_error(ActiveRecord::RecordInvalid, /Namespace route path foo has been taken before/)
end
end
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index 9840afe6c4e..d53ba497ed1 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -188,14 +188,37 @@ describe WikiPage do
end
end
- describe "#update" do
+ describe '#create', :skip_gitaly_mock do
+ context 'with valid attributes' do
+ it 'raises an error if a page with the same path already exists' do
+ create_page('New Page', 'content')
+ create_page('foo/bar', 'content')
+ expect { create_page('New Page', 'other content') }.to raise_error Gitlab::Git::Wiki::DuplicatePageError
+ expect { create_page('foo/bar', 'other content') }.to raise_error Gitlab::Git::Wiki::DuplicatePageError
+
+ destroy_page('New Page')
+ destroy_page('bar', 'foo')
+ end
+
+ it 'if the title is preceded by a / it is removed' do
+ create_page('/New Page', 'content')
+
+ expect(wiki.find_page('New Page')).not_to be_nil
+
+ destroy_page('New Page')
+ end
+ end
+ end
+
+ # Remove skip_gitaly_mock flag when gitaly_update_page implements moving pages
+ describe "#update", :skip_gitaly_mock do
before do
create_page("Update", "content")
@page = wiki.find_page("Update")
end
after do
- destroy_page(@page.title)
+ destroy_page(@page.title, @page.directory)
end
context "with valid attributes" do
@@ -233,6 +256,95 @@ describe WikiPage do
expect { @page.update(content: 'more content', last_commit_sha: 'xxx') }.to raise_error(WikiPage::PageChangedError)
end
end
+
+ context 'when renaming a page' do
+ it 'raises an error if the page already exists' do
+ create_page('Existing Page', 'content')
+
+ expect { @page.update(title: 'Existing Page', content: 'new_content') }.to raise_error(WikiPage::PageRenameError)
+ expect(@page.title).to eq 'Update'
+ expect(@page.content).to eq 'new_content'
+
+ destroy_page('Existing Page')
+ end
+
+ it 'updates the content and rename the file' do
+ new_title = 'Renamed Page'
+ new_content = 'updated content'
+
+ expect(@page.update(title: new_title, content: new_content)).to be_truthy
+
+ @page = wiki.find_page(new_title)
+
+ expect(@page).not_to be_nil
+ expect(@page.content).to eq new_content
+ end
+ end
+
+ context 'when moving a page' do
+ it 'raises an error if the page already exists' do
+ create_page('foo/Existing Page', 'content')
+
+ expect { @page.update(title: 'foo/Existing Page', content: 'new_content') }.to raise_error(WikiPage::PageRenameError)
+ expect(@page.title).to eq 'Update'
+ expect(@page.content).to eq 'new_content'
+
+ destroy_page('Existing Page', 'foo')
+ end
+
+ it 'updates the content and moves the file' do
+ new_title = 'foo/Other Page'
+ new_content = 'new_content'
+
+ expect(@page.update(title: new_title, content: new_content)).to be_truthy
+
+ page = wiki.find_page(new_title)
+
+ expect(page).not_to be_nil
+ expect(page.content).to eq new_content
+ end
+
+ context 'in subdir' do
+ before do
+ create_page('foo/Existing Page', 'content')
+ @page = wiki.find_page('foo/Existing Page')
+ end
+
+ it 'moves the page to the root folder if the title is preceded by /' do
+ expect(@page.slug).to eq 'foo/Existing-Page'
+ expect(@page.update(title: '/Existing Page', content: 'new_content')).to be_truthy
+ expect(@page.slug).to eq 'Existing-Page'
+ end
+
+ it 'does nothing if it has the same title' do
+ original_path = @page.slug
+
+ expect(@page.update(title: 'Existing Page', content: 'new_content')).to be_truthy
+ expect(@page.slug).to eq original_path
+ end
+ end
+
+ context 'in root dir' do
+ it 'does nothing if the title is preceded by /' do
+ original_path = @page.slug
+
+ expect(@page.update(title: '/Update', content: 'new_content')).to be_truthy
+ expect(@page.slug).to eq original_path
+ end
+ end
+ end
+
+ context "with invalid attributes" do
+ it 'aborts update if title blank' do
+ expect(@page.update(title: '', content: 'new_content')).to be_falsey
+ expect(@page.content).to eq 'new_content'
+
+ page = wiki.find_page('Update')
+ expect(page.content).to eq 'content'
+
+ @page.title = 'Update'
+ end
+ end
end
describe "#destroy" do
@@ -252,18 +364,34 @@ describe WikiPage do
end
describe "#versions" do
- before do
- create_page("Update", "content")
- @page = wiki.find_page("Update")
+ shared_examples 'wiki page versions' do
+ let(:page) { wiki.find_page("Update") }
+
+ before do
+ create_page("Update", "content")
+ end
+
+ after do
+ destroy_page("Update")
+ end
+
+ it "returns an array of all commits for the page" do
+ 3.times { |i| page.update(content: "content #{i}") }
+
+ expect(page.versions.count).to eq(4)
+ end
+
+ it 'returns instances of WikiPageVersion' do
+ expect(page.versions).to all( be_a(Gitlab::Git::WikiPageVersion) )
+ end
end
- after do
- destroy_page("Update")
+ context 'when Gitaly is enabled' do
+ it_behaves_like 'wiki page versions'
end
- it "returns an array of all commits for the page" do
- 3.times { |i| @page.update(content: "content #{i}") }
- expect(@page.versions.count).to eq(4)
+ context 'when Gitaly is disabled', :disable_gitaly do
+ it_behaves_like 'wiki page versions'
end
end
@@ -421,8 +549,8 @@ describe WikiPage do
wiki.wiki.write_page(name, :markdown, content, commit_details)
end
- def destroy_page(title)
- page = wiki.wiki.page(title: title)
+ def destroy_page(title, dir = '')
+ page = wiki.wiki.page(title: title, dir: dir)
wiki.delete_page(page, "test commit")
end
diff --git a/spec/policies/ci/pipeline_schedule_policy_spec.rb b/spec/policies/ci/pipeline_schedule_policy_spec.rb
index 1b0e9fac355..c0c3eda4911 100644
--- a/spec/policies/ci/pipeline_schedule_policy_spec.rb
+++ b/spec/policies/ci/pipeline_schedule_policy_spec.rb
@@ -88,5 +88,19 @@ describe Ci::PipelineSchedulePolicy, :models do
expect(policy).to be_allowed :admin_pipeline_schedule
end
end
+
+ describe 'rules for non-owner of schedule' do
+ let(:owner) { create(:user) }
+
+ before do
+ project.add_master(owner)
+ project.add_master(user)
+ pipeline_schedule.update(owner: owner)
+ end
+
+ it 'includes abilities to take ownership' do
+ expect(policy).to be_allowed :take_ownership_pipeline_schedule
+ end
+ end
end
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index f2593a1a75c..129344f105f 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -92,7 +92,7 @@ describe ProjectPolicy do
it 'does not include the read_issue permission when the issue author is not a member of the private project' do
project = create(:project, :private)
- issue = create(:issue, project: project)
+ issue = create(:issue, project: project, author: create(:user))
user = issue.author
expect(project.team.member?(issue.author)).to be false
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 3c0b4728dc2..bb0034e3237 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -30,6 +30,21 @@ describe API::Groups do
expect(json_response)
.to satisfy_one { |group| group['name'] == group1.name }
end
+
+ it 'avoids N+1 queries' do
+ # Establish baseline
+ get api("/groups", admin)
+
+ control = ActiveRecord::QueryRecorder.new do
+ get api("/groups", admin)
+ end
+
+ create(:group)
+
+ expect do
+ get api("/groups", admin)
+ end.not_to exceed_query_limit(control)
+ end
end
context "when authenticated as user" do
diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb
index f8d0b63afec..6192bbd4abb 100644
--- a/spec/requests/api/jobs_spec.rb
+++ b/spec/requests/api/jobs_spec.rb
@@ -446,16 +446,27 @@ describe API::Jobs do
end
describe 'GET /projects/:id/jobs/:job_id/trace' do
- let(:job) { create(:ci_build, :trace, pipeline: pipeline) }
-
before do
get api("/projects/#{project.id}/jobs/#{job.id}/trace", api_user)
end
context 'authorized user' do
- it 'returns specific job trace' do
- expect(response).to have_gitlab_http_status(200)
- expect(response.body).to eq(job.trace.raw)
+ context 'when trace is artifact' do
+ let(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) }
+
+ it 'returns specific job trace' do
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.body).to eq(job.trace.raw)
+ end
+ end
+
+ context 'when trace is file' do
+ let(:job) { create(:ci_build, :trace_live, pipeline: pipeline) }
+
+ it 'returns specific job trace' do
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.body).to eq(job.trace.raw)
+ end
end
end
@@ -543,11 +554,11 @@ describe API::Jobs do
end
context 'job is erasable' do
- let(:job) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline) }
+ let(:job) { create(:ci_build, :trace_artifact, :artifacts, :success, project: project, pipeline: pipeline) }
it 'erases job content' do
expect(response).to have_gitlab_http_status(201)
- expect(job).not_to have_trace
+ expect(job.trace.exist?).to be_falsy
expect(job.artifacts_file.exists?).to be_falsy
expect(job.artifacts_metadata.exists?).to be_falsy
end
@@ -561,7 +572,7 @@ describe API::Jobs do
end
context 'job is not erasable' do
- let(:job) { create(:ci_build, :trace, project: project, pipeline: pipeline) }
+ let(:job) { create(:ci_build, :trace_live, project: project, pipeline: pipeline) }
it 'responds with forbidden' do
expect(response).to have_gitlab_http_status(403)
@@ -570,7 +581,7 @@ describe API::Jobs do
context 'when a developer erases a build' do
let(:role) { :developer }
- let(:job) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline, user: owner) }
+ let(:job) { create(:ci_build, :trace_artifact, :artifacts, :success, project: project, pipeline: pipeline, user: owner) }
context 'when the build was created by the developer' do
let(:owner) { user }
@@ -593,7 +604,7 @@ describe API::Jobs do
context 'artifacts did not expire' do
let(:job) do
- create(:ci_build, :trace, :artifacts, :success,
+ create(:ci_build, :trace_artifact, :artifacts, :success,
project: project, pipeline: pipeline, artifacts_expire_at: Time.now + 7.days)
end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 97e7ffcd38e..f11cd638d96 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -2,8 +2,6 @@
require 'spec_helper'
describe API::Projects do
- include Gitlab::CurrentSettings
-
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:user3) { create(:user) }
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index cb66d23b77c..0bd88748479 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -638,7 +638,7 @@ describe API::Runner do
end
describe 'PUT /api/v4/jobs/:id' do
- let(:job) { create(:ci_build, :pending, :trace, pipeline: pipeline, runner_id: runner.id) }
+ let(:job) { create(:ci_build, :pending, :trace_live, pipeline: pipeline, runner_id: runner.id) }
before do
job.run!
@@ -680,11 +680,17 @@ describe API::Runner do
end
context 'when tace is given' do
- it 'updates a running build' do
- update_job(trace: 'BUILD TRACE UPDATED')
+ it 'creates a trace artifact' do
+ allow_any_instance_of(BuildFinishedWorker).to receive(:perform).with(job.id) do
+ CreateTraceArtifactWorker.new.perform(job.id)
+ end
+
+ update_job(state: 'success', trace: 'BUILD TRACE UPDATED')
+ job.reload
expect(response).to have_gitlab_http_status(200)
- expect(job.reload.trace.raw).to eq 'BUILD TRACE UPDATED'
+ expect(job.trace.raw).to eq 'BUILD TRACE UPDATED'
+ expect(job.job_artifacts_trace.open.read).to eq 'BUILD TRACE UPDATED'
end
end
@@ -713,7 +719,7 @@ describe API::Runner do
end
describe 'PATCH /api/v4/jobs/:id/trace' do
- let(:job) { create(:ci_build, :running, :trace, runner_id: runner.id, pipeline: pipeline) }
+ let(:job) { create(:ci_build, :running, :trace_live, runner_id: runner.id, pipeline: pipeline) }
let(:headers) { { API::Helpers::Runner::JOB_TOKEN_HEADER => job.token, 'Content-Type' => 'text/plain' } }
let(:headers_with_range) { headers.merge({ 'Content-Range' => '11-20' }) }
let(:update_interval) { 10.seconds.to_i }
@@ -774,7 +780,7 @@ describe API::Runner do
context 'when project for the build has been deleted' do
let(:job) do
- create(:ci_build, :running, :trace, runner_id: runner.id, pipeline: pipeline) do |job|
+ create(:ci_build, :running, :trace_live, runner_id: runner.id, pipeline: pipeline) do |job|
job.project.update(pending_delete: true)
end
end
@@ -945,7 +951,7 @@ describe API::Runner do
context 'when artifacts are being stored inside of tmp path' do
before do
# by configuring this path we allow to pass temp file from any path
- allow(JobArtifactUploader).to receive(:artifacts_upload_path).and_return('/')
+ allow(JobArtifactUploader).to receive(:workhorse_upload_path).and_return('/')
end
context 'when job has been erased' do
@@ -1122,7 +1128,7 @@ describe API::Runner do
# by configuring this path we allow to pass file from @tmpdir only
# but all temporary files are stored in system tmp directory
@tmpdir = Dir.mktmpdir
- allow(JobArtifactUploader).to receive(:artifacts_upload_path).and_return(@tmpdir)
+ allow(JobArtifactUploader).to receive(:workhorse_upload_path).and_return(@tmpdir)
end
after do
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 2428e63e149..f406d2ffb22 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -199,6 +199,24 @@ describe API::Users do
expect(json_response.size).to eq(1)
expect(json_response.first['username']).to eq(user.username)
end
+
+ it 'returns the correct order when sorted by id' do
+ admin
+ user
+
+ get api('/users', admin), { order_by: 'id', sort: 'asc' }
+
+ expect(response).to match_response_schema('public_api/v4/user/admins')
+ expect(json_response.size).to eq(2)
+ expect(json_response.first['id']).to eq(admin.id)
+ expect(json_response.last['id']).to eq(user.id)
+ end
+
+ it 'returns 400 when provided incorrect sort params' do
+ get api('/users', admin), { order_by: 'magic', sort: 'asc' }
+
+ expect(response).to have_gitlab_http_status(400)
+ end
end
end
diff --git a/spec/requests/api/v3/builds_spec.rb b/spec/requests/api/v3/builds_spec.rb
index 3f92288fef0..79041c6a792 100644
--- a/spec/requests/api/v3/builds_spec.rb
+++ b/spec/requests/api/v3/builds_spec.rb
@@ -352,7 +352,7 @@ describe API::V3::Builds do
end
describe 'GET /projects/:id/builds/:build_id/trace' do
- let(:build) { create(:ci_build, :trace, pipeline: pipeline) }
+ let(:build) { create(:ci_build, :trace_live, pipeline: pipeline) }
before do
get v3_api("/projects/#{project.id}/builds/#{build.id}/trace", api_user)
@@ -447,7 +447,7 @@ describe API::V3::Builds do
end
context 'job is erasable' do
- let(:build) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline) }
+ let(:build) { create(:ci_build, :trace_artifact, :artifacts, :success, project: project, pipeline: pipeline) }
it 'erases job content' do
expect(response.status).to eq 201
@@ -463,7 +463,7 @@ describe API::V3::Builds do
end
context 'job is not erasable' do
- let(:build) { create(:ci_build, :trace, project: project, pipeline: pipeline) }
+ let(:build) { create(:ci_build, :trace_live, project: project, pipeline: pipeline) }
it 'responds with forbidden' do
expect(response.status).to eq 403
@@ -478,7 +478,7 @@ describe API::V3::Builds do
context 'artifacts did not expire' do
let(:build) do
- create(:ci_build, :trace, :artifacts, :success,
+ create(:ci_build, :trace_artifact, :artifacts, :success,
project: project, pipeline: pipeline, artifacts_expire_at: Time.now + 7.days)
end
diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb
index 13e465e0b2d..5d99d9495f3 100644
--- a/spec/requests/api/v3/projects_spec.rb
+++ b/spec/requests/api/v3/projects_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe API::V3::Projects do
- include Gitlab::CurrentSettings
-
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:user3) { create(:user) }
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index bee918a20aa..930ef49b7f3 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -958,7 +958,7 @@ describe 'Git LFS API and storage' do
end
it 'responds with status 200, location of lfs store and object details' do
- expect(json_response['StoreLFSPath']).to eq("#{Gitlab.config.shared.path}/lfs-objects/tmp/upload")
+ expect(json_response['StoreLFSPath']).to eq(LfsObjectUploader.workhorse_upload_path)
expect(json_response['LfsOid']).to eq(sample_oid)
expect(json_response['LfsSize']).to eq(sample_size)
end
@@ -1075,7 +1075,7 @@ describe 'Git LFS API and storage' do
end
it 'with location of lfs store and object details' do
- expect(json_response['StoreLFSPath']).to eq("#{Gitlab.config.shared.path}/lfs-objects/tmp/upload")
+ expect(json_response['StoreLFSPath']).to eq(LfsObjectUploader.workhorse_upload_path)
expect(json_response['LfsOid']).to eq(sample_oid)
expect(json_response['LfsSize']).to eq(sample_size)
end
diff --git a/spec/services/ci/create_trace_artifact_service_spec.rb b/spec/services/ci/create_trace_artifact_service_spec.rb
new file mode 100644
index 00000000000..847a88920fe
--- /dev/null
+++ b/spec/services/ci/create_trace_artifact_service_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe Ci::CreateTraceArtifactService do
+ describe '#execute' do
+ subject { described_class.new(nil, nil).execute(job) }
+
+ let(:job) { create(:ci_build) }
+
+ context 'when the job does not have trace artifact' do
+ context 'when the job has a trace file' do
+ before do
+ allow_any_instance_of(Gitlab::Ci::Trace)
+ .to receive(:default_path) { expand_fixture_path('trace/sample_trace') }
+
+ allow_any_instance_of(JobArtifactUploader).to receive(:move_to_cache) { false }
+ allow_any_instance_of(JobArtifactUploader).to receive(:move_to_store) { false }
+ end
+
+ it 'creates trace artifact' do
+ expect { subject }.to change { Ci::JobArtifact.count }.by(1)
+
+ expect(job.job_artifacts_trace.read_attribute(:file)).to eq('sample_trace')
+ end
+
+ context 'when the job has already had trace artifact' do
+ before do
+ create(:ci_job_artifact, :trace, job: job)
+ end
+
+ it 'does not create trace artifact' do
+ expect { subject }.not_to change { Ci::JobArtifact.count }
+ end
+ end
+ end
+
+ context 'when the job does not have a trace file' do
+ it 'does not create trace artifact' do
+ expect { subject }.not_to change { Ci::JobArtifact.count }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index a06397a0782..2c2f48e323d 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -17,7 +17,8 @@ describe Ci::RetryBuildService do
%i[id status user token coverage trace runner artifacts_expire_at
artifacts_file artifacts_metadata artifacts_size created_at
updated_at started_at finished_at queued_at erased_by
- erased_at auto_canceled_by job_artifacts job_artifacts_archive job_artifacts_metadata].freeze
+ erased_at auto_canceled_by job_artifacts job_artifacts_archive
+ job_artifacts_metadata job_artifacts_trace].freeze
IGNORE_ACCESSORS =
%i[type lock_version target_url base_tags trace_sections
@@ -36,7 +37,7 @@ describe Ci::RetryBuildService do
let(:build) do
create(:ci_build, :failed, :artifacts, :expired, :erased,
:queued, :coverage, :tags, :allowed_to_fail, :on_tag,
- :triggered, :trace, :teardown_environment,
+ :triggered, :trace_artifact, :teardown_environment,
description: 'my-job', stage: 'test', pipeline: pipeline,
auto_canceled_by: create(:ci_empty_pipeline, project: project)) do |build|
##
diff --git a/spec/services/files/create_service_spec.rb b/spec/services/files/create_service_spec.rb
new file mode 100644
index 00000000000..030263b1502
--- /dev/null
+++ b/spec/services/files/create_service_spec.rb
@@ -0,0 +1,78 @@
+require "spec_helper"
+
+describe Files::CreateService do
+ let(:project) { create(:project, :repository) }
+ let(:repository) { project.repository }
+ let(:user) { create(:user) }
+ let(:file_content) { 'Test file content' }
+ let(:branch_name) { project.default_branch }
+ let(:start_branch) { branch_name }
+
+ let(:commit_params) do
+ {
+ file_path: file_path,
+ commit_message: "Update File",
+ file_content: file_content,
+ file_content_encoding: "text",
+ start_project: project,
+ start_branch: start_branch,
+ branch_name: branch_name
+ }
+ end
+
+ subject { described_class.new(project, user, commit_params) }
+
+ before do
+ project.add_master(user)
+ end
+
+ describe "#execute" do
+ context 'when file matches LFS filter' do
+ let(:file_path) { 'test_file.lfs' }
+ let(:branch_name) { 'lfs' }
+
+ context 'with LFS disabled' do
+ it 'skips gitattributes check' do
+ expect(repository).not_to receive(:attributes_at)
+
+ subject.execute
+ end
+
+ it "doesn't create LFS pointers" do
+ subject.execute
+
+ blob = repository.blob_at('lfs', file_path)
+
+ expect(blob.data).not_to start_with('version https://git-lfs.github.com/spec/v1')
+ expect(blob.data).to eq(file_content)
+ end
+ end
+
+ context 'with LFS enabled' do
+ before do
+ allow(project).to receive(:lfs_enabled?).and_return(true)
+ end
+
+ it 'creates an LFS pointer' do
+ subject.execute
+
+ blob = repository.blob_at('lfs', file_path)
+
+ expect(blob.data).to start_with('version https://git-lfs.github.com/spec/v1')
+ end
+
+ it "creates an LfsObject with the file's content" do
+ subject.execute
+
+ expect(LfsObject.last.file.read).to eq file_content
+ end
+
+ it 'links the LfsObject to the project' do
+ expect do
+ subject.execute
+ end.to change { project.lfs_objects.count }.by(1)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb
new file mode 100644
index 00000000000..bcc01b087f3
--- /dev/null
+++ b/spec/services/groups/transfer_service_spec.rb
@@ -0,0 +1,414 @@
+require 'rails_helper'
+
+describe Groups::TransferService, :postgresql do
+ let(:user) { create(:user) }
+ let(:new_parent_group) { create(:group, :public) }
+ let!(:group_member) { create(:group_member, :owner, group: group, user: user) }
+ let(:transfer_service) { described_class.new(group, user) }
+
+ shared_examples 'ensuring allowed transfer for a group' do
+ context 'with other database than PostgreSQL' do
+ before do
+ allow(Group).to receive(:supports_nested_groups?).and_return(false)
+ end
+
+ it 'should return false' do
+ expect(transfer_service.execute(new_parent_group)).to be_falsy
+ end
+
+ it 'should add an error on group' do
+ transfer_service.execute(new_parent_group)
+ expect(transfer_service.error).to eq('Transfer failed: Database is not supported.')
+ end
+ end
+
+ context "when there's an exception on Gitlab shell directories" do
+ let(:new_parent_group) { create(:group, :public) }
+
+ before do
+ allow_any_instance_of(described_class).to receive(:update_group_attributes).and_raise(Gitlab::UpdatePathError, 'namespace directory cannot be moved')
+ create(:group_member, :owner, group: new_parent_group, user: user)
+ end
+
+ it 'should return false' do
+ expect(transfer_service.execute(new_parent_group)).to be_falsy
+ end
+
+ it 'should add an error on group' do
+ transfer_service.execute(new_parent_group)
+ expect(transfer_service.error).to eq('Transfer failed: namespace directory cannot be moved')
+ end
+ end
+ end
+
+ describe '#execute' do
+ context 'when transforming a group into a root group' do
+ let!(:group) { create(:group, :public, :nested) }
+
+ it_behaves_like 'ensuring allowed transfer for a group'
+
+ context 'when the group is already a root group' do
+ let(:group) { create(:group, :public) }
+
+ it 'should add an error on group' do
+ transfer_service.execute(nil)
+ expect(transfer_service.error).to eq('Transfer failed: Group is already a root group.')
+ end
+ end
+
+ context 'when the user does not have the right policies' do
+ let!(:group_member) { create(:group_member, :guest, group: group, user: user) }
+
+ it "should return false" do
+ expect(transfer_service.execute(nil)).to be_falsy
+ end
+
+ it "should add an error on group" do
+ transfer_service.execute(new_parent_group)
+ expect(transfer_service.error).to eq("Transfer failed: You don't have enough permissions.")
+ end
+ end
+
+ context 'when there is a group with the same path' do
+ let!(:group) { create(:group, :public, :nested, path: 'not-unique') }
+
+ before do
+ create(:group, path: 'not-unique')
+ end
+
+ it 'should return false' do
+ expect(transfer_service.execute(nil)).to be_falsy
+ end
+
+ it 'should add an error on group' do
+ transfer_service.execute(nil)
+ expect(transfer_service.error).to eq('Transfer failed: The parent group already has a subgroup with the same path.')
+ end
+ end
+
+ context 'when the group is a subgroup and the transfer is valid' do
+ let!(:subgroup1) { create(:group, :private, parent: group) }
+ let!(:subgroup2) { create(:group, :internal, parent: group) }
+ let!(:project1) { create(:project, :repository, :private, namespace: group) }
+
+ before do
+ transfer_service.execute(nil)
+ group.reload
+ end
+
+ it 'should update group attributes' do
+ expect(group.parent).to be_nil
+ end
+
+ it 'should update group children path' do
+ group.children.each do |subgroup|
+ expect(subgroup.full_path).to eq("#{group.path}/#{subgroup.path}")
+ end
+ end
+
+ it 'should update group projects path' do
+ group.projects.each do |project|
+ expect(project.full_path).to eq("#{group.path}/#{project.path}")
+ end
+ end
+ end
+ end
+
+ context 'when transferring a subgroup into another group' do
+ let(:group) { create(:group, :public, :nested) }
+
+ it_behaves_like 'ensuring allowed transfer for a group'
+
+ context 'when the new parent group is the same as the previous parent group' do
+ let(:group) { create(:group, :public, :nested, parent: new_parent_group) }
+
+ it 'should return false' do
+ expect(transfer_service.execute(new_parent_group)).to be_falsy
+ end
+
+ it 'should add an error on group' do
+ transfer_service.execute(new_parent_group)
+ expect(transfer_service.error).to eq('Transfer failed: Group is already associated to the parent group.')
+ end
+ end
+
+ context 'when the user does not have the right policies' do
+ let!(:group_member) { create(:group_member, :guest, group: group, user: user) }
+
+ it "should return false" do
+ expect(transfer_service.execute(new_parent_group)).to be_falsy
+ end
+
+ it "should add an error on group" do
+ transfer_service.execute(new_parent_group)
+ expect(transfer_service.error).to eq("Transfer failed: You don't have enough permissions.")
+ end
+ end
+
+ context 'when the parent has a group with the same path' do
+ before do
+ create(:group_member, :owner, group: new_parent_group, user: user)
+ group.update_attribute(:path, "not-unique")
+ create(:group, path: "not-unique", parent: new_parent_group)
+ end
+
+ it 'should return false' do
+ expect(transfer_service.execute(new_parent_group)).to be_falsy
+ end
+
+ it 'should add an error on group' do
+ transfer_service.execute(new_parent_group)
+ expect(transfer_service.error).to eq('Transfer failed: The parent group already has a subgroup with the same path.')
+ end
+ end
+
+ context 'when the parent group has a project with the same path' do
+ let!(:group) { create(:group, :public, :nested, path: 'foo') }
+
+ before do
+ create(:group_member, :owner, group: new_parent_group, user: user)
+ create(:project, path: 'foo', namespace: new_parent_group)
+ group.update_attribute(:path, 'foo')
+ end
+
+ it 'should return false' do
+ expect(transfer_service.execute(new_parent_group)).to be_falsy
+ end
+
+ it 'should add an error on group' do
+ transfer_service.execute(new_parent_group)
+ expect(transfer_service.error).to eq('Transfer failed: Validation failed: Route path has already been taken, Route is invalid')
+ end
+ end
+
+ context 'when the group is allowed to be transferred' do
+ before do
+ create(:group_member, :owner, group: new_parent_group, user: user)
+ transfer_service.execute(new_parent_group)
+ end
+
+ context 'when the group has a lower visibility than the parent group' do
+ let(:new_parent_group) { create(:group, :public) }
+ let(:group) { create(:group, :private, :nested) }
+
+ it 'should not update the visibility for the group' do
+ group.reload
+ expect(group.private?).to be_truthy
+ expect(group.visibility_level).not_to eq(new_parent_group.visibility_level)
+ end
+ end
+
+ context 'when the group has a higher visibility than the parent group' do
+ let(:new_parent_group) { create(:group, :private) }
+ let(:group) { create(:group, :public, :nested) }
+
+ it 'should update visibility level based on the parent group' do
+ group.reload
+ expect(group.private?).to be_truthy
+ expect(group.visibility_level).to eq(new_parent_group.visibility_level)
+ end
+ end
+
+ it 'should update visibility for the group based on the parent group' do
+ expect(group.visibility_level).to eq(new_parent_group.visibility_level)
+ end
+
+ it 'should update parent group to the new parent ' do
+ expect(group.parent).to eq(new_parent_group)
+ end
+
+ it 'should return the group as children of the new parent' do
+ expect(new_parent_group.children.count).to eq(1)
+ expect(new_parent_group.children.first).to eq(group)
+ end
+
+ it 'should create a permanent redirect for the group' do
+ expect(group.redirect_routes.permanent.count).to eq(1)
+ end
+ end
+
+ context 'when transferring a group with group descendants' do
+ let!(:subgroup1) { create(:group, :private, parent: group) }
+ let!(:subgroup2) { create(:group, :internal, parent: group) }
+
+ before do
+ create(:group_member, :owner, group: new_parent_group, user: user)
+ transfer_service.execute(new_parent_group)
+ end
+
+ it 'should update subgroups path' do
+ new_parent_path = new_parent_group.path
+ group.children.each do |subgroup|
+ expect(subgroup.full_path).to eq("#{new_parent_path}/#{group.path}/#{subgroup.path}")
+ end
+ end
+
+ it 'should create permanent redirects for the subgroups' do
+ expect(group.redirect_routes.permanent.count).to eq(1)
+ expect(subgroup1.redirect_routes.permanent.count).to eq(1)
+ expect(subgroup2.redirect_routes.permanent.count).to eq(1)
+ end
+
+ context 'when the new parent has a higher visibility than the children' do
+ it 'should not update the children visibility' do
+ expect(subgroup1.private?).to be_truthy
+ expect(subgroup2.internal?).to be_truthy
+ end
+ end
+
+ context 'when the new parent has a lower visibility than the children' do
+ let!(:subgroup1) { create(:group, :public, parent: group) }
+ let!(:subgroup2) { create(:group, :public, parent: group) }
+ let(:new_parent_group) { create(:group, :private) }
+
+ it 'should update children visibility to match the new parent' do
+ group.children.each do |subgroup|
+ expect(subgroup.private?).to be_truthy
+ end
+ end
+ end
+ end
+
+ context 'when transferring a group with project descendants' do
+ let!(:project1) { create(:project, :repository, :private, namespace: group) }
+ let!(:project2) { create(:project, :repository, :internal, namespace: group) }
+
+ before do
+ TestEnv.clean_test_path
+ create(:group_member, :owner, group: new_parent_group, user: user)
+ transfer_service.execute(new_parent_group)
+ end
+
+ it 'should update projects path' do
+ new_parent_path = new_parent_group.path
+ group.projects.each do |project|
+ expect(project.full_path).to eq("#{new_parent_path}/#{group.path}/#{project.name}")
+ end
+ end
+
+ it 'should create permanent redirects for the projects' do
+ expect(group.redirect_routes.permanent.count).to eq(1)
+ expect(project1.redirect_routes.permanent.count).to eq(1)
+ expect(project2.redirect_routes.permanent.count).to eq(1)
+ end
+
+ context 'when the new parent has a higher visibility than the projects' do
+ it 'should not update projects visibility' do
+ expect(project1.private?).to be_truthy
+ expect(project2.internal?).to be_truthy
+ end
+ end
+
+ context 'when the new parent has a lower visibility than the projects' do
+ let!(:project1) { create(:project, :repository, :public, namespace: group) }
+ let!(:project2) { create(:project, :repository, :public, namespace: group) }
+ let(:new_parent_group) { create(:group, :private) }
+
+ it 'should update projects visibility to match the new parent' do
+ group.projects.each do |project|
+ expect(project.private?).to be_truthy
+ end
+ end
+ end
+ end
+
+ context 'when transferring a group with subgroups & projects descendants' do
+ let!(:project1) { create(:project, :repository, :private, namespace: group) }
+ let!(:project2) { create(:project, :repository, :internal, namespace: group) }
+ let!(:subgroup1) { create(:group, :private, parent: group) }
+ let!(:subgroup2) { create(:group, :internal, parent: group) }
+
+ before do
+ TestEnv.clean_test_path
+ create(:group_member, :owner, group: new_parent_group, user: user)
+ transfer_service.execute(new_parent_group)
+ end
+
+ it 'should update subgroups path' do
+ new_parent_path = new_parent_group.path
+ group.children.each do |subgroup|
+ expect(subgroup.full_path).to eq("#{new_parent_path}/#{group.path}/#{subgroup.path}")
+ end
+ end
+
+ it 'should update projects path' do
+ new_parent_path = new_parent_group.path
+ group.projects.each do |project|
+ expect(project.full_path).to eq("#{new_parent_path}/#{group.path}/#{project.name}")
+ end
+ end
+
+ it 'should create permanent redirect for the subgroups and projects' do
+ expect(group.redirect_routes.permanent.count).to eq(1)
+ expect(subgroup1.redirect_routes.permanent.count).to eq(1)
+ expect(subgroup2.redirect_routes.permanent.count).to eq(1)
+ expect(project1.redirect_routes.permanent.count).to eq(1)
+ expect(project2.redirect_routes.permanent.count).to eq(1)
+ end
+ end
+
+ context 'when transfering a group with nested groups and projects' do
+ let!(:group) { create(:group, :public) }
+ let!(:project1) { create(:project, :repository, :private, namespace: group) }
+ let!(:subgroup1) { create(:group, :private, parent: group) }
+ let!(:nested_subgroup) { create(:group, :private, parent: subgroup1) }
+ let!(:nested_project) { create(:project, :repository, :private, namespace: subgroup1) }
+
+ before do
+ TestEnv.clean_test_path
+ create(:group_member, :owner, group: new_parent_group, user: user)
+ transfer_service.execute(new_parent_group)
+ end
+
+ it 'should update subgroups path' do
+ new_base_path = "#{new_parent_group.path}/#{group.path}"
+ group.children.each do |children|
+ expect(children.full_path).to eq("#{new_base_path}/#{children.path}")
+ end
+
+ new_base_path = "#{new_parent_group.path}/#{group.path}/#{subgroup1.path}"
+ subgroup1.children.each do |children|
+ expect(children.full_path).to eq("#{new_base_path}/#{children.path}")
+ end
+ end
+
+ it 'should update projects path' do
+ new_parent_path = "#{new_parent_group.path}/#{group.path}"
+ subgroup1.projects.each do |project|
+ project_full_path = "#{new_parent_path}/#{project.namespace.path}/#{project.name}"
+ expect(project.full_path).to eq(project_full_path)
+ end
+ end
+
+ it 'should create permanent redirect for the subgroups and projects' do
+ expect(group.redirect_routes.permanent.count).to eq(1)
+ expect(project1.redirect_routes.permanent.count).to eq(1)
+ expect(subgroup1.redirect_routes.permanent.count).to eq(1)
+ expect(nested_subgroup.redirect_routes.permanent.count).to eq(1)
+ expect(nested_project.redirect_routes.permanent.count).to eq(1)
+ end
+ end
+
+ context 'when updating the group goes wrong' do
+ let!(:subgroup1) { create(:group, :public, parent: group) }
+ let!(:subgroup2) { create(:group, :public, parent: group) }
+ let(:new_parent_group) { create(:group, :private) }
+ let!(:project1) { create(:project, :repository, :public, namespace: group) }
+
+ before do
+ allow(group).to receive(:save!).and_raise(ActiveRecord::RecordInvalid.new(group))
+ TestEnv.clean_test_path
+ create(:group_member, :owner, group: new_parent_group, user: user)
+ transfer_service.execute(new_parent_group)
+ end
+
+ it 'should restore group and projects visibility' do
+ subgroup1.reload
+ project1.reload
+ expect(subgroup1.public?).to be_truthy
+ expect(project1.public?).to be_truthy
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index 8897a64a138..47c1ebbeb81 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -4,7 +4,7 @@ describe Issues::CloseService do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:guest) { create(:user) }
- let(:issue) { create(:issue, assignees: [user2]) }
+ let(:issue) { create(:issue, assignees: [user2], author: create(:user)) }
let(:project) { issue.project }
let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) }
diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb
index 388c9d63c7b..322c91065e7 100644
--- a/spec/services/issues/move_service_spec.rb
+++ b/spec/services/issues/move_service_spec.rb
@@ -6,7 +6,7 @@ describe Issues::MoveService do
let(:title) { 'Some issue' }
let(:description) { 'Some issue description' }
let(:old_project) { create(:project) }
- let(:new_project) { create(:project) }
+ let(:new_project) { create(:project, group: create(:group)) }
let(:milestone1) { create(:milestone, project_id: old_project.id, title: 'v9.0') }
let(:old_issue) do
@@ -250,7 +250,7 @@ describe Issues::MoveService do
context 'issue description with uploads' do
let(:uploader) { build(:file_uploader, project: old_project) }
- let(:description) { "Text and #{uploader.to_markdown}" }
+ let(:description) { "Text and #{uploader.markdown_link}" }
include_context 'issue move executed'
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 1cb6f2e097f..41237dd7160 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -13,7 +13,8 @@ describe Issues::UpdateService, :mailer do
create(:issue, title: 'Old title',
description: "for #{user2.to_reference}",
assignee_ids: [user3.id],
- project: project)
+ project: project,
+ author: create(:user))
end
before do
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index cb4c3e72aa0..e56d335a7d6 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -172,11 +172,32 @@ describe MergeRequests::BuildService do
end
end
- context 'branch starts with external issue IID followed by a hyphen' do
+ context 'branch starts with numeric characters followed by a hyphen with no issue tracker' do
let(:source_branch) { '12345-fix-issue' }
before do
+ allow(project).to receive(:external_issue_tracker).and_return(false)
+ allow(project).to receive(:issues_enabled?).and_return(false)
+ end
+
+ it 'uses the title of the commit as the title of the merge request' do
+ expect(merge_request.title).to eq(commit_1.safe_message.split("\n").first)
+ end
+
+ it 'uses the description of the commit as the description of the merge request' do
+ commit_description = commit_1.safe_message.split(/\n+/, 2).last
+
+ expect(merge_request.description).to eq("#{commit_description}")
+ end
+ end
+
+ context 'branch starts with JIRA-formatted external issue IID followed by a hyphen' do
+ let(:source_branch) { 'EXMPL-12345-fix-issue' }
+
+ before do
allow(project).to receive(:external_issue_tracker).and_return(true)
+ allow(project).to receive(:issues_enabled?).and_return(false)
+ allow(project).to receive(:external_issue_reference_pattern).and_return(IssueTrackerService.reference_pattern)
end
it 'uses the title of the commit as the title of the merge request' do
@@ -186,7 +207,7 @@ describe MergeRequests::BuildService do
it 'uses the description of the commit as the description of the merge request and appends the closes text' do
commit_description = commit_1.safe_message.split(/\n+/, 2).last
- expect(merge_request.description).to eq("#{commit_description}\n\nCloses #12345")
+ expect(merge_request.description).to eq("#{commit_description}\n\nCloses EXMPL-12345")
end
end
end
@@ -252,19 +273,46 @@ describe MergeRequests::BuildService do
end
end
- context 'branch starts with external issue IID followed by a hyphen' do
+ context 'branch starts with numeric characters followed by a hyphen with no issue tracker' do
let(:source_branch) { '12345-fix-issue' }
before do
- allow(project).to receive(:external_issue_tracker).and_return(true)
+ allow(project).to receive(:external_issue_tracker).and_return(false)
+ allow(project).to receive(:issues_enabled?).and_return(false)
end
it 'sets the title to the humanized branch title' do
expect(merge_request.title).to eq('12345 fix issue')
end
+ end
+
+ context 'branch starts with JIRA-formatted external issue IID' do
+ let(:source_branch) { 'EXMPL-12345' }
+
+ before do
+ allow(project).to receive(:external_issue_tracker).and_return(true)
+ allow(project).to receive(:issues_enabled?).and_return(false)
+ allow(project).to receive(:external_issue_reference_pattern).and_return(IssueTrackerService.reference_pattern)
+ end
+
+ it 'sets the title to the humanized branch title' do
+ expect(merge_request.title).to eq('Resolve EXMPL-12345')
+ end
it 'appends the closes text' do
- expect(merge_request.description).to eq('Closes #12345')
+ expect(merge_request.description).to eq('Closes EXMPL-12345')
+ end
+
+ context 'followed by hyphenated text' do
+ let(:source_branch) { 'EXMPL-12345-fix-issue' }
+
+ it 'sets the title to the humanized branch title' do
+ expect(merge_request.title).to eq('Resolve EXMPL-12345 "Fix issue"')
+ end
+
+ it 'appends the closes text' do
+ expect(merge_request.description).to eq('Closes EXMPL-12345')
+ end
end
end
end
diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb
index 4d12de3ecce..216e0cd4266 100644
--- a/spec/services/merge_requests/close_service_spec.rb
+++ b/spec/services/merge_requests/close_service_spec.rb
@@ -4,7 +4,7 @@ describe MergeRequests::CloseService do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:guest) { create(:user) }
- let(:merge_request) { create(:merge_request, assignee: user2) }
+ let(:merge_request) { create(:merge_request, assignee: user2, author: create(:user)) }
let(:project) { merge_request.project }
let!(:todo) { create(:todo, :assigned, user: user, project: project, target: merge_request, author: user2) }
diff --git a/spec/services/merge_requests/ff_merge_service_spec.rb b/spec/services/merge_requests/ff_merge_service_spec.rb
index aa90feeef89..5ef6365fcc9 100644
--- a/spec/services/merge_requests/ff_merge_service_spec.rb
+++ b/spec/services/merge_requests/ff_merge_service_spec.rb
@@ -7,7 +7,8 @@ describe MergeRequests::FfMergeService do
create(:merge_request,
source_branch: 'flatten-dir',
target_branch: 'improve/awesome',
- assignee: user2)
+ assignee: user2,
+ author: create(:user))
end
let(:project) { merge_request.project }
diff --git a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb
index f17db70faf6..240aa638f79 100644
--- a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb
+++ b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb
@@ -43,7 +43,7 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
it 'creates a system note' do
note = merge_request.notes.last
- expect(note.note).to match /enabled an automatic merge when the pipeline for (\w+\/\w+@)?\h{8}/
+ expect(note.note).to match %r{enabled an automatic merge when the pipeline for (\w+/\w+@)?\h{8}}
end
end
diff --git a/spec/services/merge_requests/rebase_service_spec.rb b/spec/services/merge_requests/rebase_service_spec.rb
index fc1c3d67203..757c31ab692 100644
--- a/spec/services/merge_requests/rebase_service_spec.rb
+++ b/spec/services/merge_requests/rebase_service_spec.rb
@@ -108,7 +108,7 @@ describe MergeRequests::RebaseService do
context 'git commands', :disable_gitaly do
it 'sets GL_REPOSITORY env variable when calling git commands' do
expect(repository).to receive(:popen).exactly(3)
- .with(anything, anything, hash_including('GL_REPOSITORY'))
+ .with(anything, anything, hash_including('GL_REPOSITORY'), anything)
.and_return(['', 0])
service.execute(merge_request)
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 7c3374c6113..903aa0a5078 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -74,6 +74,14 @@ describe MergeRequests::RefreshService do
expect(@fork_build_failed_todo).to be_done
end
+ it 'reloads source branch MRs memoization' do
+ refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
+
+ expect { refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') }.to change {
+ refresh_service.instance_variable_get("@source_merge_requests").first.merge_request_diff
+ }
+ end
+
context 'when source branch ref does not exists' do
before do
DeleteBranchService.new(@project, @user).execute(@merge_request.source_branch)
@@ -392,37 +400,21 @@ describe MergeRequests::RefreshService do
end
it 'references the commit that caused the Work in Progress status' do
- refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
- allow(refresh_service).to receive(:find_new_commits)
- refresh_service.instance_variable_set("@commits", [
- double(
- id: 'aaaaaaa',
- sha: '38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e',
- short_id: 'aaaaaaa',
- title: 'Fix issue',
- work_in_progress?: false
- ),
- double(
- id: 'bbbbbbb',
- sha: '498214de67004b1da3d820901307bed2a68a8ef6',
- short_id: 'bbbbbbb',
- title: 'fixup! Fix issue',
- work_in_progress?: true,
- to_reference: 'bbbbbbb'
- ),
- double(
- id: 'ccccccc',
- sha: '1b12f15a11fc6e62177bef08f47bc7b5ce50b141',
- short_id: 'ccccccc',
- title: 'fixup! Fix issue',
- work_in_progress?: true,
- to_reference: 'ccccccc'
- )
- ])
- refresh_service.execute(@oldrev, @newrev, 'refs/heads/wip')
- reload_mrs
- expect(@merge_request.notes.last.note).to eq(
- "marked as a **Work In Progress** from bbbbbbb"
+ wip_merge_request = create(:merge_request,
+ source_project: @project,
+ source_branch: 'wip',
+ target_branch: 'master',
+ target_project: @project)
+
+ commits = wip_merge_request.commits
+ oldrev = commits.last.id
+ newrev = commits.first.id
+ wip_commit = wip_merge_request.commits.find(&:work_in_progress?)
+
+ refresh_service.execute(oldrev, newrev, 'refs/heads/wip')
+
+ expect(wip_merge_request.reload.notes.last.note).to eq(
+ "marked as a **Work In Progress** from #{wip_commit.id}"
)
end
diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb
index a44d63e5f9f..9ee37c51d95 100644
--- a/spec/services/merge_requests/reopen_service_spec.rb
+++ b/spec/services/merge_requests/reopen_service_spec.rb
@@ -4,7 +4,7 @@ describe MergeRequests::ReopenService do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:guest) { create(:user) }
- let(:merge_request) { create(:merge_request, :closed, assignee: user2) }
+ let(:merge_request) { create(:merge_request, :closed, assignee: user2, author: create(:user)) }
let(:project) { merge_request.project }
before do
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 2238da2d14d..c31259239ee 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -12,7 +12,8 @@ describe MergeRequests::UpdateService, :mailer do
create(:merge_request, :simple, title: 'Old title',
description: "FYI #{user2.to_reference}",
assignee_id: user3.id,
- source_project: project)
+ source_project: project,
+ author: create(:user))
end
before do
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 5c59455e3e1..35eb84e5e88 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -458,7 +458,7 @@ describe NotificationService, :mailer do
context "merge request diff note" do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
- let(:merge_request) { create(:merge_request, source_project: project, assignee: user) }
+ let(:merge_request) { create(:merge_request, source_project: project, assignee: user, author: create(:user)) }
let(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request) }
before do
@@ -469,11 +469,13 @@ describe NotificationService, :mailer do
describe '#new_note' do
it "records sent notifications" do
- # Ensure create SentNotification by noteable = merge_request 6 times, not noteable = note
+ # 3 SentNotification are sent: the MR assignee and author, and the @u_watcher
expect(SentNotification).to receive(:record_note).with(note, any_args).exactly(3).times.and_call_original
notification.new_note(note)
+ expect(SentNotification.last(3).map(&:recipient).map(&:id))
+ .to contain_exactly(merge_request.assignee.id, merge_request.author.id, @u_watcher.id)
expect(SentNotification.last.in_reply_to_discussion_id).to eq(note.discussion_id)
end
end
diff --git a/spec/services/projects/gitlab_projects_import_service_spec.rb b/spec/services/projects/gitlab_projects_import_service_spec.rb
index bb0e274c93e..6b8f9619bc4 100644
--- a/spec/services/projects/gitlab_projects_import_service_spec.rb
+++ b/spec/services/projects/gitlab_projects_import_service_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Projects::GitlabProjectsImportService do
- set(:namespace) { build(:namespace) }
+ set(:namespace) { create(:namespace) }
let(:file) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') }
subject { described_class.new(namespace.owner, { namespace_id: namespace.id, path: path, file: file }) }
diff --git a/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb b/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb
index 50e59954f73..15699574b3a 100644
--- a/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb
+++ b/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb
@@ -6,7 +6,7 @@ describe Projects::HashedStorage::MigrateAttachmentsService do
let(:legacy_storage) { Storage::LegacyProject.new(project) }
let(:hashed_storage) { Storage::HashedProject.new(project) }
- let!(:upload) { Upload.find_by(path: file_uploader.relative_path) }
+ let!(:upload) { Upload.find_by(path: file_uploader.upload_path) }
let(:file_uploader) { build(:file_uploader, project: project) }
let(:old_path) { File.join(base_path(legacy_storage), upload.path) }
let(:new_path) { File.join(base_path(hashed_storage), upload.path) }
@@ -58,6 +58,6 @@ describe Projects::HashedStorage::MigrateAttachmentsService do
end
def base_path(storage)
- FileUploader.dynamic_path_builder(storage.disk_path)
+ File.join(FileUploader.root, storage.disk_path)
end
end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index ab3a257f36f..5b5edc1aa0d 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -54,10 +54,11 @@ describe SystemNoteService do
expect(note_lines[0]).to eq "added #{new_commits.size} commits"
end
- it 'adds a message line for each commit' do
- new_commits.each_with_index do |commit, i|
- # Skip the header
- expect(HTMLEntities.new.decode(note_lines[i + 1])).to eq "* #{commit.short_id} - #{commit.title}"
+ it 'adds a message for each commit' do
+ decoded_note_content = HTMLEntities.new.decode(subject.note)
+
+ new_commits.each do |commit|
+ expect(decoded_note_content).to include("<li>#{commit.short_id} - #{commit.title}</li>")
end
end
end
@@ -69,7 +70,7 @@ describe SystemNoteService do
let(:old_commits) { [noteable.commits.last] }
it 'includes the existing commit' do
- expect(summary_line).to eq "* #{old_commits.first.short_id} - 1 commit from branch `feature`"
+ expect(summary_line).to start_with("<ul><li>#{old_commits.first.short_id} - 1 commit from branch <code>feature</code>")
end
end
@@ -79,22 +80,16 @@ describe SystemNoteService do
context 'with oldrev' do
let(:oldrev) { noteable.commits[2].id }
- it 'includes a commit range' do
- expect(summary_line).to start_with "* #{Commit.truncate_sha(oldrev)}...#{old_commits.last.short_id}"
- end
-
- it 'includes a commit count' do
- expect(summary_line).to end_with " - 26 commits from branch `feature`"
+ it 'includes a commit range and count' do
+ expect(summary_line)
+ .to start_with("<ul><li>#{Commit.truncate_sha(oldrev)}...#{old_commits.last.short_id} - 26 commits from branch <code>feature</code>")
end
end
context 'without oldrev' do
- it 'includes a commit range' do
- expect(summary_line).to start_with "* #{old_commits[0].short_id}..#{old_commits[-1].short_id}"
- end
-
- it 'includes a commit count' do
- expect(summary_line).to end_with " - 26 commits from branch `feature`"
+ it 'includes a commit range and count' do
+ expect(summary_line)
+ .to start_with("<ul><li>#{old_commits[0].short_id}..#{old_commits[-1].short_id} - 26 commits from branch <code>feature</code>")
end
end
@@ -104,7 +99,7 @@ describe SystemNoteService do
end
it 'includes the project namespace' do
- expect(summary_line).to end_with "`#{noteable.target_project_namespace}:feature`"
+ expect(summary_line).to include("<code>#{noteable.target_project_namespace}:feature</code>")
end
end
end
@@ -308,7 +303,7 @@ describe SystemNoteService do
end
it "posts the 'merge when pipeline succeeds' system note" do
- expect(subject.note).to match(/enabled an automatic merge when the pipeline for (\w+\/\w+@)?\h{40} succeeds/)
+ expect(subject.note).to match(%r{enabled an automatic merge when the pipeline for (\w+/\w+@)?\h{40} succeeds})
end
end
@@ -693,9 +688,9 @@ describe SystemNoteService do
describe '.new_commit_summary' do
it 'escapes HTML titles' do
commit = double(title: '<pre>This is a test</pre>', short_id: '12345678')
- escaped = '&lt;pre&gt;This is a test&lt;&#x2F;pre&gt;'
+ escaped = '&lt;pre&gt;This is a test&lt;/pre&gt;'
- expect(described_class.new_commit_summary([commit])).to all(match(%r[- #{escaped}]))
+ expect(described_class.new_commit_summary([commit])).to all(match(/- #{escaped}/))
end
end
diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb
index d12b2757427..ec4ec6f4038 100644
--- a/spec/support/matchers/markdown_matchers.rb
+++ b/spec/support/matchers/markdown_matchers.rb
@@ -190,6 +190,27 @@ module MarkdownMatchers
expect(video['src']).to end_with('/assets/videos/gitlab-demo.mp4')
end
end
+
+ # ColorFilter
+ matcher :parse_colors do
+ set_default_markdown_messages
+
+ match do |actual|
+ color_chips = actual.css('code > span.gfm-color_chip > span')
+
+ expect(color_chips.count).to eq(9)
+
+ [
+ '#F00', '#F00A', '#FF0000', '#FF0000AA', 'RGB(0,255,0)',
+ 'RGB(0%,100%,0%)', 'RGBA(0,255,0,0.7)', 'HSL(540,70%,50%)',
+ 'HSLA(540,70%,50%,0.7)'
+ ].each_with_index do |color, i|
+ parsed_color = Banzai::ColorParser.parse(color)
+ expect(color_chips[i]['style']).to match("background-color: #{parsed_color};")
+ expect(color_chips[i].parent.parent.content).to match(color)
+ end
+ end
+ end
end
# Monkeypatch the matcher DSL so that we can reduce some noisy duplication for
diff --git a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb
index 935c08221e0..7ce80c82439 100644
--- a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb
@@ -2,6 +2,8 @@ shared_examples 'handle uploads' do
let(:user) { create(:user) }
let(:jpg) { fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') }
let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') }
+ let(:secret) { FileUploader.generate_secret }
+ let(:uploader_class) { FileUploader }
describe "POST #create" do
context 'when a user is not authorized to upload a file' do
@@ -65,7 +67,12 @@ shared_examples 'handle uploads' do
describe "GET #show" do
let(:show_upload) do
- get :show, params.merge(secret: "123456", filename: "image.jpg")
+ get :show, params.merge(secret: secret, filename: "rails_sample.jpg")
+ end
+
+ before do
+ expect(FileUploader).to receive(:generate_secret).and_return(secret)
+ UploadService.new(model, jpg, uploader_class).execute
end
context "when the model is public" do
@@ -75,11 +82,6 @@ shared_examples 'handle uploads' do
context "when not signed in" do
context "when the file exists" do
- before do
- allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg)
- allow(jpg).to receive(:exists?).and_return(true)
- end
-
it "responds with status 200" do
show_upload
@@ -88,6 +90,10 @@ shared_examples 'handle uploads' do
end
context "when the file doesn't exist" do
+ before do
+ allow_any_instance_of(FileUploader).to receive(:exists?).and_return(false)
+ end
+
it "responds with status 404" do
show_upload
@@ -102,11 +108,6 @@ shared_examples 'handle uploads' do
end
context "when the file exists" do
- before do
- allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg)
- allow(jpg).to receive(:exists?).and_return(true)
- end
-
it "responds with status 200" do
show_upload
@@ -115,6 +116,10 @@ shared_examples 'handle uploads' do
end
context "when the file doesn't exist" do
+ before do
+ allow_any_instance_of(FileUploader).to receive(:exists?).and_return(false)
+ end
+
it "responds with status 404" do
show_upload
@@ -131,11 +136,6 @@ shared_examples 'handle uploads' do
context "when not signed in" do
context "when the file exists" do
- before do
- allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg)
- allow(jpg).to receive(:exists?).and_return(true)
- end
-
context "when the file is an image" do
before do
allow_any_instance_of(FileUploader).to receive(:image?).and_return(true)
@@ -149,6 +149,10 @@ shared_examples 'handle uploads' do
end
context "when the file is not an image" do
+ before do
+ allow_any_instance_of(FileUploader).to receive(:image?).and_return(false)
+ end
+
it "redirects to the sign in page" do
show_upload
@@ -158,6 +162,10 @@ shared_examples 'handle uploads' do
end
context "when the file doesn't exist" do
+ before do
+ allow_any_instance_of(FileUploader).to receive(:exists?).and_return(false)
+ end
+
it "redirects to the sign in page" do
show_upload
@@ -177,11 +185,6 @@ shared_examples 'handle uploads' do
end
context "when the file exists" do
- before do
- allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg)
- allow(jpg).to receive(:exists?).and_return(true)
- end
-
it "responds with status 200" do
show_upload
@@ -190,6 +193,10 @@ shared_examples 'handle uploads' do
end
context "when the file doesn't exist" do
+ before do
+ allow_any_instance_of(FileUploader).to receive(:exists?).and_return(false)
+ end
+
it "responds with status 404" do
show_upload
@@ -200,11 +207,6 @@ shared_examples 'handle uploads' do
context "when the user doesn't have access to the model" do
context "when the file exists" do
- before do
- allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg)
- allow(jpg).to receive(:exists?).and_return(true)
- end
-
context "when the file is an image" do
before do
allow_any_instance_of(FileUploader).to receive(:image?).and_return(true)
@@ -218,6 +220,10 @@ shared_examples 'handle uploads' do
end
context "when the file is not an image" do
+ before do
+ allow_any_instance_of(FileUploader).to receive(:image?).and_return(false)
+ end
+
it "responds with status 404" do
show_upload
@@ -227,6 +233,10 @@ shared_examples 'handle uploads' do
end
context "when the file doesn't exist" do
+ before do
+ allow_any_instance_of(FileUploader).to receive(:exists?).and_return(false)
+ end
+
it "responds with status 404" do
show_upload
diff --git a/spec/support/shared_examples/uploaders/gitlab_uploader_shared_examples.rb b/spec/support/shared_examples/uploaders/gitlab_uploader_shared_examples.rb
new file mode 100644
index 00000000000..934d53e7bba
--- /dev/null
+++ b/spec/support/shared_examples/uploaders/gitlab_uploader_shared_examples.rb
@@ -0,0 +1,48 @@
+shared_examples "matches the method pattern" do |method|
+ let(:target) { subject }
+ let(:args) { nil }
+ let(:pattern) { patterns[method] }
+
+ it do
+ return skip "No pattern provided, skipping." unless pattern
+
+ expect(target.method(method).call(*args)).to match(pattern)
+ end
+end
+
+shared_examples "builds correct paths" do |**patterns|
+ let(:patterns) { patterns }
+
+ before do
+ allow(subject).to receive(:filename).and_return('<filename>')
+ end
+
+ describe "#store_dir" do
+ it_behaves_like "matches the method pattern", :store_dir
+ end
+
+ describe "#cache_dir" do
+ it_behaves_like "matches the method pattern", :cache_dir
+ end
+
+ describe "#work_dir" do
+ it_behaves_like "matches the method pattern", :work_dir
+ end
+
+ describe "#upload_path" do
+ it_behaves_like "matches the method pattern", :upload_path
+ end
+
+ describe ".absolute_path" do
+ it_behaves_like "matches the method pattern", :absolute_path do
+ let(:target) { subject.class }
+ let(:args) { [upload] }
+ end
+ end
+
+ describe ".base_dir" do
+ it_behaves_like "matches the method pattern", :base_dir do
+ let(:target) { subject.class }
+ end
+ end
+end
diff --git a/spec/support/stored_repositories.rb b/spec/support/stored_repositories.rb
index f9121cce985..52e47ae2d34 100644
--- a/spec/support/stored_repositories.rb
+++ b/spec/support/stored_repositories.rb
@@ -15,9 +15,7 @@ RSpec.configure do |config|
# Track the maximum number of failures
first_failure = Time.parse("2017-11-14 17:52:30")
last_failure = Time.parse("2017-11-14 18:54:37")
- failure_count = Gitlab::CurrentSettings
- .current_application_settings
- .circuitbreaker_failure_count_threshold + 1
+ failure_count = Gitlab::CurrentSettings.circuitbreaker_failure_count_threshold + 1
cache_key = "#{Gitlab::Git::Storage::REDIS_KEY_PREFIX}broken:#{Gitlab::Environment.hostname}"
Gitlab::Git::Storage.redis.with do |redis|
diff --git a/spec/support/stub_env.rb b/spec/support/stub_env.rb
index 695152e2d4e..36b90fc68d6 100644
--- a/spec/support/stub_env.rb
+++ b/spec/support/stub_env.rb
@@ -1,7 +1,5 @@
# Inspired by https://github.com/ljkbennett/stub_env/blob/master/lib/stub_env/helpers.rb
module StubENV
- include Gitlab::CurrentSettings
-
def stub_env(key_or_hash, value = nil)
init_stub unless env_stubbed?
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index 9e5f08fbc51..c275522159c 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -237,7 +237,7 @@ module TestEnv
end
def artifacts_path
- Gitlab.config.artifacts.path
+ Gitlab.config.artifacts.storage_path
end
# When no cached assets exist, manually hit the root path to create them
diff --git a/spec/support/track_untracked_uploads_helpers.rb b/spec/support/track_untracked_uploads_helpers.rb
index d05eda08201..5752078d2a0 100644
--- a/spec/support/track_untracked_uploads_helpers.rb
+++ b/spec/support/track_untracked_uploads_helpers.rb
@@ -1,6 +1,6 @@
module TrackUntrackedUploadsHelpers
def uploaded_file
- fixture_path = Rails.root.join('spec', 'fixtures', 'rails_sample.jpg')
+ fixture_path = Rails.root.join('spec/fixtures/rails_sample.jpg')
fixture_file_upload(fixture_path)
end
diff --git a/spec/support/unique_ip_check_shared_examples.rb b/spec/support/unique_ip_check_shared_examples.rb
index 3d9705c9c05..e5c8ac6a004 100644
--- a/spec/support/unique_ip_check_shared_examples.rb
+++ b/spec/support/unique_ip_check_shared_examples.rb
@@ -9,7 +9,7 @@ shared_context 'unique ips sign in limit' do
before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
- current_application_settings.update!(
+ Gitlab::CurrentSettings.update!(
unique_ips_limit_enabled: true,
unique_ips_limit_time_window: 10000
)
@@ -34,7 +34,7 @@ end
shared_examples 'user login operation with unique ip limit' do
include_context 'unique ips sign in limit' do
before do
- current_application_settings.update!(unique_ips_limit_per_user: 1)
+ Gitlab::CurrentSettings.update!(unique_ips_limit_per_user: 1)
end
it 'allows user authenticating from the same ip' do
@@ -52,7 +52,7 @@ end
shared_examples 'user login request with unique ip limit' do |success_status = 200|
include_context 'unique ips sign in limit' do
before do
- current_application_settings.update!(unique_ips_limit_per_user: 1)
+ Gitlab::CurrentSettings.update!(unique_ips_limit_per_user: 1)
end
it 'allows user authenticating from the same ip' do
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index b41c3b3958a..168facd51a6 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -165,7 +165,7 @@ describe 'gitlab:app namespace rake task' do
expect(tar_contents).to match('pages.tar.gz')
expect(tar_contents).to match('lfs.tar.gz')
expect(tar_contents).to match('registry.tar.gz')
- expect(tar_contents).not_to match(/^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz|pages.tar.gz|artifacts.tar.gz|registry.tar.gz)\/$/)
+ expect(tar_contents).not_to match(%r{^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz|pages.tar.gz|artifacts.tar.gz|registry.tar.gz)/$})
end
it 'deletes temp directories' do
diff --git a/spec/tasks/gitlab/git_rake_spec.rb b/spec/tasks/gitlab/git_rake_spec.rb
index dacc5dc5ae7..9aebf7b0b4a 100644
--- a/spec/tasks/gitlab/git_rake_spec.rb
+++ b/spec/tasks/gitlab/git_rake_spec.rb
@@ -19,7 +19,7 @@ describe 'gitlab:git rake tasks' do
describe 'fsck' do
it 'outputs the integrity check for a repo' do
- expect { run_rake_task('gitlab:git:fsck') }.to output(/Performed Checking integrity at .*@hashed\/1\/2\/test.git/).to_stdout
+ expect { run_rake_task('gitlab:git:fsck') }.to output(%r{Performed Checking integrity at .*@hashed/1/2/test.git}).to_stdout
end
it 'errors out about config.lock issues' do
diff --git a/spec/uploaders/attachment_uploader_spec.rb b/spec/uploaders/attachment_uploader_spec.rb
index 04ee6e9bfad..091ba824fc6 100644
--- a/spec/uploaders/attachment_uploader_spec.rb
+++ b/spec/uploaders/attachment_uploader_spec.rb
@@ -1,28 +1,14 @@
require 'spec_helper'
describe AttachmentUploader do
- let(:uploader) { described_class.new(build_stubbed(:user)) }
+ let(:note) { create(:note, :with_attachment) }
+ let(:uploader) { note.attachment }
+ let(:upload) { create(:upload, :attachment_upload, model: uploader.model) }
- describe "#store_dir" do
- it "stores in the system dir" do
- expect(uploader.store_dir).to start_with("uploads/-/system/user")
- end
+ subject { uploader }
- it "uses the old path when using object storage" do
- expect(described_class).to receive(:file_storage?).and_return(false)
- expect(uploader.store_dir).to start_with("uploads/user")
- end
- end
-
- describe '#move_to_cache' do
- it 'is true' do
- expect(uploader.move_to_cache).to eq(true)
- end
- end
-
- describe '#move_to_store' do
- it 'is true' do
- expect(uploader.move_to_store).to eq(true)
- end
- end
+ it_behaves_like 'builds correct paths',
+ store_dir: %r[uploads/-/system/note/attachment/],
+ upload_path: %r[uploads/-/system/note/attachment/],
+ absolute_path: %r[#{CarrierWave.root}/uploads/-/system/note/attachment/]
end
diff --git a/spec/uploaders/avatar_uploader_spec.rb b/spec/uploaders/avatar_uploader_spec.rb
index 1dc574699d8..bf9028c9260 100644
--- a/spec/uploaders/avatar_uploader_spec.rb
+++ b/spec/uploaders/avatar_uploader_spec.rb
@@ -1,18 +1,16 @@
require 'spec_helper'
describe AvatarUploader do
- let(:uploader) { described_class.new(build_stubbed(:user)) }
+ let(:model) { create(:user, :with_avatar) }
+ let(:uploader) { described_class.new(model, :avatar) }
+ let(:upload) { create(:upload, model: model) }
- describe "#store_dir" do
- it "stores in the system dir" do
- expect(uploader.store_dir).to start_with("uploads/-/system/user")
- end
+ subject { uploader }
- it "uses the old path when using object storage" do
- expect(described_class).to receive(:file_storage?).and_return(false)
- expect(uploader.store_dir).to start_with("uploads/user")
- end
- end
+ it_behaves_like 'builds correct paths',
+ store_dir: %r[uploads/-/system/user/avatar/],
+ upload_path: %r[uploads/-/system/user/avatar/],
+ absolute_path: %r[#{CarrierWave.root}/uploads/-/system/user/avatar/]
describe '#move_to_cache' do
it 'is false' do
diff --git a/spec/uploaders/file_mover_spec.rb b/spec/uploaders/file_mover_spec.rb
index 0cf462e9553..bc024cd307c 100644
--- a/spec/uploaders/file_mover_spec.rb
+++ b/spec/uploaders/file_mover_spec.rb
@@ -3,13 +3,13 @@ require 'spec_helper'
describe FileMover do
let(:filename) { 'banana_sample.gif' }
let(:file) { fixture_file_upload(Rails.root.join('spec', 'fixtures', filename)) }
+ let(:temp_file_path) { File.join('uploads/-/system/temp', 'secret55', filename) }
+
let(:temp_description) do
- 'test ![banana_sample](/uploads/-/system/temp/secret55/banana_sample.gif) same ![banana_sample]'\
- '(/uploads/-/system/temp/secret55/banana_sample.gif)'
+ "test ![banana_sample](/#{temp_file_path}) "\
+ "same ![banana_sample](/#{temp_file_path}) "
end
- let(:temp_file_path) { File.join('secret55', filename).to_s }
- let(:file_path) { File.join('uploads', '-', 'system', 'personal_snippet', snippet.id.to_s, 'secret55', filename).to_s }
-
+ let(:file_path) { File.join('uploads/-/system/personal_snippet', snippet.id.to_s, 'secret55', filename) }
let(:snippet) { create(:personal_snippet, description: temp_description) }
subject { described_class.new(file_path, snippet).execute }
@@ -28,8 +28,8 @@ describe FileMover do
expect(snippet.reload.description)
.to eq(
- "test ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif)"\
- " same ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif)"
+ "test ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif) "\
+ "same ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif) "
)
end
@@ -50,8 +50,8 @@ describe FileMover do
expect(snippet.reload.description)
.to eq(
- "test ![banana_sample](/uploads/-/system/temp/secret55/banana_sample.gif)"\
- " same ![banana_sample](/uploads/-/system/temp/secret55/banana_sample.gif)"
+ "test ![banana_sample](/uploads/-/system/temp/secret55/banana_sample.gif) "\
+ "same ![banana_sample](/uploads/-/system/temp/secret55/banana_sample.gif) "
)
end
diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb
index fd195d6f9b8..6a92e7fae51 100644
--- a/spec/uploaders/file_uploader_spec.rb
+++ b/spec/uploaders/file_uploader_spec.rb
@@ -1,118 +1,107 @@
require 'spec_helper'
describe FileUploader do
- let(:uploader) { described_class.new(build_stubbed(:project)) }
+ let(:group) { create(:group, name: 'awesome') }
+ let(:project) { create(:project, namespace: group, name: 'project') }
+ let(:uploader) { described_class.new(project) }
+ let(:upload) { double(model: project, path: 'secret/foo.jpg') }
- context 'legacy storage' do
- let(:project) { build_stubbed(:project) }
-
- describe '.absolute_path' do
- it 'returns the correct absolute path by building it dynamically' do
- upload = double(model: project, path: 'secret/foo.jpg')
+ subject { uploader }
- dynamic_segment = project.full_path
+ shared_examples 'builds correct legacy storage paths' do
+ include_examples 'builds correct paths',
+ store_dir: %r{awesome/project/\h+},
+ absolute_path: %r{#{described_class.root}/awesome/project/secret/foo.jpg}
+ end
- expect(described_class.absolute_path(upload))
- .to end_with("#{dynamic_segment}/secret/foo.jpg")
+ shared_examples 'uses hashed storage' do
+ context 'when rolled out attachments' do
+ before do
+ allow(project).to receive(:disk_path).and_return('ca/fe/fe/ed')
end
+
+ let(:project) { build_stubbed(:project, :hashed, namespace: group, name: 'project') }
+
+ it_behaves_like 'builds correct paths',
+ store_dir: %r{ca/fe/fe/ed/\h+},
+ absolute_path: %r{#{described_class.root}/ca/fe/fe/ed/secret/foo.jpg}
end
- describe "#store_dir" do
- it "stores in the namespace path" do
- uploader = described_class.new(project)
+ context 'when only repositories are rolled out' do
+ let(:project) { build_stubbed(:project, namespace: group, name: 'project', storage_version: Project::HASHED_STORAGE_FEATURES[:repository]) }
- expect(uploader.store_dir).to include(project.full_path)
- expect(uploader.store_dir).not_to include("system")
- end
+ it_behaves_like 'builds correct legacy storage paths'
end
end
- context 'hashed storage' do
- context 'when rolled out attachments' do
- let(:project) { build_stubbed(:project, :hashed) }
+ context 'legacy storage' do
+ it_behaves_like 'builds correct legacy storage paths'
+ include_examples 'uses hashed storage'
+ end
- describe '.absolute_path' do
- it 'returns the correct absolute path by building it dynamically' do
- upload = double(model: project, path: 'secret/foo.jpg')
+ describe 'initialize' do
+ let(:uploader) { described_class.new(double, secret: 'secret') }
- dynamic_segment = project.disk_path
+ it 'accepts a secret parameter' do
+ expect(described_class).not_to receive(:generate_secret)
+ expect(uploader.secret).to eq('secret')
+ end
+ end
- expect(described_class.absolute_path(upload))
- .to end_with("#{dynamic_segment}/secret/foo.jpg")
- end
+ describe 'callbacks' do
+ describe '#prune_store_dir after :remove' do
+ before do
+ uploader.store!(fixture_file_upload('spec/fixtures/doc_sample.txt'))
end
- describe "#store_dir" do
- it "stores in the namespace path" do
- uploader = described_class.new(project)
-
- expect(uploader.store_dir).to include(project.disk_path)
- expect(uploader.store_dir).not_to include("system")
- end
+ def store_dir
+ File.expand_path(uploader.store_dir, uploader.root)
end
- end
-
- context 'when only repositories are rolled out' do
- let(:project) { build_stubbed(:project, storage_version: Project::HASHED_STORAGE_FEATURES[:repository]) }
-
- 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.full_path
+ it 'is called' do
+ expect(uploader).to receive(:prune_store_dir).once
- expect(described_class.absolute_path(upload))
- .to end_with("#{dynamic_segment}/secret/foo.jpg")
- end
+ uploader.remove!
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
+ it 'prune the store directory' do
+ expect { uploader.remove! }
+ .to change { File.exist?(store_dir) }.from(true).to(false)
end
end
end
- describe 'initialize' do
+ describe '#secret' do
it 'generates a secret if none is provided' do
- expect(SecureRandom).to receive(:hex).and_return('secret')
-
- uploader = described_class.new(double)
-
- expect(uploader.secret).to eq 'secret'
+ expect(described_class).to receive(:generate_secret).and_return('secret')
+ expect(uploader.secret).to eq('secret')
end
+ end
- it 'accepts a secret parameter' do
- expect(SecureRandom).not_to receive(:hex)
+ describe '#upload=' do
+ let(:secret) { SecureRandom.hex }
+ let(:upload) { create(:upload, :issuable_upload, secret: secret, filename: 'file.txt') }
- uploader = described_class.new(double, 'secret')
+ it 'handles nil' do
+ expect(uploader).not_to receive(:apply_context!)
- expect(uploader.secret).to eq 'secret'
+ uploader.upload = nil
end
- end
- describe '#move_to_cache' do
- it 'is true' do
- expect(uploader.move_to_cache).to eq(true)
- end
- end
+ it 'extract the uploader context from it' do
+ expect(uploader).to receive(:apply_context!).with(a_hash_including(secret: secret, identifier: 'file.txt'))
- describe '#move_to_store' do
- it 'is true' do
- expect(uploader.move_to_store).to eq(true)
+ uploader.upload = upload
end
- end
- describe '#relative_path' do
- it 'removes the leading dynamic path segment' do
- fixture = Rails.root.join('spec', 'fixtures', 'rails_sample.jpg')
- uploader.store!(fixture_file_upload(fixture))
+ context 'uploader_context is empty' do
+ it 'fallbacks to regex based extraction' do
+ expect(upload).to receive(:uploader_context).and_return({})
- expect(uploader.relative_path).to match(/\A\h{32}\/rails_sample.jpg\z/)
+ uploader.upload = upload
+ expect(uploader.secret).to eq(secret)
+ expect(uploader.instance_variable_get(:@identifier)).to eq('file.txt')
+ end
end
end
end
diff --git a/spec/uploaders/gitlab_uploader_spec.rb b/spec/uploaders/gitlab_uploader_spec.rb
index a144b39f74f..60e35dcf235 100644
--- a/spec/uploaders/gitlab_uploader_spec.rb
+++ b/spec/uploaders/gitlab_uploader_spec.rb
@@ -4,7 +4,7 @@ require 'carrierwave/storage/fog'
describe GitlabUploader do
let(:uploader_class) { Class.new(described_class) }
- subject { uploader_class.new }
+ subject { uploader_class.new(double) }
describe '#file_storage?' do
context 'when file storage is used' do
diff --git a/spec/uploaders/job_artifact_uploader_spec.rb b/spec/uploaders/job_artifact_uploader_spec.rb
index 98a4373e9d0..5612ec7e661 100644
--- a/spec/uploaders/job_artifact_uploader_spec.rb
+++ b/spec/uploaders/job_artifact_uploader_spec.rb
@@ -3,34 +3,41 @@ require 'spec_helper'
describe JobArtifactUploader do
let(:job_artifact) { create(:ci_job_artifact) }
let(:uploader) { described_class.new(job_artifact, :file) }
- let(:local_path) { Gitlab.config.artifacts.path }
- describe '#store_dir' do
- subject { uploader.store_dir }
-
- let(:path) { "#{job_artifact.created_at.utc.strftime('%Y_%m_%d')}/#{job_artifact.job_id}/#{job_artifact.id}" }
-
- context 'when using local storage' do
- it { is_expected.to start_with(local_path) }
- it { is_expected.to match(/\h{2}\/\h{2}\/\h{64}\/\d{4}_\d{1,2}_\d{1,2}\/\d+\/\d+\z/) }
- it { is_expected.to end_with(path) }
+ subject { uploader }
+
+ it_behaves_like "builds correct paths",
+ store_dir: %r[\h{2}/\h{2}/\h{64}/\d{4}_\d{1,2}_\d{1,2}/\d+/\d+\z],
+ cache_dir: %r[artifacts/tmp/cache],
+ work_dir: %r[artifacts/tmp/work]
+
+ describe '#open' do
+ subject { uploader.open }
+
+ context 'when trace is stored in File storage' do
+ context 'when file exists' do
+ let(:file) do
+ fixture_file_upload(
+ Rails.root.join('spec/fixtures/trace/sample_trace'), 'text/plain')
+ end
+
+ before do
+ uploader.store!(file)
+ end
+
+ it 'returns io stream' do
+ is_expected.to be_a(IO)
+ end
+ end
+
+ context 'when file does not exist' do
+ it 'returns nil' do
+ is_expected.to be_nil
+ end
+ end
end
end
- describe '#cache_dir' do
- subject { uploader.cache_dir }
-
- it { is_expected.to start_with(local_path) }
- it { is_expected.to end_with('/tmp/cache') }
- end
-
- describe '#work_dir' do
- subject { uploader.work_dir }
-
- it { is_expected.to start_with(local_path) }
- it { is_expected.to end_with('/tmp/work') }
- end
-
context 'file is stored in valid local_path' do
let(:file) do
fixture_file_upload(
@@ -43,7 +50,7 @@ describe JobArtifactUploader do
subject { uploader.file.path }
- it { is_expected.to start_with(local_path) }
+ it { is_expected.to start_with("#{uploader.root}/#{uploader.class.base_dir}") }
it { is_expected.to include("/#{job_artifact.created_at.utc.strftime('%Y_%m_%d')}/") }
it { is_expected.to include("/#{job_artifact.job_id}/#{job_artifact.id}/") }
it { is_expected.to end_with("ci_build_artifacts.zip") }
diff --git a/spec/uploaders/legacy_artifact_uploader_spec.rb b/spec/uploaders/legacy_artifact_uploader_spec.rb
index efeffb78772..54c6a8b869b 100644
--- a/spec/uploaders/legacy_artifact_uploader_spec.rb
+++ b/spec/uploaders/legacy_artifact_uploader_spec.rb
@@ -3,49 +3,22 @@ require 'rails_helper'
describe LegacyArtifactUploader do
let(:job) { create(:ci_build) }
let(:uploader) { described_class.new(job, :legacy_artifacts_file) }
- let(:local_path) { Gitlab.config.artifacts.path }
+ let(:local_path) { described_class.root }
- describe '.local_store_path' do
- subject { described_class.local_store_path }
+ subject { uploader }
- it "delegate to artifacts path" do
- expect(Gitlab.config.artifacts).to receive(:path)
-
- subject
- end
- end
-
- describe '.artifacts_upload_path' do
- subject { described_class.artifacts_upload_path }
+ # TODO: move to Workhorse::UploadPath
+ describe '.workhorse_upload_path' do
+ subject { described_class.workhorse_upload_path }
it { is_expected.to start_with(local_path) }
- it { is_expected.to end_with('tmp/uploads/') }
- end
-
- describe '#store_dir' do
- subject { uploader.store_dir }
-
- let(:path) { "#{job.created_at.utc.strftime('%Y_%m')}/#{job.project_id}/#{job.id}" }
-
- context 'when using local storage' do
- it { is_expected.to start_with(local_path) }
- it { is_expected.to end_with(path) }
- end
+ it { is_expected.to end_with('tmp/uploads') }
end
- describe '#cache_dir' do
- subject { uploader.cache_dir }
-
- it { is_expected.to start_with(local_path) }
- it { is_expected.to end_with('/tmp/cache') }
- end
-
- describe '#work_dir' do
- subject { uploader.work_dir }
-
- it { is_expected.to start_with(local_path) }
- it { is_expected.to end_with('/tmp/work') }
- end
+ it_behaves_like "builds correct paths",
+ store_dir: %r[\d{4}_\d{1,2}/\d+/\d+\z],
+ cache_dir: %r[artifacts/tmp/cache],
+ work_dir: %r[artifacts/tmp/work]
describe '#filename' do
# we need to use uploader, as this makes to use mounter
@@ -69,7 +42,7 @@ describe LegacyArtifactUploader do
subject { uploader.file.path }
- it { is_expected.to start_with(local_path) }
+ it { is_expected.to start_with("#{uploader.root}") }
it { is_expected.to include("/#{job.created_at.utc.strftime('%Y_%m')}/") }
it { is_expected.to include("/#{job.project_id}/") }
it { is_expected.to end_with("ci_build_artifacts.zip") }
diff --git a/spec/uploaders/lfs_object_uploader_spec.rb b/spec/uploaders/lfs_object_uploader_spec.rb
index 7088bc23334..6ebc885daa8 100644
--- a/spec/uploaders/lfs_object_uploader_spec.rb
+++ b/spec/uploaders/lfs_object_uploader_spec.rb
@@ -2,39 +2,13 @@ require 'spec_helper'
describe LfsObjectUploader do
let(:lfs_object) { create(:lfs_object, :with_file) }
- let(:uploader) { described_class.new(lfs_object) }
+ let(:uploader) { described_class.new(lfs_object, :file) }
let(:path) { Gitlab.config.lfs.storage_path }
- describe '#move_to_cache' do
- it 'is true' do
- expect(uploader.move_to_cache).to eq(true)
- end
- end
+ subject { uploader }
- describe '#move_to_store' do
- it 'is true' do
- expect(uploader.move_to_store).to eq(true)
- end
- end
-
- describe '#store_dir' do
- subject { uploader.store_dir }
-
- it { is_expected.to start_with(path) }
- it { is_expected.to end_with("#{lfs_object.oid[0, 2]}/#{lfs_object.oid[2, 2]}") }
- end
-
- describe '#cache_dir' do
- subject { uploader.cache_dir }
-
- it { is_expected.to start_with(path) }
- it { is_expected.to end_with('/tmp/cache') }
- end
-
- describe '#work_dir' do
- subject { uploader.work_dir }
-
- it { is_expected.to start_with(path) }
- it { is_expected.to end_with('/tmp/work') }
- end
+ it_behaves_like "builds correct paths",
+ store_dir: %r[\h{2}/\h{2}],
+ cache_dir: %r[/lfs-objects/tmp/cache],
+ work_dir: %r[/lfs-objects/tmp/work]
end
diff --git a/spec/uploaders/namespace_file_uploader_spec.rb b/spec/uploaders/namespace_file_uploader_spec.rb
index c6c4500c179..24a2fc0f72e 100644
--- a/spec/uploaders/namespace_file_uploader_spec.rb
+++ b/spec/uploaders/namespace_file_uploader_spec.rb
@@ -1,21 +1,16 @@
require 'spec_helper'
+IDENTIFIER = %r{\h+/\S+}
+
describe NamespaceFileUploader do
let(:group) { build_stubbed(:group) }
let(:uploader) { described_class.new(group) }
+ let(:upload) { create(:upload, :namespace_upload, model: group) }
- describe "#store_dir" do
- it "stores in the namespace id directory" do
- expect(uploader.store_dir).to include(group.id.to_s)
- end
- end
-
- describe ".absolute_path" do
- it "stores in thecorrect directory" do
- upload_record = create(:upload, :namespace_upload, model: group)
+ subject { uploader }
- expect(described_class.absolute_path(upload_record))
- .to include("-/system/namespace/#{group.id}")
- end
- end
+ it_behaves_like 'builds correct paths',
+ store_dir: %r[uploads/-/system/namespace/\d+],
+ upload_path: IDENTIFIER,
+ absolute_path: %r[#{CarrierWave.root}/uploads/-/system/namespace/\d+/#{IDENTIFIER}]
end
diff --git a/spec/uploaders/personal_file_uploader_spec.rb b/spec/uploaders/personal_file_uploader_spec.rb
index cbafa9f478d..ed1fba6edda 100644
--- a/spec/uploaders/personal_file_uploader_spec.rb
+++ b/spec/uploaders/personal_file_uploader_spec.rb
@@ -1,25 +1,27 @@
require 'spec_helper'
+IDENTIFIER = %r{\h+/\S+}
+
describe PersonalFileUploader do
- let(:uploader) { described_class.new(build_stubbed(:project)) }
- let(:snippet) { create(:personal_snippet) }
+ let(:model) { create(:personal_snippet) }
+ let(:uploader) { described_class.new(model) }
+ let(:upload) { create(:upload, :personal_snippet_upload) }
- describe '.absolute_path' do
- it 'returns the correct absolute path by building it dynamically' do
- upload = double(model: snippet, path: 'secret/foo.jpg')
+ subject { uploader }
- dynamic_segment = "personal_snippet/#{snippet.id}"
-
- expect(described_class.absolute_path(upload)).to end_with("/-/system/#{dynamic_segment}/secret/foo.jpg")
- end
- end
+ it_behaves_like 'builds correct paths',
+ store_dir: %r[uploads/-/system/personal_snippet/\d+],
+ upload_path: IDENTIFIER,
+ absolute_path: %r[#{CarrierWave.root}/uploads/-/system/personal_snippet/\d+/#{IDENTIFIER}]
describe '#to_h' do
- it 'returns the hass' do
- uploader = described_class.new(snippet, 'secret')
+ before do
+ subject.instance_variable_set(:@secret, 'secret')
+ end
+ it 'is correct' do
allow(uploader).to receive(:file).and_return(double(extension: 'txt', filename: 'file_name'))
- expected_url = "/uploads/-/system/personal_snippet/#{snippet.id}/secret/file_name"
+ expected_url = "/uploads/-/system/personal_snippet/#{model.id}/secret/file_name"
expect(uploader.to_h).to eq(
alt: 'file_name',
diff --git a/spec/uploaders/records_uploads_spec.rb b/spec/uploaders/records_uploads_spec.rb
index 7ef7fb7d758..9a3e5d83e01 100644
--- a/spec/uploaders/records_uploads_spec.rb
+++ b/spec/uploaders/records_uploads_spec.rb
@@ -3,16 +3,16 @@ require 'rails_helper'
describe RecordsUploads do
let!(:uploader) do
class RecordsUploadsExampleUploader < GitlabUploader
- include RecordsUploads
+ include RecordsUploads::Concern
storage :file
- def model
- FactoryBot.build_stubbed(:user)
+ def dynamic_segment
+ 'co/fe/ee'
end
end
- RecordsUploadsExampleUploader.new
+ RecordsUploadsExampleUploader.new(build_stubbed(:user))
end
def upload_fixture(filename)
@@ -20,48 +20,55 @@ describe RecordsUploads do
end
describe 'callbacks' do
- it 'calls `record_upload` after `store`' do
+ let(:upload) { create(:upload) }
+
+ before do
+ uploader.upload = upload
+ end
+
+ it '#record_upload after `store`' do
expect(uploader).to receive(:record_upload).once
uploader.store!(upload_fixture('doc_sample.txt'))
end
- it 'calls `destroy_upload` after `remove`' do
- expect(uploader).to receive(:destroy_upload).once
-
+ it '#destroy_upload after `remove`' do
uploader.store!(upload_fixture('doc_sample.txt'))
+ expect(uploader).to receive(:destroy_upload).once
uploader.remove!
end
end
describe '#record_upload callback' do
- it 'returns early when not using file storage' do
- allow(uploader).to receive(:file_storage?).and_return(false)
- expect(Upload).not_to receive(:record)
-
- uploader.store!(upload_fixture('rails_sample.jpg'))
+ it 'creates an Upload record after store' do
+ expect { uploader.store!(upload_fixture('rails_sample.jpg')) }.to change { Upload.count }.by(1)
end
- it "returns early when the file doesn't exist" do
- allow(uploader).to receive(:file).and_return(double(exists?: false))
- expect(Upload).not_to receive(:record)
-
+ it 'creates a new record and assigns size, path, model, and uploader' do
uploader.store!(upload_fixture('rails_sample.jpg'))
+
+ upload = uploader.upload
+ aggregate_failures do
+ expect(upload).to be_persisted
+ expect(upload.size).to eq uploader.file.size
+ expect(upload.path).to eq uploader.upload_path
+ expect(upload.model_id).to eq uploader.model.id
+ expect(upload.model_type).to eq uploader.model.class.to_s
+ expect(upload.uploader).to eq uploader.class.to_s
+ end
end
- it 'creates an Upload record after store' do
- expect(Upload).to receive(:record)
- .with(uploader)
+ it "does not create an Upload record when the file doesn't exist" do
+ allow(uploader).to receive(:file).and_return(double(exists?: false))
- uploader.store!(upload_fixture('rails_sample.jpg'))
+ expect { uploader.store!(upload_fixture('rails_sample.jpg')) }.not_to change { Upload.count }
end
it 'does not create an Upload record if model is missing' do
- expect_any_instance_of(RecordsUploadsExampleUploader).to receive(:model).and_return(nil)
- expect(Upload).not_to receive(:record).with(uploader)
+ allow_any_instance_of(RecordsUploadsExampleUploader).to receive(:model).and_return(nil)
- uploader.store!(upload_fixture('rails_sample.jpg'))
+ expect { uploader.store!(upload_fixture('rails_sample.jpg')) }.not_to change { Upload.count }
end
it 'it destroys Upload records at the same path before recording' do
@@ -72,29 +79,15 @@ describe RecordsUploads do
uploader: uploader.class.to_s
)
+ uploader.upload = existing
uploader.store!(upload_fixture('rails_sample.jpg'))
expect { existing.reload }.to raise_error(ActiveRecord::RecordNotFound)
- expect(Upload.count).to eq 1
+ expect(Upload.count).to eq(1)
end
end
describe '#destroy_upload callback' do
- it 'returns early when not using file storage' do
- uploader.store!(upload_fixture('rails_sample.jpg'))
-
- allow(uploader).to receive(:file_storage?).and_return(false)
- expect(Upload).not_to receive(:remove_path)
-
- uploader.remove!
- end
-
- it 'returns early when file is nil' do
- expect(Upload).not_to receive(:remove_path)
-
- uploader.remove!
- end
-
it 'it destroys Upload records at the same path after removal' do
uploader.store!(upload_fixture('rails_sample.jpg'))
diff --git a/spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb b/spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb
new file mode 100644
index 00000000000..6e7d8db99c4
--- /dev/null
+++ b/spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe 'projects/pipeline_schedules/_pipeline_schedule' do
+ let(:owner) { create(:user) }
+ let(:master) { create(:user) }
+ let(:project) { create(:project) }
+ let(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly, project: project) }
+
+ before do
+ assign(:project, project)
+
+ allow(view).to receive(:current_user).and_return(user)
+ allow(view).to receive(:pipeline_schedule).and_return(pipeline_schedule)
+
+ allow(view).to receive(:can?).and_return(true)
+ end
+
+ context 'taking ownership of schedule' do
+ context 'when non-owner is signed in' do
+ let(:user) { master }
+
+ before do
+ allow(view).to receive(:can?).with(master, :take_ownership_pipeline_schedule, pipeline_schedule).and_return(true)
+ end
+
+ it 'non-owner can take ownership of pipeline' do
+ render
+
+ expect(rendered).to have_link('Take ownership')
+ end
+ end
+
+ context 'when owner is signed in' do
+ let(:user) { owner }
+
+ before do
+ allow(view).to receive(:can?).with(owner, :take_ownership_pipeline_schedule, pipeline_schedule).and_return(false)
+ end
+
+ it 'owner cannot take ownership of pipeline' do
+ render
+
+ expect(rendered).not_to have_link('Take ownership')
+ end
+ end
+ end
+end
diff --git a/spec/workers/build_finished_worker_spec.rb b/spec/workers/build_finished_worker_spec.rb
index 1a7ffd5cdbf..c7ff8cf3b92 100644
--- a/spec/workers/build_finished_worker_spec.rb
+++ b/spec/workers/build_finished_worker_spec.rb
@@ -6,17 +6,15 @@ describe BuildFinishedWorker do
let!(:build) { create(:ci_build) }
it 'calculates coverage and calls hooks' do
- expect(BuildCoverageWorker)
+ expect(BuildTraceSectionsWorker)
.to receive(:new).ordered.and_call_original
- expect(BuildHooksWorker)
+ expect(BuildCoverageWorker)
.to receive(:new).ordered.and_call_original
- expect(BuildTraceSectionsWorker)
- .to receive(:perform_async)
- expect_any_instance_of(BuildCoverageWorker)
- .to receive(:perform)
- expect_any_instance_of(BuildHooksWorker)
- .to receive(:perform)
+ expect_any_instance_of(BuildTraceSectionsWorker).to receive(:perform)
+ expect_any_instance_of(BuildCoverageWorker).to receive(:perform)
+ expect(BuildHooksWorker).to receive(:perform_async)
+ expect(CreateTraceArtifactWorker).to receive(:perform_async)
described_class.new.perform(build.id)
end
diff --git a/spec/workers/create_trace_artifact_worker_spec.rb b/spec/workers/create_trace_artifact_worker_spec.rb
new file mode 100644
index 00000000000..854abd9cca7
--- /dev/null
+++ b/spec/workers/create_trace_artifact_worker_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe CreateTraceArtifactWorker do
+ describe '#perform' do
+ subject { described_class.new.perform(job&.id) }
+
+ context 'when job is found' do
+ let(:job) { create(:ci_build) }
+
+ it 'executes service' do
+ expect_any_instance_of(Ci::CreateTraceArtifactService)
+ .to receive(:execute).with(job)
+
+ subject
+ end
+ end
+
+ context 'when job is not found' do
+ let(:job) { nil }
+
+ it 'does not execute service' do
+ expect_any_instance_of(Ci::CreateTraceArtifactService)
+ .not_to receive(:execute)
+
+ subject
+ end
+ end
+ end
+end
diff --git a/spec/workers/upload_checksum_worker_spec.rb b/spec/workers/upload_checksum_worker_spec.rb
index 911360da66c..9e50ce15871 100644
--- a/spec/workers/upload_checksum_worker_spec.rb
+++ b/spec/workers/upload_checksum_worker_spec.rb
@@ -2,18 +2,31 @@ require 'rails_helper'
describe UploadChecksumWorker do
describe '#perform' do
- it 'rescues ActiveRecord::RecordNotFound' do
- expect { described_class.new.perform(999_999) }.not_to raise_error
+ subject { described_class.new }
+
+ context 'without a valid record' do
+ it 'rescues ActiveRecord::RecordNotFound' do
+ expect { subject.perform(999_999) }.not_to raise_error
+ end
end
- it 'calls calculate_checksum_without_delay and save!' do
- upload = spy
- expect(Upload).to receive(:find).with(999_999).and_return(upload)
+ context 'with a valid record' do
+ let(:upload) { create(:user, :with_avatar).avatar.upload }
+
+ before do
+ expect(Upload).to receive(:find).and_return(upload)
+ allow(upload).to receive(:foreground_checksumable?).and_return(false)
+ end
- described_class.new.perform(999_999)
+ it 'calls calculate_checksum!' do
+ expect(upload).to receive(:calculate_checksum!)
+ subject.perform(upload.id)
+ end
- expect(upload).to have_received(:calculate_checksum)
- expect(upload).to have_received(:save!)
+ it 'calls save!' do
+ expect(upload).to receive(:save!)
+ subject.perform(upload.id)
+ end
end
end
end
diff --git a/vendor/ingress/values.yaml b/vendor/ingress/values.yaml
new file mode 100644
index 00000000000..cdb7da77e86
--- /dev/null
+++ b/vendor/ingress/values.yaml
@@ -0,0 +1,8 @@
+controller:
+ image:
+ tag: "0.10.2"
+ repository: "quay.io/kubernetes-ingress-controller/nginx-ingress-controller"
+ stats.enabled: true
+ podAnnotations:
+ prometheus.io/scrape: "true"
+ prometheus.io/port: "10254"
diff --git a/vendor/prometheus/values.yaml b/vendor/prometheus/values.yaml
index 4c709aeac76..db967514be7 100644
--- a/vendor/prometheus/values.yaml
+++ b/vendor/prometheus/values.yaml
@@ -2,7 +2,7 @@ alertmanager:
enabled: false
kubeStateMetrics:
- enabled: false
+ enabled: true
nodeExporter:
enabled: false
@@ -10,6 +10,10 @@ nodeExporter:
pushgateway:
enabled: false
+server:
+ image:
+ tag: v2.1.0
+
serverFiles:
alerts: {}
rules: {}
@@ -46,6 +50,34 @@ serverFiles:
- pod_name
target_label: environment
regex: "(.+)-.+-.+"
+ - job_name: 'kubernetes-service-endpoints'
+ kubernetes_sd_configs:
+ - role: endpoints
+ relabel_configs:
+ - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape]
+ action: keep
+ regex: true
+ - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scheme]
+ action: replace
+ target_label: __scheme__
+ regex: (https?)
+ - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_path]
+ action: replace
+ target_label: __metrics_path__
+ regex: (.+)
+ - source_labels: [__address__, __meta_kubernetes_service_annotation_prometheus_io_port]
+ action: replace
+ target_label: __address__
+ regex: (.+)(?::\d+);(\d+)
+ replacement: $1:$2
+ - action: labelmap
+ regex: __meta_kubernetes_service_label_(.+)
+ - source_labels: [__meta_kubernetes_namespace]
+ action: replace
+ target_label: kubernetes_namespace
+ - source_labels: [__meta_kubernetes_service_name]
+ action: replace
+ target_label: kubernetes_name
- job_name: kubernetes-nodes
scheme: https
tls_config:
diff --git a/yarn.lock b/yarn.lock
index d10a4372a40..bc5c19464fb 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -54,9 +54,9 @@
lodash "^4.2.0"
to-fast-properties "^2.0.0"
-"@gitlab-org/gitlab-svgs@^1.7.0":
- version "1.7.0"
- resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.7.0.tgz#dbb1330a1b1ee478378dddab53fe1a881e810f5d"
+"@gitlab-org/gitlab-svgs@^1.8.0":
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.8.0.tgz#95d6afa94395860699ddad60a82bd1bbbc2ba89f"
"@types/jquery@^2.0.40":
version "2.0.48"
@@ -3322,7 +3322,7 @@ glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1:
once "^1.3.0"
path-is-absolute "^1.0.0"
-glob@^7.1.0, glob@~7.1.2:
+glob@^7.1.0, glob@^7.1.2, glob@~7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
dependencies: