Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDylan Griffith <dyl.griffith@gmail.com>2018-05-03 10:54:12 +0300
committerDylan Griffith <dyl.griffith@gmail.com>2018-05-03 10:54:12 +0300
commitd39b3d4b8d2d4c5ded46182a5353c68a8f5bb5cd (patch)
treee8f2fac760b252928ff23074dea9d1f209838a33
parentdcb67951a817db262ddcd3b777fafc4e1995fc04 (diff)
parent2c9568edeea7d95b6e4ec3c23cdc1c027bf86d5f (diff)
Merge branch 'master' into feature/runner-per-group
-rw-r--r--.gitignore1
-rw-r--r--.gitlab-ci.yml20
-rw-r--r--CHANGELOG.md36
-rw-r--r--CONTRIBUTING.md27
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_PAGES_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile7
-rw-r--r--Gemfile.lock13
-rw-r--r--Gemfile.rails5.lock11
-rw-r--r--app/assets/javascripts/behaviors/gl_emoji.js27
-rw-r--r--app/assets/javascripts/compare.js86
-rw-r--r--app/assets/javascripts/compare_autocomplete.js49
-rw-r--r--app/assets/javascripts/emoji/index.js6
-rw-r--r--app/assets/javascripts/emoji/support/unicode_support_map.js40
-rw-r--r--app/assets/javascripts/ide/components/changed_file_icon.vue9
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/index.vue90
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue11
-rw-r--r--app/assets/javascripts/ide/components/repo_file.vue5
-rw-r--r--app/assets/javascripts/ide/components/repo_tab.vue1
-rw-r--r--app/assets/javascripts/ide/stores/actions.js19
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js11
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js11
-rw-r--r--app/assets/javascripts/ide/stores/utils.js7
-rw-r--r--app/assets/javascripts/ide/stores/workers/files_decorator_worker.js8
-rw-r--r--app/assets/javascripts/milestone_select.js73
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue2
-rw-r--r--app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue5
-rw-r--r--app/assets/javascripts/pages/projects/compare/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js60
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js11
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/target_project_dropdown.js22
-rw-r--r--app/assets/javascripts/pages/users/activity_calendar.js4
-rw-r--r--app/assets/javascripts/performance_bar/components/performance_bar_app.vue5
-rw-r--r--app/assets/javascripts/performance_bar/components/upstream_performance_bar.vue20
-rw-r--r--app/assets/javascripts/performance_bar/index.js2
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.js10
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.vue13
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue (renamed from app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js)35
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue4
-rw-r--r--app/assets/javascripts/sidebar/lib/sidebar_move_issue.js3
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue102
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue (renamed from app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js)79
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/dependencies.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/file_icon.vue101
-rw-r--r--app/assets/javascripts/vue_shared/components/identicon.vue2
-rw-r--r--app/assets/javascripts/vue_shared/models/label.js2
-rw-r--r--app/assets/stylesheets/emoji_sprites.scss5403
-rw-r--r--app/assets/stylesheets/framework.scss121
-rw-r--r--app/assets/stylesheets/framework/blocks.scss4
-rw-r--r--app/assets/stylesheets/framework/common.scss1
-rw-r--r--app/assets/stylesheets/framework/emoji_sprites.scss1813
-rw-r--r--app/assets/stylesheets/framework/images.scss35
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss10
-rw-r--r--app/assets/stylesheets/framework/variables.scss1
-rw-r--r--app/assets/stylesheets/framework/wells.scss1
-rw-r--r--app/assets/stylesheets/pages/commits.scss2
-rw-r--r--app/assets/stylesheets/pages/notes.scss4
-rw-r--r--app/assets/stylesheets/pages/repo.scss20
-rw-r--r--app/assets/stylesheets/pages/repo.scss.orig786
-rw-r--r--app/assets/stylesheets/performance_bar.scss1
-rw-r--r--app/controllers/application_controller.rb3
-rw-r--r--app/controllers/concerns/issuable_collections.rb2
-rw-r--r--app/controllers/groups/application_controller.rb2
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb4
-rw-r--r--app/controllers/profiles/active_sessions_controller.rb14
-rw-r--r--app/controllers/projects/application_controller.rb2
-rw-r--r--app/controllers/projects/lfs_storage_controller.rb3
-rw-r--r--app/controllers/projects/merge_requests/creations_controller.rb7
-rw-r--r--app/controllers/projects/notes_controller.rb4
-rw-r--r--app/finders/groups_finder.rb8
-rw-r--r--app/finders/pipelines_finder.rb9
-rw-r--r--app/helpers/active_sessions_helper.rb23
-rw-r--r--app/helpers/application_helper.rb74
-rw-r--r--app/helpers/auth_helper.rb8
-rw-r--r--app/helpers/avatars_helper.rb74
-rw-r--r--app/helpers/blob_helper.rb2
-rw-r--r--app/helpers/commits_helper.rb2
-rw-r--r--app/helpers/dropdowns_helper.rb2
-rw-r--r--app/helpers/projects_helper.rb2
-rw-r--r--app/helpers/system_note_helper.rb6
-rw-r--r--app/mailers/notify.rb1
-rw-r--r--app/models/active_session.rb110
-rw-r--r--app/models/ci/job_artifact.rb2
-rw-r--r--app/models/ci/pipeline.rb11
-rw-r--r--app/models/ci/stage.rb21
-rw-r--r--app/models/commit.rb10
-rw-r--r--app/models/commit_status.rb7
-rw-r--r--app/models/diff_note.rb15
-rw-r--r--app/models/lfs_object.rb2
-rw-r--r--app/models/members/group_member.rb6
-rw-r--r--app/models/members/project_member.rb6
-rw-r--r--app/models/user.rb2
-rw-r--r--app/services/ci/ensure_stage_service.rb1
-rw-r--r--app/services/issues/close_service.rb2
-rw-r--r--app/services/issues/move_service.rb2
-rw-r--r--app/services/issues/reopen_service.rb2
-rw-r--r--app/services/issues/update_service.rb6
-rw-r--r--app/services/merge_requests/close_service.rb2
-rw-r--r--app/services/merge_requests/merge_service.rb29
-rw-r--r--app/services/merge_requests/reopen_service.rb2
-rw-r--r--app/services/merge_requests/resolved_discussion_notification_service.rb2
-rw-r--r--app/services/merge_requests/update_service.rb11
-rw-r--r--app/services/notes/resolve_service.rb9
-rw-r--r--app/services/notification_service.rb75
-rw-r--r--app/services/projects/update_pages_service.rb29
-rw-r--r--app/services/repository_archive_clean_up_service.rb5
-rw-r--r--app/services/system_note_service.rb2
-rw-r--r--app/views/admin/projects/show.html.haml2
-rw-r--r--app/views/admin/runners/_runner.html.haml2
-rw-r--r--app/views/admin/runners/show.html.haml2
-rw-r--r--app/views/admin/services/index.html.haml3
-rw-r--r--app/views/admin/users/_user.html.haml4
-rw-r--r--app/views/admin/users/show.html.haml4
-rw-r--r--app/views/ci/variables/_variable_row.html.haml6
-rw-r--r--app/views/devise/mailer/unlock_instructions.html.haml2
-rw-r--r--app/views/devise/mailer/unlock_instructions.text.erb2
-rw-r--r--app/views/groups/_group_admin_settings.html.haml52
-rw-r--r--app/views/layouts/nav/sidebar/_profile.html.haml11
-rw-r--r--app/views/peek/_bar.html.haml7
-rw-r--r--app/views/profiles/active_sessions/_active_session.html.haml31
-rw-r--r--app/views/profiles/active_sessions/index.html.haml14
-rw-r--r--app/views/projects/branches/_branch.html.haml11
-rw-r--r--app/views/projects/diffs/_diffs.html.haml2
-rw-r--r--app/views/projects/jobs/_sidebar.html.haml2
-rw-r--r--app/views/projects/merge_requests/creations/_new_compare.html.haml34
-rw-r--r--app/views/projects/merge_requests/dropdowns/_project.html.haml2
-rw-r--r--app/views/projects/protected_branches/_branches_list.html.haml2
-rw-r--r--app/views/projects/protected_branches/_create_protected_branch.html.haml4
-rw-r--r--app/views/projects/protected_branches/_update_protected_branch.html.haml2
-rw-r--r--app/views/projects/protected_branches/shared/_protected_branch.html.haml2
-rw-r--r--app/views/projects/registry/repositories/_tag.html.haml2
-rw-r--r--app/views/projects/runners/show.html.haml2
-rw-r--r--app/views/projects/services/_index.html.haml3
-rw-r--r--app/views/projects/settings/ci_cd/_autodevops_form.html.haml1
-rw-r--r--app/views/projects/triggers/_trigger.html.haml2
-rw-r--r--app/views/sherlock/transactions/_general.html.haml3
-rw-r--r--app/views/sherlock/transactions/index.html.haml3
-rw-r--r--app/workers/all_queues.yml1
-rw-r--r--app/workers/concerns/mail_scheduler_queue.rb4
-rw-r--r--app/workers/mail_scheduler/issue_due_worker.rb2
-rw-r--r--app/workers/mail_scheduler/notification_service_worker.rb19
-rw-r--r--changelogs/unreleased/44775-avatar-on-os-fails-with-cdn.yml5
-rw-r--r--changelogs/unreleased/45451-user-deletion-modal-with-same-info-for-delete-user-or-delete-user-and-contributions.yml6
-rw-r--r--changelogs/unreleased/45481-sane-pages-artifacts.yml6
-rw-r--r--changelogs/unreleased/45572-members-invitations-scheduled-before-commit.yml5
-rw-r--r--changelogs/unreleased/45761-replace-actionview-time_ago_in_words.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-source-markdown-render-feature.yml5
-rw-r--r--changelogs/unreleased/bvl-fix-maintainer-push-error.yml5
-rw-r--r--changelogs/unreleased/docs-use-variables-deploy-policy-for-staging-and-production.yml6
-rw-r--r--changelogs/unreleased/feature-display-active-sessions.yml5
-rw-r--r--changelogs/unreleased/feature-show-only-groups-user-is-member-of-in-dashboard.yml5
-rw-r--r--changelogs/unreleased/fix-inconsistent-protected-branch-pill-baseline.yml5
-rw-r--r--changelogs/unreleased/helm-add-alpine-mirrors.yml5
-rw-r--r--changelogs/unreleased/improve-quick-actions-summary-preview.yml5
-rw-r--r--changelogs/unreleased/increase-new-issue-metadata-form-margin.yml5
-rw-r--r--changelogs/unreleased/issue_45463.yml5
-rw-r--r--changelogs/unreleased/jprovazn-commit-notes-api.yml5
-rw-r--r--changelogs/unreleased/jprovazn-generic-error.yml6
-rw-r--r--changelogs/unreleased/jr-33320-lfs-settings-interface.yml5
-rw-r--r--changelogs/unreleased/jramsay-44880-filter-pipelines-by-sha.yml5
-rw-r--r--changelogs/unreleased/move-notification-service-calls-to-sidekiq.yml5
-rw-r--r--changelogs/unreleased/osw-use-cached-highlighted-content-for-discussions.yml5
-rw-r--r--changelogs/unreleased/performance-gb-improve-pipeline-creation-service.yml5
-rw-r--r--changelogs/unreleased/refactor-move-mr-widget-wip-vue-component.yml5
-rw-r--r--changelogs/unreleased/refactor-move-no-tracking-pane-vue-component.yml5
-rw-r--r--changelogs/unreleased/refactor-move-sidebar-time-tracking-vue-component.yml5
-rw-r--r--changelogs/unreleased/restore-size-and-position-for-fork-icon.yml5
-rw-r--r--changelogs/unreleased/revert-discussion-counter-height.yml5
-rw-r--r--changelogs/unreleased/security-45689-fix-archive-cache-bug.yml5
-rw-r--r--changelogs/unreleased/security_issue_42029.yml5
-rw-r--r--changelogs/unreleased/update-doorkeeper-changelog.yml5
-rw-r--r--changelogs/unreleased/update-timeline-icon-for-description-edit.yml5
-rw-r--r--changelogs/unreleased/winh-dashboard-any-milestone.yml5
-rw-r--r--changelogs/unreleased/winh-new-mergerequest-branch-picker.yml5
-rw-r--r--changelogs/unreleased/zj-namespace-service-mandatory.yml5
-rw-r--r--changelogs/unreleased/zj-repo-checksum-opt-out.yml5
-rw-r--r--changelogs/unreleased/zj-repository-exist-mandatory.yml5
-rw-r--r--config/application.rb1
-rw-r--r--config/gitlab.yml.example24
-rw-r--r--config/initializers/1_settings.rb3
-rw-r--r--config/initializers/9_fast_gettext.rb (renamed from config/initializers/fast_gettext.rb)0
-rw-r--r--config/initializers/doorkeeper.rb2
-rw-r--r--config/initializers/omniauth.rb1
-rw-r--r--config/initializers/pages.rb2
-rw-r--r--config/initializers/peek.rb1
-rw-r--r--config/initializers/session_store.rb26
-rw-r--r--config/initializers/warden.rb12
-rw-r--r--config/karma.config.js77
-rw-r--r--config/routes/profile.rb1
-rw-r--r--config/routes/project.rb1
-rw-r--r--config/webpack.config.js3
-rw-r--r--db/migrate/20180417101040_add_tmp_stage_priority_index_to_ci_builds.rb16
-rw-r--r--db/migrate/20180417101940_add_index_to_ci_stage.rb9
-rw-r--r--db/post_migrate/20180420080616_schedule_stages_index_migration.rb29
-rw-r--r--db/schema.rb2
-rw-r--r--doc/README.md39
-rw-r--r--doc/administration/auth/jwt.md2
-rw-r--r--doc/administration/high_availability/redis.md2
-rw-r--r--doc/administration/job_artifacts.md4
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_metrics.md2
-rw-r--r--doc/administration/operations/fast_ssh_key_lookup.md2
-rw-r--r--doc/administration/uploads.md2
-rw-r--r--doc/api/README.md25
-rw-r--r--doc/api/discussions.md585
-rw-r--r--doc/api/group_badges.md2
-rw-r--r--doc/api/groups.md4
-rw-r--r--doc/api/notes.md9
-rw-r--r--doc/api/pipeline_schedules.md4
-rw-r--r--doc/api/pipelines.md1
-rw-r--r--doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md2
-rw-r--r--doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md14
-rw-r--r--doc/ci/variables/README.md2
-rw-r--r--doc/development/README.md2
-rw-r--r--doc/development/background_migrations.md4
-rw-r--r--doc/development/diffs.md115
-rw-r--r--doc/development/doc_styleguide.md4
-rw-r--r--doc/development/ee_features.md2
-rw-r--r--doc/development/fe_guide/icons.md2
-rw-r--r--doc/development/fe_guide/style_guide_js.md2
-rw-r--r--doc/development/file_storage.md2
-rw-r--r--doc/development/i18n/externalization.md2
-rw-r--r--doc/development/img/state-model-issue.pngbin7713 -> 0 bytes
-rw-r--r--doc/development/img/state-model-legend.pngbin8496 -> 0 bytes
-rw-r--r--doc/development/img/state-model-merge-request.pngbin12459 -> 0 bytes
-rw-r--r--doc/development/merge_request_performance_guidelines.md2
-rw-r--r--doc/development/object_state_models.md25
-rw-r--r--doc/development/ordering_table_columns.md2
-rw-r--r--doc/development/testing_guide/frontend_testing.md74
-rw-r--r--doc/development/testing_guide/testing_levels.md2
-rw-r--r--doc/development/ux_guide/components.md4
-rw-r--r--doc/development/what_requires_downtime.md2
-rw-r--r--doc/development/writing_documentation.md2
-rw-r--r--doc/install/database_mysql.md2
-rw-r--r--doc/install/google_cloud_platform/index.md2
-rw-r--r--doc/install/kubernetes/gitlab_runner_chart.md8
-rw-r--r--doc/integration/shibboleth.md2
-rw-r--r--doc/ssh/README.md4
-rw-r--r--doc/topics/autodevops/index.md22
-rw-r--r--doc/university/glossary/README.md2
-rw-r--r--doc/university/high-availability/aws/README.md4
-rw-r--r--doc/university/support/README.md2
-rw-r--r--doc/university/training/end-user/README.md2
-rw-r--r--doc/university/training/topics/tags.md2
-rw-r--r--doc/university/training/user_training.md2
-rw-r--r--doc/user/admin_area/settings/sign_up_restrictions.md2
-rw-r--r--doc/user/group/subgroups/index.md2
-rw-r--r--doc/user/index.md2
-rw-r--r--doc/user/profile/active_sessions.md20
-rw-r--r--doc/user/profile/img/active_sessions_list.pngbin0 -> 41649 bytes
-rw-r--r--doc/user/profile/index.md1
-rw-r--r--doc/user/project/clusters/index.md1
-rw-r--r--doc/user/project/issues/closing_issues.md6
-rw-r--r--doc/user/project/issues/crosslinking_issues.md2
-rw-r--r--doc/user/project/issues/issues_functionalities.md2
-rw-r--r--doc/user/project/milestones/index.md2
-rw-r--r--doc/user/project/pages/getting_started_part_two.md6
-rw-r--r--doc/user/project/pages/img/remove_fork_relationship.png (renamed from doc/user/project/pages/img/remove_fork_relashionship.png)bin13642 -> 13642 bytes
-rw-r--r--doc/user/project/pages/index.md36
-rw-r--r--doc/user/project/repository/reducing_the_repo_size_using_git.md2
-rw-r--r--doc/user/search/index.md2
-rw-r--r--doc/workflow/lfs/manage_large_binaries_with_git_lfs.md13
-rw-r--r--features/project/source/markdown_render.feature147
-rw-r--r--features/steps/project/forked_merge_requests.rb2
-rw-r--r--features/steps/project/source/markdown_render.rb317
-rw-r--r--features/steps/shared/markdown.rb9
-rw-r--r--lib/api/discussions.rb88
-rw-r--r--lib/api/entities.rb19
-rw-r--r--lib/api/groups.rb16
-rw-r--r--lib/api/helpers.rb4
-rw-r--r--lib/api/helpers/custom_attributes.rb3
-rw-r--r--lib/api/helpers/notes_helpers.rb56
-rw-r--r--lib/api/notes.rb30
-rw-r--r--lib/api/pipelines.rb1
-rw-r--r--lib/banzai/filter/commit_trailers_filter.rb1
-rw-r--r--lib/gitlab/background_migration/migrate_stage_index.rb47
-rw-r--r--lib/gitlab/base_doorkeeper_controller.rb8
-rw-r--r--lib/gitlab/ci/cron_parser.rb8
-rw-r--r--lib/gitlab/ci/pipeline/chain/populate.rb6
-rw-r--r--lib/gitlab/ci/pipeline/seed/stage.rb1
-rw-r--r--lib/gitlab/database/arel_methods.rb18
-rw-r--r--lib/gitlab/database/migration_helpers.rb4
-rw-r--r--lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb10
-rw-r--r--lib/gitlab/diff/file_collection/base.rb2
-rw-r--r--lib/gitlab/diff/position.rb4
-rw-r--r--lib/gitlab/git/raw_diff_change.rb4
-rw-r--r--lib/gitlab/git/remote_repository.rb7
-rw-r--r--lib/gitlab/git/repository.rb66
-rw-r--r--lib/gitlab/gitaly_client/repository_service.rb4
-rw-r--r--lib/gitlab/gon_helper.rb1
-rw-r--r--lib/gitlab/kubernetes/helm/base_command.rb3
-rw-r--r--lib/gitlab/pages_client.rb117
-rw-r--r--lib/gitlab/redis/shared_state.rb2
-rw-r--r--lib/gitlab/shell.rb44
-rw-r--r--lib/omni_auth/strategies/jwt.rb62
-rw-r--r--lib/tasks/gitlab/pages.rake9
-rw-r--r--package.json15
-rw-r--r--qa/Dockerfile2
-rw-r--r--qa/Gemfile1
-rw-r--r--qa/Gemfile.lock4
-rw-r--r--qa/qa.rb8
-rw-r--r--qa/qa/factory/repository/push.rb26
-rw-r--r--qa/qa/factory/resource/branch.rb23
-rw-r--r--qa/qa/factory/resource/deploy_key.rb14
-rw-r--r--qa/qa/factory/resource/merge_request.rb6
-rw-r--r--qa/qa/factory/resource/project.rb7
-rw-r--r--qa/qa/factory/resource/secret_variable.rb3
-rw-r--r--qa/qa/git/location.rb2
-rw-r--r--qa/qa/git/repository.rb7
-rw-r--r--qa/qa/page/base.rb4
-rw-r--r--qa/qa/page/project/settings/deploy_keys.rb12
-rw-r--r--qa/qa/page/project/settings/protected_branches.rb29
-rw-r--r--qa/qa/page/project/settings/secret_variables.rb20
-rw-r--r--qa/qa/page/project/show.rb8
-rw-r--r--qa/qa/runtime/key/base.rb36
-rw-r--r--qa/qa/runtime/key/ecdsa.rb12
-rw-r--r--qa/qa/runtime/key/ed25519.rb12
-rw-r--r--qa/qa/runtime/key/rsa.rb11
-rw-r--r--qa/qa/runtime/rsa_key.rb21
-rw-r--r--qa/qa/service/shellout.rb4
-rw-r--r--qa/qa/specs/features/project/add_deploy_key_spec.rb3
-rw-r--r--qa/qa/specs/features/project/deploy_key_clone_spec.rb136
-rw-r--r--qa/qa/specs/features/repository/clone_spec.rb6
-rw-r--r--qa/qa/specs/features/repository/protected_branches_spec.rb9
-rw-r--r--qa/spec/runtime/key/ecdsa_spec.rb18
-rw-r--r--qa/spec/runtime/key/ed25519_spec.rb9
-rw-r--r--qa/spec/runtime/key/rsa_spec.rb (renamed from qa/spec/runtime/rsa_key.rb)4
-rw-r--r--spec/controllers/projects/clusters/gcp_controller_spec.rb2
-rw-r--r--spec/controllers/projects/merge_requests/creations_controller_spec.rb30
-rw-r--r--spec/controllers/projects/raw_controller_spec.rb4
-rw-r--r--spec/factories/ci/stages.rb1
-rw-r--r--spec/factories/commit_statuses.rb1
-rw-r--r--spec/features/admin/admin_uses_repository_checks_spec.rb2
-rw-r--r--spec/features/dashboard/groups_list_spec.rb22
-rw-r--r--spec/features/dashboard/milestone_filter_spec.rb49
-rw-r--r--spec/features/groups/members/manage_access_requests_spec.rb47
-rw-r--r--spec/features/groups/members/master_manages_access_requests_spec.rb8
-rw-r--r--spec/features/issues/user_uses_slash_commands_spec.rb4
-rw-r--r--spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb32
-rw-r--r--spec/features/profiles/active_sessions_spec.rb89
-rw-r--r--spec/features/projects/files/user_browses_files_spec.rb240
-rw-r--r--spec/features/projects/issues/user_toggles_subscription_spec.rb8
-rw-r--r--spec/features/projects/members/master_manages_access_requests_spec.rb45
-rw-r--r--spec/features/projects/settings/lfs_settings_spec.rb18
-rw-r--r--spec/features/projects/tree/upload_file_spec.rb13
-rw-r--r--spec/features/projects/wiki/user_creates_wiki_page_spec.rb280
-rw-r--r--spec/features/users/active_sessions_spec.rb69
-rw-r--r--spec/finders/groups_finder_spec.rb84
-rw-r--r--spec/finders/pipelines_finder_spec.rb20
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/notes.json5
-rw-r--r--spec/helpers/application_helper_spec.rb139
-rw-r--r--spec/helpers/auth_helper_spec.rb24
-rw-r--r--spec/helpers/avatars_helper_spec.rb139
-rw-r--r--spec/helpers/blob_helper_spec.rb25
-rw-r--r--spec/javascripts/ide/components/new_dropdown/index_spec.js22
-rw-r--r--spec/javascripts/ide/components/new_dropdown/modal_spec.js14
-rw-r--r--spec/javascripts/ide/stores/actions_spec.js50
-rw-r--r--spec/javascripts/ide/stores/mutations_spec.js22
-rw-r--r--spec/javascripts/sidebar/mock_data.js2
-rw-r--r--spec/javascripts/sidebar/sidebar_move_issue_spec.js9
-rw-r--r--spec/javascripts/test_bundle.js12
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js46
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js8
-rw-r--r--spec/lib/banzai/filter/commit_trailers_filter_spec.rb40
-rw-r--r--spec/lib/gitlab/background_migration/migrate_stage_index_spec.rb35
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/create_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb9
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb3
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb87
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml1
-rw-r--r--spec/lib/gitlab/kubernetes/helm/base_command_spec.rb18
-rw-r--r--spec/lib/gitlab/kubernetes/helm/init_command_spec.rb20
-rw-r--r--spec/lib/gitlab/kubernetes/helm/install_command_spec.rb66
-rw-r--r--spec/lib/gitlab/pages_client_spec.rb172
-rw-r--r--spec/lib/omni_auth/strategies/jwt_spec.rb87
-rw-r--r--spec/migrations/schedule_stages_index_migration_spec.rb35
-rw-r--r--spec/models/active_session_spec.rb216
-rw-r--r--spec/models/ci/stage_spec.rb34
-rw-r--r--spec/models/diff_note_spec.rb33
-rw-r--r--spec/models/lfs_object_spec.rb4
-rw-r--r--spec/models/members/group_member_spec.rb48
-rw-r--r--spec/models/members/project_member_spec.rb12
-rw-r--r--spec/models/notification_setting_spec.rb7
-rw-r--r--spec/models/repository_spec.rb16
-rw-r--r--spec/requests/api/discussions_spec.rb33
-rw-r--r--spec/requests/api/jobs_spec.rb6
-rw-r--r--spec/requests/api/runner_spec.rb4
-rw-r--r--spec/requests/api/v3/builds_spec.rb4
-rw-r--r--spec/requests/openid_connect_spec.rb9
-rw-r--r--spec/services/applications/create_service_spec.rb14
-rw-r--r--spec/services/ci/retry_build_service_spec.rb4
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb4
-rw-r--r--spec/services/notes/resolve_service_spec.rb23
-rw-r--r--spec/services/notification_service_spec.rb42
-rw-r--r--spec/services/projects/update_pages_service_spec.rb61
-rw-r--r--spec/services/repository_archive_clean_up_service_spec.rb68
-rw-r--r--spec/services/system_note_service_spec.rb8
-rw-r--r--spec/support/commit_trailers_spec_helper.rb2
-rw-r--r--spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb52
-rw-r--r--spec/support/shared_examples/helm_generated_script.rb19
-rw-r--r--spec/support/shared_examples/models/members_notifications_shared_example.rb63
-rw-r--r--spec/support/shared_examples/requests/api/diff_discussions.rb57
-rw-r--r--spec/support/shared_examples/requests/api/resolvable_discussions.rb87
-rw-r--r--spec/uploaders/lfs_object_uploader_spec.rb12
-rw-r--r--spec/workers/mail_scheduler/issue_due_worker_spec.rb4
-rw-r--r--spec/workers/mail_scheduler/notification_service_worker_spec.rb44
-rw-r--r--spec/workers/namespaceless_project_destroy_worker_spec.rb15
-rw-r--r--vendor/assets/javascripts/peek.performance_bar.js182
-rw-r--r--yarn.lock767
414 files changed, 11363 insertions, 5982 deletions
diff --git a/.gitignore b/.gitignore
index e9ff0048c1c..e1561c9db9a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -72,3 +72,4 @@ eslint-report.html
/locale/**/*.time_stamp
/.rspec
/plugins/*
+/.gitlab_pages_secret
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 5bc2f1f3a0f..05487134cb1 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -110,7 +110,7 @@ stages:
# Jobs that only need to pull cache
.dedicated-no-docs-pull-cache-job: &dedicated-no-docs-pull-cache-job
<<: *dedicated-runner
- <<: *except-docs-and-qa
+ <<: *except-docs
<<: *pull-cache
dependencies:
- setup-test-env
@@ -122,6 +122,10 @@ stages:
variables:
SETUP_DB: "false"
+.dedicated-no-docs-and-no-qa-pull-cache-job: &dedicated-no-docs-and-no-qa-pull-cache-job
+ <<: *dedicated-no-docs-pull-cache-job
+ <<: *except-docs-and-qa
+
.rake-exec: &rake-exec
<<: *dedicated-no-docs-no-db-pull-cache-job
script:
@@ -222,7 +226,7 @@ stages:
- master@gitlab/gitlab-ee
.gitlab-setup: &gitlab-setup
- <<: *dedicated-no-docs-pull-cache-job
+ <<: *dedicated-no-docs-and-no-qa-pull-cache-job
<<: *use-pg
variables:
CREATE_DB_USER: "true"
@@ -262,12 +266,12 @@ stages:
# DB migration, rollback, and seed jobs
.db-migrate-reset: &db-migrate-reset
- <<: *dedicated-no-docs-pull-cache-job
+ <<: *dedicated-no-docs-and-no-qa-pull-cache-job
script:
- bundle exec rake db:migrate:reset
.migration-paths: &migration-paths
- <<: *dedicated-no-docs-pull-cache-job
+ <<: *dedicated-no-docs-and-no-qa-pull-cache-job
variables:
CREATE_DB_USER: "true"
script:
@@ -647,7 +651,7 @@ migration:path-mysql:
<<: *use-mysql
.db-rollback: &db-rollback
- <<: *dedicated-no-docs-pull-cache-job
+ <<: *dedicated-no-docs-and-no-qa-pull-cache-job
script:
- bundle exec rake db:migrate VERSION=20170523121229
- bundle exec rake db:migrate
@@ -670,7 +674,7 @@ gitlab:setup-mysql:
# Frontend-related jobs
gitlab:assets:compile:
- <<: *dedicated-no-docs-no-db-pull-cache-job
+ <<: *dedicated-no-docs-and-no-qa-pull-cache-job
dependencies: []
variables:
NODE_ENV: "production"
@@ -691,7 +695,7 @@ gitlab:assets:compile:
- webpack-report/
karma:
- <<: *dedicated-no-docs-pull-cache-job
+ <<: *dedicated-no-docs-and-no-qa-pull-cache-job
<<: *use-pg
dependencies:
- compile-assets
@@ -815,7 +819,7 @@ coverage:
- coverage/assets/
lint:javascript:report:
- <<: *dedicated-no-docs-no-db-pull-cache-job
+ <<: *dedicated-no-docs-and-no-qa-pull-cache-job
stage: post-test
dependencies:
- compile-assets
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8278119cf10..29047c3ad65 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,28 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 10.7.3 (2018-05-02)
+
+### Fixed (8 changes)
+
+- Fixed wrong avatar URL when the avatar is on object storage. !18092
+- Fix errors on pushing to an empty repository. !18462
+- Update doorkeeper to 4.3.2 to fix GitLab OAuth authentication. !18543
+- Ports omniauth-jwt gem onto GitLab OmniAuth Strategies suite. !18580
+- Fix redirection error for applications using OpenID. !18599
+- Fix commit trailer rendering when Gravatar is disabled.
+- Fix file_store for artifacts and lfs when saving.
+- Fix users not seeing labels from private groups when being a member of a child project.
+
+
+## 10.7.2 (2018-04-25)
+
+### Security (2 changes)
+
+- Serve archive requests with the correct file in all cases.
+- Sanitizes user name to avoid XSS attacks.
+
+
## 10.7.1 (2018-04-23)
### Fixed (11 changes)
@@ -237,6 +259,13 @@ entry.
- Upgrade Gitaly to upgrade its charlock_holmes.
+## 10.6.5 (2018-04-24)
+
+### Security (1 change)
+
+- Sanitizes user name to avoid XSS attacks.
+
+
## 10.6.4 (2018-04-09)
### Fixed (8 changes, 1 of them is from the community)
@@ -478,6 +507,13 @@ entry.
- Use host URL to build JIRA remote link icon.
+## 10.5.8 (2018-04-24)
+
+### Security (1 change)
+
+- Sanitizes user name to avoid XSS attacks.
+
+
## 10.5.7 (2018-04-03)
### Security (2 changes)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 29009010f0e..0f97e779129 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -25,12 +25,12 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
- [Workflow labels](#workflow-labels)
- [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc)
- [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc)
- - [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-cicd-discussion-edge-platform-etc)
+ - [Team labels (~"CI/CD", ~Discussion, ~Quality, ~Platform, etc.)](#team-labels-cicd-discussion-quality-platform-etc)
- [Milestone labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#milestone-labels-deliverable-stretch-next-patch-release)
- [Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#bug-priority-labels-p1-p2-p3-etc)
- [Severity labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#bug-severity-labels-s1-s2-s3-etc)
- [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests)
-- [Implement design & UI elements](#implement-design-ui-elements)
+- [Implement design & UI elements](#implement-design--ui-elements)
- [Issue tracker](#issue-tracker)
- [Issue triaging](#issue-triaging)
- [Feature proposals](#feature-proposals)
@@ -114,7 +114,7 @@ is a great place to start. Issues with a lower weight (1 or 2) are deemed
suitable for beginners. These issues will be of reasonable size and challenge,
for anyone to start contributing to GitLab. If you have any questions or need help visit [Getting Help](https://about.gitlab.com/getting-help/#discussion) to
learn how to communicate with GitLab. If you're looking for a Gitter or Slack channel
-please consider we favor
+please consider we favor
[asynchronous communication](https://about.gitlab.com/handbook/communication/#internal-communication) over real time communication. Thanks for your contribution!
## Workflow labels
@@ -127,7 +127,7 @@ Most issues will have labels for at least one of the following:
- Type: ~"feature proposal", ~bug, ~customer, etc.
- Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc.
-- Team: ~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.
+- Team: ~"CI/CD", ~Discussion, ~Quality, ~Platform, etc.
- Milestone: ~Deliverable, ~Stretch, ~"Next Patch Release"
- Priority: ~P1, ~P2, ~P3, ~P4
- Severity: ~S1, ~S2, ~S3, ~S4
@@ -171,13 +171,13 @@ Examples of subject labels are ~wiki, ~"container registry", ~ldap, ~api,
Subject labels are always all-lowercase.
-### Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)
+### Team labels (~"CI/CD", ~Discussion, ~Quality, ~Platform, etc.)
Team labels specify what team is responsible for this issue.
Assigning a team label makes sure issues get the attention of the appropriate
people.
-The current team labels are ~Build, ~"CI/CD", ~Discussion, ~Documentation, ~Edge,
+The current team labels are ~Build, ~"CI/CD", ~Discussion, ~Documentation, ~Quality,
~Geo, ~Gitaly, ~Monitoring, ~Platform, ~Release, ~"Security Products" and ~"UX".
The descriptions on the [labels page][labels-page] explain what falls under the
@@ -199,12 +199,12 @@ release. There are three levels of Milestone labels:
- ~Stretch: Issues that are a stretch goal for delivering in the current
milestone. If these issues are not done in the current release, they will
strongly be considered for the next release.
-- ~"Next Patch Release": Issues to put in the next patch release. Work on these
+- ~"Next Patch Release": Issues to put in the next patch release. Work on these
first, and add the "Pick Into X" label to the merge request, along with the
appropriate milestone.
Each issue scheduled for the current milestone should be labeled ~Deliverable
-or ~"Stretch". Any open issue for a previous milestone should be labeled
+or ~"Stretch". Any open issue for a previous milestone should be labeled
~"Next Patch Release", or otherwise rescheduled to a different milestone.
### Bug Priority labels (~P1, ~P2, ~P3 & etc.)
@@ -221,16 +221,16 @@ This label documents the planned timeline & urgency which is used to measure aga
#### Specific Priority guidance
-| Label | Availability / Performance |
+| Label | Availability / Performance |
|-------|--------------------------------------------------------------|
-| ~P1 | |
+| ~P1 | |
| ~P2 | The issue is (almost) guaranteed to occur in the near future |
| ~P3 | The issue is likely to occur in the near future |
| ~P4 | The issue _may_ occur but it's not likely |
### Bug Severity labels (~S1, ~S2, ~S3 & etc.)
-Severity labels help us clearly communicate the impact of a ~bug on users.
+Severity labels help us clearly communicate the impact of a ~bug on users.
| Label | Meaning | Impact of the defect | Example |
|-------|-------------------|-------------------------------------------------------|---------|
@@ -241,9 +241,9 @@ Severity labels help us clearly communicate the impact of a ~bug on users.
#### Specific Severity guidance
-| Label | Security Impact |
+| Label | Security Impact |
|-------|-------------------------------------------------------------------|
-| ~S1 | >50% customers impacted (possible company extinction level event) |
+| ~S1 | >50% customers impacted (possible company extinction level event) |
| ~S2 | Multiple customers impacted (but not apocalyptic) |
| ~S3 | A single customer impacted |
| ~S4 | No customer impact, or expected impact within 30 days |
@@ -727,4 +727,3 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[^1]: Please note that specs other than JavaScript specs are considered backend
code.
- \ No newline at end of file
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 483b7719418..cf22efd819d 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.96.1
+0.96.2
diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION
index a3df0a6959e..f374f6662e9 100644
--- a/GITLAB_PAGES_VERSION
+++ b/GITLAB_PAGES_VERSION
@@ -1 +1 @@
-0.8.0
+0.9.1
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index ee74734aa22..6aba2b245a8 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-4.1.0
+4.2.0
diff --git a/Gemfile b/Gemfile
index 71f27e0f6de..a68b044b39e 100644
--- a/Gemfile
+++ b/Gemfile
@@ -51,7 +51,6 @@ gem 'omniauth-shibboleth', '~> 1.2.0'
gem 'omniauth-twitter', '~> 1.4'
gem 'omniauth_crowd', '~> 2.2.0'
gem 'omniauth-authentiq', '~> 0.3.1'
-gem 'omniauth-jwt', '~> 0.0.2'
gem 'rack-oauth2', '~> 1.2.1'
gem 'jwt', '~> 1.5.6'
@@ -185,6 +184,9 @@ gem 're2', '~> 1.1.1'
gem 'version_sorter', '~> 2.1.0'
+# User agent parsing
+gem 'device_detector'
+
# Cache
gem 'redis-rails', '~> 5.0.2'
@@ -283,7 +285,6 @@ gem 'batch-loader', '~> 1.2.1'
gem 'peek', '~> 1.0.1'
gem 'peek-gc', '~> 0.0.2'
gem 'peek-mysql2', '~> 1.1.0', group: :mysql
-gem 'peek-performance_bar', '~> 1.3.0'
gem 'peek-pg', '~> 1.3.0', group: :postgres
gem 'peek-rblineprof', '~> 0.2.0'
gem 'peek-redis', '~> 1.2.0'
@@ -416,7 +417,7 @@ end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.97.0', require: 'gitaly'
-gem 'grpc', '~> 1.10.0'
+gem 'grpc', '~> 1.11.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed
gem 'google-protobuf', '= 3.5.1'
diff --git a/Gemfile.lock b/Gemfile.lock
index d5e1c428e25..f11df6a283e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -161,6 +161,7 @@ GEM
activerecord (>= 3.2.0, < 5.1)
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
+ device_detector (1.0.0)
devise (4.2.0)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
@@ -374,7 +375,7 @@ GEM
rake
grape_logging (1.7.0)
grape
- grpc (1.10.0)
+ grpc (1.11.0)
google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0)
googleauth (>= 0.5.1, < 0.7)
@@ -555,9 +556,6 @@ GEM
jwt (>= 1.5)
omniauth (>= 1.1.1)
omniauth-oauth2 (>= 1.5)
- omniauth-jwt (0.0.2)
- jwt
- omniauth (~> 1.1)
omniauth-kerberos (0.3.0)
omniauth-multipassword
timfel-krb5-auth (~> 0.8)
@@ -603,8 +601,6 @@ GEM
atomic (>= 1.0.0)
mysql2
peek
- peek-performance_bar (1.3.1)
- peek (>= 0.1.0)
peek-pg (1.3.0)
concurrent-ruby
concurrent-ruby-ext
@@ -1031,6 +1027,7 @@ DEPENDENCIES
database_cleaner (~> 1.5.0)
deckar01-task_list (= 2.0.0)
default_value_for (~> 3.0.0)
+ device_detector
devise (~> 4.2)
devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0)
@@ -1078,7 +1075,7 @@ DEPENDENCIES
grape-entity (~> 0.6.0)
grape-route-helpers (~> 2.1.0)
grape_logging (~> 1.7)
- grpc (~> 1.10.0)
+ grpc (~> 1.11.0)
haml_lint (~> 0.26.0)
hamlit (~> 2.6.1)
hashie-forbidden_attributes
@@ -1119,7 +1116,6 @@ DEPENDENCIES
omniauth-github (~> 1.1.1)
omniauth-gitlab (~> 1.0.2)
omniauth-google-oauth2 (~> 0.5.3)
- omniauth-jwt (~> 0.0.2)
omniauth-kerberos (~> 0.3.0)
omniauth-oauth2-generic (~> 0.2.2)
omniauth-saml (~> 1.10)
@@ -1130,7 +1126,6 @@ DEPENDENCIES
peek (~> 1.0.1)
peek-gc (~> 0.0.2)
peek-mysql2 (~> 1.1.0)
- peek-performance_bar (~> 1.3.0)
peek-pg (~> 1.3.0)
peek-rblineprof (~> 0.2.0)
peek-redis (~> 1.2.0)
diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock
index a0330cbdd02..10d5cb6a23f 100644
--- a/Gemfile.rails5.lock
+++ b/Gemfile.rails5.lock
@@ -304,12 +304,12 @@ GEM
flowdock (~> 0.7)
gitlab-grit (>= 2.4.1)
multi_json
- gitlab-gollum-lib (4.2.7.1)
+ gitlab-gollum-lib (4.2.7.2)
gemojione (~> 3.2)
github-markup (~> 1.6)
gollum-grit_adapter (~> 1.0)
nokogiri (>= 1.6.1, < 2.0)
- rouge (~> 2.1)
+ rouge (~> 3.1)
sanitize (~> 2.1)
stringex (~> 2.6)
gitlab-gollum-rugged_adapter (0.4.4)
@@ -602,8 +602,6 @@ GEM
atomic (>= 1.0.0)
mysql2
peek
- peek-performance_bar (1.3.1)
- peek (>= 0.1.0)
peek-pg (1.3.0)
concurrent-ruby
concurrent-ruby-ext
@@ -752,7 +750,7 @@ GEM
retriable (3.1.1)
rinku (2.0.4)
rotp (2.1.2)
- rouge (2.2.1)
+ rouge (3.1.1)
rqrcode (0.10.1)
chunky_png (~> 1.0)
rqrcode-rails3 (0.1.7)
@@ -1134,7 +1132,6 @@ DEPENDENCIES
peek (~> 1.0.1)
peek-gc (~> 0.0.2)
peek-mysql2 (~> 1.1.0)
- peek-performance_bar (~> 1.3.0)
peek-pg (~> 1.3.0)
peek-rblineprof (~> 0.2.0)
peek-redis (~> 1.2.0)
@@ -1166,7 +1163,7 @@ DEPENDENCIES
redis-rails (~> 5.0.2)
request_store (~> 1.3)
responders (~> 2.0)
- rouge (~> 2.0)
+ rouge (~> 3.1)
rqrcode-rails3 (~> 0.1.7)
rspec-parameterized
rspec-rails (~> 3.6.0)
diff --git a/app/assets/javascripts/behaviors/gl_emoji.js b/app/assets/javascripts/behaviors/gl_emoji.js
index 7e98e04303a..56293d5f96f 100644
--- a/app/assets/javascripts/behaviors/gl_emoji.js
+++ b/app/assets/javascripts/behaviors/gl_emoji.js
@@ -7,27 +7,24 @@ export default function installGlEmojiElement() {
const GlEmojiElementProto = Object.create(HTMLElement.prototype);
GlEmojiElementProto.createdCallback = function createdCallback() {
const emojiUnicode = this.textContent.trim();
- const {
- name,
- unicodeVersion,
- fallbackSrc,
- fallbackSpriteClass,
- } = this.dataset;
+ const { name, unicodeVersion, fallbackSrc, fallbackSpriteClass } = this.dataset;
- const isEmojiUnicode = this.childNodes && Array.prototype.every.call(
- this.childNodes,
- childNode => childNode.nodeType === 3,
- );
+ const isEmojiUnicode =
+ this.childNodes &&
+ Array.prototype.every.call(this.childNodes, childNode => childNode.nodeType === 3);
const hasImageFallback = fallbackSrc && fallbackSrc.length > 0;
const hasCssSpriteFalback = fallbackSpriteClass && fallbackSpriteClass.length > 0;
- if (
- emojiUnicode &&
- isEmojiUnicode &&
- !isEmojiUnicodeSupported(emojiUnicode, unicodeVersion)
- ) {
+ if (emojiUnicode && isEmojiUnicode && !isEmojiUnicodeSupported(emojiUnicode, unicodeVersion)) {
// CSS sprite fallback takes precedence over image fallback
if (hasCssSpriteFalback) {
+ if (!gon.emoji_sprites_css_added && gon.emoji_sprites_css_path) {
+ const emojiSpriteLinkTag = document.createElement('link');
+ emojiSpriteLinkTag.setAttribute('rel', 'stylesheet');
+ emojiSpriteLinkTag.setAttribute('href', gon.emoji_sprites_css_path);
+ document.head.appendChild(emojiSpriteLinkTag);
+ gon.emoji_sprites_css_added = true;
+ }
// IE 11 doesn't like adding multiple at once :(
this.classList.add('emoji-icon');
this.classList.add(fallbackSpriteClass);
diff --git a/app/assets/javascripts/compare.js b/app/assets/javascripts/compare.js
deleted file mode 100644
index 303a5bf4a53..00000000000
--- a/app/assets/javascripts/compare.js
+++ /dev/null
@@ -1,86 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, max-len */
-
-import $ from 'jquery';
-import { localTimeAgo } from './lib/utils/datetime_utility';
-import axios from './lib/utils/axios_utils';
-
-export default class Compare {
- constructor(opts) {
- this.opts = opts;
- this.source_loading = $(".js-source-loading");
- this.target_loading = $(".js-target-loading");
- $('.js-compare-dropdown').each((function(_this) {
- return function(i, dropdown) {
- var $dropdown;
- $dropdown = $(dropdown);
- return $dropdown.glDropdown({
- selectable: true,
- fieldName: $dropdown.data('fieldName'),
- filterable: true,
- id: function(obj, $el) {
- return $el.data('id');
- },
- toggleLabel: function(obj, $el) {
- return $el.text().trim();
- },
- clicked: function(e, el) {
- if ($dropdown.is('.js-target-branch')) {
- return _this.getTargetHtml();
- } else if ($dropdown.is('.js-source-branch')) {
- return _this.getSourceHtml();
- } else if ($dropdown.is('.js-target-project')) {
- return _this.getTargetProject();
- }
- }
- });
- };
- })(this));
- this.initialState();
- }
-
- initialState() {
- this.getSourceHtml();
- this.getTargetHtml();
- }
-
- getTargetProject() {
- $('.mr_target_commit').empty();
-
- return axios.get(this.opts.targetProjectUrl, {
- params: {
- target_project_id: $("input[name='merge_request[target_project_id]']").val(),
- },
- }).then(({ data }) => {
- $('.js-target-branch-dropdown .dropdown-content').html(data);
- });
- }
-
- getSourceHtml() {
- return this.constructor.sendAjax(this.opts.sourceBranchUrl, this.source_loading, '.mr_source_commit', {
- ref: $("input[name='merge_request[source_branch]']").val()
- });
- }
-
- getTargetHtml() {
- return this.constructor.sendAjax(this.opts.targetBranchUrl, this.target_loading, '.mr_target_commit', {
- target_project_id: $("input[name='merge_request[target_project_id]']").val(),
- ref: $("input[name='merge_request[target_branch]']").val()
- });
- }
-
- static sendAjax(url, loading, target, params) {
- const $target = $(target);
-
- loading.show();
- $target.empty();
-
- return axios.get(url, {
- params,
- }).then(({ data }) => {
- loading.hide();
- $target.html(data);
- const className = '.' + $target[0].className.replace(' ', '.');
- localTimeAgo($('.js-timeago', className));
- });
- }
-}
diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js
index 260c91cac24..9c88466e576 100644
--- a/app/assets/javascripts/compare_autocomplete.js
+++ b/app/assets/javascripts/compare_autocomplete.js
@@ -4,8 +4,9 @@ import $ from 'jquery';
import { __ } from './locale';
import axios from './lib/utils/axios_utils';
import flash from './flash';
+import { capitalizeFirstCharacter } from './lib/utils/text_utility';
-export default function initCompareAutocomplete() {
+export default function initCompareAutocomplete(limitTo = null, clickHandler = () => {}) {
$('.js-compare-dropdown').each(function() {
var $dropdown, selected;
$dropdown = $(this);
@@ -15,14 +16,27 @@ export default function initCompareAutocomplete() {
const $filterInput = $('input[type="search"]', $dropdownContainer);
$dropdown.glDropdown({
data: function(term, callback) {
- axios.get($dropdown.data('refsUrl'), {
- params: {
- ref: $dropdown.data('ref'),
- search: term,
- },
- }).then(({ data }) => {
- callback(data);
- }).catch(() => flash(__('Error fetching refs')));
+ const params = {
+ ref: $dropdown.data('ref'),
+ search: term,
+ };
+
+ if (limitTo) {
+ params.find = limitTo;
+ }
+
+ axios
+ .get($dropdown.data('refsUrl'), {
+ params,
+ })
+ .then(({ data }) => {
+ if (limitTo) {
+ callback(data[capitalizeFirstCharacter(limitTo)] || []);
+ } else {
+ callback(data);
+ }
+ })
+ .catch(() => flash(__('Error fetching refs')));
},
selectable: true,
filterable: true,
@@ -32,9 +46,15 @@ export default function initCompareAutocomplete() {
renderRow: function(ref) {
var link;
if (ref.header != null) {
- return $('<li />').addClass('dropdown-header').text(ref.header);
+ return $('<li />')
+ .addClass('dropdown-header')
+ .text(ref.header);
} else {
- link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref));
+ link = $('<a />')
+ .attr('href', '#')
+ .addClass(ref === selected ? 'is-active' : '')
+ .text(ref)
+ .attr('data-ref', escape(ref));
return $('<li />').append(link);
}
},
@@ -43,9 +63,10 @@ export default function initCompareAutocomplete() {
},
toggleLabel: function(obj, $el) {
return $el.text().trim();
- }
+ },
+ clicked: () => clickHandler($dropdown),
});
- $filterInput.on('keyup', (e) => {
+ $filterInput.on('keyup', e => {
const keyCode = e.keyCode || e.which;
if (keyCode !== 13) return;
const text = $filterInput.val();
@@ -54,7 +75,7 @@ export default function initCompareAutocomplete() {
$dropdownContainer.removeClass('open');
});
- $dropdownContainer.on('click', '.dropdown-content a', (e) => {
+ $dropdownContainer.on('click', '.dropdown-content a', e => {
$dropdown.prop('title', e.target.text.replace(/_+?/g, '-'));
if ($dropdown.hasClass('has-tooltip')) {
$dropdown.tooltip('fixTitle');
diff --git a/app/assets/javascripts/emoji/index.js b/app/assets/javascripts/emoji/index.js
index dc7672560ea..cd8dff40b88 100644
--- a/app/assets/javascripts/emoji/index.js
+++ b/app/assets/javascripts/emoji/index.js
@@ -34,7 +34,7 @@ export function getEmojiCategoryMap() {
symbols: [],
flags: [],
};
- Object.keys(emojiMap).forEach((name) => {
+ Object.keys(emojiMap).forEach(name => {
const emoji = emojiMap[name];
if (emojiCategoryMap[emoji.category]) {
emojiCategoryMap[emoji.category].push(name);
@@ -79,7 +79,9 @@ export function glEmojiTag(inputName, options) {
classList.push(fallbackSpriteClass);
}
const classAttribute = classList.length > 0 ? `class="${classList.join(' ')}"` : '';
- const fallbackSpriteAttribute = opts.sprite ? `data-fallback-sprite-class="${fallbackSpriteClass}"` : '';
+ const fallbackSpriteAttribute = opts.sprite
+ ? `data-fallback-sprite-class="${fallbackSpriteClass}"`
+ : '';
let contents = emojiInfo.moji;
if (opts.forceFallback && !opts.sprite) {
contents = emojiImageTag(name, fallbackImageSrc);
diff --git a/app/assets/javascripts/emoji/support/unicode_support_map.js b/app/assets/javascripts/emoji/support/unicode_support_map.js
index c18d07dad43..8c1861c56db 100644
--- a/app/assets/javascripts/emoji/support/unicode_support_map.js
+++ b/app/assets/javascripts/emoji/support/unicode_support_map.js
@@ -54,7 +54,8 @@ const unicodeSupportTestMap = {
function checkPixelInImageDataArray(pixelOffset, imageDataArray) {
// `4 *` because RGBA
const indexOffset = 4 * pixelOffset;
- const hasColor = imageDataArray[indexOffset + 0] ||
+ const hasColor =
+ imageDataArray[indexOffset + 0] ||
imageDataArray[indexOffset + 1] ||
imageDataArray[indexOffset + 2];
const isVisible = imageDataArray[indexOffset + 3];
@@ -75,23 +76,23 @@ const chromeVersion = chromeMatches && chromeMatches[1] && parseInt(chromeMatche
const fontSize = 16;
function generateUnicodeSupportMap(testMap) {
const testMapKeys = Object.keys(testMap);
- const numTestEntries = testMapKeys
- .reduce((list, testKey) => list.concat(testMap[testKey]), []).length;
+ const numTestEntries = testMapKeys.reduce((list, testKey) => list.concat(testMap[testKey]), [])
+ .length;
const canvas = document.createElement('canvas');
(window.gl || window).testEmojiUnicodeSupportMapCanvas = canvas;
const ctx = canvas.getContext('2d');
- canvas.width = (2 * fontSize);
- canvas.height = (numTestEntries * fontSize);
+ canvas.width = 2 * fontSize;
+ canvas.height = numTestEntries * fontSize;
ctx.fillStyle = '#000000';
ctx.textBaseline = 'middle';
ctx.font = `${fontSize}px "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`;
// Write each emoji to the canvas vertically
let writeIndex = 0;
- testMapKeys.forEach((testKey) => {
+ testMapKeys.forEach(testKey => {
const testEntry = testMap[testKey];
- [].concat(testEntry).forEach((emojiUnicode) => {
- ctx.fillText(emojiUnicode, 0, (writeIndex * fontSize) + (fontSize / 2));
+ [].concat(testEntry).forEach(emojiUnicode => {
+ ctx.fillText(emojiUnicode, 0, writeIndex * fontSize + fontSize / 2);
writeIndex += 1;
});
});
@@ -99,29 +100,25 @@ function generateUnicodeSupportMap(testMap) {
// Read from the canvas
const resultMap = {};
let readIndex = 0;
- testMapKeys.forEach((testKey) => {
+ testMapKeys.forEach(testKey => {
const testEntry = testMap[testKey];
// This needs to be a `reduce` instead of `every` because we need to
// keep the `readIndex` in sync from the writes by running all entries
- const isTestSatisfied = [].concat(testEntry).reduce((isSatisfied) => {
+ const isTestSatisfied = [].concat(testEntry).reduce(isSatisfied => {
// Sample along the vertical-middle for a couple of characters
- const imageData = ctx.getImageData(
- 0,
- (readIndex * fontSize) + (fontSize / 2),
- 2 * fontSize,
- 1,
- ).data;
+ const imageData = ctx.getImageData(0, readIndex * fontSize + fontSize / 2, 2 * fontSize, 1)
+ .data;
let isValidEmoji = false;
for (let currentPixel = 0; currentPixel < 64; currentPixel += 1) {
const isLookingAtFirstChar = currentPixel < fontSize;
- const isLookingAtSecondChar = currentPixel >= (fontSize + (fontSize / 2));
+ const isLookingAtSecondChar = currentPixel >= fontSize + fontSize / 2;
// Check for the emoji somewhere along the row
if (isLookingAtFirstChar && checkPixelInImageDataArray(currentPixel, imageData)) {
isValidEmoji = true;
- // Check to see that nothing is rendered next to the first character
- // to ensure that the ZWJ sequence rendered as one piece
+ // Check to see that nothing is rendered next to the first character
+ // to ensure that the ZWJ sequence rendered as one piece
} else if (isLookingAtSecondChar && checkPixelInImageDataArray(currentPixel, imageData)) {
isValidEmoji = false;
break;
@@ -170,7 +167,10 @@ export default function getUnicodeSupportMap() {
if (isLocalStorageAvailable) {
window.localStorage.setItem('gl-emoji-version', GL_EMOJI_VERSION);
window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent);
- window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap));
+ window.localStorage.setItem(
+ 'gl-emoji-unicode-support-map',
+ JSON.stringify(unicodeSupportMap),
+ );
}
}
diff --git a/app/assets/javascripts/ide/components/changed_file_icon.vue b/app/assets/javascripts/ide/components/changed_file_icon.vue
index 1fc11c84639..fdbc14a4b8f 100644
--- a/app/assets/javascripts/ide/components/changed_file_icon.vue
+++ b/app/assets/javascripts/ide/components/changed_file_icon.vue
@@ -26,11 +26,18 @@ export default {
required: false,
default: false,
},
+ forceModifiedIcon: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
changedIcon() {
const suffix = this.file.staged && !this.showStagedIcon ? '-solid' : '';
- return this.file.tempFile ? `file-addition${suffix}` : `file-modified${suffix}`;
+ return this.file.tempFile && !this.forceModifiedIcon
+ ? `file-addition${suffix}`
+ : `file-modified${suffix}`;
},
stagedIcon() {
return `${this.changedIcon}-solid`;
diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue
index 769e9b79cad..b1b5c0d4a28 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/index.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue
@@ -1,49 +1,54 @@
<script>
- import { mapActions } from 'vuex';
- import icon from '~/vue_shared/components/icon.vue';
- import newModal from './modal.vue';
- import upload from './upload.vue';
+import { mapActions } from 'vuex';
+import icon from '~/vue_shared/components/icon.vue';
+import newModal from './modal.vue';
+import upload from './upload.vue';
- export default {
- components: {
- icon,
- newModal,
- upload,
+export default {
+ components: {
+ icon,
+ newModal,
+ upload,
+ },
+ props: {
+ branch: {
+ type: String,
+ required: true,
},
- props: {
- branch: {
- type: String,
- required: true,
- },
- path: {
- type: String,
- required: true,
- },
+ path: {
+ type: String,
+ required: true,
},
- data() {
- return {
- openModal: false,
- modalType: '',
- dropdownOpen: false,
- };
+ },
+ data() {
+ return {
+ openModal: false,
+ modalType: '',
+ dropdownOpen: false,
+ };
+ },
+ watch: {
+ dropdownOpen() {
+ this.$nextTick(() => {
+ this.$refs.dropdownMenu.scrollIntoView();
+ });
},
- methods: {
- ...mapActions([
- 'createTempEntry',
- ]),
- createNewItem(type) {
- this.modalType = type;
- this.openModal = true;
- this.dropdownOpen = false;
- },
- hideModal() {
- this.openModal = false;
- },
- openDropdown() {
- this.dropdownOpen = !this.dropdownOpen;
- },
+ },
+ methods: {
+ ...mapActions(['createTempEntry']),
+ createNewItem(type) {
+ this.modalType = type;
+ this.openModal = true;
+ this.dropdownOpen = false;
},
- };
+ hideModal() {
+ this.openModal = false;
+ },
+ openDropdown() {
+ this.dropdownOpen = !this.dropdownOpen;
+ },
+ },
+};
</script>
<template>
@@ -71,7 +76,10 @@
css-classes="pull-left"
/>
</button>
- <ul class="dropdown-menu dropdown-menu-right">
+ <ul
+ class="dropdown-menu dropdown-menu-right"
+ ref="dropdownMenu"
+ >
<li>
<a
href="#"
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index 4b5a50785b6..a95a0225950 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -40,13 +40,6 @@ export default {
return __('Create file');
},
- formLabelName() {
- if (this.type === 'tree') {
- return __('Directory name');
- }
-
- return __('File name');
- },
},
mounted() {
this.$refs.fieldName.focus();
@@ -82,8 +75,8 @@ export default {
@submit.prevent="createEntryInStore"
>
<fieldset class="form-group append-bottom-0">
- <label class="label-light col-sm-3">
- {{ formLabelName }}
+ <label class="label-light col-sm-3 ide-new-modal-label">
+ {{ __('Name') }}
</label>
<div class="col-sm-9">
<input
diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue
index 8b18c7d28b4..e86db2da4a6 100644
--- a/app/assets/javascripts/ide/components/repo_file.vue
+++ b/app/assets/javascripts/ide/components/repo_file.vue
@@ -97,7 +97,7 @@ export default {
:file="file"
/>
</span>
- <span class="pull-right">
+ <span class="pull-right ide-file-icon-holder">
<mr-file-icon
v-if="file.mrChange"
/>
@@ -106,7 +106,8 @@ export default {
:file="file"
:show-tooltip="true"
:show-staged-icon="true"
- class="prepend-top-5 pull-right"
+ :force-modified-icon="true"
+ class="pull-right"
/>
</span>
<new-dropdown
diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue
index 35a362b01e0..a3ee3184c19 100644
--- a/app/assets/javascripts/ide/components/repo_tab.vue
+++ b/app/assets/javascripts/ide/components/repo_tab.vue
@@ -84,6 +84,7 @@ export default {
<changed-file-icon
v-else
:file="tab"
+ :force-modified-icon="true"
/>
</button>
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index cbe43f5f7f2..4c8c997e376 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -33,10 +33,7 @@ export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
}
};
-export const toggleRightPanelCollapsed = (
- { dispatch, state },
- e = undefined,
-) => {
+export const toggleRightPanelCollapsed = ({ dispatch, state }, e = undefined) => {
if (e) {
$(e.currentTarget)
.tooltip('hide')
@@ -77,7 +74,7 @@ export const createTempEntry = (
}
worker.addEventListener('message', ({ data }) => {
- const { file } = data;
+ const { file, parentPath } = data;
worker.terminate();
@@ -93,6 +90,10 @@ export const createTempEntry = (
dispatch('setFileActive', file.path);
}
+ if (parentPath && !state.entries[parentPath].opened) {
+ commit(types.TOGGLE_TREE_OPEN, parentPath);
+ }
+
resolve(file);
});
@@ -137,6 +138,14 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
commit(types.UPDATE_DELAY_VIEWER_CHANGE, delay);
};
+export const updateTempFlagForEntry = ({ commit, dispatch, state }, { file, tempFile }) => {
+ commit(types.UPDATE_TEMP_FLAG, { path: file.path, tempFile });
+
+ if (file.parentPath) {
+ dispatch('updateTempFlagForEntry', { file: state.entries[file.parentPath], tempFile });
+ }
+};
+
export const toggleFileFinder = ({ commit }, fileFindVisible) =>
commit(types.TOGGLE_FILE_FINDER, fileFindVisible);
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index d782e0a84d2..fcdb3b753b2 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -63,7 +63,7 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive
const file = state.entries[path];
commit(types.TOGGLE_LOADING, { entry: file });
return service
- .getFileData(file.url)
+ .getFileData(`${gon.relative_url_root ? gon.relative_url_root : ''}${file.url}`)
.then(res => {
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
setPageTitle(pageTitle);
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index 119debaf5f3..349ff68f1e3 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -110,6 +110,17 @@ export const updateFilesAfterCommit = (
{ root: true },
);
+ commit(
+ rootTypes.TOGGLE_FILE_CHANGED,
+ {
+ file,
+ changed: false,
+ },
+ { root: true },
+ );
+
+ dispatch('updateTempFlagForEntry', { file, tempFile: false }, { root: true });
+
eventHub.$emit(`editor.update.model.content.${file.key}`, {
content: file.content,
changed: !!changedFile,
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index c7f08449d03..f5c12db6db0 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -59,4 +59,5 @@ export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT';
export const ADD_PENDING_TAB = 'ADD_PENDING_TAB';
export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB';
+export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG';
export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index 2a6c136aeed..0c1d720df09 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -4,6 +4,7 @@ import mergeRequestMutation from './mutations/merge_request';
import fileMutations from './mutations/file';
import treeMutations from './mutations/tree';
import branchMutations from './mutations/branch';
+import { sortTree } from './utils';
export default {
[types.SET_INITIAL_DATA](state, data) {
@@ -73,7 +74,7 @@ export default {
f => foundEntry.tree.find(e => e.path === f.path) === undefined,
);
Object.assign(foundEntry, {
- tree: foundEntry.tree.concat(tree),
+ tree: sortTree(foundEntry.tree.concat(tree)),
});
}
@@ -86,10 +87,16 @@ export default {
if (!foundEntry) {
Object.assign(state.trees[`${projectId}/${branchId}`], {
- tree: state.trees[`${projectId}/${branchId}`].tree.concat(data.treeList),
+ tree: sortTree(state.trees[`${projectId}/${branchId}`].tree.concat(data.treeList)),
});
}
},
+ [types.UPDATE_TEMP_FLAG](state, { path, tempFile }) {
+ Object.assign(state.entries[path], {
+ tempFile,
+ changed: tempFile,
+ });
+ },
[types.UPDATE_VIEWER](state, viewer) {
Object.assign(state, {
viewer,
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 86612d845e0..59185f8f0ad 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -33,6 +33,7 @@ export const dataStructure = () => ({
raw: '',
content: '',
parentTreeUrl: '',
+ parentPath: '',
renderError: false,
base64: false,
editorRow: 1,
@@ -65,6 +66,7 @@ export const decorateData = entity => {
previewMode,
file_lock,
html,
+ parentPath = '',
} = entity;
return {
@@ -81,6 +83,7 @@ export const decorateData = entity => {
opened,
active,
parentTreeUrl,
+ parentPath,
changed,
renderError,
content,
@@ -121,8 +124,8 @@ const sortTreesByTypeAndName = (a, b) => {
} else if (a.type === 'blob' && b.type === 'tree') {
return 1;
}
- if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
- if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
+ if (a.name < b.name) return -1;
+ if (a.name > b.name) return 1;
return 0;
};
diff --git a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js
index a1673276900..d249b05f47c 100644
--- a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js
+++ b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js
@@ -6,6 +6,7 @@ self.addEventListener('message', e => {
const treeList = [];
let file;
+ let parentPath;
const entries = data.reduce((acc, path) => {
const pathSplit = path.split('/');
const blobName = pathSplit.pop().trim();
@@ -17,6 +18,8 @@ self.addEventListener('message', e => {
const foundEntry = acc[folderPath];
if (!foundEntry) {
+ parentPath = parentFolder ? parentFolder.path : null;
+
const tree = decorateData({
projectId,
branchId,
@@ -29,6 +32,7 @@ self.addEventListener('message', e => {
tempFile,
changed: tempFile,
opened: tempFile,
+ parentPath,
});
Object.assign(acc, {
@@ -52,6 +56,8 @@ self.addEventListener('message', e => {
if (blobName !== '') {
const fileFolder = acc[pathSplit.join('/')];
+ parentPath = fileFolder ? fileFolder.path : null;
+
file = decorateData({
projectId,
branchId,
@@ -66,6 +72,7 @@ self.addEventListener('message', e => {
content,
base64,
previewMode: viewerInformationForPath(blobName),
+ parentPath,
});
Object.assign(acc, {
@@ -86,5 +93,6 @@ self.addEventListener('message', e => {
entries,
treeList: sortTree(treeList),
file,
+ parentPath,
});
});
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 7e9a50a885d..f8b3d3061f0 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -12,7 +12,8 @@ import ModalStore from './boards/stores/modal_store';
export default class MilestoneSelect {
constructor(currentProject, els, options = {}) {
if (currentProject !== null) {
- this.currentProject = typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject;
+ this.currentProject =
+ typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject;
}
this.init(els, options);
@@ -26,7 +27,10 @@ export default class MilestoneSelect {
}
$els.each((i, dropdown) => {
- let milestoneLinkNoneTemplate, milestoneLinkTemplate, selectedMilestone, selectedMilestoneDefault;
+ let milestoneLinkNoneTemplate,
+ milestoneLinkTemplate,
+ selectedMilestone,
+ selectedMilestoneDefault;
const $dropdown = $(dropdown);
const projectId = $dropdown.data('projectId');
const milestonesUrl = $dropdown.data('milestones');
@@ -46,45 +50,47 @@ export default class MilestoneSelect {
const $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon');
const $value = $block.find('.value');
const $loading = $block.find('.block-loading').fadeOut();
- selectedMilestoneDefault = (showAny ? '' : null);
- selectedMilestoneDefault = (showNo && defaultNo ? 'No Milestone' : selectedMilestoneDefault);
+ selectedMilestoneDefault = showAny ? '' : null;
+ selectedMilestoneDefault = showNo && defaultNo ? 'No Milestone' : selectedMilestoneDefault;
selectedMilestone = $dropdown.data('selected') || selectedMilestoneDefault;
if (issueUpdateURL) {
- milestoneLinkTemplate = _.template('<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>');
+ milestoneLinkTemplate = _.template(
+ '<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>',
+ );
milestoneLinkNoneTemplate = '<span class="no-value">None</span>';
}
return $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
- data: (term, callback) => axios.get(milestonesUrl)
- .then(({ data }) => {
+ data: (term, callback) =>
+ axios.get(milestonesUrl).then(({ data }) => {
const extraOptions = [];
if (showAny) {
extraOptions.push({
- id: 0,
- name: '',
- title: 'Any Milestone'
+ id: null,
+ name: null,
+ title: 'Any Milestone',
});
}
if (showNo) {
extraOptions.push({
id: -1,
name: 'No Milestone',
- title: 'No Milestone'
+ title: 'No Milestone',
});
}
if (showUpcoming) {
extraOptions.push({
id: -2,
name: '#upcoming',
- title: 'Upcoming'
+ title: 'Upcoming',
});
}
if (showStarted) {
extraOptions.push({
id: -3,
name: '#started',
- title: 'Started'
+ title: 'Started',
});
}
if (extraOptions.length) {
@@ -106,7 +112,7 @@ export default class MilestoneSelect {
`,
filterable: true,
search: {
- fields: ['title']
+ fields: ['title'],
},
selectable: true,
toggleLabel: (selected, el, e) => {
@@ -119,7 +125,7 @@ export default class MilestoneSelect {
defaultLabel: defaultLabel,
fieldName: $dropdown.data('fieldName'),
text: milestone => _.escape(milestone.title),
- id: (milestone) => {
+ id: milestone => {
if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) {
return milestone.name;
} else {
@@ -131,7 +137,7 @@ export default class MilestoneSelect {
// display:block overrides the hide-collapse rule
return $value.css('display', '');
},
- opened: (e) => {
+ opened: e => {
const $el = $(e.currentTarget);
if ($dropdown.hasClass('js-issue-board-sidebar') || options.handleClick) {
selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
@@ -140,7 +146,7 @@ export default class MilestoneSelect {
$(`[data-milestone-id="${_.escape(selectedMilestone)}"] > a`, $el).addClass('is-active');
},
vue: $dropdown.hasClass('js-issue-board-sidebar'),
- clicked: (clickEvent) => {
+ clicked: clickEvent => {
const { $el, e } = clickEvent;
let selected = clickEvent.selectedObj;
@@ -155,11 +161,14 @@ export default class MilestoneSelect {
const page = $('body').attr('data-page');
const isIssueIndex = page === 'projects:issues:index';
- const isMRIndex = (page === page && page === 'projects:merge_requests:index');
- const isSelecting = (selected.name !== selectedMilestone);
+ const isMRIndex = page === page && page === 'projects:merge_requests:index';
+ const isSelecting = selected.name !== selectedMilestone;
selectedMilestone = isSelecting ? selected.name : selectedMilestoneDefault;
- if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
+ if (
+ $dropdown.hasClass('js-filter-bulk-update') ||
+ $dropdown.hasClass('js-issuable-form-dropdown')
+ ) {
e.preventDefault();
return;
}
@@ -177,10 +186,13 @@ export default class MilestoneSelect {
return $dropdown.closest('form').submit();
} else if ($dropdown.hasClass('js-issue-board-sidebar')) {
if (selected.id !== -1 && isSelecting) {
- gl.issueBoards.boardStoreIssueSet('milestone', new ListMilestone({
- id: selected.id,
- title: selected.name
- }));
+ gl.issueBoards.boardStoreIssueSet(
+ 'milestone',
+ new ListMilestone({
+ id: selected.id,
+ title: selected.name,
+ }),
+ );
} else {
gl.issueBoards.boardStoreIssueDelete('milestone');
}
@@ -188,7 +200,8 @@ export default class MilestoneSelect {
$dropdown.trigger('loading.gl.dropdown');
$loading.removeClass('hidden').fadeIn();
- gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
+ gl.issueBoards.BoardsStore.detail.issue
+ .update($dropdown.attr('data-issue-update'))
.then(() => {
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
@@ -203,7 +216,8 @@ export default class MilestoneSelect {
data[abilityName].milestone_id = selected != null ? selected : null;
$loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown');
- return axios.put(issueUpdateURL, data)
+ return axios
+ .put(issueUpdateURL, data)
.then(({ data }) => {
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
@@ -215,7 +229,10 @@ export default class MilestoneSelect {
data.milestone.name = data.milestone.title;
$value.html(milestoneLinkTemplate(data.milestone));
return $sidebarCollapsedValue
- .attr('data-original-title', `${data.milestone.name}<br />${data.milestone.remaining}`)
+ .attr(
+ 'data-original-title',
+ `${data.milestone.name}<br />${data.milestone.remaining}`,
+ )
.find('span')
.text(data.milestone.title);
} else {
@@ -230,7 +247,7 @@ export default class MilestoneSelect {
$loading.fadeOut();
});
}
- }
+ },
});
});
}
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
index d492d1cd001..cbe4774a360 100644
--- a/app/assets/javascripts/notes/components/discussion_counter.vue
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -86,7 +86,7 @@ export default {
v-html="resolveSvg"
></span>
</span>
- <span class=".line-resolve-text">
+ <span class="line-resolve-text">
{{ resolvedDiscussionCount }}/{{ discussionCount }} {{ countText }} resolved
</span>
</div>
diff --git a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
index 0e3ac636661..9ce176744ba 100644
--- a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
+++ b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
@@ -52,16 +52,15 @@
text() {
const keepContributionsText = s__(`AdminArea|
You are about to permanently delete the user %{username}.
- This will delete all of the issues, merge requests, and groups linked to them.
+ Issues, merge requests, and groups linked to them will be transferred to a system-wide "Ghost-user".
To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead.
Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`);
const deleteContributionsText = s__(`AdminArea|
You are about to permanently delete the user %{username}.
- Issues, merge requests, and groups linked to them will be transferred to a system-wide "Ghost-user".
+ This will delete all of the issues, merge requests, and groups linked to them.
To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead.
Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`);
-
return sprintf(this.deleteContributions ? deleteContributionsText : keepContributionsText,
{
username: `<strong>${_.escape(this.username)}</strong>`,
diff --git a/app/assets/javascripts/pages/projects/compare/index.js b/app/assets/javascripts/pages/projects/compare/index.js
index d1c78bd61db..768da8fb236 100644
--- a/app/assets/javascripts/pages/projects/compare/index.js
+++ b/app/assets/javascripts/pages/projects/compare/index.js
@@ -1,3 +1,3 @@
import initCompareAutocomplete from '~/compare_autocomplete';
-document.addEventListener('DOMContentLoaded', initCompareAutocomplete);
+document.addEventListener('DOMContentLoaded', () => initCompareAutocomplete());
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js
new file mode 100644
index 00000000000..46f3f55a400
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js
@@ -0,0 +1,60 @@
+import $ from 'jquery';
+import { localTimeAgo } from '~/lib/utils/datetime_utility';
+import axios from '~/lib/utils/axios_utils';
+import initCompareAutocomplete from '~/compare_autocomplete';
+import initTargetProjectDropdown from './target_project_dropdown';
+
+const updateCommitList = (url, $loadingIndicator, $commitList, params) => {
+ $loadingIndicator.show();
+ $commitList.empty();
+
+ return axios
+ .get(url, {
+ params,
+ })
+ .then(({ data }) => {
+ $loadingIndicator.hide();
+ $commitList.html(data);
+ localTimeAgo($('.js-timeago', $commitList));
+ });
+};
+
+export default mrNewCompareNode => {
+ const { sourceBranchUrl, targetBranchUrl } = mrNewCompareNode.dataset;
+ initTargetProjectDropdown();
+
+ const updateSourceBranchCommitList = () =>
+ updateCommitList(
+ sourceBranchUrl,
+ $(mrNewCompareNode).find('.js-source-loading'),
+ $(mrNewCompareNode).find('.mr_source_commit'),
+ {
+ ref: $(mrNewCompareNode)
+ .find("input[name='merge_request[source_branch]']")
+ .val(),
+ },
+ );
+ const updateTargetBranchCommitList = () =>
+ updateCommitList(
+ targetBranchUrl,
+ $(mrNewCompareNode).find('.js-target-loading'),
+ $(mrNewCompareNode).find('.mr_target_commit'),
+ {
+ target_project_id: $(mrNewCompareNode)
+ .find("input[name='merge_request[target_project_id]']")
+ .val(),
+ ref: $(mrNewCompareNode)
+ .find("input[name='merge_request[target_branch]']")
+ .val(),
+ },
+ );
+ initCompareAutocomplete('branches', $dropdown => {
+ if ($dropdown.is('.js-target-branch')) {
+ updateTargetBranchCommitList();
+ } else if ($dropdown.is('.js-source-branch')) {
+ updateSourceBranchCommitList();
+ }
+ });
+ updateSourceBranchCommitList();
+ updateTargetBranchCommitList();
+};
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
index 6c9afddefac..01a0b4870c1 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
@@ -1,18 +1,15 @@
-import Compare from '~/compare';
import MergeRequest from '~/merge_request';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
+import initCompare from './compare';
document.addEventListener('DOMContentLoaded', () => {
const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare');
if (mrNewCompareNode) {
- new Compare({ // eslint-disable-line no-new
- targetProjectUrl: mrNewCompareNode.dataset.targetProjectUrl,
- sourceBranchUrl: mrNewCompareNode.dataset.sourceBranchUrl,
- targetBranchUrl: mrNewCompareNode.dataset.targetBranchUrl,
- });
+ initCompare(mrNewCompareNode);
} else {
const mrNewSubmitNode = document.querySelector('.js-merge-request-new-submit');
- new MergeRequest({ // eslint-disable-line no-new
+ // eslint-disable-next-line no-new
+ new MergeRequest({
action: mrNewSubmitNode.dataset.mrSubmitAction,
});
initPipelines();
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/target_project_dropdown.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/target_project_dropdown.js
new file mode 100644
index 00000000000..b72fe6681df
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/target_project_dropdown.js
@@ -0,0 +1,22 @@
+import $ from 'jquery';
+
+export default () => {
+ const $targetProjectDropdown = $('.js-target-project');
+ $targetProjectDropdown.glDropdown({
+ selectable: true,
+ fieldName: $targetProjectDropdown.data('fieldName'),
+ filterable: true,
+ id(obj, $el) {
+ return $el.data('id');
+ },
+ toggleLabel(obj, $el) {
+ return $el.text().trim();
+ },
+ clicked({ $el }) {
+ $('.mr_target_commit').empty();
+ const $targetBranchDropdown = $('.js-target-branch');
+ $targetBranchDropdown.data('refsUrl', $el.data('refsUrl'));
+ $targetBranchDropdown.data('glDropdown').clearMenu();
+ },
+ });
+};
diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js
index cbc2d80ee18..50d042fef29 100644
--- a/app/assets/javascripts/pages/users/activity_calendar.js
+++ b/app/assets/javascripts/pages/users/activity_calendar.js
@@ -188,11 +188,11 @@ export default class ActivityCalendar {
},
{
text: 'W',
- y: 29 + this.dayYPos(2),
+ y: 29 + this.dayYPos(3),
},
{
text: 'F',
- y: 29 + this.dayYPos(3),
+ y: 29 + this.dayYPos(5),
},
];
this.svg
diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
index 2fd1715ee79..8ffaa52d9e8 100644
--- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
+++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
@@ -5,7 +5,6 @@ import PerformanceBarService from '../services/performance_bar_service';
import detailedMetric from './detailed_metric.vue';
import requestSelector from './request_selector.vue';
import simpleMetric from './simple_metric.vue';
-import upstreamPerformanceBar from './upstream_performance_bar.vue';
import Flash from '../../flash';
@@ -14,7 +13,6 @@ export default {
detailedMetric,
requestSelector,
simpleMetric,
- upstreamPerformanceBar,
},
props: {
store: {
@@ -128,9 +126,6 @@ export default {
{{ currentRequest.details.host.hostname }}
</span>
</div>
- <upstream-performance-bar
- v-if="initialRequest && currentRequest.details"
- />
<detailed-metric
v-for="metric in $options.detailedMetrics"
:key="metric.metric"
diff --git a/app/assets/javascripts/performance_bar/components/upstream_performance_bar.vue b/app/assets/javascripts/performance_bar/components/upstream_performance_bar.vue
deleted file mode 100644
index 2b5915f381f..00000000000
--- a/app/assets/javascripts/performance_bar/components/upstream_performance_bar.vue
+++ /dev/null
@@ -1,20 +0,0 @@
-<script>
-export default {
- mounted() {
- const upstreamPerformanceBar = document
- .getElementById('peek-view-performance-bar')
- .cloneNode(true);
-
- upstreamPerformanceBar.classList.remove('hidden');
-
- this.$refs.wrapper.appendChild(upstreamPerformanceBar);
- },
-};
-</script>
-<template>
- <div
- id="peek-view-performance-bar-vue"
- class="view"
- ref="wrapper"
- ></div>
-</template>
diff --git a/app/assets/javascripts/performance_bar/index.js b/app/assets/javascripts/performance_bar/index.js
index a0ddf36a672..4a98aed7679 100644
--- a/app/assets/javascripts/performance_bar/index.js
+++ b/app/assets/javascripts/performance_bar/index.js
@@ -1,5 +1,3 @@
-import 'vendor/peek.performance_bar';
-
import Vue from 'vue';
import performanceBarApp from './components/performance_bar_app.vue';
import PerformanceBarStore from './stores/performance_bar_store';
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.js b/app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.js
deleted file mode 100644
index 38da76c6771..00000000000
--- a/app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.js
+++ /dev/null
@@ -1,10 +0,0 @@
-export default {
- name: 'time-tracking-no-tracking-pane',
- template: `
- <div class="time-tracking-no-tracking-pane">
- <span class="no-value">
- {{ __('No estimate or time spent') }}
- </span>
- </div>
- `,
-};
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.vue
new file mode 100644
index 00000000000..9228184df5b
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.vue
@@ -0,0 +1,13 @@
+<script>
+export default {
+ name: 'TimeTrackingNoTrackingPane',
+};
+</script>
+
+<template>
+ <div class="time-tracking-no-tracking-pane">
+ <span class="no-value">
+ {{ __('No estimate or time spent') }}
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
index 5626cccc022..2e1d6e9643a 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js
+++ b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
@@ -1,3 +1,4 @@
+<script>
import $ from 'jquery';
import _ from 'underscore';
@@ -10,14 +11,17 @@ import Mediator from '../../sidebar_mediator';
import eventHub from '../../event_hub';
export default {
+ components: {
+ IssuableTimeTracker,
+ },
data() {
return {
mediator: new Mediator(),
store: new Store(),
};
},
- components: {
- IssuableTimeTracker,
+ mounted() {
+ this.listenForQuickActions();
},
methods: {
listenForQuickActions() {
@@ -41,18 +45,17 @@ export default {
}
},
},
- mounted() {
- this.listenForQuickActions();
- },
- template: `
- <div class="block">
- <issuable-time-tracker
- :time_estimate="store.timeEstimate"
- :time_spent="store.totalTimeSpent"
- :human_time_estimate="store.humanTimeEstimate"
- :human_time_spent="store.humanTotalTimeSpent"
- :rootPath="store.rootPath"
- />
- </div>
- `,
};
+</script>
+
+<template>
+ <div class="block">
+ <issuable-time-tracker
+ :time_estimate="store.timeEstimate"
+ :time_spent="store.totalTimeSpent"
+ :human_time_estimate="store.humanTimeEstimate"
+ :human_time_spent="store.humanTotalTimeSpent"
+ :root-path="store.rootPath"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
index 71dca498b3d..9c003aa9f8a 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
@@ -2,7 +2,7 @@
import TimeTrackingHelpState from './help_state.vue';
import TimeTrackingCollapsedState from './collapsed_state.vue';
import timeTrackingSpentOnlyPane from './spent_only_pane';
-import timeTrackingNoTrackingPane from './no_tracking_pane';
+import TimeTrackingNoTrackingPane from './no_tracking_pane.vue';
import TimeTrackingEstimateOnlyPane from './estimate_only_pane.vue';
import TimeTrackingComparisonPane from './comparison_pane.vue';
@@ -14,7 +14,7 @@ export default {
TimeTrackingCollapsedState,
TimeTrackingEstimateOnlyPane,
'time-tracking-spent-only-pane': timeTrackingSpentOnlyPane,
- 'time-tracking-no-tracking-pane': timeTrackingNoTrackingPane,
+ TimeTrackingNoTrackingPane,
TimeTrackingComparisonPane,
TimeTrackingHelpState,
},
diff --git a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
index 1eadebc7004..b267422cd97 100644
--- a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
+++ b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import _ from 'underscore';
function isValidProjectId(id) {
return id > 0;
@@ -43,7 +44,7 @@ class SidebarMoveIssue {
renderRow: project => `
<li>
<a href="#" class="js-move-issue-dropdown-item">
- ${project.name_with_namespace}
+ ${_.escape(project.name_with_namespace)}
</a>
</li>
`,
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 26eb4cffba3..3086e7d0fc9 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import Vue from 'vue';
-import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking';
+import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue';
import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue';
import SidebarMoveIssue from './lib/sidebar_move_issue';
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
index 602b68ea572..7d366c495f0 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
@@ -1,66 +1,70 @@
<script>
- import { n__ } from '~/locale';
- import statusIcon from '../mr_widget_status_icon.vue';
- import eventHub from '../../event_hub';
+import { n__ } from '~/locale';
+import statusIcon from '../mr_widget_status_icon.vue';
+import eventHub from '../../event_hub';
- export default {
- name: 'MRWidgetFailedToMerge',
+export default {
+ name: 'MRWidgetFailedToMerge',
- components: {
- statusIcon,
- },
+ components: {
+ statusIcon,
+ },
- props: {
- mr: {
- type: Object,
- required: true,
- default: () => ({}),
- },
+ props: {
+ mr: {
+ type: Object,
+ required: true,
+ default: () => ({}),
},
+ },
- data() {
- return {
- timer: 10,
- isRefreshing: false,
- };
- },
+ data() {
+ return {
+ timer: 10,
+ isRefreshing: false,
+ intervalId: null,
+ };
+ },
- computed: {
- timerText() {
- return n__(
- 'Refreshing in a second to show the updated status...',
- 'Refreshing in %d seconds to show the updated status...',
- this.timer,
- );
- },
+ computed: {
+ timerText() {
+ return n__(
+ 'Refreshing in a second to show the updated status...',
+ 'Refreshing in %d seconds to show the updated status...',
+ this.timer,
+ );
},
+ },
- mounted() {
- setInterval(() => {
- this.updateTimer();
- }, 1000);
- },
+ mounted() {
+ this.intervalId = setInterval(this.updateTimer, 1000);
+ },
- created() {
- eventHub.$emit('DisablePolling');
- },
+ created() {
+ eventHub.$emit('DisablePolling');
+ },
- methods: {
- refresh() {
- this.isRefreshing = true;
- eventHub.$emit('MRWidgetUpdateRequested');
- eventHub.$emit('EnablePolling');
- },
- updateTimer() {
- this.timer = this.timer - 1;
+ beforeDestroy() {
+ if (this.intervalId) {
+ clearInterval(this.intervalId);
+ }
+ },
- if (this.timer === 0) {
- this.refresh();
- }
- },
+ methods: {
+ refresh() {
+ this.isRefreshing = true;
+ eventHub.$emit('MRWidgetUpdateRequested');
+ eventHub.$emit('EnablePolling');
},
+ updateTimer() {
+ this.timer = this.timer - 1;
- };
+ if (this.timer === 0) {
+ this.refresh();
+ }
+ },
+ },
+};
</script>
<template>
<div class="mr-widget-body media">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
index 44e1a616a19..fe2608e8212 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
@@ -1,25 +1,26 @@
+<script>
import $ from 'jquery';
import statusIcon from '../mr_widget_status_icon.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
import eventHub from '../../event_hub';
export default {
- name: 'MRWidgetWIP',
- props: {
- mr: { type: Object, required: true },
- service: { type: Object, required: true },
+ name: 'WorkInProgress',
+ components: {
+ statusIcon,
},
directives: {
tooltip,
},
+ props: {
+ mr: { type: Object, required: true },
+ service: { type: Object, required: true },
+ },
data() {
return {
isMakingRequest: false,
};
},
- components: {
- statusIcon,
- },
methods: {
removeWIP() {
this.isMakingRequest = true;
@@ -36,32 +37,40 @@ export default {
});
},
},
- template: `
- <div class="mr-widget-body media">
- <status-icon status="warning" :show-disabled-button="Boolean(mr.removeWIPPath)" />
- <div class="media-body space-children">
- <span class="bold">
- This is a Work in Progress
- <i
- v-tooltip
- class="fa fa-question-circle"
- title="When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged"
- aria-label="When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged">
- </i>
- </span>
- <button
- v-if="mr.removeWIPPath"
- @click="removeWIP"
- :disabled="isMakingRequest"
- type="button"
- class="btn btn-default btn-xs js-remove-wip">
- <i
- v-if="isMakingRequest"
- class="fa fa-spinner fa-spin"
- aria-hidden="true" />
- Resolve WIP status
- </button>
- </div>
- </div>
- `,
};
+</script>
+
+<template>
+ <div class="mr-widget-body media">
+ <status-icon
+ status="warning"
+ :show-disabled-button="Boolean(mr.removeWIPPath)"
+ />
+ <div class="media-body space-children">
+ <span class="bold">
+ This is a Work in Progress
+ <i
+ v-tooltip
+ class="fa fa-question-circle"
+ title="When this merge request is ready,
+ remove the WIP: prefix from the title to allow it to be merged"
+ aria-label="When this merge request is ready,
+ remove the WIP: prefix from the title to allow it to be merged">
+ </i>
+ </span>
+ <button
+ v-if="mr.removeWIPPath"
+ @click="removeWIP"
+ :disabled="isMakingRequest"
+ type="button"
+ class="btn btn-default btn-xs js-remove-wip">
+ <i
+ v-if="isMakingRequest"
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true">
+ </i>
+ Resolve WIP status
+ </button>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
index 3b5c973e4a0..7f5f28091da 100644
--- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js
+++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
@@ -21,7 +21,7 @@ export { default as MergedState } from './components/states/mr_widget_merged.vue
export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge.vue';
export { default as ClosedState } from './components/states/mr_widget_closed.vue';
export { default as MergingState } from './components/states/mr_widget_merging.vue';
-export { default as WipState } from './components/states/mr_widget_wip';
+export { default as WorkInProgressState } from './components/states/work_in_progress.vue';
export { default as ArchivedState } from './components/states/mr_widget_archived.vue';
export { default as ConflictsState } from './components/states/mr_widget_conflicts.vue';
export { default as NothingToMergeState } from './components/states/nothing_to_merge.vue';
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
index 0be5d9e5a55..345f9ac1b4b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
@@ -12,7 +12,7 @@ import {
ClosedState,
MergingState,
RebaseState,
- WipState,
+ WorkInProgressState,
ArchivedState,
ConflictsState,
NothingToMergeState,
@@ -220,7 +220,7 @@ export default {
'mr-widget-closed': ClosedState,
'mr-widget-merging': MergingState,
'mr-widget-failed-to-merge': FailedToMerge,
- 'mr-widget-wip': WipState,
+ 'mr-widget-wip': WorkInProgressState,
'mr-widget-archived': ArchivedState,
'mr-widget-conflicts': ConflictsState,
'mr-widget-nothing-to-merge': NothingToMergeState,
diff --git a/app/assets/javascripts/vue_shared/components/file_icon.vue b/app/assets/javascripts/vue_shared/components/file_icon.vue
index ee1c3498748..be2755452e2 100644
--- a/app/assets/javascripts/vue_shared/components/file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/file_icon.vue
@@ -1,9 +1,9 @@
<script>
- import getIconForFile from './file_icon/file_icon_map';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
- import icon from '../../vue_shared/components/icon.vue';
+import getIconForFile from './file_icon/file_icon_map';
+import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import icon from '../../vue_shared/components/icon.vue';
- /* This is a re-usable vue component for rendering a svg sprite
+/* This is a re-usable vue component for rendering a svg sprite
icon
Sample configuration:
@@ -15,60 +15,60 @@
/>
*/
- export default {
- components: {
- loadingIcon,
- icon,
+export default {
+ components: {
+ loadingIcon,
+ icon,
+ },
+ props: {
+ fileName: {
+ type: String,
+ required: true,
},
- props: {
- fileName: {
- type: String,
- required: true,
- },
- folder: {
- type: Boolean,
- required: false,
- default: false,
- },
+ folder: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
- opened: {
- type: Boolean,
- required: false,
- default: false,
- },
+ opened: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
- loading: {
- type: Boolean,
- required: false,
- default: false,
- },
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
- size: {
- type: Number,
- required: false,
- default: 16,
- },
+ size: {
+ type: Number,
+ required: false,
+ default: 16,
+ },
- cssClasses: {
- type: String,
- required: false,
- default: '',
- },
+ cssClasses: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ spriteHref() {
+ const iconName = getIconForFile(this.fileName) || 'file';
+ return `${gon.sprite_file_icons}#${iconName}`;
+ },
+ folderIconName() {
+ return this.opened ? 'folder-open' : 'folder';
},
- computed: {
- spriteHref() {
- const iconName = getIconForFile(this.fileName) || 'file';
- return `${gon.sprite_file_icons}#${iconName}`;
- },
- folderIconName() {
- return this.opened ? 'folder-open' : 'folder';
- },
- iconSizeClass() {
- return this.size ? `s${this.size}` : '';
- },
+ iconSizeClass() {
+ return this.size ? `s${this.size}` : '';
},
- };
+ },
+};
</script>
<template>
<span>
@@ -82,6 +82,7 @@
v-if="!loading && folder"
:name="folderIconName"
:size="size"
+ css-classes="folder-icon"
/>
<loading-icon
v-if="loading"
diff --git a/app/assets/javascripts/vue_shared/components/identicon.vue b/app/assets/javascripts/vue_shared/components/identicon.vue
index 0a30f467b08..23010f40f26 100644
--- a/app/assets/javascripts/vue_shared/components/identicon.vue
+++ b/app/assets/javascripts/vue_shared/components/identicon.vue
@@ -17,7 +17,7 @@ export default {
},
computed: {
/**
- * This method is based on app/helpers/application_helper.rb#project_identicon
+ * This method is based on app/helpers/avatars_helper.rb#project_identicon
*/
identiconStyles() {
const allowedColors = [
diff --git a/app/assets/javascripts/vue_shared/models/label.js b/app/assets/javascripts/vue_shared/models/label.js
index 70b9efe0c68..d29c7fe973a 100644
--- a/app/assets/javascripts/vue_shared/models/label.js
+++ b/app/assets/javascripts/vue_shared/models/label.js
@@ -1,4 +1,4 @@
-class ListLabel {
+export default class ListLabel {
constructor(obj) {
this.id = obj.id;
this.title = obj.title;
diff --git a/app/assets/stylesheets/emoji_sprites.scss b/app/assets/stylesheets/emoji_sprites.scss
new file mode 100644
index 00000000000..8f6134c474b
--- /dev/null
+++ b/app/assets/stylesheets/emoji_sprites.scss
@@ -0,0 +1,5403 @@
+// Automatic Prettier Formatting for this big file
+// scss-lint:disable EmptyLineBetweenBlocks
+.emoji-zzz {
+ background-position: 0 0;
+}
+.emoji-1234 {
+ background-position: -20px 0;
+}
+.emoji-1F627 {
+ background-position: 0 -20px;
+}
+.emoji-8ball {
+ background-position: -20px -20px;
+}
+.emoji-a {
+ background-position: -40px 0;
+}
+.emoji-ab {
+ background-position: -40px -20px;
+}
+.emoji-abc {
+ background-position: 0 -40px;
+}
+.emoji-abcd {
+ background-position: -20px -40px;
+}
+.emoji-accept {
+ background-position: -40px -40px;
+}
+.emoji-aerial_tramway {
+ background-position: -60px 0;
+}
+.emoji-airplane {
+ background-position: -60px -20px;
+}
+.emoji-airplane_arriving {
+ background-position: -60px -40px;
+}
+.emoji-airplane_departure {
+ background-position: 0 -60px;
+}
+.emoji-airplane_small {
+ background-position: -20px -60px;
+}
+.emoji-alarm_clock {
+ background-position: -40px -60px;
+}
+.emoji-alembic {
+ background-position: -60px -60px;
+}
+.emoji-alien {
+ background-position: -80px 0;
+}
+.emoji-ambulance {
+ background-position: -80px -20px;
+}
+.emoji-amphora {
+ background-position: -80px -40px;
+}
+.emoji-anchor {
+ background-position: -80px -60px;
+}
+.emoji-angel {
+ background-position: 0 -80px;
+}
+.emoji-angel_tone1 {
+ background-position: -20px -80px;
+}
+.emoji-angel_tone2 {
+ background-position: -40px -80px;
+}
+.emoji-angel_tone3 {
+ background-position: -60px -80px;
+}
+.emoji-angel_tone4 {
+ background-position: -80px -80px;
+}
+.emoji-angel_tone5 {
+ background-position: -100px 0;
+}
+.emoji-anger {
+ background-position: -100px -20px;
+}
+.emoji-anger_right {
+ background-position: -100px -40px;
+}
+.emoji-angry {
+ background-position: -100px -60px;
+}
+.emoji-ant {
+ background-position: -100px -80px;
+}
+.emoji-apple {
+ background-position: 0 -100px;
+}
+.emoji-aquarius {
+ background-position: -20px -100px;
+}
+.emoji-aries {
+ background-position: -40px -100px;
+}
+.emoji-arrow_backward {
+ background-position: -60px -100px;
+}
+.emoji-arrow_double_down {
+ background-position: -80px -100px;
+}
+.emoji-arrow_double_up {
+ background-position: -100px -100px;
+}
+.emoji-arrow_down {
+ background-position: -120px 0;
+}
+.emoji-arrow_down_small {
+ background-position: -120px -20px;
+}
+.emoji-arrow_forward {
+ background-position: -120px -40px;
+}
+.emoji-arrow_heading_down {
+ background-position: -120px -60px;
+}
+.emoji-arrow_heading_up {
+ background-position: -120px -80px;
+}
+.emoji-arrow_left {
+ background-position: -120px -100px;
+}
+.emoji-arrow_lower_left {
+ background-position: 0 -120px;
+}
+.emoji-arrow_lower_right {
+ background-position: -20px -120px;
+}
+.emoji-arrow_right {
+ background-position: -40px -120px;
+}
+.emoji-arrow_right_hook {
+ background-position: -60px -120px;
+}
+.emoji-arrow_up {
+ background-position: -80px -120px;
+}
+.emoji-arrow_up_down {
+ background-position: -100px -120px;
+}
+.emoji-arrow_up_small {
+ background-position: -120px -120px;
+}
+.emoji-arrow_upper_left {
+ background-position: -140px 0;
+}
+.emoji-arrow_upper_right {
+ background-position: -140px -20px;
+}
+.emoji-arrows_clockwise {
+ background-position: -140px -40px;
+}
+.emoji-arrows_counterclockwise {
+ background-position: -140px -60px;
+}
+.emoji-art {
+ background-position: -140px -80px;
+}
+.emoji-articulated_lorry {
+ background-position: -140px -100px;
+}
+.emoji-asterisk {
+ background-position: -140px -120px;
+}
+.emoji-astonished {
+ background-position: 0 -140px;
+}
+.emoji-athletic_shoe {
+ background-position: -20px -140px;
+}
+.emoji-atm {
+ background-position: -40px -140px;
+}
+.emoji-atom {
+ background-position: -60px -140px;
+}
+.emoji-avocado {
+ background-position: -80px -140px;
+}
+.emoji-b {
+ background-position: -100px -140px;
+}
+.emoji-baby {
+ background-position: -120px -140px;
+}
+.emoji-baby_bottle {
+ background-position: -140px -140px;
+}
+.emoji-baby_chick {
+ background-position: -160px 0;
+}
+.emoji-baby_symbol {
+ background-position: -160px -20px;
+}
+.emoji-baby_tone1 {
+ background-position: -160px -40px;
+}
+.emoji-baby_tone2 {
+ background-position: -160px -60px;
+}
+.emoji-baby_tone3 {
+ background-position: -160px -80px;
+}
+.emoji-baby_tone4 {
+ background-position: -160px -100px;
+}
+.emoji-baby_tone5 {
+ background-position: -160px -120px;
+}
+.emoji-back {
+ background-position: -160px -140px;
+}
+.emoji-bacon {
+ background-position: 0 -160px;
+}
+.emoji-badminton {
+ background-position: -20px -160px;
+}
+.emoji-baggage_claim {
+ background-position: -40px -160px;
+}
+.emoji-balloon {
+ background-position: -60px -160px;
+}
+.emoji-ballot_box {
+ background-position: -80px -160px;
+}
+.emoji-ballot_box_with_check {
+ background-position: -100px -160px;
+}
+.emoji-bamboo {
+ background-position: -120px -160px;
+}
+.emoji-banana {
+ background-position: -140px -160px;
+}
+.emoji-bangbang {
+ background-position: -160px -160px;
+}
+.emoji-bank {
+ background-position: -180px 0;
+}
+.emoji-bar_chart {
+ background-position: -180px -20px;
+}
+.emoji-barber {
+ background-position: -180px -40px;
+}
+.emoji-baseball {
+ background-position: -180px -60px;
+}
+.emoji-basketball {
+ background-position: -180px -80px;
+}
+.emoji-basketball_player {
+ background-position: -180px -100px;
+}
+.emoji-basketball_player_tone1 {
+ background-position: -180px -120px;
+}
+.emoji-basketball_player_tone2 {
+ background-position: -180px -140px;
+}
+.emoji-basketball_player_tone3 {
+ background-position: -180px -160px;
+}
+.emoji-basketball_player_tone4 {
+ background-position: 0 -180px;
+}
+.emoji-basketball_player_tone5 {
+ background-position: -20px -180px;
+}
+.emoji-bat {
+ background-position: -40px -180px;
+}
+.emoji-bath {
+ background-position: -60px -180px;
+}
+.emoji-bath_tone1 {
+ background-position: -80px -180px;
+}
+.emoji-bath_tone2 {
+ background-position: -100px -180px;
+}
+.emoji-bath_tone3 {
+ background-position: -120px -180px;
+}
+.emoji-bath_tone4 {
+ background-position: -140px -180px;
+}
+.emoji-bath_tone5 {
+ background-position: -160px -180px;
+}
+.emoji-bathtub {
+ background-position: -180px -180px;
+}
+.emoji-battery {
+ background-position: -200px 0;
+}
+.emoji-beach {
+ background-position: -200px -20px;
+}
+.emoji-beach_umbrella {
+ background-position: -200px -40px;
+}
+.emoji-bear {
+ background-position: -200px -60px;
+}
+.emoji-bed {
+ background-position: -200px -80px;
+}
+.emoji-bee {
+ background-position: -200px -100px;
+}
+.emoji-beer {
+ background-position: -200px -120px;
+}
+.emoji-beers {
+ background-position: -200px -140px;
+}
+.emoji-beetle {
+ background-position: -200px -160px;
+}
+.emoji-beginner {
+ background-position: -200px -180px;
+}
+.emoji-bell {
+ background-position: 0 -200px;
+}
+.emoji-bellhop {
+ background-position: -20px -200px;
+}
+.emoji-bento {
+ background-position: -40px -200px;
+}
+.emoji-bicyclist {
+ background-position: -60px -200px;
+}
+.emoji-bicyclist_tone1 {
+ background-position: -80px -200px;
+}
+.emoji-bicyclist_tone2 {
+ background-position: -100px -200px;
+}
+.emoji-bicyclist_tone3 {
+ background-position: -120px -200px;
+}
+.emoji-bicyclist_tone4 {
+ background-position: -140px -200px;
+}
+.emoji-bicyclist_tone5 {
+ background-position: -160px -200px;
+}
+.emoji-bike {
+ background-position: -180px -200px;
+}
+.emoji-bikini {
+ background-position: -200px -200px;
+}
+.emoji-biohazard {
+ background-position: -220px 0;
+}
+.emoji-bird {
+ background-position: -220px -20px;
+}
+.emoji-birthday {
+ background-position: -220px -40px;
+}
+.emoji-black_circle {
+ background-position: -220px -60px;
+}
+.emoji-black_heart {
+ background-position: -220px -80px;
+}
+.emoji-black_joker {
+ background-position: -220px -100px;
+}
+.emoji-black_large_square {
+ background-position: -220px -120px;
+}
+.emoji-black_medium_small_square {
+ background-position: -220px -140px;
+}
+.emoji-black_medium_square {
+ background-position: -220px -160px;
+}
+.emoji-black_nib {
+ background-position: -220px -180px;
+}
+.emoji-black_small_square {
+ background-position: -220px -200px;
+}
+.emoji-black_square_button {
+ background-position: 0 -220px;
+}
+.emoji-blossom {
+ background-position: -20px -220px;
+}
+.emoji-blowfish {
+ background-position: -40px -220px;
+}
+.emoji-blue_book {
+ background-position: -60px -220px;
+}
+.emoji-blue_car {
+ background-position: -80px -220px;
+}
+.emoji-blue_heart {
+ background-position: -100px -220px;
+}
+.emoji-blush {
+ background-position: -120px -220px;
+}
+.emoji-boar {
+ background-position: -140px -220px;
+}
+.emoji-bomb {
+ background-position: -160px -220px;
+}
+.emoji-book {
+ background-position: -180px -220px;
+}
+.emoji-bookmark {
+ background-position: -200px -220px;
+}
+.emoji-bookmark_tabs {
+ background-position: -220px -220px;
+}
+.emoji-books {
+ background-position: -240px 0;
+}
+.emoji-boom {
+ background-position: -240px -20px;
+}
+.emoji-boot {
+ background-position: -240px -40px;
+}
+.emoji-bouquet {
+ background-position: -240px -60px;
+}
+.emoji-bow {
+ background-position: -240px -80px;
+}
+.emoji-bow_and_arrow {
+ background-position: -240px -100px;
+}
+.emoji-bow_tone1 {
+ background-position: -240px -120px;
+}
+.emoji-bow_tone2 {
+ background-position: -240px -140px;
+}
+.emoji-bow_tone3 {
+ background-position: -240px -160px;
+}
+.emoji-bow_tone4 {
+ background-position: -240px -180px;
+}
+.emoji-bow_tone5 {
+ background-position: -240px -200px;
+}
+.emoji-bowling {
+ background-position: -240px -220px;
+}
+.emoji-boxing_glove {
+ background-position: 0 -240px;
+}
+.emoji-boy {
+ background-position: -20px -240px;
+}
+.emoji-boy_tone1 {
+ background-position: -40px -240px;
+}
+.emoji-boy_tone2 {
+ background-position: -60px -240px;
+}
+.emoji-boy_tone3 {
+ background-position: -80px -240px;
+}
+.emoji-boy_tone4 {
+ background-position: -100px -240px;
+}
+.emoji-boy_tone5 {
+ background-position: -120px -240px;
+}
+.emoji-bread {
+ background-position: -140px -240px;
+}
+.emoji-bride_with_veil {
+ background-position: -160px -240px;
+}
+.emoji-bride_with_veil_tone1 {
+ background-position: -180px -240px;
+}
+.emoji-bride_with_veil_tone2 {
+ background-position: -200px -240px;
+}
+.emoji-bride_with_veil_tone3 {
+ background-position: -220px -240px;
+}
+.emoji-bride_with_veil_tone4 {
+ background-position: -240px -240px;
+}
+.emoji-bride_with_veil_tone5 {
+ background-position: -260px 0;
+}
+.emoji-bridge_at_night {
+ background-position: -260px -20px;
+}
+.emoji-briefcase {
+ background-position: -260px -40px;
+}
+.emoji-broken_heart {
+ background-position: -260px -60px;
+}
+.emoji-bug {
+ background-position: -260px -80px;
+}
+.emoji-bulb {
+ background-position: -260px -100px;
+}
+.emoji-bullettrain_front {
+ background-position: -260px -120px;
+}
+.emoji-bullettrain_side {
+ background-position: -260px -140px;
+}
+.emoji-burrito {
+ background-position: -260px -160px;
+}
+.emoji-bus {
+ background-position: -260px -180px;
+}
+.emoji-busstop {
+ background-position: -260px -200px;
+}
+.emoji-bust_in_silhouette {
+ background-position: -260px -220px;
+}
+.emoji-busts_in_silhouette {
+ background-position: -260px -240px;
+}
+.emoji-butterfly {
+ background-position: 0 -260px;
+}
+.emoji-cactus {
+ background-position: -20px -260px;
+}
+.emoji-cake {
+ background-position: -40px -260px;
+}
+.emoji-calendar {
+ background-position: -60px -260px;
+}
+.emoji-calendar_spiral {
+ background-position: -80px -260px;
+}
+.emoji-call_me {
+ background-position: -100px -260px;
+}
+.emoji-call_me_tone1 {
+ background-position: -120px -260px;
+}
+.emoji-call_me_tone2 {
+ background-position: -140px -260px;
+}
+.emoji-call_me_tone3 {
+ background-position: -160px -260px;
+}
+.emoji-call_me_tone4 {
+ background-position: -180px -260px;
+}
+.emoji-call_me_tone5 {
+ background-position: -200px -260px;
+}
+.emoji-calling {
+ background-position: -220px -260px;
+}
+.emoji-camel {
+ background-position: -240px -260px;
+}
+.emoji-camera {
+ background-position: -260px -260px;
+}
+.emoji-camera_with_flash {
+ background-position: -280px 0;
+}
+.emoji-camping {
+ background-position: -280px -20px;
+}
+.emoji-cancer {
+ background-position: -280px -40px;
+}
+.emoji-candle {
+ background-position: -280px -60px;
+}
+.emoji-candy {
+ background-position: -280px -80px;
+}
+.emoji-canoe {
+ background-position: -280px -100px;
+}
+.emoji-capital_abcd {
+ background-position: -280px -120px;
+}
+.emoji-capricorn {
+ background-position: -280px -140px;
+}
+.emoji-card_box {
+ background-position: -280px -160px;
+}
+.emoji-card_index {
+ background-position: -280px -180px;
+}
+.emoji-carousel_horse {
+ background-position: -280px -200px;
+}
+.emoji-carrot {
+ background-position: -280px -220px;
+}
+.emoji-cartwheel {
+ background-position: -280px -240px;
+}
+.emoji-cartwheel_tone1 {
+ background-position: -280px -260px;
+}
+.emoji-cartwheel_tone2 {
+ background-position: 0 -280px;
+}
+.emoji-cartwheel_tone3 {
+ background-position: -20px -280px;
+}
+.emoji-cartwheel_tone4 {
+ background-position: -40px -280px;
+}
+.emoji-cartwheel_tone5 {
+ background-position: -60px -280px;
+}
+.emoji-cat {
+ background-position: -80px -280px;
+}
+.emoji-cat2 {
+ background-position: -100px -280px;
+}
+.emoji-cd {
+ background-position: -120px -280px;
+}
+.emoji-chains {
+ background-position: -140px -280px;
+}
+.emoji-champagne {
+ background-position: -160px -280px;
+}
+.emoji-champagne_glass {
+ background-position: -180px -280px;
+}
+.emoji-chart {
+ background-position: -200px -280px;
+}
+.emoji-chart_with_downwards_trend {
+ background-position: -220px -280px;
+}
+.emoji-chart_with_upwards_trend {
+ background-position: -240px -280px;
+}
+.emoji-checkered_flag {
+ background-position: -260px -280px;
+}
+.emoji-cheese {
+ background-position: -280px -280px;
+}
+.emoji-cherries {
+ background-position: -300px 0;
+}
+.emoji-cherry_blossom {
+ background-position: -300px -20px;
+}
+.emoji-chestnut {
+ background-position: -300px -40px;
+}
+.emoji-chicken {
+ background-position: -300px -60px;
+}
+.emoji-children_crossing {
+ background-position: -300px -80px;
+}
+.emoji-chipmunk {
+ background-position: -300px -100px;
+}
+.emoji-chocolate_bar {
+ background-position: -300px -120px;
+}
+.emoji-christmas_tree {
+ background-position: -300px -140px;
+}
+.emoji-church {
+ background-position: -300px -160px;
+}
+.emoji-cinema {
+ background-position: -300px -180px;
+}
+.emoji-circus_tent {
+ background-position: -300px -200px;
+}
+.emoji-city_dusk {
+ background-position: -300px -220px;
+}
+.emoji-city_sunset {
+ background-position: -300px -240px;
+}
+.emoji-cityscape {
+ background-position: -300px -260px;
+}
+.emoji-cl {
+ background-position: -300px -280px;
+}
+.emoji-clap {
+ background-position: 0 -300px;
+}
+.emoji-clap_tone1 {
+ background-position: -20px -300px;
+}
+.emoji-clap_tone2 {
+ background-position: -40px -300px;
+}
+.emoji-clap_tone3 {
+ background-position: -60px -300px;
+}
+.emoji-clap_tone4 {
+ background-position: -80px -300px;
+}
+.emoji-clap_tone5 {
+ background-position: -100px -300px;
+}
+.emoji-clapper {
+ background-position: -120px -300px;
+}
+.emoji-classical_building {
+ background-position: -140px -300px;
+}
+.emoji-clipboard {
+ background-position: -160px -300px;
+}
+.emoji-clock {
+ background-position: -180px -300px;
+}
+.emoji-clock1 {
+ background-position: -200px -300px;
+}
+.emoji-clock10 {
+ background-position: -220px -300px;
+}
+.emoji-clock1030 {
+ background-position: -240px -300px;
+}
+.emoji-clock11 {
+ background-position: -260px -300px;
+}
+.emoji-clock1130 {
+ background-position: -280px -300px;
+}
+.emoji-clock12 {
+ background-position: -300px -300px;
+}
+.emoji-clock1230 {
+ background-position: -320px 0;
+}
+.emoji-clock130 {
+ background-position: -320px -20px;
+}
+.emoji-clock2 {
+ background-position: -320px -40px;
+}
+.emoji-clock230 {
+ background-position: -320px -60px;
+}
+.emoji-clock3 {
+ background-position: -320px -80px;
+}
+.emoji-clock330 {
+ background-position: -320px -100px;
+}
+.emoji-clock4 {
+ background-position: -320px -120px;
+}
+.emoji-clock430 {
+ background-position: -320px -140px;
+}
+.emoji-clock5 {
+ background-position: -320px -160px;
+}
+.emoji-clock530 {
+ background-position: -320px -180px;
+}
+.emoji-clock6 {
+ background-position: -320px -200px;
+}
+.emoji-clock630 {
+ background-position: -320px -220px;
+}
+.emoji-clock7 {
+ background-position: -320px -240px;
+}
+.emoji-clock730 {
+ background-position: -320px -260px;
+}
+.emoji-clock8 {
+ background-position: -320px -280px;
+}
+.emoji-clock830 {
+ background-position: -320px -300px;
+}
+.emoji-clock9 {
+ background-position: 0 -320px;
+}
+.emoji-clock930 {
+ background-position: -20px -320px;
+}
+.emoji-closed_book {
+ background-position: -40px -320px;
+}
+.emoji-closed_lock_with_key {
+ background-position: -60px -320px;
+}
+.emoji-closed_umbrella {
+ background-position: -80px -320px;
+}
+.emoji-cloud {
+ background-position: -100px -320px;
+}
+.emoji-cloud_lightning {
+ background-position: -120px -320px;
+}
+.emoji-cloud_rain {
+ background-position: -140px -320px;
+}
+.emoji-cloud_snow {
+ background-position: -160px -320px;
+}
+.emoji-cloud_tornado {
+ background-position: -180px -320px;
+}
+.emoji-clown {
+ background-position: -200px -320px;
+}
+.emoji-clubs {
+ background-position: -220px -320px;
+}
+.emoji-cocktail {
+ background-position: -240px -320px;
+}
+.emoji-coffee {
+ background-position: -260px -320px;
+}
+.emoji-coffin {
+ background-position: -280px -320px;
+}
+.emoji-cold_sweat {
+ background-position: -300px -320px;
+}
+.emoji-comet {
+ background-position: -320px -320px;
+}
+.emoji-compression {
+ background-position: -340px 0;
+}
+.emoji-computer {
+ background-position: -340px -20px;
+}
+.emoji-confetti_ball {
+ background-position: -340px -40px;
+}
+.emoji-confounded {
+ background-position: -340px -60px;
+}
+.emoji-confused {
+ background-position: -340px -80px;
+}
+.emoji-congratulations {
+ background-position: -340px -100px;
+}
+.emoji-construction {
+ background-position: -340px -120px;
+}
+.emoji-construction_site {
+ background-position: -340px -140px;
+}
+.emoji-construction_worker {
+ background-position: -340px -160px;
+}
+.emoji-construction_worker_tone1 {
+ background-position: -340px -180px;
+}
+.emoji-construction_worker_tone2 {
+ background-position: -340px -200px;
+}
+.emoji-construction_worker_tone3 {
+ background-position: -340px -220px;
+}
+.emoji-construction_worker_tone4 {
+ background-position: -340px -240px;
+}
+.emoji-construction_worker_tone5 {
+ background-position: -340px -260px;
+}
+.emoji-control_knobs {
+ background-position: -340px -280px;
+}
+.emoji-convenience_store {
+ background-position: -340px -300px;
+}
+.emoji-cookie {
+ background-position: -340px -320px;
+}
+.emoji-cooking {
+ background-position: 0 -340px;
+}
+.emoji-cool {
+ background-position: -20px -340px;
+}
+.emoji-cop {
+ background-position: -40px -340px;
+}
+.emoji-cop_tone1 {
+ background-position: -60px -340px;
+}
+.emoji-cop_tone2 {
+ background-position: -80px -340px;
+}
+.emoji-cop_tone3 {
+ background-position: -100px -340px;
+}
+.emoji-cop_tone4 {
+ background-position: -120px -340px;
+}
+.emoji-cop_tone5 {
+ background-position: -140px -340px;
+}
+.emoji-copyright {
+ background-position: -160px -340px;
+}
+.emoji-corn {
+ background-position: -180px -340px;
+}
+.emoji-couch {
+ background-position: -200px -340px;
+}
+.emoji-couple {
+ background-position: -220px -340px;
+}
+.emoji-couple_mm {
+ background-position: -240px -340px;
+}
+.emoji-couple_with_heart {
+ background-position: -260px -340px;
+}
+.emoji-couple_ww {
+ background-position: -280px -340px;
+}
+.emoji-couplekiss {
+ background-position: -300px -340px;
+}
+.emoji-cow {
+ background-position: -320px -340px;
+}
+.emoji-cow2 {
+ background-position: -340px -340px;
+}
+.emoji-cowboy {
+ background-position: -360px 0;
+}
+.emoji-crab {
+ background-position: -360px -20px;
+}
+.emoji-crayon {
+ background-position: -360px -40px;
+}
+.emoji-credit_card {
+ background-position: -360px -60px;
+}
+.emoji-crescent_moon {
+ background-position: -360px -80px;
+}
+.emoji-cricket {
+ background-position: -360px -100px;
+}
+.emoji-crocodile {
+ background-position: -360px -120px;
+}
+.emoji-croissant {
+ background-position: -360px -140px;
+}
+.emoji-cross {
+ background-position: -360px -160px;
+}
+.emoji-crossed_flags {
+ background-position: -360px -180px;
+}
+.emoji-crossed_swords {
+ background-position: -360px -200px;
+}
+.emoji-crown {
+ background-position: -360px -220px;
+}
+.emoji-cruise_ship {
+ background-position: -360px -240px;
+}
+.emoji-cry {
+ background-position: -360px -260px;
+}
+.emoji-crying_cat_face {
+ background-position: -360px -280px;
+}
+.emoji-crystal_ball {
+ background-position: -360px -300px;
+}
+.emoji-cucumber {
+ background-position: -360px -320px;
+}
+.emoji-cupid {
+ background-position: -360px -340px;
+}
+.emoji-curly_loop {
+ background-position: 0 -360px;
+}
+.emoji-currency_exchange {
+ background-position: -20px -360px;
+}
+.emoji-curry {
+ background-position: -40px -360px;
+}
+.emoji-custard {
+ background-position: -60px -360px;
+}
+.emoji-customs {
+ background-position: -80px -360px;
+}
+.emoji-cyclone {
+ background-position: -100px -360px;
+}
+.emoji-dagger {
+ background-position: -120px -360px;
+}
+.emoji-dancer {
+ background-position: -140px -360px;
+}
+.emoji-dancer_tone1 {
+ background-position: -160px -360px;
+}
+.emoji-dancer_tone2 {
+ background-position: -180px -360px;
+}
+.emoji-dancer_tone3 {
+ background-position: -200px -360px;
+}
+.emoji-dancer_tone4 {
+ background-position: -220px -360px;
+}
+.emoji-dancer_tone5 {
+ background-position: -240px -360px;
+}
+.emoji-dancers {
+ background-position: -260px -360px;
+}
+.emoji-dango {
+ background-position: -280px -360px;
+}
+.emoji-dark_sunglasses {
+ background-position: -300px -360px;
+}
+.emoji-dart {
+ background-position: -320px -360px;
+}
+.emoji-dash {
+ background-position: -340px -360px;
+}
+.emoji-date {
+ background-position: -360px -360px;
+}
+.emoji-deciduous_tree {
+ background-position: -380px 0;
+}
+.emoji-deer {
+ background-position: -380px -20px;
+}
+.emoji-department_store {
+ background-position: -380px -40px;
+}
+.emoji-desert {
+ background-position: -380px -60px;
+}
+.emoji-desktop {
+ background-position: -380px -80px;
+}
+.emoji-diamond_shape_with_a_dot_inside {
+ background-position: -380px -100px;
+}
+.emoji-diamonds {
+ background-position: -380px -120px;
+}
+.emoji-disappointed {
+ background-position: -380px -140px;
+}
+.emoji-disappointed_relieved {
+ background-position: -380px -160px;
+}
+.emoji-dividers {
+ background-position: -380px -180px;
+}
+.emoji-dizzy {
+ background-position: -380px -200px;
+}
+.emoji-dizzy_face {
+ background-position: -380px -220px;
+}
+.emoji-do_not_litter {
+ background-position: -380px -240px;
+}
+.emoji-dog {
+ background-position: -380px -260px;
+}
+.emoji-dog2 {
+ background-position: -380px -280px;
+}
+.emoji-dollar {
+ background-position: -380px -300px;
+}
+.emoji-dolls {
+ background-position: -380px -320px;
+}
+.emoji-dolphin {
+ background-position: -380px -340px;
+}
+.emoji-door {
+ background-position: -380px -360px;
+}
+.emoji-doughnut {
+ background-position: 0 -380px;
+}
+.emoji-dove {
+ background-position: -20px -380px;
+}
+.emoji-dragon {
+ background-position: -40px -380px;
+}
+.emoji-dragon_face {
+ background-position: -60px -380px;
+}
+.emoji-dress {
+ background-position: -80px -380px;
+}
+.emoji-dromedary_camel {
+ background-position: -100px -380px;
+}
+.emoji-drooling_face {
+ background-position: -120px -380px;
+}
+.emoji-droplet {
+ background-position: -140px -380px;
+}
+.emoji-drum {
+ background-position: -160px -380px;
+}
+.emoji-duck {
+ background-position: -180px -380px;
+}
+.emoji-dvd {
+ background-position: -200px -380px;
+}
+.emoji-e-mail {
+ background-position: -220px -380px;
+}
+.emoji-eagle {
+ background-position: -240px -380px;
+}
+.emoji-ear {
+ background-position: -260px -380px;
+}
+.emoji-ear_of_rice {
+ background-position: -280px -380px;
+}
+.emoji-ear_tone1 {
+ background-position: -300px -380px;
+}
+.emoji-ear_tone2 {
+ background-position: -320px -380px;
+}
+.emoji-ear_tone3 {
+ background-position: -340px -380px;
+}
+.emoji-ear_tone4 {
+ background-position: -360px -380px;
+}
+.emoji-ear_tone5 {
+ background-position: -380px -380px;
+}
+.emoji-earth_africa {
+ background-position: -400px 0;
+}
+.emoji-earth_americas {
+ background-position: -400px -20px;
+}
+.emoji-earth_asia {
+ background-position: -400px -40px;
+}
+.emoji-egg {
+ background-position: -400px -60px;
+}
+.emoji-eggplant {
+ background-position: -400px -80px;
+}
+.emoji-eight {
+ background-position: -400px -100px;
+}
+.emoji-eight_pointed_black_star {
+ background-position: -400px -120px;
+}
+.emoji-eight_spoked_asterisk {
+ background-position: -400px -140px;
+}
+.emoji-eject {
+ background-position: -400px -160px;
+}
+.emoji-electric_plug {
+ background-position: -400px -180px;
+}
+.emoji-elephant {
+ background-position: -400px -200px;
+}
+.emoji-end {
+ background-position: -400px -220px;
+}
+.emoji-envelope {
+ background-position: -400px -240px;
+}
+.emoji-envelope_with_arrow {
+ background-position: -400px -260px;
+}
+.emoji-euro {
+ background-position: -400px -280px;
+}
+.emoji-european_castle {
+ background-position: -400px -300px;
+}
+.emoji-european_post_office {
+ background-position: -400px -320px;
+}
+.emoji-evergreen_tree {
+ background-position: -400px -340px;
+}
+.emoji-exclamation {
+ background-position: -400px -360px;
+}
+.emoji-expressionless {
+ background-position: -400px -380px;
+}
+.emoji-eye {
+ background-position: 0 -400px;
+}
+.emoji-eye_in_speech_bubble {
+ background-position: -20px -400px;
+}
+.emoji-eyeglasses {
+ background-position: -40px -400px;
+}
+.emoji-eyes {
+ background-position: -60px -400px;
+}
+.emoji-face_palm {
+ background-position: -80px -400px;
+}
+.emoji-face_palm_tone1 {
+ background-position: -100px -400px;
+}
+.emoji-face_palm_tone2 {
+ background-position: -120px -400px;
+}
+.emoji-face_palm_tone3 {
+ background-position: -140px -400px;
+}
+.emoji-face_palm_tone4 {
+ background-position: -160px -400px;
+}
+.emoji-face_palm_tone5 {
+ background-position: -180px -400px;
+}
+.emoji-factory {
+ background-position: -200px -400px;
+}
+.emoji-fallen_leaf {
+ background-position: -220px -400px;
+}
+.emoji-family {
+ background-position: -240px -400px;
+}
+.emoji-family_mmb {
+ background-position: -260px -400px;
+}
+.emoji-family_mmbb {
+ background-position: -280px -400px;
+}
+.emoji-family_mmg {
+ background-position: -300px -400px;
+}
+.emoji-family_mmgb {
+ background-position: -320px -400px;
+}
+.emoji-family_mmgg {
+ background-position: -340px -400px;
+}
+.emoji-family_mwbb {
+ background-position: -360px -400px;
+}
+.emoji-family_mwg {
+ background-position: -380px -400px;
+}
+.emoji-family_mwgb {
+ background-position: -400px -400px;
+}
+.emoji-family_mwgg {
+ background-position: -420px 0;
+}
+.emoji-family_wwb {
+ background-position: -420px -20px;
+}
+.emoji-family_wwbb {
+ background-position: -420px -40px;
+}
+.emoji-family_wwg {
+ background-position: -420px -60px;
+}
+.emoji-family_wwgb {
+ background-position: -420px -80px;
+}
+.emoji-family_wwgg {
+ background-position: -420px -100px;
+}
+.emoji-fast_forward {
+ background-position: -420px -120px;
+}
+.emoji-fax {
+ background-position: -420px -140px;
+}
+.emoji-fearful {
+ background-position: -420px -160px;
+}
+.emoji-feet {
+ background-position: -420px -180px;
+}
+.emoji-fencer {
+ background-position: -420px -200px;
+}
+.emoji-ferris_wheel {
+ background-position: -420px -220px;
+}
+.emoji-ferry {
+ background-position: -420px -240px;
+}
+.emoji-field_hockey {
+ background-position: -420px -260px;
+}
+.emoji-file_cabinet {
+ background-position: -420px -280px;
+}
+.emoji-file_folder {
+ background-position: -420px -300px;
+}
+.emoji-film_frames {
+ background-position: -420px -320px;
+}
+.emoji-fingers_crossed {
+ background-position: -420px -340px;
+}
+.emoji-fingers_crossed_tone1 {
+ background-position: -420px -360px;
+}
+.emoji-fingers_crossed_tone2 {
+ background-position: -420px -380px;
+}
+.emoji-fingers_crossed_tone3 {
+ background-position: -420px -400px;
+}
+.emoji-fingers_crossed_tone4 {
+ background-position: 0 -420px;
+}
+.emoji-fingers_crossed_tone5 {
+ background-position: -20px -420px;
+}
+.emoji-fire {
+ background-position: -40px -420px;
+}
+.emoji-fire_engine {
+ background-position: -60px -420px;
+}
+.emoji-fireworks {
+ background-position: -80px -420px;
+}
+.emoji-first_place {
+ background-position: -100px -420px;
+}
+.emoji-first_quarter_moon {
+ background-position: -120px -420px;
+}
+.emoji-first_quarter_moon_with_face {
+ background-position: -140px -420px;
+}
+.emoji-fish {
+ background-position: -160px -420px;
+}
+.emoji-fish_cake {
+ background-position: -180px -420px;
+}
+.emoji-fishing_pole_and_fish {
+ background-position: -200px -420px;
+}
+.emoji-fist {
+ background-position: -220px -420px;
+}
+.emoji-fist_tone1 {
+ background-position: -240px -420px;
+}
+.emoji-fist_tone2 {
+ background-position: -260px -420px;
+}
+.emoji-fist_tone3 {
+ background-position: -280px -420px;
+}
+.emoji-fist_tone4 {
+ background-position: -300px -420px;
+}
+.emoji-fist_tone5 {
+ background-position: -320px -420px;
+}
+.emoji-five {
+ background-position: -340px -420px;
+}
+.emoji-flag_ac {
+ background-position: -360px -420px;
+}
+.emoji-flag_ad {
+ background-position: -380px -420px;
+}
+.emoji-flag_ae {
+ background-position: -400px -420px;
+}
+.emoji-flag_af {
+ background-position: -420px -420px;
+}
+.emoji-flag_ag {
+ background-position: -440px 0;
+}
+.emoji-flag_ai {
+ background-position: -440px -20px;
+}
+.emoji-flag_al {
+ background-position: -440px -40px;
+}
+.emoji-flag_am {
+ background-position: -440px -60px;
+}
+.emoji-flag_ao {
+ background-position: -440px -80px;
+}
+.emoji-flag_aq {
+ background-position: -440px -100px;
+}
+.emoji-flag_ar {
+ background-position: -440px -120px;
+}
+.emoji-flag_as {
+ background-position: -440px -140px;
+}
+.emoji-flag_at {
+ background-position: -440px -160px;
+}
+.emoji-flag_au {
+ background-position: -440px -180px;
+}
+.emoji-flag_aw {
+ background-position: -440px -200px;
+}
+.emoji-flag_ax {
+ background-position: -440px -220px;
+}
+.emoji-flag_az {
+ background-position: -440px -240px;
+}
+.emoji-flag_ba {
+ background-position: -440px -260px;
+}
+.emoji-flag_bb {
+ background-position: -440px -280px;
+}
+.emoji-flag_bd {
+ background-position: -440px -300px;
+}
+.emoji-flag_be {
+ background-position: -440px -320px;
+}
+.emoji-flag_bf {
+ background-position: -440px -340px;
+}
+.emoji-flag_bg {
+ background-position: -440px -360px;
+}
+.emoji-flag_bh {
+ background-position: -440px -380px;
+}
+.emoji-flag_bi {
+ background-position: -440px -400px;
+}
+.emoji-flag_bj {
+ background-position: -440px -420px;
+}
+.emoji-flag_bl {
+ background-position: 0 -440px;
+}
+.emoji-flag_black {
+ background-position: -20px -440px;
+}
+.emoji-flag_bm {
+ background-position: -40px -440px;
+}
+.emoji-flag_bn {
+ background-position: -60px -440px;
+}
+.emoji-flag_bo {
+ background-position: -80px -440px;
+}
+.emoji-flag_bq {
+ background-position: -100px -440px;
+}
+.emoji-flag_br {
+ background-position: -120px -440px;
+}
+.emoji-flag_bs {
+ background-position: -140px -440px;
+}
+.emoji-flag_bt {
+ background-position: -160px -440px;
+}
+.emoji-flag_bv {
+ background-position: -180px -440px;
+}
+.emoji-flag_bw {
+ background-position: -200px -440px;
+}
+.emoji-flag_by {
+ background-position: -220px -440px;
+}
+.emoji-flag_bz {
+ background-position: -240px -440px;
+}
+.emoji-flag_ca {
+ background-position: -260px -440px;
+}
+.emoji-flag_cc {
+ background-position: -280px -440px;
+}
+.emoji-flag_cd {
+ background-position: -300px -440px;
+}
+.emoji-flag_cf {
+ background-position: -320px -440px;
+}
+.emoji-flag_cg {
+ background-position: -340px -440px;
+}
+.emoji-flag_ch {
+ background-position: -360px -440px;
+}
+.emoji-flag_ci {
+ background-position: -380px -440px;
+}
+.emoji-flag_ck {
+ background-position: -400px -440px;
+}
+.emoji-flag_cl {
+ background-position: -420px -440px;
+}
+.emoji-flag_cm {
+ background-position: -440px -440px;
+}
+.emoji-flag_cn {
+ background-position: -460px 0;
+}
+.emoji-flag_co {
+ background-position: -460px -20px;
+}
+.emoji-flag_cp {
+ background-position: -460px -40px;
+}
+.emoji-flag_cr {
+ background-position: -460px -60px;
+}
+.emoji-flag_cu {
+ background-position: -460px -80px;
+}
+.emoji-flag_cv {
+ background-position: -460px -100px;
+}
+.emoji-flag_cw {
+ background-position: -460px -120px;
+}
+.emoji-flag_cx {
+ background-position: -460px -140px;
+}
+.emoji-flag_cy {
+ background-position: -460px -160px;
+}
+.emoji-flag_cz {
+ background-position: -460px -180px;
+}
+.emoji-flag_de {
+ background-position: -460px -200px;
+}
+.emoji-flag_dg {
+ background-position: -460px -220px;
+}
+.emoji-flag_dj {
+ background-position: -460px -240px;
+}
+.emoji-flag_dk {
+ background-position: -460px -260px;
+}
+.emoji-flag_dm {
+ background-position: -460px -280px;
+}
+.emoji-flag_do {
+ background-position: -460px -300px;
+}
+.emoji-flag_dz {
+ background-position: -460px -320px;
+}
+.emoji-flag_ea {
+ background-position: -460px -340px;
+}
+.emoji-flag_ec {
+ background-position: -460px -360px;
+}
+.emoji-flag_ee {
+ background-position: -460px -380px;
+}
+.emoji-flag_eg {
+ background-position: -460px -400px;
+}
+.emoji-flag_eh {
+ background-position: -460px -420px;
+}
+.emoji-flag_er {
+ background-position: -460px -440px;
+}
+.emoji-flag_es {
+ background-position: 0 -460px;
+}
+.emoji-flag_et {
+ background-position: -20px -460px;
+}
+.emoji-flag_eu {
+ background-position: -40px -460px;
+}
+.emoji-flag_fi {
+ background-position: -60px -460px;
+}
+.emoji-flag_fj {
+ background-position: -80px -460px;
+}
+.emoji-flag_fk {
+ background-position: -100px -460px;
+}
+.emoji-flag_fm {
+ background-position: -120px -460px;
+}
+.emoji-flag_fo {
+ background-position: -140px -460px;
+}
+.emoji-flag_fr {
+ background-position: -160px -460px;
+}
+.emoji-flag_ga {
+ background-position: -180px -460px;
+}
+.emoji-flag_gb {
+ background-position: -200px -460px;
+}
+.emoji-flag_gd {
+ background-position: -220px -460px;
+}
+.emoji-flag_ge {
+ background-position: -240px -460px;
+}
+.emoji-flag_gf {
+ background-position: -260px -460px;
+}
+.emoji-flag_gg {
+ background-position: -280px -460px;
+}
+.emoji-flag_gh {
+ background-position: -300px -460px;
+}
+.emoji-flag_gi {
+ background-position: -320px -460px;
+}
+.emoji-flag_gl {
+ background-position: -340px -460px;
+}
+.emoji-flag_gm {
+ background-position: -360px -460px;
+}
+.emoji-flag_gn {
+ background-position: -380px -460px;
+}
+.emoji-flag_gp {
+ background-position: -400px -460px;
+}
+.emoji-flag_gq {
+ background-position: -420px -460px;
+}
+.emoji-flag_gr {
+ background-position: -440px -460px;
+}
+.emoji-flag_gs {
+ background-position: -460px -460px;
+}
+.emoji-flag_gt {
+ background-position: -480px 0;
+}
+.emoji-flag_gu {
+ background-position: -480px -20px;
+}
+.emoji-flag_gw {
+ background-position: -480px -40px;
+}
+.emoji-flag_gy {
+ background-position: -480px -60px;
+}
+.emoji-flag_hk {
+ background-position: -480px -80px;
+}
+.emoji-flag_hm {
+ background-position: -480px -100px;
+}
+.emoji-flag_hn {
+ background-position: -480px -120px;
+}
+.emoji-flag_hr {
+ background-position: -480px -140px;
+}
+.emoji-flag_ht {
+ background-position: -480px -160px;
+}
+.emoji-flag_hu {
+ background-position: -480px -180px;
+}
+.emoji-flag_ic {
+ background-position: -480px -200px;
+}
+.emoji-flag_id {
+ background-position: -480px -220px;
+}
+.emoji-flag_ie {
+ background-position: -480px -240px;
+}
+.emoji-flag_il {
+ background-position: -480px -260px;
+}
+.emoji-flag_im {
+ background-position: -480px -280px;
+}
+.emoji-flag_in {
+ background-position: -480px -300px;
+}
+.emoji-flag_io {
+ background-position: -480px -320px;
+}
+.emoji-flag_iq {
+ background-position: -480px -340px;
+}
+.emoji-flag_ir {
+ background-position: -480px -360px;
+}
+.emoji-flag_is {
+ background-position: -480px -380px;
+}
+.emoji-flag_it {
+ background-position: -480px -400px;
+}
+.emoji-flag_je {
+ background-position: -480px -420px;
+}
+.emoji-flag_jm {
+ background-position: -480px -440px;
+}
+.emoji-flag_jo {
+ background-position: -480px -460px;
+}
+.emoji-flag_jp {
+ background-position: 0 -480px;
+}
+.emoji-flag_ke {
+ background-position: -20px -480px;
+}
+.emoji-flag_kg {
+ background-position: -40px -480px;
+}
+.emoji-flag_kh {
+ background-position: -60px -480px;
+}
+.emoji-flag_ki {
+ background-position: -80px -480px;
+}
+.emoji-flag_km {
+ background-position: -100px -480px;
+}
+.emoji-flag_kn {
+ background-position: -120px -480px;
+}
+.emoji-flag_kp {
+ background-position: -140px -480px;
+}
+.emoji-flag_kr {
+ background-position: -160px -480px;
+}
+.emoji-flag_kw {
+ background-position: -180px -480px;
+}
+.emoji-flag_ky {
+ background-position: -200px -480px;
+}
+.emoji-flag_kz {
+ background-position: -220px -480px;
+}
+.emoji-flag_la {
+ background-position: -240px -480px;
+}
+.emoji-flag_lb {
+ background-position: -260px -480px;
+}
+.emoji-flag_lc {
+ background-position: -280px -480px;
+}
+.emoji-flag_li {
+ background-position: -300px -480px;
+}
+.emoji-flag_lk {
+ background-position: -320px -480px;
+}
+.emoji-flag_lr {
+ background-position: -340px -480px;
+}
+.emoji-flag_ls {
+ background-position: -360px -480px;
+}
+.emoji-flag_lt {
+ background-position: -380px -480px;
+}
+.emoji-flag_lu {
+ background-position: -400px -480px;
+}
+.emoji-flag_lv {
+ background-position: -420px -480px;
+}
+.emoji-flag_ly {
+ background-position: -440px -480px;
+}
+.emoji-flag_ma {
+ background-position: -460px -480px;
+}
+.emoji-flag_mc {
+ background-position: -480px -480px;
+}
+.emoji-flag_md {
+ background-position: -500px 0;
+}
+.emoji-flag_me {
+ background-position: -500px -20px;
+}
+.emoji-flag_mf {
+ background-position: -500px -40px;
+}
+.emoji-flag_mg {
+ background-position: -500px -60px;
+}
+.emoji-flag_mh {
+ background-position: -500px -80px;
+}
+.emoji-flag_mk {
+ background-position: -500px -100px;
+}
+.emoji-flag_ml {
+ background-position: -500px -120px;
+}
+.emoji-flag_mm {
+ background-position: -500px -140px;
+}
+.emoji-flag_mn {
+ background-position: -500px -160px;
+}
+.emoji-flag_mo {
+ background-position: -500px -180px;
+}
+.emoji-flag_mp {
+ background-position: -500px -200px;
+}
+.emoji-flag_mq {
+ background-position: -500px -220px;
+}
+.emoji-flag_mr {
+ background-position: -500px -240px;
+}
+.emoji-flag_ms {
+ background-position: -500px -260px;
+}
+.emoji-flag_mt {
+ background-position: -500px -280px;
+}
+.emoji-flag_mu {
+ background-position: -500px -300px;
+}
+.emoji-flag_mv {
+ background-position: -500px -320px;
+}
+.emoji-flag_mw {
+ background-position: -500px -340px;
+}
+.emoji-flag_mx {
+ background-position: -500px -360px;
+}
+.emoji-flag_my {
+ background-position: -500px -380px;
+}
+.emoji-flag_mz {
+ background-position: -500px -400px;
+}
+.emoji-flag_na {
+ background-position: -500px -420px;
+}
+.emoji-flag_nc {
+ background-position: -500px -440px;
+}
+.emoji-flag_ne {
+ background-position: -500px -460px;
+}
+.emoji-flag_nf {
+ background-position: -500px -480px;
+}
+.emoji-flag_ng {
+ background-position: 0 -500px;
+}
+.emoji-flag_ni {
+ background-position: -20px -500px;
+}
+.emoji-flag_nl {
+ background-position: -40px -500px;
+}
+.emoji-flag_no {
+ background-position: -60px -500px;
+}
+.emoji-flag_np {
+ background-position: -80px -500px;
+}
+.emoji-flag_nr {
+ background-position: -100px -500px;
+}
+.emoji-flag_nu {
+ background-position: -120px -500px;
+}
+.emoji-flag_nz {
+ background-position: -140px -500px;
+}
+.emoji-flag_om {
+ background-position: -160px -500px;
+}
+.emoji-flag_pa {
+ background-position: -180px -500px;
+}
+.emoji-flag_pe {
+ background-position: -200px -500px;
+}
+.emoji-flag_pf {
+ background-position: -220px -500px;
+}
+.emoji-flag_pg {
+ background-position: -240px -500px;
+}
+.emoji-flag_ph {
+ background-position: -260px -500px;
+}
+.emoji-flag_pk {
+ background-position: -280px -500px;
+}
+.emoji-flag_pl {
+ background-position: -300px -500px;
+}
+.emoji-flag_pm {
+ background-position: -320px -500px;
+}
+.emoji-flag_pn {
+ background-position: -340px -500px;
+}
+.emoji-flag_pr {
+ background-position: -360px -500px;
+}
+.emoji-flag_ps {
+ background-position: -380px -500px;
+}
+.emoji-flag_pt {
+ background-position: -400px -500px;
+}
+.emoji-flag_pw {
+ background-position: -420px -500px;
+}
+.emoji-flag_py {
+ background-position: -440px -500px;
+}
+.emoji-flag_qa {
+ background-position: -460px -500px;
+}
+.emoji-flag_re {
+ background-position: -480px -500px;
+}
+.emoji-flag_ro {
+ background-position: -500px -500px;
+}
+.emoji-flag_rs {
+ background-position: -520px 0;
+}
+.emoji-flag_ru {
+ background-position: -520px -20px;
+}
+.emoji-flag_rw {
+ background-position: -520px -40px;
+}
+.emoji-flag_sa {
+ background-position: -520px -60px;
+}
+.emoji-flag_sb {
+ background-position: -520px -80px;
+}
+.emoji-flag_sc {
+ background-position: -520px -100px;
+}
+.emoji-flag_sd {
+ background-position: -520px -120px;
+}
+.emoji-flag_se {
+ background-position: -520px -140px;
+}
+.emoji-flag_sg {
+ background-position: -520px -160px;
+}
+.emoji-flag_sh {
+ background-position: -520px -180px;
+}
+.emoji-flag_si {
+ background-position: -520px -200px;
+}
+.emoji-flag_sj {
+ background-position: -520px -220px;
+}
+.emoji-flag_sk {
+ background-position: -520px -240px;
+}
+.emoji-flag_sl {
+ background-position: -520px -260px;
+}
+.emoji-flag_sm {
+ background-position: -520px -280px;
+}
+.emoji-flag_sn {
+ background-position: -520px -300px;
+}
+.emoji-flag_so {
+ background-position: -520px -320px;
+}
+.emoji-flag_sr {
+ background-position: -520px -340px;
+}
+.emoji-flag_ss {
+ background-position: -520px -360px;
+}
+.emoji-flag_st {
+ background-position: -520px -380px;
+}
+.emoji-flag_sv {
+ background-position: -520px -400px;
+}
+.emoji-flag_sx {
+ background-position: -520px -420px;
+}
+.emoji-flag_sy {
+ background-position: -520px -440px;
+}
+.emoji-flag_sz {
+ background-position: -520px -460px;
+}
+.emoji-flag_ta {
+ background-position: -520px -480px;
+}
+.emoji-flag_tc {
+ background-position: -520px -500px;
+}
+.emoji-flag_td {
+ background-position: 0 -520px;
+}
+.emoji-flag_tf {
+ background-position: -20px -520px;
+}
+.emoji-flag_tg {
+ background-position: -40px -520px;
+}
+.emoji-flag_th {
+ background-position: -60px -520px;
+}
+.emoji-flag_tj {
+ background-position: -80px -520px;
+}
+.emoji-flag_tk {
+ background-position: -100px -520px;
+}
+.emoji-flag_tl {
+ background-position: -120px -520px;
+}
+.emoji-flag_tm {
+ background-position: -140px -520px;
+}
+.emoji-flag_tn {
+ background-position: -160px -520px;
+}
+.emoji-flag_to {
+ background-position: -180px -520px;
+}
+.emoji-flag_tr {
+ background-position: -200px -520px;
+}
+.emoji-flag_tt {
+ background-position: -220px -520px;
+}
+.emoji-flag_tv {
+ background-position: -240px -520px;
+}
+.emoji-flag_tw {
+ background-position: -260px -520px;
+}
+.emoji-flag_tz {
+ background-position: -280px -520px;
+}
+.emoji-flag_ua {
+ background-position: -300px -520px;
+}
+.emoji-flag_ug {
+ background-position: -320px -520px;
+}
+.emoji-flag_um {
+ background-position: -340px -520px;
+}
+.emoji-flag_us {
+ background-position: -360px -520px;
+}
+.emoji-flag_uy {
+ background-position: -380px -520px;
+}
+.emoji-flag_uz {
+ background-position: -400px -520px;
+}
+.emoji-flag_va {
+ background-position: -420px -520px;
+}
+.emoji-flag_vc {
+ background-position: -440px -520px;
+}
+.emoji-flag_ve {
+ background-position: -460px -520px;
+}
+.emoji-flag_vg {
+ background-position: -480px -520px;
+}
+.emoji-flag_vi {
+ background-position: -500px -520px;
+}
+.emoji-flag_vn {
+ background-position: -520px -520px;
+}
+.emoji-flag_vu {
+ background-position: -540px 0;
+}
+.emoji-flag_wf {
+ background-position: -540px -20px;
+}
+.emoji-flag_white {
+ background-position: -540px -40px;
+}
+.emoji-flag_ws {
+ background-position: -540px -60px;
+}
+.emoji-flag_xk {
+ background-position: -540px -80px;
+}
+.emoji-flag_ye {
+ background-position: -540px -100px;
+}
+.emoji-flag_yt {
+ background-position: -540px -120px;
+}
+.emoji-flag_za {
+ background-position: -540px -140px;
+}
+.emoji-flag_zm {
+ background-position: -540px -160px;
+}
+.emoji-flag_zw {
+ background-position: -540px -180px;
+}
+.emoji-flags {
+ background-position: -540px -200px;
+}
+.emoji-flashlight {
+ background-position: -540px -220px;
+}
+.emoji-fleur-de-lis {
+ background-position: -540px -240px;
+}
+.emoji-floppy_disk {
+ background-position: -540px -260px;
+}
+.emoji-flower_playing_cards {
+ background-position: -540px -280px;
+}
+.emoji-flushed {
+ background-position: -540px -300px;
+}
+.emoji-fog {
+ background-position: -540px -320px;
+}
+.emoji-foggy {
+ background-position: -540px -340px;
+}
+.emoji-football {
+ background-position: -540px -360px;
+}
+.emoji-footprints {
+ background-position: -540px -380px;
+}
+.emoji-fork_and_knife {
+ background-position: -540px -400px;
+}
+.emoji-fork_knife_plate {
+ background-position: -540px -420px;
+}
+.emoji-fountain {
+ background-position: -540px -440px;
+}
+.emoji-four {
+ background-position: -540px -460px;
+}
+.emoji-four_leaf_clover {
+ background-position: -540px -480px;
+}
+.emoji-fox {
+ background-position: -540px -500px;
+}
+.emoji-frame_photo {
+ background-position: -540px -520px;
+}
+.emoji-free {
+ background-position: 0 -540px;
+}
+.emoji-french_bread {
+ background-position: -20px -540px;
+}
+.emoji-fried_shrimp {
+ background-position: -40px -540px;
+}
+.emoji-fries {
+ background-position: -60px -540px;
+}
+.emoji-frog {
+ background-position: -80px -540px;
+}
+.emoji-frowning {
+ background-position: -100px -540px;
+}
+.emoji-frowning2 {
+ background-position: -120px -540px;
+}
+.emoji-fuelpump {
+ background-position: -140px -540px;
+}
+.emoji-full_moon {
+ background-position: -160px -540px;
+}
+.emoji-full_moon_with_face {
+ background-position: -180px -540px;
+}
+.emoji-game_die {
+ background-position: -200px -540px;
+}
+.emoji-gay_pride_flag {
+ background-position: -220px -540px;
+}
+.emoji-gear {
+ background-position: -240px -540px;
+}
+.emoji-gem {
+ background-position: -260px -540px;
+}
+.emoji-gemini {
+ background-position: -280px -540px;
+}
+.emoji-ghost {
+ background-position: -300px -540px;
+}
+.emoji-gift {
+ background-position: -320px -540px;
+}
+.emoji-gift_heart {
+ background-position: -340px -540px;
+}
+.emoji-girl {
+ background-position: -360px -540px;
+}
+.emoji-girl_tone1 {
+ background-position: -380px -540px;
+}
+.emoji-girl_tone2 {
+ background-position: -400px -540px;
+}
+.emoji-girl_tone3 {
+ background-position: -420px -540px;
+}
+.emoji-girl_tone4 {
+ background-position: -440px -540px;
+}
+.emoji-girl_tone5 {
+ background-position: -460px -540px;
+}
+.emoji-globe_with_meridians {
+ background-position: -480px -540px;
+}
+.emoji-goal {
+ background-position: -500px -540px;
+}
+.emoji-goat {
+ background-position: -520px -540px;
+}
+.emoji-golf {
+ background-position: -540px -540px;
+}
+.emoji-golfer {
+ background-position: -560px 0;
+}
+.emoji-gorilla {
+ background-position: -560px -20px;
+}
+.emoji-grapes {
+ background-position: -560px -40px;
+}
+.emoji-green_apple {
+ background-position: -560px -60px;
+}
+.emoji-green_book {
+ background-position: -560px -80px;
+}
+.emoji-green_heart {
+ background-position: -560px -100px;
+}
+.emoji-grey_exclamation {
+ background-position: -560px -120px;
+}
+.emoji-grey_question {
+ background-position: -560px -140px;
+}
+.emoji-grimacing {
+ background-position: -560px -160px;
+}
+.emoji-grin {
+ background-position: -560px -180px;
+}
+.emoji-grinning {
+ background-position: -560px -200px;
+}
+.emoji-guardsman {
+ background-position: -560px -220px;
+}
+.emoji-guardsman_tone1 {
+ background-position: -560px -240px;
+}
+.emoji-guardsman_tone2 {
+ background-position: -560px -260px;
+}
+.emoji-guardsman_tone3 {
+ background-position: -560px -280px;
+}
+.emoji-guardsman_tone4 {
+ background-position: -560px -300px;
+}
+.emoji-guardsman_tone5 {
+ background-position: -560px -320px;
+}
+.emoji-guitar {
+ background-position: -560px -340px;
+}
+.emoji-gun {
+ background-position: -560px -360px;
+}
+.emoji-haircut {
+ background-position: -560px -380px;
+}
+.emoji-haircut_tone1 {
+ background-position: -560px -400px;
+}
+.emoji-haircut_tone2 {
+ background-position: -560px -420px;
+}
+.emoji-haircut_tone3 {
+ background-position: -560px -440px;
+}
+.emoji-haircut_tone4 {
+ background-position: -560px -460px;
+}
+.emoji-haircut_tone5 {
+ background-position: -560px -480px;
+}
+.emoji-hamburger {
+ background-position: -560px -500px;
+}
+.emoji-hammer {
+ background-position: -560px -520px;
+}
+.emoji-hammer_pick {
+ background-position: -560px -540px;
+}
+.emoji-hamster {
+ background-position: 0 -560px;
+}
+.emoji-hand_splayed {
+ background-position: -20px -560px;
+}
+.emoji-hand_splayed_tone1 {
+ background-position: -40px -560px;
+}
+.emoji-hand_splayed_tone2 {
+ background-position: -60px -560px;
+}
+.emoji-hand_splayed_tone3 {
+ background-position: -80px -560px;
+}
+.emoji-hand_splayed_tone4 {
+ background-position: -100px -560px;
+}
+.emoji-hand_splayed_tone5 {
+ background-position: -120px -560px;
+}
+.emoji-handbag {
+ background-position: -140px -560px;
+}
+.emoji-handball {
+ background-position: -160px -560px;
+}
+.emoji-handball_tone1 {
+ background-position: -180px -560px;
+}
+.emoji-handball_tone2 {
+ background-position: -200px -560px;
+}
+.emoji-handball_tone3 {
+ background-position: -220px -560px;
+}
+.emoji-handball_tone4 {
+ background-position: -240px -560px;
+}
+.emoji-handball_tone5 {
+ background-position: -260px -560px;
+}
+.emoji-handshake {
+ background-position: -280px -560px;
+}
+.emoji-handshake_tone1 {
+ background-position: -300px -560px;
+}
+.emoji-handshake_tone2 {
+ background-position: -320px -560px;
+}
+.emoji-handshake_tone3 {
+ background-position: -340px -560px;
+}
+.emoji-handshake_tone4 {
+ background-position: -360px -560px;
+}
+.emoji-handshake_tone5 {
+ background-position: -380px -560px;
+}
+.emoji-hash {
+ background-position: -400px -560px;
+}
+.emoji-hatched_chick {
+ background-position: -420px -560px;
+}
+.emoji-hatching_chick {
+ background-position: -440px -560px;
+}
+.emoji-head_bandage {
+ background-position: -460px -560px;
+}
+.emoji-headphones {
+ background-position: -480px -560px;
+}
+.emoji-hear_no_evil {
+ background-position: -500px -560px;
+}
+.emoji-heart {
+ background-position: -520px -560px;
+}
+.emoji-heart_decoration {
+ background-position: -540px -560px;
+}
+.emoji-heart_exclamation {
+ background-position: -560px -560px;
+}
+.emoji-heart_eyes {
+ background-position: -580px 0;
+}
+.emoji-heart_eyes_cat {
+ background-position: -580px -20px;
+}
+.emoji-heartbeat {
+ background-position: -580px -40px;
+}
+.emoji-heartpulse {
+ background-position: -580px -60px;
+}
+.emoji-hearts {
+ background-position: -580px -80px;
+}
+.emoji-heavy_check_mark {
+ background-position: -580px -100px;
+}
+.emoji-heavy_division_sign {
+ background-position: -580px -120px;
+}
+.emoji-heavy_dollar_sign {
+ background-position: -580px -140px;
+}
+.emoji-heavy_minus_sign {
+ background-position: -580px -160px;
+}
+.emoji-heavy_multiplication_x {
+ background-position: -580px -180px;
+}
+.emoji-heavy_plus_sign {
+ background-position: -580px -200px;
+}
+.emoji-helicopter {
+ background-position: -580px -220px;
+}
+.emoji-helmet_with_cross {
+ background-position: -580px -240px;
+}
+.emoji-herb {
+ background-position: -580px -260px;
+}
+.emoji-hibiscus {
+ background-position: -580px -280px;
+}
+.emoji-high_brightness {
+ background-position: -580px -300px;
+}
+.emoji-high_heel {
+ background-position: -580px -320px;
+}
+.emoji-hockey {
+ background-position: -580px -340px;
+}
+.emoji-hole {
+ background-position: -580px -360px;
+}
+.emoji-homes {
+ background-position: -580px -380px;
+}
+.emoji-honey_pot {
+ background-position: -580px -400px;
+}
+.emoji-horse {
+ background-position: -580px -420px;
+}
+.emoji-horse_racing {
+ background-position: -580px -440px;
+}
+.emoji-horse_racing_tone1 {
+ background-position: -580px -460px;
+}
+.emoji-horse_racing_tone2 {
+ background-position: -580px -480px;
+}
+.emoji-horse_racing_tone3 {
+ background-position: -580px -500px;
+}
+.emoji-horse_racing_tone4 {
+ background-position: -580px -520px;
+}
+.emoji-horse_racing_tone5 {
+ background-position: -580px -540px;
+}
+.emoji-hospital {
+ background-position: -580px -560px;
+}
+.emoji-hot_pepper {
+ background-position: 0 -580px;
+}
+.emoji-hotdog {
+ background-position: -20px -580px;
+}
+.emoji-hotel {
+ background-position: -40px -580px;
+}
+.emoji-hotsprings {
+ background-position: -60px -580px;
+}
+.emoji-hourglass {
+ background-position: -80px -580px;
+}
+.emoji-hourglass_flowing_sand {
+ background-position: -100px -580px;
+}
+.emoji-house {
+ background-position: -120px -580px;
+}
+.emoji-house_abandoned {
+ background-position: -140px -580px;
+}
+.emoji-house_with_garden {
+ background-position: -160px -580px;
+}
+.emoji-hugging {
+ background-position: -180px -580px;
+}
+.emoji-hushed {
+ background-position: -200px -580px;
+}
+.emoji-ice_cream {
+ background-position: -220px -580px;
+}
+.emoji-ice_skate {
+ background-position: -240px -580px;
+}
+.emoji-icecream {
+ background-position: -260px -580px;
+}
+.emoji-id {
+ background-position: -280px -580px;
+}
+.emoji-ideograph_advantage {
+ background-position: -300px -580px;
+}
+.emoji-imp {
+ background-position: -320px -580px;
+}
+.emoji-inbox_tray {
+ background-position: -340px -580px;
+}
+.emoji-incoming_envelope {
+ background-position: -360px -580px;
+}
+.emoji-information_desk_person {
+ background-position: -380px -580px;
+}
+.emoji-information_desk_person_tone1 {
+ background-position: -400px -580px;
+}
+.emoji-information_desk_person_tone2 {
+ background-position: -420px -580px;
+}
+.emoji-information_desk_person_tone3 {
+ background-position: -440px -580px;
+}
+.emoji-information_desk_person_tone4 {
+ background-position: -460px -580px;
+}
+.emoji-information_desk_person_tone5 {
+ background-position: -480px -580px;
+}
+.emoji-information_source {
+ background-position: -500px -580px;
+}
+.emoji-innocent {
+ background-position: -520px -580px;
+}
+.emoji-interrobang {
+ background-position: -540px -580px;
+}
+.emoji-iphone {
+ background-position: -560px -580px;
+}
+.emoji-island {
+ background-position: -580px -580px;
+}
+.emoji-izakaya_lantern {
+ background-position: -600px 0;
+}
+.emoji-jack_o_lantern {
+ background-position: -600px -20px;
+}
+.emoji-japan {
+ background-position: -600px -40px;
+}
+.emoji-japanese_castle {
+ background-position: -600px -60px;
+}
+.emoji-japanese_goblin {
+ background-position: -600px -80px;
+}
+.emoji-japanese_ogre {
+ background-position: -600px -100px;
+}
+.emoji-jeans {
+ background-position: -600px -120px;
+}
+.emoji-joy {
+ background-position: -600px -140px;
+}
+.emoji-joy_cat {
+ background-position: -600px -160px;
+}
+.emoji-joystick {
+ background-position: -600px -180px;
+}
+.emoji-juggling {
+ background-position: -600px -200px;
+}
+.emoji-juggling_tone1 {
+ background-position: -600px -220px;
+}
+.emoji-juggling_tone2 {
+ background-position: -600px -240px;
+}
+.emoji-juggling_tone3 {
+ background-position: -600px -260px;
+}
+.emoji-juggling_tone4 {
+ background-position: -600px -280px;
+}
+.emoji-juggling_tone5 {
+ background-position: -600px -300px;
+}
+.emoji-kaaba {
+ background-position: -600px -320px;
+}
+.emoji-key {
+ background-position: -600px -340px;
+}
+.emoji-key2 {
+ background-position: -600px -360px;
+}
+.emoji-keyboard {
+ background-position: -600px -380px;
+}
+.emoji-kimono {
+ background-position: -600px -400px;
+}
+.emoji-kiss {
+ background-position: -600px -420px;
+}
+.emoji-kiss_mm {
+ background-position: -600px -440px;
+}
+.emoji-kiss_ww {
+ background-position: -600px -460px;
+}
+.emoji-kissing {
+ background-position: -600px -480px;
+}
+.emoji-kissing_cat {
+ background-position: -600px -500px;
+}
+.emoji-kissing_closed_eyes {
+ background-position: -600px -520px;
+}
+.emoji-kissing_heart {
+ background-position: -600px -540px;
+}
+.emoji-kissing_smiling_eyes {
+ background-position: -600px -560px;
+}
+.emoji-kiwi {
+ background-position: -600px -580px;
+}
+.emoji-knife {
+ background-position: 0 -600px;
+}
+.emoji-koala {
+ background-position: -20px -600px;
+}
+.emoji-koko {
+ background-position: -40px -600px;
+}
+.emoji-label {
+ background-position: -60px -600px;
+}
+.emoji-large_blue_circle {
+ background-position: -80px -600px;
+}
+.emoji-large_blue_diamond {
+ background-position: -100px -600px;
+}
+.emoji-large_orange_diamond {
+ background-position: -120px -600px;
+}
+.emoji-last_quarter_moon {
+ background-position: -140px -600px;
+}
+.emoji-last_quarter_moon_with_face {
+ background-position: -160px -600px;
+}
+.emoji-laughing {
+ background-position: -180px -600px;
+}
+.emoji-leaves {
+ background-position: -200px -600px;
+}
+.emoji-ledger {
+ background-position: -220px -600px;
+}
+.emoji-left_facing_fist {
+ background-position: -240px -600px;
+}
+.emoji-left_facing_fist_tone1 {
+ background-position: -260px -600px;
+}
+.emoji-left_facing_fist_tone2 {
+ background-position: -280px -600px;
+}
+.emoji-left_facing_fist_tone3 {
+ background-position: -300px -600px;
+}
+.emoji-left_facing_fist_tone4 {
+ background-position: -320px -600px;
+}
+.emoji-left_facing_fist_tone5 {
+ background-position: -340px -600px;
+}
+.emoji-left_luggage {
+ background-position: -360px -600px;
+}
+.emoji-left_right_arrow {
+ background-position: -380px -600px;
+}
+.emoji-leftwards_arrow_with_hook {
+ background-position: -400px -600px;
+}
+.emoji-lemon {
+ background-position: -420px -600px;
+}
+.emoji-leo {
+ background-position: -440px -600px;
+}
+.emoji-leopard {
+ background-position: -460px -600px;
+}
+.emoji-level_slider {
+ background-position: -480px -600px;
+}
+.emoji-levitate {
+ background-position: -500px -600px;
+}
+.emoji-libra {
+ background-position: -520px -600px;
+}
+.emoji-lifter {
+ background-position: -540px -600px;
+}
+.emoji-lifter_tone1 {
+ background-position: -560px -600px;
+}
+.emoji-lifter_tone2 {
+ background-position: -580px -600px;
+}
+.emoji-lifter_tone3 {
+ background-position: -600px -600px;
+}
+.emoji-lifter_tone4 {
+ background-position: -620px 0;
+}
+.emoji-lifter_tone5 {
+ background-position: -620px -20px;
+}
+.emoji-light_rail {
+ background-position: -620px -40px;
+}
+.emoji-link {
+ background-position: -620px -60px;
+}
+.emoji-lion_face {
+ background-position: -620px -80px;
+}
+.emoji-lips {
+ background-position: -620px -100px;
+}
+.emoji-lipstick {
+ background-position: -620px -120px;
+}
+.emoji-lizard {
+ background-position: -620px -140px;
+}
+.emoji-lock {
+ background-position: -620px -160px;
+}
+.emoji-lock_with_ink_pen {
+ background-position: -620px -180px;
+}
+.emoji-lollipop {
+ background-position: -620px -200px;
+}
+.emoji-loop {
+ background-position: -620px -220px;
+}
+.emoji-loud_sound {
+ background-position: -620px -240px;
+}
+.emoji-loudspeaker {
+ background-position: -620px -260px;
+}
+.emoji-love_hotel {
+ background-position: -620px -280px;
+}
+.emoji-love_letter {
+ background-position: -620px -300px;
+}
+.emoji-low_brightness {
+ background-position: -620px -320px;
+}
+.emoji-lying_face {
+ background-position: -620px -340px;
+}
+.emoji-m {
+ background-position: -620px -360px;
+}
+.emoji-mag {
+ background-position: -620px -380px;
+}
+.emoji-mag_right {
+ background-position: -620px -400px;
+}
+.emoji-mahjong {
+ background-position: -620px -420px;
+}
+.emoji-mailbox {
+ background-position: -620px -440px;
+}
+.emoji-mailbox_closed {
+ background-position: -620px -460px;
+}
+.emoji-mailbox_with_mail {
+ background-position: -620px -480px;
+}
+.emoji-mailbox_with_no_mail {
+ background-position: -620px -500px;
+}
+.emoji-man {
+ background-position: -620px -520px;
+}
+.emoji-man_dancing {
+ background-position: -620px -540px;
+}
+.emoji-man_dancing_tone1 {
+ background-position: -620px -560px;
+}
+.emoji-man_dancing_tone2 {
+ background-position: -620px -580px;
+}
+.emoji-man_dancing_tone3 {
+ background-position: -620px -600px;
+}
+.emoji-man_dancing_tone4 {
+ background-position: 0 -620px;
+}
+.emoji-man_dancing_tone5 {
+ background-position: -20px -620px;
+}
+.emoji-man_in_tuxedo {
+ background-position: -40px -620px;
+}
+.emoji-man_in_tuxedo_tone1 {
+ background-position: -60px -620px;
+}
+.emoji-man_in_tuxedo_tone2 {
+ background-position: -80px -620px;
+}
+.emoji-man_in_tuxedo_tone3 {
+ background-position: -100px -620px;
+}
+.emoji-man_in_tuxedo_tone4 {
+ background-position: -120px -620px;
+}
+.emoji-man_in_tuxedo_tone5 {
+ background-position: -140px -620px;
+}
+.emoji-man_tone1 {
+ background-position: -160px -620px;
+}
+.emoji-man_tone2 {
+ background-position: -180px -620px;
+}
+.emoji-man_tone3 {
+ background-position: -200px -620px;
+}
+.emoji-man_tone4 {
+ background-position: -220px -620px;
+}
+.emoji-man_tone5 {
+ background-position: -240px -620px;
+}
+.emoji-man_with_gua_pi_mao {
+ background-position: -260px -620px;
+}
+.emoji-man_with_gua_pi_mao_tone1 {
+ background-position: -280px -620px;
+}
+.emoji-man_with_gua_pi_mao_tone2 {
+ background-position: -300px -620px;
+}
+.emoji-man_with_gua_pi_mao_tone3 {
+ background-position: -320px -620px;
+}
+.emoji-man_with_gua_pi_mao_tone4 {
+ background-position: -340px -620px;
+}
+.emoji-man_with_gua_pi_mao_tone5 {
+ background-position: -360px -620px;
+}
+.emoji-man_with_turban {
+ background-position: -380px -620px;
+}
+.emoji-man_with_turban_tone1 {
+ background-position: -400px -620px;
+}
+.emoji-man_with_turban_tone2 {
+ background-position: -420px -620px;
+}
+.emoji-man_with_turban_tone3 {
+ background-position: -440px -620px;
+}
+.emoji-man_with_turban_tone4 {
+ background-position: -460px -620px;
+}
+.emoji-man_with_turban_tone5 {
+ background-position: -480px -620px;
+}
+.emoji-mans_shoe {
+ background-position: -500px -620px;
+}
+.emoji-map {
+ background-position: -520px -620px;
+}
+.emoji-maple_leaf {
+ background-position: -540px -620px;
+}
+.emoji-martial_arts_uniform {
+ background-position: -560px -620px;
+}
+.emoji-mask {
+ background-position: -580px -620px;
+}
+.emoji-massage {
+ background-position: -600px -620px;
+}
+.emoji-massage_tone1 {
+ background-position: -620px -620px;
+}
+.emoji-massage_tone2 {
+ background-position: -640px 0;
+}
+.emoji-massage_tone3 {
+ background-position: -640px -20px;
+}
+.emoji-massage_tone4 {
+ background-position: -640px -40px;
+}
+.emoji-massage_tone5 {
+ background-position: -640px -60px;
+}
+.emoji-meat_on_bone {
+ background-position: -640px -80px;
+}
+.emoji-medal {
+ background-position: -640px -100px;
+}
+.emoji-mega {
+ background-position: -640px -120px;
+}
+.emoji-melon {
+ background-position: -640px -140px;
+}
+.emoji-menorah {
+ background-position: -640px -160px;
+}
+.emoji-mens {
+ background-position: -640px -180px;
+}
+.emoji-metal {
+ background-position: -640px -200px;
+}
+.emoji-metal_tone1 {
+ background-position: -640px -220px;
+}
+.emoji-metal_tone2 {
+ background-position: -640px -240px;
+}
+.emoji-metal_tone3 {
+ background-position: -640px -260px;
+}
+.emoji-metal_tone4 {
+ background-position: -640px -280px;
+}
+.emoji-metal_tone5 {
+ background-position: -640px -300px;
+}
+.emoji-metro {
+ background-position: -640px -320px;
+}
+.emoji-microphone {
+ background-position: -640px -340px;
+}
+.emoji-microphone2 {
+ background-position: -640px -360px;
+}
+.emoji-microscope {
+ background-position: -640px -380px;
+}
+.emoji-middle_finger {
+ background-position: -640px -400px;
+}
+.emoji-middle_finger_tone1 {
+ background-position: -640px -420px;
+}
+.emoji-middle_finger_tone2 {
+ background-position: -640px -440px;
+}
+.emoji-middle_finger_tone3 {
+ background-position: -640px -460px;
+}
+.emoji-middle_finger_tone4 {
+ background-position: -640px -480px;
+}
+.emoji-middle_finger_tone5 {
+ background-position: -640px -500px;
+}
+.emoji-military_medal {
+ background-position: -640px -520px;
+}
+.emoji-milk {
+ background-position: -640px -540px;
+}
+.emoji-milky_way {
+ background-position: -640px -560px;
+}
+.emoji-minibus {
+ background-position: -640px -580px;
+}
+.emoji-minidisc {
+ background-position: -640px -600px;
+}
+.emoji-mobile_phone_off {
+ background-position: -640px -620px;
+}
+.emoji-money_mouth {
+ background-position: 0 -640px;
+}
+.emoji-money_with_wings {
+ background-position: -20px -640px;
+}
+.emoji-moneybag {
+ background-position: -40px -640px;
+}
+.emoji-monkey {
+ background-position: -60px -640px;
+}
+.emoji-monkey_face {
+ background-position: -80px -640px;
+}
+.emoji-monorail {
+ background-position: -100px -640px;
+}
+.emoji-mortar_board {
+ background-position: -120px -640px;
+}
+.emoji-mosque {
+ background-position: -140px -640px;
+}
+.emoji-motor_scooter {
+ background-position: -160px -640px;
+}
+.emoji-motorboat {
+ background-position: -180px -640px;
+}
+.emoji-motorcycle {
+ background-position: -200px -640px;
+}
+.emoji-motorway {
+ background-position: -220px -640px;
+}
+.emoji-mount_fuji {
+ background-position: -240px -640px;
+}
+.emoji-mountain {
+ background-position: -260px -640px;
+}
+.emoji-mountain_bicyclist {
+ background-position: -280px -640px;
+}
+.emoji-mountain_bicyclist_tone1 {
+ background-position: -300px -640px;
+}
+.emoji-mountain_bicyclist_tone2 {
+ background-position: -320px -640px;
+}
+.emoji-mountain_bicyclist_tone3 {
+ background-position: -340px -640px;
+}
+.emoji-mountain_bicyclist_tone4 {
+ background-position: -360px -640px;
+}
+.emoji-mountain_bicyclist_tone5 {
+ background-position: -380px -640px;
+}
+.emoji-mountain_cableway {
+ background-position: -400px -640px;
+}
+.emoji-mountain_railway {
+ background-position: -420px -640px;
+}
+.emoji-mountain_snow {
+ background-position: -440px -640px;
+}
+.emoji-mouse {
+ background-position: -460px -640px;
+}
+.emoji-mouse2 {
+ background-position: -480px -640px;
+}
+.emoji-mouse_three_button {
+ background-position: -500px -640px;
+}
+.emoji-movie_camera {
+ background-position: -520px -640px;
+}
+.emoji-moyai {
+ background-position: -540px -640px;
+}
+.emoji-mrs_claus {
+ background-position: -560px -640px;
+}
+.emoji-mrs_claus_tone1 {
+ background-position: -580px -640px;
+}
+.emoji-mrs_claus_tone2 {
+ background-position: -600px -640px;
+}
+.emoji-mrs_claus_tone3 {
+ background-position: -620px -640px;
+}
+.emoji-mrs_claus_tone4 {
+ background-position: -640px -640px;
+}
+.emoji-mrs_claus_tone5 {
+ background-position: -660px 0;
+}
+.emoji-muscle {
+ background-position: -660px -20px;
+}
+.emoji-muscle_tone1 {
+ background-position: -660px -40px;
+}
+.emoji-muscle_tone2 {
+ background-position: -660px -60px;
+}
+.emoji-muscle_tone3 {
+ background-position: -660px -80px;
+}
+.emoji-muscle_tone4 {
+ background-position: -660px -100px;
+}
+.emoji-muscle_tone5 {
+ background-position: -660px -120px;
+}
+.emoji-mushroom {
+ background-position: -660px -140px;
+}
+.emoji-musical_keyboard {
+ background-position: -660px -160px;
+}
+.emoji-musical_note {
+ background-position: -660px -180px;
+}
+.emoji-musical_score {
+ background-position: -660px -200px;
+}
+.emoji-mute {
+ background-position: -660px -220px;
+}
+.emoji-nail_care {
+ background-position: -660px -240px;
+}
+.emoji-nail_care_tone1 {
+ background-position: -660px -260px;
+}
+.emoji-nail_care_tone2 {
+ background-position: -660px -280px;
+}
+.emoji-nail_care_tone3 {
+ background-position: -660px -300px;
+}
+.emoji-nail_care_tone4 {
+ background-position: -660px -320px;
+}
+.emoji-nail_care_tone5 {
+ background-position: -660px -340px;
+}
+.emoji-name_badge {
+ background-position: -660px -360px;
+}
+.emoji-nauseated_face {
+ background-position: -660px -380px;
+}
+.emoji-necktie {
+ background-position: -660px -400px;
+}
+.emoji-negative_squared_cross_mark {
+ background-position: -660px -420px;
+}
+.emoji-nerd {
+ background-position: -660px -440px;
+}
+.emoji-neutral_face {
+ background-position: -660px -460px;
+}
+.emoji-new {
+ background-position: -660px -480px;
+}
+.emoji-new_moon {
+ background-position: -660px -500px;
+}
+.emoji-new_moon_with_face {
+ background-position: -660px -520px;
+}
+.emoji-newspaper {
+ background-position: -660px -540px;
+}
+.emoji-newspaper2 {
+ background-position: -660px -560px;
+}
+.emoji-ng {
+ background-position: -660px -580px;
+}
+.emoji-night_with_stars {
+ background-position: -660px -600px;
+}
+.emoji-nine {
+ background-position: -660px -620px;
+}
+.emoji-no_bell {
+ background-position: -660px -640px;
+}
+.emoji-no_bicycles {
+ background-position: 0 -660px;
+}
+.emoji-no_entry {
+ background-position: -20px -660px;
+}
+.emoji-no_entry_sign {
+ background-position: -40px -660px;
+}
+.emoji-no_good {
+ background-position: -60px -660px;
+}
+.emoji-no_good_tone1 {
+ background-position: -80px -660px;
+}
+.emoji-no_good_tone2 {
+ background-position: -100px -660px;
+}
+.emoji-no_good_tone3 {
+ background-position: -120px -660px;
+}
+.emoji-no_good_tone4 {
+ background-position: -140px -660px;
+}
+.emoji-no_good_tone5 {
+ background-position: -160px -660px;
+}
+.emoji-no_mobile_phones {
+ background-position: -180px -660px;
+}
+.emoji-no_mouth {
+ background-position: -200px -660px;
+}
+.emoji-no_pedestrians {
+ background-position: -220px -660px;
+}
+.emoji-no_smoking {
+ background-position: -240px -660px;
+}
+.emoji-non-potable_water {
+ background-position: -260px -660px;
+}
+.emoji-nose {
+ background-position: -280px -660px;
+}
+.emoji-nose_tone1 {
+ background-position: -300px -660px;
+}
+.emoji-nose_tone2 {
+ background-position: -320px -660px;
+}
+.emoji-nose_tone3 {
+ background-position: -340px -660px;
+}
+.emoji-nose_tone4 {
+ background-position: -360px -660px;
+}
+.emoji-nose_tone5 {
+ background-position: -380px -660px;
+}
+.emoji-notebook {
+ background-position: -400px -660px;
+}
+.emoji-notebook_with_decorative_cover {
+ background-position: -420px -660px;
+}
+.emoji-notepad_spiral {
+ background-position: -440px -660px;
+}
+.emoji-notes {
+ background-position: -460px -660px;
+}
+.emoji-nut_and_bolt {
+ background-position: -480px -660px;
+}
+.emoji-o {
+ background-position: -500px -660px;
+}
+.emoji-o2 {
+ background-position: -520px -660px;
+}
+.emoji-ocean {
+ background-position: -540px -660px;
+}
+.emoji-octagonal_sign {
+ background-position: -560px -660px;
+}
+.emoji-octopus {
+ background-position: -580px -660px;
+}
+.emoji-oden {
+ background-position: -600px -660px;
+}
+.emoji-office {
+ background-position: -620px -660px;
+}
+.emoji-oil {
+ background-position: -640px -660px;
+}
+.emoji-ok {
+ background-position: -660px -660px;
+}
+.emoji-ok_hand {
+ background-position: -680px 0;
+}
+.emoji-ok_hand_tone1 {
+ background-position: -680px -20px;
+}
+.emoji-ok_hand_tone2 {
+ background-position: -680px -40px;
+}
+.emoji-ok_hand_tone3 {
+ background-position: -680px -60px;
+}
+.emoji-ok_hand_tone4 {
+ background-position: -680px -80px;
+}
+.emoji-ok_hand_tone5 {
+ background-position: -680px -100px;
+}
+.emoji-ok_woman {
+ background-position: -680px -120px;
+}
+.emoji-ok_woman_tone1 {
+ background-position: -680px -140px;
+}
+.emoji-ok_woman_tone2 {
+ background-position: -680px -160px;
+}
+.emoji-ok_woman_tone3 {
+ background-position: -680px -180px;
+}
+.emoji-ok_woman_tone4 {
+ background-position: -680px -200px;
+}
+.emoji-ok_woman_tone5 {
+ background-position: -680px -220px;
+}
+.emoji-older_man {
+ background-position: -680px -240px;
+}
+.emoji-older_man_tone1 {
+ background-position: -680px -260px;
+}
+.emoji-older_man_tone2 {
+ background-position: -680px -280px;
+}
+.emoji-older_man_tone3 {
+ background-position: -680px -300px;
+}
+.emoji-older_man_tone4 {
+ background-position: -680px -320px;
+}
+.emoji-older_man_tone5 {
+ background-position: -680px -340px;
+}
+.emoji-older_woman {
+ background-position: -680px -360px;
+}
+.emoji-older_woman_tone1 {
+ background-position: -680px -380px;
+}
+.emoji-older_woman_tone2 {
+ background-position: -680px -400px;
+}
+.emoji-older_woman_tone3 {
+ background-position: -680px -420px;
+}
+.emoji-older_woman_tone4 {
+ background-position: -680px -440px;
+}
+.emoji-older_woman_tone5 {
+ background-position: -680px -460px;
+}
+.emoji-om_symbol {
+ background-position: -680px -480px;
+}
+.emoji-on {
+ background-position: -680px -500px;
+}
+.emoji-oncoming_automobile {
+ background-position: -680px -520px;
+}
+.emoji-oncoming_bus {
+ background-position: -680px -540px;
+}
+.emoji-oncoming_police_car {
+ background-position: -680px -560px;
+}
+.emoji-oncoming_taxi {
+ background-position: -680px -580px;
+}
+.emoji-one {
+ background-position: -680px -600px;
+}
+.emoji-open_file_folder {
+ background-position: -680px -620px;
+}
+.emoji-open_hands {
+ background-position: -680px -640px;
+}
+.emoji-open_hands_tone1 {
+ background-position: -680px -660px;
+}
+.emoji-open_hands_tone2 {
+ background-position: 0 -680px;
+}
+.emoji-open_hands_tone3 {
+ background-position: -20px -680px;
+}
+.emoji-open_hands_tone4 {
+ background-position: -40px -680px;
+}
+.emoji-open_hands_tone5 {
+ background-position: -60px -680px;
+}
+.emoji-open_mouth {
+ background-position: -80px -680px;
+}
+.emoji-ophiuchus {
+ background-position: -100px -680px;
+}
+.emoji-orange_book {
+ background-position: -120px -680px;
+}
+.emoji-orthodox_cross {
+ background-position: -140px -680px;
+}
+.emoji-outbox_tray {
+ background-position: -160px -680px;
+}
+.emoji-owl {
+ background-position: -180px -680px;
+}
+.emoji-ox {
+ background-position: -200px -680px;
+}
+.emoji-package {
+ background-position: -220px -680px;
+}
+.emoji-page_facing_up {
+ background-position: -240px -680px;
+}
+.emoji-page_with_curl {
+ background-position: -260px -680px;
+}
+.emoji-pager {
+ background-position: -280px -680px;
+}
+.emoji-paintbrush {
+ background-position: -300px -680px;
+}
+.emoji-palm_tree {
+ background-position: -320px -680px;
+}
+.emoji-pancakes {
+ background-position: -340px -680px;
+}
+.emoji-panda_face {
+ background-position: -360px -680px;
+}
+.emoji-paperclip {
+ background-position: -380px -680px;
+}
+.emoji-paperclips {
+ background-position: -400px -680px;
+}
+.emoji-park {
+ background-position: -420px -680px;
+}
+.emoji-parking {
+ background-position: -440px -680px;
+}
+.emoji-part_alternation_mark {
+ background-position: -460px -680px;
+}
+.emoji-partly_sunny {
+ background-position: -480px -680px;
+}
+.emoji-passport_control {
+ background-position: -500px -680px;
+}
+.emoji-pause_button {
+ background-position: -520px -680px;
+}
+.emoji-peace {
+ background-position: -540px -680px;
+}
+.emoji-peach {
+ background-position: -560px -680px;
+}
+.emoji-peanuts {
+ background-position: -580px -680px;
+}
+.emoji-pear {
+ background-position: -600px -680px;
+}
+.emoji-pen_ballpoint {
+ background-position: -620px -680px;
+}
+.emoji-pen_fountain {
+ background-position: -640px -680px;
+}
+.emoji-pencil {
+ background-position: -660px -680px;
+}
+.emoji-pencil2 {
+ background-position: -680px -680px;
+}
+.emoji-penguin {
+ background-position: -700px 0;
+}
+.emoji-pensive {
+ background-position: -700px -20px;
+}
+.emoji-performing_arts {
+ background-position: -700px -40px;
+}
+.emoji-persevere {
+ background-position: -700px -60px;
+}
+.emoji-person_frowning {
+ background-position: -700px -80px;
+}
+.emoji-person_frowning_tone1 {
+ background-position: -700px -100px;
+}
+.emoji-person_frowning_tone2 {
+ background-position: -700px -120px;
+}
+.emoji-person_frowning_tone3 {
+ background-position: -700px -140px;
+}
+.emoji-person_frowning_tone4 {
+ background-position: -700px -160px;
+}
+.emoji-person_frowning_tone5 {
+ background-position: -700px -180px;
+}
+.emoji-person_with_blond_hair {
+ background-position: -700px -200px;
+}
+.emoji-person_with_blond_hair_tone1 {
+ background-position: -700px -220px;
+}
+.emoji-person_with_blond_hair_tone2 {
+ background-position: -700px -240px;
+}
+.emoji-person_with_blond_hair_tone3 {
+ background-position: -700px -260px;
+}
+.emoji-person_with_blond_hair_tone4 {
+ background-position: -700px -280px;
+}
+.emoji-person_with_blond_hair_tone5 {
+ background-position: -700px -300px;
+}
+.emoji-person_with_pouting_face {
+ background-position: -700px -320px;
+}
+.emoji-person_with_pouting_face_tone1 {
+ background-position: -700px -340px;
+}
+.emoji-person_with_pouting_face_tone2 {
+ background-position: -700px -360px;
+}
+.emoji-person_with_pouting_face_tone3 {
+ background-position: -700px -380px;
+}
+.emoji-person_with_pouting_face_tone4 {
+ background-position: -700px -400px;
+}
+.emoji-person_with_pouting_face_tone5 {
+ background-position: -700px -420px;
+}
+.emoji-pick {
+ background-position: -700px -440px;
+}
+.emoji-pig {
+ background-position: -700px -460px;
+}
+.emoji-pig2 {
+ background-position: -700px -480px;
+}
+.emoji-pig_nose {
+ background-position: -700px -500px;
+}
+.emoji-pill {
+ background-position: -700px -520px;
+}
+.emoji-pineapple {
+ background-position: -700px -540px;
+}
+.emoji-ping_pong {
+ background-position: -700px -560px;
+}
+.emoji-pisces {
+ background-position: -700px -580px;
+}
+.emoji-pizza {
+ background-position: -700px -600px;
+}
+.emoji-place_of_worship {
+ background-position: -700px -620px;
+}
+.emoji-play_pause {
+ background-position: -700px -640px;
+}
+.emoji-point_down {
+ background-position: -700px -660px;
+}
+.emoji-point_down_tone1 {
+ background-position: -700px -680px;
+}
+.emoji-point_down_tone2 {
+ background-position: 0 -700px;
+}
+.emoji-point_down_tone3 {
+ background-position: -20px -700px;
+}
+.emoji-point_down_tone4 {
+ background-position: -40px -700px;
+}
+.emoji-point_down_tone5 {
+ background-position: -60px -700px;
+}
+.emoji-point_left {
+ background-position: -80px -700px;
+}
+.emoji-point_left_tone1 {
+ background-position: -100px -700px;
+}
+.emoji-point_left_tone2 {
+ background-position: -120px -700px;
+}
+.emoji-point_left_tone3 {
+ background-position: -140px -700px;
+}
+.emoji-point_left_tone4 {
+ background-position: -160px -700px;
+}
+.emoji-point_left_tone5 {
+ background-position: -180px -700px;
+}
+.emoji-point_right {
+ background-position: -200px -700px;
+}
+.emoji-point_right_tone1 {
+ background-position: -220px -700px;
+}
+.emoji-point_right_tone2 {
+ background-position: -240px -700px;
+}
+.emoji-point_right_tone3 {
+ background-position: -260px -700px;
+}
+.emoji-point_right_tone4 {
+ background-position: -280px -700px;
+}
+.emoji-point_right_tone5 {
+ background-position: -300px -700px;
+}
+.emoji-point_up {
+ background-position: -320px -700px;
+}
+.emoji-point_up_2 {
+ background-position: -340px -700px;
+}
+.emoji-point_up_2_tone1 {
+ background-position: -360px -700px;
+}
+.emoji-point_up_2_tone2 {
+ background-position: -380px -700px;
+}
+.emoji-point_up_2_tone3 {
+ background-position: -400px -700px;
+}
+.emoji-point_up_2_tone4 {
+ background-position: -420px -700px;
+}
+.emoji-point_up_2_tone5 {
+ background-position: -440px -700px;
+}
+.emoji-point_up_tone1 {
+ background-position: -460px -700px;
+}
+.emoji-point_up_tone2 {
+ background-position: -480px -700px;
+}
+.emoji-point_up_tone3 {
+ background-position: -500px -700px;
+}
+.emoji-point_up_tone4 {
+ background-position: -520px -700px;
+}
+.emoji-point_up_tone5 {
+ background-position: -540px -700px;
+}
+.emoji-police_car {
+ background-position: -560px -700px;
+}
+.emoji-poodle {
+ background-position: -580px -700px;
+}
+.emoji-poop {
+ background-position: -600px -700px;
+}
+.emoji-popcorn {
+ background-position: -620px -700px;
+}
+.emoji-post_office {
+ background-position: -640px -700px;
+}
+.emoji-postal_horn {
+ background-position: -660px -700px;
+}
+.emoji-postbox {
+ background-position: -680px -700px;
+}
+.emoji-potable_water {
+ background-position: -700px -700px;
+}
+.emoji-potato {
+ background-position: -720px 0;
+}
+.emoji-pouch {
+ background-position: -720px -20px;
+}
+.emoji-poultry_leg {
+ background-position: -720px -40px;
+}
+.emoji-pound {
+ background-position: -720px -60px;
+}
+.emoji-pouting_cat {
+ background-position: -720px -80px;
+}
+.emoji-pray {
+ background-position: -720px -100px;
+}
+.emoji-pray_tone1 {
+ background-position: -720px -120px;
+}
+.emoji-pray_tone2 {
+ background-position: -720px -140px;
+}
+.emoji-pray_tone3 {
+ background-position: -720px -160px;
+}
+.emoji-pray_tone4 {
+ background-position: -720px -180px;
+}
+.emoji-pray_tone5 {
+ background-position: -720px -200px;
+}
+.emoji-prayer_beads {
+ background-position: -720px -220px;
+}
+.emoji-pregnant_woman {
+ background-position: -720px -240px;
+}
+.emoji-pregnant_woman_tone1 {
+ background-position: -720px -260px;
+}
+.emoji-pregnant_woman_tone2 {
+ background-position: -720px -280px;
+}
+.emoji-pregnant_woman_tone3 {
+ background-position: -720px -300px;
+}
+.emoji-pregnant_woman_tone4 {
+ background-position: -720px -320px;
+}
+.emoji-pregnant_woman_tone5 {
+ background-position: -720px -340px;
+}
+.emoji-prince {
+ background-position: -720px -360px;
+}
+.emoji-prince_tone1 {
+ background-position: -720px -380px;
+}
+.emoji-prince_tone2 {
+ background-position: -720px -400px;
+}
+.emoji-prince_tone3 {
+ background-position: -720px -420px;
+}
+.emoji-prince_tone4 {
+ background-position: -720px -440px;
+}
+.emoji-prince_tone5 {
+ background-position: -720px -460px;
+}
+.emoji-princess {
+ background-position: -720px -480px;
+}
+.emoji-princess_tone1 {
+ background-position: -720px -500px;
+}
+.emoji-princess_tone2 {
+ background-position: -720px -520px;
+}
+.emoji-princess_tone3 {
+ background-position: -720px -540px;
+}
+.emoji-princess_tone4 {
+ background-position: -720px -560px;
+}
+.emoji-princess_tone5 {
+ background-position: -720px -580px;
+}
+.emoji-printer {
+ background-position: -720px -600px;
+}
+.emoji-projector {
+ background-position: -720px -620px;
+}
+.emoji-punch {
+ background-position: -720px -640px;
+}
+.emoji-punch_tone1 {
+ background-position: -720px -660px;
+}
+.emoji-punch_tone2 {
+ background-position: -720px -680px;
+}
+.emoji-punch_tone3 {
+ background-position: -720px -700px;
+}
+.emoji-punch_tone4 {
+ background-position: 0 -720px;
+}
+.emoji-punch_tone5 {
+ background-position: -20px -720px;
+}
+.emoji-purple_heart {
+ background-position: -40px -720px;
+}
+.emoji-purse {
+ background-position: -60px -720px;
+}
+.emoji-pushpin {
+ background-position: -80px -720px;
+}
+.emoji-put_litter_in_its_place {
+ background-position: -100px -720px;
+}
+.emoji-question {
+ background-position: -120px -720px;
+}
+.emoji-rabbit {
+ background-position: -140px -720px;
+}
+.emoji-rabbit2 {
+ background-position: -160px -720px;
+}
+.emoji-race_car {
+ background-position: -180px -720px;
+}
+.emoji-racehorse {
+ background-position: -200px -720px;
+}
+.emoji-radio {
+ background-position: -220px -720px;
+}
+.emoji-radio_button {
+ background-position: -240px -720px;
+}
+.emoji-radioactive {
+ background-position: -260px -720px;
+}
+.emoji-rage {
+ background-position: -280px -720px;
+}
+.emoji-railway_car {
+ background-position: -300px -720px;
+}
+.emoji-railway_track {
+ background-position: -320px -720px;
+}
+.emoji-rainbow {
+ background-position: -340px -720px;
+}
+.emoji-raised_back_of_hand {
+ background-position: -360px -720px;
+}
+.emoji-raised_back_of_hand_tone1 {
+ background-position: -380px -720px;
+}
+.emoji-raised_back_of_hand_tone2 {
+ background-position: -400px -720px;
+}
+.emoji-raised_back_of_hand_tone3 {
+ background-position: -420px -720px;
+}
+.emoji-raised_back_of_hand_tone4 {
+ background-position: -440px -720px;
+}
+.emoji-raised_back_of_hand_tone5 {
+ background-position: -460px -720px;
+}
+.emoji-raised_hand {
+ background-position: -480px -720px;
+}
+.emoji-raised_hand_tone1 {
+ background-position: -500px -720px;
+}
+.emoji-raised_hand_tone2 {
+ background-position: -520px -720px;
+}
+.emoji-raised_hand_tone3 {
+ background-position: -540px -720px;
+}
+.emoji-raised_hand_tone4 {
+ background-position: -560px -720px;
+}
+.emoji-raised_hand_tone5 {
+ background-position: -580px -720px;
+}
+.emoji-raised_hands {
+ background-position: -600px -720px;
+}
+.emoji-raised_hands_tone1 {
+ background-position: -620px -720px;
+}
+.emoji-raised_hands_tone2 {
+ background-position: -640px -720px;
+}
+.emoji-raised_hands_tone3 {
+ background-position: -660px -720px;
+}
+.emoji-raised_hands_tone4 {
+ background-position: -680px -720px;
+}
+.emoji-raised_hands_tone5 {
+ background-position: -700px -720px;
+}
+.emoji-raising_hand {
+ background-position: -720px -720px;
+}
+.emoji-raising_hand_tone1 {
+ background-position: -740px 0;
+}
+.emoji-raising_hand_tone2 {
+ background-position: -740px -20px;
+}
+.emoji-raising_hand_tone3 {
+ background-position: -740px -40px;
+}
+.emoji-raising_hand_tone4 {
+ background-position: -740px -60px;
+}
+.emoji-raising_hand_tone5 {
+ background-position: -740px -80px;
+}
+.emoji-ram {
+ background-position: -740px -100px;
+}
+.emoji-ramen {
+ background-position: -740px -120px;
+}
+.emoji-rat {
+ background-position: -740px -140px;
+}
+.emoji-record_button {
+ background-position: -740px -160px;
+}
+.emoji-recycle {
+ background-position: -740px -180px;
+}
+.emoji-red_car {
+ background-position: -740px -200px;
+}
+.emoji-red_circle {
+ background-position: -740px -220px;
+}
+.emoji-registered {
+ background-position: -740px -240px;
+}
+.emoji-relaxed {
+ background-position: -740px -260px;
+}
+.emoji-relieved {
+ background-position: -740px -280px;
+}
+.emoji-reminder_ribbon {
+ background-position: -740px -300px;
+}
+.emoji-repeat {
+ background-position: -740px -320px;
+}
+.emoji-repeat_one {
+ background-position: -740px -340px;
+}
+.emoji-restroom {
+ background-position: -740px -360px;
+}
+.emoji-revolving_hearts {
+ background-position: -740px -380px;
+}
+.emoji-rewind {
+ background-position: -740px -400px;
+}
+.emoji-rhino {
+ background-position: -740px -420px;
+}
+.emoji-ribbon {
+ background-position: -740px -440px;
+}
+.emoji-rice {
+ background-position: -740px -460px;
+}
+.emoji-rice_ball {
+ background-position: -740px -480px;
+}
+.emoji-rice_cracker {
+ background-position: -740px -500px;
+}
+.emoji-rice_scene {
+ background-position: -740px -520px;
+}
+.emoji-right_facing_fist {
+ background-position: -740px -540px;
+}
+.emoji-right_facing_fist_tone1 {
+ background-position: -740px -560px;
+}
+.emoji-right_facing_fist_tone2 {
+ background-position: -740px -580px;
+}
+.emoji-right_facing_fist_tone3 {
+ background-position: -740px -600px;
+}
+.emoji-right_facing_fist_tone4 {
+ background-position: -740px -620px;
+}
+.emoji-right_facing_fist_tone5 {
+ background-position: -740px -640px;
+}
+.emoji-ring {
+ background-position: -740px -660px;
+}
+.emoji-robot {
+ background-position: -740px -680px;
+}
+.emoji-rocket {
+ background-position: -740px -700px;
+}
+.emoji-rofl {
+ background-position: -740px -720px;
+}
+.emoji-roller_coaster {
+ background-position: 0 -740px;
+}
+.emoji-rolling_eyes {
+ background-position: -20px -740px;
+}
+.emoji-rooster {
+ background-position: -40px -740px;
+}
+.emoji-rose {
+ background-position: -60px -740px;
+}
+.emoji-rosette {
+ background-position: -80px -740px;
+}
+.emoji-rotating_light {
+ background-position: -100px -740px;
+}
+.emoji-round_pushpin {
+ background-position: -120px -740px;
+}
+.emoji-rowboat {
+ background-position: -140px -740px;
+}
+.emoji-rowboat_tone1 {
+ background-position: -160px -740px;
+}
+.emoji-rowboat_tone2 {
+ background-position: -180px -740px;
+}
+.emoji-rowboat_tone3 {
+ background-position: -200px -740px;
+}
+.emoji-rowboat_tone4 {
+ background-position: -220px -740px;
+}
+.emoji-rowboat_tone5 {
+ background-position: -240px -740px;
+}
+.emoji-rugby_football {
+ background-position: -260px -740px;
+}
+.emoji-runner {
+ background-position: -280px -740px;
+}
+.emoji-runner_tone1 {
+ background-position: -300px -740px;
+}
+.emoji-runner_tone2 {
+ background-position: -320px -740px;
+}
+.emoji-runner_tone3 {
+ background-position: -340px -740px;
+}
+.emoji-runner_tone4 {
+ background-position: -360px -740px;
+}
+.emoji-runner_tone5 {
+ background-position: -380px -740px;
+}
+.emoji-running_shirt_with_sash {
+ background-position: -400px -740px;
+}
+.emoji-sa {
+ background-position: -420px -740px;
+}
+.emoji-sagittarius {
+ background-position: -440px -740px;
+}
+.emoji-sailboat {
+ background-position: -460px -740px;
+}
+.emoji-sake {
+ background-position: -480px -740px;
+}
+.emoji-salad {
+ background-position: -500px -740px;
+}
+.emoji-sandal {
+ background-position: -520px -740px;
+}
+.emoji-santa {
+ background-position: -540px -740px;
+}
+.emoji-santa_tone1 {
+ background-position: -560px -740px;
+}
+.emoji-santa_tone2 {
+ background-position: -580px -740px;
+}
+.emoji-santa_tone3 {
+ background-position: -600px -740px;
+}
+.emoji-santa_tone4 {
+ background-position: -620px -740px;
+}
+.emoji-santa_tone5 {
+ background-position: -640px -740px;
+}
+.emoji-satellite {
+ background-position: -660px -740px;
+}
+.emoji-satellite_orbital {
+ background-position: -680px -740px;
+}
+.emoji-saxophone {
+ background-position: -700px -740px;
+}
+.emoji-scales {
+ background-position: -720px -740px;
+}
+.emoji-school {
+ background-position: -740px -740px;
+}
+.emoji-school_satchel {
+ background-position: -760px 0;
+}
+.emoji-scissors {
+ background-position: -760px -20px;
+}
+.emoji-scooter {
+ background-position: -760px -40px;
+}
+.emoji-scorpion {
+ background-position: -760px -60px;
+}
+.emoji-scorpius {
+ background-position: -760px -80px;
+}
+.emoji-scream {
+ background-position: -760px -100px;
+}
+.emoji-scream_cat {
+ background-position: -760px -120px;
+}
+.emoji-scroll {
+ background-position: -760px -140px;
+}
+.emoji-seat {
+ background-position: -760px -160px;
+}
+.emoji-second_place {
+ background-position: -760px -180px;
+}
+.emoji-secret {
+ background-position: -760px -200px;
+}
+.emoji-see_no_evil {
+ background-position: -760px -220px;
+}
+.emoji-seedling {
+ background-position: -760px -240px;
+}
+.emoji-selfie {
+ background-position: -760px -260px;
+}
+.emoji-selfie_tone1 {
+ background-position: -760px -280px;
+}
+.emoji-selfie_tone2 {
+ background-position: -760px -300px;
+}
+.emoji-selfie_tone3 {
+ background-position: -760px -320px;
+}
+.emoji-selfie_tone4 {
+ background-position: -760px -340px;
+}
+.emoji-selfie_tone5 {
+ background-position: -760px -360px;
+}
+.emoji-seven {
+ background-position: -760px -380px;
+}
+.emoji-shallow_pan_of_food {
+ background-position: -760px -400px;
+}
+.emoji-shamrock {
+ background-position: -760px -420px;
+}
+.emoji-shark {
+ background-position: -760px -440px;
+}
+.emoji-shaved_ice {
+ background-position: -760px -460px;
+}
+.emoji-sheep {
+ background-position: -760px -480px;
+}
+.emoji-shell {
+ background-position: -760px -500px;
+}
+.emoji-shield {
+ background-position: -760px -520px;
+}
+.emoji-shinto_shrine {
+ background-position: -760px -540px;
+}
+.emoji-ship {
+ background-position: -760px -560px;
+}
+.emoji-shirt {
+ background-position: -760px -580px;
+}
+.emoji-shopping_bags {
+ background-position: -760px -600px;
+}
+.emoji-shopping_cart {
+ background-position: -760px -620px;
+}
+.emoji-shower {
+ background-position: -760px -640px;
+}
+.emoji-shrimp {
+ background-position: -760px -660px;
+}
+.emoji-shrug {
+ background-position: -760px -680px;
+}
+.emoji-shrug_tone1 {
+ background-position: -760px -700px;
+}
+.emoji-shrug_tone2 {
+ background-position: -760px -720px;
+}
+.emoji-shrug_tone3 {
+ background-position: -760px -740px;
+}
+.emoji-shrug_tone4 {
+ background-position: 0 -760px;
+}
+.emoji-shrug_tone5 {
+ background-position: -20px -760px;
+}
+.emoji-signal_strength {
+ background-position: -40px -760px;
+}
+.emoji-six {
+ background-position: -60px -760px;
+}
+.emoji-six_pointed_star {
+ background-position: -80px -760px;
+}
+.emoji-ski {
+ background-position: -100px -760px;
+}
+.emoji-skier {
+ background-position: -120px -760px;
+}
+.emoji-skull {
+ background-position: -140px -760px;
+}
+.emoji-skull_crossbones {
+ background-position: -160px -760px;
+}
+.emoji-sleeping {
+ background-position: -180px -760px;
+}
+.emoji-sleeping_accommodation {
+ background-position: -200px -760px;
+}
+.emoji-sleepy {
+ background-position: -220px -760px;
+}
+.emoji-slight_frown {
+ background-position: -240px -760px;
+}
+.emoji-slight_smile {
+ background-position: -260px -760px;
+}
+.emoji-slot_machine {
+ background-position: -280px -760px;
+}
+.emoji-small_blue_diamond {
+ background-position: -300px -760px;
+}
+.emoji-small_orange_diamond {
+ background-position: -320px -760px;
+}
+.emoji-small_red_triangle {
+ background-position: -340px -760px;
+}
+.emoji-small_red_triangle_down {
+ background-position: -360px -760px;
+}
+.emoji-smile {
+ background-position: -380px -760px;
+}
+.emoji-smile_cat {
+ background-position: -400px -760px;
+}
+.emoji-smiley {
+ background-position: -420px -760px;
+}
+.emoji-smiley_cat {
+ background-position: -440px -760px;
+}
+.emoji-smiling_imp {
+ background-position: -460px -760px;
+}
+.emoji-smirk {
+ background-position: -480px -760px;
+}
+.emoji-smirk_cat {
+ background-position: -500px -760px;
+}
+.emoji-smoking {
+ background-position: -520px -760px;
+}
+.emoji-snail {
+ background-position: -540px -760px;
+}
+.emoji-snake {
+ background-position: -560px -760px;
+}
+.emoji-sneezing_face {
+ background-position: -580px -760px;
+}
+.emoji-snowboarder {
+ background-position: -600px -760px;
+}
+.emoji-snowflake {
+ background-position: -620px -760px;
+}
+.emoji-snowman {
+ background-position: -640px -760px;
+}
+.emoji-snowman2 {
+ background-position: -660px -760px;
+}
+.emoji-sob {
+ background-position: -680px -760px;
+}
+.emoji-soccer {
+ background-position: -700px -760px;
+}
+.emoji-soon {
+ background-position: -720px -760px;
+}
+.emoji-sos {
+ background-position: -740px -760px;
+}
+.emoji-sound {
+ background-position: -760px -760px;
+}
+.emoji-space_invader {
+ background-position: -780px 0;
+}
+.emoji-spades {
+ background-position: -780px -20px;
+}
+.emoji-spaghetti {
+ background-position: -780px -40px;
+}
+.emoji-sparkle {
+ background-position: -780px -60px;
+}
+.emoji-sparkler {
+ background-position: -780px -80px;
+}
+.emoji-sparkles {
+ background-position: -780px -100px;
+}
+.emoji-sparkling_heart {
+ background-position: -780px -120px;
+}
+.emoji-speak_no_evil {
+ background-position: -780px -140px;
+}
+.emoji-speaker {
+ background-position: -780px -160px;
+}
+.emoji-speaking_head {
+ background-position: -780px -180px;
+}
+.emoji-speech_balloon {
+ background-position: -780px -200px;
+}
+.emoji-speech_left {
+ background-position: -780px -220px;
+}
+.emoji-speedboat {
+ background-position: -780px -240px;
+}
+.emoji-spider {
+ background-position: -780px -260px;
+}
+.emoji-spider_web {
+ background-position: -780px -280px;
+}
+.emoji-spoon {
+ background-position: -780px -300px;
+}
+.emoji-spy {
+ background-position: -780px -320px;
+}
+.emoji-spy_tone1 {
+ background-position: -780px -340px;
+}
+.emoji-spy_tone2 {
+ background-position: -780px -360px;
+}
+.emoji-spy_tone3 {
+ background-position: -780px -380px;
+}
+.emoji-spy_tone4 {
+ background-position: -780px -400px;
+}
+.emoji-spy_tone5 {
+ background-position: -780px -420px;
+}
+.emoji-squid {
+ background-position: -780px -440px;
+}
+.emoji-stadium {
+ background-position: -780px -460px;
+}
+.emoji-star {
+ background-position: -780px -480px;
+}
+.emoji-star2 {
+ background-position: -780px -500px;
+}
+.emoji-star_and_crescent {
+ background-position: -780px -520px;
+}
+.emoji-star_of_david {
+ background-position: -780px -540px;
+}
+.emoji-stars {
+ background-position: -780px -560px;
+}
+.emoji-station {
+ background-position: -780px -580px;
+}
+.emoji-statue_of_liberty {
+ background-position: -780px -600px;
+}
+.emoji-steam_locomotive {
+ background-position: -780px -620px;
+}
+.emoji-stew {
+ background-position: -780px -640px;
+}
+.emoji-stop_button {
+ background-position: -780px -660px;
+}
+.emoji-stopwatch {
+ background-position: -780px -680px;
+}
+.emoji-straight_ruler {
+ background-position: -780px -700px;
+}
+.emoji-strawberry {
+ background-position: -780px -720px;
+}
+.emoji-stuck_out_tongue {
+ background-position: -780px -740px;
+}
+.emoji-stuck_out_tongue_closed_eyes {
+ background-position: -780px -760px;
+}
+.emoji-stuck_out_tongue_winking_eye {
+ background-position: 0 -780px;
+}
+.emoji-stuffed_flatbread {
+ background-position: -20px -780px;
+}
+.emoji-sun_with_face {
+ background-position: -40px -780px;
+}
+.emoji-sunflower {
+ background-position: -60px -780px;
+}
+.emoji-sunglasses {
+ background-position: -80px -780px;
+}
+.emoji-sunny {
+ background-position: -100px -780px;
+}
+.emoji-sunrise {
+ background-position: -120px -780px;
+}
+.emoji-sunrise_over_mountains {
+ background-position: -140px -780px;
+}
+.emoji-surfer {
+ background-position: -160px -780px;
+}
+.emoji-surfer_tone1 {
+ background-position: -180px -780px;
+}
+.emoji-surfer_tone2 {
+ background-position: -200px -780px;
+}
+.emoji-surfer_tone3 {
+ background-position: -220px -780px;
+}
+.emoji-surfer_tone4 {
+ background-position: -240px -780px;
+}
+.emoji-surfer_tone5 {
+ background-position: -260px -780px;
+}
+.emoji-sushi {
+ background-position: -280px -780px;
+}
+.emoji-suspension_railway {
+ background-position: -300px -780px;
+}
+.emoji-sweat {
+ background-position: -320px -780px;
+}
+.emoji-sweat_drops {
+ background-position: -340px -780px;
+}
+.emoji-sweat_smile {
+ background-position: -360px -780px;
+}
+.emoji-sweet_potato {
+ background-position: -380px -780px;
+}
+.emoji-swimmer {
+ background-position: -400px -780px;
+}
+.emoji-swimmer_tone1 {
+ background-position: -420px -780px;
+}
+.emoji-swimmer_tone2 {
+ background-position: -440px -780px;
+}
+.emoji-swimmer_tone3 {
+ background-position: -460px -780px;
+}
+.emoji-swimmer_tone4 {
+ background-position: -480px -780px;
+}
+.emoji-swimmer_tone5 {
+ background-position: -500px -780px;
+}
+.emoji-symbols {
+ background-position: -520px -780px;
+}
+.emoji-synagogue {
+ background-position: -540px -780px;
+}
+.emoji-syringe {
+ background-position: -560px -780px;
+}
+.emoji-taco {
+ background-position: -580px -780px;
+}
+.emoji-tada {
+ background-position: -600px -780px;
+}
+.emoji-tanabata_tree {
+ background-position: -620px -780px;
+}
+.emoji-tangerine {
+ background-position: -640px -780px;
+}
+.emoji-taurus {
+ background-position: -660px -780px;
+}
+.emoji-taxi {
+ background-position: -680px -780px;
+}
+.emoji-tea {
+ background-position: -700px -780px;
+}
+.emoji-telephone {
+ background-position: -720px -780px;
+}
+.emoji-telephone_receiver {
+ background-position: -740px -780px;
+}
+.emoji-telescope {
+ background-position: -760px -780px;
+}
+.emoji-ten {
+ background-position: -780px -780px;
+}
+.emoji-tennis {
+ background-position: -800px 0;
+}
+.emoji-tent {
+ background-position: -800px -20px;
+}
+.emoji-thermometer {
+ background-position: -800px -40px;
+}
+.emoji-thermometer_face {
+ background-position: -800px -60px;
+}
+.emoji-thinking {
+ background-position: -800px -80px;
+}
+.emoji-third_place {
+ background-position: -800px -100px;
+}
+.emoji-thought_balloon {
+ background-position: -800px -120px;
+}
+.emoji-three {
+ background-position: -800px -140px;
+}
+.emoji-thumbsdown {
+ background-position: -800px -160px;
+}
+.emoji-thumbsdown_tone1 {
+ background-position: -800px -180px;
+}
+.emoji-thumbsdown_tone2 {
+ background-position: -800px -200px;
+}
+.emoji-thumbsdown_tone3 {
+ background-position: -800px -220px;
+}
+.emoji-thumbsdown_tone4 {
+ background-position: -800px -240px;
+}
+.emoji-thumbsdown_tone5 {
+ background-position: -800px -260px;
+}
+.emoji-thumbsup {
+ background-position: -800px -280px;
+}
+.emoji-thumbsup_tone1 {
+ background-position: -800px -300px;
+}
+.emoji-thumbsup_tone2 {
+ background-position: -800px -320px;
+}
+.emoji-thumbsup_tone3 {
+ background-position: -800px -340px;
+}
+.emoji-thumbsup_tone4 {
+ background-position: -800px -360px;
+}
+.emoji-thumbsup_tone5 {
+ background-position: -800px -380px;
+}
+.emoji-thunder_cloud_rain {
+ background-position: -800px -400px;
+}
+.emoji-ticket {
+ background-position: -800px -420px;
+}
+.emoji-tickets {
+ background-position: -800px -440px;
+}
+.emoji-tiger {
+ background-position: -800px -460px;
+}
+.emoji-tiger2 {
+ background-position: -800px -480px;
+}
+.emoji-timer {
+ background-position: -800px -500px;
+}
+.emoji-tired_face {
+ background-position: -800px -520px;
+}
+.emoji-tm {
+ background-position: -800px -540px;
+}
+.emoji-toilet {
+ background-position: -800px -560px;
+}
+.emoji-tokyo_tower {
+ background-position: -800px -580px;
+}
+.emoji-tomato {
+ background-position: -800px -600px;
+}
+.emoji-tone1 {
+ background-position: -800px -620px;
+}
+.emoji-tone2 {
+ background-position: -800px -640px;
+}
+.emoji-tone3 {
+ background-position: -800px -660px;
+}
+.emoji-tone4 {
+ background-position: -800px -680px;
+}
+.emoji-tone5 {
+ background-position: -800px -700px;
+}
+.emoji-tongue {
+ background-position: -800px -720px;
+}
+.emoji-tools {
+ background-position: -800px -740px;
+}
+.emoji-top {
+ background-position: -800px -760px;
+}
+.emoji-tophat {
+ background-position: -800px -780px;
+}
+.emoji-track_next {
+ background-position: 0 -800px;
+}
+.emoji-track_previous {
+ background-position: -20px -800px;
+}
+.emoji-trackball {
+ background-position: -40px -800px;
+}
+.emoji-tractor {
+ background-position: -60px -800px;
+}
+.emoji-traffic_light {
+ background-position: -80px -800px;
+}
+.emoji-train {
+ background-position: -100px -800px;
+}
+.emoji-train2 {
+ background-position: -120px -800px;
+}
+.emoji-tram {
+ background-position: -140px -800px;
+}
+.emoji-triangular_flag_on_post {
+ background-position: -160px -800px;
+}
+.emoji-triangular_ruler {
+ background-position: -180px -800px;
+}
+.emoji-trident {
+ background-position: -200px -800px;
+}
+.emoji-triumph {
+ background-position: -220px -800px;
+}
+.emoji-trolleybus {
+ background-position: -240px -800px;
+}
+.emoji-trophy {
+ background-position: -260px -800px;
+}
+.emoji-tropical_drink {
+ background-position: -280px -800px;
+}
+.emoji-tropical_fish {
+ background-position: -300px -800px;
+}
+.emoji-truck {
+ background-position: -320px -800px;
+}
+.emoji-trumpet {
+ background-position: -340px -800px;
+}
+.emoji-tulip {
+ background-position: -360px -800px;
+}
+.emoji-tumbler_glass {
+ background-position: -380px -800px;
+}
+.emoji-turkey {
+ background-position: -400px -800px;
+}
+.emoji-turtle {
+ background-position: -420px -800px;
+}
+.emoji-tv {
+ background-position: -440px -800px;
+}
+.emoji-twisted_rightwards_arrows {
+ background-position: -460px -800px;
+}
+.emoji-two {
+ background-position: -480px -800px;
+}
+.emoji-two_hearts {
+ background-position: -500px -800px;
+}
+.emoji-two_men_holding_hands {
+ background-position: -520px -800px;
+}
+.emoji-two_women_holding_hands {
+ background-position: -540px -800px;
+}
+.emoji-u5272 {
+ background-position: -560px -800px;
+}
+.emoji-u5408 {
+ background-position: -580px -800px;
+}
+.emoji-u55b6 {
+ background-position: -600px -800px;
+}
+.emoji-u6307 {
+ background-position: -620px -800px;
+}
+.emoji-u6708 {
+ background-position: -640px -800px;
+}
+.emoji-u6709 {
+ background-position: -660px -800px;
+}
+.emoji-u6e80 {
+ background-position: -680px -800px;
+}
+.emoji-u7121 {
+ background-position: -700px -800px;
+}
+.emoji-u7533 {
+ background-position: -720px -800px;
+}
+.emoji-u7981 {
+ background-position: -740px -800px;
+}
+.emoji-u7a7a {
+ background-position: -760px -800px;
+}
+.emoji-umbrella {
+ background-position: -780px -800px;
+}
+.emoji-umbrella2 {
+ background-position: -800px -800px;
+}
+.emoji-unamused {
+ background-position: -820px 0;
+}
+.emoji-underage {
+ background-position: -820px -20px;
+}
+.emoji-unicorn {
+ background-position: -820px -40px;
+}
+.emoji-unlock {
+ background-position: -820px -60px;
+}
+.emoji-up {
+ background-position: -820px -80px;
+}
+.emoji-upside_down {
+ background-position: -820px -100px;
+}
+.emoji-urn {
+ background-position: -820px -120px;
+}
+.emoji-v {
+ background-position: -820px -140px;
+}
+.emoji-v_tone1 {
+ background-position: -820px -160px;
+}
+.emoji-v_tone2 {
+ background-position: -820px -180px;
+}
+.emoji-v_tone3 {
+ background-position: -820px -200px;
+}
+.emoji-v_tone4 {
+ background-position: -820px -220px;
+}
+.emoji-v_tone5 {
+ background-position: -820px -240px;
+}
+.emoji-vertical_traffic_light {
+ background-position: -820px -260px;
+}
+.emoji-vhs {
+ background-position: -820px -280px;
+}
+.emoji-vibration_mode {
+ background-position: -820px -300px;
+}
+.emoji-video_camera {
+ background-position: -820px -320px;
+}
+.emoji-video_game {
+ background-position: -820px -340px;
+}
+.emoji-violin {
+ background-position: -820px -360px;
+}
+.emoji-virgo {
+ background-position: -820px -380px;
+}
+.emoji-volcano {
+ background-position: -820px -400px;
+}
+.emoji-volleyball {
+ background-position: -820px -420px;
+}
+.emoji-vs {
+ background-position: -820px -440px;
+}
+.emoji-vulcan {
+ background-position: -820px -460px;
+}
+.emoji-vulcan_tone1 {
+ background-position: -820px -480px;
+}
+.emoji-vulcan_tone2 {
+ background-position: -820px -500px;
+}
+.emoji-vulcan_tone3 {
+ background-position: -820px -520px;
+}
+.emoji-vulcan_tone4 {
+ background-position: -820px -540px;
+}
+.emoji-vulcan_tone5 {
+ background-position: -820px -560px;
+}
+.emoji-walking {
+ background-position: -820px -580px;
+}
+.emoji-walking_tone1 {
+ background-position: -820px -600px;
+}
+.emoji-walking_tone2 {
+ background-position: -820px -620px;
+}
+.emoji-walking_tone3 {
+ background-position: -820px -640px;
+}
+.emoji-walking_tone4 {
+ background-position: -820px -660px;
+}
+.emoji-walking_tone5 {
+ background-position: -820px -680px;
+}
+.emoji-waning_crescent_moon {
+ background-position: -820px -700px;
+}
+.emoji-waning_gibbous_moon {
+ background-position: -820px -720px;
+}
+.emoji-warning {
+ background-position: -820px -740px;
+}
+.emoji-wastebasket {
+ background-position: -820px -760px;
+}
+.emoji-watch {
+ background-position: -820px -780px;
+}
+.emoji-water_buffalo {
+ background-position: -820px -800px;
+}
+.emoji-water_polo {
+ background-position: 0 -820px;
+}
+.emoji-water_polo_tone1 {
+ background-position: -20px -820px;
+}
+.emoji-water_polo_tone2 {
+ background-position: -40px -820px;
+}
+.emoji-water_polo_tone3 {
+ background-position: -60px -820px;
+}
+.emoji-water_polo_tone4 {
+ background-position: -80px -820px;
+}
+.emoji-water_polo_tone5 {
+ background-position: -100px -820px;
+}
+.emoji-watermelon {
+ background-position: -120px -820px;
+}
+.emoji-wave {
+ background-position: -140px -820px;
+}
+.emoji-wave_tone1 {
+ background-position: -160px -820px;
+}
+.emoji-wave_tone2 {
+ background-position: -180px -820px;
+}
+.emoji-wave_tone3 {
+ background-position: -200px -820px;
+}
+.emoji-wave_tone4 {
+ background-position: -220px -820px;
+}
+.emoji-wave_tone5 {
+ background-position: -240px -820px;
+}
+.emoji-wavy_dash {
+ background-position: -260px -820px;
+}
+.emoji-waxing_crescent_moon {
+ background-position: -280px -820px;
+}
+.emoji-waxing_gibbous_moon {
+ background-position: -300px -820px;
+}
+.emoji-wc {
+ background-position: -320px -820px;
+}
+.emoji-weary {
+ background-position: -340px -820px;
+}
+.emoji-wedding {
+ background-position: -360px -820px;
+}
+.emoji-whale {
+ background-position: -380px -820px;
+}
+.emoji-whale2 {
+ background-position: -400px -820px;
+}
+.emoji-wheel_of_dharma {
+ background-position: -420px -820px;
+}
+.emoji-wheelchair {
+ background-position: -440px -820px;
+}
+.emoji-white_check_mark {
+ background-position: -460px -820px;
+}
+.emoji-white_circle {
+ background-position: -480px -820px;
+}
+.emoji-white_flower {
+ background-position: -500px -820px;
+}
+.emoji-white_large_square {
+ background-position: -520px -820px;
+}
+.emoji-white_medium_small_square {
+ background-position: -540px -820px;
+}
+.emoji-white_medium_square {
+ background-position: -560px -820px;
+}
+.emoji-white_small_square {
+ background-position: -580px -820px;
+}
+.emoji-white_square_button {
+ background-position: -600px -820px;
+}
+.emoji-white_sun_cloud {
+ background-position: -620px -820px;
+}
+.emoji-white_sun_rain_cloud {
+ background-position: -640px -820px;
+}
+.emoji-white_sun_small_cloud {
+ background-position: -660px -820px;
+}
+.emoji-wilted_rose {
+ background-position: -680px -820px;
+}
+.emoji-wind_blowing_face {
+ background-position: -700px -820px;
+}
+.emoji-wind_chime {
+ background-position: -720px -820px;
+}
+.emoji-wine_glass {
+ background-position: -740px -820px;
+}
+.emoji-wink {
+ background-position: -760px -820px;
+}
+.emoji-wolf {
+ background-position: -780px -820px;
+}
+.emoji-woman {
+ background-position: -800px -820px;
+}
+.emoji-woman_tone1 {
+ background-position: -820px -820px;
+}
+.emoji-woman_tone2 {
+ background-position: -840px 0;
+}
+.emoji-woman_tone3 {
+ background-position: -840px -20px;
+}
+.emoji-woman_tone4 {
+ background-position: -840px -40px;
+}
+.emoji-woman_tone5 {
+ background-position: -840px -60px;
+}
+.emoji-womans_clothes {
+ background-position: -840px -80px;
+}
+.emoji-womans_hat {
+ background-position: -840px -100px;
+}
+.emoji-womens {
+ background-position: -840px -120px;
+}
+.emoji-worried {
+ background-position: -840px -140px;
+}
+.emoji-wrench {
+ background-position: -840px -160px;
+}
+.emoji-wrestlers {
+ background-position: -840px -180px;
+}
+.emoji-wrestlers_tone1 {
+ background-position: -840px -200px;
+}
+.emoji-wrestlers_tone2 {
+ background-position: -840px -220px;
+}
+.emoji-wrestlers_tone3 {
+ background-position: -840px -240px;
+}
+.emoji-wrestlers_tone4 {
+ background-position: -840px -260px;
+}
+.emoji-wrestlers_tone5 {
+ background-position: -840px -280px;
+}
+.emoji-writing_hand {
+ background-position: -840px -300px;
+}
+.emoji-writing_hand_tone1 {
+ background-position: -840px -320px;
+}
+.emoji-writing_hand_tone2 {
+ background-position: -840px -340px;
+}
+.emoji-writing_hand_tone3 {
+ background-position: -840px -360px;
+}
+.emoji-writing_hand_tone4 {
+ background-position: -840px -380px;
+}
+.emoji-writing_hand_tone5 {
+ background-position: -840px -400px;
+}
+.emoji-x {
+ background-position: -840px -420px;
+}
+.emoji-yellow_heart {
+ background-position: -840px -440px;
+}
+.emoji-yen {
+ background-position: -840px -460px;
+}
+.emoji-yin_yang {
+ background-position: -840px -480px;
+}
+.emoji-yum {
+ background-position: -840px -500px;
+}
+.emoji-zap {
+ background-position: -840px -520px;
+}
+.emoji-zero {
+ background-position: -840px -540px;
+}
+.emoji-zipper_mouth {
+ background-position: -840px -560px;
+}
+.emoji-100 {
+ background-position: -840px -580px;
+}
+
+.emoji-icon {
+ background-image: image-url('emoji.png');
+ background-repeat: no-repeat;
+ color: transparent;
+ text-indent: -99em;
+ height: 20px;
+ width: 20px;
+
+ @media only screen and (-webkit-min-device-pixel-ratio: 2),
+ only screen and (min--moz-device-pixel-ratio: 2),
+ only screen and (-o-min-device-pixel-ratio: 2/1),
+ only screen and (min-device-pixel-ratio: 2),
+ only screen and (min-resolution: 192dpi),
+ only screen and (min-resolution: 2dppx) {
+ background-image: image-url('emoji@2x.png');
+ background-size: 860px 840px;
+ }
+}
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 2fccfa4011c..360dcb6afef 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -1,64 +1,63 @@
-@import "framework/variables";
-@import "framework/mixins";
+@import 'framework/variables';
+@import 'framework/mixins';
@import 'framework/tw_bootstrap_variables';
@import 'framework/tw_bootstrap';
-@import "framework/layout";
+@import 'framework/layout';
-@import "framework/animations";
-@import "framework/vue_transitions";
-@import "framework/avatar";
-@import "framework/asciidoctor";
-@import "framework/banner";
-@import "framework/blocks";
-@import "framework/buttons";
-@import "framework/badges";
-@import "framework/calendar";
-@import "framework/callout";
-@import "framework/common";
-@import "framework/dropdowns";
-@import "framework/files";
-@import "framework/filters";
-@import "framework/flash";
-@import "framework/forms";
-@import "framework/gfm";
-@import "framework/gitlab_theme";
-@import "framework/header";
-@import "framework/highlight";
-@import "framework/issue_box";
-@import "framework/jquery";
-@import "framework/lists";
-@import "framework/logo";
-@import "framework/markdown_area";
-@import "framework/media_object";
-@import "framework/mobile";
-@import "framework/modal";
-@import "framework/pagination";
-@import "framework/panels";
-@import "framework/popup";
-@import "framework/secondary_navigation_elements";
-@import "framework/selects";
-@import "framework/sidebar";
-@import "framework/contextual_sidebar";
-@import "framework/tables";
-@import "framework/notes";
-@import "framework/tabs";
-@import "framework/timeline";
-@import "framework/tooltips";
-@import "framework/toggle";
-@import "framework/typography";
-@import "framework/zen";
-@import "framework/blank";
-@import "framework/wells";
-@import "framework/page_header";
-@import "framework/awards";
-@import "framework/images";
-@import "framework/broadcast_messages";
-@import "framework/emojis";
-@import "framework/emoji_sprites";
-@import "framework/icons";
-@import "framework/snippets";
-@import "framework/memory_graph";
-@import "framework/responsive_tables";
-@import "framework/stacked_progress_bar";
-@import "framework/ci_variable_list";
-@import "framework/feature_highlight";
+@import 'framework/animations';
+@import 'framework/vue_transitions';
+@import 'framework/avatar';
+@import 'framework/asciidoctor';
+@import 'framework/banner';
+@import 'framework/blocks';
+@import 'framework/buttons';
+@import 'framework/badges';
+@import 'framework/calendar';
+@import 'framework/callout';
+@import 'framework/common';
+@import 'framework/dropdowns';
+@import 'framework/files';
+@import 'framework/filters';
+@import 'framework/flash';
+@import 'framework/forms';
+@import 'framework/gfm';
+@import 'framework/gitlab_theme';
+@import 'framework/header';
+@import 'framework/highlight';
+@import 'framework/issue_box';
+@import 'framework/jquery';
+@import 'framework/lists';
+@import 'framework/logo';
+@import 'framework/markdown_area';
+@import 'framework/media_object';
+@import 'framework/mobile';
+@import 'framework/modal';
+@import 'framework/pagination';
+@import 'framework/panels';
+@import 'framework/popup';
+@import 'framework/secondary_navigation_elements';
+@import 'framework/selects';
+@import 'framework/sidebar';
+@import 'framework/contextual_sidebar';
+@import 'framework/tables';
+@import 'framework/notes';
+@import 'framework/tabs';
+@import 'framework/timeline';
+@import 'framework/tooltips';
+@import 'framework/toggle';
+@import 'framework/typography';
+@import 'framework/zen';
+@import 'framework/blank';
+@import 'framework/wells';
+@import 'framework/page_header';
+@import 'framework/awards';
+@import 'framework/images';
+@import 'framework/broadcast_messages';
+@import 'framework/emojis';
+@import 'framework/icons';
+@import 'framework/snippets';
+@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/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index c5c7afe25be..c60f65e7a85 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -46,7 +46,7 @@
}
&.middle-block {
- margin-top: 0;
+ margin-top: $gl-padding-24;
margin-bottom: 0;
}
@@ -61,7 +61,7 @@
}
&.footer-block {
- margin-top: 0;
+ margin-top: $gl-padding-24;
border-bottom: 0;
margin-bottom: -$gl-padding;
}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index e058a0b35b7..2faea55a5f5 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -452,6 +452,7 @@ img.emoji {
/** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; }
+.prepend-top-2 { margin-top: 2px; }
.prepend-top-5 { margin-top: 5px; }
.prepend-top-8 { margin-top: $grid-size; }
.prepend-top-10 { margin-top: 10px; }
diff --git a/app/assets/stylesheets/framework/emoji_sprites.scss b/app/assets/stylesheets/framework/emoji_sprites.scss
deleted file mode 100644
index 0174e17b660..00000000000
--- a/app/assets/stylesheets/framework/emoji_sprites.scss
+++ /dev/null
@@ -1,1813 +0,0 @@
-.emoji-zzz { background-position: 0 0; }
-.emoji-1234 { background-position: -20px 0; }
-.emoji-1F627 { background-position: 0 -20px; }
-.emoji-8ball { background-position: -20px -20px; }
-.emoji-a { background-position: -40px 0; }
-.emoji-ab { background-position: -40px -20px; }
-.emoji-abc { background-position: 0 -40px; }
-.emoji-abcd { background-position: -20px -40px; }
-.emoji-accept { background-position: -40px -40px; }
-.emoji-aerial_tramway { background-position: -60px 0; }
-.emoji-airplane { background-position: -60px -20px; }
-.emoji-airplane_arriving { background-position: -60px -40px; }
-.emoji-airplane_departure { background-position: 0 -60px; }
-.emoji-airplane_small { background-position: -20px -60px; }
-.emoji-alarm_clock { background-position: -40px -60px; }
-.emoji-alembic { background-position: -60px -60px; }
-.emoji-alien { background-position: -80px 0; }
-.emoji-ambulance { background-position: -80px -20px; }
-.emoji-amphora { background-position: -80px -40px; }
-.emoji-anchor { background-position: -80px -60px; }
-.emoji-angel { background-position: 0 -80px; }
-.emoji-angel_tone1 { background-position: -20px -80px; }
-.emoji-angel_tone2 { background-position: -40px -80px; }
-.emoji-angel_tone3 { background-position: -60px -80px; }
-.emoji-angel_tone4 { background-position: -80px -80px; }
-.emoji-angel_tone5 { background-position: -100px 0; }
-.emoji-anger { background-position: -100px -20px; }
-.emoji-anger_right { background-position: -100px -40px; }
-.emoji-angry { background-position: -100px -60px; }
-.emoji-ant { background-position: -100px -80px; }
-.emoji-apple { background-position: 0 -100px; }
-.emoji-aquarius { background-position: -20px -100px; }
-.emoji-aries { background-position: -40px -100px; }
-.emoji-arrow_backward { background-position: -60px -100px; }
-.emoji-arrow_double_down { background-position: -80px -100px; }
-.emoji-arrow_double_up { background-position: -100px -100px; }
-.emoji-arrow_down { background-position: -120px 0; }
-.emoji-arrow_down_small { background-position: -120px -20px; }
-.emoji-arrow_forward { background-position: -120px -40px; }
-.emoji-arrow_heading_down { background-position: -120px -60px; }
-.emoji-arrow_heading_up { background-position: -120px -80px; }
-.emoji-arrow_left { background-position: -120px -100px; }
-.emoji-arrow_lower_left { background-position: 0 -120px; }
-.emoji-arrow_lower_right { background-position: -20px -120px; }
-.emoji-arrow_right { background-position: -40px -120px; }
-.emoji-arrow_right_hook { background-position: -60px -120px; }
-.emoji-arrow_up { background-position: -80px -120px; }
-.emoji-arrow_up_down { background-position: -100px -120px; }
-.emoji-arrow_up_small { background-position: -120px -120px; }
-.emoji-arrow_upper_left { background-position: -140px 0; }
-.emoji-arrow_upper_right { background-position: -140px -20px; }
-.emoji-arrows_clockwise { background-position: -140px -40px; }
-.emoji-arrows_counterclockwise { background-position: -140px -60px; }
-.emoji-art { background-position: -140px -80px; }
-.emoji-articulated_lorry { background-position: -140px -100px; }
-.emoji-asterisk { background-position: -140px -120px; }
-.emoji-astonished { background-position: 0 -140px; }
-.emoji-athletic_shoe { background-position: -20px -140px; }
-.emoji-atm { background-position: -40px -140px; }
-.emoji-atom { background-position: -60px -140px; }
-.emoji-avocado { background-position: -80px -140px; }
-.emoji-b { background-position: -100px -140px; }
-.emoji-baby { background-position: -120px -140px; }
-.emoji-baby_bottle { background-position: -140px -140px; }
-.emoji-baby_chick { background-position: -160px 0; }
-.emoji-baby_symbol { background-position: -160px -20px; }
-.emoji-baby_tone1 { background-position: -160px -40px; }
-.emoji-baby_tone2 { background-position: -160px -60px; }
-.emoji-baby_tone3 { background-position: -160px -80px; }
-.emoji-baby_tone4 { background-position: -160px -100px; }
-.emoji-baby_tone5 { background-position: -160px -120px; }
-.emoji-back { background-position: -160px -140px; }
-.emoji-bacon { background-position: 0 -160px; }
-.emoji-badminton { background-position: -20px -160px; }
-.emoji-baggage_claim { background-position: -40px -160px; }
-.emoji-balloon { background-position: -60px -160px; }
-.emoji-ballot_box { background-position: -80px -160px; }
-.emoji-ballot_box_with_check { background-position: -100px -160px; }
-.emoji-bamboo { background-position: -120px -160px; }
-.emoji-banana { background-position: -140px -160px; }
-.emoji-bangbang { background-position: -160px -160px; }
-.emoji-bank { background-position: -180px 0; }
-.emoji-bar_chart { background-position: -180px -20px; }
-.emoji-barber { background-position: -180px -40px; }
-.emoji-baseball { background-position: -180px -60px; }
-.emoji-basketball { background-position: -180px -80px; }
-.emoji-basketball_player { background-position: -180px -100px; }
-.emoji-basketball_player_tone1 { background-position: -180px -120px; }
-.emoji-basketball_player_tone2 { background-position: -180px -140px; }
-.emoji-basketball_player_tone3 { background-position: -180px -160px; }
-.emoji-basketball_player_tone4 { background-position: 0 -180px; }
-.emoji-basketball_player_tone5 { background-position: -20px -180px; }
-.emoji-bat { background-position: -40px -180px; }
-.emoji-bath { background-position: -60px -180px; }
-.emoji-bath_tone1 { background-position: -80px -180px; }
-.emoji-bath_tone2 { background-position: -100px -180px; }
-.emoji-bath_tone3 { background-position: -120px -180px; }
-.emoji-bath_tone4 { background-position: -140px -180px; }
-.emoji-bath_tone5 { background-position: -160px -180px; }
-.emoji-bathtub { background-position: -180px -180px; }
-.emoji-battery { background-position: -200px 0; }
-.emoji-beach { background-position: -200px -20px; }
-.emoji-beach_umbrella { background-position: -200px -40px; }
-.emoji-bear { background-position: -200px -60px; }
-.emoji-bed { background-position: -200px -80px; }
-.emoji-bee { background-position: -200px -100px; }
-.emoji-beer { background-position: -200px -120px; }
-.emoji-beers { background-position: -200px -140px; }
-.emoji-beetle { background-position: -200px -160px; }
-.emoji-beginner { background-position: -200px -180px; }
-.emoji-bell { background-position: 0 -200px; }
-.emoji-bellhop { background-position: -20px -200px; }
-.emoji-bento { background-position: -40px -200px; }
-.emoji-bicyclist { background-position: -60px -200px; }
-.emoji-bicyclist_tone1 { background-position: -80px -200px; }
-.emoji-bicyclist_tone2 { background-position: -100px -200px; }
-.emoji-bicyclist_tone3 { background-position: -120px -200px; }
-.emoji-bicyclist_tone4 { background-position: -140px -200px; }
-.emoji-bicyclist_tone5 { background-position: -160px -200px; }
-.emoji-bike { background-position: -180px -200px; }
-.emoji-bikini { background-position: -200px -200px; }
-.emoji-biohazard { background-position: -220px 0; }
-.emoji-bird { background-position: -220px -20px; }
-.emoji-birthday { background-position: -220px -40px; }
-.emoji-black_circle { background-position: -220px -60px; }
-.emoji-black_heart { background-position: -220px -80px; }
-.emoji-black_joker { background-position: -220px -100px; }
-.emoji-black_large_square { background-position: -220px -120px; }
-.emoji-black_medium_small_square { background-position: -220px -140px; }
-.emoji-black_medium_square { background-position: -220px -160px; }
-.emoji-black_nib { background-position: -220px -180px; }
-.emoji-black_small_square { background-position: -220px -200px; }
-.emoji-black_square_button { background-position: 0 -220px; }
-.emoji-blossom { background-position: -20px -220px; }
-.emoji-blowfish { background-position: -40px -220px; }
-.emoji-blue_book { background-position: -60px -220px; }
-.emoji-blue_car { background-position: -80px -220px; }
-.emoji-blue_heart { background-position: -100px -220px; }
-.emoji-blush { background-position: -120px -220px; }
-.emoji-boar { background-position: -140px -220px; }
-.emoji-bomb { background-position: -160px -220px; }
-.emoji-book { background-position: -180px -220px; }
-.emoji-bookmark { background-position: -200px -220px; }
-.emoji-bookmark_tabs { background-position: -220px -220px; }
-.emoji-books { background-position: -240px 0; }
-.emoji-boom { background-position: -240px -20px; }
-.emoji-boot { background-position: -240px -40px; }
-.emoji-bouquet { background-position: -240px -60px; }
-.emoji-bow { background-position: -240px -80px; }
-.emoji-bow_and_arrow { background-position: -240px -100px; }
-.emoji-bow_tone1 { background-position: -240px -120px; }
-.emoji-bow_tone2 { background-position: -240px -140px; }
-.emoji-bow_tone3 { background-position: -240px -160px; }
-.emoji-bow_tone4 { background-position: -240px -180px; }
-.emoji-bow_tone5 { background-position: -240px -200px; }
-.emoji-bowling { background-position: -240px -220px; }
-.emoji-boxing_glove { background-position: 0 -240px; }
-.emoji-boy { background-position: -20px -240px; }
-.emoji-boy_tone1 { background-position: -40px -240px; }
-.emoji-boy_tone2 { background-position: -60px -240px; }
-.emoji-boy_tone3 { background-position: -80px -240px; }
-.emoji-boy_tone4 { background-position: -100px -240px; }
-.emoji-boy_tone5 { background-position: -120px -240px; }
-.emoji-bread { background-position: -140px -240px; }
-.emoji-bride_with_veil { background-position: -160px -240px; }
-.emoji-bride_with_veil_tone1 { background-position: -180px -240px; }
-.emoji-bride_with_veil_tone2 { background-position: -200px -240px; }
-.emoji-bride_with_veil_tone3 { background-position: -220px -240px; }
-.emoji-bride_with_veil_tone4 { background-position: -240px -240px; }
-.emoji-bride_with_veil_tone5 { background-position: -260px 0; }
-.emoji-bridge_at_night { background-position: -260px -20px; }
-.emoji-briefcase { background-position: -260px -40px; }
-.emoji-broken_heart { background-position: -260px -60px; }
-.emoji-bug { background-position: -260px -80px; }
-.emoji-bulb { background-position: -260px -100px; }
-.emoji-bullettrain_front { background-position: -260px -120px; }
-.emoji-bullettrain_side { background-position: -260px -140px; }
-.emoji-burrito { background-position: -260px -160px; }
-.emoji-bus { background-position: -260px -180px; }
-.emoji-busstop { background-position: -260px -200px; }
-.emoji-bust_in_silhouette { background-position: -260px -220px; }
-.emoji-busts_in_silhouette { background-position: -260px -240px; }
-.emoji-butterfly { background-position: 0 -260px; }
-.emoji-cactus { background-position: -20px -260px; }
-.emoji-cake { background-position: -40px -260px; }
-.emoji-calendar { background-position: -60px -260px; }
-.emoji-calendar_spiral { background-position: -80px -260px; }
-.emoji-call_me { background-position: -100px -260px; }
-.emoji-call_me_tone1 { background-position: -120px -260px; }
-.emoji-call_me_tone2 { background-position: -140px -260px; }
-.emoji-call_me_tone3 { background-position: -160px -260px; }
-.emoji-call_me_tone4 { background-position: -180px -260px; }
-.emoji-call_me_tone5 { background-position: -200px -260px; }
-.emoji-calling { background-position: -220px -260px; }
-.emoji-camel { background-position: -240px -260px; }
-.emoji-camera { background-position: -260px -260px; }
-.emoji-camera_with_flash { background-position: -280px 0; }
-.emoji-camping { background-position: -280px -20px; }
-.emoji-cancer { background-position: -280px -40px; }
-.emoji-candle { background-position: -280px -60px; }
-.emoji-candy { background-position: -280px -80px; }
-.emoji-canoe { background-position: -280px -100px; }
-.emoji-capital_abcd { background-position: -280px -120px; }
-.emoji-capricorn { background-position: -280px -140px; }
-.emoji-card_box { background-position: -280px -160px; }
-.emoji-card_index { background-position: -280px -180px; }
-.emoji-carousel_horse { background-position: -280px -200px; }
-.emoji-carrot { background-position: -280px -220px; }
-.emoji-cartwheel { background-position: -280px -240px; }
-.emoji-cartwheel_tone1 { background-position: -280px -260px; }
-.emoji-cartwheel_tone2 { background-position: 0 -280px; }
-.emoji-cartwheel_tone3 { background-position: -20px -280px; }
-.emoji-cartwheel_tone4 { background-position: -40px -280px; }
-.emoji-cartwheel_tone5 { background-position: -60px -280px; }
-.emoji-cat { background-position: -80px -280px; }
-.emoji-cat2 { background-position: -100px -280px; }
-.emoji-cd { background-position: -120px -280px; }
-.emoji-chains { background-position: -140px -280px; }
-.emoji-champagne { background-position: -160px -280px; }
-.emoji-champagne_glass { background-position: -180px -280px; }
-.emoji-chart { background-position: -200px -280px; }
-.emoji-chart_with_downwards_trend { background-position: -220px -280px; }
-.emoji-chart_with_upwards_trend { background-position: -240px -280px; }
-.emoji-checkered_flag { background-position: -260px -280px; }
-.emoji-cheese { background-position: -280px -280px; }
-.emoji-cherries { background-position: -300px 0; }
-.emoji-cherry_blossom { background-position: -300px -20px; }
-.emoji-chestnut { background-position: -300px -40px; }
-.emoji-chicken { background-position: -300px -60px; }
-.emoji-children_crossing { background-position: -300px -80px; }
-.emoji-chipmunk { background-position: -300px -100px; }
-.emoji-chocolate_bar { background-position: -300px -120px; }
-.emoji-christmas_tree { background-position: -300px -140px; }
-.emoji-church { background-position: -300px -160px; }
-.emoji-cinema { background-position: -300px -180px; }
-.emoji-circus_tent { background-position: -300px -200px; }
-.emoji-city_dusk { background-position: -300px -220px; }
-.emoji-city_sunset { background-position: -300px -240px; }
-.emoji-cityscape { background-position: -300px -260px; }
-.emoji-cl { background-position: -300px -280px; }
-.emoji-clap { background-position: 0 -300px; }
-.emoji-clap_tone1 { background-position: -20px -300px; }
-.emoji-clap_tone2 { background-position: -40px -300px; }
-.emoji-clap_tone3 { background-position: -60px -300px; }
-.emoji-clap_tone4 { background-position: -80px -300px; }
-.emoji-clap_tone5 { background-position: -100px -300px; }
-.emoji-clapper { background-position: -120px -300px; }
-.emoji-classical_building { background-position: -140px -300px; }
-.emoji-clipboard { background-position: -160px -300px; }
-.emoji-clock { background-position: -180px -300px; }
-.emoji-clock1 { background-position: -200px -300px; }
-.emoji-clock10 { background-position: -220px -300px; }
-.emoji-clock1030 { background-position: -240px -300px; }
-.emoji-clock11 { background-position: -260px -300px; }
-.emoji-clock1130 { background-position: -280px -300px; }
-.emoji-clock12 { background-position: -300px -300px; }
-.emoji-clock1230 { background-position: -320px 0; }
-.emoji-clock130 { background-position: -320px -20px; }
-.emoji-clock2 { background-position: -320px -40px; }
-.emoji-clock230 { background-position: -320px -60px; }
-.emoji-clock3 { background-position: -320px -80px; }
-.emoji-clock330 { background-position: -320px -100px; }
-.emoji-clock4 { background-position: -320px -120px; }
-.emoji-clock430 { background-position: -320px -140px; }
-.emoji-clock5 { background-position: -320px -160px; }
-.emoji-clock530 { background-position: -320px -180px; }
-.emoji-clock6 { background-position: -320px -200px; }
-.emoji-clock630 { background-position: -320px -220px; }
-.emoji-clock7 { background-position: -320px -240px; }
-.emoji-clock730 { background-position: -320px -260px; }
-.emoji-clock8 { background-position: -320px -280px; }
-.emoji-clock830 { background-position: -320px -300px; }
-.emoji-clock9 { background-position: 0 -320px; }
-.emoji-clock930 { background-position: -20px -320px; }
-.emoji-closed_book { background-position: -40px -320px; }
-.emoji-closed_lock_with_key { background-position: -60px -320px; }
-.emoji-closed_umbrella { background-position: -80px -320px; }
-.emoji-cloud { background-position: -100px -320px; }
-.emoji-cloud_lightning { background-position: -120px -320px; }
-.emoji-cloud_rain { background-position: -140px -320px; }
-.emoji-cloud_snow { background-position: -160px -320px; }
-.emoji-cloud_tornado { background-position: -180px -320px; }
-.emoji-clown { background-position: -200px -320px; }
-.emoji-clubs { background-position: -220px -320px; }
-.emoji-cocktail { background-position: -240px -320px; }
-.emoji-coffee { background-position: -260px -320px; }
-.emoji-coffin { background-position: -280px -320px; }
-.emoji-cold_sweat { background-position: -300px -320px; }
-.emoji-comet { background-position: -320px -320px; }
-.emoji-compression { background-position: -340px 0; }
-.emoji-computer { background-position: -340px -20px; }
-.emoji-confetti_ball { background-position: -340px -40px; }
-.emoji-confounded { background-position: -340px -60px; }
-.emoji-confused { background-position: -340px -80px; }
-.emoji-congratulations { background-position: -340px -100px; }
-.emoji-construction { background-position: -340px -120px; }
-.emoji-construction_site { background-position: -340px -140px; }
-.emoji-construction_worker { background-position: -340px -160px; }
-.emoji-construction_worker_tone1 { background-position: -340px -180px; }
-.emoji-construction_worker_tone2 { background-position: -340px -200px; }
-.emoji-construction_worker_tone3 { background-position: -340px -220px; }
-.emoji-construction_worker_tone4 { background-position: -340px -240px; }
-.emoji-construction_worker_tone5 { background-position: -340px -260px; }
-.emoji-control_knobs { background-position: -340px -280px; }
-.emoji-convenience_store { background-position: -340px -300px; }
-.emoji-cookie { background-position: -340px -320px; }
-.emoji-cooking { background-position: 0 -340px; }
-.emoji-cool { background-position: -20px -340px; }
-.emoji-cop { background-position: -40px -340px; }
-.emoji-cop_tone1 { background-position: -60px -340px; }
-.emoji-cop_tone2 { background-position: -80px -340px; }
-.emoji-cop_tone3 { background-position: -100px -340px; }
-.emoji-cop_tone4 { background-position: -120px -340px; }
-.emoji-cop_tone5 { background-position: -140px -340px; }
-.emoji-copyright { background-position: -160px -340px; }
-.emoji-corn { background-position: -180px -340px; }
-.emoji-couch { background-position: -200px -340px; }
-.emoji-couple { background-position: -220px -340px; }
-.emoji-couple_mm { background-position: -240px -340px; }
-.emoji-couple_with_heart { background-position: -260px -340px; }
-.emoji-couple_ww { background-position: -280px -340px; }
-.emoji-couplekiss { background-position: -300px -340px; }
-.emoji-cow { background-position: -320px -340px; }
-.emoji-cow2 { background-position: -340px -340px; }
-.emoji-cowboy { background-position: -360px 0; }
-.emoji-crab { background-position: -360px -20px; }
-.emoji-crayon { background-position: -360px -40px; }
-.emoji-credit_card { background-position: -360px -60px; }
-.emoji-crescent_moon { background-position: -360px -80px; }
-.emoji-cricket { background-position: -360px -100px; }
-.emoji-crocodile { background-position: -360px -120px; }
-.emoji-croissant { background-position: -360px -140px; }
-.emoji-cross { background-position: -360px -160px; }
-.emoji-crossed_flags { background-position: -360px -180px; }
-.emoji-crossed_swords { background-position: -360px -200px; }
-.emoji-crown { background-position: -360px -220px; }
-.emoji-cruise_ship { background-position: -360px -240px; }
-.emoji-cry { background-position: -360px -260px; }
-.emoji-crying_cat_face { background-position: -360px -280px; }
-.emoji-crystal_ball { background-position: -360px -300px; }
-.emoji-cucumber { background-position: -360px -320px; }
-.emoji-cupid { background-position: -360px -340px; }
-.emoji-curly_loop { background-position: 0 -360px; }
-.emoji-currency_exchange { background-position: -20px -360px; }
-.emoji-curry { background-position: -40px -360px; }
-.emoji-custard { background-position: -60px -360px; }
-.emoji-customs { background-position: -80px -360px; }
-.emoji-cyclone { background-position: -100px -360px; }
-.emoji-dagger { background-position: -120px -360px; }
-.emoji-dancer { background-position: -140px -360px; }
-.emoji-dancer_tone1 { background-position: -160px -360px; }
-.emoji-dancer_tone2 { background-position: -180px -360px; }
-.emoji-dancer_tone3 { background-position: -200px -360px; }
-.emoji-dancer_tone4 { background-position: -220px -360px; }
-.emoji-dancer_tone5 { background-position: -240px -360px; }
-.emoji-dancers { background-position: -260px -360px; }
-.emoji-dango { background-position: -280px -360px; }
-.emoji-dark_sunglasses { background-position: -300px -360px; }
-.emoji-dart { background-position: -320px -360px; }
-.emoji-dash { background-position: -340px -360px; }
-.emoji-date { background-position: -360px -360px; }
-.emoji-deciduous_tree { background-position: -380px 0; }
-.emoji-deer { background-position: -380px -20px; }
-.emoji-department_store { background-position: -380px -40px; }
-.emoji-desert { background-position: -380px -60px; }
-.emoji-desktop { background-position: -380px -80px; }
-.emoji-diamond_shape_with_a_dot_inside { background-position: -380px -100px; }
-.emoji-diamonds { background-position: -380px -120px; }
-.emoji-disappointed { background-position: -380px -140px; }
-.emoji-disappointed_relieved { background-position: -380px -160px; }
-.emoji-dividers { background-position: -380px -180px; }
-.emoji-dizzy { background-position: -380px -200px; }
-.emoji-dizzy_face { background-position: -380px -220px; }
-.emoji-do_not_litter { background-position: -380px -240px; }
-.emoji-dog { background-position: -380px -260px; }
-.emoji-dog2 { background-position: -380px -280px; }
-.emoji-dollar { background-position: -380px -300px; }
-.emoji-dolls { background-position: -380px -320px; }
-.emoji-dolphin { background-position: -380px -340px; }
-.emoji-door { background-position: -380px -360px; }
-.emoji-doughnut { background-position: 0 -380px; }
-.emoji-dove { background-position: -20px -380px; }
-.emoji-dragon { background-position: -40px -380px; }
-.emoji-dragon_face { background-position: -60px -380px; }
-.emoji-dress { background-position: -80px -380px; }
-.emoji-dromedary_camel { background-position: -100px -380px; }
-.emoji-drooling_face { background-position: -120px -380px; }
-.emoji-droplet { background-position: -140px -380px; }
-.emoji-drum { background-position: -160px -380px; }
-.emoji-duck { background-position: -180px -380px; }
-.emoji-dvd { background-position: -200px -380px; }
-.emoji-e-mail { background-position: -220px -380px; }
-.emoji-eagle { background-position: -240px -380px; }
-.emoji-ear { background-position: -260px -380px; }
-.emoji-ear_of_rice { background-position: -280px -380px; }
-.emoji-ear_tone1 { background-position: -300px -380px; }
-.emoji-ear_tone2 { background-position: -320px -380px; }
-.emoji-ear_tone3 { background-position: -340px -380px; }
-.emoji-ear_tone4 { background-position: -360px -380px; }
-.emoji-ear_tone5 { background-position: -380px -380px; }
-.emoji-earth_africa { background-position: -400px 0; }
-.emoji-earth_americas { background-position: -400px -20px; }
-.emoji-earth_asia { background-position: -400px -40px; }
-.emoji-egg { background-position: -400px -60px; }
-.emoji-eggplant { background-position: -400px -80px; }
-.emoji-eight { background-position: -400px -100px; }
-.emoji-eight_pointed_black_star { background-position: -400px -120px; }
-.emoji-eight_spoked_asterisk { background-position: -400px -140px; }
-.emoji-eject { background-position: -400px -160px; }
-.emoji-electric_plug { background-position: -400px -180px; }
-.emoji-elephant { background-position: -400px -200px; }
-.emoji-end { background-position: -400px -220px; }
-.emoji-envelope { background-position: -400px -240px; }
-.emoji-envelope_with_arrow { background-position: -400px -260px; }
-.emoji-euro { background-position: -400px -280px; }
-.emoji-european_castle { background-position: -400px -300px; }
-.emoji-european_post_office { background-position: -400px -320px; }
-.emoji-evergreen_tree { background-position: -400px -340px; }
-.emoji-exclamation { background-position: -400px -360px; }
-.emoji-expressionless { background-position: -400px -380px; }
-.emoji-eye { background-position: 0 -400px; }
-.emoji-eye_in_speech_bubble { background-position: -20px -400px; }
-.emoji-eyeglasses { background-position: -40px -400px; }
-.emoji-eyes { background-position: -60px -400px; }
-.emoji-face_palm { background-position: -80px -400px; }
-.emoji-face_palm_tone1 { background-position: -100px -400px; }
-.emoji-face_palm_tone2 { background-position: -120px -400px; }
-.emoji-face_palm_tone3 { background-position: -140px -400px; }
-.emoji-face_palm_tone4 { background-position: -160px -400px; }
-.emoji-face_palm_tone5 { background-position: -180px -400px; }
-.emoji-factory { background-position: -200px -400px; }
-.emoji-fallen_leaf { background-position: -220px -400px; }
-.emoji-family { background-position: -240px -400px; }
-.emoji-family_mmb { background-position: -260px -400px; }
-.emoji-family_mmbb { background-position: -280px -400px; }
-.emoji-family_mmg { background-position: -300px -400px; }
-.emoji-family_mmgb { background-position: -320px -400px; }
-.emoji-family_mmgg { background-position: -340px -400px; }
-.emoji-family_mwbb { background-position: -360px -400px; }
-.emoji-family_mwg { background-position: -380px -400px; }
-.emoji-family_mwgb { background-position: -400px -400px; }
-.emoji-family_mwgg { background-position: -420px 0; }
-.emoji-family_wwb { background-position: -420px -20px; }
-.emoji-family_wwbb { background-position: -420px -40px; }
-.emoji-family_wwg { background-position: -420px -60px; }
-.emoji-family_wwgb { background-position: -420px -80px; }
-.emoji-family_wwgg { background-position: -420px -100px; }
-.emoji-fast_forward { background-position: -420px -120px; }
-.emoji-fax { background-position: -420px -140px; }
-.emoji-fearful { background-position: -420px -160px; }
-.emoji-feet { background-position: -420px -180px; }
-.emoji-fencer { background-position: -420px -200px; }
-.emoji-ferris_wheel { background-position: -420px -220px; }
-.emoji-ferry { background-position: -420px -240px; }
-.emoji-field_hockey { background-position: -420px -260px; }
-.emoji-file_cabinet { background-position: -420px -280px; }
-.emoji-file_folder { background-position: -420px -300px; }
-.emoji-film_frames { background-position: -420px -320px; }
-.emoji-fingers_crossed { background-position: -420px -340px; }
-.emoji-fingers_crossed_tone1 { background-position: -420px -360px; }
-.emoji-fingers_crossed_tone2 { background-position: -420px -380px; }
-.emoji-fingers_crossed_tone3 { background-position: -420px -400px; }
-.emoji-fingers_crossed_tone4 { background-position: 0 -420px; }
-.emoji-fingers_crossed_tone5 { background-position: -20px -420px; }
-.emoji-fire { background-position: -40px -420px; }
-.emoji-fire_engine { background-position: -60px -420px; }
-.emoji-fireworks { background-position: -80px -420px; }
-.emoji-first_place { background-position: -100px -420px; }
-.emoji-first_quarter_moon { background-position: -120px -420px; }
-.emoji-first_quarter_moon_with_face { background-position: -140px -420px; }
-.emoji-fish { background-position: -160px -420px; }
-.emoji-fish_cake { background-position: -180px -420px; }
-.emoji-fishing_pole_and_fish { background-position: -200px -420px; }
-.emoji-fist { background-position: -220px -420px; }
-.emoji-fist_tone1 { background-position: -240px -420px; }
-.emoji-fist_tone2 { background-position: -260px -420px; }
-.emoji-fist_tone3 { background-position: -280px -420px; }
-.emoji-fist_tone4 { background-position: -300px -420px; }
-.emoji-fist_tone5 { background-position: -320px -420px; }
-.emoji-five { background-position: -340px -420px; }
-.emoji-flag_ac { background-position: -360px -420px; }
-.emoji-flag_ad { background-position: -380px -420px; }
-.emoji-flag_ae { background-position: -400px -420px; }
-.emoji-flag_af { background-position: -420px -420px; }
-.emoji-flag_ag { background-position: -440px 0; }
-.emoji-flag_ai { background-position: -440px -20px; }
-.emoji-flag_al { background-position: -440px -40px; }
-.emoji-flag_am { background-position: -440px -60px; }
-.emoji-flag_ao { background-position: -440px -80px; }
-.emoji-flag_aq { background-position: -440px -100px; }
-.emoji-flag_ar { background-position: -440px -120px; }
-.emoji-flag_as { background-position: -440px -140px; }
-.emoji-flag_at { background-position: -440px -160px; }
-.emoji-flag_au { background-position: -440px -180px; }
-.emoji-flag_aw { background-position: -440px -200px; }
-.emoji-flag_ax { background-position: -440px -220px; }
-.emoji-flag_az { background-position: -440px -240px; }
-.emoji-flag_ba { background-position: -440px -260px; }
-.emoji-flag_bb { background-position: -440px -280px; }
-.emoji-flag_bd { background-position: -440px -300px; }
-.emoji-flag_be { background-position: -440px -320px; }
-.emoji-flag_bf { background-position: -440px -340px; }
-.emoji-flag_bg { background-position: -440px -360px; }
-.emoji-flag_bh { background-position: -440px -380px; }
-.emoji-flag_bi { background-position: -440px -400px; }
-.emoji-flag_bj { background-position: -440px -420px; }
-.emoji-flag_bl { background-position: 0 -440px; }
-.emoji-flag_black { background-position: -20px -440px; }
-.emoji-flag_bm { background-position: -40px -440px; }
-.emoji-flag_bn { background-position: -60px -440px; }
-.emoji-flag_bo { background-position: -80px -440px; }
-.emoji-flag_bq { background-position: -100px -440px; }
-.emoji-flag_br { background-position: -120px -440px; }
-.emoji-flag_bs { background-position: -140px -440px; }
-.emoji-flag_bt { background-position: -160px -440px; }
-.emoji-flag_bv { background-position: -180px -440px; }
-.emoji-flag_bw { background-position: -200px -440px; }
-.emoji-flag_by { background-position: -220px -440px; }
-.emoji-flag_bz { background-position: -240px -440px; }
-.emoji-flag_ca { background-position: -260px -440px; }
-.emoji-flag_cc { background-position: -280px -440px; }
-.emoji-flag_cd { background-position: -300px -440px; }
-.emoji-flag_cf { background-position: -320px -440px; }
-.emoji-flag_cg { background-position: -340px -440px; }
-.emoji-flag_ch { background-position: -360px -440px; }
-.emoji-flag_ci { background-position: -380px -440px; }
-.emoji-flag_ck { background-position: -400px -440px; }
-.emoji-flag_cl { background-position: -420px -440px; }
-.emoji-flag_cm { background-position: -440px -440px; }
-.emoji-flag_cn { background-position: -460px 0; }
-.emoji-flag_co { background-position: -460px -20px; }
-.emoji-flag_cp { background-position: -460px -40px; }
-.emoji-flag_cr { background-position: -460px -60px; }
-.emoji-flag_cu { background-position: -460px -80px; }
-.emoji-flag_cv { background-position: -460px -100px; }
-.emoji-flag_cw { background-position: -460px -120px; }
-.emoji-flag_cx { background-position: -460px -140px; }
-.emoji-flag_cy { background-position: -460px -160px; }
-.emoji-flag_cz { background-position: -460px -180px; }
-.emoji-flag_de { background-position: -460px -200px; }
-.emoji-flag_dg { background-position: -460px -220px; }
-.emoji-flag_dj { background-position: -460px -240px; }
-.emoji-flag_dk { background-position: -460px -260px; }
-.emoji-flag_dm { background-position: -460px -280px; }
-.emoji-flag_do { background-position: -460px -300px; }
-.emoji-flag_dz { background-position: -460px -320px; }
-.emoji-flag_ea { background-position: -460px -340px; }
-.emoji-flag_ec { background-position: -460px -360px; }
-.emoji-flag_ee { background-position: -460px -380px; }
-.emoji-flag_eg { background-position: -460px -400px; }
-.emoji-flag_eh { background-position: -460px -420px; }
-.emoji-flag_er { background-position: -460px -440px; }
-.emoji-flag_es { background-position: 0 -460px; }
-.emoji-flag_et { background-position: -20px -460px; }
-.emoji-flag_eu { background-position: -40px -460px; }
-.emoji-flag_fi { background-position: -60px -460px; }
-.emoji-flag_fj { background-position: -80px -460px; }
-.emoji-flag_fk { background-position: -100px -460px; }
-.emoji-flag_fm { background-position: -120px -460px; }
-.emoji-flag_fo { background-position: -140px -460px; }
-.emoji-flag_fr { background-position: -160px -460px; }
-.emoji-flag_ga { background-position: -180px -460px; }
-.emoji-flag_gb { background-position: -200px -460px; }
-.emoji-flag_gd { background-position: -220px -460px; }
-.emoji-flag_ge { background-position: -240px -460px; }
-.emoji-flag_gf { background-position: -260px -460px; }
-.emoji-flag_gg { background-position: -280px -460px; }
-.emoji-flag_gh { background-position: -300px -460px; }
-.emoji-flag_gi { background-position: -320px -460px; }
-.emoji-flag_gl { background-position: -340px -460px; }
-.emoji-flag_gm { background-position: -360px -460px; }
-.emoji-flag_gn { background-position: -380px -460px; }
-.emoji-flag_gp { background-position: -400px -460px; }
-.emoji-flag_gq { background-position: -420px -460px; }
-.emoji-flag_gr { background-position: -440px -460px; }
-.emoji-flag_gs { background-position: -460px -460px; }
-.emoji-flag_gt { background-position: -480px 0; }
-.emoji-flag_gu { background-position: -480px -20px; }
-.emoji-flag_gw { background-position: -480px -40px; }
-.emoji-flag_gy { background-position: -480px -60px; }
-.emoji-flag_hk { background-position: -480px -80px; }
-.emoji-flag_hm { background-position: -480px -100px; }
-.emoji-flag_hn { background-position: -480px -120px; }
-.emoji-flag_hr { background-position: -480px -140px; }
-.emoji-flag_ht { background-position: -480px -160px; }
-.emoji-flag_hu { background-position: -480px -180px; }
-.emoji-flag_ic { background-position: -480px -200px; }
-.emoji-flag_id { background-position: -480px -220px; }
-.emoji-flag_ie { background-position: -480px -240px; }
-.emoji-flag_il { background-position: -480px -260px; }
-.emoji-flag_im { background-position: -480px -280px; }
-.emoji-flag_in { background-position: -480px -300px; }
-.emoji-flag_io { background-position: -480px -320px; }
-.emoji-flag_iq { background-position: -480px -340px; }
-.emoji-flag_ir { background-position: -480px -360px; }
-.emoji-flag_is { background-position: -480px -380px; }
-.emoji-flag_it { background-position: -480px -400px; }
-.emoji-flag_je { background-position: -480px -420px; }
-.emoji-flag_jm { background-position: -480px -440px; }
-.emoji-flag_jo { background-position: -480px -460px; }
-.emoji-flag_jp { background-position: 0 -480px; }
-.emoji-flag_ke { background-position: -20px -480px; }
-.emoji-flag_kg { background-position: -40px -480px; }
-.emoji-flag_kh { background-position: -60px -480px; }
-.emoji-flag_ki { background-position: -80px -480px; }
-.emoji-flag_km { background-position: -100px -480px; }
-.emoji-flag_kn { background-position: -120px -480px; }
-.emoji-flag_kp { background-position: -140px -480px; }
-.emoji-flag_kr { background-position: -160px -480px; }
-.emoji-flag_kw { background-position: -180px -480px; }
-.emoji-flag_ky { background-position: -200px -480px; }
-.emoji-flag_kz { background-position: -220px -480px; }
-.emoji-flag_la { background-position: -240px -480px; }
-.emoji-flag_lb { background-position: -260px -480px; }
-.emoji-flag_lc { background-position: -280px -480px; }
-.emoji-flag_li { background-position: -300px -480px; }
-.emoji-flag_lk { background-position: -320px -480px; }
-.emoji-flag_lr { background-position: -340px -480px; }
-.emoji-flag_ls { background-position: -360px -480px; }
-.emoji-flag_lt { background-position: -380px -480px; }
-.emoji-flag_lu { background-position: -400px -480px; }
-.emoji-flag_lv { background-position: -420px -480px; }
-.emoji-flag_ly { background-position: -440px -480px; }
-.emoji-flag_ma { background-position: -460px -480px; }
-.emoji-flag_mc { background-position: -480px -480px; }
-.emoji-flag_md { background-position: -500px 0; }
-.emoji-flag_me { background-position: -500px -20px; }
-.emoji-flag_mf { background-position: -500px -40px; }
-.emoji-flag_mg { background-position: -500px -60px; }
-.emoji-flag_mh { background-position: -500px -80px; }
-.emoji-flag_mk { background-position: -500px -100px; }
-.emoji-flag_ml { background-position: -500px -120px; }
-.emoji-flag_mm { background-position: -500px -140px; }
-.emoji-flag_mn { background-position: -500px -160px; }
-.emoji-flag_mo { background-position: -500px -180px; }
-.emoji-flag_mp { background-position: -500px -200px; }
-.emoji-flag_mq { background-position: -500px -220px; }
-.emoji-flag_mr { background-position: -500px -240px; }
-.emoji-flag_ms { background-position: -500px -260px; }
-.emoji-flag_mt { background-position: -500px -280px; }
-.emoji-flag_mu { background-position: -500px -300px; }
-.emoji-flag_mv { background-position: -500px -320px; }
-.emoji-flag_mw { background-position: -500px -340px; }
-.emoji-flag_mx { background-position: -500px -360px; }
-.emoji-flag_my { background-position: -500px -380px; }
-.emoji-flag_mz { background-position: -500px -400px; }
-.emoji-flag_na { background-position: -500px -420px; }
-.emoji-flag_nc { background-position: -500px -440px; }
-.emoji-flag_ne { background-position: -500px -460px; }
-.emoji-flag_nf { background-position: -500px -480px; }
-.emoji-flag_ng { background-position: 0 -500px; }
-.emoji-flag_ni { background-position: -20px -500px; }
-.emoji-flag_nl { background-position: -40px -500px; }
-.emoji-flag_no { background-position: -60px -500px; }
-.emoji-flag_np { background-position: -80px -500px; }
-.emoji-flag_nr { background-position: -100px -500px; }
-.emoji-flag_nu { background-position: -120px -500px; }
-.emoji-flag_nz { background-position: -140px -500px; }
-.emoji-flag_om { background-position: -160px -500px; }
-.emoji-flag_pa { background-position: -180px -500px; }
-.emoji-flag_pe { background-position: -200px -500px; }
-.emoji-flag_pf { background-position: -220px -500px; }
-.emoji-flag_pg { background-position: -240px -500px; }
-.emoji-flag_ph { background-position: -260px -500px; }
-.emoji-flag_pk { background-position: -280px -500px; }
-.emoji-flag_pl { background-position: -300px -500px; }
-.emoji-flag_pm { background-position: -320px -500px; }
-.emoji-flag_pn { background-position: -340px -500px; }
-.emoji-flag_pr { background-position: -360px -500px; }
-.emoji-flag_ps { background-position: -380px -500px; }
-.emoji-flag_pt { background-position: -400px -500px; }
-.emoji-flag_pw { background-position: -420px -500px; }
-.emoji-flag_py { background-position: -440px -500px; }
-.emoji-flag_qa { background-position: -460px -500px; }
-.emoji-flag_re { background-position: -480px -500px; }
-.emoji-flag_ro { background-position: -500px -500px; }
-.emoji-flag_rs { background-position: -520px 0; }
-.emoji-flag_ru { background-position: -520px -20px; }
-.emoji-flag_rw { background-position: -520px -40px; }
-.emoji-flag_sa { background-position: -520px -60px; }
-.emoji-flag_sb { background-position: -520px -80px; }
-.emoji-flag_sc { background-position: -520px -100px; }
-.emoji-flag_sd { background-position: -520px -120px; }
-.emoji-flag_se { background-position: -520px -140px; }
-.emoji-flag_sg { background-position: -520px -160px; }
-.emoji-flag_sh { background-position: -520px -180px; }
-.emoji-flag_si { background-position: -520px -200px; }
-.emoji-flag_sj { background-position: -520px -220px; }
-.emoji-flag_sk { background-position: -520px -240px; }
-.emoji-flag_sl { background-position: -520px -260px; }
-.emoji-flag_sm { background-position: -520px -280px; }
-.emoji-flag_sn { background-position: -520px -300px; }
-.emoji-flag_so { background-position: -520px -320px; }
-.emoji-flag_sr { background-position: -520px -340px; }
-.emoji-flag_ss { background-position: -520px -360px; }
-.emoji-flag_st { background-position: -520px -380px; }
-.emoji-flag_sv { background-position: -520px -400px; }
-.emoji-flag_sx { background-position: -520px -420px; }
-.emoji-flag_sy { background-position: -520px -440px; }
-.emoji-flag_sz { background-position: -520px -460px; }
-.emoji-flag_ta { background-position: -520px -480px; }
-.emoji-flag_tc { background-position: -520px -500px; }
-.emoji-flag_td { background-position: 0 -520px; }
-.emoji-flag_tf { background-position: -20px -520px; }
-.emoji-flag_tg { background-position: -40px -520px; }
-.emoji-flag_th { background-position: -60px -520px; }
-.emoji-flag_tj { background-position: -80px -520px; }
-.emoji-flag_tk { background-position: -100px -520px; }
-.emoji-flag_tl { background-position: -120px -520px; }
-.emoji-flag_tm { background-position: -140px -520px; }
-.emoji-flag_tn { background-position: -160px -520px; }
-.emoji-flag_to { background-position: -180px -520px; }
-.emoji-flag_tr { background-position: -200px -520px; }
-.emoji-flag_tt { background-position: -220px -520px; }
-.emoji-flag_tv { background-position: -240px -520px; }
-.emoji-flag_tw { background-position: -260px -520px; }
-.emoji-flag_tz { background-position: -280px -520px; }
-.emoji-flag_ua { background-position: -300px -520px; }
-.emoji-flag_ug { background-position: -320px -520px; }
-.emoji-flag_um { background-position: -340px -520px; }
-.emoji-flag_us { background-position: -360px -520px; }
-.emoji-flag_uy { background-position: -380px -520px; }
-.emoji-flag_uz { background-position: -400px -520px; }
-.emoji-flag_va { background-position: -420px -520px; }
-.emoji-flag_vc { background-position: -440px -520px; }
-.emoji-flag_ve { background-position: -460px -520px; }
-.emoji-flag_vg { background-position: -480px -520px; }
-.emoji-flag_vi { background-position: -500px -520px; }
-.emoji-flag_vn { background-position: -520px -520px; }
-.emoji-flag_vu { background-position: -540px 0; }
-.emoji-flag_wf { background-position: -540px -20px; }
-.emoji-flag_white { background-position: -540px -40px; }
-.emoji-flag_ws { background-position: -540px -60px; }
-.emoji-flag_xk { background-position: -540px -80px; }
-.emoji-flag_ye { background-position: -540px -100px; }
-.emoji-flag_yt { background-position: -540px -120px; }
-.emoji-flag_za { background-position: -540px -140px; }
-.emoji-flag_zm { background-position: -540px -160px; }
-.emoji-flag_zw { background-position: -540px -180px; }
-.emoji-flags { background-position: -540px -200px; }
-.emoji-flashlight { background-position: -540px -220px; }
-.emoji-fleur-de-lis { background-position: -540px -240px; }
-.emoji-floppy_disk { background-position: -540px -260px; }
-.emoji-flower_playing_cards { background-position: -540px -280px; }
-.emoji-flushed { background-position: -540px -300px; }
-.emoji-fog { background-position: -540px -320px; }
-.emoji-foggy { background-position: -540px -340px; }
-.emoji-football { background-position: -540px -360px; }
-.emoji-footprints { background-position: -540px -380px; }
-.emoji-fork_and_knife { background-position: -540px -400px; }
-.emoji-fork_knife_plate { background-position: -540px -420px; }
-.emoji-fountain { background-position: -540px -440px; }
-.emoji-four { background-position: -540px -460px; }
-.emoji-four_leaf_clover { background-position: -540px -480px; }
-.emoji-fox { background-position: -540px -500px; }
-.emoji-frame_photo { background-position: -540px -520px; }
-.emoji-free { background-position: 0 -540px; }
-.emoji-french_bread { background-position: -20px -540px; }
-.emoji-fried_shrimp { background-position: -40px -540px; }
-.emoji-fries { background-position: -60px -540px; }
-.emoji-frog { background-position: -80px -540px; }
-.emoji-frowning { background-position: -100px -540px; }
-.emoji-frowning2 { background-position: -120px -540px; }
-.emoji-fuelpump { background-position: -140px -540px; }
-.emoji-full_moon { background-position: -160px -540px; }
-.emoji-full_moon_with_face { background-position: -180px -540px; }
-.emoji-game_die { background-position: -200px -540px; }
-.emoji-gay_pride_flag { background-position: -220px -540px; }
-.emoji-gear { background-position: -240px -540px; }
-.emoji-gem { background-position: -260px -540px; }
-.emoji-gemini { background-position: -280px -540px; }
-.emoji-ghost { background-position: -300px -540px; }
-.emoji-gift { background-position: -320px -540px; }
-.emoji-gift_heart { background-position: -340px -540px; }
-.emoji-girl { background-position: -360px -540px; }
-.emoji-girl_tone1 { background-position: -380px -540px; }
-.emoji-girl_tone2 { background-position: -400px -540px; }
-.emoji-girl_tone3 { background-position: -420px -540px; }
-.emoji-girl_tone4 { background-position: -440px -540px; }
-.emoji-girl_tone5 { background-position: -460px -540px; }
-.emoji-globe_with_meridians { background-position: -480px -540px; }
-.emoji-goal { background-position: -500px -540px; }
-.emoji-goat { background-position: -520px -540px; }
-.emoji-golf { background-position: -540px -540px; }
-.emoji-golfer { background-position: -560px 0; }
-.emoji-gorilla { background-position: -560px -20px; }
-.emoji-grapes { background-position: -560px -40px; }
-.emoji-green_apple { background-position: -560px -60px; }
-.emoji-green_book { background-position: -560px -80px; }
-.emoji-green_heart { background-position: -560px -100px; }
-.emoji-grey_exclamation { background-position: -560px -120px; }
-.emoji-grey_question { background-position: -560px -140px; }
-.emoji-grimacing { background-position: -560px -160px; }
-.emoji-grin { background-position: -560px -180px; }
-.emoji-grinning { background-position: -560px -200px; }
-.emoji-guardsman { background-position: -560px -220px; }
-.emoji-guardsman_tone1 { background-position: -560px -240px; }
-.emoji-guardsman_tone2 { background-position: -560px -260px; }
-.emoji-guardsman_tone3 { background-position: -560px -280px; }
-.emoji-guardsman_tone4 { background-position: -560px -300px; }
-.emoji-guardsman_tone5 { background-position: -560px -320px; }
-.emoji-guitar { background-position: -560px -340px; }
-.emoji-gun { background-position: -560px -360px; }
-.emoji-haircut { background-position: -560px -380px; }
-.emoji-haircut_tone1 { background-position: -560px -400px; }
-.emoji-haircut_tone2 { background-position: -560px -420px; }
-.emoji-haircut_tone3 { background-position: -560px -440px; }
-.emoji-haircut_tone4 { background-position: -560px -460px; }
-.emoji-haircut_tone5 { background-position: -560px -480px; }
-.emoji-hamburger { background-position: -560px -500px; }
-.emoji-hammer { background-position: -560px -520px; }
-.emoji-hammer_pick { background-position: -560px -540px; }
-.emoji-hamster { background-position: 0 -560px; }
-.emoji-hand_splayed { background-position: -20px -560px; }
-.emoji-hand_splayed_tone1 { background-position: -40px -560px; }
-.emoji-hand_splayed_tone2 { background-position: -60px -560px; }
-.emoji-hand_splayed_tone3 { background-position: -80px -560px; }
-.emoji-hand_splayed_tone4 { background-position: -100px -560px; }
-.emoji-hand_splayed_tone5 { background-position: -120px -560px; }
-.emoji-handbag { background-position: -140px -560px; }
-.emoji-handball { background-position: -160px -560px; }
-.emoji-handball_tone1 { background-position: -180px -560px; }
-.emoji-handball_tone2 { background-position: -200px -560px; }
-.emoji-handball_tone3 { background-position: -220px -560px; }
-.emoji-handball_tone4 { background-position: -240px -560px; }
-.emoji-handball_tone5 { background-position: -260px -560px; }
-.emoji-handshake { background-position: -280px -560px; }
-.emoji-handshake_tone1 { background-position: -300px -560px; }
-.emoji-handshake_tone2 { background-position: -320px -560px; }
-.emoji-handshake_tone3 { background-position: -340px -560px; }
-.emoji-handshake_tone4 { background-position: -360px -560px; }
-.emoji-handshake_tone5 { background-position: -380px -560px; }
-.emoji-hash { background-position: -400px -560px; }
-.emoji-hatched_chick { background-position: -420px -560px; }
-.emoji-hatching_chick { background-position: -440px -560px; }
-.emoji-head_bandage { background-position: -460px -560px; }
-.emoji-headphones { background-position: -480px -560px; }
-.emoji-hear_no_evil { background-position: -500px -560px; }
-.emoji-heart { background-position: -520px -560px; }
-.emoji-heart_decoration { background-position: -540px -560px; }
-.emoji-heart_exclamation { background-position: -560px -560px; }
-.emoji-heart_eyes { background-position: -580px 0; }
-.emoji-heart_eyes_cat { background-position: -580px -20px; }
-.emoji-heartbeat { background-position: -580px -40px; }
-.emoji-heartpulse { background-position: -580px -60px; }
-.emoji-hearts { background-position: -580px -80px; }
-.emoji-heavy_check_mark { background-position: -580px -100px; }
-.emoji-heavy_division_sign { background-position: -580px -120px; }
-.emoji-heavy_dollar_sign { background-position: -580px -140px; }
-.emoji-heavy_minus_sign { background-position: -580px -160px; }
-.emoji-heavy_multiplication_x { background-position: -580px -180px; }
-.emoji-heavy_plus_sign { background-position: -580px -200px; }
-.emoji-helicopter { background-position: -580px -220px; }
-.emoji-helmet_with_cross { background-position: -580px -240px; }
-.emoji-herb { background-position: -580px -260px; }
-.emoji-hibiscus { background-position: -580px -280px; }
-.emoji-high_brightness { background-position: -580px -300px; }
-.emoji-high_heel { background-position: -580px -320px; }
-.emoji-hockey { background-position: -580px -340px; }
-.emoji-hole { background-position: -580px -360px; }
-.emoji-homes { background-position: -580px -380px; }
-.emoji-honey_pot { background-position: -580px -400px; }
-.emoji-horse { background-position: -580px -420px; }
-.emoji-horse_racing { background-position: -580px -440px; }
-.emoji-horse_racing_tone1 { background-position: -580px -460px; }
-.emoji-horse_racing_tone2 { background-position: -580px -480px; }
-.emoji-horse_racing_tone3 { background-position: -580px -500px; }
-.emoji-horse_racing_tone4 { background-position: -580px -520px; }
-.emoji-horse_racing_tone5 { background-position: -580px -540px; }
-.emoji-hospital { background-position: -580px -560px; }
-.emoji-hot_pepper { background-position: 0 -580px; }
-.emoji-hotdog { background-position: -20px -580px; }
-.emoji-hotel { background-position: -40px -580px; }
-.emoji-hotsprings { background-position: -60px -580px; }
-.emoji-hourglass { background-position: -80px -580px; }
-.emoji-hourglass_flowing_sand { background-position: -100px -580px; }
-.emoji-house { background-position: -120px -580px; }
-.emoji-house_abandoned { background-position: -140px -580px; }
-.emoji-house_with_garden { background-position: -160px -580px; }
-.emoji-hugging { background-position: -180px -580px; }
-.emoji-hushed { background-position: -200px -580px; }
-.emoji-ice_cream { background-position: -220px -580px; }
-.emoji-ice_skate { background-position: -240px -580px; }
-.emoji-icecream { background-position: -260px -580px; }
-.emoji-id { background-position: -280px -580px; }
-.emoji-ideograph_advantage { background-position: -300px -580px; }
-.emoji-imp { background-position: -320px -580px; }
-.emoji-inbox_tray { background-position: -340px -580px; }
-.emoji-incoming_envelope { background-position: -360px -580px; }
-.emoji-information_desk_person { background-position: -380px -580px; }
-.emoji-information_desk_person_tone1 { background-position: -400px -580px; }
-.emoji-information_desk_person_tone2 { background-position: -420px -580px; }
-.emoji-information_desk_person_tone3 { background-position: -440px -580px; }
-.emoji-information_desk_person_tone4 { background-position: -460px -580px; }
-.emoji-information_desk_person_tone5 { background-position: -480px -580px; }
-.emoji-information_source { background-position: -500px -580px; }
-.emoji-innocent { background-position: -520px -580px; }
-.emoji-interrobang { background-position: -540px -580px; }
-.emoji-iphone { background-position: -560px -580px; }
-.emoji-island { background-position: -580px -580px; }
-.emoji-izakaya_lantern { background-position: -600px 0; }
-.emoji-jack_o_lantern { background-position: -600px -20px; }
-.emoji-japan { background-position: -600px -40px; }
-.emoji-japanese_castle { background-position: -600px -60px; }
-.emoji-japanese_goblin { background-position: -600px -80px; }
-.emoji-japanese_ogre { background-position: -600px -100px; }
-.emoji-jeans { background-position: -600px -120px; }
-.emoji-joy { background-position: -600px -140px; }
-.emoji-joy_cat { background-position: -600px -160px; }
-.emoji-joystick { background-position: -600px -180px; }
-.emoji-juggling { background-position: -600px -200px; }
-.emoji-juggling_tone1 { background-position: -600px -220px; }
-.emoji-juggling_tone2 { background-position: -600px -240px; }
-.emoji-juggling_tone3 { background-position: -600px -260px; }
-.emoji-juggling_tone4 { background-position: -600px -280px; }
-.emoji-juggling_tone5 { background-position: -600px -300px; }
-.emoji-kaaba { background-position: -600px -320px; }
-.emoji-key { background-position: -600px -340px; }
-.emoji-key2 { background-position: -600px -360px; }
-.emoji-keyboard { background-position: -600px -380px; }
-.emoji-kimono { background-position: -600px -400px; }
-.emoji-kiss { background-position: -600px -420px; }
-.emoji-kiss_mm { background-position: -600px -440px; }
-.emoji-kiss_ww { background-position: -600px -460px; }
-.emoji-kissing { background-position: -600px -480px; }
-.emoji-kissing_cat { background-position: -600px -500px; }
-.emoji-kissing_closed_eyes { background-position: -600px -520px; }
-.emoji-kissing_heart { background-position: -600px -540px; }
-.emoji-kissing_smiling_eyes { background-position: -600px -560px; }
-.emoji-kiwi { background-position: -600px -580px; }
-.emoji-knife { background-position: 0 -600px; }
-.emoji-koala { background-position: -20px -600px; }
-.emoji-koko { background-position: -40px -600px; }
-.emoji-label { background-position: -60px -600px; }
-.emoji-large_blue_circle { background-position: -80px -600px; }
-.emoji-large_blue_diamond { background-position: -100px -600px; }
-.emoji-large_orange_diamond { background-position: -120px -600px; }
-.emoji-last_quarter_moon { background-position: -140px -600px; }
-.emoji-last_quarter_moon_with_face { background-position: -160px -600px; }
-.emoji-laughing { background-position: -180px -600px; }
-.emoji-leaves { background-position: -200px -600px; }
-.emoji-ledger { background-position: -220px -600px; }
-.emoji-left_facing_fist { background-position: -240px -600px; }
-.emoji-left_facing_fist_tone1 { background-position: -260px -600px; }
-.emoji-left_facing_fist_tone2 { background-position: -280px -600px; }
-.emoji-left_facing_fist_tone3 { background-position: -300px -600px; }
-.emoji-left_facing_fist_tone4 { background-position: -320px -600px; }
-.emoji-left_facing_fist_tone5 { background-position: -340px -600px; }
-.emoji-left_luggage { background-position: -360px -600px; }
-.emoji-left_right_arrow { background-position: -380px -600px; }
-.emoji-leftwards_arrow_with_hook { background-position: -400px -600px; }
-.emoji-lemon { background-position: -420px -600px; }
-.emoji-leo { background-position: -440px -600px; }
-.emoji-leopard { background-position: -460px -600px; }
-.emoji-level_slider { background-position: -480px -600px; }
-.emoji-levitate { background-position: -500px -600px; }
-.emoji-libra { background-position: -520px -600px; }
-.emoji-lifter { background-position: -540px -600px; }
-.emoji-lifter_tone1 { background-position: -560px -600px; }
-.emoji-lifter_tone2 { background-position: -580px -600px; }
-.emoji-lifter_tone3 { background-position: -600px -600px; }
-.emoji-lifter_tone4 { background-position: -620px 0; }
-.emoji-lifter_tone5 { background-position: -620px -20px; }
-.emoji-light_rail { background-position: -620px -40px; }
-.emoji-link { background-position: -620px -60px; }
-.emoji-lion_face { background-position: -620px -80px; }
-.emoji-lips { background-position: -620px -100px; }
-.emoji-lipstick { background-position: -620px -120px; }
-.emoji-lizard { background-position: -620px -140px; }
-.emoji-lock { background-position: -620px -160px; }
-.emoji-lock_with_ink_pen { background-position: -620px -180px; }
-.emoji-lollipop { background-position: -620px -200px; }
-.emoji-loop { background-position: -620px -220px; }
-.emoji-loud_sound { background-position: -620px -240px; }
-.emoji-loudspeaker { background-position: -620px -260px; }
-.emoji-love_hotel { background-position: -620px -280px; }
-.emoji-love_letter { background-position: -620px -300px; }
-.emoji-low_brightness { background-position: -620px -320px; }
-.emoji-lying_face { background-position: -620px -340px; }
-.emoji-m { background-position: -620px -360px; }
-.emoji-mag { background-position: -620px -380px; }
-.emoji-mag_right { background-position: -620px -400px; }
-.emoji-mahjong { background-position: -620px -420px; }
-.emoji-mailbox { background-position: -620px -440px; }
-.emoji-mailbox_closed { background-position: -620px -460px; }
-.emoji-mailbox_with_mail { background-position: -620px -480px; }
-.emoji-mailbox_with_no_mail { background-position: -620px -500px; }
-.emoji-man { background-position: -620px -520px; }
-.emoji-man_dancing { background-position: -620px -540px; }
-.emoji-man_dancing_tone1 { background-position: -620px -560px; }
-.emoji-man_dancing_tone2 { background-position: -620px -580px; }
-.emoji-man_dancing_tone3 { background-position: -620px -600px; }
-.emoji-man_dancing_tone4 { background-position: 0 -620px; }
-.emoji-man_dancing_tone5 { background-position: -20px -620px; }
-.emoji-man_in_tuxedo { background-position: -40px -620px; }
-.emoji-man_in_tuxedo_tone1 { background-position: -60px -620px; }
-.emoji-man_in_tuxedo_tone2 { background-position: -80px -620px; }
-.emoji-man_in_tuxedo_tone3 { background-position: -100px -620px; }
-.emoji-man_in_tuxedo_tone4 { background-position: -120px -620px; }
-.emoji-man_in_tuxedo_tone5 { background-position: -140px -620px; }
-.emoji-man_tone1 { background-position: -160px -620px; }
-.emoji-man_tone2 { background-position: -180px -620px; }
-.emoji-man_tone3 { background-position: -200px -620px; }
-.emoji-man_tone4 { background-position: -220px -620px; }
-.emoji-man_tone5 { background-position: -240px -620px; }
-.emoji-man_with_gua_pi_mao { background-position: -260px -620px; }
-.emoji-man_with_gua_pi_mao_tone1 { background-position: -280px -620px; }
-.emoji-man_with_gua_pi_mao_tone2 { background-position: -300px -620px; }
-.emoji-man_with_gua_pi_mao_tone3 { background-position: -320px -620px; }
-.emoji-man_with_gua_pi_mao_tone4 { background-position: -340px -620px; }
-.emoji-man_with_gua_pi_mao_tone5 { background-position: -360px -620px; }
-.emoji-man_with_turban { background-position: -380px -620px; }
-.emoji-man_with_turban_tone1 { background-position: -400px -620px; }
-.emoji-man_with_turban_tone2 { background-position: -420px -620px; }
-.emoji-man_with_turban_tone3 { background-position: -440px -620px; }
-.emoji-man_with_turban_tone4 { background-position: -460px -620px; }
-.emoji-man_with_turban_tone5 { background-position: -480px -620px; }
-.emoji-mans_shoe { background-position: -500px -620px; }
-.emoji-map { background-position: -520px -620px; }
-.emoji-maple_leaf { background-position: -540px -620px; }
-.emoji-martial_arts_uniform { background-position: -560px -620px; }
-.emoji-mask { background-position: -580px -620px; }
-.emoji-massage { background-position: -600px -620px; }
-.emoji-massage_tone1 { background-position: -620px -620px; }
-.emoji-massage_tone2 { background-position: -640px 0; }
-.emoji-massage_tone3 { background-position: -640px -20px; }
-.emoji-massage_tone4 { background-position: -640px -40px; }
-.emoji-massage_tone5 { background-position: -640px -60px; }
-.emoji-meat_on_bone { background-position: -640px -80px; }
-.emoji-medal { background-position: -640px -100px; }
-.emoji-mega { background-position: -640px -120px; }
-.emoji-melon { background-position: -640px -140px; }
-.emoji-menorah { background-position: -640px -160px; }
-.emoji-mens { background-position: -640px -180px; }
-.emoji-metal { background-position: -640px -200px; }
-.emoji-metal_tone1 { background-position: -640px -220px; }
-.emoji-metal_tone2 { background-position: -640px -240px; }
-.emoji-metal_tone3 { background-position: -640px -260px; }
-.emoji-metal_tone4 { background-position: -640px -280px; }
-.emoji-metal_tone5 { background-position: -640px -300px; }
-.emoji-metro { background-position: -640px -320px; }
-.emoji-microphone { background-position: -640px -340px; }
-.emoji-microphone2 { background-position: -640px -360px; }
-.emoji-microscope { background-position: -640px -380px; }
-.emoji-middle_finger { background-position: -640px -400px; }
-.emoji-middle_finger_tone1 { background-position: -640px -420px; }
-.emoji-middle_finger_tone2 { background-position: -640px -440px; }
-.emoji-middle_finger_tone3 { background-position: -640px -460px; }
-.emoji-middle_finger_tone4 { background-position: -640px -480px; }
-.emoji-middle_finger_tone5 { background-position: -640px -500px; }
-.emoji-military_medal { background-position: -640px -520px; }
-.emoji-milk { background-position: -640px -540px; }
-.emoji-milky_way { background-position: -640px -560px; }
-.emoji-minibus { background-position: -640px -580px; }
-.emoji-minidisc { background-position: -640px -600px; }
-.emoji-mobile_phone_off { background-position: -640px -620px; }
-.emoji-money_mouth { background-position: 0 -640px; }
-.emoji-money_with_wings { background-position: -20px -640px; }
-.emoji-moneybag { background-position: -40px -640px; }
-.emoji-monkey { background-position: -60px -640px; }
-.emoji-monkey_face { background-position: -80px -640px; }
-.emoji-monorail { background-position: -100px -640px; }
-.emoji-mortar_board { background-position: -120px -640px; }
-.emoji-mosque { background-position: -140px -640px; }
-.emoji-motor_scooter { background-position: -160px -640px; }
-.emoji-motorboat { background-position: -180px -640px; }
-.emoji-motorcycle { background-position: -200px -640px; }
-.emoji-motorway { background-position: -220px -640px; }
-.emoji-mount_fuji { background-position: -240px -640px; }
-.emoji-mountain { background-position: -260px -640px; }
-.emoji-mountain_bicyclist { background-position: -280px -640px; }
-.emoji-mountain_bicyclist_tone1 { background-position: -300px -640px; }
-.emoji-mountain_bicyclist_tone2 { background-position: -320px -640px; }
-.emoji-mountain_bicyclist_tone3 { background-position: -340px -640px; }
-.emoji-mountain_bicyclist_tone4 { background-position: -360px -640px; }
-.emoji-mountain_bicyclist_tone5 { background-position: -380px -640px; }
-.emoji-mountain_cableway { background-position: -400px -640px; }
-.emoji-mountain_railway { background-position: -420px -640px; }
-.emoji-mountain_snow { background-position: -440px -640px; }
-.emoji-mouse { background-position: -460px -640px; }
-.emoji-mouse2 { background-position: -480px -640px; }
-.emoji-mouse_three_button { background-position: -500px -640px; }
-.emoji-movie_camera { background-position: -520px -640px; }
-.emoji-moyai { background-position: -540px -640px; }
-.emoji-mrs_claus { background-position: -560px -640px; }
-.emoji-mrs_claus_tone1 { background-position: -580px -640px; }
-.emoji-mrs_claus_tone2 { background-position: -600px -640px; }
-.emoji-mrs_claus_tone3 { background-position: -620px -640px; }
-.emoji-mrs_claus_tone4 { background-position: -640px -640px; }
-.emoji-mrs_claus_tone5 { background-position: -660px 0; }
-.emoji-muscle { background-position: -660px -20px; }
-.emoji-muscle_tone1 { background-position: -660px -40px; }
-.emoji-muscle_tone2 { background-position: -660px -60px; }
-.emoji-muscle_tone3 { background-position: -660px -80px; }
-.emoji-muscle_tone4 { background-position: -660px -100px; }
-.emoji-muscle_tone5 { background-position: -660px -120px; }
-.emoji-mushroom { background-position: -660px -140px; }
-.emoji-musical_keyboard { background-position: -660px -160px; }
-.emoji-musical_note { background-position: -660px -180px; }
-.emoji-musical_score { background-position: -660px -200px; }
-.emoji-mute { background-position: -660px -220px; }
-.emoji-nail_care { background-position: -660px -240px; }
-.emoji-nail_care_tone1 { background-position: -660px -260px; }
-.emoji-nail_care_tone2 { background-position: -660px -280px; }
-.emoji-nail_care_tone3 { background-position: -660px -300px; }
-.emoji-nail_care_tone4 { background-position: -660px -320px; }
-.emoji-nail_care_tone5 { background-position: -660px -340px; }
-.emoji-name_badge { background-position: -660px -360px; }
-.emoji-nauseated_face { background-position: -660px -380px; }
-.emoji-necktie { background-position: -660px -400px; }
-.emoji-negative_squared_cross_mark { background-position: -660px -420px; }
-.emoji-nerd { background-position: -660px -440px; }
-.emoji-neutral_face { background-position: -660px -460px; }
-.emoji-new { background-position: -660px -480px; }
-.emoji-new_moon { background-position: -660px -500px; }
-.emoji-new_moon_with_face { background-position: -660px -520px; }
-.emoji-newspaper { background-position: -660px -540px; }
-.emoji-newspaper2 { background-position: -660px -560px; }
-.emoji-ng { background-position: -660px -580px; }
-.emoji-night_with_stars { background-position: -660px -600px; }
-.emoji-nine { background-position: -660px -620px; }
-.emoji-no_bell { background-position: -660px -640px; }
-.emoji-no_bicycles { background-position: 0 -660px; }
-.emoji-no_entry { background-position: -20px -660px; }
-.emoji-no_entry_sign { background-position: -40px -660px; }
-.emoji-no_good { background-position: -60px -660px; }
-.emoji-no_good_tone1 { background-position: -80px -660px; }
-.emoji-no_good_tone2 { background-position: -100px -660px; }
-.emoji-no_good_tone3 { background-position: -120px -660px; }
-.emoji-no_good_tone4 { background-position: -140px -660px; }
-.emoji-no_good_tone5 { background-position: -160px -660px; }
-.emoji-no_mobile_phones { background-position: -180px -660px; }
-.emoji-no_mouth { background-position: -200px -660px; }
-.emoji-no_pedestrians { background-position: -220px -660px; }
-.emoji-no_smoking { background-position: -240px -660px; }
-.emoji-non-potable_water { background-position: -260px -660px; }
-.emoji-nose { background-position: -280px -660px; }
-.emoji-nose_tone1 { background-position: -300px -660px; }
-.emoji-nose_tone2 { background-position: -320px -660px; }
-.emoji-nose_tone3 { background-position: -340px -660px; }
-.emoji-nose_tone4 { background-position: -360px -660px; }
-.emoji-nose_tone5 { background-position: -380px -660px; }
-.emoji-notebook { background-position: -400px -660px; }
-.emoji-notebook_with_decorative_cover { background-position: -420px -660px; }
-.emoji-notepad_spiral { background-position: -440px -660px; }
-.emoji-notes { background-position: -460px -660px; }
-.emoji-nut_and_bolt { background-position: -480px -660px; }
-.emoji-o { background-position: -500px -660px; }
-.emoji-o2 { background-position: -520px -660px; }
-.emoji-ocean { background-position: -540px -660px; }
-.emoji-octagonal_sign { background-position: -560px -660px; }
-.emoji-octopus { background-position: -580px -660px; }
-.emoji-oden { background-position: -600px -660px; }
-.emoji-office { background-position: -620px -660px; }
-.emoji-oil { background-position: -640px -660px; }
-.emoji-ok { background-position: -660px -660px; }
-.emoji-ok_hand { background-position: -680px 0; }
-.emoji-ok_hand_tone1 { background-position: -680px -20px; }
-.emoji-ok_hand_tone2 { background-position: -680px -40px; }
-.emoji-ok_hand_tone3 { background-position: -680px -60px; }
-.emoji-ok_hand_tone4 { background-position: -680px -80px; }
-.emoji-ok_hand_tone5 { background-position: -680px -100px; }
-.emoji-ok_woman { background-position: -680px -120px; }
-.emoji-ok_woman_tone1 { background-position: -680px -140px; }
-.emoji-ok_woman_tone2 { background-position: -680px -160px; }
-.emoji-ok_woman_tone3 { background-position: -680px -180px; }
-.emoji-ok_woman_tone4 { background-position: -680px -200px; }
-.emoji-ok_woman_tone5 { background-position: -680px -220px; }
-.emoji-older_man { background-position: -680px -240px; }
-.emoji-older_man_tone1 { background-position: -680px -260px; }
-.emoji-older_man_tone2 { background-position: -680px -280px; }
-.emoji-older_man_tone3 { background-position: -680px -300px; }
-.emoji-older_man_tone4 { background-position: -680px -320px; }
-.emoji-older_man_tone5 { background-position: -680px -340px; }
-.emoji-older_woman { background-position: -680px -360px; }
-.emoji-older_woman_tone1 { background-position: -680px -380px; }
-.emoji-older_woman_tone2 { background-position: -680px -400px; }
-.emoji-older_woman_tone3 { background-position: -680px -420px; }
-.emoji-older_woman_tone4 { background-position: -680px -440px; }
-.emoji-older_woman_tone5 { background-position: -680px -460px; }
-.emoji-om_symbol { background-position: -680px -480px; }
-.emoji-on { background-position: -680px -500px; }
-.emoji-oncoming_automobile { background-position: -680px -520px; }
-.emoji-oncoming_bus { background-position: -680px -540px; }
-.emoji-oncoming_police_car { background-position: -680px -560px; }
-.emoji-oncoming_taxi { background-position: -680px -580px; }
-.emoji-one { background-position: -680px -600px; }
-.emoji-open_file_folder { background-position: -680px -620px; }
-.emoji-open_hands { background-position: -680px -640px; }
-.emoji-open_hands_tone1 { background-position: -680px -660px; }
-.emoji-open_hands_tone2 { background-position: 0 -680px; }
-.emoji-open_hands_tone3 { background-position: -20px -680px; }
-.emoji-open_hands_tone4 { background-position: -40px -680px; }
-.emoji-open_hands_tone5 { background-position: -60px -680px; }
-.emoji-open_mouth { background-position: -80px -680px; }
-.emoji-ophiuchus { background-position: -100px -680px; }
-.emoji-orange_book { background-position: -120px -680px; }
-.emoji-orthodox_cross { background-position: -140px -680px; }
-.emoji-outbox_tray { background-position: -160px -680px; }
-.emoji-owl { background-position: -180px -680px; }
-.emoji-ox { background-position: -200px -680px; }
-.emoji-package { background-position: -220px -680px; }
-.emoji-page_facing_up { background-position: -240px -680px; }
-.emoji-page_with_curl { background-position: -260px -680px; }
-.emoji-pager { background-position: -280px -680px; }
-.emoji-paintbrush { background-position: -300px -680px; }
-.emoji-palm_tree { background-position: -320px -680px; }
-.emoji-pancakes { background-position: -340px -680px; }
-.emoji-panda_face { background-position: -360px -680px; }
-.emoji-paperclip { background-position: -380px -680px; }
-.emoji-paperclips { background-position: -400px -680px; }
-.emoji-park { background-position: -420px -680px; }
-.emoji-parking { background-position: -440px -680px; }
-.emoji-part_alternation_mark { background-position: -460px -680px; }
-.emoji-partly_sunny { background-position: -480px -680px; }
-.emoji-passport_control { background-position: -500px -680px; }
-.emoji-pause_button { background-position: -520px -680px; }
-.emoji-peace { background-position: -540px -680px; }
-.emoji-peach { background-position: -560px -680px; }
-.emoji-peanuts { background-position: -580px -680px; }
-.emoji-pear { background-position: -600px -680px; }
-.emoji-pen_ballpoint { background-position: -620px -680px; }
-.emoji-pen_fountain { background-position: -640px -680px; }
-.emoji-pencil { background-position: -660px -680px; }
-.emoji-pencil2 { background-position: -680px -680px; }
-.emoji-penguin { background-position: -700px 0; }
-.emoji-pensive { background-position: -700px -20px; }
-.emoji-performing_arts { background-position: -700px -40px; }
-.emoji-persevere { background-position: -700px -60px; }
-.emoji-person_frowning { background-position: -700px -80px; }
-.emoji-person_frowning_tone1 { background-position: -700px -100px; }
-.emoji-person_frowning_tone2 { background-position: -700px -120px; }
-.emoji-person_frowning_tone3 { background-position: -700px -140px; }
-.emoji-person_frowning_tone4 { background-position: -700px -160px; }
-.emoji-person_frowning_tone5 { background-position: -700px -180px; }
-.emoji-person_with_blond_hair { background-position: -700px -200px; }
-.emoji-person_with_blond_hair_tone1 { background-position: -700px -220px; }
-.emoji-person_with_blond_hair_tone2 { background-position: -700px -240px; }
-.emoji-person_with_blond_hair_tone3 { background-position: -700px -260px; }
-.emoji-person_with_blond_hair_tone4 { background-position: -700px -280px; }
-.emoji-person_with_blond_hair_tone5 { background-position: -700px -300px; }
-.emoji-person_with_pouting_face { background-position: -700px -320px; }
-.emoji-person_with_pouting_face_tone1 { background-position: -700px -340px; }
-.emoji-person_with_pouting_face_tone2 { background-position: -700px -360px; }
-.emoji-person_with_pouting_face_tone3 { background-position: -700px -380px; }
-.emoji-person_with_pouting_face_tone4 { background-position: -700px -400px; }
-.emoji-person_with_pouting_face_tone5 { background-position: -700px -420px; }
-.emoji-pick { background-position: -700px -440px; }
-.emoji-pig { background-position: -700px -460px; }
-.emoji-pig2 { background-position: -700px -480px; }
-.emoji-pig_nose { background-position: -700px -500px; }
-.emoji-pill { background-position: -700px -520px; }
-.emoji-pineapple { background-position: -700px -540px; }
-.emoji-ping_pong { background-position: -700px -560px; }
-.emoji-pisces { background-position: -700px -580px; }
-.emoji-pizza { background-position: -700px -600px; }
-.emoji-place_of_worship { background-position: -700px -620px; }
-.emoji-play_pause { background-position: -700px -640px; }
-.emoji-point_down { background-position: -700px -660px; }
-.emoji-point_down_tone1 { background-position: -700px -680px; }
-.emoji-point_down_tone2 { background-position: 0 -700px; }
-.emoji-point_down_tone3 { background-position: -20px -700px; }
-.emoji-point_down_tone4 { background-position: -40px -700px; }
-.emoji-point_down_tone5 { background-position: -60px -700px; }
-.emoji-point_left { background-position: -80px -700px; }
-.emoji-point_left_tone1 { background-position: -100px -700px; }
-.emoji-point_left_tone2 { background-position: -120px -700px; }
-.emoji-point_left_tone3 { background-position: -140px -700px; }
-.emoji-point_left_tone4 { background-position: -160px -700px; }
-.emoji-point_left_tone5 { background-position: -180px -700px; }
-.emoji-point_right { background-position: -200px -700px; }
-.emoji-point_right_tone1 { background-position: -220px -700px; }
-.emoji-point_right_tone2 { background-position: -240px -700px; }
-.emoji-point_right_tone3 { background-position: -260px -700px; }
-.emoji-point_right_tone4 { background-position: -280px -700px; }
-.emoji-point_right_tone5 { background-position: -300px -700px; }
-.emoji-point_up { background-position: -320px -700px; }
-.emoji-point_up_2 { background-position: -340px -700px; }
-.emoji-point_up_2_tone1 { background-position: -360px -700px; }
-.emoji-point_up_2_tone2 { background-position: -380px -700px; }
-.emoji-point_up_2_tone3 { background-position: -400px -700px; }
-.emoji-point_up_2_tone4 { background-position: -420px -700px; }
-.emoji-point_up_2_tone5 { background-position: -440px -700px; }
-.emoji-point_up_tone1 { background-position: -460px -700px; }
-.emoji-point_up_tone2 { background-position: -480px -700px; }
-.emoji-point_up_tone3 { background-position: -500px -700px; }
-.emoji-point_up_tone4 { background-position: -520px -700px; }
-.emoji-point_up_tone5 { background-position: -540px -700px; }
-.emoji-police_car { background-position: -560px -700px; }
-.emoji-poodle { background-position: -580px -700px; }
-.emoji-poop { background-position: -600px -700px; }
-.emoji-popcorn { background-position: -620px -700px; }
-.emoji-post_office { background-position: -640px -700px; }
-.emoji-postal_horn { background-position: -660px -700px; }
-.emoji-postbox { background-position: -680px -700px; }
-.emoji-potable_water { background-position: -700px -700px; }
-.emoji-potato { background-position: -720px 0; }
-.emoji-pouch { background-position: -720px -20px; }
-.emoji-poultry_leg { background-position: -720px -40px; }
-.emoji-pound { background-position: -720px -60px; }
-.emoji-pouting_cat { background-position: -720px -80px; }
-.emoji-pray { background-position: -720px -100px; }
-.emoji-pray_tone1 { background-position: -720px -120px; }
-.emoji-pray_tone2 { background-position: -720px -140px; }
-.emoji-pray_tone3 { background-position: -720px -160px; }
-.emoji-pray_tone4 { background-position: -720px -180px; }
-.emoji-pray_tone5 { background-position: -720px -200px; }
-.emoji-prayer_beads { background-position: -720px -220px; }
-.emoji-pregnant_woman { background-position: -720px -240px; }
-.emoji-pregnant_woman_tone1 { background-position: -720px -260px; }
-.emoji-pregnant_woman_tone2 { background-position: -720px -280px; }
-.emoji-pregnant_woman_tone3 { background-position: -720px -300px; }
-.emoji-pregnant_woman_tone4 { background-position: -720px -320px; }
-.emoji-pregnant_woman_tone5 { background-position: -720px -340px; }
-.emoji-prince { background-position: -720px -360px; }
-.emoji-prince_tone1 { background-position: -720px -380px; }
-.emoji-prince_tone2 { background-position: -720px -400px; }
-.emoji-prince_tone3 { background-position: -720px -420px; }
-.emoji-prince_tone4 { background-position: -720px -440px; }
-.emoji-prince_tone5 { background-position: -720px -460px; }
-.emoji-princess { background-position: -720px -480px; }
-.emoji-princess_tone1 { background-position: -720px -500px; }
-.emoji-princess_tone2 { background-position: -720px -520px; }
-.emoji-princess_tone3 { background-position: -720px -540px; }
-.emoji-princess_tone4 { background-position: -720px -560px; }
-.emoji-princess_tone5 { background-position: -720px -580px; }
-.emoji-printer { background-position: -720px -600px; }
-.emoji-projector { background-position: -720px -620px; }
-.emoji-punch { background-position: -720px -640px; }
-.emoji-punch_tone1 { background-position: -720px -660px; }
-.emoji-punch_tone2 { background-position: -720px -680px; }
-.emoji-punch_tone3 { background-position: -720px -700px; }
-.emoji-punch_tone4 { background-position: 0 -720px; }
-.emoji-punch_tone5 { background-position: -20px -720px; }
-.emoji-purple_heart { background-position: -40px -720px; }
-.emoji-purse { background-position: -60px -720px; }
-.emoji-pushpin { background-position: -80px -720px; }
-.emoji-put_litter_in_its_place { background-position: -100px -720px; }
-.emoji-question { background-position: -120px -720px; }
-.emoji-rabbit { background-position: -140px -720px; }
-.emoji-rabbit2 { background-position: -160px -720px; }
-.emoji-race_car { background-position: -180px -720px; }
-.emoji-racehorse { background-position: -200px -720px; }
-.emoji-radio { background-position: -220px -720px; }
-.emoji-radio_button { background-position: -240px -720px; }
-.emoji-radioactive { background-position: -260px -720px; }
-.emoji-rage { background-position: -280px -720px; }
-.emoji-railway_car { background-position: -300px -720px; }
-.emoji-railway_track { background-position: -320px -720px; }
-.emoji-rainbow { background-position: -340px -720px; }
-.emoji-raised_back_of_hand { background-position: -360px -720px; }
-.emoji-raised_back_of_hand_tone1 { background-position: -380px -720px; }
-.emoji-raised_back_of_hand_tone2 { background-position: -400px -720px; }
-.emoji-raised_back_of_hand_tone3 { background-position: -420px -720px; }
-.emoji-raised_back_of_hand_tone4 { background-position: -440px -720px; }
-.emoji-raised_back_of_hand_tone5 { background-position: -460px -720px; }
-.emoji-raised_hand { background-position: -480px -720px; }
-.emoji-raised_hand_tone1 { background-position: -500px -720px; }
-.emoji-raised_hand_tone2 { background-position: -520px -720px; }
-.emoji-raised_hand_tone3 { background-position: -540px -720px; }
-.emoji-raised_hand_tone4 { background-position: -560px -720px; }
-.emoji-raised_hand_tone5 { background-position: -580px -720px; }
-.emoji-raised_hands { background-position: -600px -720px; }
-.emoji-raised_hands_tone1 { background-position: -620px -720px; }
-.emoji-raised_hands_tone2 { background-position: -640px -720px; }
-.emoji-raised_hands_tone3 { background-position: -660px -720px; }
-.emoji-raised_hands_tone4 { background-position: -680px -720px; }
-.emoji-raised_hands_tone5 { background-position: -700px -720px; }
-.emoji-raising_hand { background-position: -720px -720px; }
-.emoji-raising_hand_tone1 { background-position: -740px 0; }
-.emoji-raising_hand_tone2 { background-position: -740px -20px; }
-.emoji-raising_hand_tone3 { background-position: -740px -40px; }
-.emoji-raising_hand_tone4 { background-position: -740px -60px; }
-.emoji-raising_hand_tone5 { background-position: -740px -80px; }
-.emoji-ram { background-position: -740px -100px; }
-.emoji-ramen { background-position: -740px -120px; }
-.emoji-rat { background-position: -740px -140px; }
-.emoji-record_button { background-position: -740px -160px; }
-.emoji-recycle { background-position: -740px -180px; }
-.emoji-red_car { background-position: -740px -200px; }
-.emoji-red_circle { background-position: -740px -220px; }
-.emoji-registered { background-position: -740px -240px; }
-.emoji-relaxed { background-position: -740px -260px; }
-.emoji-relieved { background-position: -740px -280px; }
-.emoji-reminder_ribbon { background-position: -740px -300px; }
-.emoji-repeat { background-position: -740px -320px; }
-.emoji-repeat_one { background-position: -740px -340px; }
-.emoji-restroom { background-position: -740px -360px; }
-.emoji-revolving_hearts { background-position: -740px -380px; }
-.emoji-rewind { background-position: -740px -400px; }
-.emoji-rhino { background-position: -740px -420px; }
-.emoji-ribbon { background-position: -740px -440px; }
-.emoji-rice { background-position: -740px -460px; }
-.emoji-rice_ball { background-position: -740px -480px; }
-.emoji-rice_cracker { background-position: -740px -500px; }
-.emoji-rice_scene { background-position: -740px -520px; }
-.emoji-right_facing_fist { background-position: -740px -540px; }
-.emoji-right_facing_fist_tone1 { background-position: -740px -560px; }
-.emoji-right_facing_fist_tone2 { background-position: -740px -580px; }
-.emoji-right_facing_fist_tone3 { background-position: -740px -600px; }
-.emoji-right_facing_fist_tone4 { background-position: -740px -620px; }
-.emoji-right_facing_fist_tone5 { background-position: -740px -640px; }
-.emoji-ring { background-position: -740px -660px; }
-.emoji-robot { background-position: -740px -680px; }
-.emoji-rocket { background-position: -740px -700px; }
-.emoji-rofl { background-position: -740px -720px; }
-.emoji-roller_coaster { background-position: 0 -740px; }
-.emoji-rolling_eyes { background-position: -20px -740px; }
-.emoji-rooster { background-position: -40px -740px; }
-.emoji-rose { background-position: -60px -740px; }
-.emoji-rosette { background-position: -80px -740px; }
-.emoji-rotating_light { background-position: -100px -740px; }
-.emoji-round_pushpin { background-position: -120px -740px; }
-.emoji-rowboat { background-position: -140px -740px; }
-.emoji-rowboat_tone1 { background-position: -160px -740px; }
-.emoji-rowboat_tone2 { background-position: -180px -740px; }
-.emoji-rowboat_tone3 { background-position: -200px -740px; }
-.emoji-rowboat_tone4 { background-position: -220px -740px; }
-.emoji-rowboat_tone5 { background-position: -240px -740px; }
-.emoji-rugby_football { background-position: -260px -740px; }
-.emoji-runner { background-position: -280px -740px; }
-.emoji-runner_tone1 { background-position: -300px -740px; }
-.emoji-runner_tone2 { background-position: -320px -740px; }
-.emoji-runner_tone3 { background-position: -340px -740px; }
-.emoji-runner_tone4 { background-position: -360px -740px; }
-.emoji-runner_tone5 { background-position: -380px -740px; }
-.emoji-running_shirt_with_sash { background-position: -400px -740px; }
-.emoji-sa { background-position: -420px -740px; }
-.emoji-sagittarius { background-position: -440px -740px; }
-.emoji-sailboat { background-position: -460px -740px; }
-.emoji-sake { background-position: -480px -740px; }
-.emoji-salad { background-position: -500px -740px; }
-.emoji-sandal { background-position: -520px -740px; }
-.emoji-santa { background-position: -540px -740px; }
-.emoji-santa_tone1 { background-position: -560px -740px; }
-.emoji-santa_tone2 { background-position: -580px -740px; }
-.emoji-santa_tone3 { background-position: -600px -740px; }
-.emoji-santa_tone4 { background-position: -620px -740px; }
-.emoji-santa_tone5 { background-position: -640px -740px; }
-.emoji-satellite { background-position: -660px -740px; }
-.emoji-satellite_orbital { background-position: -680px -740px; }
-.emoji-saxophone { background-position: -700px -740px; }
-.emoji-scales { background-position: -720px -740px; }
-.emoji-school { background-position: -740px -740px; }
-.emoji-school_satchel { background-position: -760px 0; }
-.emoji-scissors { background-position: -760px -20px; }
-.emoji-scooter { background-position: -760px -40px; }
-.emoji-scorpion { background-position: -760px -60px; }
-.emoji-scorpius { background-position: -760px -80px; }
-.emoji-scream { background-position: -760px -100px; }
-.emoji-scream_cat { background-position: -760px -120px; }
-.emoji-scroll { background-position: -760px -140px; }
-.emoji-seat { background-position: -760px -160px; }
-.emoji-second_place { background-position: -760px -180px; }
-.emoji-secret { background-position: -760px -200px; }
-.emoji-see_no_evil { background-position: -760px -220px; }
-.emoji-seedling { background-position: -760px -240px; }
-.emoji-selfie { background-position: -760px -260px; }
-.emoji-selfie_tone1 { background-position: -760px -280px; }
-.emoji-selfie_tone2 { background-position: -760px -300px; }
-.emoji-selfie_tone3 { background-position: -760px -320px; }
-.emoji-selfie_tone4 { background-position: -760px -340px; }
-.emoji-selfie_tone5 { background-position: -760px -360px; }
-.emoji-seven { background-position: -760px -380px; }
-.emoji-shallow_pan_of_food { background-position: -760px -400px; }
-.emoji-shamrock { background-position: -760px -420px; }
-.emoji-shark { background-position: -760px -440px; }
-.emoji-shaved_ice { background-position: -760px -460px; }
-.emoji-sheep { background-position: -760px -480px; }
-.emoji-shell { background-position: -760px -500px; }
-.emoji-shield { background-position: -760px -520px; }
-.emoji-shinto_shrine { background-position: -760px -540px; }
-.emoji-ship { background-position: -760px -560px; }
-.emoji-shirt { background-position: -760px -580px; }
-.emoji-shopping_bags { background-position: -760px -600px; }
-.emoji-shopping_cart { background-position: -760px -620px; }
-.emoji-shower { background-position: -760px -640px; }
-.emoji-shrimp { background-position: -760px -660px; }
-.emoji-shrug { background-position: -760px -680px; }
-.emoji-shrug_tone1 { background-position: -760px -700px; }
-.emoji-shrug_tone2 { background-position: -760px -720px; }
-.emoji-shrug_tone3 { background-position: -760px -740px; }
-.emoji-shrug_tone4 { background-position: 0 -760px; }
-.emoji-shrug_tone5 { background-position: -20px -760px; }
-.emoji-signal_strength { background-position: -40px -760px; }
-.emoji-six { background-position: -60px -760px; }
-.emoji-six_pointed_star { background-position: -80px -760px; }
-.emoji-ski { background-position: -100px -760px; }
-.emoji-skier { background-position: -120px -760px; }
-.emoji-skull { background-position: -140px -760px; }
-.emoji-skull_crossbones { background-position: -160px -760px; }
-.emoji-sleeping { background-position: -180px -760px; }
-.emoji-sleeping_accommodation { background-position: -200px -760px; }
-.emoji-sleepy { background-position: -220px -760px; }
-.emoji-slight_frown { background-position: -240px -760px; }
-.emoji-slight_smile { background-position: -260px -760px; }
-.emoji-slot_machine { background-position: -280px -760px; }
-.emoji-small_blue_diamond { background-position: -300px -760px; }
-.emoji-small_orange_diamond { background-position: -320px -760px; }
-.emoji-small_red_triangle { background-position: -340px -760px; }
-.emoji-small_red_triangle_down { background-position: -360px -760px; }
-.emoji-smile { background-position: -380px -760px; }
-.emoji-smile_cat { background-position: -400px -760px; }
-.emoji-smiley { background-position: -420px -760px; }
-.emoji-smiley_cat { background-position: -440px -760px; }
-.emoji-smiling_imp { background-position: -460px -760px; }
-.emoji-smirk { background-position: -480px -760px; }
-.emoji-smirk_cat { background-position: -500px -760px; }
-.emoji-smoking { background-position: -520px -760px; }
-.emoji-snail { background-position: -540px -760px; }
-.emoji-snake { background-position: -560px -760px; }
-.emoji-sneezing_face { background-position: -580px -760px; }
-.emoji-snowboarder { background-position: -600px -760px; }
-.emoji-snowflake { background-position: -620px -760px; }
-.emoji-snowman { background-position: -640px -760px; }
-.emoji-snowman2 { background-position: -660px -760px; }
-.emoji-sob { background-position: -680px -760px; }
-.emoji-soccer { background-position: -700px -760px; }
-.emoji-soon { background-position: -720px -760px; }
-.emoji-sos { background-position: -740px -760px; }
-.emoji-sound { background-position: -760px -760px; }
-.emoji-space_invader { background-position: -780px 0; }
-.emoji-spades { background-position: -780px -20px; }
-.emoji-spaghetti { background-position: -780px -40px; }
-.emoji-sparkle { background-position: -780px -60px; }
-.emoji-sparkler { background-position: -780px -80px; }
-.emoji-sparkles { background-position: -780px -100px; }
-.emoji-sparkling_heart { background-position: -780px -120px; }
-.emoji-speak_no_evil { background-position: -780px -140px; }
-.emoji-speaker { background-position: -780px -160px; }
-.emoji-speaking_head { background-position: -780px -180px; }
-.emoji-speech_balloon { background-position: -780px -200px; }
-.emoji-speech_left { background-position: -780px -220px; }
-.emoji-speedboat { background-position: -780px -240px; }
-.emoji-spider { background-position: -780px -260px; }
-.emoji-spider_web { background-position: -780px -280px; }
-.emoji-spoon { background-position: -780px -300px; }
-.emoji-spy { background-position: -780px -320px; }
-.emoji-spy_tone1 { background-position: -780px -340px; }
-.emoji-spy_tone2 { background-position: -780px -360px; }
-.emoji-spy_tone3 { background-position: -780px -380px; }
-.emoji-spy_tone4 { background-position: -780px -400px; }
-.emoji-spy_tone5 { background-position: -780px -420px; }
-.emoji-squid { background-position: -780px -440px; }
-.emoji-stadium { background-position: -780px -460px; }
-.emoji-star { background-position: -780px -480px; }
-.emoji-star2 { background-position: -780px -500px; }
-.emoji-star_and_crescent { background-position: -780px -520px; }
-.emoji-star_of_david { background-position: -780px -540px; }
-.emoji-stars { background-position: -780px -560px; }
-.emoji-station { background-position: -780px -580px; }
-.emoji-statue_of_liberty { background-position: -780px -600px; }
-.emoji-steam_locomotive { background-position: -780px -620px; }
-.emoji-stew { background-position: -780px -640px; }
-.emoji-stop_button { background-position: -780px -660px; }
-.emoji-stopwatch { background-position: -780px -680px; }
-.emoji-straight_ruler { background-position: -780px -700px; }
-.emoji-strawberry { background-position: -780px -720px; }
-.emoji-stuck_out_tongue { background-position: -780px -740px; }
-.emoji-stuck_out_tongue_closed_eyes { background-position: -780px -760px; }
-.emoji-stuck_out_tongue_winking_eye { background-position: 0 -780px; }
-.emoji-stuffed_flatbread { background-position: -20px -780px; }
-.emoji-sun_with_face { background-position: -40px -780px; }
-.emoji-sunflower { background-position: -60px -780px; }
-.emoji-sunglasses { background-position: -80px -780px; }
-.emoji-sunny { background-position: -100px -780px; }
-.emoji-sunrise { background-position: -120px -780px; }
-.emoji-sunrise_over_mountains { background-position: -140px -780px; }
-.emoji-surfer { background-position: -160px -780px; }
-.emoji-surfer_tone1 { background-position: -180px -780px; }
-.emoji-surfer_tone2 { background-position: -200px -780px; }
-.emoji-surfer_tone3 { background-position: -220px -780px; }
-.emoji-surfer_tone4 { background-position: -240px -780px; }
-.emoji-surfer_tone5 { background-position: -260px -780px; }
-.emoji-sushi { background-position: -280px -780px; }
-.emoji-suspension_railway { background-position: -300px -780px; }
-.emoji-sweat { background-position: -320px -780px; }
-.emoji-sweat_drops { background-position: -340px -780px; }
-.emoji-sweat_smile { background-position: -360px -780px; }
-.emoji-sweet_potato { background-position: -380px -780px; }
-.emoji-swimmer { background-position: -400px -780px; }
-.emoji-swimmer_tone1 { background-position: -420px -780px; }
-.emoji-swimmer_tone2 { background-position: -440px -780px; }
-.emoji-swimmer_tone3 { background-position: -460px -780px; }
-.emoji-swimmer_tone4 { background-position: -480px -780px; }
-.emoji-swimmer_tone5 { background-position: -500px -780px; }
-.emoji-symbols { background-position: -520px -780px; }
-.emoji-synagogue { background-position: -540px -780px; }
-.emoji-syringe { background-position: -560px -780px; }
-.emoji-taco { background-position: -580px -780px; }
-.emoji-tada { background-position: -600px -780px; }
-.emoji-tanabata_tree { background-position: -620px -780px; }
-.emoji-tangerine { background-position: -640px -780px; }
-.emoji-taurus { background-position: -660px -780px; }
-.emoji-taxi { background-position: -680px -780px; }
-.emoji-tea { background-position: -700px -780px; }
-.emoji-telephone { background-position: -720px -780px; }
-.emoji-telephone_receiver { background-position: -740px -780px; }
-.emoji-telescope { background-position: -760px -780px; }
-.emoji-ten { background-position: -780px -780px; }
-.emoji-tennis { background-position: -800px 0; }
-.emoji-tent { background-position: -800px -20px; }
-.emoji-thermometer { background-position: -800px -40px; }
-.emoji-thermometer_face { background-position: -800px -60px; }
-.emoji-thinking { background-position: -800px -80px; }
-.emoji-third_place { background-position: -800px -100px; }
-.emoji-thought_balloon { background-position: -800px -120px; }
-.emoji-three { background-position: -800px -140px; }
-.emoji-thumbsdown { background-position: -800px -160px; }
-.emoji-thumbsdown_tone1 { background-position: -800px -180px; }
-.emoji-thumbsdown_tone2 { background-position: -800px -200px; }
-.emoji-thumbsdown_tone3 { background-position: -800px -220px; }
-.emoji-thumbsdown_tone4 { background-position: -800px -240px; }
-.emoji-thumbsdown_tone5 { background-position: -800px -260px; }
-.emoji-thumbsup { background-position: -800px -280px; }
-.emoji-thumbsup_tone1 { background-position: -800px -300px; }
-.emoji-thumbsup_tone2 { background-position: -800px -320px; }
-.emoji-thumbsup_tone3 { background-position: -800px -340px; }
-.emoji-thumbsup_tone4 { background-position: -800px -360px; }
-.emoji-thumbsup_tone5 { background-position: -800px -380px; }
-.emoji-thunder_cloud_rain { background-position: -800px -400px; }
-.emoji-ticket { background-position: -800px -420px; }
-.emoji-tickets { background-position: -800px -440px; }
-.emoji-tiger { background-position: -800px -460px; }
-.emoji-tiger2 { background-position: -800px -480px; }
-.emoji-timer { background-position: -800px -500px; }
-.emoji-tired_face { background-position: -800px -520px; }
-.emoji-tm { background-position: -800px -540px; }
-.emoji-toilet { background-position: -800px -560px; }
-.emoji-tokyo_tower { background-position: -800px -580px; }
-.emoji-tomato { background-position: -800px -600px; }
-.emoji-tone1 { background-position: -800px -620px; }
-.emoji-tone2 { background-position: -800px -640px; }
-.emoji-tone3 { background-position: -800px -660px; }
-.emoji-tone4 { background-position: -800px -680px; }
-.emoji-tone5 { background-position: -800px -700px; }
-.emoji-tongue { background-position: -800px -720px; }
-.emoji-tools { background-position: -800px -740px; }
-.emoji-top { background-position: -800px -760px; }
-.emoji-tophat { background-position: -800px -780px; }
-.emoji-track_next { background-position: 0 -800px; }
-.emoji-track_previous { background-position: -20px -800px; }
-.emoji-trackball { background-position: -40px -800px; }
-.emoji-tractor { background-position: -60px -800px; }
-.emoji-traffic_light { background-position: -80px -800px; }
-.emoji-train { background-position: -100px -800px; }
-.emoji-train2 { background-position: -120px -800px; }
-.emoji-tram { background-position: -140px -800px; }
-.emoji-triangular_flag_on_post { background-position: -160px -800px; }
-.emoji-triangular_ruler { background-position: -180px -800px; }
-.emoji-trident { background-position: -200px -800px; }
-.emoji-triumph { background-position: -220px -800px; }
-.emoji-trolleybus { background-position: -240px -800px; }
-.emoji-trophy { background-position: -260px -800px; }
-.emoji-tropical_drink { background-position: -280px -800px; }
-.emoji-tropical_fish { background-position: -300px -800px; }
-.emoji-truck { background-position: -320px -800px; }
-.emoji-trumpet { background-position: -340px -800px; }
-.emoji-tulip { background-position: -360px -800px; }
-.emoji-tumbler_glass { background-position: -380px -800px; }
-.emoji-turkey { background-position: -400px -800px; }
-.emoji-turtle { background-position: -420px -800px; }
-.emoji-tv { background-position: -440px -800px; }
-.emoji-twisted_rightwards_arrows { background-position: -460px -800px; }
-.emoji-two { background-position: -480px -800px; }
-.emoji-two_hearts { background-position: -500px -800px; }
-.emoji-two_men_holding_hands { background-position: -520px -800px; }
-.emoji-two_women_holding_hands { background-position: -540px -800px; }
-.emoji-u5272 { background-position: -560px -800px; }
-.emoji-u5408 { background-position: -580px -800px; }
-.emoji-u55b6 { background-position: -600px -800px; }
-.emoji-u6307 { background-position: -620px -800px; }
-.emoji-u6708 { background-position: -640px -800px; }
-.emoji-u6709 { background-position: -660px -800px; }
-.emoji-u6e80 { background-position: -680px -800px; }
-.emoji-u7121 { background-position: -700px -800px; }
-.emoji-u7533 { background-position: -720px -800px; }
-.emoji-u7981 { background-position: -740px -800px; }
-.emoji-u7a7a { background-position: -760px -800px; }
-.emoji-umbrella { background-position: -780px -800px; }
-.emoji-umbrella2 { background-position: -800px -800px; }
-.emoji-unamused { background-position: -820px 0; }
-.emoji-underage { background-position: -820px -20px; }
-.emoji-unicorn { background-position: -820px -40px; }
-.emoji-unlock { background-position: -820px -60px; }
-.emoji-up { background-position: -820px -80px; }
-.emoji-upside_down { background-position: -820px -100px; }
-.emoji-urn { background-position: -820px -120px; }
-.emoji-v { background-position: -820px -140px; }
-.emoji-v_tone1 { background-position: -820px -160px; }
-.emoji-v_tone2 { background-position: -820px -180px; }
-.emoji-v_tone3 { background-position: -820px -200px; }
-.emoji-v_tone4 { background-position: -820px -220px; }
-.emoji-v_tone5 { background-position: -820px -240px; }
-.emoji-vertical_traffic_light { background-position: -820px -260px; }
-.emoji-vhs { background-position: -820px -280px; }
-.emoji-vibration_mode { background-position: -820px -300px; }
-.emoji-video_camera { background-position: -820px -320px; }
-.emoji-video_game { background-position: -820px -340px; }
-.emoji-violin { background-position: -820px -360px; }
-.emoji-virgo { background-position: -820px -380px; }
-.emoji-volcano { background-position: -820px -400px; }
-.emoji-volleyball { background-position: -820px -420px; }
-.emoji-vs { background-position: -820px -440px; }
-.emoji-vulcan { background-position: -820px -460px; }
-.emoji-vulcan_tone1 { background-position: -820px -480px; }
-.emoji-vulcan_tone2 { background-position: -820px -500px; }
-.emoji-vulcan_tone3 { background-position: -820px -520px; }
-.emoji-vulcan_tone4 { background-position: -820px -540px; }
-.emoji-vulcan_tone5 { background-position: -820px -560px; }
-.emoji-walking { background-position: -820px -580px; }
-.emoji-walking_tone1 { background-position: -820px -600px; }
-.emoji-walking_tone2 { background-position: -820px -620px; }
-.emoji-walking_tone3 { background-position: -820px -640px; }
-.emoji-walking_tone4 { background-position: -820px -660px; }
-.emoji-walking_tone5 { background-position: -820px -680px; }
-.emoji-waning_crescent_moon { background-position: -820px -700px; }
-.emoji-waning_gibbous_moon { background-position: -820px -720px; }
-.emoji-warning { background-position: -820px -740px; }
-.emoji-wastebasket { background-position: -820px -760px; }
-.emoji-watch { background-position: -820px -780px; }
-.emoji-water_buffalo { background-position: -820px -800px; }
-.emoji-water_polo { background-position: 0 -820px; }
-.emoji-water_polo_tone1 { background-position: -20px -820px; }
-.emoji-water_polo_tone2 { background-position: -40px -820px; }
-.emoji-water_polo_tone3 { background-position: -60px -820px; }
-.emoji-water_polo_tone4 { background-position: -80px -820px; }
-.emoji-water_polo_tone5 { background-position: -100px -820px; }
-.emoji-watermelon { background-position: -120px -820px; }
-.emoji-wave { background-position: -140px -820px; }
-.emoji-wave_tone1 { background-position: -160px -820px; }
-.emoji-wave_tone2 { background-position: -180px -820px; }
-.emoji-wave_tone3 { background-position: -200px -820px; }
-.emoji-wave_tone4 { background-position: -220px -820px; }
-.emoji-wave_tone5 { background-position: -240px -820px; }
-.emoji-wavy_dash { background-position: -260px -820px; }
-.emoji-waxing_crescent_moon { background-position: -280px -820px; }
-.emoji-waxing_gibbous_moon { background-position: -300px -820px; }
-.emoji-wc { background-position: -320px -820px; }
-.emoji-weary { background-position: -340px -820px; }
-.emoji-wedding { background-position: -360px -820px; }
-.emoji-whale { background-position: -380px -820px; }
-.emoji-whale2 { background-position: -400px -820px; }
-.emoji-wheel_of_dharma { background-position: -420px -820px; }
-.emoji-wheelchair { background-position: -440px -820px; }
-.emoji-white_check_mark { background-position: -460px -820px; }
-.emoji-white_circle { background-position: -480px -820px; }
-.emoji-white_flower { background-position: -500px -820px; }
-.emoji-white_large_square { background-position: -520px -820px; }
-.emoji-white_medium_small_square { background-position: -540px -820px; }
-.emoji-white_medium_square { background-position: -560px -820px; }
-.emoji-white_small_square { background-position: -580px -820px; }
-.emoji-white_square_button { background-position: -600px -820px; }
-.emoji-white_sun_cloud { background-position: -620px -820px; }
-.emoji-white_sun_rain_cloud { background-position: -640px -820px; }
-.emoji-white_sun_small_cloud { background-position: -660px -820px; }
-.emoji-wilted_rose { background-position: -680px -820px; }
-.emoji-wind_blowing_face { background-position: -700px -820px; }
-.emoji-wind_chime { background-position: -720px -820px; }
-.emoji-wine_glass { background-position: -740px -820px; }
-.emoji-wink { background-position: -760px -820px; }
-.emoji-wolf { background-position: -780px -820px; }
-.emoji-woman { background-position: -800px -820px; }
-.emoji-woman_tone1 { background-position: -820px -820px; }
-.emoji-woman_tone2 { background-position: -840px 0; }
-.emoji-woman_tone3 { background-position: -840px -20px; }
-.emoji-woman_tone4 { background-position: -840px -40px; }
-.emoji-woman_tone5 { background-position: -840px -60px; }
-.emoji-womans_clothes { background-position: -840px -80px; }
-.emoji-womans_hat { background-position: -840px -100px; }
-.emoji-womens { background-position: -840px -120px; }
-.emoji-worried { background-position: -840px -140px; }
-.emoji-wrench { background-position: -840px -160px; }
-.emoji-wrestlers { background-position: -840px -180px; }
-.emoji-wrestlers_tone1 { background-position: -840px -200px; }
-.emoji-wrestlers_tone2 { background-position: -840px -220px; }
-.emoji-wrestlers_tone3 { background-position: -840px -240px; }
-.emoji-wrestlers_tone4 { background-position: -840px -260px; }
-.emoji-wrestlers_tone5 { background-position: -840px -280px; }
-.emoji-writing_hand { background-position: -840px -300px; }
-.emoji-writing_hand_tone1 { background-position: -840px -320px; }
-.emoji-writing_hand_tone2 { background-position: -840px -340px; }
-.emoji-writing_hand_tone3 { background-position: -840px -360px; }
-.emoji-writing_hand_tone4 { background-position: -840px -380px; }
-.emoji-writing_hand_tone5 { background-position: -840px -400px; }
-.emoji-x { background-position: -840px -420px; }
-.emoji-yellow_heart { background-position: -840px -440px; }
-.emoji-yen { background-position: -840px -460px; }
-.emoji-yin_yang { background-position: -840px -480px; }
-.emoji-yum { background-position: -840px -500px; }
-.emoji-zap { background-position: -840px -520px; }
-.emoji-zero { background-position: -840px -540px; }
-.emoji-zipper_mouth { background-position: -840px -560px; }
-.emoji-100 { background-position: -840px -580px; }
-
-.emoji-icon {
- background-image: image-url('emoji.png');
- background-repeat: no-repeat;
- color: transparent;
- text-indent: -99em;
- height: 20px;
- width: 20px;
-
- @media only screen and (-webkit-min-device-pixel-ratio: 2),
- only screen and (min--moz-device-pixel-ratio: 2),
- only screen and (-o-min-device-pixel-ratio: 2/1),
- only screen and (min-device-pixel-ratio: 2),
- only screen and (min-resolution: 192dpi),
- only screen and (min-resolution: 2dppx) {
- background-image: image-url('emoji@2x.png');
- background-size: 860px 840px;
- }
-}
diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss
index 62a0fba3da3..ab3cceceae9 100644
--- a/app/assets/stylesheets/framework/images.scss
+++ b/app/assets/stylesheets/framework/images.scss
@@ -39,35 +39,10 @@
svg {
fill: currentColor;
- &.s8 {
- @include svg-size(8px);
- }
-
- &.s12 {
- @include svg-size(12px);
- }
-
- &.s16 {
- @include svg-size(16px);
- }
-
- &.s18 {
- @include svg-size(18px);
- }
-
- &.s24 {
- @include svg-size(24px);
- }
-
- &.s32 {
- @include svg-size(32px);
- }
-
- &.s48 {
- @include svg-size(48px);
- }
-
- &.s72 {
- @include svg-size(72px);
+ $svg-sizes: 8 12 16 18 24 32 48 72;
+ @each $svg-size in $svg-sizes {
+ &.s#{$svg-size} {
+ @include svg-size(#{$svg-size}px);
+ }
}
}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 938f5f49c09..7b5d1c2cf8b 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -107,6 +107,16 @@
padding-top: 10px;
}
+.referenced-commands {
+ background: $blue-50;
+ padding: $gl-padding-8 $gl-padding;
+ border-radius: $border-radius-default;
+
+ p {
+ margin: 0;
+ }
+}
+
.md-preview-holder {
min-height: 167px;
padding: 10px 0;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 8c44ebc85ef..3d28df455bb 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -212,6 +212,7 @@ $tooltip-font-size: 12px;
/*
* Padding
*/
+$gl-padding-24: 24px;
$gl-padding: 16px;
$gl-padding-8: 8px;
$gl-padding-4: 4px;
diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss
index 2f3a80daa90..3fa7a260017 100644
--- a/app/assets/stylesheets/framework/wells.scss
+++ b/app/assets/stylesheets/framework/wells.scss
@@ -19,6 +19,7 @@
.fork-svg {
margin-right: 4px;
+ vertical-align: bottom;
}
}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index e9384d41e00..1aca3c5cf1a 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -70,7 +70,7 @@
}
.branch-info .commit-icon {
- margin-right: 3px;
+ margin-right: 8px;
svg {
top: 3px;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 81e98f358a8..6d5c6cb136f 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -772,7 +772,3 @@ ul.notes {
height: auto;
}
}
-
-.line-resolve-text {
- vertical-align: middle;
-}
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 6d77260b658..6342042374f 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -55,6 +55,7 @@
white-space: nowrap;
text-overflow: ellipsis;
max-width: inherit;
+ line-height: 22px;
svg {
vertical-align: middle;
@@ -67,6 +68,11 @@
}
}
+ .ide-file-icon-holder {
+ display: flex;
+ align-items: center;
+ }
+
.ide-file-changed-icon {
margin-left: auto;
@@ -77,7 +83,6 @@
.ide-new-btn {
display: none;
- margin-bottom: -4px;
margin-right: -8px;
}
@@ -90,10 +95,8 @@
}
}
- &.folder {
- svg {
- fill: $gl-text-color-secondary;
- }
+ .folder-icon {
+ fill: $gl-text-color-secondary;
}
}
@@ -111,6 +114,7 @@
.file-col-commit-message {
display: flex;
overflow: visible;
+ align-items: center;
padding: 6px 12px;
}
@@ -438,7 +442,7 @@
.projects-sidebar {
display: flex;
flex-direction: column;
- height: 100%;
+ flex: 1;
.context-header {
width: auto;
@@ -967,3 +971,7 @@
background: transparent;
resize: none;
}
+
+.ide-new-modal-label {
+ line-height: 34px;
+}
diff --git a/app/assets/stylesheets/pages/repo.scss.orig b/app/assets/stylesheets/pages/repo.scss.orig
deleted file mode 100644
index 57b995adb64..00000000000
--- a/app/assets/stylesheets/pages/repo.scss.orig
+++ /dev/null
@@ -1,786 +0,0 @@
-.project-refs-form,
-.project-refs-target-form {
- display: inline-block;
-}
-
-.fade-enter,
-.fade-leave-to {
- opacity: 0;
-}
-
-.commit-message {
- @include str-truncated(250px);
-}
-
-.editable-mode {
- display: inline-block;
-}
-
-.ide-view {
- display: flex;
- height: calc(100vh - #{$header-height});
- margin-top: 40px;
- color: $almost-black;
- border-top: 1px solid $white-dark;
- border-bottom: 1px solid $white-dark;
-
- &.is-collapsed {
- .ide-file-list {
- max-width: 250px;
- }
- }
-
- .file-status-icon {
- width: 10px;
- height: 10px;
- }
-}
-
-.ide-file-list {
- flex: 1;
-
- .file {
- cursor: pointer;
-
- &.file-open {
- background: $white-normal;
- }
-
- .ide-file-name {
- flex: 1;
- white-space: nowrap;
- text-overflow: ellipsis;
-
- svg {
- vertical-align: middle;
- margin-right: 2px;
- }
-
- .loading-container {
- margin-right: 4px;
- display: inline-block;
- }
- }
-
- .ide-file-changed-icon {
- margin-left: auto;
- }
-
- .ide-new-btn {
- display: none;
- margin-bottom: -4px;
- margin-right: -8px;
- }
-
- &:hover {
- .ide-new-btn {
- display: block;
- }
- }
-
- &.folder {
- svg {
- fill: $gl-text-color-secondary;
- }
- }
- }
-
- a {
- color: $gl-text-color;
- }
-
- th {
- position: sticky;
- top: 0;
- }
-}
-
-.file-name,
-.file-col-commit-message {
- display: flex;
- overflow: visible;
- padding: 6px 12px;
-}
-
-.multi-file-loading-container {
- margin-top: 10px;
- padding: 10px;
-
- .animation-container {
- background: $gray-light;
-
- div {
- background: $gray-light;
- }
- }
-}
-
-.multi-file-table-col-commit-message {
- white-space: nowrap;
- width: 50%;
-}
-
-.multi-file-edit-pane {
- display: flex;
- flex-direction: column;
- flex: 1;
- border-left: 1px solid $white-dark;
- overflow: hidden;
-}
-
-.multi-file-tabs {
- display: flex;
- background-color: $white-normal;
- box-shadow: inset 0 -1px $white-dark;
-
- > ul {
- display: flex;
- overflow-x: auto;
- }
-
- li {
- position: relative;
- }
-
- .dropdown {
- display: flex;
- margin-left: auto;
- margin-bottom: 1px;
- padding: 0 $grid-size;
- border-left: 1px solid $white-dark;
- background-color: $white-light;
-
- &.shadow {
- box-shadow: 0 0 10px $dropdown-shadow-color;
- }
-
- .btn {
- margin-top: auto;
- margin-bottom: auto;
- }
- }
-}
-
-.multi-file-tab {
- @include str-truncated(150px);
- padding: ($gl-padding / 2) ($gl-padding + 12) ($gl-padding / 2) $gl-padding;
- background-color: $gray-normal;
- border-right: 1px solid $white-dark;
- border-bottom: 1px solid $white-dark;
- cursor: pointer;
-
- svg {
- vertical-align: middle;
- }
-
- &.active {
- background-color: $white-light;
- border-bottom-color: $white-light;
- }
-}
-
-.multi-file-tab-close {
- position: absolute;
- right: 8px;
- top: 50%;
- width: 16px;
- height: 16px;
- padding: 0;
- background: none;
- border: 0;
- border-radius: $border-radius-default;
- color: $theme-gray-900;
- transform: translateY(-50%);
-
- svg {
- position: relative;
- top: -1px;
- }
-
- &:hover {
- background-color: $theme-gray-200;
- }
-
- &:focus {
- background-color: $blue-500;
- color: $white-light;
- outline: 0;
-
- svg {
- fill: currentColor;
- }
- }
-}
-
-.multi-file-edit-pane-content {
- flex: 1;
- height: 0;
-}
-
-.blob-editor-container {
- flex: 1;
- height: 0;
- display: flex;
- flex-direction: column;
- justify-content: center;
-
- .vertical-center {
- min-height: auto;
- }
-
- .monaco-editor .lines-content .cigr {
- display: none;
- }
-
- .monaco-diff-editor.vs {
- .editor.modified {
- box-shadow: none;
- }
-
- .diagonal-fill {
- display: none !important;
- }
-
- .diffOverview {
- background-color: $white-light;
- border-left: 1px solid $white-dark;
- cursor: ns-resize;
- }
-
- .diffViewport {
- display: none;
- }
-
- .char-insert {
- background-color: $line-added-dark;
- }
-
- .char-delete {
- background-color: $line-removed-dark;
- }
-
- .line-numbers {
- color: $black-transparent;
- }
-
- .view-overlays {
- .line-insert {
- background-color: $line-added;
- }
-
- .line-delete {
- background-color: $line-removed;
- }
- }
-
- .margin {
- background-color: $gray-light;
- border-right: 1px solid $white-normal;
-
- .line-insert {
- border-right: 1px solid $line-added-dark;
- }
-
- .line-delete {
- border-right: 1px solid $line-removed-dark;
- }
- }
-
- .margin-view-overlays .insert-sign,
- .margin-view-overlays .delete-sign {
- opacity: 0.4;
- }
-
- .cursors-layer {
- display: none;
- }
- }
-}
-
-.multi-file-editor-holder {
- height: 100%;
-}
-
-.multi-file-editor-btn-group {
- padding: $gl-bar-padding $gl-padding;
- border-top: 1px solid $white-dark;
- border-bottom: 1px solid $white-dark;
- background: $white-light;
-}
-
-.ide-status-bar {
- padding: $gl-bar-padding $gl-padding;
- background: $white-light;
- display: flex;
- justify-content: space-between;
-
- svg {
- vertical-align: middle;
- }
-}
-
-// Not great, but this is to deal with our current output
-.multi-file-preview-holder {
- height: 100%;
- overflow: scroll;
-
- .file-content.code {
- display: flex;
-
- i {
- margin-left: -10px;
- }
- }
-
- .line-numbers {
- min-width: 50px;
- }
-
- .file-content,
- .line-numbers,
- .blob-content,
- .code {
- min-height: 100%;
- }
-}
-
-.file-content.blob-no-preview {
- a {
- margin-left: auto;
- margin-right: auto;
- }
-}
-
-.multi-file-commit-panel {
- display: flex;
- position: relative;
- flex-direction: column;
- width: 340px;
- padding: 0;
- background-color: $gray-light;
- padding-right: 3px;
-
- .projects-sidebar {
- display: flex;
- flex-direction: column;
-
- .context-header {
- width: auto;
- margin-right: 0;
- }
- }
-
- .multi-file-commit-panel-inner {
- display: flex;
- flex: 1;
- flex-direction: column;
- }
-
- .multi-file-commit-panel-inner-scroll {
- display: flex;
- flex: 1;
- flex-direction: column;
- overflow: auto;
- }
-
- &.is-collapsed {
- width: 60px;
-
- .multi-file-commit-list {
- padding-top: $gl-padding;
- overflow: hidden;
- }
-
- .multi-file-context-bar-icon {
- align-items: center;
-
- svg {
- float: none;
- margin: 0;
- }
- }
- }
-
- .branch-container {
- border-left: 4px solid $indigo-700;
- margin-bottom: $gl-bar-padding;
- }
-
- .branch-header {
- background: $white-dark;
- display: flex;
- }
-
- .branch-header-title {
- flex: 1;
- padding: $grid-size $gl-padding;
- color: $indigo-700;
- font-weight: $gl-font-weight-bold;
-
- svg {
- vertical-align: middle;
- }
- }
-
- .branch-header-btns {
- padding: $gl-vert-padding $gl-padding;
- }
-
- .left-collapse-btn {
- display: none;
- background: $gray-light;
- text-align: left;
- border-top: 1px solid $white-dark;
-
- svg {
- vertical-align: middle;
- }
- }
-}
-
-.multi-file-context-bar-icon {
- padding: 10px;
-
- svg {
- margin-right: 10px;
- float: left;
- }
-}
-
-.multi-file-commit-panel-section {
- display: flex;
- flex-direction: column;
- flex: 1;
-}
-
-.multi-file-commit-empty-state-container {
- align-items: center;
- justify-content: center;
-}
-
-.multi-file-commit-panel-header {
- display: flex;
- align-items: center;
- margin-bottom: 12px;
- border-bottom: 1px solid $white-dark;
- padding: $gl-btn-padding 0;
-
- &.is-collapsed {
- border-bottom: 1px solid $white-dark;
-
- svg {
- margin-left: auto;
- margin-right: auto;
- }
-
- .multi-file-commit-panel-collapse-btn {
- margin-right: auto;
- margin-left: auto;
- border-left: 0;
- }
- }
-}
-
-.multi-file-commit-panel-header-title {
- display: flex;
- flex: 1;
- padding: 0 $gl-btn-padding;
-
- svg {
- margin-right: $gl-btn-padding;
- }
-}
-
-.multi-file-commit-panel-collapse-btn {
- border-left: 1px solid $white-dark;
-}
-
-.multi-file-commit-list {
- flex: 1;
- overflow: auto;
- padding: $gl-padding 0;
- min-height: 60px;
-}
-
-.multi-file-commit-list-item {
- display: flex;
- padding: 0;
- align-items: center;
-
- .multi-file-discard-btn {
- display: none;
- margin-left: auto;
- color: $gl-link-color;
- padding: 0 2px;
-
- &:focus,
- &:hover {
- text-decoration: underline;
- }
- }
-
- &:hover {
- background: $white-normal;
-
- .multi-file-discard-btn {
- display: block;
- }
- }
-}
-
-.multi-file-addition {
- fill: $green-500;
-}
-
-.multi-file-modified {
- fill: $orange-500;
-}
-
-.multi-file-commit-list-collapsed {
- display: flex;
- flex-direction: column;
-
- > svg {
- margin-left: auto;
- margin-right: auto;
- }
-
- .file-status-icon {
- width: 10px;
- height: 10px;
- margin-left: 3px;
- }
-}
-
-.multi-file-commit-list-path {
- padding: $grid-size / 2;
- padding-left: $gl-padding;
- background: none;
- border: 0;
- text-align: left;
- width: 100%;
- min-width: 0;
-
- svg {
- min-width: 16px;
- vertical-align: middle;
- display: inline-block;
- }
-
- &:hover,
- &:focus {
- outline: 0;
- }
-}
-
-.multi-file-commit-list-file-path {
- @include str-truncated(100%);
-
- &:hover {
- text-decoration: underline;
- }
-
- &:active {
- text-decoration: none;
- }
-}
-
-.multi-file-commit-form {
- padding: $gl-padding;
- border-top: 1px solid $white-dark;
-
- .btn {
- font-size: $gl-font-size;
- }
-}
-
-.multi-file-commit-message.form-control {
- height: 160px;
- resize: none;
-}
-
-.dirty-diff {
- // !important need to override monaco inline style
- width: 4px !important;
- left: 0 !important;
-
- &-modified {
- background-color: $blue-500;
- }
-
- &-added {
- background-color: $green-600;
- }
-
- &-removed {
- height: 0 !important;
- width: 0 !important;
- bottom: -2px;
- border-style: solid;
- border-width: 5px;
- border-color: transparent transparent transparent $red-500;
-
- &::before {
- content: '';
- position: absolute;
- left: 0;
- top: 0;
- width: 100px;
- height: 1px;
- background-color: rgba($red-500, 0.5);
- }
- }
-}
-
-.ide-loading {
- display: flex;
- height: 100vh;
- align-items: center;
- justify-content: center;
-}
-
-.ide-empty-state {
- display: flex;
- height: 100vh;
- align-items: center;
- justify-content: center;
-}
-
-.ide-new-btn {
- .dropdown-toggle svg {
- margin-top: -2px;
- margin-bottom: 2px;
- }
-
- .dropdown-menu {
- left: auto;
- right: 0;
-
- label {
- font-weight: $gl-font-weight-normal;
- padding: 5px 8px;
- margin-bottom: 0;
- }
- }
-}
-
-.ide {
- overflow: hidden;
-
- &.nav-only {
- .flash-container {
- margin-top: $header-height;
- margin-bottom: 0;
- }
-
- .alert-wrapper .flash-container .flash-alert:last-child,
- .alert-wrapper .flash-container .flash-notice:last-child {
- margin-bottom: 0;
- }
-
- .content-wrapper {
- margin-top: $header-height;
- padding-bottom: 0;
- }
-
- &.flash-shown {
- .content-wrapper {
- margin-top: 0;
- }
-
- .ide-view {
- height: calc(100vh - #{$header-height + $flash-height});
- }
- }
-
- .projects-sidebar {
- .multi-file-commit-panel-inner-scroll {
- flex: 1;
- }
- }
- }
-}
-
-.with-performance-bar .ide.nav-only {
- .flash-container {
- margin-top: #{$header-height + $performance-bar-height};
- }
-
- .content-wrapper {
- margin-top: #{$header-height + $performance-bar-height};
- padding-bottom: 0;
- }
-
- .ide-view {
- height: calc(100vh - #{$header-height + $performance-bar-height});
- }
-
- &.flash-shown {
- .content-wrapper {
- margin-top: 0;
- }
-
- .ide-view {
- height: calc(
- 100vh - #{$header-height + $performance-bar-height + $flash-height}
- );
- }
- }
-}
-
-.dragHandle {
- position: absolute;
- top: 0;
- bottom: 0;
- width: 3px;
- background-color: $white-dark;
-
- &.dragright {
- right: 0;
- }
-
- &.dragleft {
- left: 0;
- }
-}
-
-.ide-commit-radios {
- label {
- font-weight: normal;
- }
-
- .help-block {
- margin-top: 0;
- line-height: 0;
- }
-}
-
-.ide-commit-new-branch {
- margin-left: 25px;
-}
-
-.ide-external-links {
- p {
- margin: 0;
- }
-}
-
-.ide-sidebar-link {
- padding: $gl-padding-8 $gl-padding;
- background: $indigo-700;
- color: $white-light;
- text-decoration: none;
- display: flex;
- align-items: center;
-
- &:focus,
- &:hover {
- color: $white-light;
- text-decoration: underline;
- background: $indigo-500;
- }
-
- &:active {
- background: $indigo-800;
- }
-}
diff --git a/app/assets/stylesheets/performance_bar.scss b/app/assets/stylesheets/performance_bar.scss
index 45ae94abaff..06ef58531d7 100644
--- a/app/assets/stylesheets/performance_bar.scss
+++ b/app/assets/stylesheets/performance_bar.scss
@@ -1,5 +1,4 @@
@import 'framework/variables';
-@import 'peek/views/performance_bar';
@import 'peek/views/rblineprof';
#js-peek {
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 0fdd4d2cb47..8ad13a82f89 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -110,7 +110,8 @@ class ApplicationController < ActionController::Base
def log_exception(exception)
Raven.capture_exception(exception) if sentry_enabled?
- application_trace = ActionDispatch::ExceptionWrapper.new(env, exception).application_trace
+ backtrace_cleaner = Gitlab.rails5? ? env["action_dispatch.backtrace_cleaner"] : env
+ application_trace = ActionDispatch::ExceptionWrapper.new(backtrace_cleaner, exception).application_trace
application_trace.map! { |t| " #{t}\n" }
logger.error "\n#{exception.class.name} (#{exception.message}):\n#{application_trace.join}"
end
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 34228cf0b82..ca1b80a36a0 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -57,7 +57,7 @@ module IssuableCollections
out_of_range = @issuables.current_page > total_pages # rubocop:disable Gitlab/ModuleWithInstanceVariables
if out_of_range
- redirect_to(url_for(params.merge(page: total_pages, only_path: true)))
+ redirect_to(url_for(safe_params.merge(page: total_pages, only_path: true)))
end
out_of_range
diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb
index 9f3bb60b4cc..62213561898 100644
--- a/app/controllers/groups/application_controller.rb
+++ b/app/controllers/groups/application_controller.rb
@@ -33,6 +33,6 @@ class Groups::ApplicationController < ApplicationController
def build_canonical_path(group)
params[:group_id] = group.to_param
- url_for(params)
+ url_for(safe_params)
end
end
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index 9137bc92810..40d9fa18a10 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -8,8 +8,8 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
omniauth_flow(Gitlab::Auth::OAuth)
end
- Gitlab.config.omniauth.providers.each do |provider|
- alias_method provider['name'], :handle_omniauth
+ AuthHelper.providers_for_base_controller.each do |provider|
+ alias_method provider, :handle_omniauth
end
# Extend the standard implementation to also increment
diff --git a/app/controllers/profiles/active_sessions_controller.rb b/app/controllers/profiles/active_sessions_controller.rb
new file mode 100644
index 00000000000..f0cdc228366
--- /dev/null
+++ b/app/controllers/profiles/active_sessions_controller.rb
@@ -0,0 +1,14 @@
+class Profiles::ActiveSessionsController < Profiles::ApplicationController
+ def index
+ @sessions = ActiveSession.list(current_user)
+ end
+
+ def destroy
+ ActiveSession.destroy(current_user, params[:id])
+
+ respond_to do |format|
+ format.html { redirect_to profile_active_sessions_url, status: 302 }
+ format.js { head :ok }
+ end
+ end
+end
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index 032bb2267e7..5ab6d103c89 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -25,7 +25,7 @@ class Projects::ApplicationController < ApplicationController
params[:namespace_id] = project.namespace.to_param
params[:project_id] = project.to_param
- url_for(params)
+ url_for(safe_params)
end
def repository
diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb
index ebde0df1f7b..43d8867a536 100644
--- a/app/controllers/projects/lfs_storage_controller.rb
+++ b/app/controllers/projects/lfs_storage_controller.rb
@@ -77,8 +77,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
def link_to_project!(object)
if object && !object.projects.exists?(storage_project.id)
- object.projects << storage_project
- object.save!
+ object.lfs_objects_projects.create!(project: storage_project)
end
end
end
diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb
index 4a377fefc62..81129456ad8 100644
--- a/app/controllers/projects/merge_requests/creations_controller.rb
+++ b/app/controllers/projects/merge_requests/creations_controller.rb
@@ -83,13 +83,6 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
render layout: false
end
- def update_branches
- @target_project = selected_target_project
- @target_branches = @target_project ? @target_project.repository.branch_names : []
-
- render layout: false
- end
-
private
def build_merge_request
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 86c50d88a2a..bc13b8ad7ba 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -33,9 +33,7 @@ class Projects::NotesController < Projects::ApplicationController
def resolve
return render_404 unless note.resolvable?
- note.resolve!(current_user)
-
- MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(note.noteable)
+ Notes::ResolveService.new(project, current_user).execute(note)
discussion = note.discussion
diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb
index 0282b378d88..0754123a3cf 100644
--- a/app/finders/groups_finder.rb
+++ b/app/finders/groups_finder.rb
@@ -39,7 +39,7 @@ class GroupsFinder < UnionFinder
def all_groups
return [owned_groups] if params[:owned]
- return [Group.all] if current_user&.full_private_access?
+ return [Group.all] if current_user&.full_private_access? && all_available?
groups = []
groups << Gitlab::GroupHierarchy.new(groups_for_ancestors, groups_for_descendants).all_groups if current_user
@@ -67,6 +67,10 @@ class GroupsFinder < UnionFinder
end
def include_public_groups?
- current_user.nil? || params.fetch(:all_available, true)
+ current_user.nil? || all_available?
+ end
+
+ def all_available?
+ params.fetch(:all_available, true)
end
end
diff --git a/app/finders/pipelines_finder.rb b/app/finders/pipelines_finder.rb
index f187a3b61fe..0a487839aff 100644
--- a/app/finders/pipelines_finder.rb
+++ b/app/finders/pipelines_finder.rb
@@ -14,6 +14,7 @@ class PipelinesFinder
items = by_scope(items)
items = by_status(items)
items = by_ref(items)
+ items = by_sha(items)
items = by_name(items)
items = by_username(items)
items = by_yaml_errors(items)
@@ -69,6 +70,14 @@ class PipelinesFinder
end
end
+ def by_sha(items)
+ if params[:sha].present?
+ items.where(sha: params[:sha])
+ else
+ items
+ end
+ end
+
def by_name(items)
if params[:name].present?
items.joins(:user).where(users: { name: params[:name] })
diff --git a/app/helpers/active_sessions_helper.rb b/app/helpers/active_sessions_helper.rb
new file mode 100644
index 00000000000..97b6dac67c5
--- /dev/null
+++ b/app/helpers/active_sessions_helper.rb
@@ -0,0 +1,23 @@
+module ActiveSessionsHelper
+ # Maps a device type as defined in `ActiveSession` to an svg icon name and
+ # outputs the icon html.
+ #
+ # see `DeviceDetector::Device::DEVICE_NAMES` about the available device types
+ def active_session_device_type_icon(active_session)
+ icon_name =
+ case active_session.device_type
+ when 'smartphone', 'feature phone', 'phablet'
+ 'mobile'
+ when 'tablet'
+ 'tablet'
+ when 'tv', 'smart display', 'camera', 'portable media player', 'console'
+ 'media'
+ when 'car browser'
+ 'car'
+ else
+ 'monitor-o'
+ end
+
+ sprite_icon(icon_name, size: 16, css_class: 'prepend-top-2')
+ end
+end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 228c8d2e8f9..6aa307b4db4 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -32,80 +32,6 @@ module ApplicationHelper
args.any? { |v| v.to_s.downcase == action_name }
end
- def project_icon(project_id, options = {})
- project =
- if project_id.respond_to?(:avatar_url)
- project_id
- else
- Project.find_by_full_path(project_id)
- end
-
- if project.avatar_url
- image_tag project.avatar_url, options
- else # generated icon
- project_identicon(project, options)
- end
- end
-
- def project_identicon(project, options = {})
- allowed_colors = {
- red: 'FFEBEE',
- purple: 'F3E5F5',
- indigo: 'E8EAF6',
- blue: 'E3F2FD',
- teal: 'E0F2F1',
- orange: 'FBE9E7',
- gray: 'EEEEEE'
- }
-
- options[:class] ||= ''
- options[:class] << ' identicon'
- bg_key = project.id % 7
- style = "background-color: ##{allowed_colors.values[bg_key]}; color: #555"
-
- content_tag(:div, class: options[:class], style: style) do
- project.name[0, 1].upcase
- end
- end
-
- # Takes both user and email and returns the avatar_icon by
- # user (preferred) or email.
- def avatar_icon_for(user = nil, email = nil, size = nil, scale = 2, only_path: true)
- if user
- avatar_icon_for_user(user, size, scale, only_path: only_path)
- elsif email
- avatar_icon_for_email(email, size, scale, only_path: only_path)
- else
- default_avatar
- end
- end
-
- def avatar_icon_for_email(email = nil, size = nil, scale = 2, only_path: true)
- user = User.find_by_any_email(email.try(:downcase))
- if user
- avatar_icon_for_user(user, size, scale, only_path: only_path)
- else
- gravatar_icon(email, size, scale)
- end
- end
-
- def avatar_icon_for_user(user = nil, size = nil, scale = 2, only_path: true)
- if user
- user.avatar_url(size: size, only_path: only_path) || default_avatar
- else
- gravatar_icon(nil, size, scale)
- end
- end
-
- def gravatar_icon(user_email = '', size = nil, scale = 2)
- GravatarService.new.execute(user_email, size, scale) ||
- default_avatar
- end
-
- def default_avatar
- asset_path('no_avatar.png')
- end
-
def last_commit(project)
if project.repo_exists?
time_ago_with_tooltip(project.repository.commit.committed_date)
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index c109954f3a3..d2daee22aba 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -1,6 +1,6 @@
module AuthHelper
PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq).freeze
- FORM_BASED_PROVIDERS = [/\Aldap/, 'crowd'].freeze
+ LDAP_PROVIDER = /\Aldap/
def ldap_enabled?
Gitlab::Auth::LDAP::Config.enabled?
@@ -23,7 +23,7 @@ module AuthHelper
end
def form_based_provider?(name)
- FORM_BASED_PROVIDERS.any? { |pattern| pattern === name.to_s }
+ [LDAP_PROVIDER, 'crowd'].any? { |pattern| pattern === name.to_s }
end
def form_based_providers
@@ -38,6 +38,10 @@ module AuthHelper
auth_providers.reject { |provider| form_based_provider?(provider) }
end
+ def providers_for_base_controller
+ auth_providers.reject { |provider| LDAP_PROVIDER === provider }
+ end
+
def enabled_button_based_providers
disabled_providers = Gitlab::CurrentSettings.disabled_oauth_sign_in_sources || []
diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb
index 21b6c0a8ad5..d339c01d492 100644
--- a/app/helpers/avatars_helper.rb
+++ b/app/helpers/avatars_helper.rb
@@ -1,4 +1,78 @@
module AvatarsHelper
+ def project_icon(project_id, options = {})
+ project =
+ if project_id.respond_to?(:avatar_url)
+ project_id
+ else
+ Project.find_by_full_path(project_id)
+ end
+
+ if project.avatar_url
+ image_tag project.avatar_url, options
+ else # generated icon
+ project_identicon(project, options)
+ end
+ end
+
+ def project_identicon(project, options = {})
+ allowed_colors = {
+ red: 'FFEBEE',
+ purple: 'F3E5F5',
+ indigo: 'E8EAF6',
+ blue: 'E3F2FD',
+ teal: 'E0F2F1',
+ orange: 'FBE9E7',
+ gray: 'EEEEEE'
+ }
+
+ options[:class] ||= ''
+ options[:class] << ' identicon'
+ bg_key = project.id % 7
+ style = "background-color: ##{allowed_colors.values[bg_key]}; color: #555"
+
+ content_tag(:div, class: options[:class], style: style) do
+ project.name[0, 1].upcase
+ end
+ end
+
+ # Takes both user and email and returns the avatar_icon by
+ # user (preferred) or email.
+ def avatar_icon_for(user = nil, email = nil, size = nil, scale = 2, only_path: true)
+ if user
+ avatar_icon_for_user(user, size, scale, only_path: only_path)
+ elsif email
+ avatar_icon_for_email(email, size, scale, only_path: only_path)
+ else
+ default_avatar
+ end
+ end
+
+ def avatar_icon_for_email(email = nil, size = nil, scale = 2, only_path: true)
+ user = User.find_by_any_email(email.try(:downcase))
+ if user
+ avatar_icon_for_user(user, size, scale, only_path: only_path)
+ else
+ gravatar_icon(email, size, scale)
+ end
+ end
+
+ def avatar_icon_for_user(user = nil, size = nil, scale = 2, only_path: true)
+ if user
+ user.avatar_url(size: size, only_path: only_path) || default_avatar
+ else
+ gravatar_icon(nil, size, scale)
+ end
+ end
+
+ def gravatar_icon(user_email = '', size = nil, scale = 2)
+ GravatarService.new.execute(user_email, size, scale) ||
+ default_avatar
+ end
+
+ def default_avatar
+ ActionController::Base.helpers.image_path('no_avatar.png')
+ end
+
def author_avatar(commit_or_event, options = {})
user_avatar(options.merge({
user: commit_or_event.author,
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index fef29789832..e7a36e20050 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -17,7 +17,7 @@ module BlobHelper
end
def ide_edit_path(project = @project, ref = @ref, path = @path, options = {})
- "#{ide_path}/project#{edit_blob_path(project, ref, path, options)}"
+ "#{ide_path}/project#{url_for([project, "edit", "blob", id: [ref, path], script_name: "/"])}"
end
def edit_blob_button(project = @project, ref = @ref, path = @path, options = {})
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 98894b86551..e594a1d0ba3 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -63,7 +63,7 @@ module CommitsHelper
# Returns a link formatted as a commit branch link
def commit_branch_link(url, text)
link_to(url, class: 'label label-gray ref-name branch-link') do
- sprite_icon('fork', size: 16, css_class: 'fork-svg') + "#{text}"
+ sprite_icon('fork', size: 12, css_class: 'fork-svg') + "#{text}"
end
end
diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb
index 5089da519df..5a2360b4661 100644
--- a/app/helpers/dropdowns_helper.rb
+++ b/app/helpers/dropdowns_helper.rb
@@ -41,7 +41,7 @@ module DropdownsHelper
def dropdown_toggle(toggle_text, data_attr, options = {})
default_label = data_attr[:default_label]
- content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.key?(:toggle_class)}", id: (options[:id] if options.key?(:id)), type: "button", data: data_attr) do
+ content_tag(:button, disabled: options[:disabled], class: "dropdown-menu-toggle #{options[:toggle_class] if options.key?(:toggle_class)}", id: (options[:id] if options.key?(:id)), type: "button", data: data_attr) do
output = content_tag(:span, toggle_text, class: "dropdown-toggle-text #{'is-default' if toggle_text == default_label}")
output << icon('chevron-down')
output.html_safe
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 801e624e1de..eb81dc2de43 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -442,7 +442,7 @@ module ProjectsHelper
visibilityHelpPath: help_page_path('public_access/public_access'),
registryAvailable: Gitlab.config.registry.enabled,
registryHelpPath: help_page_path('user/project/container_registry'),
- lfsAvailable: Gitlab.config.lfs.enabled && current_user.admin?,
+ lfsAvailable: Gitlab.config.lfs.enabled,
lfsHelpPath: help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
}
diff --git a/app/helpers/system_note_helper.rb b/app/helpers/system_note_helper.rb
index 00fe67d6ffb..5b4a141dbcf 100644
--- a/app/helpers/system_note_helper.rb
+++ b/app/helpers/system_note_helper.rb
@@ -1,14 +1,14 @@
module SystemNoteHelper
ICON_NAMES_BY_ACTION = {
'commit' => 'commit',
- 'description' => 'pencil',
+ 'description' => 'pencil-square',
'merge' => 'git-merge',
'merged' => 'git-merge',
'opened' => 'issue-open',
'closed' => 'issue-close',
'time_tracking' => 'timer',
'assignee' => 'user',
- 'title' => 'pencil',
+ 'title' => 'pencil-square',
'task' => 'task-done',
'label' => 'label',
'cross_reference' => 'comment-dots',
@@ -18,7 +18,7 @@ module SystemNoteHelper
'milestone' => 'clock',
'discussion' => 'comment',
'moved' => 'arrow-right',
- 'outdated' => 'pencil',
+ 'outdated' => 'pencil-square',
'duplicate' => 'issue-duplicate',
'locked' => 'lock',
'unlocked' => 'lock-open'
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index e4212775956..3646e08a15f 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -16,6 +16,7 @@ class Notify < BaseMailer
helper BlobHelper
helper EmailsHelper
helper MembersHelper
+ helper AvatarsHelper
helper GitlabRoutingHelper
def test_email(recipient_email, subject, body)
diff --git a/app/models/active_session.rb b/app/models/active_session.rb
new file mode 100644
index 00000000000..b4a86dbb331
--- /dev/null
+++ b/app/models/active_session.rb
@@ -0,0 +1,110 @@
+class ActiveSession
+ include ActiveModel::Model
+
+ attr_accessor :created_at, :updated_at,
+ :session_id, :ip_address,
+ :browser, :os, :device_name, :device_type
+
+ def current?(session)
+ return false if session_id.nil? || session.id.nil?
+
+ session_id == session.id
+ end
+
+ def human_device_type
+ device_type&.titleize
+ end
+
+ def self.set(user, request)
+ Gitlab::Redis::SharedState.with do |redis|
+ session_id = request.session.id
+ client = DeviceDetector.new(request.user_agent)
+ timestamp = Time.current
+
+ active_user_session = new(
+ ip_address: request.ip,
+ browser: client.name,
+ os: client.os_name,
+ device_name: client.device_name,
+ device_type: client.device_type,
+ created_at: user.current_sign_in_at || timestamp,
+ updated_at: timestamp,
+ session_id: session_id
+ )
+
+ redis.pipelined do
+ redis.setex(
+ key_name(user.id, session_id),
+ Settings.gitlab['session_expire_delay'] * 60,
+ Marshal.dump(active_user_session)
+ )
+
+ redis.sadd(
+ lookup_key_name(user.id),
+ session_id
+ )
+ end
+ end
+ end
+
+ def self.list(user)
+ Gitlab::Redis::SharedState.with do |redis|
+ cleaned_up_lookup_entries(redis, user.id).map do |entry|
+ # rubocop:disable Security/MarshalLoad
+ Marshal.load(entry)
+ # rubocop:enable Security/MarshalLoad
+ end
+ end
+ end
+
+ def self.destroy(user, session_id)
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.srem(lookup_key_name(user.id), session_id)
+
+ deleted_keys = redis.del(key_name(user.id, session_id))
+
+ # only allow deleting the devise session if we could actually find a
+ # related active session. this prevents another user from deleting
+ # someone else's session.
+ if deleted_keys > 0
+ redis.del("#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id}")
+ end
+ end
+ end
+
+ def self.cleanup(user)
+ Gitlab::Redis::SharedState.with do |redis|
+ cleaned_up_lookup_entries(redis, user.id)
+ end
+ end
+
+ def self.key_name(user_id, session_id = '*')
+ "#{Gitlab::Redis::SharedState::USER_SESSIONS_NAMESPACE}:#{user_id}:#{session_id}"
+ end
+
+ def self.lookup_key_name(user_id)
+ "#{Gitlab::Redis::SharedState::USER_SESSIONS_LOOKUP_NAMESPACE}:#{user_id}"
+ end
+
+ def self.cleaned_up_lookup_entries(redis, user_id)
+ lookup_key = lookup_key_name(user_id)
+
+ session_ids = redis.smembers(lookup_key)
+
+ entry_keys = session_ids.map { |session_id| key_name(user_id, session_id) }
+ return [] if entry_keys.empty?
+
+ entries = redis.mget(entry_keys)
+
+ session_ids_and_entries = session_ids.zip(entries)
+
+ # remove expired keys.
+ # only the single key entries are automatically expired by redis, the
+ # lookup entries in the set need to be removed manually.
+ session_ids_and_entries.reject { |_session_id, entry| entry }.each do |session_id, _entry|
+ redis.srem(lookup_key, session_id)
+ end
+
+ session_ids_and_entries.select { |_session_id, entry| entry }.map { |_session_id, entry| entry }
+ end
+end
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 39676efa08c..3b952391b7e 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -13,7 +13,7 @@ module Ci
after_save :update_project_statistics_after_save, if: :size_changed?
after_destroy :update_project_statistics_after_destroy, unless: :project_destroyed?
- after_save :update_file_store
+ after_save :update_file_store, if: :file_changed?
scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) }
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 434b9b64c65..e1b9bc76475 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -530,6 +530,17 @@ module Ci
@latest_builds_with_artifacts ||= builds.latest.with_artifacts_archive.to_a
end
+ # Rails 5.0 autogenerated question mark enum methods return wrong result if enum value is nil.
+ # They always return `false`.
+ # These methods overwrite autogenerated ones to return correct results.
+ def unknown?
+ Gitlab.rails5? ? source.nil? : super
+ end
+
+ def unknown_source?
+ Gitlab.rails5? ? config_source.nil? : super
+ end
+
private
def ci_yaml_from_repo
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index 75b8ea2a371..5a1eeb966aa 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -13,14 +13,27 @@ module Ci
has_many :statuses, class_name: 'CommitStatus', foreign_key: :stage_id
has_many :builds, foreign_key: :stage_id
- validates :project, presence: true, unless: :importing?
- validates :pipeline, presence: true, unless: :importing?
- validates :name, presence: true, unless: :importing?
+ with_options unless: :importing? do
+ validates :project, presence: true
+ validates :pipeline, presence: true
+ validates :name, presence: true
+ validates :position, presence: true
+ end
- after_initialize do |stage|
+ after_initialize do
self.status = DEFAULT_STATUS if self.status.nil?
end
+ before_validation unless: :importing? do
+ next if position.present?
+
+ self.position = statuses.select(:stage_idx)
+ .where('stage_idx IS NOT NULL')
+ .group(:stage_idx)
+ .order('COUNT(*) DESC')
+ .first&.stage_idx.to_i
+ end
+
state_machine :status, initial: :created do
event :enqueue do
transition created: :pending
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 9750e9298ec..b46f9f34689 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -105,6 +105,10 @@ class Commit
end
end
end
+
+ def parent_class
+ ::Project
+ end
end
attr_accessor :raw
@@ -420,6 +424,12 @@ class Commit
# no-op but needs to be defined since #persisted? is defined
end
+ def touch_later
+ # No-op.
+ # This method is called by ActiveRecord.
+ # We don't want to do anything for `Commit` model, so this is empty.
+ end
+
WIP_REGEX = /\A\s*(((?i)(\[WIP\]|WIP:|WIP)\s|WIP$))|(fixup!|squash!)\s/.freeze
def work_in_progress?
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index b6276c2fb50..97d89422594 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -189,4 +189,11 @@ class CommitStatus < ActiveRecord::Base
v =~ /\d+/ ? v.to_i : v
end
end
+
+ # Rails 5.0 autogenerated question mark enum methods return wrong result if enum value is nil.
+ # They always return `false`.
+ # This method overwrites the autogenerated one to return correct result.
+ def unknown_failure?
+ Gitlab.rails5? ? failure_reason.nil? : super
+ end
end
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index 15122cbc693..616a626419b 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -54,7 +54,20 @@ class DiffNote < Note
end
def diff_file
- @diff_file ||= self.original_position.diff_file(self.project.repository)
+ @diff_file ||=
+ begin
+ if created_at_diff?(noteable.diff_refs)
+ # We're able to use the already persisted diffs (Postgres) if we're
+ # presenting a "current version" of the MR discussion diff.
+ # So no need to make an extra Gitaly diff request for it.
+ # As an extra benefit, the returned `diff_file` already
+ # has `highlighted_diff_lines` data set from Redis on
+ # `Diff::FileCollection::MergeRequestDiff`.
+ noteable.diffs(paths: original_position.paths, expanded: true).diff_files.first
+ else
+ original_position.diff_file(self.project.repository)
+ end
+ end
end
def diff_line
diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb
index 6b7f280fb70..84487031ee5 100644
--- a/app/models/lfs_object.rb
+++ b/app/models/lfs_object.rb
@@ -11,7 +11,7 @@ class LfsObject < ActiveRecord::Base
mount_uploader :file, LfsObjectUploader
- after_save :update_file_store
+ after_save :update_file_store, if: :file_changed?
def update_file_store
# The file.object_store is set during `uploader.store!`
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index 661e668dbf9..5da739f9618 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -37,20 +37,20 @@ class GroupMember < Member
private
def send_invite
- notification_service.invite_group_member(self, @raw_invite_token)
+ run_after_commit_or_now { notification_service.invite_group_member(self, @raw_invite_token) }
super
end
def post_create_hook
- notification_service.new_group_member(self)
+ run_after_commit_or_now { notification_service.new_group_member(self) }
super
end
def post_update_hook
if access_level_changed?
- notification_service.update_group_member(self)
+ run_after_commit { notification_service.update_group_member(self) }
end
super
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 1c7ed4a96df..024106056b4 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -92,7 +92,7 @@ class ProjectMember < Member
private
def send_invite
- notification_service.invite_project_member(self, @raw_invite_token)
+ run_after_commit_or_now { notification_service.invite_project_member(self, @raw_invite_token) }
super
end
@@ -100,7 +100,7 @@ class ProjectMember < Member
def post_create_hook
unless owner?
event_service.join_project(self.project, self.user)
- notification_service.new_project_member(self)
+ run_after_commit_or_now { notification_service.new_project_member(self) }
end
super
@@ -108,7 +108,7 @@ class ProjectMember < Member
def post_update_hook
if access_level_changed?
- notification_service.update_project_member(self)
+ run_after_commit { notification_service.update_project_member(self) }
end
super
diff --git a/app/models/user.rb b/app/models/user.rb
index b0668148972..4a602ffbb05 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -910,7 +910,7 @@ class User < ActiveRecord::Base
def delete_async(deleted_by:, params: {})
block if params[:hard_delete]
- DeleteUserWorker.perform_async(deleted_by.id, id, params)
+ DeleteUserWorker.perform_async(deleted_by.id, id, params.to_h)
end
def notification_service
diff --git a/app/services/ci/ensure_stage_service.rb b/app/services/ci/ensure_stage_service.rb
index 87f19b333de..b8c7be2d350 100644
--- a/app/services/ci/ensure_stage_service.rb
+++ b/app/services/ci/ensure_stage_service.rb
@@ -42,6 +42,7 @@ module Ci
def create_stage
Ci::Stage.create!(name: @build.stage,
+ position: @build.stage_idx,
pipeline: @build.pipeline,
project: @build.project)
end
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index fee5bc38f7b..4a99367c575 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -26,7 +26,7 @@ module Issues
issue.update(closed_by: current_user)
event_service.close_issue(issue, current_user)
create_note(issue, commit) if system_note
- notification_service.close_issue(issue, current_user) if notifications
+ notification_service.async.close_issue(issue, current_user) if notifications
todo_service.close_issue(issue, current_user)
execute_hooks(issue, 'close')
invalidate_cache_counts(issue, users: issue.assignees)
diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb
index 7140890d201..78e79344c99 100644
--- a/app/services/issues/move_service.rb
+++ b/app/services/issues/move_service.rb
@@ -139,7 +139,7 @@ module Issues
end
def notify_participants
- notification_service.issue_moved(@old_issue, @new_issue, @current_user)
+ notification_service.async.issue_moved(@old_issue, @new_issue, @current_user)
end
end
end
diff --git a/app/services/issues/reopen_service.rb b/app/services/issues/reopen_service.rb
index 62b4b4b6a1e..02224f3357a 100644
--- a/app/services/issues/reopen_service.rb
+++ b/app/services/issues/reopen_service.rb
@@ -6,7 +6,7 @@ module Issues
if issue.reopen
event_service.reopen_issue(issue, current_user)
create_note(issue, 'reopened')
- notification_service.reopen_issue(issue, current_user)
+ notification_service.async.reopen_issue(issue, current_user)
execute_hooks(issue, 'reopen')
invalidate_cache_counts(issue, users: issue.assignees)
issue.update_project_counter_caches
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index 1374f10c586..1000e1842b6 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -30,7 +30,7 @@ module Issues
if issue.assignees != old_assignees
create_assignee_note(issue, old_assignees)
- notification_service.reassigned_issue(issue, current_user, old_assignees)
+ notification_service.async.reassigned_issue(issue, current_user, old_assignees)
todo_service.reassigned_issue(issue, current_user, old_assignees)
end
@@ -41,13 +41,13 @@ module Issues
added_labels = issue.labels - old_labels
if added_labels.present?
- notification_service.relabeled_issue(issue, added_labels, current_user)
+ notification_service.async.relabeled_issue(issue, added_labels, current_user)
end
added_mentions = issue.mentioned_users - old_mentioned_users
if added_mentions.present?
- notification_service.new_mentions_in_issue(issue, added_mentions, current_user)
+ notification_service.async.new_mentions_in_issue(issue, added_mentions, current_user)
end
end
diff --git a/app/services/merge_requests/close_service.rb b/app/services/merge_requests/close_service.rb
index f727ec002e7..db701c1145d 100644
--- a/app/services/merge_requests/close_service.rb
+++ b/app/services/merge_requests/close_service.rb
@@ -10,7 +10,7 @@ module MergeRequests
if merge_request.close
create_event(merge_request)
create_note(merge_request)
- notification_service.close_mr(merge_request, current_user)
+ notification_service.async.close_mr(merge_request, current_user)
todo_service.close_merge_request(merge_request, current_user)
execute_hooks(merge_request, 'close')
invalidate_cache_counts(merge_request, users: merge_request.assignees)
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index cedfcb50e09..2209a60a840 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -50,21 +50,30 @@ module MergeRequests
end
def commit
- message = params[:commit_message] || merge_request.merge_commit_message
-
log_info("Git merge started on JID #{merge_jid}")
- commit_id = repository.merge(current_user, source, merge_request, message)
- log_info("Git merge finished on JID #{merge_jid} commit #{commit_id}")
+ commit_id = try_merge
+
+ if commit_id
+ log_info("Git merge finished on JID #{merge_jid} commit #{commit_id}")
+ else
+ raise MergeError, 'Conflicts detected during merge'
+ end
- raise MergeError, 'Conflicts detected during merge' unless commit_id
+ merge_request.update!(merge_commit_sha: commit_id)
+ end
+
+ def try_merge
+ message = params[:commit_message] || merge_request.merge_commit_message
- merge_request.update(merge_commit_sha: commit_id)
+ repository.merge(current_user, source, merge_request, message)
rescue Gitlab::Git::HooksService::PreReceiveError => e
- raise MergeError, e.message
- rescue StandardError => e
- raise MergeError, "Something went wrong during merge: #{e.message}"
+ handle_merge_error(log_message: e.message)
+ raise MergeError, 'Something went wrong during merge pre-receive hook'
+ rescue => e
+ handle_merge_error(log_message: e.message)
+ raise MergeError, 'Something went wrong during merge'
ensure
- merge_request.update(in_progress_merge_commit_sha: nil)
+ merge_request.update!(in_progress_merge_commit_sha: nil)
end
def after_merge
diff --git a/app/services/merge_requests/reopen_service.rb b/app/services/merge_requests/reopen_service.rb
index 120677a7149..8f1c95ac1b7 100644
--- a/app/services/merge_requests/reopen_service.rb
+++ b/app/services/merge_requests/reopen_service.rb
@@ -6,7 +6,7 @@ module MergeRequests
if merge_request.reopen
create_event(merge_request)
create_note(merge_request, 'reopened')
- notification_service.reopen_mr(merge_request, current_user)
+ notification_service.async.reopen_mr(merge_request, current_user)
execute_hooks(merge_request, 'reopen')
merge_request.reload_diff(current_user)
merge_request.mark_as_unchecked
diff --git a/app/services/merge_requests/resolved_discussion_notification_service.rb b/app/services/merge_requests/resolved_discussion_notification_service.rb
index 3a09350c847..66a0cbc81d4 100644
--- a/app/services/merge_requests/resolved_discussion_notification_service.rb
+++ b/app/services/merge_requests/resolved_discussion_notification_service.rb
@@ -4,7 +4,7 @@ module MergeRequests
return unless merge_request.discussions_resolved?
SystemNoteService.resolve_all_discussions(merge_request, project, current_user)
- notification_service.resolve_all_discussions(merge_request, current_user)
+ notification_service.async.resolve_all_discussions(merge_request, current_user)
end
end
end
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 8a40ad88182..7350725e223 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -21,6 +21,7 @@ module MergeRequests
update(merge_request)
end
+ # rubocop:disable Metrics/AbcSize
def handle_changes(merge_request, options)
old_associations = options.fetch(:old_associations, {})
old_labels = old_associations.fetch(:labels, [])
@@ -42,8 +43,11 @@ module MergeRequests
end
if merge_request.previous_changes.include?('assignee_id')
+ old_assignee_id = merge_request.previous_changes['assignee_id'].first
+ old_assignee = User.find(old_assignee_id) if old_assignee_id
+
create_assignee_note(merge_request)
- notification_service.reassigned_merge_request(merge_request, current_user)
+ notification_service.async.reassigned_merge_request(merge_request, current_user, old_assignee)
todo_service.reassigned_merge_request(merge_request, current_user)
end
@@ -54,7 +58,7 @@ module MergeRequests
added_labels = merge_request.labels - old_labels
if added_labels.present?
- notification_service.relabeled_merge_request(
+ notification_service.async.relabeled_merge_request(
merge_request,
added_labels,
current_user
@@ -63,13 +67,14 @@ module MergeRequests
added_mentions = merge_request.mentioned_users - old_mentioned_users
if added_mentions.present?
- notification_service.new_mentions_in_merge_request(
+ notification_service.async.new_mentions_in_merge_request(
merge_request,
added_mentions,
current_user
)
end
end
+ # rubocop:enable Metrics/AbcSize
def merge_from_quick_action(merge_request)
last_diff_sha = params.delete(:merge)
diff --git a/app/services/notes/resolve_service.rb b/app/services/notes/resolve_service.rb
new file mode 100644
index 00000000000..0db8ee809a9
--- /dev/null
+++ b/app/services/notes/resolve_service.rb
@@ -0,0 +1,9 @@
+module Notes
+ class ResolveService < ::BaseService
+ def execute(note)
+ note.resolve!(current_user)
+
+ ::MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(note.noteable)
+ end
+ end
+end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 274161df946..55a1735e54b 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -7,7 +7,32 @@
# Ex.
# NotificationService.new.new_issue(issue, current_user)
#
+# When calculating the recipients of a notification is expensive (for instance,
+# in the new issue case), `#async` will make that calculation happen in Sidekiq
+# instead:
+#
+# NotificationService.new.async.new_issue(issue, current_user)
+#
class NotificationService
+ class Async
+ attr_reader :parent
+ delegate :respond_to_missing, to: :parent
+
+ def initialize(parent)
+ @parent = parent
+ end
+
+ def method_missing(meth, *args)
+ return super unless parent.respond_to?(meth)
+
+ MailScheduler::NotificationServiceWorker.perform_async(meth.to_s, *args)
+ end
+ end
+
+ def async
+ @async ||= Async.new(self)
+ end
+
# Always notify user about ssh key added
# only if ssh key is not deploy key
#
@@ -142,8 +167,23 @@ class NotificationService
# * merge_request assignee if their notification level is not Disabled
# * users with custom level checked with "reassign merge request"
#
- def reassigned_merge_request(merge_request, current_user)
- reassign_resource_email(merge_request, current_user, :reassigned_merge_request_email)
+ def reassigned_merge_request(merge_request, current_user, previous_assignee)
+ recipients = NotificationRecipientService.build_recipients(
+ merge_request,
+ current_user,
+ action: "reassign",
+ previous_assignee: previous_assignee
+ )
+
+ recipients.each do |recipient|
+ mailer.reassigned_merge_request_email(
+ recipient.user.id,
+ merge_request.id,
+ previous_assignee&.id,
+ current_user.id,
+ recipient.reason
+ ).deliver_later
+ end
end
# When we add labels to a merge request we should send an email to:
@@ -421,29 +461,6 @@ class NotificationService
end
end
- def reassign_resource_email(target, current_user, method)
- previous_assignee_id = previous_record(target, 'assignee_id')
- previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
-
- recipients = NotificationRecipientService.build_recipients(
- target,
- current_user,
- action: "reassign",
- previous_assignee: previous_assignee
- )
-
- recipients.each do |recipient|
- mailer.send(
- method,
- recipient.user.id,
- target.id,
- previous_assignee_id,
- current_user.id,
- recipient.reason
- ).deliver_later
- end
- end
-
def relabeled_resource_email(target, labels, current_user, method)
recipients = labels.flat_map { |l| l.subscribers(target.project) }.uniq
recipients = notifiable_users(
@@ -471,14 +488,6 @@ class NotificationService
Notify
end
- def previous_record(object, attribute)
- return unless object && attribute
-
- if object.previous_changes.include?(attribute)
- object.previous_changes[attribute].first
- end
- end
-
private
def recipients_for_pages_domain(domain)
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index de77f6bf585..1d8caec9c6f 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -1,6 +1,6 @@
module Projects
class UpdatePagesService < BaseService
- InvaildStateError = Class.new(StandardError)
+ InvalidStateError = Class.new(StandardError)
FailedToExtractError = Class.new(StandardError)
BLOCK_SIZE = 32.kilobytes
@@ -21,8 +21,8 @@ module Projects
@status.enqueue!
@status.run!
- raise InvaildStateError, 'missing pages artifacts' unless build.artifacts?
- raise InvaildStateError, 'pages are outdated' unless latest?
+ raise InvalidStateError, 'missing pages artifacts' unless build.artifacts?
+ raise InvalidStateError, 'pages are outdated' unless latest?
# Create temporary directory in which we will extract the artifacts
FileUtils.mkdir_p(tmp_path)
@@ -31,16 +31,16 @@ module Projects
# Check if we did extract public directory
archive_public_path = File.join(archive_path, 'public')
- raise InvaildStateError, 'pages miss the public folder' unless Dir.exist?(archive_public_path)
- raise InvaildStateError, 'pages are outdated' unless latest?
+ raise InvalidStateError, 'pages miss the public folder' unless Dir.exist?(archive_public_path)
+ raise InvalidStateError, 'pages are outdated' unless latest?
deploy_page!(archive_public_path)
success
end
- rescue InvaildStateError => e
+ rescue InvalidStateError => e
error(e.message)
rescue => e
- error(e.message, false)
+ error(e.message)
raise e
end
@@ -48,17 +48,15 @@ module Projects
def success
@status.success
- delete_artifact!
super
end
- def error(message, allow_delete_artifact = true)
+ def error(message)
register_failure
log_error("Projects::UpdatePagesService: #{message}")
@status.allow_failure = !latest?
@status.description = message
@status.drop(:script_failure)
- delete_artifact! if allow_delete_artifact
super
end
@@ -77,18 +75,18 @@ module Projects
if artifacts.ends_with?('.zip')
extract_zip_archive!(temp_path)
else
- raise InvaildStateError, 'unsupported artifacts format'
+ raise InvalidStateError, 'unsupported artifacts format'
end
end
def extract_zip_archive!(temp_path)
- raise InvaildStateError, 'missing artifacts metadata' unless build.artifacts_metadata?
+ raise InvalidStateError, 'missing artifacts metadata' unless build.artifacts_metadata?
# Calculate page size after extract
public_entry = build.artifacts_metadata_entry(SITE_PATH, recursive: true)
if public_entry.total_size > max_size
- raise InvaildStateError, "artifacts for pages are too large: #{public_entry.total_size}"
+ raise InvalidStateError, "artifacts for pages are too large: #{public_entry.total_size}"
end
# Requires UnZip at least 6.00 Info-ZIP.
@@ -162,11 +160,6 @@ module Projects
build.artifacts_file.path
end
- def delete_artifact!
- build.reload # Reload stable object to prevent erase artifacts with old state
- build.erase_artifacts! unless build.has_expiring_artifacts?
- end
-
def latest_sha
project.commit(build.ref).try(:sha).to_s
ensure
diff --git a/app/services/repository_archive_clean_up_service.rb b/app/services/repository_archive_clean_up_service.rb
index 9a88459b511..ba7be4b3f89 100644
--- a/app/services/repository_archive_clean_up_service.rb
+++ b/app/services/repository_archive_clean_up_service.rb
@@ -20,11 +20,12 @@ class RepositoryArchiveCleanUpService
private
def clean_up_old_archives
- run(%W(find #{path} -not -path #{path} -type f \( -name \*.tar -o -name \*.bz2 -o -name \*.tar.gz -o -name \*.zip \) -maxdepth 2 -mmin +#{mmin} -delete))
+ run(%W(find #{path} -mindepth 1 -maxdepth 3 -type f \( -name \*.tar -o -name \*.bz2 -o -name \*.tar.gz -o -name \*.zip \) -mmin +#{mmin} -delete))
end
def clean_up_empty_directories
- run(%W(find #{path} -not -path #{path} -type d -empty -name \*.git -maxdepth 1 -delete))
+ run(%W(find #{path} -mindepth 2 -maxdepth 2 -type d -empty -delete))
+ run(%W(find #{path} -mindepth 1 -maxdepth 1 -type d -empty -delete))
end
def run(cmd)
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 958ef065012..00bf5434b7f 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -159,7 +159,7 @@ module SystemNoteService
body = if noteable.time_estimate == 0
"removed time estimate"
else
- "changed time estimate to #{parsed_time},"
+ "changed time estimate to #{parsed_time}"
end
create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking'))
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index aeba9788fda..75ca5106fd5 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -13,7 +13,7 @@
.panel
.panel-heading.alert.alert-danger
Last repository check
- = "(#{time_ago_in_words(@project.last_repository_check_at)} ago)"
+ = "(#{time_ago_with_tooltip(@project.last_repository_check_at)})"
failed. See
= link_to 'repocheck.log', admin_logs_path
for error messages.
diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml
index 6e76e7c2768..99fbbaec487 100644
--- a/app/views/admin/runners/_runner.html.haml
+++ b/app/views/admin/runners/_runner.html.haml
@@ -33,7 +33,7 @@
= tag
%td
- if runner.contacted_at
- #{time_ago_in_words(runner.contacted_at)} ago
+ = time_ago_with_tooltip runner.contacted_at
- else
Never
%td.admin-runner-btn-group-cell
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index 3b5fedfb058..d022016f70d 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -111,4 +111,4 @@
%td.timestamp
- if build.finished_at
- %span #{time_ago_in_words build.finished_at} ago
+ %span= time_ago_with_tooltip build.finished_at
diff --git a/app/views/admin/services/index.html.haml b/app/views/admin/services/index.html.haml
index 50132572096..89872c1b91a 100644
--- a/app/views/admin/services/index.html.haml
+++ b/app/views/admin/services/index.html.haml
@@ -20,5 +20,4 @@
%td
= service.description
%td.light
- = time_ago_in_words service.updated_at
- ago
+ = time_ago_with_tooltip service.updated_at
diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml
index 2ff4221efbd..badf3dd74b3 100644
--- a/app/views/admin/users/_user.html.haml
+++ b/app/views/admin/users/_user.html.haml
@@ -43,7 +43,7 @@
delete_user_url: admin_user_path(user),
block_user_url: block_admin_user_path(user),
username: user.name,
- delete_contributions: 'false' }, type: 'button' }
+ delete_contributions: false }, type: 'button' }
= s_('AdminUsers|Delete user')
%li
@@ -52,5 +52,5 @@
delete_user_url: admin_user_path(user, hard_delete: true),
block_user_url: block_admin_user_path(user),
username: user.name,
- delete_contributions: 'true' }, type: 'button' }
+ delete_contributions: true }, type: 'button' }
= s_('AdminUsers|Delete user and contributions')
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index ec3be869797..814ccdae8f3 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -183,7 +183,7 @@
delete_user_url: admin_user_path(@user),
block_user_url: block_admin_user_path(@user),
username: @user.name,
- delete_contributions: 'false' }, type: 'button' }
+ delete_contributions: false }, type: 'button' }
= s_('AdminUsers|Delete user')
- else
- if @user.solo_owned_groups.present?
@@ -215,7 +215,7 @@
delete_user_url: admin_user_path(@user, hard_delete: true),
block_user_url: block_admin_user_path(@user),
username: @user.name,
- delete_contributions: 'true' }, type: 'button' }
+ delete_contributions: true }, type: 'button' }
= s_('AdminUsers|Delete user and contributions')
- else
%p
diff --git a/app/views/ci/variables/_variable_row.html.haml b/app/views/ci/variables/_variable_row.html.haml
index 440623b34f5..571eb28f195 100644
--- a/app/views/ci/variables/_variable_row.html.haml
+++ b/app/views/ci/variables/_variable_row.html.haml
@@ -17,14 +17,14 @@
.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",
+ %input.js-ci-variable-input-key.ci-variable-body-item.qa-ci-variable-input-key.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) }
+ .form-control.js-secret-value-placeholder.qa-ci-variable-input-value{ class: ('hide' unless id) }
= '*' * 20
- %textarea.js-ci-variable-input-value.js-secret-value.form-control{ class: ('hide' if id),
+ %textarea.js-ci-variable-input-value.js-secret-value.qa-ci-variable-input-value.form-control{ class: ('hide' if id),
rows: 1,
name: value_input_name,
placeholder: s_('CiVariables|Input variable value') }
diff --git a/app/views/devise/mailer/unlock_instructions.html.haml b/app/views/devise/mailer/unlock_instructions.html.haml
index 79e3a35cc9a..8ddfd3ea74a 100644
--- a/app/views/devise/mailer/unlock_instructions.html.haml
+++ b/app/views/devise/mailer/unlock_instructions.html.haml
@@ -2,7 +2,7 @@
= email_default_heading("Hello, #{@resource.name}!")
%p
Your GitLab account has been locked due to an excessive amount of unsuccessful
- sign in attempts. Your account will automatically unlock in #{time_ago_in_words(Devise.unlock_in.from_now)}
+ sign in attempts. Your account will automatically unlock in #{distance_of_time_in_words(Devise.unlock_in)}
or you may click the link below to unlock now.
#cta
= link_to('Unlock account', unlock_url(@resource, unlock_token: @token))
diff --git a/app/views/devise/mailer/unlock_instructions.text.erb b/app/views/devise/mailer/unlock_instructions.text.erb
index 3aea3e20145..8d4abbf3500 100644
--- a/app/views/devise/mailer/unlock_instructions.text.erb
+++ b/app/views/devise/mailer/unlock_instructions.text.erb
@@ -1,7 +1,7 @@
Hello, <%= @resource.name %>!
Your GitLab account has been locked due to an excessive amount of unsuccessful
-sign in attempts. Your account will automatically unlock in <%= time_ago_in_words(Devise.unlock_in.from_now) %>
+sign in attempts. Your account will automatically unlock in <%= distance_of_time_in_words(Devise.unlock_in) %>
or you may click the link below to unlock now.
<%= unlock_url(@resource, unlock_token: @token) %>
diff --git a/app/views/groups/_group_admin_settings.html.haml b/app/views/groups/_group_admin_settings.html.haml
index 2ace1e2dd1e..65e95f3aeef 100644
--- a/app/views/groups/_group_admin_settings.html.haml
+++ b/app/views/groups/_group_admin_settings.html.haml
@@ -1,28 +1,26 @@
-- if current_user.admin?
- .form-group
- = f.label :lfs_enabled, 'Large File Storage', class: 'control-label'
- .col-sm-10
- .checkbox
- = f.label :lfs_enabled do
- = f.check_box :lfs_enabled, checked: @group.lfs_enabled?
- %strong
- Allow projects within this group to use Git LFS
- = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
- %br/
- %span.descr This setting can be overridden in each project.
+.form-group
+ = f.label :lfs_enabled, 'Large File Storage', class: 'control-label'
+ .col-sm-10
+ .checkbox
+ = f.label :lfs_enabled do
+ = f.check_box :lfs_enabled, checked: @group.lfs_enabled?
+ %strong
+ Allow projects within this group to use Git LFS
+ = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
+ %br/
+ %span.descr This setting can be overridden in each project.
-- if can? current_user, :admin_group, @group
- .form-group
- = f.label :require_two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2'
- .col-sm-10
- .checkbox
- = f.label :require_two_factor_authentication do
- = f.check_box :require_two_factor_authentication
- %strong
- Require all users in this group to setup Two-factor authentication
- = link_to icon('question-circle'), help_page_path('security/two_factor_authentication', anchor: 'enforcing-2fa-for-all-users-in-a-group')
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.text_field :two_factor_grace_period, class: 'form-control'
- .help-block Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication
+.form-group
+ = f.label :require_two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2'
+ .col-sm-10
+ .checkbox
+ = f.label :require_two_factor_authentication do
+ = f.check_box :require_two_factor_authentication
+ %strong
+ Require all users in this group to setup Two-factor authentication
+ = link_to icon('question-circle'), help_page_path('security/two_factor_authentication', anchor: 'enforcing-2fa-for-all-users-in-a-group')
+.form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.text_field :two_factor_grace_period, class: 'form-control'
+ .help-block Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication
diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml
index c878fcf2808..6cbd163dd41 100644
--- a/app/views/layouts/nav/sidebar/_profile.html.haml
+++ b/app/views/layouts/nav/sidebar/_profile.html.haml
@@ -129,6 +129,17 @@
= link_to profile_preferences_path do
%strong.fly-out-top-item-name
#{ _('Preferences') }
+ = nav_link(controller: :active_sessions) do
+ = link_to profile_active_sessions_path do
+ .nav-icon-container
+ = sprite_icon('monitor-lines')
+ %span.nav-item-name
+ Active Sessions
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: :active_sessions, html_options: { class: "fly-out-top-item" } ) do
+ = link_to profile_active_sessions_path do
+ %strong.fly-out-top-item-name
+ #{ _('Active Sessions') }
= nav_link(path: 'profiles#audit_log') do
= link_to audit_log_profile_path do
.nav-icon-container
diff --git a/app/views/peek/_bar.html.haml b/app/views/peek/_bar.html.haml
index b4d86e1601c..cb0cccb8f8a 100644
--- a/app/views/peek/_bar.html.haml
+++ b/app/views/peek/_bar.html.haml
@@ -3,10 +3,5 @@
#js-peek{ data: { env: Peek.env,
request_id: Peek.request_id,
peek_url: peek_routes.results_url,
- profile_url: url_for(params.merge(lineprofiler: 'true')) },
+ profile_url: url_for(safe_params.merge(lineprofiler: 'true')) },
class: Peek.env }
-
-#peek-view-performance-bar.hidden
- = render_server_response_time
- %span#serverstats
- %ul.performance-bar
diff --git a/app/views/profiles/active_sessions/_active_session.html.haml b/app/views/profiles/active_sessions/_active_session.html.haml
new file mode 100644
index 00000000000..d40b771f48b
--- /dev/null
+++ b/app/views/profiles/active_sessions/_active_session.html.haml
@@ -0,0 +1,31 @@
+- is_current_session = active_session.current?(session)
+
+%li
+ .pull-left.append-right-10{ data: { toggle: 'tooltip' }, title: active_session.human_device_type }
+ = active_session_device_type_icon(active_session)
+
+ .description.pull-left
+ %div
+ %strong= active_session.ip_address
+ - if is_current_session
+ %div This is your current session
+ - else
+ %div
+ Last accessed on
+ = l(active_session.updated_at, format: :short)
+
+ %div
+ %strong= active_session.browser
+ on
+ %strong= active_session.os
+
+ %div
+ %strong Signed in
+ on
+ = l(active_session.created_at, format: :short)
+
+ - unless is_current_session
+ .pull-right
+ = link_to profile_active_session_path(active_session.session_id), data: { confirm: 'Are you sure? The device will be signed out of GitLab.' }, method: :delete, class: "btn btn-danger prepend-left-10" do
+ %span.sr-only Revoke
+ Revoke
diff --git a/app/views/profiles/active_sessions/index.html.haml b/app/views/profiles/active_sessions/index.html.haml
new file mode 100644
index 00000000000..d0250bb4eab
--- /dev/null
+++ b/app/views/profiles/active_sessions/index.html.haml
@@ -0,0 +1,14 @@
+- page_title 'Active Sessions'
+- @content_class = "limit-container-width" unless fluid_layout
+
+.row.prepend-top-default
+ .col-lg-4.profile-settings-sidebar
+ %h4.prepend-top-0
+ = page_title
+ %p
+ This is a list of devices that have logged into your account. Revoke any sessions that you do not recognize.
+ .col-lg-8
+ .append-bottom-default
+
+ %ul.well-list
+ = render partial: 'profiles/active_sessions/active_session', collection: @sessions
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index d0c01f95cb7..0e012b5a216 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -8,18 +8,17 @@
%li{ class: "branch-item js-branch-#{branch.name}" }
.branch-info
.branch-title
- = link_to project_tree_path(@project, branch.name), class: 'item-title str-truncated-100 ref-name' do
- = sprite_icon('fork', size: 12)
+ = sprite_icon('fork', size: 12)
+ = link_to project_tree_path(@project, branch.name), class: 'item-title str-truncated-100 ref-name prepend-left-8' do
= branch.name
- &nbsp;
- if branch.name == @repository.root_ref
- %span.label.label-primary default
+ %span.label.label-primary.prepend-left-5 default
- elsif merged
- %span.label.label-info.has-tooltip{ title: s_('Branches|Merged into %{default_branch}') % { default_branch: @repository.root_ref } }
+ %span.label.label-info.has-tooltip.prepend-left-5{ title: s_('Branches|Merged into %{default_branch}') % { default_branch: @repository.root_ref } }
= s_('Branches|merged')
- if protected_branch?(@project, branch)
- %span.label.label-success
+ %span.label.label-success.prepend-left-5
= s_('Branches|protected')
.block-truncated
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 376f672f424..9f420ee86f7 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -8,7 +8,7 @@
.files-changed-inner
.inline-parallel-buttons.hidden-xs.hidden-sm
- if !diffs_expanded? && diff_files.any? { |diff_file| diff_file.collapsed? }
- = link_to 'Expand all', url_for(params.merge(expanded: 1, format: nil)), class: 'btn btn-default'
+ = link_to 'Expand all', url_for(safe_params.merge(expanded: 1, format: nil)), class: 'btn btn-default'
- if show_whitespace_toggle
- if current_controller?(:commit)
= commit_diff_whitespace_link(diffs.project, @commit, class: 'hidden-xs')
diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml
index 7f0bef5ede0..826404c2008 100644
--- a/app/views/projects/jobs/_sidebar.html.haml
+++ b/app/views/projects/jobs/_sidebar.html.haml
@@ -15,7 +15,7 @@
- elsif @build.has_expiring_artifacts?
%p.build-detail-row
The artifacts will be removed in
- %span= time_ago_in_words @build.artifacts_expire_at
+ %span= time_ago_with_tooltip @build.artifacts_expire_at
- if @build.artifacts?
.btn-group.btn-group-justified{ role: :group }
diff --git a/app/views/projects/merge_requests/creations/_new_compare.html.haml b/app/views/projects/merge_requests/creations/_new_compare.html.haml
index f81db9b4e28..4e10511411f 100644
--- a/app/views/projects/merge_requests/creations/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_compare.html.haml
@@ -3,7 +3,7 @@
= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], url: project_new_merge_request_path(@project), method: :get, html: { class: "merge-request-form form-inline js-requires-input" } do |f|
.hide.alert.alert-danger.mr-compare-errors
- .js-merge-request-new-compare.row{ 'data-target-project-url': project_new_merge_request_update_branches_path(@source_project), 'data-source-branch-url': project_new_merge_request_branch_from_path(@source_project), 'data-target-branch-url': project_new_merge_request_branch_to_path(@source_project) }
+ .js-merge-request-new-compare.row{ 'data-source-branch-url': project_new_merge_request_branch_from_path(@source_project), 'data-target-branch-url': project_new_merge_request_branch_to_path(@source_project) }
.col-md-6
.panel.panel-default.panel-new-merge-request
.panel-heading
@@ -11,7 +11,7 @@
.panel-body.clearfix
.merge-request-select.dropdown
= f.hidden_field :source_project_id
- = dropdown_toggle @merge_request.source_project_path, { toggle: "dropdown", field_name: "#{f.object_name}[source_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-source-project" }
+ = dropdown_toggle @merge_request.source_project_path, { toggle: "dropdown", 'field-name': "#{f.object_name}[source_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-source-project" }
.dropdown-menu.dropdown-menu-selectable.dropdown-source-project
= dropdown_title("Select source project")
= dropdown_filter("Search projects")
@@ -21,14 +21,12 @@
selected: f.object.source_project_id
.merge-request-select.dropdown
= f.hidden_field :source_branch
- = dropdown_toggle f.object.source_branch || "Select source branch", { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch git-revision-dropdown-toggle" }
- .dropdown-menu.dropdown-menu-selectable.dropdown-source-branch.git-revision-dropdown
- = dropdown_title("Select source branch")
- = dropdown_filter("Search branches")
- = dropdown_content do
- = render 'projects/merge_requests/dropdowns/branch',
- branches: @merge_request.source_branches,
- selected: f.object.source_branch
+ = dropdown_toggle f.object.source_branch || _("Select source branch"), { toggle: "dropdown", 'field-name': "#{f.object_name}[source_branch]", 'refs-url': refs_project_path(@source_project), selected: f.object.source_branch }, { toggle_class: "js-compare-dropdown js-source-branch git-revision-dropdown-toggle" }
+ .dropdown-menu.dropdown-menu-selectable.js-source-branch-dropdown.git-revision-dropdown
+ = dropdown_title(_("Select source branch"))
+ = dropdown_filter(_("Search branches"))
+ = dropdown_content
+ = dropdown_loading
.panel-footer
.text-center= icon('spinner spin', class: 'js-source-loading')
%ul.list-unstyled.mr_source_commit
@@ -41,7 +39,7 @@
- projects = target_projects(@project)
.merge-request-select.dropdown
= f.hidden_field :target_project_id
- = dropdown_toggle f.object.target_project.full_path, { toggle: "dropdown", field_name: "#{f.object_name}[target_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-target-project" }
+ = dropdown_toggle f.object.target_project.full_path, { toggle: "dropdown", 'field-name': "#{f.object_name}[target_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-target-project" }
.dropdown-menu.dropdown-menu-selectable.dropdown-target-project
= dropdown_title("Select target project")
= dropdown_filter("Search projects")
@@ -51,14 +49,12 @@
selected: f.object.target_project_id
.merge-request-select.dropdown
= f.hidden_field :target_branch
- = dropdown_toggle f.object.target_branch, { toggle: "dropdown", field_name: "#{f.object_name}[target_branch]" }, { toggle_class: "js-compare-dropdown js-target-branch git-revision-dropdown-toggle" }
- .dropdown-menu.dropdown-menu-selectable.dropdown-target-branch.js-target-branch-dropdown.git-revision-dropdown
- = dropdown_title("Select target branch")
- = dropdown_filter("Search branches")
- = dropdown_content do
- = render 'projects/merge_requests/dropdowns/branch',
- branches: @merge_request.target_branches,
- selected: f.object.target_branch
+ = dropdown_toggle f.object.target_branch, { toggle: "dropdown", 'field-name': "#{f.object_name}[target_branch]", 'refs-url': refs_project_path(f.object.target_project), selected: f.object.target_branch }, { toggle_class: "js-compare-dropdown js-target-branch git-revision-dropdown-toggle" }
+ .dropdown-menu.dropdown-menu-selectable.js-target-branch-dropdown.git-revision-dropdown
+ = dropdown_title(_("Select target branch"))
+ = dropdown_filter(_("Search branches"))
+ = dropdown_content
+ = dropdown_loading
.panel-footer
.text-center= icon('spinner spin', class: "js-target-loading")
%ul.list-unstyled.mr_target_commit
diff --git a/app/views/projects/merge_requests/dropdowns/_project.html.haml b/app/views/projects/merge_requests/dropdowns/_project.html.haml
index aaf1ab00eeb..b3cf3c1d369 100644
--- a/app/views/projects/merge_requests/dropdowns/_project.html.haml
+++ b/app/views/projects/merge_requests/dropdowns/_project.html.haml
@@ -1,5 +1,5 @@
%ul
- projects.each do |project|
%li
- %a{ href: "#", class: "#{('is-active' if selected == project.id)}", data: { id: project.id } }
+ %a{ href: "#", class: "#{('is-active' if selected == project.id)}", data: { id: project.id, 'refs-url': refs_project_path(project) } }
= project.full_path
diff --git a/app/views/projects/protected_branches/_branches_list.html.haml b/app/views/projects/protected_branches/_branches_list.html.haml
index 5377d745371..24d2b971472 100644
--- a/app/views/projects/protected_branches/_branches_list.html.haml
+++ b/app/views/projects/protected_branches/_branches_list.html.haml
@@ -1,4 +1,4 @@
- can_admin_project = can?(current_user, :admin_project, @project)
= render layout: 'projects/protected_branches/shared/branches_list', locals: { can_admin_project: can_admin_project } do
- = render partial: 'projects/protected_branches/protected_branch', collection: @protected_branches, locals: { can_admin_project: can_admin_project}
+ = render partial: 'projects/protected_branches/protected_branch', collection: @protected_branches
diff --git a/app/views/projects/protected_branches/_create_protected_branch.html.haml b/app/views/projects/protected_branches/_create_protected_branch.html.haml
index 12ccae10260..24b53555cdc 100644
--- a/app/views/projects/protected_branches/_create_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/_create_protected_branch.html.haml
@@ -1,8 +1,8 @@
- content_for :merge_access_levels do
.merge_access_levels-container
= dropdown_tag('Select',
- options: { toggle_class: 'js-allowed-to-merge wide',
- dropdown_class: 'dropdown-menu-selectable capitalize-header',
+ options: { toggle_class: 'js-allowed-to-merge qa-allowed-to-merge-select wide',
+ dropdown_class: 'dropdown-menu-selectable qa-allowed-to-merge-dropdown capitalize-header',
data: { field_name: 'protected_branch[merge_access_levels_attributes][0][access_level]', input_id: 'merge_access_levels_attributes' }})
- content_for :push_access_levels do
.push_access_levels-container
diff --git a/app/views/projects/protected_branches/_update_protected_branch.html.haml b/app/views/projects/protected_branches/_update_protected_branch.html.haml
index 98363f2018a..f242459f69b 100644
--- a/app/views/projects/protected_branches/_update_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/_update_protected_branch.html.haml
@@ -1,7 +1,7 @@
%td
= hidden_field_tag "allowed_to_merge_#{protected_branch.id}", protected_branch.merge_access_levels.first.access_level
= dropdown_tag( (protected_branch.merge_access_levels.first.humanize || 'Select') ,
- options: { toggle_class: 'js-allowed-to-merge', dropdown_class: 'dropdown-menu-selectable js-allowed-to-merge-container capitalize-header',
+ options: { toggle_class: 'js-allowed-to-merge qa-allowed-to-merge', dropdown_class: 'dropdown-menu-selectable js-allowed-to-merge-container capitalize-header',
data: { field_name: "allowed_to_merge_#{protected_branch.id}", access_level_id: protected_branch.merge_access_levels.first.id }})
%td
= hidden_field_tag "allowed_to_push_#{protected_branch.id}", protected_branch.push_access_levels.first.access_level
diff --git a/app/views/projects/protected_branches/shared/_protected_branch.html.haml b/app/views/projects/protected_branches/shared/_protected_branch.html.haml
index f5b21f0e887..2d3b2af00c2 100644
--- a/app/views/projects/protected_branches/shared/_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/shared/_protected_branch.html.haml
@@ -21,4 +21,4 @@
- if can_admin_project
%td
- = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: 'btn btn-warning'
+ = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_branch], disabled: local_assigns[:disabled], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-warning"
diff --git a/app/views/projects/registry/repositories/_tag.html.haml b/app/views/projects/registry/repositories/_tag.html.haml
index 0b082a2137f..0223372bff8 100644
--- a/app/views/projects/registry/repositories/_tag.html.haml
+++ b/app/views/projects/registry/repositories/_tag.html.haml
@@ -18,7 +18,7 @@
\-
%td
- if tag.created_at
- = time_ago_in_words(tag.created_at)
+ = time_ago_with_tooltip tag.created_at
- else
.light
\-
diff --git a/app/views/projects/runners/show.html.haml b/app/views/projects/runners/show.html.haml
index f33e7e25b68..322152cfaca 100644
--- a/app/views/projects/runners/show.html.haml
+++ b/app/views/projects/runners/show.html.haml
@@ -62,6 +62,6 @@
%td Last contact
%td
- if @runner.contacted_at
- #{time_ago_in_words(@runner.contacted_at)} ago
+ = time_ago_with_tooltip @runner.contacted_at
- else
Never
diff --git a/app/views/projects/services/_index.html.haml b/app/views/projects/services/_index.html.haml
index 915c6b22162..dac7d4d1bbb 100644
--- a/app/views/projects/services/_index.html.haml
+++ b/app/views/projects/services/_index.html.haml
@@ -27,5 +27,4 @@
= service.description
%td.light
- if service.updated_at.present?
- = time_ago_in_words service.updated_at
- ago
+ = time_ago_with_tooltip service.updated_at
diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
index 7b410101c05..71e77dae69e 100644
--- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
@@ -36,5 +36,6 @@
= form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
.help-block
= s_('CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages.')
+ = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'auto-devops-base-domain'), target: '_blank'
= f.submit 'Save changes', class: "btn btn-success prepend-top-15"
diff --git a/app/views/projects/triggers/_trigger.html.haml b/app/views/projects/triggers/_trigger.html.haml
index 6249c32b7cc..9201680f119 100644
--- a/app/views/projects/triggers/_trigger.html.haml
+++ b/app/views/projects/triggers/_trigger.html.haml
@@ -25,7 +25,7 @@
%td
- if trigger.last_used
- #{time_ago_in_words(trigger.last_used)} ago
+ = time_ago_with_tooltip trigger.last_used
- else
Never
diff --git a/app/views/sherlock/transactions/_general.html.haml b/app/views/sherlock/transactions/_general.html.haml
index 8533b130da6..a37fb5d449a 100644
--- a/app/views/sherlock/transactions/_general.html.haml
+++ b/app/views/sherlock/transactions/_general.html.haml
@@ -35,5 +35,4 @@
%span.light
#{t('sherlock.finished_at')}:
%strong
- = time_ago_in_words(@transaction.finished_at)
- = t('sherlock.ago')
+ = time_ago_with_tooltip @transaction.finished_at
diff --git a/app/views/sherlock/transactions/index.html.haml b/app/views/sherlock/transactions/index.html.haml
index bc05659dfa8..6ed7e9e21a6 100644
--- a/app/views/sherlock/transactions/index.html.haml
+++ b/app/views/sherlock/transactions/index.html.haml
@@ -35,8 +35,7 @@
= t('sherlock.seconds')
%td= trans.queries.length
%td
- = time_ago_in_words(trans.finished_at)
- = t('sherlock.ago')
+ = time_ago_with_tooltip trans.finished_at
%td
= link_to(sherlock_transaction_path(trans), class: 'btn btn-xs') do
= t('sherlock.view')
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 9aea3bad27b..c469aea7052 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -41,6 +41,7 @@
- github_importer:github_import_stage_import_repository
- mail_scheduler:mail_scheduler_issue_due
+- mail_scheduler:mail_scheduler_notification_service
- object_storage_upload
- object_storage:object_storage_background_move
diff --git a/app/workers/concerns/mail_scheduler_queue.rb b/app/workers/concerns/mail_scheduler_queue.rb
index 9df55ad9522..f3e9680d756 100644
--- a/app/workers/concerns/mail_scheduler_queue.rb
+++ b/app/workers/concerns/mail_scheduler_queue.rb
@@ -4,4 +4,8 @@ module MailSchedulerQueue
included do
queue_namespace :mail_scheduler
end
+
+ def notification_service
+ @notification_service ||= NotificationService.new
+ end
end
diff --git a/app/workers/mail_scheduler/issue_due_worker.rb b/app/workers/mail_scheduler/issue_due_worker.rb
index b06079d68ca..54285884a52 100644
--- a/app/workers/mail_scheduler/issue_due_worker.rb
+++ b/app/workers/mail_scheduler/issue_due_worker.rb
@@ -4,8 +4,6 @@ module MailScheduler
include MailSchedulerQueue
def perform(project_id)
- notification_service = NotificationService.new
-
Issue.opened.due_tomorrow.in_projects(project_id).preload(:project).find_each do |issue|
notification_service.issue_due(issue)
end
diff --git a/app/workers/mail_scheduler/notification_service_worker.rb b/app/workers/mail_scheduler/notification_service_worker.rb
new file mode 100644
index 00000000000..7cfe0aa0df1
--- /dev/null
+++ b/app/workers/mail_scheduler/notification_service_worker.rb
@@ -0,0 +1,19 @@
+require 'active_job/arguments'
+
+module MailScheduler
+ class NotificationServiceWorker
+ include ApplicationWorker
+ include MailSchedulerQueue
+
+ def perform(meth, *args)
+ deserialized_args = ActiveJob::Arguments.deserialize(args)
+
+ notification_service.public_send(meth, *deserialized_args) # rubocop:disable GitlabSecurity/PublicSend
+ rescue ActiveJob::DeserializationError
+ end
+
+ def self.perform_async(*args)
+ super(*ActiveJob::Arguments.serialize(args))
+ end
+ end
+end
diff --git a/changelogs/unreleased/44775-avatar-on-os-fails-with-cdn.yml b/changelogs/unreleased/44775-avatar-on-os-fails-with-cdn.yml
deleted file mode 100644
index 80b5b4a8abe..00000000000
--- a/changelogs/unreleased/44775-avatar-on-os-fails-with-cdn.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed wrong avatar URL when the avatar is on object storage.
-merge_request: 18092
-author:
-type: fixed
diff --git a/changelogs/unreleased/45451-user-deletion-modal-with-same-info-for-delete-user-or-delete-user-and-contributions.yml b/changelogs/unreleased/45451-user-deletion-modal-with-same-info-for-delete-user-or-delete-user-and-contributions.yml
new file mode 100644
index 00000000000..707a18745c8
--- /dev/null
+++ b/changelogs/unreleased/45451-user-deletion-modal-with-same-info-for-delete-user-or-delete-user-and-contributions.yml
@@ -0,0 +1,6 @@
+---
+title: Correct text and functionality for delete user / delete user and contributions
+ modal.
+merge_request: 18463
+author: Marc Schwede
+type: fixed
diff --git a/changelogs/unreleased/45481-sane-pages-artifacts.yml b/changelogs/unreleased/45481-sane-pages-artifacts.yml
new file mode 100644
index 00000000000..b9c68b70012
--- /dev/null
+++ b/changelogs/unreleased/45481-sane-pages-artifacts.yml
@@ -0,0 +1,6 @@
+---
+title: Don't automatically remove artifacts for pages jobs after pages:deploy has
+ run
+merge_request: 18628
+author:
+type: fixed
diff --git a/changelogs/unreleased/45572-members-invitations-scheduled-before-commit.yml b/changelogs/unreleased/45572-members-invitations-scheduled-before-commit.yml
new file mode 100644
index 00000000000..7cdea436d47
--- /dev/null
+++ b/changelogs/unreleased/45572-members-invitations-scheduled-before-commit.yml
@@ -0,0 +1,5 @@
+---
+title: Ensure member notifications are sent after the member actual creation/update in the DB
+merge_request: 18538
+author:
+type: fixed
diff --git a/changelogs/unreleased/45761-replace-actionview-time_ago_in_words.yml b/changelogs/unreleased/45761-replace-actionview-time_ago_in_words.yml
new file mode 100644
index 00000000000..adf4db90407
--- /dev/null
+++ b/changelogs/unreleased/45761-replace-actionview-time_ago_in_words.yml
@@ -0,0 +1,5 @@
+---
+title: Replace time_ago_in_words with JS-based one
+merge_request: 18607
+author: Takuya Noguchi
+type: performance
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-source-markdown-render-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-source-markdown-render-feature.yml
new file mode 100644
index 00000000000..657ed782880
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-replace-spinach-project-source-markdown-render-feature.yml
@@ -0,0 +1,5 @@
+---
+title: Replace the `project/source/markdown_render.feature` spinach test with an rspec analog
+merge_request: 18525
+author: "@blackst0ne"
+type: other
diff --git a/changelogs/unreleased/bvl-fix-maintainer-push-error.yml b/changelogs/unreleased/bvl-fix-maintainer-push-error.yml
deleted file mode 100644
index 66ab8fbf884..00000000000
--- a/changelogs/unreleased/bvl-fix-maintainer-push-error.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix errors on pushing to an empty repository
-merge_request: 18462
-author:
-type: fixed
diff --git a/changelogs/unreleased/docs-use-variables-deploy-policy-for-staging-and-production.yml b/changelogs/unreleased/docs-use-variables-deploy-policy-for-staging-and-production.yml
new file mode 100644
index 00000000000..aa23a89a175
--- /dev/null
+++ b/changelogs/unreleased/docs-use-variables-deploy-policy-for-staging-and-production.yml
@@ -0,0 +1,6 @@
+---
+title: Add documentation about how to use variables to define deploy policies for
+ staging/production environments
+merge_request: 18675
+author:
+type: other
diff --git a/changelogs/unreleased/feature-display-active-sessions.yml b/changelogs/unreleased/feature-display-active-sessions.yml
new file mode 100644
index 00000000000..14cfa66953e
--- /dev/null
+++ b/changelogs/unreleased/feature-display-active-sessions.yml
@@ -0,0 +1,5 @@
+---
+title: Display active sessions and allow the user to revoke any of it
+merge_request: 17867
+author: Alexis Reigel
+type: added
diff --git a/changelogs/unreleased/feature-show-only-groups-user-is-member-of-in-dashboard.yml b/changelogs/unreleased/feature-show-only-groups-user-is-member-of-in-dashboard.yml
new file mode 100644
index 00000000000..6e2273ed9af
--- /dev/null
+++ b/changelogs/unreleased/feature-show-only-groups-user-is-member-of-in-dashboard.yml
@@ -0,0 +1,5 @@
+---
+title: For group dashboard, we no longer show groups which the visitor is not a member of (this applies to admins and auditors)
+merge_request: 17884
+author: Roger Rüttimann
+type: changed
diff --git a/changelogs/unreleased/fix-inconsistent-protected-branch-pill-baseline.yml b/changelogs/unreleased/fix-inconsistent-protected-branch-pill-baseline.yml
new file mode 100644
index 00000000000..fb6dffaf226
--- /dev/null
+++ b/changelogs/unreleased/fix-inconsistent-protected-branch-pill-baseline.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed inconsistent protected branch pill baseline
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/helm-add-alpine-mirrors.yml b/changelogs/unreleased/helm-add-alpine-mirrors.yml
new file mode 100644
index 00000000000..656c4f911d0
--- /dev/null
+++ b/changelogs/unreleased/helm-add-alpine-mirrors.yml
@@ -0,0 +1,5 @@
+---
+title: Increase cluster applications installer availability using alpine linux mirrors
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/improve-quick-actions-summary-preview.yml b/changelogs/unreleased/improve-quick-actions-summary-preview.yml
new file mode 100644
index 00000000000..bc75c169ad7
--- /dev/null
+++ b/changelogs/unreleased/improve-quick-actions-summary-preview.yml
@@ -0,0 +1,5 @@
+---
+title: Improve quick actions summary preview
+merge_request: 18659
+author: George Tsiolis
+type: changed
diff --git a/changelogs/unreleased/increase-new-issue-metadata-form-margin.yml b/changelogs/unreleased/increase-new-issue-metadata-form-margin.yml
new file mode 100644
index 00000000000..a7196f67969
--- /dev/null
+++ b/changelogs/unreleased/increase-new-issue-metadata-form-margin.yml
@@ -0,0 +1,5 @@
+---
+title: Increase new issue metadata form margin
+merge_request: 18630
+author: George Tsiolis
+type: fixed
diff --git a/changelogs/unreleased/issue_45463.yml b/changelogs/unreleased/issue_45463.yml
deleted file mode 100644
index a350568d04b..00000000000
--- a/changelogs/unreleased/issue_45463.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix users not seeing labels from private groups when being a member of a child project
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/jprovazn-commit-notes-api.yml b/changelogs/unreleased/jprovazn-commit-notes-api.yml
new file mode 100644
index 00000000000..4665d800ccf
--- /dev/null
+++ b/changelogs/unreleased/jprovazn-commit-notes-api.yml
@@ -0,0 +1,5 @@
+---
+title: Add discussion API for merge requests and commits
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/jprovazn-generic-error.yml b/changelogs/unreleased/jprovazn-generic-error.yml
new file mode 100644
index 00000000000..ced3b84fe02
--- /dev/null
+++ b/changelogs/unreleased/jprovazn-generic-error.yml
@@ -0,0 +1,6 @@
+---
+title: Display only generic message on merge error to avoid exposing any potentially
+ sensitive or user unfriendly backend messages.
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/jr-33320-lfs-settings-interface.yml b/changelogs/unreleased/jr-33320-lfs-settings-interface.yml
new file mode 100644
index 00000000000..b39308f5474
--- /dev/null
+++ b/changelogs/unreleased/jr-33320-lfs-settings-interface.yml
@@ -0,0 +1,5 @@
+---
+title: Show group and project LFS settings in the interface to Owners and Masters
+merge_request: 18562
+author:
+type: changed
diff --git a/changelogs/unreleased/jramsay-44880-filter-pipelines-by-sha.yml b/changelogs/unreleased/jramsay-44880-filter-pipelines-by-sha.yml
new file mode 100644
index 00000000000..3654aa28ff4
--- /dev/null
+++ b/changelogs/unreleased/jramsay-44880-filter-pipelines-by-sha.yml
@@ -0,0 +1,5 @@
+---
+title: Add sha filter to pipelines list API
+merge_request: 18125
+author:
+type: changed
diff --git a/changelogs/unreleased/move-notification-service-calls-to-sidekiq.yml b/changelogs/unreleased/move-notification-service-calls-to-sidekiq.yml
new file mode 100644
index 00000000000..b2517884d3c
--- /dev/null
+++ b/changelogs/unreleased/move-notification-service-calls-to-sidekiq.yml
@@ -0,0 +1,5 @@
+---
+title: Compute notification recipients in background jobs
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/osw-use-cached-highlighted-content-for-discussions.yml b/changelogs/unreleased/osw-use-cached-highlighted-content-for-discussions.yml
new file mode 100644
index 00000000000..03a11a3038a
--- /dev/null
+++ b/changelogs/unreleased/osw-use-cached-highlighted-content-for-discussions.yml
@@ -0,0 +1,5 @@
+---
+title: Use persisted diff data instead fetching Git on discussions
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/performance-gb-improve-pipeline-creation-service.yml b/changelogs/unreleased/performance-gb-improve-pipeline-creation-service.yml
new file mode 100644
index 00000000000..bd308f37bec
--- /dev/null
+++ b/changelogs/unreleased/performance-gb-improve-pipeline-creation-service.yml
@@ -0,0 +1,5 @@
+---
+title: Improve performance of a service responsible for creating a pipeline
+merge_request: 18582
+author:
+type: performance
diff --git a/changelogs/unreleased/refactor-move-mr-widget-wip-vue-component.yml b/changelogs/unreleased/refactor-move-mr-widget-wip-vue-component.yml
new file mode 100644
index 00000000000..0f045431aae
--- /dev/null
+++ b/changelogs/unreleased/refactor-move-mr-widget-wip-vue-component.yml
@@ -0,0 +1,5 @@
+---
+title: Move WorkInProgress vue component
+merge_request: 17536
+author: George Tsiolis
+type: performance
diff --git a/changelogs/unreleased/refactor-move-no-tracking-pane-vue-component.yml b/changelogs/unreleased/refactor-move-no-tracking-pane-vue-component.yml
new file mode 100644
index 00000000000..4bb088a1e58
--- /dev/null
+++ b/changelogs/unreleased/refactor-move-no-tracking-pane-vue-component.yml
@@ -0,0 +1,5 @@
+---
+title: Move TimeTrackingNoTrackingPane vue component
+merge_request: 18676
+author: George Tsiolis
+type: performance
diff --git a/changelogs/unreleased/refactor-move-sidebar-time-tracking-vue-component.yml b/changelogs/unreleased/refactor-move-sidebar-time-tracking-vue-component.yml
new file mode 100644
index 00000000000..4f578bfcf26
--- /dev/null
+++ b/changelogs/unreleased/refactor-move-sidebar-time-tracking-vue-component.yml
@@ -0,0 +1,5 @@
+---
+title: Move SidebarTimeTracking vue component
+merge_request: 18677
+author: George Tsiolis
+type: performance
diff --git a/changelogs/unreleased/restore-size-and-position-for-fork-icon.yml b/changelogs/unreleased/restore-size-and-position-for-fork-icon.yml
new file mode 100644
index 00000000000..dd8dad0b17d
--- /dev/null
+++ b/changelogs/unreleased/restore-size-and-position-for-fork-icon.yml
@@ -0,0 +1,5 @@
+---
+title: Fix size and position for fork icon
+merge_request: 18449
+author: George Tsiolis
+type: changed
diff --git a/changelogs/unreleased/revert-discussion-counter-height.yml b/changelogs/unreleased/revert-discussion-counter-height.yml
new file mode 100644
index 00000000000..331ff997009
--- /dev/null
+++ b/changelogs/unreleased/revert-discussion-counter-height.yml
@@ -0,0 +1,5 @@
+---
+title: Revert discussion counter height
+merge_request: 18656
+author: George Tsiolis
+type: changed
diff --git a/changelogs/unreleased/security-45689-fix-archive-cache-bug.yml b/changelogs/unreleased/security-45689-fix-archive-cache-bug.yml
new file mode 100644
index 00000000000..0103a7fc430
--- /dev/null
+++ b/changelogs/unreleased/security-45689-fix-archive-cache-bug.yml
@@ -0,0 +1,5 @@
+---
+title: Serve archive requests with the correct file in all cases
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security_issue_42029.yml b/changelogs/unreleased/security_issue_42029.yml
new file mode 100644
index 00000000000..0772e33f930
--- /dev/null
+++ b/changelogs/unreleased/security_issue_42029.yml
@@ -0,0 +1,5 @@
+---
+title: Sanitizes user name to avoid XSS attacks
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/update-doorkeeper-changelog.yml b/changelogs/unreleased/update-doorkeeper-changelog.yml
deleted file mode 100644
index b47bdf4a28d..00000000000
--- a/changelogs/unreleased/update-doorkeeper-changelog.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update doorkeeper to 4.3.2 to fix GitLab OAuth authentication
-merge_request: 18543
-author:
-type: fixed
diff --git a/changelogs/unreleased/update-timeline-icon-for-description-edit.yml b/changelogs/unreleased/update-timeline-icon-for-description-edit.yml
new file mode 100644
index 00000000000..560db00e503
--- /dev/null
+++ b/changelogs/unreleased/update-timeline-icon-for-description-edit.yml
@@ -0,0 +1,5 @@
+---
+title: Update timeline icon for description edit
+merge_request: 18633
+author: George Tsiolis
+type: changed
diff --git a/changelogs/unreleased/winh-dashboard-any-milestone.yml b/changelogs/unreleased/winh-dashboard-any-milestone.yml
new file mode 100644
index 00000000000..49eecd3da2b
--- /dev/null
+++ b/changelogs/unreleased/winh-dashboard-any-milestone.yml
@@ -0,0 +1,5 @@
+---
+title: Reset milestone filter when clicking "Any Milestone" in dashboard
+merge_request: 18531
+author:
+type: fixed
diff --git a/changelogs/unreleased/winh-new-mergerequest-branch-picker.yml b/changelogs/unreleased/winh-new-mergerequest-branch-picker.yml
new file mode 100644
index 00000000000..401ecd09ef2
--- /dev/null
+++ b/changelogs/unreleased/winh-new-mergerequest-branch-picker.yml
@@ -0,0 +1,5 @@
+---
+title: Load branches on new merge request page asynchronously
+merge_request: 18315
+author:
+type: changed
diff --git a/changelogs/unreleased/zj-namespace-service-mandatory.yml b/changelogs/unreleased/zj-namespace-service-mandatory.yml
new file mode 100644
index 00000000000..d890741c51b
--- /dev/null
+++ b/changelogs/unreleased/zj-namespace-service-mandatory.yml
@@ -0,0 +1,5 @@
+---
+title: Finish NamespaceService migration to Gitaly
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/zj-repo-checksum-opt-out.yml b/changelogs/unreleased/zj-repo-checksum-opt-out.yml
new file mode 100644
index 00000000000..98dfedf7475
--- /dev/null
+++ b/changelogs/unreleased/zj-repo-checksum-opt-out.yml
@@ -0,0 +1,5 @@
+---
+title: Compute Gitlab::Git::Repository#checksum on Gitaly by default
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/zj-repository-exist-mandatory.yml b/changelogs/unreleased/zj-repository-exist-mandatory.yml
new file mode 100644
index 00000000000..7d83446e90f
--- /dev/null
+++ b/changelogs/unreleased/zj-repository-exist-mandatory.yml
@@ -0,0 +1,5 @@
+---
+title: Repository#exists? is always executed through Gitaly
+merge_request:
+author:
+type: performance
diff --git a/config/application.rb b/config/application.rb
index ad7338763f7..09f706e3d70 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -115,6 +115,7 @@ module Gitlab
config.assets.precompile << "test.css"
config.assets.precompile << "snippets.css"
config.assets.precompile << "locale/**/app.js"
+ config.assets.precompile << "emoji_sprites.css"
# Import gitlab-svgs directly from vendored directory
config.assets.paths << "#{config.root}/node_modules/@gitlab-org/gitlab-svgs/dist"
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 8c39a1f2aa9..7eb44b8059e 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -184,18 +184,18 @@ production: &base
# base_dir: uploads/-/system
object_store:
enabled: false
- # remote_directory: uploads # Bucket name
+ remote_directory: uploads # Bucket name
# direct_upload: false # Use Object Storage directly for uploads instead of background uploads if enabled (Default: false)
# background_upload: false # Temporary option to limit automatic upload (Default: true)
# proxy_download: false # Passthrough all downloads via GitLab instead of using Redirects to Object Storage
- connection:
- provider: AWS
- aws_access_key_id: AWS_ACCESS_KEY_ID
- aws_secret_access_key: AWS_SECRET_ACCESS_KEY
- region: us-east-1
- # host: 'localhost' # default: s3.amazonaws.com
- # endpoint: 'http://127.0.0.1:9000' # default: nil
- # path_style: true # Use 'host/bucket_name/object' instead of 'bucket_name.host/object'
+ connection:
+ provider: AWS
+ aws_access_key_id: AWS_ACCESS_KEY_ID
+ aws_secret_access_key: AWS_SECRET_ACCESS_KEY
+ region: us-east-1
+ # host: 'localhost' # default: s3.amazonaws.com
+ # endpoint: 'http://127.0.0.1:9000' # default: nil
+ # path_style: true # Use 'host/bucket_name/object' instead of 'bucket_name.host/object'
## GitLab Pages
pages:
@@ -212,6 +212,8 @@ production: &base
artifacts_server: true
# external_http: ["1.1.1.1:80", "[2001::1]:80"] # If defined, enables custom domain support in GitLab Pages
# external_https: ["1.1.1.1:443", "[2001::1]:443"] # If defined, enables custom domain and certificate support in GitLab Pages
+ admin:
+ address: unix:/home/git/gitlab/tmp/sockets/private/pages-admin.socket # TCP connections are supported too (e.g. tcp://host:port)
## Mattermost
## For enabling Add to Mattermost button
@@ -532,7 +534,7 @@ production: &base
# required_claims: ["name", "email"],
# info_map: { name: "name", email: "email" },
# auth_url: 'https://example.com/',
- # valid_within: nil,
+ # valid_within: null,
# }
# }
# - { name: 'saml',
@@ -823,7 +825,7 @@ test:
required_claims: ["name", "email"],
info_map: { name: "name", email: "email" },
auth_url: 'https://example.com/',
- valid_within: nil,
+ valid_within: null,
}
}
- { name: 'auth0',
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 575f27d1ea9..5248bd858a0 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -215,6 +215,9 @@ Settings.pages['external_http'] ||= false unless Settings.pages['external_ht
Settings.pages['external_https'] ||= false unless Settings.pages['external_https'].present?
Settings.pages['artifacts_server'] ||= Settings.pages['enabled'] if Settings.pages['artifacts_server'].nil?
+Settings.pages['admin'] ||= Settingslogic.new({})
+Settings.pages.admin['certificate'] ||= ''
+
#
# Git LFS
#
diff --git a/config/initializers/fast_gettext.rb b/config/initializers/9_fast_gettext.rb
index fd0167aa476..fd0167aa476 100644
--- a/config/initializers/fast_gettext.rb
+++ b/config/initializers/9_fast_gettext.rb
diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb
index 2079d3acb72..e3a342590d4 100644
--- a/config/initializers/doorkeeper.rb
+++ b/config/initializers/doorkeeper.rb
@@ -104,5 +104,5 @@ Doorkeeper.configure do
# set to true if you want this to be allowed
# wildcard_redirect_uri false
- base_controller 'ApplicationController'
+ base_controller '::Gitlab::BaseDoorkeeperController'
end
diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb
index 00baea08613..e33ebb25c4c 100644
--- a/config/initializers/omniauth.rb
+++ b/config/initializers/omniauth.rb
@@ -25,5 +25,6 @@ end
module OmniAuth
module Strategies
autoload :Bitbucket, Rails.root.join('lib', 'omni_auth', 'strategies', 'bitbucket')
+ autoload :Jwt, Rails.root.join('lib', 'omni_auth', 'strategies', 'jwt')
end
end
diff --git a/config/initializers/pages.rb b/config/initializers/pages.rb
new file mode 100644
index 00000000000..835197557e8
--- /dev/null
+++ b/config/initializers/pages.rb
@@ -0,0 +1,2 @@
+Gitlab::PagesClient.read_or_create_token
+Gitlab::PagesClient.load_certificate
diff --git a/config/initializers/peek.rb b/config/initializers/peek.rb
index ba04a2bf5fa..bc9b52ceef7 100644
--- a/config/initializers/peek.rb
+++ b/config/initializers/peek.rb
@@ -1,7 +1,6 @@
Rails.application.config.peek.adapter = :redis, { client: ::Redis.new(Gitlab::Redis::Cache.params) }
Peek.into Peek::Views::Host
-Peek.into Peek::Views::PerformanceBar
if Gitlab::Database.mysql?
require 'peek-mysql2'
diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb
index f2fde1e0048..da24881885e 100644
--- a/config/initializers/session_store.rb
+++ b/config/initializers/session_store.rb
@@ -15,19 +15,15 @@ cookie_key = if Rails.env.development?
"_gitlab_session"
end
-if Rails.env.test?
- Gitlab::Application.config.session_store :cookie_store, key: "_gitlab_session"
-else
- sessions_config = Gitlab::Redis::SharedState.params
- sessions_config[:namespace] = Gitlab::Redis::SharedState::SESSION_NAMESPACE
+sessions_config = Gitlab::Redis::SharedState.params
+sessions_config[:namespace] = Gitlab::Redis::SharedState::SESSION_NAMESPACE
- Gitlab::Application.config.session_store(
- :redis_store, # Using the cookie_store would enable session replay attacks.
- servers: sessions_config,
- key: cookie_key,
- secure: Gitlab.config.gitlab.https,
- httponly: true,
- expires_in: Settings.gitlab['session_expire_delay'] * 60,
- path: Rails.application.config.relative_url_root.nil? ? '/' : Gitlab::Application.config.relative_url_root
- )
-end
+Gitlab::Application.config.session_store(
+ :redis_store, # Using the cookie_store would enable session replay attacks.
+ servers: sessions_config,
+ key: cookie_key,
+ secure: Gitlab.config.gitlab.https,
+ httponly: true,
+ expires_in: Settings.gitlab['session_expire_delay'] * 60,
+ path: Rails.application.config.relative_url_root.nil? ? '/' : Gitlab::Application.config.relative_url_root
+)
diff --git a/config/initializers/warden.rb b/config/initializers/warden.rb
index ee034d21eae..bf079f8e1a7 100644
--- a/config/initializers/warden.rb
+++ b/config/initializers/warden.rb
@@ -6,4 +6,16 @@ Rails.application.configure do |config|
Warden::Manager.before_failure do |env, opts|
Gitlab::Auth::BlockedUserTracker.log_if_user_blocked(env)
end
+
+ Warden::Manager.after_authentication do |user, auth, opts|
+ ActiveSession.cleanup(user)
+ end
+
+ Warden::Manager.after_set_user only: :fetch do |user, auth, opts|
+ ActiveSession.set(user, auth.request)
+ end
+
+ Warden::Manager.before_logout do |user, auth, opts|
+ ActiveSession.destroy(user || auth.user, auth.request.session.id)
+ end
end
diff --git a/config/karma.config.js b/config/karma.config.js
index 691cda98861..3eb220eed99 100644
--- a/config/karma.config.js
+++ b/config/karma.config.js
@@ -1,8 +1,16 @@
-var path = require('path');
-var webpack = require('webpack');
-var argumentsParser = require('commander');
-var webpackConfig = require('./webpack.config.js');
-var ROOT_PATH = path.resolve(__dirname, '..');
+const path = require('path');
+const glob = require('glob');
+const chalk = require('chalk');
+const webpack = require('webpack');
+const argumentsParser = require('commander');
+const webpackConfig = require('./webpack.config.js');
+
+const ROOT_PATH = path.resolve(__dirname, '..');
+
+function fatalError(message) {
+ console.error(chalk.red(`\nError: ${message}\n`));
+ process.exit(1);
+}
// remove problematic plugins
if (webpackConfig.plugins) {
@@ -15,33 +23,70 @@ if (webpackConfig.plugins) {
});
}
-var testFiles = argumentsParser
+const specFilters = argumentsParser
.option(
'-f, --filter-spec [filter]',
'Filter run spec files by path. Multiple filters are like a logical OR.',
- (val, memo) => {
- memo.push(val);
+ (filter, memo) => {
+ memo.push(filter, filter.replace(/\/?$/, '/**/*.js'));
return memo;
},
[]
)
.parse(process.argv).filterSpec;
-webpackConfig.plugins.push(
- new webpack.DefinePlugin({
- 'process.env.TEST_FILES': JSON.stringify(testFiles),
- })
-);
+if (specFilters.length) {
+ const specsPath = /^(?:\.[\\\/])?spec[\\\/]javascripts[\\\/]/;
+
+ // resolve filters
+ let filteredSpecFiles = specFilters.map(filter =>
+ glob
+ .sync(filter, {
+ root: ROOT_PATH,
+ matchBase: true,
+ })
+ .filter(path => path.endsWith('spec.js'))
+ );
+
+ // flatten
+ filteredSpecFiles = Array.prototype.concat.apply([], filteredSpecFiles);
+
+ // remove duplicates
+ filteredSpecFiles = [...new Set(filteredSpecFiles)];
+
+ if (filteredSpecFiles.length < 1) {
+ fatalError('Your filter did not match any test files.');
+ }
+
+ if (!filteredSpecFiles.every(file => specsPath.test(file))) {
+ fatalError('Test files must be located within /spec/javascripts.');
+ }
+
+ const newContext = filteredSpecFiles.reduce((context, file) => {
+ const relativePath = file.replace(specsPath, '');
+ context[file] = `./${relativePath}`;
+ return context;
+ }, {});
+
+ webpackConfig.plugins.push(
+ new webpack.ContextReplacementPlugin(
+ /spec[\\\/]javascripts$/,
+ path.join(ROOT_PATH, 'spec/javascripts'),
+ newContext
+ )
+ );
+}
-webpackConfig.devtool = process.env.BABEL_ENV !== 'coverage' && 'cheap-inline-source-map';
+webpackConfig.entry = undefined;
+webpackConfig.devtool = 'cheap-inline-source-map';
// Karma configuration
module.exports = function(config) {
process.env.TZ = 'Etc/UTC';
- var progressReporter = process.env.CI ? 'mocha' : 'progress';
+ const progressReporter = process.env.CI ? 'mocha' : 'progress';
- var karmaConfig = {
+ const karmaConfig = {
basePath: ROOT_PATH,
browsers: ['ChromeHeadlessCustom'],
customLaunchers: {
diff --git a/config/routes/profile.rb b/config/routes/profile.rb
index bcfc17a5f66..a9ba5ac2c0b 100644
--- a/config/routes/profile.rb
+++ b/config/routes/profile.rb
@@ -30,6 +30,7 @@ resource :profile, only: [:show, :update] do
put :revoke
end
end
+ resources :active_sessions, only: [:index, :destroy]
resources :emails, only: [:index, :create, :destroy] do
member do
put :resend_confirmation_instructions
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 382d5b1e3c7..7fffd16f3cf 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -161,7 +161,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
get :diff_for_path
- get :update_branches
get :branch_from
get :branch_to
end
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 39e9fbbd530..b9d098ff9b9 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -69,6 +69,9 @@ const config = {
test: /\.js$/,
exclude: /(node_modules|vendor\/assets)/,
loader: 'babel-loader',
+ options: {
+ cacheDirectory: path.join(ROOT_PATH, 'tmp/cache/babel-loader'),
+ },
},
{
test: /\.vue$/,
diff --git a/db/migrate/20180417101040_add_tmp_stage_priority_index_to_ci_builds.rb b/db/migrate/20180417101040_add_tmp_stage_priority_index_to_ci_builds.rb
new file mode 100644
index 00000000000..ee82c70ecf8
--- /dev/null
+++ b/db/migrate/20180417101040_add_tmp_stage_priority_index_to_ci_builds.rb
@@ -0,0 +1,16 @@
+class AddTmpStagePriorityIndexToCiBuilds < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index(:ci_builds, [:stage_id, :stage_idx],
+ where: 'stage_idx IS NOT NULL', name: 'tmp_build_stage_position_index')
+ end
+
+ def down
+ remove_concurrent_index_by_name(:ci_builds, 'tmp_build_stage_position_index')
+ end
+end
diff --git a/db/migrate/20180417101940_add_index_to_ci_stage.rb b/db/migrate/20180417101940_add_index_to_ci_stage.rb
new file mode 100644
index 00000000000..9dac78db774
--- /dev/null
+++ b/db/migrate/20180417101940_add_index_to_ci_stage.rb
@@ -0,0 +1,9 @@
+class AddIndexToCiStage < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :ci_stages, :position, :integer
+ end
+end
diff --git a/db/post_migrate/20180420080616_schedule_stages_index_migration.rb b/db/post_migrate/20180420080616_schedule_stages_index_migration.rb
new file mode 100644
index 00000000000..1d0daad002f
--- /dev/null
+++ b/db/post_migrate/20180420080616_schedule_stages_index_migration.rb
@@ -0,0 +1,29 @@
+class ScheduleStagesIndexMigration < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ MIGRATION = 'MigrateStageIndex'.freeze
+ BATCH_SIZE = 10000
+
+ disable_ddl_transaction!
+
+ class Stage < ActiveRecord::Base
+ include EachBatch
+ self.table_name = 'ci_stages'
+ end
+
+ def up
+ disable_statement_timeout
+
+ Stage.all.tap do |relation|
+ queue_background_migration_jobs_by_range_at_intervals(relation,
+ MIGRATION,
+ 5.minutes,
+ batch_size: BATCH_SIZE)
+ end
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 8e79469d956..c88d6f3f9e9 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -322,6 +322,7 @@ ActiveRecord::Schema.define(version: 20180430143705) do
add_index "ci_builds", ["project_id", "id"], name: "index_ci_builds_on_project_id_and_id", using: :btree
add_index "ci_builds", ["protected"], name: "index_ci_builds_on_protected", using: :btree
add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree
+ add_index "ci_builds", ["stage_id", "stage_idx"], name: "tmp_build_stage_position_index", where: "(stage_idx IS NOT NULL)", using: :btree
add_index "ci_builds", ["stage_id"], name: "index_ci_builds_on_stage_id", using: :btree
add_index "ci_builds", ["status", "type", "runner_id"], name: "index_ci_builds_on_status_and_type_and_runner_id", using: :btree
add_index "ci_builds", ["status"], name: "index_ci_builds_on_status", using: :btree
@@ -495,6 +496,7 @@ ActiveRecord::Schema.define(version: 20180430143705) do
t.string "name"
t.integer "status"
t.integer "lock_version"
+ t.integer "position"
end
add_index "ci_stages", ["pipeline_id", "name"], name: "index_ci_stages_on_pipeline_id_and_name", unique: true, using: :btree
diff --git a/doc/README.md b/doc/README.md
index a841a4cfbf1..a2e152ce383 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -15,8 +15,8 @@ To understand what features you have access to, check the [GitLab subscriptions]
| General documentation | GitLab CI/CD docs |
| :----- | :----- |
-| [User documentation](user/index.md) | [GitLab CI/CD](ci/README.md) |
-| [Administrator documentation](administration/index.md) | [GitLab CI/CD quick start guide](ci/quick_start/README.md) |
+| [User documentation](user/index.md) | [GitLab CI/CD quick start guide](ci/quick_start/README.md) |
+| [Administrator documentation](administration/index.md) | [GitLab CI/CD examples](ci/examples/README.md) |
| [Contributor documentation](#contributor-documentation) | [Configuring `.gitlab-ci.yml`](ci/yaml/README.md) |
| [Getting started with GitLab](#getting-started-with-gitlab) | [Using Docker images](ci/docker/using_docker_images.md) |
| [API](api/README.md) | [Auto DevOps](topics/autodevops/index.md) |
@@ -90,6 +90,7 @@ Manage your [repositories](user/project/repository/index.md) from the UI (user i
- [Create a file](user/project/repository/web_editor.md#create-a-file)
- [Upload a file](user/project/repository/web_editor.md#upload-a-file)
- [File templates](user/project/repository/web_editor.md#template-dropdowns)
+ - [Jupyter Notebook files](user/project/repository/index.md#jupyter-notebook-files)
- [Create a directory](user/project/repository/web_editor.md#create-a-directory)
- [Start a merge request](user/project/repository/web_editor.md#tips) (when committing via UI)
- [Branches](user/project/repository/branches/index.md)
@@ -100,6 +101,14 @@ Manage your [repositories](user/project/repository/index.md) from the UI (user i
- [Commits](user/project/repository/index.md#commits)
- [Signing commits](user/project/repository/gpg_signed_commits/index.md): use GPG to sign your commits.
+#### Merge Requests
+
+- [Merge Requests](user/project/merge_requests/index.md)
+ - [Work In Progress "WIP" Merge Requests](user/project/merge_requests/work_in_progress_merge_requests.md)
+ - [Merge Request discussion resolution](user/discussions/index.md#moving-a-single-discussion-to-a-new-issue): Resolve discussions, move discussions in a merge request to an issue, only allow merge requests to be merged if all discussions are resolved.
+ - [Checkout merge requests locally](user/project/merge_requests/index.md#checkout-merge-requests-locally)
+ - [Cherry-pick](user/project/merge_requests/cherry_pick_changes.md)
+
#### Integrations
- [Project Services](user/project/integrations/project_services.md): Integrate a project with external services, such as CI and chat.
@@ -113,18 +122,16 @@ Manage your [repositories](user/project/repository/index.md) from the UI (user i
### Verify
-Spot errors sooner and shorten feedback cycles with built-in code review, code testing,
-Code Quality, and Review Apps. Customize your approval workflow controls, automatically
-test the quality of your code, and spin up a staging environment for every code change.
-GitLab Continuous Integration is the most popular next generation testing system that
-auto scales to run your tests faster.
+Spot errors sooner, improve security and shorten feedback cycles with built-in
+static code analysis, code testing, code quality, dependency checking and review
+apps. Customize your approval workflow controls, automatically test the quality
+of your code, and spin up a staging environment for every code change. GitLab
+Continuous Integration is the most popular next generation testing system that
+scales to run your tests faster.
-- [Merge Requests](user/project/merge_requests/index.md)
- - [Work In Progress Merge Requests](user/project/merge_requests/work_in_progress_merge_requests.md)
- - [Merge Request discussion resolution](user/discussions/index.md#moving-a-single-discussion-to-a-new-issue): Resolve discussions, move discussions in a merge request to an issue, only allow merge requests to be merged if all discussions are resolved.
- - [Checkout merge requests locally](user/project/merge_requests/index.md#checkout-merge-requests-locally)
- - [Cherry-pick](user/project/merge_requests/cherry_pick_changes.md)
+- [GitLab CI/CD](ci/README.md): Explore the features and capabilities of Continuous Integration, Continuous Delivery, and Continuous Deployment with GitLab.
- [Review Apps](ci/review_apps/index.md): Preview changes to your app right from a merge request.
+- [Pipeline Graphs](ci/pipelines.md#pipeline-graphs)
### Package
@@ -132,7 +139,6 @@ GitLab Container Registry gives you the enhanced security and access controls of
custom Docker images without 3rd party add-ons. Easily upload and download images
from GitLab CI/CD with full Git repository management integration.
-- [GitLab CI/CD](ci/README.md): Explore the features and capabilities of Continuous Integration, Continuous Delivery, and Continuous Deployment with GitLab.
- [GitLab Container Registry](user/project/container_registry.md): Learn how to use GitLab's built-in Container Registry.
### Release
@@ -141,9 +147,11 @@ Spend less time configuring your tools, and more time creating. Whether you’re
deploying to one server or thousands, build, test, and release your code
confidently and securely with GitLab’s built-in Continuous Delivery and Deployment.
-- [GitLab Pages](user/project/pages/index.md): Build, test, and deploy a static site directly from GitLab.
- [Auto Deploy](topics/autodevops/index.md#auto-deploy): Configure GitLab CI for the deployment of your application.
- [Environments and deployments](ci/environments.md): With environments, you can control the continuous deployment of your software within GitLab.
+- [GitLab Pages](user/project/pages/index.md): Build, test, and deploy a static site directly from GitLab.
+- [Scheduled Pipelines](user/project/pipelines/schedules.md)
+- [Protected Runners](ci/runners/README.md#protected-runners)
### Configure
@@ -152,6 +160,9 @@ Auto Devops. Best practice templates get you started with minimal to zero
configuration. Then customize everything from buildpacks to CI/CD.
- [Auto DevOps](topics/autodevops/index.md)
+- [Deployment of Helm, Ingress, and Prometheus on Kubernetes](user/project/clusters/index.md#installing-applications)
+- [Protected secret variables](ci/variables/README.md#protected-secret-variables)
+- [Easy creation of Kubernetes clusters on GKE](user/project/clusters/index.md#adding-and-creating-a-new-gke-cluster-via-gitlab)
### Monitor
diff --git a/doc/administration/auth/jwt.md b/doc/administration/auth/jwt.md
index b51e705ab52..8b00f52ffc1 100644
--- a/doc/administration/auth/jwt.md
+++ b/doc/administration/auth/jwt.md
@@ -50,7 +50,7 @@ JWT will provide you with a secret key for you to use.
required_claims: ["name", "email"],
info_map: { name: "name", email: "email" },
auth_url: 'https://example.com/',
- valid_within: nil,
+ valid_within: null,
}
}
```
diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md
index 430f865f1e7..031fb31ca4f 100644
--- a/doc/administration/high_availability/redis.md
+++ b/doc/administration/high_availability/redis.md
@@ -323,7 +323,7 @@ The prerequisites for a HA Redis setup are the following:
# machines to connect to it.
redis['port'] = 6379
- # The same password for Redeis authentication you set up for the master node.
+ # The same password for Redis authentication you set up for the master node.
redis['password'] = 'redis-password-goes-here'
# The IP of the master Redis node.
diff --git a/doc/administration/job_artifacts.md b/doc/administration/job_artifacts.md
index 896cb93e5ed..77fe4d561a1 100644
--- a/doc/administration/job_artifacts.md
+++ b/doc/administration/job_artifacts.md
@@ -107,7 +107,7 @@ For source installations the following settings are nested under `artifacts:` an
| Setting | Description | Default |
|---------|-------------|---------|
| `enabled` | Enable/disable object storage | `false` |
-| `remote_directory` | The bucket name where Artfacts will be stored| |
+| `remote_directory` | The bucket name where Artifacts will be stored| |
| `direct_upload` | Set to true to enable direct upload of Artifacts without the need of local shared storage. Option may be removed once we decide to support only single storage for all files. Currently only `Google` provider is supported | `false` |
| `background_upload` | Set to false to disable automatic upload. Option may be removed once upload is direct to S3 | `true` |
| `proxy_download` | Set to true to enable proxying all files served. Option allows to reduce egress traffic as this allows clients to download directly from remote storage instead of proxying all data | `false` |
@@ -148,7 +148,7 @@ _The artifacts are stored by default in
```
NOTE: For GitLab 9.4+, if you are using AWS IAM profiles, be sure to omit the
- AWS access key and secret acces key/value pairs. For example:
+ AWS access key and secret access key/value pairs. For example:
```ruby
gitlab_rails['artifacts_object_store_connection'] = {
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index f495990d9a4..69600cad25c 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -46,7 +46,7 @@ In this experimental phase, only a few metrics are available:
| redis_ping_latency_seconds | Gauge | 9.4 | Round trip time of the redis ping |
| user_session_logins_total | Counter | 9.4 | Counter of how many users have logged in |
| filesystem_circuitbreaker_latency_seconds | Gauge | 9.5 | Time spent validating if a storage is accessible |
-| filesystem_circuitbreaker | Gauge | 9.5 | Wether or not the circuit for a certain shard is broken or not |
+| filesystem_circuitbreaker | Gauge | 9.5 | Whether or not the circuit for a certain shard is broken or not |
| circuitbreaker_storage_check_duration_seconds | Histogram | 10.3 | Time a single storage probe took |
## Metrics shared directory
diff --git a/doc/administration/operations/fast_ssh_key_lookup.md b/doc/administration/operations/fast_ssh_key_lookup.md
index bd6c7bb07b5..89331238ce4 100644
--- a/doc/administration/operations/fast_ssh_key_lookup.md
+++ b/doc/administration/operations/fast_ssh_key_lookup.md
@@ -31,7 +31,7 @@ GitLab Shell provides a way to authorize SSH users via a fast, indexed lookup
to the GitLab database. GitLab Shell uses the fingerprint of the SSH key to
check whether the user is authorized to access GitLab.
-Add the following to your `sshd_config` file. This is usuaully located at
+Add the following to your `sshd_config` file. This is usually located at
`/etc/ssh/sshd_config`, but it will be `/assets/sshd_config` if you're using
Omnibus Docker:
diff --git a/doc/administration/uploads.md b/doc/administration/uploads.md
index 2fa3284b6be..7f0bd8f04e3 100644
--- a/doc/administration/uploads.md
+++ b/doc/administration/uploads.md
@@ -104,7 +104,7 @@ _The uploads are stored by default in
```
>**Note:**
-If you are using AWS IAM profiles, be sure to omit the AWS access key and secret acces key/value pairs.
+If you are using AWS IAM profiles, be sure to omit the AWS access key and secret access key/value pairs.
```ruby
gitlab_rails['uploads_object_store_connection'] = {
diff --git a/doc/api/README.md b/doc/api/README.md
index 9879c667150..e777fc63d2b 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -86,6 +86,29 @@ have been resolved to our satisfaction by the relicensing of the reference
implementations under MIT, and the use of the OWF license for the GraphQL
specification.
+## Compatibility Guidelines
+
+The HTTP API is versioned using a single number, the current one being 4. This
+number symbolises the same as the major version number as described by
+[SemVer](https://semver.org/). This mean that backward incompatible changes
+will require this version number to change. However, the minor version is
+not explicit. This allows for a stable API endpoint, but also means new
+features can be added to the API in the same version number.
+
+New features and bug fixes are released in tandem with a new GitLab, and apart
+from incidental patch and security releases, are released on the 22nd each
+month. Backward incompatible changes (e.g. endpoints removal, parameters
+removal etc.), as well as removal of entire API versions are done in tandem
+with a major point release of GitLab itself. All deprecations and changes
+between two versions should be listed in the documentation. For the changes
+between v3 and v4; please read the [v3 to v4 documentation](v3_to_v4.md)
+
+#### Current status
+
+Currently two API versions are available, v3 and v4. v3 is deprecated and
+will soon be removed. Deletion is scheduled for
+[GitLab 11.0](https://gitlab.com/gitlab-org/gitlab-ce/issues/36819).
+
## Basic usage
API requests should be prefixed with `api` and the API version. The API version
@@ -270,7 +293,7 @@ The following table gives an overview of how the API functions generally behave.
| `GET` | Access one or more resources and return the result as JSON. |
| `POST` | Return `201 Created` if the resource is successfully created and return the newly created resource as JSON. |
| `GET` / `PUT` | Return `200 OK` if the resource is accessed or modified successfully. The (modified) result is returned as JSON. |
-| `DELETE` | Returns `204 No Content` if the resuource was deleted successfully. |
+| `DELETE` | Returns `204 No Content` if the resource was deleted successfully. |
The following table shows the possible return codes for API requests.
diff --git a/doc/api/discussions.md b/doc/api/discussions.md
index c341b7f2009..65e2f9d6cd9 100644
--- a/doc/api/discussions.md
+++ b/doc/api/discussions.md
@@ -1,6 +1,6 @@
# Discussions API
-Discussions are set of related notes on snippets or issues.
+Discussions are set of related notes on snippets, issues, merge requests or commits.
## Issues
@@ -61,7 +61,8 @@ GET /projects/:id/issues/:issue_iid/discussions
"system": false,
"noteable_id": 3,
"noteable_type": "Issue",
- "noteable_iid": null
+ "noteable_iid": null,
+ "resolvable": false
}
]
},
@@ -87,7 +88,8 @@ GET /projects/:id/issues/:issue_iid/discussions
"system": false,
"noteable_id": 3,
"noteable_type": "Issue",
- "noteable_iid": null
+ "noteable_iid": null,
+ "resolvable": false
}
]
}
@@ -265,7 +267,8 @@ GET /projects/:id/snippets/:snippet_id/discussions
"system": false,
"noteable_id": 3,
"noteable_type": "Snippet",
- "noteable_id": null
+ "noteable_id": null,
+ "resolvable": false
}
]
},
@@ -291,7 +294,8 @@ GET /projects/:id/snippets/:snippet_id/discussions
"system": false,
"noteable_id": 3,
"noteable_type": "Snippet",
- "noteable_id": null
+ "noteable_id": null,
+ "resolvable": false
}
]
}
@@ -409,3 +413,574 @@ Parameters:
```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/snippets/11/discussions/636
```
+
+## Merge requests
+
+### List project merge request discussions
+
+Gets a list of all discussions for a single merge request.
+
+```
+GET /projects/:id/merge_requests/:merge_request_iid/discussions
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | ------------ |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `merge_request_iid` | integer | yes | The IID of a merge request |
+
+```json
+[
+ {
+ "id": "6a9c1750b37d513a43987b574953fceb50b03ce7",
+ "individual_note": false,
+ "notes": [
+ {
+ "id": 1126,
+ "type": "DiscussionNote",
+ "body": "discussion text",
+ "attachment": null,
+ "author": {
+ "id": 1,
+ "name": "root",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/00afb8fb6ab07c3ee3e9c1f38777e2f4?s=80&d=identicon",
+ "web_url": "http://localhost:3000/root"
+ },
+ "created_at": "2018-03-03T21:54:39.668Z",
+ "updated_at": "2018-03-03T21:54:39.668Z",
+ "system": false,
+ "noteable_id": 3,
+ "noteable_type": "Merge request",
+ "noteable_iid": null,
+ "resolved": false,
+ "resolvable": true,
+ "resolved_by": null
+ },
+ {
+ "id": 1129,
+ "type": "DiscussionNote",
+ "body": "reply to the discussion",
+ "attachment": null,
+ "author": {
+ "id": 1,
+ "name": "root",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/00afb8fb6ab07c3ee3e9c1f38777e2f4?s=80&d=identicon",
+ "web_url": "http://localhost:3000/root"
+ },
+ "created_at": "2018-03-04T13:38:02.127Z",
+ "updated_at": "2018-03-04T13:38:02.127Z",
+ "system": false,
+ "noteable_id": 3,
+ "noteable_type": "Merge request",
+ "noteable_iid": null,
+ "resolved": false,
+ "resolvable": true,
+ "resolved_by": null
+ }
+ ]
+ },
+ {
+ "id": "87805b7c09016a7058e91bdbe7b29d1f284a39e6",
+ "individual_note": true,
+ "notes": [
+ {
+ "id": 1128,
+ "type": null,
+ "body": "a single comment",
+ "attachment": null,
+ "author": {
+ "id": 1,
+ "name": "root",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/00afb8fb6ab07c3ee3e9c1f38777e2f4?s=80&d=identicon",
+ "web_url": "http://localhost:3000/root"
+ },
+ "created_at": "2018-03-04T09:17:22.520Z",
+ "updated_at": "2018-03-04T09:17:22.520Z",
+ "system": false,
+ "noteable_id": 3,
+ "noteable_type": "Merge request",
+ "noteable_iid": null,
+ "resolved": false,
+ "resolvable": true,
+ "resolved_by": null
+ }
+ ]
+ }
+]
+```
+
+Diff comments contain also position:
+
+```json
+[
+ {
+ "id": "87805b7c09016a7058e91bdbe7b29d1f284a39e6",
+ "individual_note": false,
+ "notes": [
+ {
+ "id": 1128,
+ "type": DiffNote,
+ "body": "diff comment",
+ "attachment": null,
+ "author": {
+ "id": 1,
+ "name": "root",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/00afb8fb6ab07c3ee3e9c1f38777e2f4?s=80&d=identicon",
+ "web_url": "http://localhost:3000/root"
+ },
+ "created_at": "2018-03-04T09:17:22.520Z",
+ "updated_at": "2018-03-04T09:17:22.520Z",
+ "system": false,
+ "noteable_id": 3,
+ "noteable_type": "Merge request",
+ "noteable_iid": null,
+ "position": {
+ "base_sha": "b5d6e7b1613fca24d250fa8e5bc7bcc3dd6002ef",
+ "start_sha": "7c9c2ead8a320fb7ba0b4e234bd9529a2614e306",
+ "head_sha": "4803c71e6b1833ca72b8b26ef2ecd5adc8a38031",
+ "old_path": "package.json",
+ "new_path": "package.json",
+ "position_type": "text",
+ "old_line": 27,
+ "new_line": 27
+ },
+ "resolved": false,
+ "resolvable": true,
+ "resolved_by": null
+ }
+ ]
+ }
+]
+```
+
+```bash
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions
+```
+
+### Get single merge request discussion
+
+Returns a single discussion for a specific project merge request
+
+```
+GET /projects/:id/merge_requests/:merge_request_iid/discussions/:discussion_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| ------------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `merge_request_iid` | integer | yes | The IID of a merge request |
+| `discussion_id` | integer | yes | The ID of a discussion |
+
+```bash
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7
+```
+
+### Create new merge request discussion
+
+Creates a new discussion to a single project merge request. This is similar to creating
+a note but but another comments (replies) can be added to it later.
+
+```
+POST /projects/:id/merge_requests/:merge_request_iid/discussions
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| ------------------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `merge_request_iid` | integer | yes | The IID of a merge request |
+| `body` | string | yes | The content of a discussion |
+| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z |
+| `position` | hash | no | Position when creating a diff note |
+| `position[base_sha]` | string | yes | Base commit SHA in the source branch |
+| `position[start_sha]` | string | yes | SHA referencing commit in target branch |
+| `position[head_sha]` | string | yes | SHA referencing HEAD of this merge request |
+| `position[position_type]` | string | yes | Type of the position reference', allowed values: 'text' or 'image' |
+| `position[new_path]` | string | no | File path after change |
+| `position[new_line]` | integer | no | Line number after change (for 'text' diff notes) |
+| `position[old_path]` | string | no | File path before change |
+| `position[old_line]` | integer | no | Line number before change (for 'text' diff notes) |
+| `position[width]` | integer | no | Width of the image (for 'image' diff notes) |
+| `position[height]` | integer | no | Height of the image (for 'image' diff notes) |
+| `position[x]` | integer | no | X coordinate (for 'image' diff notes) |
+| `position[y]` | integer | no | Y coordinate (for 'image' diff notes) |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions?body=comment
+```
+
+### Resolve a merge request discussion
+
+Resolve/unresolve whole discussion of a merge request.
+
+```
+PUT /projects/:id/merge_requests/:merge_request_iid/discussions/:discussion_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| ------------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `merge_request_iid` | integer | yes | The IID of a merge request |
+| `discussion_id` | integer | yes | The ID of a discussion |
+| `resolved` | boolean | yes | Resolve/unresolve the discussion |
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7?resolved=true
+```
+
+
+### Add note to existing merge request discussion
+
+Adds a new note to the discussion.
+
+```
+POST /projects/:id/merge_requests/:merge_request_iid/discussions/:discussion_id/notes
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| ------------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `merge_request_iid` | integer | yes | The IID of a merge request |
+| `discussion_id` | integer | yes | The ID of a discussion |
+| `note_id` | integer | yes | The ID of a discussion note |
+| `body` | string | yes | The content of a discussion |
+| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes?body=comment
+```
+
+### Modify an existing merge request discussion note
+
+Modify or resolve an existing discussion note of a merge request.
+
+```
+PUT /projects/:id/merge_requests/:merge_request_iid/discussions/:discussion_id/notes/:note_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| ------------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `merge_request_iid` | integer | yes | The IID of a merge request |
+| `discussion_id` | integer | yes | The ID of a discussion |
+| `note_id` | integer | yes | The ID of a discussion note |
+| `body` | string | no | The content of a discussion (exactly one of `body` or `resolved` must be set |
+| `resolved` | boolean | no | Resolve/unresolve the note (exactly one of `body` or `resolved` must be set |
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes/1108?body=comment
+```
+
+Resolving a note:
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes/1108?resolved=true
+```
+
+### Delete a merge request discussion note
+
+Deletes an existing discussion note of a merge request.
+
+```
+DELETE /projects/:id/merge_requests/:merge_request_iid/discussions/:discussion_id/notes/:note_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| ------------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `merge_request_iid` | integer | yes | The IID of a merge request |
+| `discussion_id` | integer | yes | The ID of a discussion |
+| `note_id` | integer | yes | The ID of a discussion note |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions/636
+```
+
+## Commits
+
+### List project commit discussions
+
+Gets a list of all discussions for a single commit.
+
+```
+GET /projects/:id/commits/:commit_id/discussions
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | ------------ |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `commit_id` | integer | yes | The ID of a commit |
+
+```json
+[
+ {
+ "id": "6a9c1750b37d513a43987b574953fceb50b03ce7",
+ "individual_note": false,
+ "notes": [
+ {
+ "id": 1126,
+ "type": "DiscussionNote",
+ "body": "discussion text",
+ "attachment": null,
+ "author": {
+ "id": 1,
+ "name": "root",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/00afb8fb6ab07c3ee3e9c1f38777e2f4?s=80&d=identicon",
+ "web_url": "http://localhost:3000/root"
+ },
+ "created_at": "2018-03-03T21:54:39.668Z",
+ "updated_at": "2018-03-03T21:54:39.668Z",
+ "system": false,
+ "noteable_id": 3,
+ "noteable_type": "Commit",
+ "noteable_iid": null,
+ "resolvable": false
+ },
+ {
+ "id": 1129,
+ "type": "DiscussionNote",
+ "body": "reply to the discussion",
+ "attachment": null,
+ "author": {
+ "id": 1,
+ "name": "root",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/00afb8fb6ab07c3ee3e9c1f38777e2f4?s=80&d=identicon",
+ "web_url": "http://localhost:3000/root"
+ },
+ "created_at": "2018-03-04T13:38:02.127Z",
+ "updated_at": "2018-03-04T13:38:02.127Z",
+ "system": false,
+ "noteable_id": 3,
+ "noteable_type": "Commit",
+ "noteable_iid": null,
+ "resolvable": false
+ }
+ ]
+ },
+ {
+ "id": "87805b7c09016a7058e91bdbe7b29d1f284a39e6",
+ "individual_note": true,
+ "notes": [
+ {
+ "id": 1128,
+ "type": null,
+ "body": "a single comment",
+ "attachment": null,
+ "author": {
+ "id": 1,
+ "name": "root",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/00afb8fb6ab07c3ee3e9c1f38777e2f4?s=80&d=identicon",
+ "web_url": "http://localhost:3000/root"
+ },
+ "created_at": "2018-03-04T09:17:22.520Z",
+ "updated_at": "2018-03-04T09:17:22.520Z",
+ "system": false,
+ "noteable_id": 3,
+ "noteable_type": "Commit",
+ "noteable_iid": null,
+ "resolvable": false
+ }
+ ]
+ }
+]
+```
+
+Diff comments contain also position:
+
+```json
+[
+ {
+ "id": "87805b7c09016a7058e91bdbe7b29d1f284a39e6",
+ "individual_note": false,
+ "notes": [
+ {
+ "id": 1128,
+ "type": DiffNote,
+ "body": "diff comment",
+ "attachment": null,
+ "author": {
+ "id": 1,
+ "name": "root",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/00afb8fb6ab07c3ee3e9c1f38777e2f4?s=80&d=identicon",
+ "web_url": "http://localhost:3000/root"
+ },
+ "created_at": "2018-03-04T09:17:22.520Z",
+ "updated_at": "2018-03-04T09:17:22.520Z",
+ "system": false,
+ "noteable_id": 3,
+ "noteable_type": "Commit",
+ "noteable_iid": null,
+ "position": {
+ "base_sha": "b5d6e7b1613fca24d250fa8e5bc7bcc3dd6002ef",
+ "start_sha": "7c9c2ead8a320fb7ba0b4e234bd9529a2614e306",
+ "head_sha": "4803c71e6b1833ca72b8b26ef2ecd5adc8a38031",
+ "old_path": "package.json",
+ "new_path": "package.json",
+ "position_type": "text",
+ "old_line": 27,
+ "new_line": 27
+ },
+ "resolvable": false
+ }
+ ]
+ }
+]
+```
+
+```bash
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/commits/11/discussions
+```
+
+### Get single commit discussion
+
+Returns a single discussion for a specific project commit
+
+```
+GET /projects/:id/commits/:commit_id/discussions/:discussion_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| ------------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `commit_id` | integer | yes | The ID of a commit |
+| `discussion_id` | integer | yes | The ID of a discussion |
+
+```bash
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/commits/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7
+```
+
+### Create new commit discussion
+
+Creates a new discussion to a single project commit. This is similar to creating
+a note but but another comments (replies) can be added to it later.
+
+```
+POST /projects/:id/commits/:commit_id/discussions
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| ------------------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `commit_id` | integer | yes | The ID of a commit |
+| `body` | string | yes | The content of a discussion |
+| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z |
+| `position` | hash | no | Position when creating a diff note |
+| `position[base_sha]` | string | yes | Base commit SHA in the source branch |
+| `position[start_sha]` | string | yes | SHA referencing commit in target branch |
+| `position[head_sha]` | string | yes | SHA referencing HEAD of this commit |
+| `position[position_type]` | string | yes | Type of the position reference', allowed values: 'text' or 'image' |
+| `position[new_path]` | string | no | File path after change |
+| `position[new_line]` | integer | no | Line number after change |
+| `position[old_path]` | string | no | File path before change |
+| `position[old_line]` | integer | no | Line number before change |
+| `position[width]` | integer | no | Width of the image (for 'image' diff notes) |
+| `position[height]` | integer | no | Height of the image (for 'image' diff notes) |
+| `position[x]` | integer | no | X coordinate (for 'image' diff notes) |
+| `position[y]` | integer | no | Y coordinate (for 'image' diff notes) |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/commits/11/discussions?body=comment
+```
+
+### Add note to existing commit discussion
+
+Adds a new note to the discussion.
+
+```
+POST /projects/:id/commits/:commit_id/discussions/:discussion_id/notes
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| ------------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `commit_id` | integer | yes | The ID of a commit |
+| `discussion_id` | integer | yes | The ID of a discussion |
+| `note_id` | integer | yes | The ID of a discussion note |
+| `body` | string | yes | The content of a discussion |
+| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/commits/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes?body=comment
+```
+
+### Modify an existing commit discussion note
+
+Modify or resolve an existing discussion note of a commit.
+
+```
+PUT /projects/:id/commits/:commit_id/discussions/:discussion_id/notes/:note_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| ------------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `commit_id` | integer | yes | The ID of a commit |
+| `discussion_id` | integer | yes | The ID of a discussion |
+| `note_id` | integer | yes | The ID of a discussion note |
+| `body` | string | no | The content of a note |
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/commits/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes/1108?body=comment
+```
+
+Resolving a note:
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/commits/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes/1108?resolved=true
+```
+
+### Delete a commit discussion note
+
+Deletes an existing discussion note of a commit.
+
+```
+DELETE /projects/:id/commits/:commit_id/discussions/:discussion_id/notes/:note_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| ------------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `commit_id` | integer | yes | The ID of a commit |
+| `discussion_id` | integer | yes | The ID of a discussion |
+| `note_id` | integer | yes | The ID of a discussion note |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/commits/11/discussions/636
+```
diff --git a/doc/api/group_badges.md b/doc/api/group_badges.md
index 0d7d0fd9c42..f2353542a5c 100644
--- a/doc/api/group_badges.md
+++ b/doc/api/group_badges.md
@@ -12,7 +12,7 @@ Badges support placeholders that will be replaced in real time in both the link
- **%{default_branch}**: will be replaced by the project default branch.
- **%{commit_sha}**: will be replaced by the last project's commit sha.
-Because these enpoints aren't inside a project's context, the information used to replace the placeholders will be
+Because these endpoints aren't inside a project's context, the information used to replace the placeholders will be
from the first group's project by creation date. If the group hasn't got any project the original URL with the placeholders will be returned.
## List all badges of a group
diff --git a/doc/api/groups.md b/doc/api/groups.md
index 1aed8aac64e..923fd662a5b 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -10,7 +10,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `skip_groups` | array of integers | no | Skip the group IDs passed |
-| `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users) |
+| `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users, `true` for admin) |
| `search` | string | no | Return the list of authorized groups matching the search criteria |
| `order_by` | string | no | Order groups by `name` or `path`. Default is `name` |
| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
@@ -94,7 +94,7 @@ Parameters:
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) of the parent group |
| `skip_groups` | array of integers | no | Skip the group IDs passed |
-| `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users) |
+| `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users, `true` for admin) |
| `search` | string | no | Return the list of authorized groups matching the search criteria |
| `order_by` | string | no | Order groups by `name` or `path`. Default is `name` |
| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
diff --git a/doc/api/notes.md b/doc/api/notes.md
index aa38d22845c..d29c5b94915 100644
--- a/doc/api/notes.md
+++ b/doc/api/notes.md
@@ -39,7 +39,8 @@ GET /projects/:id/issues/:issue_iid/notes?sort=asc&order_by=updated_at
"system": true,
"noteable_id": 377,
"noteable_type": "Issue",
- "noteable_iid": 377
+ "noteable_iid": 377,
+ "resolvable": false
},
{
"id": 305,
@@ -58,7 +59,8 @@ GET /projects/:id/issues/:issue_iid/notes?sort=asc&order_by=updated_at
"system": true,
"noteable_id": 121,
"noteable_type": "Issue",
- "noteable_iid": 121
+ "noteable_iid": 121,
+ "resolvable": false
}
]
```
@@ -314,7 +316,8 @@ Parameters:
"system": false,
"noteable_id": 2,
"noteable_type": "MergeRequest",
- "noteable_iid": 2
+ "noteable_iid": 2,
+ "resolvable": false
}
```
diff --git a/doc/api/pipeline_schedules.md b/doc/api/pipeline_schedules.md
index c28f48e5fc6..137f1fdddec 100644
--- a/doc/api/pipeline_schedules.md
+++ b/doc/api/pipeline_schedules.md
@@ -108,7 +108,7 @@ POST /projects/:id/pipeline_schedules
| `description` | string | yes | The description of pipeline schedule |
| `ref` | string | yes | The branch/tag name will be triggered |
| `cron ` | string | yes | The cron (e.g. `0 1 * * *`) ([Cron syntax](https://en.wikipedia.org/wiki/Cron)) |
-| `cron_timezone ` | string | no | The timezone supproted by `ActiveSupport::TimeZone` (e.g. `Pacific Time (US & Canada)`) (default: `'UTC'`) |
+| `cron_timezone ` | string | no | The timezone supported by `ActiveSupport::TimeZone` (e.g. `Pacific Time (US & Canada)`) (default: `'UTC'`) |
| `active ` | boolean | no | The activation of pipeline schedule. If false is set, the pipeline schedule will deactivated initially (default: `true`) |
```sh
@@ -153,7 +153,7 @@ PUT /projects/:id/pipeline_schedules/:pipeline_schedule_id
| `description` | string | no | The description of pipeline schedule |
| `ref` | string | no | The branch/tag name will be triggered |
| `cron ` | string | no | The cron (e.g. `0 1 * * *`) ([Cron syntax](https://en.wikipedia.org/wiki/Cron)) |
-| `cron_timezone ` | string | no | The timezone supproted by `ActiveSupport::TimeZone` (e.g. `Pacific Time (US & Canada)`) or `TZInfo::Timezone` (e.g. `America/Los_Angeles`) |
+| `cron_timezone ` | string | no | The timezone supported by `ActiveSupport::TimeZone` (e.g. `Pacific Time (US & Canada)`) or `TZInfo::Timezone` (e.g. `America/Los_Angeles`) |
| `active ` | boolean | no | The activation of pipeline schedule. If false is set, the pipeline schedule will deactivated initially. |
```sh
diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md
index a6631cab8c3..899f5da6647 100644
--- a/doc/api/pipelines.md
+++ b/doc/api/pipelines.md
@@ -14,6 +14,7 @@ GET /projects/:id/pipelines
| `scope` | string | no | The scope of pipelines, one of: `running`, `pending`, `finished`, `branches`, `tags` |
| `status` | string | no | The status of pipelines, one of: `running`, `pending`, `success`, `failed`, `canceled`, `skipped` |
| `ref` | string | no | The ref of pipelines |
+| `sha` | string | no | The sha or pipelines |
| `yaml_errors`| boolean | no | Returns pipelines with invalid configurations |
| `name`| string | no | The name of the user who triggered pipelines |
| `username`| string | no | The username of the user who triggered pipelines |
diff --git a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md b/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md
index bfc8558a580..3d21c0cc306 100644
--- a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md
+++ b/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md
@@ -509,7 +509,7 @@ and unit tests, all running and deployed at every push to master - with shocking
Errors can be easily debugged through GitLab's build logs, and within minutes of a successful commit,
you can see the changes live on your game.
-Setting up Continous Integration and Continuous Deployment from the start with Dark Nova enables
+Setting up Continuous Integration and Continuous Deployment from the start with Dark Nova enables
rapid but stable development. We can easily test changes in a separate [environment](../../../ci/environments.md#introduction-to-environments-and-deployments),
or multiple environments if needed. Balancing and updating a multiplayer game can be ongoing
and tedious, but having faith in a stable deployment with GitLab CI/CD allows
diff --git a/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md b/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md
index 7f6519fd38e..a2de0408797 100644
--- a/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md
+++ b/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md
@@ -30,7 +30,7 @@ and GitLab UI._
Many components and concepts are similar to Ruby on Rails or Python's Django. High developer
productivity and high application performance are only a few advantages on learning how to use it.
-Working on the MVC pattern, it's was designed to be modular and flexible. Easy to mantain a growing
+Working on the MVC pattern, it's was designed to be modular and flexible. Easy to maintain a growing
app is a plus.
Phoenix can run in any OS where Erlang is supported:
@@ -48,7 +48,7 @@ Check the [Phoenix learning guide][phoenix-learning-guide] for more information.
### What is Elixir?
[Elixir][elixir-site] is a dynamic, functional language created to use all the maturity of Erlang
-(30 years old!) in these days, in an easy way. It has similarities with Ruby, specially on sintax,
+(30 years old!) in these days, in an easy way. It has similarities with Ruby, specially on syntax,
so Ruby developers are quite excited with the rapid growing of Elixir. A full-stack Ruby developer
can learn how to use Elixir and Phoenix in just a few weeks!
@@ -162,7 +162,7 @@ productive, because every time we, or our co-workers push any code, GitLab CI/CD
test the changes, telling us in realtime if anything goes wrong.
Certainly, when our application starts to grow, we'll need more developers working on the same
-project and this process of building and testing can easely become a mess without proper management.
+project and this process of building and testing can easily become a mess without proper management.
That's also why GitLab CI/CD is so important to our application. Every time someone pushes its code to
GitLab, we'll quickly know if their changes broke something or not. We don't need to stop everything
we're doing to test manually and locally every change our team does.
@@ -237,7 +237,7 @@ Finished in 0.7 seconds
Randomized with seed 610000
```
-Our test was successfull. It's time to push our files to GitLab.
+Our test was successful. It's time to push our files to GitLab.
## Configuring CI/CD Pipeline
@@ -302,7 +302,7 @@ template** and select **Elixir**:
```
It's important to install `postgresql-client` to let GitLab CI/CD access PostgreSQL and create our
- database with the login information provided earlier. More important is to respect the identation,
+ database with the login information provided earlier. More important is to respect the indentation,
to avoid syntax errors when running the build.
- And finally, we'll let `mix` session intact.
@@ -333,7 +333,7 @@ mix:
- mix test
```
-For safety, we can check if we get any syntax errors before submiting this file to GitLab. Copy the
+For safety, we can check if we get any syntax errors before submitting this file to GitLab. Copy the
contents of `.gitlab-ci.yml` and paste it on [GitLab CI/CD Lint tool][ci-lint]. Please note that
this link will only work for logged in users.
@@ -384,7 +384,7 @@ working properly.
When we have a growing application with many developers working on it, or when we have an open
source project being watched and contributed by the community, it is really important to have our
-code permanently working. GitLab CI/CD is a time saving powerfull tool to help us mantain our code
+code permanently working. GitLab CI/CD is a time saving powerful tool to help us maintain our code
organized and working.
As we could see in this post, GitLab CI/CD is really really easy to configure and use. We have [many
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 146df15899f..38a988f4507 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -551,7 +551,7 @@ You can find a full list of unsupported variables below:
- `CI_DEPLOY_USER`
- `CI_DEPLOY_PASSWORD`
-These variables are also not supported in a contex of a
+These variables are also not supported in a context of a
[dynamic environment name][dynamic-environments].
[ce-13784]: https://gitlab.com/gitlab-org/gitlab-ce/issues/13784 "Simple protection of CI secret variables"
diff --git a/doc/development/README.md b/doc/development/README.md
index 45e9565f9a7..3c77e99b8cf 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -39,9 +39,9 @@ comments: false
- [Sidekiq debugging](sidekiq_debugging.md)
- [Gotchas](gotchas.md) to avoid
- [Avoid modules with instance variables](module_with_instance_variables.md) if possible
-- [Issue and merge requests state models](object_state_models.md)
- [How to dump production data to staging](db_dump.md)
- [Working with the GitHub importer](github_importer.md)
+- [Working with Merge Request diffs](diffs.md)
## Performance guides
diff --git a/doc/development/background_migrations.md b/doc/development/background_migrations.md
index ce69694ab6a..46c5baddb9c 100644
--- a/doc/development/background_migrations.md
+++ b/doc/development/background_migrations.md
@@ -24,7 +24,7 @@ Some examples where background migrations can be useful:
* Migrating events from one table to multiple separate tables.
* Populating one column based on JSON stored in another column.
-* Migrating data that depends on the output of exernal services (e.g. an API).
+* Migrating data that depends on the output of external services (e.g. an API).
## Isolation
@@ -46,7 +46,7 @@ See [Sidekiq best practices guidelines](https://github.com/mperham/sidekiq/wiki/
for more details.
Make sure that in case that your migration job is going to be retried data
-integrity is guarateed.
+integrity is guaranteed.
## How It Works
diff --git a/doc/development/diffs.md b/doc/development/diffs.md
new file mode 100644
index 00000000000..55fc16e0b33
--- /dev/null
+++ b/doc/development/diffs.md
@@ -0,0 +1,115 @@
+# Working with Merge Request diffs
+
+Currently we rely on different sources to present merge request diffs, these include:
+
+- Rugged gem
+- Gitaly service
+- Database (through `merge_request_diff_files`)
+- Redis (cached highlighted diffs)
+
+We're constantly moving Rugged calls to Gitaly and the progress can be followed through [Gitaly repo](https://gitlab.com/gitlab-org/gitaly).
+
+## Architecture overview
+
+When refreshing a Merge Request (pushing to a source branch, force-pushing to target branch, or if the target branch now contains any commits from the MR)
+we fetch the comparison information using `Gitlab::Git::Compare`, which fetches `base` and `head` data using Gitaly and diff between them through
+`Gitlab::Git::Diff.between` (which uses _Gitaly_ if it's enabled, otherwise _Rugged_).
+The diffs fetching process _limits_ single file diff sizes and the overall size of the whole diff through a series of constant values. Raw diff files are
+then persisted on `merge_request_diff_files` table.
+
+Even though diffs higher than 10kb are collapsed (`Gitlab::Git::Diff::COLLAPSE_LIMIT`), we still keep them on Postgres. However, diff files over _safety limits_
+(see the [Diff limits section](#diff-limits)) are _not_ persisted.
+
+In order to present diffs information on the Merge Request diffs page, we:
+
+1. Fetch all diff files from database `merge_request_diff_files`
+2. Fetch the _old_ and _new_ file blobs in batch to:
+ 1. Highlight old and new file content
+ 2. Know which viewer it should use for each file (text, image, deleted, etc)
+ 3. Know if the file content changed
+ 4. Know if it was stored externally
+ 5. Know if it had storage errors
+3. If the diff file is cacheable (text-based), it's cached on Redis
+using `Gitlab::Diff::FileCollection::MergeRequestDiff`
+
+## Diff limits
+
+As explained above, we limit single diff files and the size of the whole diff. There are scenarios where we collapse the diff file,
+and cases where the diff file is not presented at all, and the user is guided to the Blob view. Here we'll go into details about
+these limits.
+
+### Diff collection limits
+
+Limits that act onto all diff files collection. Files number, lines number and files size are considered.
+
+```ruby
+Gitlab::Git::DiffCollection.collection_limits[:safe_max_files] = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_files] = 100
+```
+
+File diffs will be collapsed (but be expandable) if 100 files have already been rendered.
+
+
+```ruby
+Gitlab::Git::DiffCollection.collection_limits[:safe_max_lines] = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines] = 5000
+```
+
+File diffs will be collapsed (but be expandable) if 5000 lines have already been rendered.
+
+
+```ruby
+Gitlab::Git::DiffCollection.collection_limits[:safe_max_bytes] = Gitlab::Git::DiffCollection.collection_limits[:safe_max_files] * 5.kilobytes = 500.kilobytes
+```
+
+File diffs will be collapsed (but be expandable) if 500 kilobytes have already been rendered.
+
+
+```ruby
+Gitlab::Git::DiffCollection.collection_limits[:max_files] = Commit::DIFF_HARD_LIMIT_FILES = 1000
+```
+
+No more files will be rendered at all if 1000 files have already been rendered.
+
+
+```ruby
+Gitlab::Git::DiffCollection.collection_limits[:max_lines] = Commit::DIFF_HARD_LIMIT_LINES = 50000
+```
+
+No more files will be rendered at all if 50,000 lines have already been rendered.
+
+```ruby
+Gitlab::Git::DiffCollection.collection_limits[:max_bytes] = Gitlab::Git::DiffCollection.collection_limits[:max_files] * 5.kilobytes = 5000.kilobytes
+```
+
+No more files will be rendered at all if 5 megabytes have already been rendered.
+
+
+### Individual diff file limits
+
+Limits that act onto each diff file of a collection. Files number, lines number and files size are considered.
+
+```ruby
+Gitlab::Git::Diff::COLLAPSE_LIMIT = 10.kilobytes
+```
+
+File diff will be collapsed (but be expandable) if it is larger than 10 kilobytes.
+
+```ruby
+Gitlab::Git::Diff::SIZE_LIMIT = 100.kilobytes
+```
+
+File diff will not be rendered if it's larger than 100 kilobytes.
+
+
+```ruby
+Commit::DIFF_SAFE_LINES = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines] = 5000
+```
+
+File diff will be suppressed (technically different from collapsed, but behaves the same, and is expandable) if it has more than 5000 lines.
+
+## Viewers
+
+Diff Viewers, which can be found on `models/diff_viewer/*` are classes used to map metadata about each type of Diff File. It has information
+whether it's a binary, which partial should be used to render it or which File extensions this class accounts for.
+
+`DiffViewer::Base` validates _blobs_ (old and new versions) content, extension and file type in order to check if it can be rendered.
+
diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md
index 0550ea527cb..5da015ca557 100644
--- a/doc/development/doc_styleguide.md
+++ b/doc/development/doc_styleguide.md
@@ -4,7 +4,7 @@ The documentation style guide defines the markup structure used in
GitLab documentation. Check the
[documentation guidelines](writing_documentation.md) for general development instructions.
-Check the GitLab hanbook for the [writing styles guidelines](https://about.gitlab.com/handbook/communication/#writing-style-guidelines).
+Check the GitLab handbook for the [writing styles guidelines](https://about.gitlab.com/handbook/communication/#writing-style-guidelines).
## Text
@@ -19,7 +19,7 @@ Check the GitLab hanbook for the [writing styles guidelines](https://about.gitla
- Unless there's a logical reason not to, add documents in alphabetical order
- Write in US English
- Use [single spaces][] instead of double spaces
-- Jump a line between different markups (e.g., after every paragraph, hearder, list, etc)
+- Jump a line between different markups (e.g., after every paragraph, header, list, etc)
- Capitalize "G" and "L" in GitLab
- Capitalize feature, products, and methods names. E.g.: GitLab Runner, Geo,
Issue Boards, Git, Prometheus, Continuous Integration.
diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md
index 287143d6255..4873090a2d4 100644
--- a/doc/development/ee_features.md
+++ b/doc/development/ee_features.md
@@ -279,7 +279,7 @@ end
```
In `lib/gitlab/visibility_level.rb` this method is used to return the
-allowed visibilty levels:
+allowed visibility levels:
```ruby
def levels_for_user(user = nil)
diff --git a/doc/development/fe_guide/icons.md b/doc/development/fe_guide/icons.md
index b288ee95722..b469a9c6aef 100644
--- a/doc/development/fe_guide/icons.md
+++ b/doc/development/fe_guide/icons.md
@@ -49,7 +49,7 @@ Please use the following function inside JS to render an icon :
All Icons and Illustrations are managed in the [gitlab-svgs](https://gitlab.com/gitlab-org/gitlab-svgs) repository which is added as a dev-dependency.
-To upgrade to a new SVG Sprite version run `yarn upgrade @gitlab-org/gitlab-svgs` and then run `yarn run svg`. This task will copy the svg sprite and all illustrations in the correct folders. The updated files should be tracked in Git as those are referenced.
+To upgrade to a new SVG Sprite version run `yarn upgrade @gitlab-org/gitlab-svgs`.
# SVG Illustrations
diff --git a/doc/development/fe_guide/style_guide_js.md b/doc/development/fe_guide/style_guide_js.md
index 7b5fa6ca42f..677168937c7 100644
--- a/doc/development/fe_guide/style_guide_js.md
+++ b/doc/development/fe_guide/style_guide_js.md
@@ -236,7 +236,7 @@ export class Foo {
}
```
-On the other hand, if a class only needs to extend a third party/add event listeners in some specific cases, they should be initialized oustside of the constructor.
+On the other hand, if a class only needs to extend a third party/add event listeners in some specific cases, they should be initialized outside of the constructor.
1. Prefer `.map`, `.reduce` or `.filter` over `.forEach`
A forEach will most likely cause side effects, it will be mutating the array being iterated. Prefer using `.map`,
diff --git a/doc/development/file_storage.md b/doc/development/file_storage.md
index 34a02bd2c3c..fdbd7f1fa37 100644
--- a/doc/development/file_storage.md
+++ b/doc/development/file_storage.md
@@ -84,7 +84,7 @@ The `RecordsUploads::Concern` concern will create an `Upload` entry for every fi
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
+The `CarrierWave::Uploader#store_dir` is overridden 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)
diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md
index b1bec84a2f3..0edcb23c7c5 100644
--- a/doc/development/i18n/externalization.md
+++ b/doc/development/i18n/externalization.md
@@ -270,7 +270,7 @@ If there are merge conflicts in the `gitlab.pot` file, you can delete the file
and regenerate it using the same command. Confirm that you are not deleting any strings accidentally by looking over the diff.
The command also updates the translation files for each language: `locale/*/gitlab.po`
-These changes can be discarded, the languange files will be updated by Crowdin
+These changes can be discarded, the language files will be updated by Crowdin
automatically.
Discard all of them at once like this:
diff --git a/doc/development/img/state-model-issue.png b/doc/development/img/state-model-issue.png
deleted file mode 100644
index ee33b6886c6..00000000000
--- a/doc/development/img/state-model-issue.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/img/state-model-legend.png b/doc/development/img/state-model-legend.png
deleted file mode 100644
index 1c121f2588c..00000000000
--- a/doc/development/img/state-model-legend.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/img/state-model-merge-request.png b/doc/development/img/state-model-merge-request.png
deleted file mode 100644
index e00da10cac2..00000000000
--- a/doc/development/img/state-model-merge-request.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/merge_request_performance_guidelines.md b/doc/development/merge_request_performance_guidelines.md
index 2b4126b43ef..12badbe39b2 100644
--- a/doc/development/merge_request_performance_guidelines.md
+++ b/doc/development/merge_request_performance_guidelines.md
@@ -162,7 +162,7 @@ need for running complex operations to fetch the data. You should use Redis if
data should be cached for a certain time period instead of the duration of the
transaction.
-For example, say you process multiple snippets of text containiner username
+For example, say you process multiple snippets of text containing username
mentions (e.g. `Hello @alice` and `How are you doing @alice?`). By caching the
user objects for every username we can remove the need for running the same
query for every mention of `@alice`.
diff --git a/doc/development/object_state_models.md b/doc/development/object_state_models.md
deleted file mode 100644
index 623bbf143ef..00000000000
--- a/doc/development/object_state_models.md
+++ /dev/null
@@ -1,25 +0,0 @@
-# Object state models
-
-## Diagrams
-
-[GitLab object state models](https://drive.google.com/drive/u/3/folders/0B5tDlHAM4iZINmpvYlJXcDVqMGc)
-
----
-
-## Legend
-
-![legend](img/state-model-legend.png)
-
----
-
-## Issue
-
-[`app/models/issue.rb`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/models/issue.rb)
-![issue](img/state-model-issue.png)
-
----
-
-## Merge request
-
-[`app/models/merge_request.rb`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/models/merge_request.rb)
-![merge request](img/state-model-merge-request.png) \ No newline at end of file
diff --git a/doc/development/ordering_table_columns.md b/doc/development/ordering_table_columns.md
index 249e70c7b0e..5d00e1f7a0c 100644
--- a/doc/development/ordering_table_columns.md
+++ b/doc/development/ordering_table_columns.md
@@ -30,7 +30,7 @@ example) at the end.
## Type Sizes
-While the PostgreSQL docuemntation
+While the PostgreSQL documentation
(https://www.postgresql.org/docs/current/static/datatype.html) contains plenty
of information we will list the sizes of common types here so it's easier to
look them up. Here "word" refers to the word size, which is 4 bytes for a 32
diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md
index af477f5ab99..0d0d511582b 100644
--- a/doc/development/testing_guide/frontend_testing.md
+++ b/doc/development/testing_guide/frontend_testing.md
@@ -62,6 +62,7 @@ describe('.methodName', () => {
});
});
```
+
#### Testing promises
When testing Promises you should always make sure that the test is asynchronous and rejections are handled.
@@ -69,9 +70,9 @@ Your Promise chain should therefore end with a call of the `done` callback and `
```javascript
// Good
-it('tests a promise', (done) => {
+it('tests a promise', done => {
promise
- .then((data) => {
+ .then(data => {
expect(data).toBe(asExpected);
})
.then(done)
@@ -79,10 +80,10 @@ it('tests a promise', (done) => {
});
// Good
-it('tests a promise rejection', (done) => {
+it('tests a promise rejection', done => {
promise
.then(done.fail)
- .catch((error) => {
+ .catch(error => {
expect(error).toBe(expectedError);
})
.then(done)
@@ -91,38 +92,37 @@ it('tests a promise rejection', (done) => {
// Bad (missing done callback)
it('tests a promise', () => {
- promise
- .then((data) => {
- expect(data).toBe(asExpected);
- })
+ promise.then(data => {
+ expect(data).toBe(asExpected);
+ });
});
// Bad (missing catch)
-it('tests a promise', (done) => {
+it('tests a promise', done => {
promise
- .then((data) => {
+ .then(data => {
expect(data).toBe(asExpected);
})
- .then(done)
+ .then(done);
});
// Bad (use done.fail in asynchronous tests)
-it('tests a promise', (done) => {
+it('tests a promise', done => {
promise
- .then((data) => {
+ .then(data => {
expect(data).toBe(asExpected);
})
.then(done)
- .catch(fail)
+ .catch(fail);
});
// Bad (missing catch)
-it('tests a promise rejection', (done) => {
+it('tests a promise rejection', done => {
promise
- .catch((error) => {
+ .catch(error => {
expect(error).toBe(expectedError);
})
- .then(done)
+ .then(done);
});
```
@@ -139,7 +139,7 @@ documentation for these methods can be found in the [jasmine introduction page](
Sometimes you may need to spy on a method that is directly imported by another
module. GitLab has a custom `spyOnDependency` method which utilizes
[babel-plugin-rewire](https://github.com/speedskater/babel-plugin-rewire) to
-achieve this. It can be used like so:
+achieve this. It can be used like so:
```javascript
// my_module.js
@@ -181,8 +181,8 @@ See this [section][vue-test].
`rake karma` runs the frontend-only (JavaScript) tests.
It consists of two subtasks:
-- `rake karma:fixtures` (re-)generates fixtures
-- `rake karma:tests` actually executes the tests
+* `rake karma:fixtures` (re-)generates fixtures
+* `rake karma:tests` actually executes the tests
As long as the fixtures don't change, `rake karma:tests` (or `yarn karma`)
is sufficient (and saves you some time).
@@ -217,6 +217,14 @@ yarn karma-start --filter-spec profile/account/components/
yarn karma-start -f vue_shared -f vue_mr_widget
```
+You can also use glob syntax to match files. Remember to put quotes around the
+glob otherwise your shell may split it into multiple arguments:
+
+```bash
+# Run all specs named `file_spec` within the IDE subdirectory
+yarn karma -f 'spec/javascripts/ide/**/file_spec.js'
+```
+
## RSpec feature integration tests
Information on setting up and running RSpec integration tests with
@@ -231,14 +239,14 @@ supported by the PhantomJS test runner which is used for both Karma and RSpec
tests. We polyfill some JavaScript objects for older browsers, but some
features are still unavailable:
-- Array.from
-- Array.first
-- Async functions
-- Generators
-- Array destructuring
-- For..Of
-- Symbol/Symbol.iterator
-- Spread
+* Array.from
+* Array.first
+* Async functions
+* Generators
+* Array destructuring
+* For..Of
+* Symbol/Symbol.iterator
+* Spread
Until these are polyfilled appropriately, they should not be used. Please
update this list with additional unsupported features.
@@ -295,11 +303,11 @@ Scenario: Developer can approve merge request
[jasmine-focus]: https://jasmine.github.io/2.5/focused_specs.html
[jasmine-jquery]: https://github.com/velesin/jasmine-jquery
[karma]: http://karma-runner.github.io/
-[vue-test]:https://docs.gitlab.com/ce/development/fe_guide/vue.html#testing-vue-components
-[RSpec]: https://github.com/rspec/rspec-rails#feature-specs
-[Capybara]: https://github.com/teamcapybara/capybara
-[Karma]: http://karma-runner.github.io/
-[Jasmine]: https://jasmine.github.io/
+[vue-test]: https://docs.gitlab.com/ce/development/fe_guide/vue.html#testing-vue-components
+[rspec]: https://github.com/rspec/rspec-rails#feature-specs
+[capybara]: https://github.com/teamcapybara/capybara
+[karma]: http://karma-runner.github.io/
+[jasmine]: https://jasmine.github.io/
---
diff --git a/doc/development/testing_guide/testing_levels.md b/doc/development/testing_guide/testing_levels.md
index e86c1f5232a..51794f7f4df 100644
--- a/doc/development/testing_guide/testing_levels.md
+++ b/doc/development/testing_guide/testing_levels.md
@@ -28,7 +28,7 @@ records should use stubs/doubles as much as possible.
| `app/uploaders/` | `spec/uploaders/` | RSpec | |
| `app/views/` | `spec/views/` | RSpec | |
| `app/workers/` | `spec/workers/` | RSpec | |
-| `app/assets/javascripts/` | `spec/javascripts/` | Karma | More details in the [Frontent Testing guide](frontend_testing.md) section. |
+| `app/assets/javascripts/` | `spec/javascripts/` | Karma | More details in the [Frontend Testing guide](frontend_testing.md) section. |
## Integration tests
diff --git a/doc/development/ux_guide/components.md b/doc/development/ux_guide/components.md
index 012c64be79f..b57520a00e0 100644
--- a/doc/development/ux_guide/components.md
+++ b/doc/development/ux_guide/components.md
@@ -219,7 +219,7 @@ Blocks are a way to group related information.
#### Content blocks
-Content blocks (`.content-block`) are the basic grouping of content. They are commonly used in [lists](#lists), and are separated by a botton border.
+Content blocks (`.content-block`) are the basic grouping of content. They are commonly used in [lists](#lists), and are separated by a button border.
![Content block](img/components-contentblock.png)
@@ -281,7 +281,7 @@ Modals are only used for having a conversation and confirmation with the user. T
| Modal with 2 actions | Modal with 3 actions | Special confirmation |
| --------------------- | --------------------- | -------------------- |
-| ![two-actions](img/modals-general-confimation-dialog.png) | ![three-actions](img/modals-three-buttons.png) | ![spcial-confirmation](img/modals-special-confimation-dialog.png) |
+| ![two-actions](img/modals-general-confimation-dialog.png) | ![three-actions](img/modals-three-buttons.png) | ![special-confirmation](img/modals-special-confimation-dialog.png) |
> TODO: Special case for modal.
diff --git a/doc/development/what_requires_downtime.md b/doc/development/what_requires_downtime.md
index 9d0c62ecc35..b8be8daa157 100644
--- a/doc/development/what_requires_downtime.md
+++ b/doc/development/what_requires_downtime.md
@@ -255,7 +255,7 @@ otherwise it will raise a `TypeError`.
## Adding Indexes
Adding indexes is an expensive process that blocks INSERT and UPDATE queries for
-the duration. When using PostgreSQL one can work arounds this by using the
+the duration. When using PostgreSQL one can work around this by using the
`CONCURRENTLY` option:
```sql
diff --git a/doc/development/writing_documentation.md b/doc/development/writing_documentation.md
index d6a13e7483a..9bca4637830 100644
--- a/doc/development/writing_documentation.md
+++ b/doc/development/writing_documentation.md
@@ -49,7 +49,7 @@ do before.
**Use cases**: provide at least two, ideally three, use cases for every major feature.
You should answer this question: what can you do with this feature/change? Use cases
-are examples of how this feauture or change can be used in real life.
+are examples of how this feature or change can be used in real life.
Examples:
- CE and EE: [Issues](../user/project/issues/index.md#use-cases)
diff --git a/doc/install/database_mysql.md b/doc/install/database_mysql.md
index 5c7557ed2b3..e1af086f418 100644
--- a/doc/install/database_mysql.md
+++ b/doc/install/database_mysql.md
@@ -91,7 +91,7 @@ Follow the below instructions to ensure you use the most up to date requirements
#### Check for InnoDB File-Per-Table Tablespaces
-We need to check, enable and maybe convert your existing GitLab DB tables to the [InnoDB File-Per-Table Tablespaces](http://dev.mysql.com/doc/refman/5.7/en/innodb-multiple-tablespaces.html) as a prerequise for supporting **utfb8mb4 with long indexes** required by recent GitLab databases.
+We need to check, enable and maybe convert your existing GitLab DB tables to the [InnoDB File-Per-Table Tablespaces](http://dev.mysql.com/doc/refman/5.7/en/innodb-multiple-tablespaces.html) as a prerequisite for supporting **utfb8mb4 with long indexes** required by recent GitLab databases.
# Login to MySQL
mysql -u root -p
diff --git a/doc/install/google_cloud_platform/index.md b/doc/install/google_cloud_platform/index.md
index 3389f0260f9..2691495e0d4 100644
--- a/doc/install/google_cloud_platform/index.md
+++ b/doc/install/google_cloud_platform/index.md
@@ -2,7 +2,7 @@
![GCP landing page](img/gcp_landing.png)
-Gettung started with GitLab on a [Google Cloud Platform (GCP)][gcp] instance is quick and easy.
+Getting started with GitLab on a [Google Cloud Platform (GCP)][gcp] instance is quick and easy.
## Prerequisites
diff --git a/doc/install/kubernetes/gitlab_runner_chart.md b/doc/install/kubernetes/gitlab_runner_chart.md
index a03c49cbd89..0a093c9ec32 100644
--- a/doc/install/kubernetes/gitlab_runner_chart.md
+++ b/doc/install/kubernetes/gitlab_runner_chart.md
@@ -50,12 +50,12 @@ Here is a snippet of the important settings:
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.
+## be retrieved from your GitLab Instance.
## ref: https://docs.gitlab.com/ce/ci/runners/README.html#creating-and-registering-a-runner
##
runnerRegistrationToken: ""
-## Set the certsSecretName in order to pass custom certficates for GitLab Runner to use
+## Set the certsSecretName in order to pass custom certificates for GitLab Runner to use
## Provide resource name for a Kubernetes Secret Object in the same namespace,
## this is used to populate the /etc/gitlab-runner/certs directory
## ref: https://docs.gitlab.com/runner/configuration/tls-self-signed.html#supported-options-for-self-signed-certificates
@@ -130,7 +130,7 @@ runners:
### Enabling RBAC support
-If your cluster has RBAC enabled, you can choose to either have the chart create its own sevice account or provide one.
+If your cluster has RBAC enabled, you can choose to either have the chart create its own service account or provide one.
To have the chart create the service account for you, set `rbac.create` to true.
@@ -208,7 +208,7 @@ You then need to provide the secret's name to the GitLab Runner chart.
Add the following to your `values.yaml`
```yaml
-## Set the certsSecretName in order to pass custom certficates for GitLab Runner to use
+## Set the certsSecretName in order to pass custom certificates for GitLab Runner to use
## Provide resource name for a Kubernetes Secret Object in the same namespace,
## this is used to populate the /etc/gitlab-runner/certs directory
## ref: https://docs.gitlab.com/runner/configuration/tls-self-signed.html#supported-options-for-self-signed-certificates
diff --git a/doc/integration/shibboleth.md b/doc/integration/shibboleth.md
index e0fc1bb801f..8611d4f7315 100644
--- a/doc/integration/shibboleth.md
+++ b/doc/integration/shibboleth.md
@@ -43,7 +43,7 @@ exclude shibboleth URLs from rewriting, add "RewriteCond %{REQUEST_URI} !/Shibbo
RequestHeader set X_FORWARDED_PROTO 'https'
```
-1. Edit /etc/gitlab/gitlab.rb configuration file, your shibboleth attributes should be in form of "HTTP_ATTRIBUTE" and you should addjust them to your need and environment. Add any other configuration you need.
+1. Edit /etc/gitlab/gitlab.rb configuration file, your shibboleth attributes should be in form of "HTTP_ATTRIBUTE" and you should adjust them to your need and environment. Add any other configuration you need.
File should look like this:
```
diff --git a/doc/ssh/README.md b/doc/ssh/README.md
index aa14a39e4c9..b71e9bf3000 100644
--- a/doc/ssh/README.md
+++ b/doc/ssh/README.md
@@ -196,7 +196,7 @@ This is really useful for integrating repositories to secured, shared Continuous
Integration (CI) services or other shared services.
GitLab administrators can set up the Global Shared Deploy key in GitLab and
add the private key to any shared systems. Individual repositories opt into
-exposing their repsitory using these keys when a project masters (or higher)
+exposing their repository using these keys when a project masters (or higher)
authorizes a Global Shared Deploy key to be used with their project.
Global Shared Keys can provide greater security compared to Per-Project Deploy
@@ -224,7 +224,7 @@ if there is at least one Global Deploy Key configured.
CAUTION: **Warning:**
Defining Global Deploy Keys does not expose any given repository via
-the key until that respository adds the Global Deploy Key to their project.
+the key until that repository adds the Global Deploy Key to their project.
In this way the Global Deploy Keys enable access by other systems, but do
not implicitly give any access just by setting them up.
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 8c4a2925356..882ddf4d2c5 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -135,6 +135,11 @@ and `1.2.3.4` is the IP address of your load balancer; generally NGINX
([see prerequisites](#prerequisites)). How to set up the DNS record is beyond
the scope of this document; you should check with your DNS provider.
+Alternatively you can use free public services like [xip.io](http://xip.io) or
+[nip.io](http://nip.io) which provide automatic wildcard DNS without any
+configuration. Just set the Auto DevOps base domain to `1.2.3.4.xip.io` or
+`1.2.3.4.nip.io`.
+
Once set up, all requests will hit the load balancer, which in turn will route
them to the Kubernetes pods that run your application(s).
@@ -490,6 +495,7 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac
| `POSTGRES_PASSWORD` | The PostgreSQL password; defaults to `testing-password`. Set it to use a custom password. |
| `POSTGRES_DB` | The PostgreSQL database name; defaults to the value of [`$CI_ENVIRONMENT_SLUG`](../../ci/variables/README.md#predefined-variables-environment-variables). Set it to use a custom database name. |
| `BUILDPACK_URL` | The buildpack's full URL. It can point to either Git repositories or a tarball URL. For Git repositories, it is possible to point to a specific `ref`, for example `https://github.com/heroku/heroku-buildpack-ruby.git#v142` |
+| `STAGING_ENABLED` | From GitLab 10.8, this variable can be used to define a [deploy policy for staging and production environments](#deploy-policy-for-staging-and-production-environments). |
TIP: **Tip:**
Set up the replica variables using a
@@ -556,6 +562,22 @@ service:
internalPort: 5000
```
+#### Deploy policy for staging and production environments
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ci-yml/merge_requests/160)
+in GitLab 10.8.
+
+The normal behavior of Auto DevOps is to use Continuous Deployment, pushing
+automatically to the `production` environment every time a new pipeline is run
+on the default branch. However, there are cases where you might want to use a
+staging environment and deploy to production manually. For this scenario, the
+`STAGING_ENABLED` environment variable was introduced.
+
+If `STAGING_ENABLED` is defined in your project (e.g., set `STAGING_ENABLED` to
+`1` as a secret variable), then the application will be automatically deployed
+to a `staging` environment, and a `production_manual` job will be created for
+you when you're ready to manually deploy to production.
+
## Currently supported languages
NOTE: **Note:**
diff --git a/doc/university/glossary/README.md b/doc/university/glossary/README.md
index a9ccbf5a085..945d6a578b0 100644
--- a/doc/university/glossary/README.md
+++ b/doc/university/glossary/README.md
@@ -89,7 +89,7 @@ A [copy](https://git-scm.com/docs/git-clone) of a repository stored on your mach
### Code Review
-Examination of a progam's code. The main aim is to maintain high quality standards of code that is being shipped. Merge requests [serve as a code review tool](https://about.gitlab.com/2014/09/29/gitlab-flow/) in GitLab.
+Examination of a program's code. The main aim is to maintain high quality standards of code that is being shipped. Merge requests [serve as a code review tool](https://about.gitlab.com/2014/09/29/gitlab-flow/) in GitLab.
### Code Snippet
diff --git a/doc/university/high-availability/aws/README.md b/doc/university/high-availability/aws/README.md
index 47ccd0e6dbc..f340164b882 100644
--- a/doc/university/high-availability/aws/README.md
+++ b/doc/university/high-availability/aws/README.md
@@ -354,11 +354,11 @@ add the following script to the User Data section:
- mount -a -t nfs
- sudo gitlab-ctl reconfigure
-On the security group section we can chosse our existing
+On the security group section we can choose our existing
`gitlab-ec2-security-group` group which has already been tested.
After this is launched we are able to start creating our Auto Scaling
-Group. Start by giving it a name and assinging it our VPC and private
+Group. Start by giving it a name and assigning it our VPC and private
subnets. We also want to always start with two instances and if you
scroll down to Advanced Details we can choose to receive traffic from ELBs.
Lets enable that option and select our ELB. We also want to use the ELB's
diff --git a/doc/university/support/README.md b/doc/university/support/README.md
index 25d5fe351ca..d1d5db6bbcd 100644
--- a/doc/university/support/README.md
+++ b/doc/university/support/README.md
@@ -163,7 +163,7 @@ Some tickets need specific knowledge or a deep understanding of a particular com
- Aim to have a good understanding of the problems that customers are facing
- Aim to have gained experience in scheduling and participating in calls with customers
-- Aim to have a good understanding of ticket flow through Zendesk and how to interat with our various channels
+- Aim to have a good understanding of ticket flow through Zendesk and how to interact with our various channels
### Stage 4
diff --git a/doc/university/training/end-user/README.md b/doc/university/training/end-user/README.md
index a882bf0eb48..9b8a8db58e2 100644
--- a/doc/university/training/end-user/README.md
+++ b/doc/university/training/end-user/README.md
@@ -27,7 +27,7 @@ project.
### Short Story of Git
-- 1991-2002: The Linux kernel was being maintaned by sharing archived files
+- 1991-2002: The Linux kernel was being maintained by sharing archived files
and patches.
- 2002: The Linux kernel project began using a DVCS called BitKeeper
- 2005: BitKeeper revoked the free-of-charge status and Git was created
diff --git a/doc/university/training/topics/tags.md b/doc/university/training/topics/tags.md
index ab48d52d3c3..6333ceedbd7 100644
--- a/doc/university/training/topics/tags.md
+++ b/doc/university/training/topics/tags.md
@@ -9,7 +9,7 @@ comments: false
- Useful for marking deployments and releases
- Annotated tags are an unchangeable part of Git history
- Soft/lightweight tags can be set and removed at will
-- Many projects combine an anotated release tag with a stable branch
+- Many projects combine an annotated release tag with a stable branch
- Consider setting deployment/release tags automatically
----------
diff --git a/doc/university/training/user_training.md b/doc/university/training/user_training.md
index 90e1d2ba5e8..dccb6cbf071 100644
--- a/doc/university/training/user_training.md
+++ b/doc/university/training/user_training.md
@@ -279,7 +279,7 @@ See GitLab merge requests for examples:
- Useful for marking deployments and releases
- Annotated tags are an unchangeable part of Git history
- Soft/lightweight tags can be set and removed at will
-- Many projects combine an anotated release tag with a stable branch
+- Many projects combine an annotated release tag with a stable branch
- Consider setting deployment/release tags automatically
---
diff --git a/doc/user/admin_area/settings/sign_up_restrictions.md b/doc/user/admin_area/settings/sign_up_restrictions.md
index 603b826e7f2..26329f20339 100644
--- a/doc/user/admin_area/settings/sign_up_restrictions.md
+++ b/doc/user/admin_area/settings/sign_up_restrictions.md
@@ -1,7 +1,7 @@
# Sign-up restrictions
You can block email addresses of specific domains, or whitelist only some
-specifc domains via the **Application Settings** in the Admin area.
+specific domains via the **Application Settings** in the Admin area.
>**Note**: These restrictions are only applied during sign-up. An admin is
able to add add a user through the admin panel with a disallowed domain. Also
diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md
index 2a982344e5f..02f8ef08117 100644
--- a/doc/user/group/subgroups/index.md
+++ b/doc/user/group/subgroups/index.md
@@ -55,7 +55,7 @@ first group being the name of the distro and subsequent groups split like:
Another example of GitLab as a company would be the following:
- Organization Group - GitLab
- - Category Subroup - Marketing
+ - Category Subgroup - Marketing
- (project) Design
- (project) General
- Category Subgroup - Software
diff --git a/doc/user/index.md b/doc/user/index.md
index 43b6fd53b91..2494df46f1c 100644
--- a/doc/user/index.md
+++ b/doc/user/index.md
@@ -56,7 +56,7 @@ With GitLab Enterprise Edition, you can also:
[Merge Request Approvals](https://docs.gitlab.com/ee/user/project/merge_requests/index.html#merge-request-approvals),
[Multiple Assignees for Issues](https://docs.gitlab.com/ee/user/project/issues/multiple_assignees_for_issues.html),
and [Multiple Issue Boards](https://docs.gitlab.com/ee/user/project/issue_board.html#multiple-issue-boards)
-- Create formal relashionships between issues with [Related Issues](https://docs.gitlab.com/ee/user/project/issues/related_issues.html)
+- Create formal relationships between issues with [Related Issues](https://docs.gitlab.com/ee/user/project/issues/related_issues.html)
- Use [Burndown Charts](https://docs.gitlab.com/ee/user/project/milestones/burndown_charts.html) to track progress during a sprint or while working on a new version of their software.
- Leverage [Elasticsearch](https://docs.gitlab.com/ee/integration/elasticsearch.html) with [Advanced Global Search](https://docs.gitlab.com/ee/user/search/advanced_global_search.html) and [Advanced Syntax Search](https://docs.gitlab.com/ee/user/search/advanced_search_syntax.html) for faster, more advanced code search across your entire GitLab instance
- [Authenticate users with Kerberos](https://docs.gitlab.com/ee/integration/kerberos.html)
diff --git a/doc/user/profile/active_sessions.md b/doc/user/profile/active_sessions.md
new file mode 100644
index 00000000000..5119c0e30d0
--- /dev/null
+++ b/doc/user/profile/active_sessions.md
@@ -0,0 +1,20 @@
+# Active Sessions
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17867)
+> in GitLab 10.8.
+
+GitLab lists all devices that have logged into your account. This allows you to
+review the sessions and revoke any of it that you don't recognize.
+
+## Listing all active sessions
+
+1. On the upper right corner, click on your avatar and go to your **Settings**.
+1. Navigate to the **Active Sessions** tab.
+
+![Active sessions list](img/active_sessions_list.png)
+
+## Revoking a session
+
+1. Navigate to your [profile's](#profile-settings) **Settings > Active Sessions**.
+1. Click on **Revoke** besides a session. The current session cannot be
+ revoked, as this would sign you out of GitLab.
diff --git a/doc/user/profile/img/active_sessions_list.png b/doc/user/profile/img/active_sessions_list.png
new file mode 100644
index 00000000000..76a52220bcd
--- /dev/null
+++ b/doc/user/profile/img/active_sessions_list.png
Binary files differ
diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md
index ab16f8d14c1..91cdef8d1dd 100644
--- a/doc/user/profile/index.md
+++ b/doc/user/profile/index.md
@@ -39,6 +39,7 @@ From there, you can:
- Manage [SSH keys](../../ssh/README.md#ssh) to access your account via SSH
- Manage your [preferences](preferences.md#syntax-highlighting-theme)
to customize your own GitLab experience
+- [View your active sessions](active_sessions.md) and revoke any of them if necessary
- Access your audit log, a security log of important events involving your account
## Changing your username
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index 716787532fc..edb875bc7e6 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -238,6 +238,7 @@ work.
The default environment scope is `*`, which means all jobs, regardless of their
environment, will use that cluster. Each scope can only be used by a single
cluster in a project, and a validation error will occur if otherwise.
+Also, jobs that don't have an environment keyword set will not be able to access any cluster.
---
diff --git a/doc/user/project/issues/closing_issues.md b/doc/user/project/issues/closing_issues.md
index dcfa5ff59b2..1d88745af9f 100644
--- a/doc/user/project/issues/closing_issues.md
+++ b/doc/user/project/issues/closing_issues.md
@@ -48,12 +48,12 @@ link to each other, but the MR will NOT close the issue(s) when merged.
## From the Issue Board
-You can close an issue from [Issue Boards](../issue_board.md) by draging an issue card
+You can close an issue from [Issue Boards](../issue_board.md) by dragging an issue card
from its list and dropping into **Closed**.
![close issue from the Issue Board](img/close_issue_from_board.gif)
-## Customizing the issue closing patern
+## Customizing the issue closing pattern
Alternatively, a GitLab **administrator** can
-[customize the issue closing patern](../../../administration/issue_closing_pattern.md).
+[customize the issue closing pattern](../../../administration/issue_closing_pattern.md).
diff --git a/doc/user/project/issues/crosslinking_issues.md b/doc/user/project/issues/crosslinking_issues.md
index cc8988be36b..786d1c81b1b 100644
--- a/doc/user/project/issues/crosslinking_issues.md
+++ b/doc/user/project/issues/crosslinking_issues.md
@@ -60,4 +60,4 @@ or simply link both issue and merge request as described in the
### Close an issue by merging a merge request
-To [close an issue when a merge request is merged](closing_issues.md#via-merge-request), use the [automatic issue closing patern](automatic_issue_closing.md).
+To [close an issue when a merge request is merged](closing_issues.md#via-merge-request), use the [automatic issue closing pattern](automatic_issue_closing.md).
diff --git a/doc/user/project/issues/issues_functionalities.md b/doc/user/project/issues/issues_functionalities.md
index cf5cf1794ee..e9903b01c82 100644
--- a/doc/user/project/issues/issues_functionalities.md
+++ b/doc/user/project/issues/issues_functionalities.md
@@ -152,7 +152,7 @@ know you like it without spamming them.
These text fields also fully support
[GitLab Flavored Markdown](../../markdown.md#gitlab-flavored-markdown-gfm).
-#### 17. Comment, start a discusion, or comment and close
+#### 17. Comment, start a discussion, or comment and close
Once you wrote your comment, you can either:
diff --git a/doc/user/project/milestones/index.md b/doc/user/project/milestones/index.md
index 10e6321eb82..64bb33be547 100644
--- a/doc/user/project/milestones/index.md
+++ b/doc/user/project/milestones/index.md
@@ -10,7 +10,7 @@ Milestones allow you to organize issues and merge requests into a cohesive group
- **Project milestones** can be assigned to issues or merge requests in that project only.
- **Group milestones** can be assigned to any issue or merge request of any project in that group.
-- In the [future](https://gitlab.com/gitlab-org/gitlab-ce/issues/36862), you will be able to assign group milestones to issues and merge reqeusts of projects in [subgroups](../../group/subgroups/index.md).
+- In the [future](https://gitlab.com/gitlab-org/gitlab-ce/issues/36862), you will be able to assign group milestones to issues and merge requests of projects in [subgroups](../../group/subgroups/index.md).
## Creating milestones
diff --git a/doc/user/project/pages/getting_started_part_two.md b/doc/user/project/pages/getting_started_part_two.md
index 2274cac8ace..556bf1db116 100644
--- a/doc/user/project/pages/getting_started_part_two.md
+++ b/doc/user/project/pages/getting_started_part_two.md
@@ -50,14 +50,14 @@ created for the steps below.
1. [Fork a sample project](../../../gitlab-basics/fork-project.md) from the [Pages group](https://gitlab.com/pages)
1. Trigger a build (push a change to any file)
1. As soon as the build passes, your website will have been deployed with GitLab Pages. Your website URL will be available under your project's **Settings** > **Pages**
-1. Optionally, remove the fork relationship by navigating to your project's **Settings** > expanding **Advanced settings** and scrolling down to **Remove fork relashionship**:
+1. Optionally, remove the fork relationship by navigating to your project's **Settings** > expanding **Advanced settings** and scrolling down to **Remove fork relationship**:
- ![remove fork relashionship](img/remove_fork_relashionship.png)
+ ![remove fork relationship](img/remove_fork_relationship.png)
To turn a **project website** forked from the Pages group into a **user/group** website, you'll need to:
- Rename it to `namespace.gitlab.io`: navigate to project's **Settings** > expand **Advanced settings** > and scroll down to **Rename repository**
-- Adjust your SSG's [base URL](#urls-and-baseurls) to from `"project-name"` to `""`. This setting will be at a different place for each SSG, as each of them have their own structure and file tree. Most likelly, it will be in the SSG's config file.
+- Adjust your SSG's [base URL](#urls-and-baseurls) to from `"project-name"` to `""`. This setting will be at a different place for each SSG, as each of them have their own structure and file tree. Most likely, it will be in the SSG's config file.
> **Notes:**
>
diff --git a/doc/user/project/pages/img/remove_fork_relashionship.png b/doc/user/project/pages/img/remove_fork_relationship.png
index 67c45491f08..67c45491f08 100644
--- a/doc/user/project/pages/img/remove_fork_relashionship.png
+++ b/doc/user/project/pages/img/remove_fork_relationship.png
Binary files differ
diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md
index a65aa758198..a97ce84b861 100644
--- a/doc/user/project/pages/index.md
+++ b/doc/user/project/pages/index.md
@@ -1,23 +1,22 @@
# GitLab Pages
-With GitLab Pages you can host your website at no cost.
-
-Your files live in a GitLab project's [repository](../repository/index.md),
-from which you can deploy [static websites](#explore-gitlab-pages).
-GitLab Pages supports all static site generators (SSGs).
+With GitLab Pages it's easy to publish your project website. GitLab Pages is a hosting service for static websites, at no additional cost.
## Getting Started
-Follow the steps below to get your website live. They shouldn't take more than
-5 minutes to complete:
+[Create a project from scratch](getting_started_part_two.md#create-a-project-from-scratch)
+to get you started quickly, or,
+alternatively, start from an existing project as follows:
-- 1. [Fork](../../../gitlab-basics/fork-project.md#how-to-fork-a-project) an [example project](https://gitlab.com/pages)
-- 2. Change a file to trigger a GitLab CI/CD pipeline
-- 3. Visit your project's **Settings > Pages** to see your **website link**, and click on it. Bam! Your website is live.
+- 1. [Fork](../../../gitlab-basics/fork-project.md#how-to-fork-a-project) an [example project](https://gitlab.com/pages):
+by forking a project, you create a copy of the codebase you're forking from to start from a template instead of starting from scratch.
+- 2. Change a file to trigger a GitLab CI/CD pipeline: GitLab CI/CD will build and deploy your site to GitLab Pages.
+- 3. Visit your project's **Settings > Pages** to see your **website link**, and click on it. Bam! Your website is live! :)
_Further steps (optional):_
-- 4. Remove the [fork relationship](getting_started_part_two.md#fork-a-project-to-get-started-from) (_You don't need the relationship unless you intent to contribute back to the example project you forked from_).
+- 4. Remove the [fork relationship](getting_started_part_two.md#fork-a-project-to-get-started-from)
+(_You don't need the relationship unless you intent to contribute back to the example project you forked from_).
- 5. Make it a [user/group website](getting_started_part_one.md#user-and-group-websites)
**Watch a video with the steps above: https://www.youtube.com/watch?v=TWqh9MtT4Bg**
@@ -27,14 +26,23 @@ _Advanced options:_
- [Use a custom domain](getting_started_part_three.md#adding-your-custom-domain-to-gitlab-pages)
- Apply [SSL/TLS certification](getting_started_part_three.md#ssl-tls-certificates) to your custom domain
-## Explore GitLab Pages
+## How Does It Work?
With GitLab Pages you can create [static websites](getting_started_part_one.md#what-you-need-to-know-before-getting-started)
-for your GitLab projects, groups, or user accounts. You can use any static
-website generator: Jekyll, Middleman, Hexo, Hugo, Pelican, you name it!
+for your GitLab projects, groups, or user accounts.
+
+It supports plain static content, such as HTML, and **all** [static site generators (SSGs)](https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/), such as Jekyll, Middleman, Hexo, Hugo, and Pelican.
+
Connect as many custom domains as you like and bring your own TLS certificate
to secure them.
+Your files live in a project [repository](../repository/index.md) on GitLab.
+[GitLab CI](../../../ci/README.md) picks up those files and makes them available at, typically,
+`http://<username>.gilab.io/<projectname>`. Please read through the docs on
+[GitLab Pages domains](getting_started_part_one.md#gitlab-pages-domain) for more info.
+
+## Explore GitLab Pages
+
Read the following tutorials to know more about:
- [Static websites and GitLab Pages domains](getting_started_part_one.md): Understand what is a static website, and how GitLab Pages default domains work
diff --git a/doc/user/project/repository/reducing_the_repo_size_using_git.md b/doc/user/project/repository/reducing_the_repo_size_using_git.md
index 08805a4dc99..a06ecc3220f 100644
--- a/doc/user/project/repository/reducing_the_repo_size_using_git.md
+++ b/doc/user/project/repository/reducing_the_repo_size_using_git.md
@@ -1,6 +1,6 @@
# Reducing the repository size using Git
-A GitLab Entrerprise Edition administrator can set a [repository size limit][admin-repo-size]
+A GitLab Enterprise Edition administrator can set a [repository size limit][admin-repo-size]
which will prevent you to exceed it.
When a project has reached its size limit, you will not be able to push to it,
diff --git a/doc/user/search/index.md b/doc/user/search/index.md
index 2b23c494dc4..4f1b96b775c 100644
--- a/doc/user/search/index.md
+++ b/doc/user/search/index.md
@@ -96,7 +96,7 @@ On the field **Filter by name**, type the project or group name you want to find
will filter them for you as you type.
You can also look for the projects you starred (**Starred projects**), and **Explore** all
-public and internal projects available in GitLab.com, from which you can filter by visibitily,
+public and internal projects available in GitLab.com, from which you can filter by visibility,
through **Trending**, best rated with **Most starts**, or **All** of them.
You can also sort them by **Name**, **Last created**, **Oldest created**, **Last updated**,
diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
index 104ac0cf31b..0d592a6d43e 100644
--- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
+++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
@@ -243,7 +243,7 @@ GitLab checks files to detect LFS pointers on push. If LFS pointers are detected
Verify that LFS in installed locally and consider a manual push with `git lfs push --all`.
-If you are storing LFS files outside of GitLab you can disable LFS on the project by settting `lfs_enabled: false` with the [projects api](../../api/projects.md#edit-project).
+If you are storing LFS files outside of GitLab you can disable LFS on the project by setting `lfs_enabled: false` with the [projects api](../../api/projects.md#edit-project).
### Hosting LFS objects externally
@@ -251,13 +251,4 @@ It is possible to host LFS objects externally by setting a custom LFS url with `
Because GitLab verifies the existence of objects referenced by LFS pointers, push will fail when LFS is enabled for the project.
-LFS can be disabled for a project by Owners and Masters using the [Project API](../../api/projects.md#edit-project).
-
-```bash
-curl --request PUT \
- --url https://example.com/api/v4/projects/<PROJECT_ID> \
- --header 'Private-Token: <YOUR_PRIVATE_TOKEN>' \
- --data 'lfs_enabled=false'
-```
-
-Note, `<PROJECT_ID>` can also be substituted with a [namespaced path](../../api/README.md#namespaced-path-encoding).
+LFS can be disabled from the [Project settings](../../user/project/settings/index.md).
diff --git a/features/project/source/markdown_render.feature b/features/project/source/markdown_render.feature
deleted file mode 100644
index fe4466ad241..00000000000
--- a/features/project/source/markdown_render.feature
+++ /dev/null
@@ -1,147 +0,0 @@
-Feature: Project Source Markdown Render
- Background:
- Given I sign in as a user
- And I own project "Delta"
- And I visit markdown branch
-
- # Tree README
-
- @javascript
- Scenario: Tree view should have correct links in README
- Given I go directory which contains README file
- And I click on a relative link in README
- Then I should see the correct markdown
-
- @javascript
- Scenario: I browse files from markdown branch
- Then I should see files from repository in markdown
- And I should see rendered README which contains correct links
- And I click on Gitlab API in README
- Then I should see correct document rendered
-
- @javascript
- Scenario: I view README in markdown branch
- Then I should see files from repository in markdown
- And I should see rendered README which contains correct links
- And I click on Rake tasks in README
- Then I should see correct directory rendered
-
- @javascript
- Scenario: I view README in markdown branch to see reference links to directory
- Then I should see files from repository in markdown
- And I should see rendered README which contains correct links
- And I click on GitLab API doc directory in README
- Then I should see correct doc/api directory rendered
-
- @javascript
- Scenario: I view README in markdown branch to see reference links to file
- Then I should see files from repository in markdown
- And I should see rendered README which contains correct links
- And I click on Maintenance in README
- Then I should see correct maintenance file rendered
-
- @javascript
- Scenario: README headers should have header links
- Then I should see rendered README which contains correct links
- And Header "Application details" should have correct id and link
-
- # Blob
-
- @javascript
- Scenario: I navigate to doc directory to view documentation in markdown
- And I navigate to the doc/api/README
- And I see correct file rendered
- And I click on users in doc/api/README
- Then I should see the correct document file
-
- @javascript
- Scenario: I navigate to doc directory to view user doc in markdown
- And I navigate to the doc/api/README
- And I see correct file rendered
- And I click on raketasks in doc/api/README
- Then I should see correct directory rendered
-
- @javascript
- Scenario: I navigate to doc directory to view user doc in markdown
- And I navigate to the doc/api/README
- And Header "GitLab API" should have correct id and link
-
- # Markdown branch
-
- @javascript
- Scenario: I browse files from markdown branch
- When I visit markdown branch
- Then I should see files from repository in markdown branch
- And I should see rendered README which contains correct links
- And I click on Gitlab API in README
- Then I should see correct document rendered for markdown branch
-
- @javascript
- Scenario: I browse directory from markdown branch
- When I visit markdown branch
- Then I should see files from repository in markdown branch
- And I should see rendered README which contains correct links
- And I click on Rake tasks in README
- Then I should see correct directory rendered for markdown branch
-
- @javascript
- Scenario: I navigate to doc directory to view documentation in markdown branch
- When I visit markdown branch
- And I navigate to the doc/api/README
- And I see correct file rendered in markdown branch
- And I click on users in doc/api/README
- Then I should see the users document file in markdown branch
-
- @javascript
- Scenario: I navigate to doc directory to view user doc in markdown branch
- When I visit markdown branch
- And I navigate to the doc/api/README
- And I see correct file rendered in markdown branch
- And I click on raketasks in doc/api/README
- Then I should see correct directory rendered for markdown branch
-
- @javascript
- Scenario: Tree markdown links view empty urls should have correct urls
- When I visit markdown branch
- Then The link with text "empty" should have url "tree/markdown"
- When I visit markdown branch "README.md" blob
- Then The link with text "empty" should have url "blob/markdown/README.md"
- When I visit markdown branch "d" tree
- Then The link with text "empty" should have url "tree/markdown/d"
- When I visit markdown branch "d/README.md" blob
- Then The link with text "empty" should have url "blob/markdown/d/README.md"
-
- # "ID" means "#id" on the tests below, because we are unable to escape the hash sign.
- # which Spinach interprets as the start of a comment.
- @javascript
- Scenario: All markdown links with ids should have correct urls
- When I visit markdown branch
- Then The link with text "ID" should have url "tree/markdownID"
- Then The link with text "/ID" should have url "tree/markdownID"
- Then The link with text "README.mdID" should have url "blob/markdown/README.mdID"
- Then The link with text "d/README.mdID" should have url "blob/markdown/d/README.mdID"
- When I visit markdown branch "README.md" blob
- Then The link with text "ID" should have url "blob/markdown/README.mdID"
- Then The link with text "/ID" should have url "blob/markdown/README.mdID"
- Then The link with text "README.mdID" should have url "blob/markdown/README.mdID"
- Then The link with text "d/README.mdID" should have url "blob/markdown/d/README.mdID"
-
- # Wiki
-
- Scenario: I create a wiki page with different links
- Given I go to wiki page
- And I add various links to the wiki page
- Then Wiki page should have added links
- And I click on test link
- Then I see new wiki page named test
- When I go back to wiki page home
- And I click on GitLab API doc link
- Then I see Gitlab API document
- When I go back to wiki page home
- And I click on Rake tasks link
- Then I see Rake tasks directory
-
- Scenario: Wiki headers should have should have ids generated for them.
- Given I go to wiki page
- And I add a header to the wiki page
- Then Wiki header should have correct id and link
diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb
index fd51ee1a316..82b931b2246 100644
--- a/features/steps/project/forked_merge_requests.rb
+++ b/features/steps/project/forked_merge_requests.rb
@@ -53,7 +53,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
first('.js-source-branch').click
wait_for_requests
- first('.dropdown-source-branch .dropdown-content a', text: 'fix').click
+ first('.js-source-branch-dropdown .dropdown-content a', text: 'fix').click
click_button "Compare branches and continue"
diff --git a/features/steps/project/source/markdown_render.rb b/features/steps/project/source/markdown_render.rb
deleted file mode 100644
index db99c179439..00000000000
--- a/features/steps/project/source/markdown_render.rb
+++ /dev/null
@@ -1,317 +0,0 @@
-# If you need to modify the existing seed repository for your tests,
-# it is recommended that you make the changes on the `markdown` branch of the seed project repository,
-# which should only be used by tests in this file. See `/spec/factories.rb#project` for more info.
-class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedMarkdown
- include WaitForRequests
-
- step 'I own project "Delta"' do
- @project = ::Project.find_by(name: "Delta")
- @project ||= create(:project, :repository, name: "Delta", namespace: @user.namespace)
- @project.add_master(@user)
- end
-
- step 'I should see files from repository in markdown' do
- expect(current_path).to eq project_tree_path(@project, "markdown")
- expect(page).to have_content "README.md"
- expect(page).to have_content "CHANGELOG"
- end
-
- step 'I should see rendered README which contains correct links' do
- expect(page).to have_content "Welcome to GitLab GitLab is a free project and repository management application"
- expect(page).to have_link "GitLab API doc"
- expect(page).to have_link "GitLab API website"
- expect(page).to have_link "Rake tasks"
- expect(page).to have_link "backup and restore procedure"
- expect(page).to have_link "GitLab API doc directory"
- expect(page).to have_link "Maintenance"
- end
-
- step 'I click on Gitlab API in README' do
- click_link "GitLab API doc"
- end
-
- step 'I should see correct document rendered' do
- expect(current_path).to eq project_blob_path(@project, "markdown/doc/api/README.md")
- wait_for_requests
- expect(page).to have_content "All API requests require authentication"
- end
-
- step 'I click on Rake tasks in README' do
- click_link "Rake tasks"
- end
-
- step 'I should see correct directory rendered' do
- expect(current_path).to eq project_tree_path(@project, "markdown/doc/raketasks")
- expect(page).to have_content "backup_restore.md"
- expect(page).to have_content "maintenance.md"
- end
-
- step 'I click on GitLab API doc directory in README' do
- click_link "GitLab API doc directory"
- end
-
- step 'I should see correct doc/api directory rendered' do
- expect(current_path).to eq project_tree_path(@project, "markdown/doc/api")
- expect(page).to have_content "README.md"
- expect(page).to have_content "users.md"
- end
-
- step 'I click on Maintenance in README' do
- click_link "Maintenance"
- end
-
- step 'I should see correct maintenance file rendered' do
- expect(current_path).to eq project_blob_path(@project, "markdown/doc/raketasks/maintenance.md")
- wait_for_requests
- expect(page).to have_content "bundle exec rake gitlab:env:info RAILS_ENV=production"
- end
-
- step 'I click on link "empty" in the README' do
- page.within('.readme-holder') do
- click_link "empty"
- end
- end
-
- step 'I click on link "id" in the README' do
- page.within('.readme-holder') do
- click_link "#id"
- end
- end
-
- step 'I navigate to the doc/api/README' do
- page.within '.tree-table' do
- click_link "doc"
- end
-
- page.within '.tree-table' do
- click_link "api"
- end
-
- wait_for_requests
-
- page.within '.tree-table' do
- click_link "README.md"
- end
- end
-
- step 'I see correct file rendered' do
- expect(current_path).to eq project_blob_path(@project, "markdown/doc/api/README.md")
- wait_for_requests
- expect(page).to have_content "Contents"
- expect(page).to have_link "Users"
- expect(page).to have_link "Rake tasks"
- end
-
- step 'I click on users in doc/api/README' do
- click_link "Users"
- end
-
- step 'I should see the correct document file' do
- expect(current_path).to eq project_blob_path(@project, "markdown/doc/api/users.md")
- expect(page).to have_content "Get a list of users."
- end
-
- step 'I click on raketasks in doc/api/README' do
- click_link "Rake tasks"
- end
-
- # Markdown branch
-
- When 'I visit markdown branch' do
- visit project_tree_path(@project, "markdown")
- wait_for_requests
- end
-
- When 'I visit markdown branch "README.md" blob' do
- visit project_blob_path(@project, "markdown/README.md")
- end
-
- When 'I visit markdown branch "d" tree' do
- visit project_tree_path(@project, "markdown/d")
- end
-
- When 'I visit markdown branch "d/README.md" blob' do
- visit project_blob_path(@project, "markdown/d/README.md")
- end
-
- step 'I should see files from repository in markdown branch' do
- expect(current_path).to eq project_tree_path(@project, "markdown")
- expect(page).to have_content "README.md"
- expect(page).to have_content "CHANGELOG"
- end
-
- step 'I see correct file rendered in markdown branch' do
- expect(current_path).to eq project_blob_path(@project, "markdown/doc/api/README.md")
- wait_for_requests
- expect(page).to have_content "Contents"
- expect(page).to have_link "Users"
- expect(page).to have_link "Rake tasks"
- end
-
- step 'I should see correct document rendered for markdown branch' do
- expect(current_path).to eq project_blob_path(@project, "markdown/doc/api/README.md")
- wait_for_requests
- expect(page).to have_content "All API requests require authentication"
- end
-
- step 'I should see correct directory rendered for markdown branch' do
- expect(current_path).to eq project_tree_path(@project, "markdown/doc/raketasks")
- expect(page).to have_content "backup_restore.md"
- expect(page).to have_content "maintenance.md"
- end
-
- step 'I should see the users document file in markdown branch' do
- expect(current_path).to eq project_blob_path(@project, "markdown/doc/api/users.md")
- expect(page).to have_content "Get a list of users."
- end
-
- # Expected link contents
-
- step 'The link with text "empty" should have url "tree/markdown"' do
- wait_for_requests
- find('a', text: /^empty$/)['href'] == current_host + project_tree_path(@project, "markdown")
- end
-
- step 'The link with text "empty" should have url "blob/markdown/README.md"' do
- find('a', text: /^empty$/)['href'] == current_host + project_blob_path(@project, "markdown/README.md")
- end
-
- step 'The link with text "empty" should have url "tree/markdown/d"' do
- find('a', text: /^empty$/)['href'] == current_host + project_tree_path(@project, "markdown/d")
- end
-
- step 'The link with text "empty" should have '\
- 'url "blob/markdown/d/README.md"' do
- find('a', text: /^empty$/)['href'] == current_host + project_blob_path(@project, "markdown/d/README.md")
- 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'
- end
-
- step 'The link with text "/ID" should have url "tree/markdownID"' do
- find('a', text: %r{^/#id$})['href'] == current_host + project_tree_path(@project, "markdown") + '#id'
- end
-
- step 'The link with text "README.mdID" '\
- 'should have url "blob/markdown/README.mdID"' do
- find('a', text: /^README.md#id$/)['href'] == current_host + project_blob_path(@project, "markdown/README.md") + '#id'
- end
-
- step 'The link with text "d/README.mdID" should have '\
- 'url "blob/markdown/d/README.mdID"' do
- 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
- wait_for_requests
- find('a', text: /^#id$/)['href'] == current_host + project_blob_path(@project, "markdown/README.md") + '#id'
- end
-
- step 'The link with text "/ID" should have url "blob/markdown/README.mdID"' do
- find('a', text: %r{^/#id$})['href'] == current_host + project_blob_path(@project, "markdown/README.md") + '#id'
- end
-
- # Wiki
-
- step 'I go to wiki page' do
- first(:link, "Wiki").click
- expect(current_path).to eq project_wiki_path(@project, "home")
- end
-
- step 'I add various links to the wiki page' do
- fill_in "wiki[content]", with: "[test](test)\n[GitLab API doc](api)\n[Rake tasks](raketasks)\n"
- fill_in "wiki[message]", with: "Adding links to wiki"
- page.within '.wiki-form' do
- click_button "Create page"
- end
- end
-
- step 'Wiki page should have added links' do
- expect(current_path).to eq project_wiki_path(@project, "home")
- expect(page).to have_content "test GitLab API doc Rake tasks"
- end
-
- step 'I add a header to the wiki page' do
- fill_in "wiki[content]", with: "# Wiki header\n"
- fill_in "wiki[message]", with: "Add header to wiki"
- page.within '.wiki-form' do
- click_button "Create page"
- end
- end
-
- step 'Wiki header should have correct id and link' do
- header_should_have_correct_id_and_link(1, 'Wiki header', 'wiki-header')
- end
-
- step 'I click on test link' do
- click_link "test"
- end
-
- step 'I see new wiki page named test' do
- expect(current_path).to eq project_wiki_path(@project, "test")
-
- page.within(:css, ".nav-text") do
- expect(page).to have_content "Test"
- expect(page).to have_content "Create Page"
- end
- end
-
- When 'I go back to wiki page home' do
- visit project_wiki_path(@project, "home")
- expect(current_path).to eq project_wiki_path(@project, "home")
- end
-
- step 'I click on GitLab API doc link' do
- click_link "GitLab API"
- end
-
- step 'I see Gitlab API document' do
- expect(current_path).to eq project_wiki_path(@project, "api")
-
- page.within(:css, ".nav-text") do
- expect(page).to have_content "Create"
- expect(page).to have_content "Api"
- end
- end
-
- step 'I click on Rake tasks link' do
- click_link "Rake tasks"
- end
-
- step 'I see Rake tasks directory' do
- expect(current_path).to eq project_wiki_path(@project, "raketasks")
-
- page.within(:css, ".nav-text") do
- expect(page).to have_content "Create"
- expect(page).to have_content "Rake"
- end
- end
-
- step 'I go directory which contains README file' do
- visit project_tree_path(@project, "markdown/doc/api")
- expect(current_path).to eq project_tree_path(@project, "markdown/doc/api")
- end
-
- step 'I click on a relative link in README' do
- click_link "Users"
- end
-
- step 'I should see the correct markdown' do
- expect(current_path).to eq project_blob_path(@project, "markdown/doc/api/users.md")
- wait_for_requests
- expect(page).to have_content "List users"
- end
-
- step 'Header "Application details" should have correct id and link' do
- wait_for_requests
- header_should_have_correct_id_and_link(2, 'Application details', 'application-details')
- end
-
- step 'Header "GitLab API" should have correct id and link' do
- header_should_have_correct_id_and_link(1, 'GitLab API', 'gitlab-api')
- end
-end
diff --git a/features/steps/shared/markdown.rb b/features/steps/shared/markdown.rb
index 9d522936fb6..65118f07ca2 100644
--- a/features/steps/shared/markdown.rb
+++ b/features/steps/shared/markdown.rb
@@ -1,15 +1,6 @@
module SharedMarkdown
include Spinach::DSL
- def header_should_have_correct_id_and_link(level, text, id, parent = ".wiki")
- node = find("#{parent} h#{level} a#user-content-#{id}")
- expect(node[:href]).to end_with "##{id}"
-
- # Work around a weird Capybara behavior where calling `parent` on a node
- # returns the whole document, not the node's actual parent element
- expect(find(:xpath, "#{node.path}/..").text).to eq text
- end
-
step 'I should not see the Markdown preview' do
expect(find('.gfm-form .js-md-preview')).not_to be_visible
end
diff --git a/lib/api/discussions.rb b/lib/api/discussions.rb
index 7975f35ab1e..13c34e3473a 100644
--- a/lib/api/discussions.rb
+++ b/lib/api/discussions.rb
@@ -5,11 +5,12 @@ module API
before { authenticate! }
- NOTEABLE_TYPES = [Issue, Snippet].freeze
+ NOTEABLE_TYPES = [Issue, Snippet, MergeRequest, Commit].freeze
NOTEABLE_TYPES.each do |noteable_type|
parent_type = noteable_type.parent_class.to_s.underscore
noteables_str = noteable_type.to_s.underscore.pluralize
+ noteables_path = noteable_type == Commit ? "repository/#{noteables_str}" : noteables_str
params do
requires :id, type: String, desc: "The ID of a #{parent_type}"
@@ -19,14 +20,12 @@ module API
success Entities::Discussion
end
params do
- requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
use :pagination
end
- get ":id/#{noteables_str}/:noteable_id/discussions" do
+ get ":id/#{noteables_path}/:noteable_id/discussions" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
- break not_found!("Discussions") unless can?(current_user, noteable_read_ability_name(noteable), noteable)
-
notes = noteable.notes
.inc_relations_for_view
.includes(:noteable)
@@ -43,13 +42,13 @@ module API
end
params do
requires :discussion_id, type: String, desc: 'The ID of a discussion'
- requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
end
- get ":id/#{noteables_str}/:noteable_id/discussions/:discussion_id" do
+ get ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
notes = readable_discussion_notes(noteable, params[:discussion_id])
- if notes.empty? || !can?(current_user, noteable_read_ability_name(noteable), noteable)
+ if notes.empty?
break not_found!("Discussion")
end
@@ -62,19 +61,36 @@ module API
success Entities::Discussion
end
params do
- requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
requires :body, type: String, desc: 'The content of a note'
optional :created_at, type: String, desc: 'The creation date of the note'
+ optional :position, type: Hash do
+ requires :base_sha, type: String, desc: 'Base commit SHA in the source branch'
+ requires :start_sha, type: String, desc: 'SHA referencing commit in target branch'
+ requires :head_sha, type: String, desc: 'SHA referencing HEAD of this merge request'
+ requires :position_type, type: String, desc: 'Type of the position reference', values: %w(text image)
+ optional :new_path, type: String, desc: 'File path after change'
+ optional :new_line, type: Integer, desc: 'Line number after change'
+ optional :old_path, type: String, desc: 'File path before change'
+ optional :old_line, type: Integer, desc: 'Line number before change'
+ optional :width, type: Integer, desc: 'Width of the image'
+ optional :height, type: Integer, desc: 'Height of the image'
+ optional :x, type: Integer, desc: 'X coordinate in the image'
+ optional :y, type: Integer, desc: 'Y coordinate in the image'
+ end
end
- post ":id/#{noteables_str}/:noteable_id/discussions" do
+ post ":id/#{noteables_path}/:noteable_id/discussions" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
+ type = params[:position] ? 'DiffNote' : 'DiscussionNote'
+ id_key = noteable.is_a?(Commit) ? :commit_id : :noteable_id
opts = {
note: params[:body],
created_at: params[:created_at],
- type: 'DiscussionNote',
+ type: type,
noteable_type: noteables_str.classify,
- noteable_id: noteable.id
+ position: params[:position],
+ id_key => noteable.id
}
note = create_note(noteable, opts)
@@ -91,13 +107,13 @@ module API
end
params do
requires :discussion_id, type: String, desc: 'The ID of a discussion'
- requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
end
- get ":id/#{noteables_str}/:noteable_id/discussions/:discussion_id/notes" do
+ get ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
notes = readable_discussion_notes(noteable, params[:discussion_id])
- if notes.empty? || !can?(current_user, noteable_read_ability_name(noteable), noteable)
+ if notes.empty?
break not_found!("Notes")
end
@@ -108,12 +124,12 @@ module API
success Entities::Note
end
params do
- requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
requires :discussion_id, type: String, desc: 'The ID of a discussion'
requires :body, type: String, desc: 'The content of a note'
optional :created_at, type: String, desc: 'The creation date of the note'
end
- post ":id/#{noteables_str}/:noteable_id/discussions/:discussion_id/notes" do
+ post ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
notes = readable_discussion_notes(noteable, params[:discussion_id])
@@ -139,11 +155,11 @@ module API
success Entities::Note
end
params do
- requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
requires :discussion_id, type: String, desc: 'The ID of a discussion'
requires :note_id, type: Integer, desc: 'The ID of a note'
end
- get ":id/#{noteables_str}/:noteable_id/discussions/:discussion_id/notes/:note_id" do
+ get ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes/:note_id" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
get_note(noteable, params[:note_id])
@@ -153,30 +169,52 @@ module API
success Entities::Note
end
params do
- requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
requires :discussion_id, type: String, desc: 'The ID of a discussion'
requires :note_id, type: Integer, desc: 'The ID of a note'
- requires :body, type: String, desc: 'The content of a note'
+ optional :body, type: String, desc: 'The content of a note'
+ optional :resolved, type: Boolean, desc: 'Mark note resolved/unresolved'
+ exactly_one_of :body, :resolved
end
- put ":id/#{noteables_str}/:noteable_id/discussions/:discussion_id/notes/:note_id" do
+ put ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes/:note_id" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
- update_note(noteable, params[:note_id])
+ if params[:resolved].nil?
+ update_note(noteable, params[:note_id])
+ else
+ resolve_note(noteable, params[:note_id], params[:resolved])
+ end
end
desc "Delete a comment in a #{noteable_type.to_s.downcase} discussion" do
success Entities::Note
end
params do
- requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
requires :discussion_id, type: String, desc: 'The ID of a discussion'
requires :note_id, type: Integer, desc: 'The ID of a note'
end
- delete ":id/#{noteables_str}/:noteable_id/discussions/:discussion_id/notes/:note_id" do
+ delete ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes/:note_id" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
delete_note(noteable, params[:note_id])
end
+
+ if Noteable::RESOLVABLE_TYPES.include?(noteable_type.to_s)
+ desc "Resolve/unresolve an existing #{noteable_type.to_s.downcase} discussion" do
+ success Entities::Discussion
+ end
+ params do
+ requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
+ requires :discussion_id, type: String, desc: 'The ID of a discussion'
+ requires :resolved, type: Boolean, desc: 'Mark discussion resolved/unresolved'
+ end
+ put ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id" do
+ noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
+
+ resolve_discussion(noteable, params[:discussion_id], params[:resolved])
+ end
+ end
end
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index f28c4bcc784..6ff1c0624e5 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -291,6 +291,10 @@ module API
end
end
+ class DiffRefs < Grape::Entity
+ expose :base_sha, :head_sha, :start_sha
+ end
+
class Commit < Grape::Entity
expose :id, :short_id, :title, :created_at
expose :parent_ids
@@ -606,6 +610,8 @@ module API
merge_request.metrics&.pipeline
end
+ expose :diff_refs, using: Entities::DiffRefs
+
def build_available?(options)
options[:project]&.feature_available?(:builds, options[:current_user])
end
@@ -647,6 +653,11 @@ module API
expose :id, :key, :created_at
end
+ class DiffPosition < Grape::Entity
+ expose :base_sha, :start_sha, :head_sha, :old_path, :new_path,
+ :position_type
+ end
+
class Note < Grape::Entity
# Only Issue and MergeRequest have iid
NOTEABLE_TYPES_WITH_IID = %w(Issue MergeRequest).freeze
@@ -660,6 +671,14 @@ module API
expose :system?, as: :system
expose :noteable_id, :noteable_type
+ expose :position, if: ->(note, options) { note.diff_note? } do |note|
+ note.position.to_h
+ end
+
+ expose :resolvable?, as: :resolvable
+ expose :resolved?, as: :resolved, if: ->(note, options) { note.resolvable? }
+ expose :resolved_by, using: Entities::UserBasic, if: ->(note, options) { note.resolvable? }
+
# Avoid N+1 queries as much as possible
expose(:noteable_iid) { |note| note.noteable.iid if NOTEABLE_TYPES_WITH_IID.include?(note.noteable_type) }
end
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 4a4df1b8b9e..92e3d5cc10a 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -37,13 +37,11 @@ module API
use :pagination
end
- def find_groups(params)
- find_params = {
- all_available: params[:all_available],
- custom_attributes: params[:custom_attributes],
- owned: params[:owned]
- }
- find_params[:parent] = find_group!(params[:id]) if params[:id]
+ def find_groups(params, parent_id = nil)
+ find_params = params.slice(:all_available, :custom_attributes, :owned)
+ find_params[:parent] = find_group!(parent_id) if parent_id
+ find_params[:all_available] =
+ find_params.fetch(:all_available, current_user&.full_private_access?)
groups = GroupsFinder.new(current_user, find_params).execute
groups = groups.search(params[:search]) if params[:search].present?
@@ -85,7 +83,7 @@ module API
use :with_custom_attributes
end
get do
- groups = find_groups(params)
+ groups = find_groups(declared_params(include_missing: false), params[:id])
present_groups params, groups
end
@@ -213,7 +211,7 @@ module API
use :with_custom_attributes
end
get ":id/subgroups" do
- groups = find_groups(params)
+ groups = find_groups(declared_params(include_missing: false), params[:id])
present_groups params, groups
end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index b8657cd7ee4..2ed331d4fd2 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -171,6 +171,10 @@ module API
MergeRequestsFinder.new(current_user, project_id: user_project.id).find_by!(iid: iid)
end
+ def find_project_commit(id)
+ user_project.commit_by(oid: id)
+ end
+
def find_project_snippet(id)
finder_params = { project: user_project }
SnippetsFinder.new(current_user, finder_params).find(id)
diff --git a/lib/api/helpers/custom_attributes.rb b/lib/api/helpers/custom_attributes.rb
index 70e4eda95f8..10d652e33f5 100644
--- a/lib/api/helpers/custom_attributes.rb
+++ b/lib/api/helpers/custom_attributes.rb
@@ -7,6 +7,9 @@ module API
helpers do
params :with_custom_attributes do
optional :with_custom_attributes, type: Boolean, default: false, desc: 'Include custom attributes in the response'
+
+ optional :custom_attributes, type: Hash,
+ desc: 'Filter with custom attributes'
end
def with_custom_attributes(collection_or_resource, options = {})
diff --git a/lib/api/helpers/notes_helpers.rb b/lib/api/helpers/notes_helpers.rb
index b74b8149834..b4bfb677d72 100644
--- a/lib/api/helpers/notes_helpers.rb
+++ b/lib/api/helpers/notes_helpers.rb
@@ -21,6 +21,23 @@ module API
end
end
+ def resolve_note(noteable, note_id, resolved)
+ note = noteable.notes.find(note_id)
+
+ authorize! :resolve_note, note
+
+ bad_request!("Note is not resolvable") unless note.resolvable?
+
+ if resolved
+ parent = noteable_parent(noteable)
+ ::Notes::ResolveService.new(parent, current_user).execute(note)
+ else
+ note.unresolve!
+ end
+
+ present note, with: Entities::Note
+ end
+
def delete_note(noteable, note_id)
note = noteable.notes.find(note_id)
@@ -35,7 +52,7 @@ module API
def get_note(noteable, note_id)
note = noteable.notes.with_metadata.find(params[:note_id])
- can_read_note = can?(current_user, noteable_read_ability_name(noteable), noteable) && !note.cross_reference_not_visible_for?(current_user)
+ can_read_note = !note.cross_reference_not_visible_for?(current_user)
if can_read_note
present note, with: Entities::Note
@@ -49,7 +66,20 @@ module API
end
def find_noteable(parent, noteables_str, noteable_id)
- public_send("find_#{parent}_#{noteables_str.singularize}", noteable_id) # rubocop:disable GitlabSecurity/PublicSend
+ noteable = public_send("find_#{parent}_#{noteables_str.singularize}", noteable_id) # rubocop:disable GitlabSecurity/PublicSend
+
+ readable =
+ if noteable.is_a?(Commit)
+ # for commits there is not :read_commit policy, check if user
+ # has :read_note permission on the commit's project
+ can?(current_user, :read_note, user_project)
+ else
+ can?(current_user, noteable_read_ability_name(noteable), noteable)
+ end
+
+ return not_found!(noteables_str) unless readable
+
+ noteable
end
def noteable_parent(noteable)
@@ -57,11 +87,8 @@ module API
end
def create_note(noteable, opts)
- noteables_str = noteable.model_name.to_s.underscore.pluralize
-
- return not_found!(noteables_str) unless can?(current_user, noteable_read_ability_name(noteable), noteable)
-
- authorize! :create_note, noteable
+ policy_object = noteable.is_a?(Commit) ? user_project : noteable
+ authorize!(:create_note, policy_object)
parent = noteable_parent(noteable)
@@ -73,6 +100,21 @@ module API
project = parent if parent.is_a?(Project)
::Notes::CreateService.new(project, current_user, opts).execute
end
+
+ def resolve_discussion(noteable, discussion_id, resolved)
+ discussion = noteable.find_discussion(discussion_id)
+
+ forbidden! unless discussion.can_resolve?(current_user)
+
+ if resolved
+ parent = noteable_parent(noteable)
+ ::Discussions::ResolveService.new(parent, current_user, merge_request: noteable).execute(discussion)
+ else
+ discussion.unresolve!
+ end
+
+ present discussion, with: Entities::Discussion
+ end
end
end
end
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index 69f1df6b341..39923e6d5b5 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -31,23 +31,19 @@ module API
get ":id/#{noteables_str}/:noteable_id/notes" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
- if can?(current_user, noteable_read_ability_name(noteable), noteable)
- # We exclude notes that are cross-references and that cannot be viewed
- # by the current user. By doing this exclusion at this level and not
- # at the DB query level (which we cannot in that case), the current
- # page can have less elements than :per_page even if
- # there's more than one page.
- raw_notes = noteable.notes.with_metadata.reorder(params[:order_by] => params[:sort])
- notes =
- # paginate() only works with a relation. This could lead to a
- # mismatch between the pagination headers info and the actual notes
- # array returned, but this is really a edge-case.
- paginate(raw_notes)
- .reject { |n| n.cross_reference_not_visible_for?(current_user) }
- present notes, with: Entities::Note
- else
- not_found!("Notes")
- end
+ # We exclude notes that are cross-references and that cannot be viewed
+ # by the current user. By doing this exclusion at this level and not
+ # at the DB query level (which we cannot in that case), the current
+ # page can have less elements than :per_page even if
+ # there's more than one page.
+ raw_notes = noteable.notes.with_metadata.reorder(params[:order_by] => params[:sort])
+ notes =
+ # paginate() only works with a relation. This could lead to a
+ # mismatch between the pagination headers info and the actual notes
+ # array returned, but this is really a edge-case.
+ paginate(raw_notes)
+ .reject { |n| n.cross_reference_not_visible_for?(current_user) }
+ present notes, with: Entities::Note
end
desc "Get a single #{noteable_type.to_s.downcase} note" do
diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb
index d2b8b832e4e..735591fedd5 100644
--- a/lib/api/pipelines.rb
+++ b/lib/api/pipelines.rb
@@ -19,6 +19,7 @@ module API
optional :status, type: String, values: HasStatus::AVAILABLE_STATUSES,
desc: 'The status of pipelines'
optional :ref, type: String, desc: 'The ref of pipelines'
+ optional :sha, type: String, desc: 'The sha of pipelines'
optional :yaml_errors, type: Boolean, desc: 'Returns pipelines with invalid configurations'
optional :name, type: String, desc: 'The name of the user who triggered pipelines'
optional :username, type: String, desc: 'The username of the user who triggered pipelines'
diff --git a/lib/banzai/filter/commit_trailers_filter.rb b/lib/banzai/filter/commit_trailers_filter.rb
index ef16df1f3ae..7b55e8b36f6 100644
--- a/lib/banzai/filter/commit_trailers_filter.rb
+++ b/lib/banzai/filter/commit_trailers_filter.rb
@@ -13,7 +13,6 @@ module Banzai
# * https://git.wiki.kernel.org/index.php/CommitMessageConventions
class CommitTrailersFilter < HTML::Pipeline::Filter
include ActionView::Helpers::TagHelper
- include ApplicationHelper
include AvatarsHelper
TRAILER_REGEXP = /(?<label>[[:alpha:]-]+-by:)/i.freeze
diff --git a/lib/gitlab/background_migration/migrate_stage_index.rb b/lib/gitlab/background_migration/migrate_stage_index.rb
new file mode 100644
index 00000000000..f90f35a913d
--- /dev/null
+++ b/lib/gitlab/background_migration/migrate_stage_index.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ class MigrateStageIndex
+ def perform(start_id, stop_id)
+ migrate_stage_index_sql(start_id.to_i, stop_id.to_i).tap do |sql|
+ ActiveRecord::Base.connection.execute(sql)
+ end
+ end
+
+ private
+
+ def migrate_stage_index_sql(start_id, stop_id)
+ if Gitlab::Database.postgresql?
+ <<~SQL
+ WITH freqs AS (
+ SELECT stage_id, stage_idx, COUNT(*) AS freq FROM ci_builds
+ WHERE stage_id BETWEEN #{start_id} AND #{stop_id}
+ AND stage_idx IS NOT NULL
+ GROUP BY stage_id, stage_idx
+ ), indexes AS (
+ SELECT DISTINCT stage_id, last_value(stage_idx)
+ OVER (PARTITION BY stage_id ORDER BY freq ASC) AS index
+ FROM freqs
+ )
+
+ UPDATE ci_stages SET position = indexes.index
+ FROM indexes WHERE indexes.stage_id = ci_stages.id
+ AND ci_stages.position IS NULL;
+ SQL
+ else
+ <<~SQL
+ UPDATE ci_stages
+ SET position =
+ (SELECT stage_idx FROM ci_builds
+ WHERE ci_builds.stage_id = ci_stages.id
+ GROUP BY ci_builds.stage_idx ORDER BY COUNT(*) DESC LIMIT 1)
+ WHERE ci_stages.id BETWEEN #{start_id} AND #{stop_id}
+ AND ci_stages.position IS NULL
+ SQL
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/base_doorkeeper_controller.rb b/lib/gitlab/base_doorkeeper_controller.rb
new file mode 100644
index 00000000000..e4227af25d2
--- /dev/null
+++ b/lib/gitlab/base_doorkeeper_controller.rb
@@ -0,0 +1,8 @@
+# This is a base controller for doorkeeper.
+# It adds the `can?` helper used in the views.
+module Gitlab
+ class BaseDoorkeeperController < ActionController::Base
+ include Gitlab::Allowable
+ helper_method :can?
+ end
+end
diff --git a/lib/gitlab/ci/cron_parser.rb b/lib/gitlab/ci/cron_parser.rb
index 551483d0aaa..73f36735e35 100644
--- a/lib/gitlab/ci/cron_parser.rb
+++ b/lib/gitlab/ci/cron_parser.rb
@@ -6,7 +6,7 @@ module Gitlab
def initialize(cron, cron_timezone = 'UTC')
@cron = cron
- @cron_timezone = ActiveSupport::TimeZone.find_tzinfo(cron_timezone).name
+ @cron_timezone = timezone_name(cron_timezone)
end
def next_time_from(time)
@@ -24,6 +24,12 @@ module Gitlab
private
+ def timezone_name(timezone)
+ ActiveSupport::TimeZone.find_tzinfo(timezone).name
+ rescue TZInfo::InvalidTimezoneIdentifier
+ timezone
+ end
+
# NOTE:
# cron_timezone can only accept timezones listed in TZInfo::Timezone.
# Aliases of Timezones from ActiveSupport::TimeZone are NOT accepted,
diff --git a/lib/gitlab/ci/pipeline/chain/populate.rb b/lib/gitlab/ci/pipeline/chain/populate.rb
index d299a5677de..69b8a8fc68f 100644
--- a/lib/gitlab/ci/pipeline/chain/populate.rb
+++ b/lib/gitlab/ci/pipeline/chain/populate.rb
@@ -14,14 +14,10 @@ module Gitlab
@command.seeds_block&.call(pipeline)
##
- # Populate pipeline with all stages and builds from pipeline seeds.
+ # Populate pipeline with all stages, and stages with builds.
#
pipeline.stage_seeds.each do |stage|
pipeline.stages << stage.to_resource
-
- stage.seeds.each do |build|
- pipeline.builds << build.to_resource
- end
end
if pipeline.stages.none?
diff --git a/lib/gitlab/ci/pipeline/seed/stage.rb b/lib/gitlab/ci/pipeline/seed/stage.rb
index c101f30d6e8..2b58d9863a0 100644
--- a/lib/gitlab/ci/pipeline/seed/stage.rb
+++ b/lib/gitlab/ci/pipeline/seed/stage.rb
@@ -19,6 +19,7 @@ module Gitlab
def attributes
{ name: @attributes.fetch(:name),
+ position: @attributes.fetch(:index),
pipeline: @pipeline,
project: @pipeline.project }
end
diff --git a/lib/gitlab/database/arel_methods.rb b/lib/gitlab/database/arel_methods.rb
new file mode 100644
index 00000000000..d7e3ce08b32
--- /dev/null
+++ b/lib/gitlab/database/arel_methods.rb
@@ -0,0 +1,18 @@
+module Gitlab
+ module Database
+ module ArelMethods
+ private
+
+ # In Arel 7.0.0 (Arel 7.1.4 is used in Rails 5.0) the `engine` parameter of `Arel::UpdateManager#initializer`
+ # was removed.
+ # Remove this file and inline this method when removing rails5? code.
+ def arel_update_manager
+ if Gitlab.rails5?
+ Arel::UpdateManager.new
+ else
+ Arel::UpdateManager.new(ActiveRecord::Base)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index 77079e5e72b..c21bae5e16b 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -1,6 +1,8 @@
module Gitlab
module Database
module MigrationHelpers
+ include Gitlab::Database::ArelMethods
+
BACKGROUND_MIGRATION_BATCH_SIZE = 1000 # Number of rows to process per job
BACKGROUND_MIGRATION_JOB_BUFFER_SIZE = 1000 # Number of jobs to bulk queue at a time
@@ -314,7 +316,7 @@ module Gitlab
stop_arel = yield table, stop_arel if block_given?
stop_row = exec_query(stop_arel.to_sql).to_hash.first
- update_arel = Arel::UpdateManager.new(ActiveRecord::Base)
+ update_arel = arel_update_manager
.table(table)
.set([[table[column], value]])
.where(table[:id].gteq(start_id))
diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb
index 1a697396ff1..14de28a1d08 100644
--- a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb
+++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb
@@ -3,6 +3,8 @@ module Gitlab
module RenameReservedPathsMigration
module V1
class RenameBase
+ include Gitlab::Database::ArelMethods
+
attr_reader :paths, :migration
delegate :update_column_in_batches,
@@ -62,10 +64,10 @@ module Gitlab
old_full_path,
new_full_path)
- update = Arel::UpdateManager.new(ActiveRecord::Base)
- .table(routes)
- .set([[routes[:path], replace_statement]])
- .where(Arel::Nodes::SqlLiteral.new(filter))
+ update = arel_update_manager
+ .table(routes)
+ .set([[routes[:path], replace_statement]])
+ .where(Arel::Nodes::SqlLiteral.new(filter))
execute(update.to_sql)
end
diff --git a/lib/gitlab/diff/file_collection/base.rb b/lib/gitlab/diff/file_collection/base.rb
index a6007ebf531..c79d8d3cb21 100644
--- a/lib/gitlab/diff/file_collection/base.rb
+++ b/lib/gitlab/diff/file_collection/base.rb
@@ -36,6 +36,8 @@ module Gitlab
private
def decorate_diff!(diff)
+ return diff if diff.is_a?(File)
+
Gitlab::Diff::File.new(diff, repository: project.repository, diff_refs: diff_refs, fallback_diff_refs: fallback_diff_refs)
end
end
diff --git a/lib/gitlab/diff/position.rb b/lib/gitlab/diff/position.rb
index 690b27cde81..978962ab2eb 100644
--- a/lib/gitlab/diff/position.rb
+++ b/lib/gitlab/diff/position.rb
@@ -12,6 +12,10 @@ module Gitlab
:head_sha,
:old_line,
:new_line,
+ :width,
+ :height,
+ :x,
+ :y,
:position_type, to: :formatter
# A position can belong to a text line or to an image coordinate
diff --git a/lib/gitlab/git/raw_diff_change.rb b/lib/gitlab/git/raw_diff_change.rb
index eb3d8819239..92f6c45ce25 100644
--- a/lib/gitlab/git/raw_diff_change.rb
+++ b/lib/gitlab/git/raw_diff_change.rb
@@ -38,7 +38,9 @@ module Gitlab
end
def extract_operation
- case @raw_operation&.first(1)
+ return :unknown unless @raw_operation
+
+ case @raw_operation[0]
when 'A'
:added
when 'C'
diff --git a/lib/gitlab/git/remote_repository.rb b/lib/gitlab/git/remote_repository.rb
index 6bd6e58feeb..f40e59a8dd0 100644
--- a/lib/gitlab/git/remote_repository.rb
+++ b/lib/gitlab/git/remote_repository.rb
@@ -12,7 +12,7 @@ module Gitlab
# class.
#
class RemoteRepository
- attr_reader :path, :relative_path, :gitaly_repository
+ attr_reader :relative_path, :gitaly_repository
def initialize(repository)
@relative_path = repository.relative_path
@@ -21,7 +21,6 @@ module Gitlab
# These instance variables will not be available in gitaly-ruby, where
# we have no disk access to this repository.
@repository = repository
- @path = repository.path
end
def empty?
@@ -69,6 +68,10 @@ module Gitlab
env
end
+ def path
+ @repository.path
+ end
+
private
# Must return an object that responds to 'address' and 'storage'.
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 5a6e2e0b937..84d37f77fbb 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -142,15 +142,7 @@ module Gitlab
end
def exists?
- Gitlab::GitalyClient.migrate(:repository_exists, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
- if enabled
- gitaly_repository_client.exists?
- else
- circuit_breaker.perform do
- File.exist?(File.join(path, 'refs'))
- end
- end
- end
+ gitaly_repository_client.exists?
end
# Returns an Array of branch names
@@ -399,18 +391,6 @@ module Gitlab
nil
end
- def archive_prefix(ref, sha, append_sha:)
- append_sha = (ref != sha) if append_sha.nil?
-
- project_name = self.name.chomp('.git')
- formatted_ref = ref.tr('/', '-')
-
- prefix_segments = [project_name, formatted_ref]
- prefix_segments << sha if append_sha
-
- prefix_segments.join('-')
- end
-
def archive_metadata(ref, storage_path, format = "tar.gz", append_sha:)
ref ||= root_ref
commit = Gitlab::Git::Commit.find(self, ref)
@@ -421,12 +401,44 @@ module Gitlab
{
'RepoPath' => path,
'ArchivePrefix' => prefix,
- 'ArchivePath' => archive_file_path(prefix, storage_path, format),
+ 'ArchivePath' => archive_file_path(storage_path, commit.id, prefix, format),
'CommitId' => commit.id
}
end
- def archive_file_path(name, storage_path, format = "tar.gz")
+ # This is both the filename of the archive (missing the extension) and the
+ # name of the top-level member of the archive under which all files go
+ #
+ # FIXME: The generated prefix is incorrect for projects with hashed
+ # storage enabled
+ def archive_prefix(ref, sha, append_sha:)
+ append_sha = (ref != sha) if append_sha.nil?
+
+ project_name = self.name.chomp('.git')
+ formatted_ref = ref.tr('/', '-')
+
+ prefix_segments = [project_name, formatted_ref]
+ prefix_segments << sha if append_sha
+
+ prefix_segments.join('-')
+ end
+ private :archive_prefix
+
+ # The full path on disk where the archive should be stored. This is used
+ # to cache the archive between requests.
+ #
+ # The path is a global namespace, so needs to be globally unique. This is
+ # achieved by including `gl_repository` in the path.
+ #
+ # Archives relating to a particular ref when the SHA is not present in the
+ # filename must be invalidated when the ref is updated to point to a new
+ # SHA. This is achieved by including the SHA in the path.
+ #
+ # As this is a full path on disk, it is not "cloud native". This should
+ # be resolved by either removing the cache, or moving the implementation
+ # into Gitaly and removing the ArchivePath parameter from the git-archive
+ # senddata response.
+ def archive_file_path(storage_path, sha, name, format = "tar.gz")
# Build file path
return nil unless name
@@ -444,8 +456,9 @@ module Gitlab
end
file_name = "#{name}.#{extension}"
- File.join(storage_path, self.name, file_name)
+ File.join(storage_path, self.gl_repository, sha, file_name)
end
+ private :archive_file_path
# Return repo size in megabytes
def size
@@ -1187,6 +1200,8 @@ module Gitlab
if is_enabled
gitaly_fetch_ref(source_repository, source_ref: source_ref, target_ref: target_ref)
else
+ # When removing this code, also remove source_repository#path
+ # to remove deprecated method calls
local_fetch_ref(source_repository.path, source_ref: source_ref, target_ref: target_ref)
end
end
@@ -1554,7 +1569,8 @@ module Gitlab
end
def checksum
- gitaly_migrate(:calculate_checksum) do |is_enabled|
+ gitaly_migrate(:calculate_checksum,
+ status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
gitaly_repository_client.calculate_checksum
else
diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb
index bf5a491e28d..498187997e1 100644
--- a/lib/gitlab/gitaly_client/repository_service.rb
+++ b/lib/gitlab/gitaly_client/repository_service.rb
@@ -142,7 +142,7 @@ module Gitlab
:repository_service,
:is_rebase_in_progress,
request,
- timeout: GitalyClient.default_timeout
+ timeout: GitalyClient.fast_timeout
)
response.in_progress
@@ -159,7 +159,7 @@ module Gitlab
:repository_service,
:is_squash_in_progress,
request,
- timeout: GitalyClient.default_timeout
+ timeout: GitalyClient.fast_timeout
)
response.in_progress
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index a7e055ac444..c741dabe168 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -19,6 +19,7 @@ module Gitlab
gon.gitlab_logo = ActionController::Base.helpers.asset_path('gitlab_logo.png')
gon.sprite_icons = IconsHelper.sprite_icon_path
gon.sprite_file_icons = IconsHelper.sprite_file_icons_path
+ gon.emoji_sprites_css_path = ActionController::Base.helpers.stylesheet_path('emoji_sprites')
gon.test_env = Rails.env.test?
gon.suggested_label_colors = LabelsHelper.suggested_colors
diff --git a/lib/gitlab/kubernetes/helm/base_command.rb b/lib/gitlab/kubernetes/helm/base_command.rb
index 6e4df05aa7e..3d778da90c7 100644
--- a/lib/gitlab/kubernetes/helm/base_command.rb
+++ b/lib/gitlab/kubernetes/helm/base_command.rb
@@ -15,6 +15,9 @@ module Gitlab
def generate_script
<<~HEREDOC
set -eo pipefail
+ ALPINE_VERSION=$(cat /etc/alpine-release | cut -d '.' -f 1,2)
+ echo http://mirror.clarkson.edu/alpine/v$ALPINE_VERSION/main >> /etc/apk/repositories
+ echo http://mirror1.hs-esslingen.de/pub/Mirrors/alpine/v$ALPINE_VERSION/main >> /etc/apk/repositories
apk add -U ca-certificates openssl >/dev/null
wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v#{Gitlab::Kubernetes::Helm::HELM_VERSION}-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
mv /tmp/linux-amd64/helm /usr/bin/
diff --git a/lib/gitlab/pages_client.rb b/lib/gitlab/pages_client.rb
new file mode 100644
index 00000000000..7b358a3bd1b
--- /dev/null
+++ b/lib/gitlab/pages_client.rb
@@ -0,0 +1,117 @@
+module Gitlab
+ class PagesClient
+ class << self
+ attr_reader :certificate, :token
+
+ def call(service, rpc, request, timeout: nil)
+ kwargs = request_kwargs(timeout)
+ stub(service).__send__(rpc, request, kwargs) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ # This function is not thread-safe. Call it from an initializer only.
+ def read_or_create_token
+ @token = read_token
+ rescue Errno::ENOENT
+ # TODO: uncomment this when omnibus knows how to write the token file for us
+ # https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests/2466
+ #
+ # write_token(SecureRandom.random_bytes(64))
+ #
+ # # Read from disk in case someone else won the race and wrote the file
+ # # before us. If this fails again let the exception bubble up.
+ # @token = read_token
+ end
+
+ # This function is not thread-safe. Call it from an initializer only.
+ def load_certificate
+ cert_path = config.certificate
+ return unless cert_path.present?
+
+ @certificate = File.read(cert_path)
+ end
+
+ def ping
+ request = Grpc::Health::V1::HealthCheckRequest.new
+ call(:health_check, :check, request, timeout: 5.seconds)
+ end
+
+ private
+
+ def request_kwargs(timeout)
+ encoded_token = Base64.strict_encode64(token.to_s)
+ metadata = {
+ 'authorization' => "Bearer #{encoded_token}"
+ }
+
+ result = { metadata: metadata }
+
+ return result unless timeout
+
+ # Do not use `Time.now` for deadline calculation, since it
+ # will be affected by Timecop in some tests, but grpc's c-core
+ # uses system time instead of timecop's time, so tests will fail
+ # `Time.at(Process.clock_gettime(Process::CLOCK_REALTIME))` will
+ # circumvent timecop
+ deadline = Time.at(Process.clock_gettime(Process::CLOCK_REALTIME)) + timeout
+ result[:deadline] = deadline
+
+ result
+ end
+
+ def stub(name)
+ stub_class(name).new(address, grpc_creds)
+ end
+
+ def stub_class(name)
+ if name == :health_check
+ Grpc::Health::V1::Health::Stub
+ else
+ # TODO use pages namespace
+ Gitaly.const_get(name.to_s.camelcase.to_sym).const_get(:Stub)
+ end
+ end
+
+ def address
+ addr = config.address
+ addr = addr.sub(%r{^tcp://}, '') if URI(addr).scheme == 'tcp'
+ addr
+ end
+
+ def grpc_creds
+ if address.start_with?('unix:')
+ :this_channel_is_insecure
+ elsif @certificate
+ GRPC::Core::ChannelCredentials.new(@certificate)
+ else
+ # Use system certificate pool
+ GRPC::Core::ChannelCredentials.new
+ end
+ end
+
+ def config
+ Gitlab.config.pages.admin
+ end
+
+ def read_token
+ File.read(token_path)
+ end
+
+ def token_path
+ Rails.root.join('.gitlab_pages_secret').to_s
+ end
+
+ def write_token(new_token)
+ Tempfile.open(File.basename(token_path), File.dirname(token_path), encoding: 'ascii-8bit') do |f|
+ f.write(new_token)
+ f.close
+ File.link(f.path, token_path)
+ end
+ rescue Errno::EACCES => ex
+ # TODO stop rescuing this exception in GitLab 11.0 https://gitlab.com/gitlab-org/gitlab-ce/issues/45672
+ Rails.logger.error("Could not write pages admin token file: #{ex}")
+ rescue Errno::EEXIST
+ # Another process wrote the token file concurrently with us. Use their token, not ours.
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/redis/shared_state.rb b/lib/gitlab/redis/shared_state.rb
index 10bec7a90da..e5a0fdae7ef 100644
--- a/lib/gitlab/redis/shared_state.rb
+++ b/lib/gitlab/redis/shared_state.rb
@@ -5,6 +5,8 @@ module Gitlab
module Redis
class SharedState < ::Gitlab::Redis::Wrapper
SESSION_NAMESPACE = 'session:gitlab'.freeze
+ USER_SESSIONS_NAMESPACE = 'session:user:gitlab'.freeze
+ USER_SESSIONS_LOOKUP_NAMESPACE = 'session:lookup:user:gitlab'.freeze
DEFAULT_REDIS_SHARED_STATE_URL = 'redis://localhost:6382'.freeze
REDIS_SHARED_STATE_CONFIG_ENV_VAR_NAME = 'GITLAB_REDIS_SHARED_STATE_CONFIG_FILE'.freeze
diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb
index 156115f8a8f..4a691d640b3 100644
--- a/lib/gitlab/shell.rb
+++ b/lib/gitlab/shell.rb
@@ -294,17 +294,7 @@ module Gitlab
# add_namespace("default", "gitlab")
#
def add_namespace(storage, name)
- Gitlab::GitalyClient.migrate(:add_namespace,
- status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
- if enabled
- Gitlab::GitalyClient::NamespaceService.new(storage).add(name)
- else
- path = full_path(storage, name)
- FileUtils.mkdir_p(path, mode: 0770) unless exists?(storage, name)
- end
- end
- rescue Errno::EEXIST => e
- Rails.logger.warn("Directory exists as a file: #{e} at: #{path}")
+ Gitlab::GitalyClient::NamespaceService.new(storage).add(name)
rescue GRPC::InvalidArgument => e
raise ArgumentError, e.message
end
@@ -316,14 +306,7 @@ module Gitlab
# rm_namespace("default", "gitlab")
#
def rm_namespace(storage, name)
- Gitlab::GitalyClient.migrate(:remove_namespace,
- status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
- if enabled
- Gitlab::GitalyClient::NamespaceService.new(storage).remove(name)
- else
- FileUtils.rm_r(full_path(storage, name), force: true)
- end
- end
+ Gitlab::GitalyClient::NamespaceService.new(storage).remove(name)
rescue GRPC::InvalidArgument => e
raise ArgumentError, e.message
end
@@ -335,17 +318,7 @@ module Gitlab
# mv_namespace("/path/to/storage", "gitlab", "gitlabhq")
#
def mv_namespace(storage, old_name, new_name)
- Gitlab::GitalyClient.migrate(:rename_namespace,
- status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
- if enabled
- Gitlab::GitalyClient::NamespaceService.new(storage)
- .rename(old_name, new_name)
- else
- break false if exists?(storage, new_name) || !exists?(storage, old_name)
-
- FileUtils.mv(full_path(storage, old_name), full_path(storage, new_name))
- end
- end
+ Gitlab::GitalyClient::NamespaceService.new(storage).rename(old_name, new_name)
rescue GRPC::InvalidArgument
false
end
@@ -370,17 +343,8 @@ module Gitlab
# exists?(storage, 'gitlab')
# exists?(storage, 'gitlab/cookies.git')
#
- # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/385
def exists?(storage, dir_name)
- Gitlab::GitalyClient.migrate(:namespace_exists,
- status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
- if enabled
- Gitlab::GitalyClient::NamespaceService.new(storage)
- .exists?(dir_name)
- else
- File.exist?(full_path(storage, dir_name))
- end
- end
+ Gitlab::GitalyClient::NamespaceService.new(storage).exists?(dir_name)
end
protected
diff --git a/lib/omni_auth/strategies/jwt.rb b/lib/omni_auth/strategies/jwt.rb
new file mode 100644
index 00000000000..2349b2a28aa
--- /dev/null
+++ b/lib/omni_auth/strategies/jwt.rb
@@ -0,0 +1,62 @@
+require 'omniauth'
+require 'jwt'
+
+module OmniAuth
+ module Strategies
+ class JWT
+ ClaimInvalid = Class.new(StandardError)
+
+ include OmniAuth::Strategy
+
+ args [:secret]
+
+ option :secret, nil
+ option :algorithm, 'HS256'
+ option :uid_claim, 'email'
+ option :required_claims, %w(name email)
+ option :info_map, { name: "name", email: "email" }
+ option :auth_url, nil
+ option :valid_within, nil
+
+ uid { decoded[options.uid_claim] }
+
+ extra do
+ { raw_info: decoded }
+ end
+
+ info do
+ options.info_map.each_with_object({}) do |(k, v), h|
+ h[k.to_s] = decoded[v.to_s]
+ end
+ end
+
+ def request_phase
+ redirect options.auth_url
+ end
+
+ def decoded
+ @decoded ||= ::JWT.decode(request.params['jwt'], options.secret, options.algorithm).first
+
+ (options.required_claims || []).each do |field|
+ raise ClaimInvalid, "Missing required '#{field}' claim" unless @decoded.key?(field.to_s)
+ end
+
+ raise ClaimInvalid, "Missing required 'iat' claim" if options.valid_within && !@decoded["iat"]
+
+ if options.valid_within && (Time.now.to_i - @decoded["iat"]).abs > options.valid_within
+ raise ClaimInvalid, "'iat' timestamp claim is too skewed from present"
+ end
+
+ @decoded
+ end
+
+ def callback_phase
+ super
+ rescue ClaimInvalid => e
+ fail! :claim_invalid, e
+ end
+ end
+
+ class Jwt < JWT; end
+ end
+end
diff --git a/lib/tasks/gitlab/pages.rake b/lib/tasks/gitlab/pages.rake
new file mode 100644
index 00000000000..100e480bd66
--- /dev/null
+++ b/lib/tasks/gitlab/pages.rake
@@ -0,0 +1,9 @@
+namespace :gitlab do
+ namespace :pages do
+ desc 'Ping the pages admin API'
+ task admin_ping: :gitlab_environment do
+ Gitlab::PagesClient.ping
+ puts "OK: gitlab-pages admin API is reachable"
+ end
+ end
+end
diff --git a/package.json b/package.json
index 96dee321548..e95add1d7e9 100644
--- a/package.json
+++ b/package.json
@@ -83,11 +83,11 @@
"underscore": "^1.8.3",
"url-loader": "^0.6.2",
"visibilityjs": "^1.2.4",
- "vue": "^2.5.13",
+ "vue": "^2.5.16",
"vue-loader": "^14.1.1",
- "vue-resource": "^1.3.5",
+ "vue-resource": "^1.5.0",
"vue-router": "^3.0.1",
- "vue-template-compiler": "^2.5.13",
+ "vue-template-compiler": "^2.5.16",
"vue-virtual-scroll-list": "^1.2.5",
"vuex": "^3.0.1",
"webpack": "^3.11.0",
@@ -98,10 +98,11 @@
"devDependencies": {
"axios-mock-adapter": "^1.10.0",
"babel-eslint": "^8.0.2",
- "babel-plugin-istanbul": "^4.1.5",
+ "babel-plugin-istanbul": "^4.1.6",
"babel-plugin-rewire": "^1.1.0",
"babel-template": "^6.26.0",
"babel-types": "^6.26.0",
+ "chalk": "^2.4.1",
"commander": "^2.15.1",
"eslint": "^3.18.0",
"eslint-config-airbnb-base": "^10.0.1",
@@ -116,13 +117,13 @@
"istanbul": "^0.4.5",
"jasmine-core": "^2.9.0",
"jasmine-jquery": "^2.1.1",
- "karma": "^2.0.0",
+ "karma": "^2.0.2",
"karma-chrome-launcher": "^2.2.0",
- "karma-coverage-istanbul-reporter": "^1.4.1",
+ "karma-coverage-istanbul-reporter": "^1.4.2",
"karma-jasmine": "^1.1.1",
"karma-mocha-reporter": "^2.2.5",
"karma-sourcemap-loader": "^0.3.7",
- "karma-webpack": "2.0.7",
+ "karma-webpack": "3.0.0",
"nodemon": "^1.15.1",
"prettier": "1.11.1",
"webpack-dev-server": "^2.11.2"
diff --git a/qa/Dockerfile b/qa/Dockerfile
index ed2ee73bea0..77cee9c5461 100644
--- a/qa/Dockerfile
+++ b/qa/Dockerfile
@@ -1,4 +1,4 @@
-FROM ruby:2.4
+FROM ruby:2.4-stretch
LABEL maintainer "Grzegorz Bizon <grzegorz@gitlab.com>"
ENV DEBIAN_FRONTEND noninteractive
diff --git a/qa/Gemfile b/qa/Gemfile
index c3e61568f3d..d69c71003ae 100644
--- a/qa/Gemfile
+++ b/qa/Gemfile
@@ -6,5 +6,4 @@ gem 'capybara-screenshot', '~> 1.0.18'
gem 'rake', '~> 12.3.0'
gem 'rspec', '~> 3.7'
gem 'selenium-webdriver', '~> 3.8.0'
-gem 'net-ssh', require: false
gem 'airborne', '~> 0.2.13'
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index 51d2e4d7a10..1bc424335f8 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -46,9 +46,8 @@ GEM
mini_mime (1.0.0)
mini_portile2 (2.3.0)
minitest (5.11.1)
- net-ssh (4.1.0)
netrc (0.11.0)
- nokogiri (1.8.1)
+ nokogiri (1.8.2)
mini_portile2 (~> 2.3.0)
pry (0.11.3)
coderay (~> 1.1.0)
@@ -98,7 +97,6 @@ DEPENDENCIES
airborne (~> 0.2.13)
capybara (~> 2.16.1)
capybara-screenshot (~> 1.0.18)
- net-ssh
pry-byebug (~> 3.5.1)
rake (~> 12.3.0)
rspec (~> 3.7)
diff --git a/qa/qa.rb b/qa/qa.rb
index fff99a1d31b..40e12c8b336 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -11,9 +11,15 @@ module QA
autoload :Scenario, 'qa/runtime/scenario'
autoload :Browser, 'qa/runtime/browser'
autoload :Env, 'qa/runtime/env'
- autoload :RSAKey, 'qa/runtime/rsa_key'
autoload :Address, 'qa/runtime/address'
autoload :API, 'qa/runtime/api'
+
+ module Key
+ autoload :Base, 'qa/runtime/key/base'
+ autoload :RSA, 'qa/runtime/key/rsa'
+ autoload :ECDSA, 'qa/runtime/key/ecdsa'
+ autoload :ED25519, 'qa/runtime/key/ed25519'
+ end
end
##
diff --git a/qa/qa/factory/repository/push.rb b/qa/qa/factory/repository/push.rb
index 6e8905cde78..795f1f9cb1a 100644
--- a/qa/qa/factory/repository/push.rb
+++ b/qa/qa/factory/repository/push.rb
@@ -2,7 +2,10 @@ module QA
module Factory
module Repository
class Push < Factory::Base
- attr_writer :file_name, :file_content, :commit_message, :branch_name, :new_branch
+ attr_accessor :file_name, :file_content, :commit_message,
+ :branch_name, :new_branch
+
+ attr_writer :remote_branch
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-with-code'
@@ -17,23 +20,32 @@ module QA
@new_branch = true
end
+ def remote_branch
+ @remote_branch ||= branch_name
+ end
+
def fabricate!
project.visit!
Git::Repository.perform do |repository|
- repository.location = Page::Project::Show.act do
+ repository.uri = Page::Project::Show.act do
choose_repository_clone_http
- repository_location
+ repository_location.uri
end
repository.use_default_credentials
repository.clone
repository.configure_identity('GitLab QA', 'root@gitlab.com')
- repository.checkout(@branch_name) unless @new_branch
- repository.add_file(@file_name, @file_content)
- repository.commit(@commit_message)
- repository.push_changes(@branch_name)
+ if new_branch
+ repository.checkout_new_branch(branch_name)
+ else
+ repository.checkout(branch_name)
+ end
+
+ repository.add_file(file_name, file_content)
+ repository.commit(commit_message)
+ repository.push_changes("#{branch_name}:#{remote_branch}")
end
end
end
diff --git a/qa/qa/factory/resource/branch.rb b/qa/qa/factory/resource/branch.rb
index d0ef142e90d..1785441f5a8 100644
--- a/qa/qa/factory/resource/branch.rb
+++ b/qa/qa/factory/resource/branch.rb
@@ -2,7 +2,8 @@ module QA
module Factory
module Resource
class Branch < Factory::Base
- attr_accessor :project, :branch_name, :allow_to_push, :protected
+ attr_accessor :project, :branch_name,
+ :allow_to_push, :allow_to_merge, :protected
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'protected-branch-project'
@@ -23,6 +24,7 @@ module QA
def initialize
@branch_name = 'test/branch'
@allow_to_push = true
+ @allow_to_merge = true
@protected = false
end
@@ -39,7 +41,9 @@ module QA
resource.project = project
resource.file_name = 'README.md'
resource.commit_message = 'Add readme'
- resource.branch_name = "master:#{@branch_name}"
+ resource.branch_name = 'master'
+ resource.new_branch = false
+ resource.remote_branch = @branch_name
end
Page::Project::Show.act { wait_for_push }
@@ -63,7 +67,22 @@ module QA
page.allow_no_one_to_push
end
+ if allow_to_merge
+ page.allow_devs_and_masters_to_merge
+ else
+ page.allow_no_one_to_merge
+ end
+
+ page.wait(reload: false) do
+ !page.first('.btn-create').disabled?
+ end
+
page.protect_branch
+
+ # Wait for page load, which resets the expanded sections
+ page.wait(reload: false) do
+ !page.has_content?('Collapse')
+ end
end
end
end
diff --git a/qa/qa/factory/resource/deploy_key.rb b/qa/qa/factory/resource/deploy_key.rb
index ff0b4a46b77..ea8a3ad687d 100644
--- a/qa/qa/factory/resource/deploy_key.rb
+++ b/qa/qa/factory/resource/deploy_key.rb
@@ -4,15 +4,15 @@ module QA
class DeployKey < Factory::Base
attr_accessor :title, :key
- product :title do
+ product :fingerprint do |resource|
Page::Project::Settings::Repository.act do
- expand_deploy_keys(&:key_title)
- end
- end
+ expand_deploy_keys do |key|
+ key_offset = key.key_titles.index do |title|
+ title.text == resource.title
+ end
- product :fingerprint do
- Page::Project::Settings::Repository.act do
- expand_deploy_keys(&:key_fingerprint)
+ key.key_fingerprints[key_offset].text
+ end
end
end
diff --git a/qa/qa/factory/resource/merge_request.rb b/qa/qa/factory/resource/merge_request.rb
index 539fe6b8a70..7588ac5735d 100644
--- a/qa/qa/factory/resource/merge_request.rb
+++ b/qa/qa/factory/resource/merge_request.rb
@@ -24,12 +24,14 @@ module QA
dependency Factory::Repository::Push, as: :target do |push, factory|
factory.project.visit!
push.project = factory.project
- push.branch_name = "master:#{factory.target_branch}"
+ push.branch_name = 'master'
+ push.remote_branch = factory.target_branch
end
dependency Factory::Repository::Push, as: :source do |push, factory|
push.project = factory.project
- push.branch_name = "#{factory.target_branch}:#{factory.source_branch}"
+ push.branch_name = factory.target_branch
+ push.remote_branch = factory.source_branch
push.file_name = "added_file.txt"
push.file_content = "File Added"
end
diff --git a/qa/qa/factory/resource/project.rb b/qa/qa/factory/resource/project.rb
index 7df2dc6618c..cda1b35ba6a 100644
--- a/qa/qa/factory/resource/project.rb
+++ b/qa/qa/factory/resource/project.rb
@@ -17,6 +17,13 @@ module QA
Page::Project::Show.act { project_name }
end
+ product :repository_ssh_location do
+ Page::Project::Show.act do
+ choose_repository_clone_ssh
+ repository_location
+ end
+ end
+
def fabricate!
group.visit!
diff --git a/qa/qa/factory/resource/secret_variable.rb b/qa/qa/factory/resource/secret_variable.rb
index c734d739b4a..12a830da116 100644
--- a/qa/qa/factory/resource/secret_variable.rb
+++ b/qa/qa/factory/resource/secret_variable.rb
@@ -16,8 +16,7 @@ module QA
Page::Project::Settings::CICD.perform do |setting|
setting.expand_secret_variables do |page|
- page.fill_variable_key(key)
- page.fill_variable_value(value)
+ page.fill_variable(key, value)
page.save_variables
end
diff --git a/qa/qa/git/location.rb b/qa/qa/git/location.rb
index 30538388530..b74f38f3ae3 100644
--- a/qa/qa/git/location.rb
+++ b/qa/qa/git/location.rb
@@ -14,7 +14,7 @@ module QA
def initialize(git_uri)
@git_uri = git_uri
@uri =
- if git_uri.start_with?('ssh://')
+ if git_uri =~ %r{\A(?:ssh|http|https)://}
URI.parse(git_uri)
else
*rest, path = git_uri.split(':')
diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb
index 2f9f06ba277..1367671e3ca 100644
--- a/qa/qa/git/repository.rb
+++ b/qa/qa/git/repository.rb
@@ -15,8 +15,7 @@ module QA
end
end
- def location=(address)
- @location = address
+ def uri=(address)
@uri = URI(address)
end
@@ -43,6 +42,10 @@ module QA
`git checkout "#{branch_name}"`
end
+ def checkout_new_branch(branch_name)
+ `git checkout -b "#{branch_name}"`
+ end
+
def shallow_clone
clone('--depth 1')
end
diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb
index a313d46205d..0a69af88570 100644
--- a/qa/qa/page/base.rb
+++ b/qa/qa/page/base.rb
@@ -64,6 +64,10 @@ module QA
find(element_selector_css(name))
end
+ def all_elements(name)
+ all(element_selector_css(name))
+ end
+
def click_element(name)
find_element(name).click
end
diff --git a/qa/qa/page/project/settings/deploy_keys.rb b/qa/qa/page/project/settings/deploy_keys.rb
index 332e84724c7..4428e263bbb 100644
--- a/qa/qa/page/project/settings/deploy_keys.rb
+++ b/qa/qa/page/project/settings/deploy_keys.rb
@@ -42,6 +42,18 @@ module QA
end
end
+ def key_titles
+ within_project_deploy_keys do
+ all_elements(:key_title)
+ end
+ end
+
+ def key_fingerprints
+ within_project_deploy_keys do
+ all_elements(:key_fingerprint)
+ end
+ end
+
private
def within_project_deploy_keys
diff --git a/qa/qa/page/project/settings/protected_branches.rb b/qa/qa/page/project/settings/protected_branches.rb
index f3563401124..63bc3aaa2bc 100644
--- a/qa/qa/page/project/settings/protected_branches.rb
+++ b/qa/qa/page/project/settings/protected_branches.rb
@@ -11,6 +11,13 @@ module QA
view 'app/views/projects/protected_branches/_create_protected_branch.html.haml' do
element :allowed_to_push_select
element :allowed_to_push_dropdown
+ element :allowed_to_merge_select
+ element :allowed_to_merge_dropdown
+ end
+
+ view 'app/views/projects/protected_branches/_update_protected_branch.html.haml' do
+ element :allowed_to_push
+ element :allowed_to_merge
end
view 'app/views/projects/protected_branches/shared/_branches_list.html.haml' do
@@ -30,11 +37,19 @@ module QA
end
def allow_no_one_to_push
- allow_to_push('No one')
+ click_allow(:push, 'No one')
end
def allow_devs_and_masters_to_push
- allow_to_push('Developers + Masters')
+ click_allow(:push, 'Developers + Masters')
+ end
+
+ def allow_no_one_to_merge
+ click_allow(:merge, 'No one')
+ end
+
+ def allow_devs_and_masters_to_merge
+ click_allow(:merge, 'Developers + Masters')
end
def protect_branch
@@ -55,11 +70,15 @@ module QA
private
- def allow_to_push(text)
- click_element :allowed_to_push_select
+ def click_allow(action, text)
+ click_element :"allowed_to_#{action}_select"
- within_element(:allowed_to_push_dropdown) do
+ within_element(:"allowed_to_#{action}_dropdown") do
click_on text
+
+ wait(reload: false) do
+ has_css?('.is-active')
+ end
end
end
end
diff --git a/qa/qa/page/project/settings/secret_variables.rb b/qa/qa/page/project/settings/secret_variables.rb
index c95c79f137d..d2f5d5a9060 100644
--- a/qa/qa/page/project/settings/secret_variables.rb
+++ b/qa/qa/page/project/settings/secret_variables.rb
@@ -7,10 +7,8 @@ module QA
view 'app/views/ci/variables/_variable_row.html.haml' do
element :variable_row, '.ci-variable-row-body'
- element :variable_key, '.js-ci-variable-input-key'
- element :variable_value, '.js-ci-variable-input-value'
- element :key_placeholder, 'Input variable key'
- element :value_placeholder, 'Input variable value'
+ element :variable_key, '.qa-ci-variable-input-key'
+ element :variable_value, '.qa-ci-variable-input-value'
end
view 'app/views/ci/variables/_index.html.haml' do
@@ -18,12 +16,14 @@ module QA
element :reveal_values, '.js-secret-value-reveal-button'
end
- def fill_variable_key(key)
- fill_in('Input variable key', with: key, match: :first)
- end
+ def fill_variable(key, value)
+ keys = all_elements(:ci_variable_input_key)
+ index = keys.size - 1
- def fill_variable_value(value)
- fill_in('Input variable value', with: value, match: :first)
+ # After we fill the key, JS would generate another field so
+ # we need to use the same index to find the corresponding one.
+ keys[index].set(key)
+ all_elements(:ci_variable_input_value)[index].set(value)
end
def save_variables
@@ -36,7 +36,7 @@ module QA
def variable_value(key)
within('.ci-variable-row-body', text: key) do
- find('.js-ci-variable-input-value').value
+ find('.qa-ci-variable-input-value').value
end
end
end
diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb
index c7e7ece792d..5bbef040330 100644
--- a/qa/qa/page/project/show.rb
+++ b/qa/qa/page/project/show.rb
@@ -38,11 +38,7 @@ module QA
end
def repository_location
- find('#project_clone').value
- end
-
- def repository_location_uri
- Git::Location.new(repository_location)
+ Git::Location.new(find('#project_clone').value)
end
def project_name
@@ -91,7 +87,7 @@ module QA
end
# Ensure git clone textbox was updated
- repository_location.include?(detect_text)
+ repository_location.git_uri.include?(detect_text)
end
end
end
diff --git a/qa/qa/runtime/key/base.rb b/qa/qa/runtime/key/base.rb
new file mode 100644
index 00000000000..c7e5ebada7b
--- /dev/null
+++ b/qa/qa/runtime/key/base.rb
@@ -0,0 +1,36 @@
+module QA
+ module Runtime
+ module Key
+ class Base
+ attr_reader :name, :bits, :private_key, :public_key, :fingerprint
+
+ def initialize(name, bits)
+ @name = name
+ @bits = bits
+
+ Dir.mktmpdir do |dir|
+ path = "#{dir}/id_#{name}"
+
+ ssh_keygen(name, bits, path)
+ populate_key_data(path)
+ end
+ end
+
+ private
+
+ def ssh_keygen(name, bits, path)
+ cmd = %W[ssh-keygen -t #{name} -b #{bits} -f #{path} -N] << ''
+
+ Service::Shellout.shell(cmd)
+ end
+
+ def populate_key_data(path)
+ @private_key = File.binread(path)
+ @public_key = File.binread("#{path}.pub")
+ @fingerprint =
+ `ssh-keygen -l -E md5 -f #{path} | cut -d' ' -f2 | cut -d: -f2-`.chomp
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/runtime/key/ecdsa.rb b/qa/qa/runtime/key/ecdsa.rb
new file mode 100644
index 00000000000..20adad45913
--- /dev/null
+++ b/qa/qa/runtime/key/ecdsa.rb
@@ -0,0 +1,12 @@
+# rubocop:disable Naming/FileName
+module QA
+ module Runtime
+ module Key
+ class ECDSA < Base
+ def initialize(bits = 521)
+ super('ecdsa', bits)
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/runtime/key/ed25519.rb b/qa/qa/runtime/key/ed25519.rb
new file mode 100644
index 00000000000..63865c1cee5
--- /dev/null
+++ b/qa/qa/runtime/key/ed25519.rb
@@ -0,0 +1,12 @@
+# rubocop:disable Naming/FileName
+module QA
+ module Runtime
+ module Key
+ class ED25519 < Base
+ def initialize
+ super('ed25519', 256)
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/runtime/key/rsa.rb b/qa/qa/runtime/key/rsa.rb
new file mode 100644
index 00000000000..d94bde52325
--- /dev/null
+++ b/qa/qa/runtime/key/rsa.rb
@@ -0,0 +1,11 @@
+module QA
+ module Runtime
+ module Key
+ class RSA < Base
+ def initialize(bits = 4096)
+ super('rsa', bits)
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/runtime/rsa_key.rb b/qa/qa/runtime/rsa_key.rb
deleted file mode 100644
index fcd7dcc4f02..00000000000
--- a/qa/qa/runtime/rsa_key.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-require 'net/ssh'
-require 'forwardable'
-
-module QA
- module Runtime
- class RSAKey
- extend Forwardable
-
- attr_reader :key
- def_delegators :@key, :fingerprint, :to_pem
-
- def initialize(bits = 4096)
- @key = OpenSSL::PKey::RSA.new(bits)
- end
-
- def public_key
- @public_key ||= "#{key.ssh_type} #{[key.to_blob].pack('m0')}"
- end
- end
- end
-end
diff --git a/qa/qa/service/shellout.rb b/qa/qa/service/shellout.rb
index 76fb2af6319..1ca9504bb33 100644
--- a/qa/qa/service/shellout.rb
+++ b/qa/qa/service/shellout.rb
@@ -5,6 +5,8 @@ module QA
module Shellout
CommandError = Class.new(StandardError)
+ module_function
+
##
# TODO, make it possible to use generic QA framework classes
# as a library - gitlab-org/gitlab-qa#94
@@ -12,7 +14,7 @@ module QA
def shell(command)
puts "Executing `#{command}`"
- Open3.popen2e(command) do |_in, out, wait|
+ Open3.popen2e(*command) do |_in, out, wait|
out.each { |line| puts line }
if wait.value.exited? && wait.value.exitstatus.nonzero?
diff --git a/qa/qa/specs/features/project/add_deploy_key_spec.rb b/qa/qa/specs/features/project/add_deploy_key_spec.rb
index b9998dda895..de53613dee1 100644
--- a/qa/qa/specs/features/project/add_deploy_key_spec.rb
+++ b/qa/qa/specs/features/project/add_deploy_key_spec.rb
@@ -4,7 +4,7 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- key = Runtime::RSAKey.new
+ key = Runtime::Key::RSA.new
deploy_key_title = 'deploy key title'
deploy_key_value = key.public_key
@@ -13,7 +13,6 @@ module QA
resource.key = deploy_key_value
end
- expect(deploy_key.title).to eq(deploy_key_title)
expect(deploy_key.fingerprint).to eq(key.fingerprint)
end
end
diff --git a/qa/qa/specs/features/project/deploy_key_clone_spec.rb b/qa/qa/specs/features/project/deploy_key_clone_spec.rb
index 19d3c83758a..98ea86bf75e 100644
--- a/qa/qa/specs/features/project/deploy_key_clone_spec.rb
+++ b/qa/qa/specs/features/project/deploy_key_clone_spec.rb
@@ -2,79 +2,103 @@ require 'digest/sha1'
module QA
feature 'cloning code using a deploy key', :core, :docker do
- let(:runner_name) { "qa-runner-#{Time.now.to_i}" }
- let(:key) { Runtime::RSAKey.new }
+ def login
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.act { sign_in_using_credentials }
+ end
- given(:project) do
- Factory::Resource::Project.fabricate! do |resource|
+ before(:all) do
+ login
+
+ @runner_name = "qa-runner-#{Time.now.to_i}"
+
+ @project = Factory::Resource::Project.fabricate! do |resource|
resource.name = 'deploy-key-clone-project'
end
- end
- after do
- Service::Runner.new(runner_name).remove!
- end
-
- scenario 'user sets up a deploy key to clone code using pipelines' do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.act { sign_in_using_credentials }
+ @repository_location = @project.repository_ssh_location
Factory::Resource::Runner.fabricate! do |resource|
- resource.project = project
- resource.name = runner_name
+ resource.project = @project
+ resource.name = @runner_name
resource.tags = %w[qa docker]
resource.image = 'gitlab/gitlab-runner:ubuntu'
end
- Factory::Resource::DeployKey.fabricate! do |resource|
- resource.project = project
- resource.title = 'deploy key title'
- resource.key = key.public_key
- end
+ Page::Menu::Main.act { sign_out }
+ end
- Factory::Resource::SecretVariable.fabricate! do |resource|
- resource.project = project
- resource.key = 'DEPLOY_KEY'
- resource.value = key.to_pem
- end
+ after(:all) do
+ Service::Runner.new(@runner_name).remove!
+ end
- project.visit!
+ keys = [
+ Runtime::Key::RSA.new(8192),
+ Runtime::Key::ECDSA.new(521),
+ Runtime::Key::ED25519.new
+ ]
- repository_uri = Page::Project::Show.act do
- choose_repository_clone_ssh
- repository_location_uri
- end
+ keys.each do |key|
+ scenario "user sets up a deploy key with #{key.name}(#{key.bits}) to clone code using pipelines" do
+ login
- gitlab_ci = <<~YAML
- cat-config:
- script:
- - mkdir -p ~/.ssh
- - ssh-keyscan -p #{repository_uri.port} #{repository_uri.host} >> ~/.ssh/known_hosts
- - eval $(ssh-agent -s)
- - echo "$DEPLOY_KEY" | ssh-add -
- - git clone #{repository_uri.git_uri}
- - sha1sum #{project.name}/.gitlab-ci.yml
- tags:
- - qa
- - docker
- YAML
-
- Factory::Repository::Push.fabricate! do |resource|
- resource.project = project
- resource.file_name = '.gitlab-ci.yml'
- resource.commit_message = 'Add .gitlab-ci.yml'
- resource.file_content = gitlab_ci
- end
+ Factory::Resource::DeployKey.fabricate! do |resource|
+ resource.project = @project
+ resource.title = "deploy key #{key.name}(#{key.bits})"
+ resource.key = key.public_key
+ end
+
+ deploy_key_name = "DEPLOY_KEY_#{key.name}_#{key.bits}"
+
+ Factory::Resource::SecretVariable.fabricate! do |resource|
+ resource.project = @project
+ resource.key = deploy_key_name
+ resource.value = key.private_key
+ end
+
+ gitlab_ci = <<~YAML
+ cat-config:
+ script:
+ - mkdir -p ~/.ssh
+ - ssh-keyscan -p #{@repository_location.port} #{@repository_location.host} >> ~/.ssh/known_hosts
+ - eval $(ssh-agent -s)
+ - ssh-add -D
+ - echo "$#{deploy_key_name}" | ssh-add -
+ - git clone #{@repository_location.git_uri}
+ - cd #{@project.name}
+ - git checkout #{deploy_key_name}
+ - sha1sum .gitlab-ci.yml
+ tags:
+ - qa
+ - docker
+ YAML
+
+ Factory::Repository::Push.fabricate! do |resource|
+ resource.project = @project
+ resource.file_name = '.gitlab-ci.yml'
+ resource.commit_message = 'Add .gitlab-ci.yml'
+ resource.file_content = gitlab_ci
+ resource.branch_name = deploy_key_name
+ resource.new_branch = true
+ end
+
+ sha1sum = Digest::SHA1.hexdigest(gitlab_ci)
+
+ Page::Project::Show.act { wait_for_push }
+ Page::Menu::Side.act { click_ci_cd_pipelines }
+ Page::Project::Pipeline::Index.act { go_to_latest_pipeline }
- sha1sum = Digest::SHA1.hexdigest(gitlab_ci)
+ Page::Project::Pipeline::Show.act do
+ go_to_first_job
- Page::Project::Show.act { wait_for_push }
- Page::Menu::Side.act { click_ci_cd_pipelines }
- Page::Project::Pipeline::Index.act { go_to_latest_pipeline }
- Page::Project::Pipeline::Show.act { go_to_first_job }
+ wait do
+ !has_content?('running')
+ end
+ end
- Page::Project::Job::Show.perform do |job|
- expect(job.output).to include(sha1sum)
+ Page::Project::Job::Show.perform do |job|
+ expect(job.output).to include(sha1sum)
+ end
end
end
end
diff --git a/qa/qa/specs/features/repository/clone_spec.rb b/qa/qa/specs/features/repository/clone_spec.rb
index 2adb7524a46..bc9eb57bdb4 100644
--- a/qa/qa/specs/features/repository/clone_spec.rb
+++ b/qa/qa/specs/features/repository/clone_spec.rb
@@ -18,7 +18,7 @@ module QA
end
Git::Repository.perform do |repository|
- repository.location = location
+ repository.uri = location.uri
repository.use_default_credentials
repository.act do
@@ -33,7 +33,7 @@ module QA
scenario 'user performs a deep clone' do
Git::Repository.perform do |repository|
- repository.location = location
+ repository.uri = location.uri
repository.use_default_credentials
repository.act { clone }
@@ -44,7 +44,7 @@ module QA
scenario 'user performs a shallow clone' do
Git::Repository.perform do |repository|
- repository.location = location
+ repository.uri = location.uri
repository.use_default_credentials
repository.act { shallow_clone }
diff --git a/qa/qa/specs/features/repository/protected_branches_spec.rb b/qa/qa/specs/features/repository/protected_branches_spec.rb
index 88fa4994e32..406b2772b64 100644
--- a/qa/qa/specs/features/repository/protected_branches_spec.rb
+++ b/qa/qa/specs/features/repository/protected_branches_spec.rb
@@ -19,6 +19,13 @@ module QA
Page::Main::Login.act { sign_in_using_credentials }
end
+ after do
+ # We need to clear localStorage because we're using it for the dropdown,
+ # and capybara doesn't do this for us.
+ # https://github.com/teamcapybara/capybara/issues/1702
+ Capybara.execute_script 'localStorage.clear()'
+ end
+
scenario 'user is able to protect a branch' do
protected_branch = Factory::Resource::Branch.fabricate! do |resource|
resource.branch_name = branch_name
@@ -42,7 +49,7 @@ module QA
project.visit!
Git::Repository.perform do |repository|
- repository.location = location
+ repository.uri = location.uri
repository.use_default_credentials
repository.act do
diff --git a/qa/spec/runtime/key/ecdsa_spec.rb b/qa/spec/runtime/key/ecdsa_spec.rb
new file mode 100644
index 00000000000..8951e82b9bb
--- /dev/null
+++ b/qa/spec/runtime/key/ecdsa_spec.rb
@@ -0,0 +1,18 @@
+describe QA::Runtime::Key::ECDSA do
+ describe '#public_key' do
+ [256, 384, 521].each do |bits|
+ it "generates a public #{bits}-bits ECDSA key" do
+ subject = described_class.new(bits).public_key
+
+ expect(subject).to match(%r{\Aecdsa\-sha2\-\w+ AAAA[0-9A-Za-z+/]+={0,3}})
+ end
+ end
+ end
+
+ describe '#new' do
+ it 'does not support arbitrary bits' do
+ expect { described_class.new(123) }
+ .to raise_error(QA::Service::Shellout::CommandError)
+ end
+ end
+end
diff --git a/qa/spec/runtime/key/ed25519_spec.rb b/qa/spec/runtime/key/ed25519_spec.rb
new file mode 100644
index 00000000000..4844e7affdf
--- /dev/null
+++ b/qa/spec/runtime/key/ed25519_spec.rb
@@ -0,0 +1,9 @@
+describe QA::Runtime::Key::ED25519 do
+ describe '#public_key' do
+ subject { described_class.new.public_key }
+
+ it 'generates a public ED25519 key' do
+ expect(subject).to match(%r{\Assh\-ed25519 AAAA[0-9A-Za-z+/]})
+ end
+ end
+end
diff --git a/qa/spec/runtime/rsa_key.rb b/qa/spec/runtime/key/rsa_spec.rb
index 6d7ab4dcd2e..fbcc7ffdcb4 100644
--- a/qa/spec/runtime/rsa_key.rb
+++ b/qa/spec/runtime/key/rsa_spec.rb
@@ -1,9 +1,9 @@
-describe QA::Runtime::RSAKey do
+describe QA::Runtime::Key::RSA do
describe '#public_key' do
subject { described_class.new.public_key }
it 'generates a public RSA key' do
- expect(subject).to match(%r{\Assh\-rsa AAAA[0-9A-Za-z+/]+={0,3}\z})
+ expect(subject).to match(%r{\Assh\-rsa AAAA[0-9A-Za-z+/]+={0,3}})
end
end
end
diff --git a/spec/controllers/projects/clusters/gcp_controller_spec.rb b/spec/controllers/projects/clusters/gcp_controller_spec.rb
index e14ba29fa70..715bb9f5e52 100644
--- a/spec/controllers/projects/clusters/gcp_controller_spec.rb
+++ b/spec/controllers/projects/clusters/gcp_controller_spec.rb
@@ -142,7 +142,7 @@ describe Projects::Clusters::GcpController do
context 'when google project billing is enabled' do
before do
- redis_double = double
+ redis_double = double.as_null_object
allow(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis_double)
allow(redis_double).to receive(:get).with(CheckGcpProjectBillingWorker.redis_shared_state_key_for('token')).and_return('true')
end
diff --git a/spec/controllers/projects/merge_requests/creations_controller_spec.rb b/spec/controllers/projects/merge_requests/creations_controller_spec.rb
index 24310b847e8..00d76f3c39a 100644
--- a/spec/controllers/projects/merge_requests/creations_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/creations_controller_spec.rb
@@ -157,34 +157,4 @@ describe Projects::MergeRequests::CreationsController do
expect(response).to have_gitlab_http_status(200)
end
end
-
- describe 'GET #update_branches' do
- before do
- allow(Ability).to receive(:allowed?).and_call_original
- end
-
- it 'lists the branches of another fork if the user has access' do
- expect(Ability).to receive(:allowed?).with(user, :read_project, project) { true }
-
- get :update_branches,
- namespace_id: fork_project.namespace,
- project_id: fork_project,
- target_project_id: project.id
-
- expect(assigns(:target_branches)).not_to be_empty
- expect(response).to have_gitlab_http_status(200)
- end
-
- it 'does not list branches when the user cannot read the project' do
- expect(Ability).to receive(:allowed?).with(user, :read_project, project) { false }
-
- get :update_branches,
- namespace_id: fork_project.namespace,
- project_id: fork_project,
- target_project_id: project.id
-
- expect(response).to have_gitlab_http_status(200)
- expect(assigns(:target_branches)).to eq([])
- end
- end
end
diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb
index 08e2ccf893a..c3468536ae1 100644
--- a/spec/controllers/projects/raw_controller_spec.rb
+++ b/spec/controllers/projects/raw_controller_spec.rb
@@ -54,9 +54,9 @@ describe Projects::RawController do
end
context 'and lfs uses object storage' do
+ let(:lfs_object) { create(:lfs_object, :with_file, oid: '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', size: '1575078') }
+
before do
- lfs_object.file = fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "`/png")
- lfs_object.save!
stub_lfs_object_storage
lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
end
diff --git a/spec/factories/ci/stages.rb b/spec/factories/ci/stages.rb
index 25309033571..ce61e6bf759 100644
--- a/spec/factories/ci/stages.rb
+++ b/spec/factories/ci/stages.rb
@@ -21,6 +21,7 @@ FactoryBot.define do
pipeline factory: :ci_empty_pipeline
name 'test'
+ position 1
status 'pending'
end
end
diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb
index ce5fbc343ee..53368c64e10 100644
--- a/spec/factories/commit_statuses.rb
+++ b/spec/factories/commit_statuses.rb
@@ -2,6 +2,7 @@ FactoryBot.define do
factory :commit_status, class: CommitStatus do
name 'default'
stage 'test'
+ stage_idx 0
status 'success'
description 'commit status'
pipeline factory: :ci_pipeline_with_one_job
diff --git a/spec/features/admin/admin_uses_repository_checks_spec.rb b/spec/features/admin/admin_uses_repository_checks_spec.rb
index f1ac73ff819..90cf5a53787 100644
--- a/spec/features/admin/admin_uses_repository_checks_spec.rb
+++ b/spec/features/admin/admin_uses_repository_checks_spec.rb
@@ -19,7 +19,7 @@ feature 'Admin uses repository checks' do
expect(page).to have_content('Repository check was triggered')
end
- scenario 'to see a single failed repository check' do
+ scenario 'to see a single failed repository check', :js do
project = create(:project)
project.update_columns(
last_repository_check_failed: true,
diff --git a/spec/features/dashboard/groups_list_spec.rb b/spec/features/dashboard/groups_list_spec.rb
index a71020002dc..ed47f7ed390 100644
--- a/spec/features/dashboard/groups_list_spec.rb
+++ b/spec/features/dashboard/groups_list_spec.rb
@@ -40,7 +40,7 @@ feature 'Dashboard Groups page', :js do
expect(page).to have_content(nested_group.name)
end
- describe 'when filtering groups', :nested_groups do
+ context 'when filtering groups', :nested_groups do
before do
group.add_owner(user)
nested_group.add_owner(user)
@@ -75,7 +75,7 @@ feature 'Dashboard Groups page', :js do
end
end
- describe 'group with subgroups', :nested_groups do
+ context 'with subgroups', :nested_groups do
let!(:subgroup) { create(:group, :public, parent: group) }
before do
@@ -106,7 +106,7 @@ feature 'Dashboard Groups page', :js do
end
end
- describe 'when using pagination' do
+ context 'when using pagination' do
let(:group) { create(:group, created_at: 5.days.ago) }
let(:group2) { create(:group, created_at: 2.days.ago) }
@@ -141,4 +141,20 @@ feature 'Dashboard Groups page', :js do
expect(page).not_to have_selector("#group-#{group2.id}")
end
end
+
+ context 'when signed in as admin' do
+ let(:admin) { create(:admin) }
+
+ it 'shows only groups admin is member of' do
+ group.add_owner(admin)
+ expect(another_group).to be_persisted
+
+ sign_in(admin)
+ visit dashboard_groups_path
+ wait_for_requests
+
+ expect(page).to have_content(group.name)
+ expect(page).not_to have_content(another_group.name)
+ end
+ end
end
diff --git a/spec/features/dashboard/milestone_filter_spec.rb b/spec/features/dashboard/milestone_filter_spec.rb
index c965b565ca3..8cd57f4f327 100644
--- a/spec/features/dashboard/milestone_filter_spec.rb
+++ b/spec/features/dashboard/milestone_filter_spec.rb
@@ -10,13 +10,16 @@ feature 'Dashboard > milestone filter', :js do
let!(:issue) { create :issue, author: user, project: project, milestone: milestone }
let!(:issue2) { create :issue, author: user, project: project, milestone: milestone2 }
+ dropdown_toggle_button = '.js-milestone-select'
+
before do
sign_in(user)
- visit issues_dashboard_path(author_id: user.id)
end
context 'default state' do
it 'shows issues with Any Milestone' do
+ visit issues_dashboard_path(author_id: user.id)
+
page.all('.issue-info').each do |issue_info|
expect(issue_info.text).to match(/v\d.0/)
end
@@ -24,31 +27,51 @@ feature 'Dashboard > milestone filter', :js do
end
context 'filtering by milestone' do
- milestone_select_selector = '.js-milestone-select'
-
before do
- filter_item_select('v1.0', milestone_select_selector)
- find(milestone_select_selector).click
+ visit issues_dashboard_path(author_id: user.id)
+ filter_item_select('v1.0', dropdown_toggle_button)
+ find(dropdown_toggle_button).click
wait_for_requests
end
it 'shows issues with Milestone v1.0' do
expect(find('.issues-list')).to have_selector('.issue', count: 1)
- expect(find('.dropdown-content')).to have_selector('a.is-active', count: 1)
+ expect(find('.milestone-filter .dropdown-content')).to have_selector('a.is-active', count: 1)
end
it 'should not change active Milestone unless clicked' do
- expect(find('.dropdown-content')).to have_selector('a.is-active', count: 1)
+ page.within '.milestone-filter' do
+ expect(find('.dropdown-content')).to have_selector('a.is-active', count: 1)
+
+ find('.dropdown-menu-close').click
- # open & close dropdown
- find('.dropdown-menu-close').click
+ expect(page).not_to have_selector('.dropdown.open')
+
+ find(dropdown_toggle_button).click
+
+ expect(find('.dropdown-content')).to have_selector('a.is-active', count: 1)
+ expect(find('.dropdown-content a.is-active')).to have_content('v1.0')
+ end
+ end
+ end
+
+ context 'with milestone filter in URL' do
+ before do
+ visit issues_dashboard_path(author_id: user.id, milestone_title: milestone.title)
+ find(dropdown_toggle_button).click
+ wait_for_requests
+ end
+
+ it 'has milestone selected' do
+ expect(find('.milestone-filter .dropdown-content')).to have_css('.is-active', text: milestone.title)
+ end
- expect(find('.milestone-filter')).not_to have_selector('.dropdown.open')
+ it 'removes milestone filter from URL after clicking "Any Milestone"' do
+ expect(current_url).to include("milestone_title=#{milestone.title}")
- find(milestone_select_selector).click
+ find('.milestone-filter .dropdown-content li', text: 'Any Milestone').click
- expect(find('.dropdown-content')).to have_selector('a.is-active', count: 1)
- expect(find('.dropdown-content a.is-active')).to have_content('v1.0')
+ expect(current_url).not_to include('milestone_title')
end
end
end
diff --git a/spec/features/groups/members/manage_access_requests_spec.rb b/spec/features/groups/members/manage_access_requests_spec.rb
deleted file mode 100644
index b83cd657ef7..00000000000
--- a/spec/features/groups/members/manage_access_requests_spec.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-require 'spec_helper'
-
-feature 'Groups > Members > Manage access requests' do
- let(:user) { create(:user) }
- let(:owner) { create(:user) }
- let(:group) { create(:group, :public, :access_requestable) }
-
- background do
- group.request_access(user)
- group.add_owner(owner)
- sign_in(owner)
- end
-
- scenario 'owner can see access requests' do
- visit group_group_members_path(group)
-
- expect_visible_access_request(group, user)
- end
-
- scenario 'owner can grant access' do
- visit group_group_members_path(group)
-
- expect_visible_access_request(group, user)
-
- perform_enqueued_jobs { click_on 'Grant access' }
-
- expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email]
- expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{group.name} group was granted"
- end
-
- scenario 'owner can deny access' do
- visit group_group_members_path(group)
-
- expect_visible_access_request(group, user)
-
- perform_enqueued_jobs { click_on 'Deny access' }
-
- expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email]
- expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{group.name} group was denied"
- end
-
- def expect_visible_access_request(group, user)
- expect(group.requesters.exists?(user_id: user)).to be_truthy
- expect(page).to have_content "Users requesting access to #{group.name} 1"
- expect(page).to have_content user.name
- end
-end
diff --git a/spec/features/groups/members/master_manages_access_requests_spec.rb b/spec/features/groups/members/master_manages_access_requests_spec.rb
new file mode 100644
index 00000000000..2fd6d1ec599
--- /dev/null
+++ b/spec/features/groups/members/master_manages_access_requests_spec.rb
@@ -0,0 +1,8 @@
+require 'spec_helper'
+
+feature 'Groups > Members > Master manages access requests' do
+ it_behaves_like 'Master manages access requests' do
+ let(:entity) { create(:group, :public, :access_requestable) }
+ let(:members_page_path) { group_group_members_path(entity) }
+ end
+end
diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb
index ff2a0e15719..ddd64fa1412 100644
--- a/spec/features/issues/user_uses_slash_commands_spec.rb
+++ b/spec/features/issues/user_uses_slash_commands_spec.rb
@@ -178,9 +178,10 @@ feature 'Issues > User uses quick actions', :js do
end
context 'when the project is valid but the user not authorized' do
- let(:project_unauthorized) {create(:project, :public)}
+ let(:project_unauthorized) { create(:project, :public) }
before do
+ gitlab_sign_out
sign_in(user)
visit project_issue_path(project, issue)
end
@@ -195,6 +196,7 @@ feature 'Issues > User uses quick actions', :js do
context 'when the project is invalid' do
before do
+ gitlab_sign_out
sign_in(user)
visit project_issue_path(project, issue)
end
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 dbca279569a..42c279af117 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
@@ -19,7 +19,7 @@ describe 'Merge request > User selects branches for new MR', :js do
expect(page).to have_content('Target branch')
first('.js-source-branch').click
- find('.dropdown-source-branch .dropdown-content a', match: :first).click
+ find('.js-source-branch-dropdown .dropdown-content a', match: :first).click
expect(page).to have_content "b83d6e3"
end
@@ -35,22 +35,16 @@ describe 'Merge request > User selects branches for new MR', :js do
expect(page).to have_content('Target branch')
first('.js-target-branch').click
- find('.dropdown-target-branch .dropdown-content a', text: 'v1.1.0', match: :first).click
+ find('.js-target-branch-dropdown .dropdown-content a', text: 'v1.1.0', match: :first).click
expect(page).to have_content "b83d6e3"
end
it 'generates a diff for an orphaned branch' do
- visit project_merge_requests_path(project)
-
- page.within '.content' do
- click_link 'New merge request'
- end
- expect(page).to have_content('Source branch')
- expect(page).to have_content('Target branch')
+ visit project_new_merge_request_path(project)
find('.js-source-branch', match: :first).click
- find('.dropdown-source-branch .dropdown-content a', text: 'orphaned-branch', match: :first).click
+ find('.js-source-branch-dropdown .dropdown-content a', text: 'orphaned-branch', match: :first).click
click_button "Compare branches"
click_link "Changes"
@@ -71,19 +65,18 @@ describe 'Merge request > User selects branches for new MR', :js do
first('.js-source-branch').click
- input = find('.dropdown-source-branch .dropdown-input-field')
- input.click
- input.send_keys('orphaned-branch')
+ page.within '.js-source-branch-dropdown' do
+ input = find('.dropdown-input-field')
+ input.click
+ input.send_keys('orphaned-branch')
- find('.dropdown-source-branch .dropdown-content li', match: :first)
- source_items = all('.dropdown-source-branch .dropdown-content li')
-
- expect(source_items.count).to eq(1)
+ expect(page).to have_css('.dropdown-content li', count: 1)
+ end
first('.js-target-branch').click
- find('.dropdown-target-branch .dropdown-content li', match: :first)
- target_items = all('.dropdown-target-branch .dropdown-content li')
+ find('.js-target-branch-dropdown .dropdown-content li', match: :first)
+ target_items = all('.js-target-branch-dropdown .dropdown-content li')
expect(target_items.count).to be > 1
end
@@ -171,7 +164,6 @@ describe 'Merge request > User selects branches for new MR', :js do
page.within('.merge-request') do
click_link 'Pipelines'
- wait_for_requests
expect(page).to have_content "##{pipeline.id}"
end
diff --git a/spec/features/profiles/active_sessions_spec.rb b/spec/features/profiles/active_sessions_spec.rb
new file mode 100644
index 00000000000..4045cfd21c4
--- /dev/null
+++ b/spec/features/profiles/active_sessions_spec.rb
@@ -0,0 +1,89 @@
+require 'rails_helper'
+
+feature 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do
+ let(:user) do
+ create(:user).tap do |user|
+ user.current_sign_in_at = Time.current
+ end
+ end
+
+ around do |example|
+ Timecop.freeze(Time.zone.parse('2018-03-12 09:06')) do
+ example.run
+ end
+ end
+
+ scenario 'User sees their active sessions' do
+ Capybara::Session.new(:session1)
+ Capybara::Session.new(:session2)
+
+ # note: headers can only be set on the non-js (aka. rack-test) driver
+ using_session :session1 do
+ Capybara.page.driver.header(
+ 'User-Agent',
+ 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:58.0) Gecko/20100101 Firefox/58.0'
+ )
+
+ gitlab_sign_in(user)
+ end
+
+ # set an additional session on another device
+ using_session :session2 do
+ Capybara.page.driver.header(
+ 'User-Agent',
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_1_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12B466 [FBDV/iPhone7,2]'
+ )
+
+ gitlab_sign_in(user)
+ end
+
+ using_session :session1 do
+ visit profile_active_sessions_path
+
+ expect(page).to have_content(
+ '127.0.0.1 ' \
+ 'This is your current session ' \
+ 'Firefox on Ubuntu ' \
+ 'Signed in on 12 Mar 09:06'
+ )
+
+ expect(page).to have_selector '[title="Desktop"]', count: 1
+
+ expect(page).to have_content(
+ '127.0.0.1 ' \
+ 'Last accessed on 12 Mar 09:06 ' \
+ 'Mobile Safari on iOS ' \
+ 'Signed in on 12 Mar 09:06'
+ )
+
+ expect(page).to have_selector '[title="Smartphone"]', count: 1
+ end
+ end
+
+ scenario 'User can revoke a session', :js, :redis_session_store do
+ Capybara::Session.new(:session1)
+ Capybara::Session.new(:session2)
+
+ # set an additional session in another browser
+ using_session :session2 do
+ gitlab_sign_in(user)
+ end
+
+ using_session :session1 do
+ gitlab_sign_in(user)
+ visit profile_active_sessions_path
+
+ expect(page).to have_link('Revoke', count: 1)
+
+ accept_confirm { click_on 'Revoke' }
+
+ expect(page).not_to have_link('Revoke')
+ end
+
+ using_session :session2 do
+ visit profile_active_sessions_path
+
+ expect(page).to have_content('You need to sign in or sign up before continuing.')
+ end
+ end
+end
diff --git a/spec/features/projects/files/user_browses_files_spec.rb b/spec/features/projects/files/user_browses_files_spec.rb
index 9c1f11f4c12..41f6c52fb8a 100644
--- a/spec/features/projects/files/user_browses_files_spec.rb
+++ b/spec/features/projects/files/user_browses_files_spec.rb
@@ -1,14 +1,12 @@
-require 'spec_helper'
+require "spec_helper"
-describe 'Projects > Files > User browses files' do
+describe "User browses files" do
let(:fork_message) do
"You're not allowed to make changes to this project directly. "\
"A fork of this project has been created that you can make changes in, so you can submit a merge request."
end
- let(:project) { create(:project, :repository, name: 'Shop') }
- let(:project2) { create(:project, :repository, name: 'Another Project', path: 'another-project') }
- let(:project2_tree_path_root_ref) { project_tree_path(project2, project2.repository.root_ref) }
- let(:tree_path_ref_6d39438) { project_tree_path(project, '6d39438') }
+ let(:project) { create(:project, :repository, name: "Shop") }
+ let(:project2) { create(:project, :repository, name: "Another Project", path: "another-project") }
let(:tree_path_root_ref) { project_tree_path(project, project.repository.root_ref) }
let(:user) { project.owner }
@@ -16,57 +14,55 @@ describe 'Projects > Files > User browses files' do
sign_in(user)
end
- it 'shows last commit for current directory' do
+ it "shows last commit for current directory" do
visit(tree_path_root_ref)
- click_link 'files'
+ click_link("files")
- last_commit = project.repository.last_commit_for_path(project.default_branch, 'files')
- page.within('.blob-commit-info') do
- expect(page).to have_content last_commit.short_id
- expect(page).to have_content last_commit.author_name
+ last_commit = project.repository.last_commit_for_path(project.default_branch, "files")
+
+ page.within(".blob-commit-info") do
+ expect(page).to have_content(last_commit.short_id).and have_content(last_commit.author_name)
end
end
- context 'when browsing the master branch' do
+ context "when browsing the master branch" do
before do
visit(tree_path_root_ref)
end
- it 'shows files from a repository' do
- expect(page).to have_content('VERSION')
- expect(page).to have_content('.gitignore')
- expect(page).to have_content('LICENSE')
+ it "shows files from a repository" do
+ expect(page).to have_content("VERSION")
+ .and have_content(".gitignore")
+ .and have_content("LICENSE")
end
- it 'shows the "Browse Directory" link' do
- click_link('files')
- click_link('History')
+ it "shows the `Browse Directory` link" do
+ click_link("files")
+ click_link("History")
- expect(page).to have_link('Browse Directory')
- expect(page).not_to have_link('Browse Code')
+ expect(page).to have_link("Browse Directory").and have_no_link("Browse Code")
end
- it 'shows the "Browse File" link' do
- page.within('.tree-table') do
- click_link('README.md')
+ it "shows the `Browse File` link" do
+ page.within(".tree-table") do
+ click_link("README.md")
end
- click_link('History')
- expect(page).to have_link('Browse File')
- expect(page).not_to have_link('Browse Files')
+ click_link("History")
+
+ expect(page).to have_link("Browse File").and have_no_link("Browse Files")
end
- it 'shows the "Browse Files" link' do
- click_link('History')
+ it "shows the `Browse Files` link" do
+ click_link("History")
- expect(page).to have_link('Browse Files')
- expect(page).not_to have_link('Browse Directory')
+ expect(page).to have_link("Browse Files").and have_no_link("Browse Directory")
end
- it 'redirects to the permalink URL' do
- click_link('.gitignore')
- click_link('Permalink')
+ it "redirects to the permalink URL" do
+ click_link(".gitignore")
+ click_link("Permalink")
permalink_path = project_blob_path(project, "#{project.repository.commit.sha}/.gitignore")
@@ -74,80 +70,180 @@ describe 'Projects > Files > User browses files' do
end
end
- context 'when browsing a specific ref' do
+ context "when browsing the `markdown` branch", :js do
+ context "when browsing the root" do
+ before do
+ visit(project_tree_path(project, "markdown"))
+ end
+
+ it "shows correct files and links" do
+ # rubocop:disable Lint/Void
+ # Test the full URLs of links instead of relative paths by `have_link(text: "...", href: "...")`.
+ find("a", text: /^empty$/)["href"] == project_tree_url(project, "markdown")
+ find("a", text: /^#id$/)["href"] == project_tree_url(project, "markdown", anchor: "#id")
+ find("a", text: %r{^/#id$})["href"] == project_tree_url(project, "markdown", anchor: "#id")
+ find("a", text: /^README.md#id$/)["href"] == project_blob_url(project, "markdown/README.md", anchor: "#id")
+ find("a", text: %r{^d/README.md#id$})["href"] == project_blob_url(project, "d/markdown/README.md", anchor: "#id")
+ # rubocop:enable Lint/Void
+
+ expect(current_path).to eq(project_tree_path(project, "markdown"))
+ expect(page).to have_content("README.md")
+ .and have_content("CHANGELOG")
+ .and have_content("Welcome to GitLab GitLab is a free project and repository management application")
+ .and have_link("GitLab API doc")
+ .and have_link("GitLab API website")
+ .and have_link("Rake tasks")
+ .and have_link("backup and restore procedure")
+ .and have_link("GitLab API doc directory")
+ .and have_link("Maintenance")
+ .and have_header_with_correct_id_and_link(2, "Application details", "application-details")
+ end
+
+ it "shows correct content of file" do
+ click_link("GitLab API doc")
+
+ expect(current_path).to eq(project_blob_path(project, "markdown/doc/api/README.md"))
+ expect(page).to have_content("All API requests require authentication")
+ .and have_content("Contents")
+ .and have_link("Users")
+ .and have_link("Rake tasks")
+ .and have_header_with_correct_id_and_link(1, "GitLab API", "gitlab-api")
+
+ click_link("Users")
+
+ expect(current_path).to eq(project_blob_path(project, "markdown/doc/api/users.md"))
+ expect(page).to have_content("Get a list of users.")
+
+ page.go_back
+
+ click_link("Rake tasks")
+
+ expect(current_path).to eq(project_tree_path(project, "markdown/doc/raketasks"))
+ expect(page).to have_content("backup_restore.md").and have_content("maintenance.md")
+
+ click_link("shop")
+ click_link("Maintenance")
+
+ expect(current_path).to eq(project_blob_path(project, "markdown/doc/raketasks/maintenance.md"))
+ expect(page).to have_content("bundle exec rake gitlab:env:info RAILS_ENV=production")
+
+ click_link("shop")
+
+ page.within(".tree-table") do
+ click_link("README.md")
+ end
+
+ page.go_back
+
+ page.within(".tree-table") do
+ click_link("d")
+ end
+
+ # rubocop:disable Lint/Void
+ # Test the full URLs of links instead of relative paths by `have_link(text: "...", href: "...")`.
+ find("a", text: /^empty$/)["href"] == project_tree_url(project, "markdown/d")
+ # rubocop:enable Lint/Void
+
+ page.within(".tree-table") do
+ click_link("README.md")
+ end
+
+ # rubocop:disable Lint/Void
+ # Test the full URLs of links instead of relative paths by `have_link(text: "...", href: "...")`.
+ find("a", text: /^empty$/)["href"] == project_blob_url(project, "markdown/d/README.md")
+ # rubocop:enable Lint/Void
+ end
+
+ it "shows correct content of directory" do
+ click_link("GitLab API doc directory")
+
+ expect(current_path).to eq(project_tree_path(project, "markdown/doc/api"))
+ expect(page).to have_content("README.md").and have_content("users.md")
+
+ click_link("Users")
+
+ expect(current_path).to eq(project_blob_path(project, "markdown/doc/api/users.md"))
+ expect(page).to have_content("List users").and have_content("Get a list of users.")
+ end
+ end
+ end
+
+ context "when browsing a specific ref" do
+ let(:ref) { project_tree_path(project, "6d39438") }
+
before do
- visit(tree_path_ref_6d39438)
+ visit(ref)
end
- it 'shows files from a repository for "6d39438"' do
- expect(current_path).to eq(tree_path_ref_6d39438)
- expect(page).to have_content('.gitignore')
- expect(page).to have_content('LICENSE')
+ it "shows files from a repository for `6d39438`" do
+ expect(current_path).to eq(ref)
+ expect(page).to have_content(".gitignore").and have_content("LICENSE")
end
- it 'shows files from a repository with apostroph in its name', :js do
- first('.js-project-refs-dropdown').click
+ it "shows files from a repository with apostroph in its name", :js do
+ first(".js-project-refs-dropdown").click
- page.within('.project-refs-form') do
+ page.within(".project-refs-form") do
click_link("'test'")
end
- expect(page).to have_selector('.dropdown-toggle-text', text: "'test'")
+ expect(page).to have_selector(".dropdown-toggle-text", text: "'test'")
visit(project_tree_path(project, "'test'"))
- expect(page).to have_css('.tree-commit-link', visible: true)
- expect(page).not_to have_content('Loading commit data...')
+ expect(page).to have_css(".tree-commit-link").and have_no_content("Loading commit data...")
end
- it 'shows the code with a leading dot in the directory', :js do
- first('.js-project-refs-dropdown').click
+ it "shows the code with a leading dot in the directory", :js do
+ first(".js-project-refs-dropdown").click
- page.within('.project-refs-form') do
- click_link('fix')
+ page.within(".project-refs-form") do
+ click_link("fix")
end
- visit(project_tree_path(project, 'fix/.testdir'))
+ visit(project_tree_path(project, "fix/.testdir"))
- expect(page).to have_css('.tree-commit-link', visible: true)
- expect(page).not_to have_content('Loading commit data...')
+ expect(page).to have_css(".tree-commit-link").and have_no_content("Loading commit data...")
end
- it 'does not show the permalink link' do
- click_link('.gitignore')
+ it "does not show the permalink link" do
+ click_link(".gitignore")
- expect(page).not_to have_link('permalink')
+ expect(page).not_to have_link("permalink")
end
end
- context 'when browsing a file content' do
+ context "when browsing a file content" do
before do
visit(tree_path_root_ref)
- click_link('.gitignore')
+
+ click_link(".gitignore")
end
- it 'shows a file content', :js do
- wait_for_requests
- expect(page).to have_content('*.rbc')
+ it "shows a file content", :js do
+ expect(page).to have_content("*.rbc")
end
- it 'is possible to blame' do
- click_link 'Blame'
+ it "is possible to blame" do
+ click_link("Blame")
- expect(page).to have_content "*.rb"
- expect(page).to have_content "Dmitriy Zaporozhets"
- expect(page).to have_content "Initial commit"
+ expect(page).to have_content("*.rb")
+ .and have_content("Dmitriy Zaporozhets")
+ .and have_content("Initial commit")
end
end
- context 'when browsing a raw file' do
+ context "when browsing a raw file" do
before do
- visit(project_blob_path(project, File.join(RepoHelpers.sample_commit.id, RepoHelpers.sample_blob.path)))
+ path = File.join(RepoHelpers.sample_commit.id, RepoHelpers.sample_blob.path)
+
+ visit(project_blob_path(project, path))
end
- it 'shows a raw file content' do
- click_link('Open raw')
- expect(source).to eq('') # Body is filled in by gitlab-workhorse
+ it "shows a raw file content" do
+ click_link("Open raw")
+
+ expect(source).to eq("") # Body is filled in by gitlab-workhorse
end
end
end
diff --git a/spec/features/projects/issues/user_toggles_subscription_spec.rb b/spec/features/projects/issues/user_toggles_subscription_spec.rb
index 117a614b980..c2b2a193682 100644
--- a/spec/features/projects/issues/user_toggles_subscription_spec.rb
+++ b/spec/features/projects/issues/user_toggles_subscription_spec.rb
@@ -1,9 +1,9 @@
require "spec_helper"
describe "User toggles subscription", :js do
- set(:project) { create(:project_empty_repo, :public) }
- set(:user) { create(:user) }
- set(:issue) { create(:issue, project: project, author: user) }
+ let(:project) { create(:project_empty_repo, :public) }
+ let(:user) { create(:user) }
+ let(:issue) { create(:issue, project: project, author: user) }
before do
project.add_developer(user)
@@ -12,7 +12,7 @@ describe "User toggles subscription", :js do
visit(project_issue_path(project, issue))
end
- it "unsibscribes from issue" do
+ it "unsubscribes from issue" do
subscription_button = find(".js-issuable-subscribe-button")
# Check we're subscribed.
diff --git a/spec/features/projects/members/master_manages_access_requests_spec.rb b/spec/features/projects/members/master_manages_access_requests_spec.rb
index 1f4eec0a317..3ac6ca4fc86 100644
--- a/spec/features/projects/members/master_manages_access_requests_spec.rb
+++ b/spec/features/projects/members/master_manages_access_requests_spec.rb
@@ -1,47 +1,8 @@
require 'spec_helper'
feature 'Projects > Members > Master manages access requests' do
- let(:user) { create(:user) }
- let(:master) { create(:user) }
- let(:project) { create(:project, :public, :access_requestable) }
-
- background do
- project.request_access(user)
- project.add_master(master)
- sign_in(master)
- end
-
- scenario 'master can see access requests' do
- visit project_project_members_path(project)
-
- expect_visible_access_request(project, user)
- end
-
- scenario 'master can grant access' do
- visit project_project_members_path(project)
-
- expect_visible_access_request(project, user)
-
- perform_enqueued_jobs { click_on 'Grant access' }
-
- expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email]
- expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{project.full_name} project was granted"
- end
-
- scenario 'master can deny access' do
- visit project_project_members_path(project)
-
- expect_visible_access_request(project, user)
-
- perform_enqueued_jobs { click_on 'Deny access' }
-
- expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email]
- expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{project.full_name} project was denied"
- end
-
- def expect_visible_access_request(project, user)
- expect(project.requesters.exists?(user_id: user)).to be_truthy
- expect(page).to have_content "Users requesting access to #{project.name} 1"
- expect(page).to have_content user.name
+ it_behaves_like 'Master manages access requests' do
+ let(:entity) { create(:project, :public, :access_requestable) }
+ let(:members_page_path) { project_project_members_path(entity) }
end
end
diff --git a/spec/features/projects/settings/lfs_settings_spec.rb b/spec/features/projects/settings/lfs_settings_spec.rb
index 0fd28a5681c..342be1d2a9d 100644
--- a/spec/features/projects/settings/lfs_settings_spec.rb
+++ b/spec/features/projects/settings/lfs_settings_spec.rb
@@ -1,21 +1,27 @@
require 'rails_helper'
describe 'Projects > Settings > LFS settings' do
- let(:admin) { create(:admin) }
let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:role) { :master }
context 'LFS enabled setting' do
before do
allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
- sign_in(admin)
+ sign_in(user)
+ project.add_role(user, role)
end
- it 'displays the correct elements', :js do
- visit edit_project_path(project)
+ context 'for master' do
+ let(:role) { :master }
- expect(page).to have_content('Git Large File Storage')
- expect(page).to have_selector('input[name="project[lfs_enabled]"] + button', visible: true)
+ it 'displays the correct elements', :js do
+ visit edit_project_path(project)
+
+ expect(page).to have_content('Git Large File Storage')
+ expect(page).to have_selector('input[name="project[lfs_enabled]"] + button', visible: true)
+ end
end
end
end
diff --git a/spec/features/projects/tree/upload_file_spec.rb b/spec/features/projects/tree/upload_file_spec.rb
index 8e53ae15700..4dfc325b37e 100644
--- a/spec/features/projects/tree/upload_file_spec.rb
+++ b/spec/features/projects/tree/upload_file_spec.rb
@@ -35,17 +35,4 @@ feature 'Multi-file editor upload file', :js do
expect(page).to have_selector('.multi-file-tab', text: 'doc_sample.txt')
expect(find('.blob-editor-container .lines-content')['innerText']).to have_content(File.open(txt_file, &:readline))
end
-
- it 'uploads image file' do
- find('.add-to-tree').click
-
- # make the field visible so capybara can use it
- execute_script('document.querySelector("#file-upload").classList.remove("hidden")')
- attach_file('file-upload', img_file)
-
- find('.add-to-tree').click
-
- expect(page).to have_selector('.multi-file-tab', text: 'dk.png')
- expect(page).not_to have_selector('.monaco-editor')
- end
end
diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
index 4a9d1cb87e1..fe6fa55fa75 100644
--- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
@@ -1,6 +1,6 @@
-require 'spec_helper'
+require "spec_helper"
-describe 'User creates wiki page' do
+describe "User creates wiki page" do
let(:user) { create(:user) }
before do
@@ -10,67 +10,104 @@ describe 'User creates wiki page' do
visit(project_wikis_path(project))
end
- context 'when wiki is empty' do
- context 'in a user namespace' do
+ context "when wiki is empty" do
+ context "in a user namespace" do
let(:project) { create(:project, namespace: user.namespace) }
- it 'shows validation error message' do
- page.within('.wiki-form') do
- fill_in(:wiki_content, with: '')
- click_on('Create page')
+ it "shows validation error message" do
+ page.within(".wiki-form") do
+ fill_in(:wiki_content, with: "")
+
+ click_on("Create page")
end
- expect(page).to have_content('The form contains the following error:')
- expect(page).to have_content("Content can't be blank")
+ expect(page).to have_content("The form contains the following error:").and have_content("Content can't be blank")
+
+ page.within(".wiki-form") do
+ fill_in(:wiki_content, with: "[link test](test)")
- page.within('.wiki-form') do
- fill_in(:wiki_content, with: '[link test](test)')
- click_on('Create page')
+ click_on("Create page")
end
- expect(page).to have_content('Home')
- expect(page).to have_content('link test')
+ expect(page).to have_content("Home").and have_content("link test")
- click_link('link test')
+ click_link("link test")
- expect(page).to have_content('Create Page')
+ expect(page).to have_content("Create Page")
end
- it 'shows non-escaped link in the pages list', :js do
- click_link('New page')
+ it "shows non-escaped link in the pages list", :js do
+ click_link("New page")
- page.within('#modal-new-wiki') do
- fill_in(:new_wiki_path, with: 'one/two/three-test')
- click_on('Create page')
+ page.within("#modal-new-wiki") do
+ fill_in(:new_wiki_path, with: "one/two/three-test")
+
+ click_on("Create page")
end
- page.within('.wiki-form') do
- fill_in(:wiki_content, with: 'wiki content')
- click_on('Create page')
+ page.within(".wiki-form") do
+ fill_in(:wiki_content, with: "wiki content")
+
+ click_on("Create page")
end
- expect(current_path).to include('one/two/three-test')
+ expect(current_path).to include("one/two/three-test")
expect(page).to have_xpath("//a[@href='/#{project.full_path}/wikis/one/two/three-test']")
end
- it 'has "Create home" as a commit message' do
- expect(page).to have_field('wiki[message]', with: 'Create home')
+ it "has `Create home` as a commit message" do
+ expect(page).to have_field("wiki[message]", with: "Create home")
end
- it 'creates a page from the home page' do
- fill_in(:wiki_content, with: 'My awesome wiki!')
+ it "creates a page from the home page" do
+ fill_in(:wiki_content, with: "[test](test)\n[GitLab API doc](api)\n[Rake tasks](raketasks)\n# Wiki header\n")
+ fill_in(:wiki_message, with: "Adding links to wiki")
+
+ page.within(".wiki-form") do
+ click_button("Create page")
+ end
+
+ expect(current_path).to eq(project_wiki_path(project, "home"))
+ expect(page).to have_content("test GitLab API doc Rake tasks Wiki header")
+ .and have_content("Home")
+ .and have_content("Last edited by #{user.name}")
+ .and have_header_with_correct_id_and_link(1, "Wiki header", "wiki-header")
+
+ click_link("test")
- page.within('.wiki-form') do
- click_button('Create page')
+ expect(current_path).to eq(project_wiki_path(project, "test"))
+
+ page.within(:css, ".nav-text") do
+ expect(page).to have_content("Test").and have_content("Create Page")
+ end
+
+ click_link("Home")
+
+ expect(current_path).to eq(project_wiki_path(project, "home"))
+
+ click_link("GitLab API")
+
+ expect(current_path).to eq(project_wiki_path(project, "api"))
+
+ page.within(:css, ".nav-text") do
+ expect(page).to have_content("Create").and have_content("Api")
end
- expect(page).to have_content('Home')
- expect(page).to have_content("Last edited by #{user.name}")
- expect(page).to have_content('My awesome wiki!')
+ click_link("Home")
+
+ expect(current_path).to eq(project_wiki_path(project, "home"))
+
+ click_link("Rake tasks")
+
+ expect(current_path).to eq(project_wiki_path(project, "raketasks"))
+
+ page.within(:css, ".nav-text") do
+ expect(page).to have_content("Create").and have_content("Rake")
+ end
end
- it 'creates ASCII wiki with LaTeX blocks', :js do
- stub_application_setting(plantuml_url: 'http://localhost', plantuml_enabled: true)
+ it "creates ASCII wiki with LaTeX blocks", :js do
+ stub_application_setting(plantuml_url: "http://localhost", plantuml_enabled: true)
ascii_content = <<~MD
:stem: latexmath
@@ -90,153 +127,164 @@ describe 'User creates wiki page' do
stem:[2+2] is 4
MD
- find('#wiki_format option[value=asciidoc]').select_option
+ find("#wiki_format option[value=asciidoc]").select_option
+
fill_in(:wiki_content, with: ascii_content)
- page.within('.wiki-form') do
- click_button('Create page')
+ page.within(".wiki-form") do
+ click_button("Create page")
end
- page.within '.wiki' do
- expect(page).to have_selector('.katex', count: 3)
- expect(page).to have_content('2+2 is 4')
+ page.within ".wiki" do
+ expect(page).to have_selector(".katex", count: 3).and have_content("2+2 is 4")
end
end
end
- context 'in a group namespace', :js do
+ context "in a group namespace", :js do
let(:project) { create(:project, namespace: create(:group, :public)) }
- it 'has "Create home" as a commit message' do
- expect(page).to have_field('wiki[message]', with: 'Create home')
+ it "has `Create home` as a commit message" do
+ expect(page).to have_field("wiki[message]", with: "Create home")
end
- it 'creates a page from from the home page' do
- page.within('.wiki-form') do
- fill_in(:wiki_content, with: 'My awesome wiki!')
- click_button('Create page')
+ it "creates a page from from the home page" do
+ page.within(".wiki-form") do
+ fill_in(:wiki_content, with: "My awesome wiki!")
+
+ click_button("Create page")
end
- expect(page).to have_content('Home')
- expect(page).to have_content("Last edited by #{user.name}")
- expect(page).to have_content('My awesome wiki!')
+ expect(page).to have_content("Home")
+ .and have_content("Last edited by #{user.name}")
+ .and have_content("My awesome wiki!")
end
end
end
- context 'when wiki is not empty', :js do
+ context "when wiki is not empty", :js do
before do
- create(:wiki_page, wiki: create(:project, namespace: user.namespace).wiki, attrs: { title: 'home', content: 'Home page' })
+ create(:wiki_page, wiki: create(:project, namespace: user.namespace).wiki, attrs: { title: "home", content: "Home page" })
end
- context 'in a user namespace' do
+ context "in a user namespace" do
let(:project) { create(:project, namespace: user.namespace) }
- context 'via the "new wiki page" page' do
- it 'creates a page with a single word' do
- click_link('New page')
+ context "via the `new wiki page` page" do
+ it "creates a page with a single word" do
+ click_link("New page")
- page.within('#modal-new-wiki') do
- fill_in(:new_wiki_path, with: 'foo')
- click_button('Create page')
+ page.within("#modal-new-wiki") do
+ fill_in(:new_wiki_path, with: "foo")
+
+ click_button("Create page")
end
# Commit message field should have correct value.
- expect(page).to have_field('wiki[message]', with: 'Create foo')
+ expect(page).to have_field("wiki[message]", with: "Create foo")
+
+ page.within(".wiki-form") do
+ fill_in(:wiki_content, with: "My awesome wiki!")
- page.within('.wiki-form') do
- fill_in(:wiki_content, with: 'My awesome wiki!')
- click_button('Create page')
+ click_button("Create page")
end
- expect(page).to have_content('Foo')
- expect(page).to have_content("Last edited by #{user.name}")
- expect(page).to have_content('My awesome wiki!')
+ expect(page).to have_content("Foo")
+ .and have_content("Last edited by #{user.name}")
+ .and have_content("My awesome wiki!")
end
- it 'creates a page with spaces in the name' do
- click_link('New page')
+ it "creates a page with spaces in the name" do
+ click_link("New page")
- page.within('#modal-new-wiki') do
- fill_in(:new_wiki_path, with: 'Spaces in the name')
- click_button('Create page')
+ page.within("#modal-new-wiki") do
+ fill_in(:new_wiki_path, with: "Spaces in the name")
+
+ click_button("Create page")
end
# Commit message field should have correct value.
- expect(page).to have_field('wiki[message]', with: 'Create spaces in the name')
+ expect(page).to have_field("wiki[message]", with: "Create spaces in the name")
+
+ page.within(".wiki-form") do
+ fill_in(:wiki_content, with: "My awesome wiki!")
- page.within('.wiki-form') do
- fill_in(:wiki_content, with: 'My awesome wiki!')
- click_button('Create page')
+ click_button("Create page")
end
- expect(page).to have_content('Spaces in the name')
- expect(page).to have_content("Last edited by #{user.name}")
- expect(page).to have_content('My awesome wiki!')
+ expect(page).to have_content("Spaces in the name")
+ .and have_content("Last edited by #{user.name}")
+ .and have_content("My awesome wiki!")
end
- it 'creates a page with hyphens in the name' do
- click_link('New page')
+ it "creates a page with hyphens in the name" do
+ click_link("New page")
- page.within('#modal-new-wiki') do
- fill_in(:new_wiki_path, with: 'hyphens-in-the-name')
- click_button('Create page')
+ page.within("#modal-new-wiki") do
+ fill_in(:new_wiki_path, with: "hyphens-in-the-name")
+
+ click_button("Create page")
end
# Commit message field should have correct value.
- expect(page).to have_field('wiki[message]', with: 'Create hyphens in the name')
+ expect(page).to have_field("wiki[message]", with: "Create hyphens in the name")
+
+ page.within(".wiki-form") do
+ fill_in(:wiki_content, with: "My awesome wiki!")
- page.within('.wiki-form') do
- fill_in(:wiki_content, with: 'My awesome wiki!')
- click_button('Create page')
+ click_button("Create page")
end
- expect(page).to have_content('Hyphens in the name')
- expect(page).to have_content("Last edited by #{user.name}")
- expect(page).to have_content('My awesome wiki!')
+ expect(page).to have_content("Hyphens in the name")
+ .and have_content("Last edited by #{user.name}")
+ .and have_content("My awesome wiki!")
end
end
- it 'shows the autocompletion dropdown' do
- click_link('New page')
+ it "shows the autocompletion dropdown" do
+ click_link("New page")
- page.within('#modal-new-wiki') do
- fill_in(:new_wiki_path, with: 'test-autocomplete')
- click_button('Create page')
+ page.within("#modal-new-wiki") do
+ fill_in(:new_wiki_path, with: "test-autocomplete")
+
+ click_button("Create page")
end
- page.within('.wiki-form') do
- find('#wiki_content').native.send_keys('')
- fill_in(:wiki_content, with: '@')
+ page.within(".wiki-form") do
+ find("#wiki_content").native.send_keys("")
+
+ fill_in(:wiki_content, with: "@")
end
- expect(page).to have_selector('.atwho-view')
+ expect(page).to have_selector(".atwho-view")
end
end
- context 'in a group namespace' do
+ context "in a group namespace" do
let(:project) { create(:project, namespace: create(:group, :public)) }
- context 'via the "new wiki page" page' do
- it 'creates a page' do
- click_link('New page')
+ context "via the `new wiki page` page" do
+ it "creates a page" do
+ click_link("New page")
- page.within('#modal-new-wiki') do
- fill_in(:new_wiki_path, with: 'foo')
- click_button('Create page')
+ page.within("#modal-new-wiki") do
+ fill_in(:new_wiki_path, with: "foo")
+
+ click_button("Create page")
end
# Commit message field should have correct value.
- expect(page).to have_field('wiki[message]', with: 'Create foo')
+ expect(page).to have_field("wiki[message]", with: "Create foo")
+
+ page.within(".wiki-form") do
+ fill_in(:wiki_content, with: "My awesome wiki!")
- page.within('.wiki-form') do
- fill_in(:wiki_content, with: 'My awesome wiki!')
- click_button('Create page')
+ click_button("Create page")
end
- expect(page).to have_content('Foo')
- expect(page).to have_content("Last edited by #{user.name}")
- expect(page).to have_content('My awesome wiki!')
+ expect(page).to have_content("Foo")
+ .and have_content("Last edited by #{user.name}")
+ .and have_content("My awesome wiki!")
end
end
end
diff --git a/spec/features/users/active_sessions_spec.rb b/spec/features/users/active_sessions_spec.rb
new file mode 100644
index 00000000000..631d7e3bced
--- /dev/null
+++ b/spec/features/users/active_sessions_spec.rb
@@ -0,0 +1,69 @@
+require 'spec_helper'
+
+feature 'Active user sessions', :clean_gitlab_redis_shared_state do
+ scenario 'Successful login adds a new active user login' do
+ now = Time.zone.parse('2018-03-12 09:06')
+ Timecop.freeze(now) do
+ user = create(:user)
+ gitlab_sign_in(user)
+ expect(current_path).to eq root_path
+
+ sessions = ActiveSession.list(user)
+ expect(sessions.count).to eq 1
+
+ # refresh the current page updates the updated_at
+ Timecop.freeze(now + 1.minute) do
+ visit current_path
+
+ sessions = ActiveSession.list(user)
+ expect(sessions.first).to have_attributes(
+ created_at: Time.zone.parse('2018-03-12 09:06'),
+ updated_at: Time.zone.parse('2018-03-12 09:07')
+ )
+ end
+ end
+ end
+
+ scenario 'Successful login cleans up obsolete entries' do
+ user = create(:user)
+
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.sadd("session:lookup:user:gitlab:#{user.id}", '59822c7d9fcdfa03725eff41782ad97d')
+ end
+
+ gitlab_sign_in(user)
+
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.smembers("session:lookup:user:gitlab:#{user.id}")).not_to include '59822c7d9fcdfa03725eff41782ad97d'
+ end
+ end
+
+ scenario 'Sessionless login does not clean up obsolete entries' do
+ user = create(:user)
+ personal_access_token = create(:personal_access_token, user: user)
+
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.sadd("session:lookup:user:gitlab:#{user.id}", '59822c7d9fcdfa03725eff41782ad97d')
+ end
+
+ visit user_path(user, :atom, private_token: personal_access_token.token)
+ expect(page.status_code).to eq 200
+
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.smembers("session:lookup:user:gitlab:#{user.id}")).to include '59822c7d9fcdfa03725eff41782ad97d'
+ end
+ end
+
+ scenario 'Logout deletes the active user login' do
+ user = create(:user)
+ gitlab_sign_in(user)
+ expect(current_path).to eq root_path
+
+ expect(ActiveSession.list(user).count).to eq 1
+
+ gitlab_sign_out
+ expect(current_path).to eq new_user_session_path
+
+ expect(ActiveSession.list(user)).to be_empty
+ end
+end
diff --git a/spec/finders/groups_finder_spec.rb b/spec/finders/groups_finder_spec.rb
index abc470788e1..16c0d418d98 100644
--- a/spec/finders/groups_finder_spec.rb
+++ b/spec/finders/groups_finder_spec.rb
@@ -2,43 +2,71 @@ require 'spec_helper'
describe GroupsFinder do
describe '#execute' do
- let(:user) { create(:user) }
-
- context 'root level groups' do
- let!(:private_group) { create(:group, :private) }
- let!(:internal_group) { create(:group, :internal) }
- let!(:public_group) { create(:group, :public) }
-
- context 'without a user' do
- subject { described_class.new.execute }
-
- it { is_expected.to eq([public_group]) }
+ let(:user) { create(:user) }
+
+ describe 'root level groups' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:user_type, :params, :results) do
+ nil | { all_available: true } | %i(public_group user_public_group)
+ nil | { all_available: false } | %i(public_group user_public_group)
+ nil | {} | %i(public_group user_public_group)
+
+ :regular | { all_available: true } | %i(public_group internal_group user_public_group user_internal_group
+ user_private_group)
+ :regular | { all_available: false } | %i(user_public_group user_internal_group user_private_group)
+ :regular | {} | %i(public_group internal_group user_public_group user_internal_group user_private_group)
+
+ :external | { all_available: true } | %i(public_group user_public_group user_internal_group user_private_group)
+ :external | { all_available: false } | %i(user_public_group user_internal_group user_private_group)
+ :external | {} | %i(public_group user_public_group user_internal_group user_private_group)
+
+ :admin | { all_available: true } | %i(public_group internal_group private_group user_public_group
+ user_internal_group user_private_group)
+ :admin | { all_available: false } | %i(user_public_group user_internal_group user_private_group)
+ :admin | {} | %i(public_group internal_group private_group user_public_group user_internal_group
+ user_private_group)
end
- context 'with a user' do
- subject { described_class.new(user).execute }
-
- context 'normal user' do
- it { is_expected.to contain_exactly(public_group, internal_group) }
- end
-
- context 'external user' do
- let(:user) { create(:user, external: true) }
-
- it { is_expected.to contain_exactly(public_group) }
+ with_them do
+ before do
+ # Fixme: Because of an issue: https://github.com/tomykaira/rspec-parameterized/issues/8#issuecomment-381888428
+ # The groups need to be created here, not with let syntax, and also compared by name and not ids
+
+ @groups = {
+ private_group: create(:group, :private, name: 'private_group'),
+ internal_group: create(:group, :internal, name: 'internal_group'),
+ public_group: create(:group, :public, name: 'public_group'),
+
+ user_private_group: create(:group, :private, name: 'user_private_group'),
+ user_internal_group: create(:group, :internal, name: 'user_internal_group'),
+ user_public_group: create(:group, :public, name: 'user_public_group')
+ }
+
+ if user_type
+ user =
+ case user_type
+ when :regular
+ create(:user)
+ when :external
+ create(:user, external: true)
+ when :admin
+ create(:user, :admin)
+ end
+ @groups.values_at(:user_private_group, :user_internal_group, :user_public_group).each do |group|
+ group.add_developer(user)
+ end
+ end
end
- context 'user is member of the private group' do
- before do
- private_group.add_guest(user)
- end
+ subject { described_class.new(User.last, params).execute.to_a }
- it { is_expected.to contain_exactly(public_group, internal_group, private_group) }
- end
+ it { is_expected.to match_array(@groups.values_at(*results)) }
end
end
context 'subgroups', :nested_groups do
+ let(:user) { create(:user) }
let!(:parent_group) { create(:group, :public) }
let!(:public_subgroup) { create(:group, :public, parent: parent_group) }
let!(:internal_subgroup) { create(:group, :internal, parent: parent_group) }
diff --git a/spec/finders/pipelines_finder_spec.rb b/spec/finders/pipelines_finder_spec.rb
index 2b19cda35b0..d6253b605b9 100644
--- a/spec/finders/pipelines_finder_spec.rb
+++ b/spec/finders/pipelines_finder_spec.rb
@@ -203,5 +203,25 @@ describe PipelinesFinder do
end
end
end
+
+ context 'when sha is specified' do
+ let!(:pipeline) { create(:ci_pipeline, project: project, sha: '97de212e80737a608d939f648d959671fb0a0142') }
+
+ context 'when sha exists' do
+ let(:params) { { sha: '97de212e80737a608d939f648d959671fb0a0142' } }
+
+ it 'returns matched pipelines' do
+ is_expected.to eq([pipeline])
+ end
+ end
+
+ context 'when sha does not exist' do
+ let(:params) { { sha: 'invalid-sha' } }
+
+ it 'returns empty' do
+ is_expected.to be_empty
+ end
+ end
+ end
end
end
diff --git a/spec/fixtures/api/schemas/public_api/v4/notes.json b/spec/fixtures/api/schemas/public_api/v4/notes.json
index 4c4ca3b582f..0f9c221fd6d 100644
--- a/spec/fixtures/api/schemas/public_api/v4/notes.json
+++ b/spec/fixtures/api/schemas/public_api/v4/notes.json
@@ -24,7 +24,10 @@
"system": { "type": "boolean" },
"noteable_id": { "type": "integer" },
"noteable_iid": { "type": "integer" },
- "noteable_type": { "type": "string" }
+ "noteable_type": { "type": "string" },
+ "resolved": { "type": "boolean" },
+ "resolvable": { "type": "boolean" },
+ "resolved_by": { "type": ["string", "null"] }
},
"required": [
"id", "body", "attachment", "author", "created_at", "updated_at",
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 43cb0dfe163..5e454f8b310 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -2,8 +2,6 @@
require 'spec_helper'
describe ApplicationHelper do
- include UploadHelpers
-
describe 'current_controller?' do
it 'returns true when controller matches argument' do
stub_controller_name('foo')
@@ -54,143 +52,6 @@ describe ApplicationHelper do
end
end
- describe 'project_icon' do
- it 'returns an url for the avatar' do
- project = create(:project, :public, avatar: File.open(uploaded_image_temp_path))
-
- expect(helper.project_icon(project.full_path).to_s)
- .to eq "<img data-src=\"#{project.avatar.url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />"
- end
- end
-
- describe 'avatar_icon_for' do
- let!(:user) { create(:user, avatar: File.open(uploaded_image_temp_path), email: 'bar@example.com') }
- let(:email) { 'foo@example.com' }
- let!(:another_user) { create(:user, avatar: File.open(uploaded_image_temp_path), email: email) }
-
- it 'prefers the user to retrieve the avatar_url' do
- expect(helper.avatar_icon_for(user, email).to_s)
- .to eq(user.avatar.url)
- end
-
- it 'falls back to email lookup if no user given' do
- expect(helper.avatar_icon_for(nil, email).to_s)
- .to eq(another_user.avatar.url)
- end
- end
-
- describe 'avatar_icon_for_email' do
- let(:user) { create(:user, avatar: File.open(uploaded_image_temp_path)) }
-
- context 'using an email' do
- context 'when there is a matching user' do
- it 'returns a relative URL for the avatar' do
- expect(helper.avatar_icon_for_email(user.email).to_s)
- .to eq(user.avatar.url)
- end
- end
-
- context 'when no user exists for the email' do
- it 'calls gravatar_icon' do
- expect(helper).to receive(:gravatar_icon).with('foo@example.com', 20, 2)
-
- helper.avatar_icon_for_email('foo@example.com', 20, 2)
- end
- end
-
- context 'without an email passed' do
- it 'calls gravatar_icon' do
- expect(helper).to receive(:gravatar_icon).with(nil, 20, 2)
-
- helper.avatar_icon_for_email(nil, 20, 2)
- end
- end
- end
- end
-
- describe 'avatar_icon_for_user' do
- let(:user) { create(:user, avatar: File.open(uploaded_image_temp_path)) }
-
- context 'with a user object passed' do
- it 'returns a relative URL for the avatar' do
- expect(helper.avatar_icon_for_user(user).to_s)
- .to eq(user.avatar.url)
- end
- end
-
- context 'without a user object passed' do
- it 'calls gravatar_icon' do
- expect(helper).to receive(:gravatar_icon).with(nil, 20, 2)
-
- helper.avatar_icon_for_user(nil, 20, 2)
- end
- end
- end
-
- describe 'gravatar_icon' do
- let(:user_email) { 'user@email.com' }
-
- context 'with Gravatar disabled' do
- before do
- stub_application_setting(gravatar_enabled?: false)
- end
-
- it 'returns a generic avatar' do
- expect(helper.gravatar_icon(user_email)).to match_asset_path('no_avatar.png')
- end
- end
-
- context 'with Gravatar enabled' do
- before do
- stub_application_setting(gravatar_enabled?: true)
- end
-
- it 'returns a generic avatar when email is blank' do
- expect(helper.gravatar_icon('')).to match_asset_path('no_avatar.png')
- end
-
- it 'returns a valid Gravatar URL' do
- stub_config_setting(https: false)
-
- expect(helper.gravatar_icon(user_email))
- .to match('https://www.gravatar.com/avatar/b58c6f14d292556214bd64909bcdb118')
- end
-
- it 'uses HTTPs when configured' do
- stub_config_setting(https: true)
-
- expect(helper.gravatar_icon(user_email))
- .to match('https://secure.gravatar.com')
- end
-
- it 'returns custom gravatar path when gravatar_url is set' do
- stub_gravatar_setting(plain_url: 'http://example.local/?s=%{size}&hash=%{hash}')
-
- expect(gravatar_icon(user_email, 20))
- .to eq('http://example.local/?s=40&hash=b58c6f14d292556214bd64909bcdb118')
- end
-
- it 'accepts a custom size argument' do
- expect(helper.gravatar_icon(user_email, 64)).to include '?s=128'
- end
-
- it 'defaults size to 40@2x when given an invalid size' do
- expect(helper.gravatar_icon(user_email, nil)).to include '?s=80'
- end
-
- it 'accepts a scaling factor' do
- expect(helper.gravatar_icon(user_email, 40, 3)).to include '?s=120'
- end
-
- it 'ignores case and surrounding whitespace' do
- normal = helper.gravatar_icon('foo@example.com')
- upcase = helper.gravatar_icon(' FOO@EXAMPLE.COM ')
-
- expect(normal).to eq upcase
- end
- end
- end
-
describe 'simple_sanitize' do
let(:a_tag) { '<a href="#">Foo</a>' }
diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb
index c94fedd615b..120b23e66ac 100644
--- a/spec/helpers/auth_helper_spec.rb
+++ b/spec/helpers/auth_helper_spec.rb
@@ -18,6 +18,30 @@ describe AuthHelper do
end
end
+ describe "providers_for_base_controller" do
+ it 'returns all enabled providers from devise' do
+ allow(helper).to receive(:auth_providers) { [:twitter, :github] }
+ expect(helper.providers_for_base_controller).to include(*[:twitter, :github])
+ end
+
+ it 'excludes ldap providers' do
+ allow(helper).to receive(:auth_providers) { [:twitter, :ldapmain] }
+ expect(helper.providers_for_base_controller).not_to include(:ldapmain)
+ end
+ end
+
+ describe "form_based_providers" do
+ it 'includes LDAP providers' do
+ allow(helper).to receive(:auth_providers) { [:twitter, :ldapmain] }
+ expect(helper.form_based_providers).to eq %i(ldapmain)
+ end
+
+ it 'includes crowd provider' do
+ allow(helper).to receive(:auth_providers) { [:twitter, :crowd] }
+ expect(helper.form_based_providers).to eq %i(crowd)
+ end
+ end
+
describe 'enabled_button_based_providers' do
before do
allow(helper).to receive(:auth_providers) { [:twitter, :github] }
diff --git a/spec/helpers/avatars_helper_spec.rb b/spec/helpers/avatars_helper_spec.rb
index 04c6d259135..5856bccb5b8 100644
--- a/spec/helpers/avatars_helper_spec.rb
+++ b/spec/helpers/avatars_helper_spec.rb
@@ -1,10 +1,147 @@
require 'rails_helper'
describe AvatarsHelper do
- include ApplicationHelper
+ include UploadHelpers
let(:user) { create(:user) }
+ describe '#project_icon' do
+ it 'returns an url for the avatar' do
+ project = create(:project, :public, avatar: File.open(uploaded_image_temp_path))
+
+ expect(helper.project_icon(project.full_path).to_s)
+ .to eq "<img data-src=\"#{project.avatar.url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />"
+ end
+ end
+
+ describe '#avatar_icon_for' do
+ let!(:user) { create(:user, avatar: File.open(uploaded_image_temp_path), email: 'bar@example.com') }
+ let(:email) { 'foo@example.com' }
+ let!(:another_user) { create(:user, avatar: File.open(uploaded_image_temp_path), email: email) }
+
+ it 'prefers the user to retrieve the avatar_url' do
+ expect(helper.avatar_icon_for(user, email).to_s)
+ .to eq(user.avatar.url)
+ end
+
+ it 'falls back to email lookup if no user given' do
+ expect(helper.avatar_icon_for(nil, email).to_s)
+ .to eq(another_user.avatar.url)
+ end
+ end
+
+ describe '#avatar_icon_for_email' do
+ let(:user) { create(:user, avatar: File.open(uploaded_image_temp_path)) }
+
+ context 'using an email' do
+ context 'when there is a matching user' do
+ it 'returns a relative URL for the avatar' do
+ expect(helper.avatar_icon_for_email(user.email).to_s)
+ .to eq(user.avatar.url)
+ end
+ end
+
+ context 'when no user exists for the email' do
+ it 'calls gravatar_icon' do
+ expect(helper).to receive(:gravatar_icon).with('foo@example.com', 20, 2)
+
+ helper.avatar_icon_for_email('foo@example.com', 20, 2)
+ end
+ end
+
+ context 'without an email passed' do
+ it 'calls gravatar_icon' do
+ expect(helper).to receive(:gravatar_icon).with(nil, 20, 2)
+
+ helper.avatar_icon_for_email(nil, 20, 2)
+ end
+ end
+ end
+ end
+
+ describe '#avatar_icon_for_user' do
+ let(:user) { create(:user, avatar: File.open(uploaded_image_temp_path)) }
+
+ context 'with a user object passed' do
+ it 'returns a relative URL for the avatar' do
+ expect(helper.avatar_icon_for_user(user).to_s)
+ .to eq(user.avatar.url)
+ end
+ end
+
+ context 'without a user object passed' do
+ it 'calls gravatar_icon' do
+ expect(helper).to receive(:gravatar_icon).with(nil, 20, 2)
+
+ helper.avatar_icon_for_user(nil, 20, 2)
+ end
+ end
+ end
+
+ describe '#gravatar_icon' do
+ let(:user_email) { 'user@email.com' }
+
+ context 'with Gravatar disabled' do
+ before do
+ stub_application_setting(gravatar_enabled?: false)
+ end
+
+ it 'returns a generic avatar' do
+ expect(helper.gravatar_icon(user_email)).to match_asset_path('no_avatar.png')
+ end
+ end
+
+ context 'with Gravatar enabled' do
+ before do
+ stub_application_setting(gravatar_enabled?: true)
+ end
+
+ it 'returns a generic avatar when email is blank' do
+ expect(helper.gravatar_icon('')).to match_asset_path('no_avatar.png')
+ end
+
+ it 'returns a valid Gravatar URL' do
+ stub_config_setting(https: false)
+
+ expect(helper.gravatar_icon(user_email))
+ .to match('https://www.gravatar.com/avatar/b58c6f14d292556214bd64909bcdb118')
+ end
+
+ it 'uses HTTPs when configured' do
+ stub_config_setting(https: true)
+
+ expect(helper.gravatar_icon(user_email))
+ .to match('https://secure.gravatar.com')
+ end
+
+ it 'returns custom gravatar path when gravatar_url is set' do
+ stub_gravatar_setting(plain_url: 'http://example.local/?s=%{size}&hash=%{hash}')
+
+ expect(gravatar_icon(user_email, 20))
+ .to eq('http://example.local/?s=40&hash=b58c6f14d292556214bd64909bcdb118')
+ end
+
+ it 'accepts a custom size argument' do
+ expect(helper.gravatar_icon(user_email, 64)).to include '?s=128'
+ end
+
+ it 'defaults size to 40@2x when given an invalid size' do
+ expect(helper.gravatar_icon(user_email, nil)).to include '?s=80'
+ end
+
+ it 'accepts a scaling factor' do
+ expect(helper.gravatar_icon(user_email, 40, 3)).to include '?s=120'
+ end
+
+ it 'ignores case and surrounding whitespace' do
+ normal = helper.gravatar_icon('foo@example.com')
+ upcase = helper.gravatar_icon(' FOO@EXAMPLE.COM ')
+
+ expect(normal).to eq upcase
+ end
+ end
+ end
+
describe '#user_avatar' do
subject { helper.user_avatar(user: user) }
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index 1fa194fe1b8..8de615ad8c2 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -242,4 +242,29 @@ describe BlobHelper do
end
end
end
+
+ describe '#ide_edit_path' do
+ let(:project) { create(:project) }
+
+ around do |example|
+ old_script_name = Rails.application.routes.default_url_options[:script_name]
+ begin
+ example.run
+ ensure
+ Rails.application.routes.default_url_options[:script_name] = old_script_name
+ end
+ end
+
+ it 'returns full IDE path' do
+ Rails.application.routes.default_url_options[:script_name] = nil
+
+ expect(helper.ide_edit_path(project, "master", "")).to eq("/-/ide/project/#{project.namespace.path}/#{project.path}/edit/master/")
+ end
+
+ it 'returns IDE path without relative_url_root' do
+ Rails.application.routes.default_url_options[:script_name] = "/gitlab"
+
+ expect(helper.ide_edit_path(project, "master", "")).to eq("/gitlab/-/ide/project/#{project.namespace.path}/#{project.path}/edit/master/")
+ end
+ end
end
diff --git a/spec/javascripts/ide/components/new_dropdown/index_spec.js b/spec/javascripts/ide/components/new_dropdown/index_spec.js
index e08abe7d849..7b637f37eba 100644
--- a/spec/javascripts/ide/components/new_dropdown/index_spec.js
+++ b/spec/javascripts/ide/components/new_dropdown/index_spec.js
@@ -32,12 +32,8 @@ describe('new dropdown component', () => {
it('renders new file, upload and new directory links', () => {
expect(vm.$el.querySelectorAll('a')[0].textContent.trim()).toBe('New file');
- expect(vm.$el.querySelectorAll('a')[1].textContent.trim()).toBe(
- 'Upload file',
- );
- expect(vm.$el.querySelectorAll('a')[2].textContent.trim()).toBe(
- 'New directory',
- );
+ expect(vm.$el.querySelectorAll('a')[1].textContent.trim()).toBe('Upload file');
+ expect(vm.$el.querySelectorAll('a')[2].textContent.trim()).toBe('New directory');
});
describe('createNewItem', () => {
@@ -81,4 +77,18 @@ describe('new dropdown component', () => {
.catch(done.fail);
});
});
+
+ describe('dropdownOpen', () => {
+ it('scrolls dropdown into view', done => {
+ spyOn(vm.$refs.dropdownMenu, 'scrollIntoView');
+
+ vm.dropdownOpen = true;
+
+ setTimeout(() => {
+ expect(vm.$refs.dropdownMenu.scrollIntoView).toHaveBeenCalled();
+
+ done();
+ });
+ });
+ });
});
diff --git a/spec/javascripts/ide/components/new_dropdown/modal_spec.js b/spec/javascripts/ide/components/new_dropdown/modal_spec.js
index a6e1e5a0d35..f362ed4db65 100644
--- a/spec/javascripts/ide/components/new_dropdown/modal_spec.js
+++ b/spec/javascripts/ide/components/new_dropdown/modal_spec.js
@@ -25,25 +25,17 @@ describe('new file modal component', () => {
it(`sets modal title as ${type}`, () => {
const title = type === 'tree' ? 'directory' : 'file';
- expect(vm.$el.querySelector('.modal-title').textContent.trim()).toBe(
- `Create new ${title}`,
- );
+ expect(vm.$el.querySelector('.modal-title').textContent.trim()).toBe(`Create new ${title}`);
});
it(`sets button label as ${type}`, () => {
const title = type === 'tree' ? 'directory' : 'file';
- expect(vm.$el.querySelector('.btn-success').textContent.trim()).toBe(
- `Create ${title}`,
- );
+ expect(vm.$el.querySelector('.btn-success').textContent.trim()).toBe(`Create ${title}`);
});
it(`sets form label as ${type}`, () => {
- const title = type === 'tree' ? 'Directory' : 'File';
-
- expect(vm.$el.querySelector('.label-light').textContent.trim()).toBe(
- `${title} name`,
- );
+ expect(vm.$el.querySelector('.label-light').textContent.trim()).toBe('Name');
});
describe('createEntryInStore', () => {
diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js
index b6eadf56f9d..a64af5b941b 100644
--- a/spec/javascripts/ide/stores/actions_spec.js
+++ b/spec/javascripts/ide/stores/actions_spec.js
@@ -1,4 +1,9 @@
-import actions, { stageAllChanges, unstageAllChanges, toggleFileFinder } from '~/ide/stores/actions';
+import actions, {
+ stageAllChanges,
+ unstageAllChanges,
+ toggleFileFinder,
+ updateTempFlagForEntry,
+} from '~/ide/stores/actions';
import store from '~/ide/stores';
import * as types from '~/ide/stores/mutation_types';
import router from '~/ide/ide_router';
@@ -340,6 +345,49 @@ describe('Multi-file store actions', () => {
});
});
+ describe('updateTempFlagForEntry', () => {
+ it('commits UPDATE_TEMP_FLAG', done => {
+ const f = {
+ ...file(),
+ path: 'test',
+ tempFile: true,
+ };
+ store.state.entries[f.path] = f;
+
+ testAction(
+ updateTempFlagForEntry,
+ { file: f, tempFile: false },
+ store.state,
+ [{ type: 'UPDATE_TEMP_FLAG', payload: { path: f.path, tempFile: false } }],
+ [],
+ done,
+ );
+ });
+
+ it('commits UPDATE_TEMP_FLAG and dispatches for parent', done => {
+ const parent = {
+ ...file(),
+ path: 'testing',
+ };
+ const f = {
+ ...file(),
+ path: 'test',
+ parentPath: 'testing',
+ };
+ store.state.entries[parent.path] = parent;
+ store.state.entries[f.path] = f;
+
+ testAction(
+ updateTempFlagForEntry,
+ { file: f, tempFile: false },
+ store.state,
+ [{ type: 'UPDATE_TEMP_FLAG', payload: { path: f.path, tempFile: false } }],
+ [{ type: 'updateTempFlagForEntry', payload: { file: parent, tempFile: false } }],
+ done,
+ );
+ });
+ });
+
describe('toggleFileFinder', () => {
it('commits TOGGLE_FILE_FINDER', done => {
testAction(
diff --git a/spec/javascripts/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js
index 575039e755e..997711d1e19 100644
--- a/spec/javascripts/ide/stores/mutations_spec.js
+++ b/spec/javascripts/ide/stores/mutations_spec.js
@@ -87,6 +87,28 @@ describe('Multi-file store mutations', () => {
});
});
+ describe('UPDATE_TEMP_FLAG', () => {
+ beforeEach(() => {
+ localState.entries.test = {
+ ...file(),
+ tempFile: true,
+ changed: true,
+ };
+ });
+
+ it('updates tempFile flag', () => {
+ mutations.UPDATE_TEMP_FLAG(localState, { path: 'test', tempFile: false });
+
+ expect(localState.entries.test.tempFile).toBe(false);
+ });
+
+ it('updates changed flag', () => {
+ mutations.UPDATE_TEMP_FLAG(localState, { path: 'test', tempFile: false });
+
+ expect(localState.entries.test.changed).toBe(false);
+ });
+ });
+
describe('TOGGLE_FILE_FINDER', () => {
it('updates fileFindVisible', () => {
mutations.TOGGLE_FILE_FINDER(localState, true);
diff --git a/spec/javascripts/sidebar/mock_data.js b/spec/javascripts/sidebar/mock_data.js
index 8b6e8b24f00..fcd7bea3f6d 100644
--- a/spec/javascripts/sidebar/mock_data.js
+++ b/spec/javascripts/sidebar/mock_data.js
@@ -138,7 +138,7 @@ const RESPONSE_MAP = {
},
{
id: 20,
- name_with_namespace: 'foo / bar',
+ name_with_namespace: '<img src=x onerror=alert(document.domain)> foo / bar',
},
],
},
diff --git a/spec/javascripts/sidebar/sidebar_move_issue_spec.js b/spec/javascripts/sidebar/sidebar_move_issue_spec.js
index a3fb965fbab..00847df4b60 100644
--- a/spec/javascripts/sidebar/sidebar_move_issue_spec.js
+++ b/spec/javascripts/sidebar/sidebar_move_issue_spec.js
@@ -69,6 +69,15 @@ describe('SidebarMoveIssue', function () {
expect($.fn.glDropdown).toHaveBeenCalled();
});
+
+ it('escapes html from project name', (done) => {
+ this.$toggleButton.dropdown('toggle');
+
+ setTimeout(() => {
+ expect(this.$content.find('.js-move-issue-dropdown-item')[1].innerHTML.trim()).toEqual('&lt;img src=x onerror=alert(document.domain)&gt; foo / bar');
+ done();
+ });
+ });
});
describe('onConfirmClicked', () => {
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index bcd15f5eae2..2411d33a496 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -84,21 +84,11 @@ beforeEach(() => {
const axiosDefaultAdapter = getDefaultAdapter();
-let testFiles = process.env.TEST_FILES || [];
-if (testFiles.length > 0) {
- testFiles = testFiles.map(path => path.replace(/^spec\/javascripts\//, '').replace(/\.js$/, ''));
- console.log(`Running only tests matching: ${testFiles}`);
-} else {
- console.log('Running all tests');
-}
-
// render all of our tests
const testsContext = require.context('.', true, /_spec$/);
testsContext.keys().forEach(function(path) {
try {
- if (testFiles.length === 0 || testFiles.some(p => path.includes(p))) {
- testsContext(path);
- }
+ testsContext(path);
} catch (err) {
console.error('[ERROR] Unable to load spec: ', path);
describe('Test bundle', function() {
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js
index dd1d62cd4ed..a0a74648328 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js
@@ -4,21 +4,37 @@ import eventHub from '~/vue_merge_request_widget/event_hub';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetFailedToMerge', () => {
+ const dummyIntervalId = 1337;
let Component;
let vm;
beforeEach(() => {
Component = Vue.extend(failedToMergeComponent);
spyOn(eventHub, '$emit');
- vm = mountComponent(Component, { mr: {
- mergeError: 'Merge error happened.',
- } });
+ spyOn(window, 'setInterval').and.returnValue(dummyIntervalId);
+ spyOn(window, 'clearInterval').and.stub();
+ vm = mountComponent(Component, {
+ mr: {
+ mergeError: 'Merge error happened.',
+ },
+ });
});
afterEach(() => {
vm.$destroy();
});
+ it('sets interval to refresh', () => {
+ expect(window.setInterval).toHaveBeenCalledWith(vm.updateTimer, 1000);
+ expect(vm.intervalId).toBe(dummyIntervalId);
+ });
+
+ it('clears interval when destroying ', () => {
+ vm.$destroy();
+
+ expect(window.clearInterval).toHaveBeenCalledWith(dummyIntervalId);
+ });
+
describe('computed', () => {
describe('timerText', () => {
it('should return correct timer text', () => {
@@ -65,11 +81,13 @@ describe('MRWidgetFailedToMerge', () => {
});
describe('while it is refreshing', () => {
- it('renders Refresing now', (done) => {
+ it('renders Refresing now', done => {
vm.isRefreshing = true;
Vue.nextTick(() => {
- expect(vm.$el.querySelector('.js-refresh-label').textContent.trim()).toEqual('Refreshing now');
+ expect(vm.$el.querySelector('.js-refresh-label').textContent.trim()).toEqual(
+ 'Refreshing now',
+ );
done();
});
});
@@ -78,11 +96,15 @@ describe('MRWidgetFailedToMerge', () => {
describe('while it is not regresing', () => {
it('renders warning icon and disabled merge button', () => {
expect(vm.$el.querySelector('.js-ci-status-icon-warning')).not.toBeNull();
- expect(vm.$el.querySelector('.js-disabled-merge-button').getAttribute('disabled')).toEqual('disabled');
+ expect(vm.$el.querySelector('.js-disabled-merge-button').getAttribute('disabled')).toEqual(
+ 'disabled',
+ );
});
it('renders given error', () => {
- expect(vm.$el.querySelector('.has-error-message').textContent.trim()).toEqual('Merge error happened..');
+ expect(vm.$el.querySelector('.has-error-message').textContent.trim()).toEqual(
+ 'Merge error happened..',
+ );
});
it('renders refresh button', () => {
@@ -90,13 +112,13 @@ describe('MRWidgetFailedToMerge', () => {
});
it('renders remaining time', () => {
- expect(
- vm.$el.querySelector('.has-custom-error').textContent.trim(),
- ).toEqual('Refreshing in 10 seconds to show the updated status...');
+ expect(vm.$el.querySelector('.has-custom-error').textContent.trim()).toEqual(
+ 'Refreshing in 10 seconds to show the updated status...',
+ );
});
});
- it('should just generic merge failed message if merge_error is not available', (done) => {
+ it('should just generic merge failed message if merge_error is not available', done => {
vm.mr.mergeError = null;
Vue.nextTick(() => {
@@ -106,7 +128,7 @@ describe('MRWidgetFailedToMerge', () => {
});
});
- it('should show refresh label when refresh requested', (done) => {
+ it('should show refresh label when refresh requested', done => {
vm.refresh();
Vue.nextTick(() => {
expect(vm.$el.innerText).not.toContain('Merge failed. Refreshing');
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js
index 98ab61a0367..cea603368bf 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
-import wipComponent from '~/vue_merge_request_widget/components/states/mr_widget_wip';
+import WorkInProgress from '~/vue_merge_request_widget/components/states/work_in_progress.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
const createComponent = () => {
- const Component = Vue.extend(wipComponent);
+ const Component = Vue.extend(WorkInProgress);
const mr = {
title: 'The best MR ever',
removeWIPPath: '/path/to/remove/wip',
@@ -17,10 +17,10 @@ const createComponent = () => {
});
};
-describe('MRWidgetWIP', () => {
+describe('Wip', () => {
describe('props', () => {
it('should have props', () => {
- const { mr, service } = wipComponent.props;
+ const { mr, service } = WorkInProgress.props;
expect(mr.type instanceof Object).toBeTruthy();
expect(mr.required).toBeTruthy();
diff --git a/spec/lib/banzai/filter/commit_trailers_filter_spec.rb b/spec/lib/banzai/filter/commit_trailers_filter_spec.rb
index 1fd145116df..068cdc85a07 100644
--- a/spec/lib/banzai/filter/commit_trailers_filter_spec.rb
+++ b/spec/lib/banzai/filter/commit_trailers_filter_spec.rb
@@ -47,16 +47,36 @@ describe Banzai::Filter::CommitTrailersFilter do
)
end
- it 'non GitLab users and replaces them with mailto links' do
- _, message_html = build_commit_message(
- trailer: trailer,
- name: FFaker::Name.name,
- email: email
- )
+ context 'non GitLab users' do
+ shared_examples 'mailto links' do
+ it 'replaces them with mailto links' do
+ _, message_html = build_commit_message(
+ trailer: trailer,
+ name: FFaker::Name.name,
+ email: email
+ )
- doc = filter(message_html)
+ doc = filter(message_html)
- expect_to_have_mailto_link(doc, email: email, trailer: trailer)
+ expect_to_have_mailto_link_with_avatar(doc, email: email, trailer: trailer)
+ end
+ end
+
+ context 'when Gravatar is disabled' do
+ before do
+ stub_application_setting(gravatar_enabled: false)
+ end
+
+ it_behaves_like 'mailto links'
+ end
+
+ context 'when Gravatar is enabled' do
+ before do
+ stub_application_setting(gravatar_enabled: true)
+ end
+
+ it_behaves_like 'mailto links'
+ end
end
it 'multiple trailers in the same message' do
@@ -69,7 +89,7 @@ describe Banzai::Filter::CommitTrailersFilter do
doc = filter(message)
expect_to_have_user_link_with_avatar(doc, user: user, trailer: trailer)
- expect_to_have_mailto_link(doc, email: email, trailer: different_trailer)
+ expect_to_have_mailto_link_with_avatar(doc, email: email, trailer: different_trailer)
end
context 'special names' do
@@ -90,7 +110,7 @@ describe Banzai::Filter::CommitTrailersFilter do
doc = filter(message_html)
- expect_to_have_mailto_link(doc, email: email, trailer: trailer)
+ expect_to_have_mailto_link_with_avatar(doc, email: email, trailer: trailer)
expect(doc.text).to match Regexp.escape(message)
end
end
diff --git a/spec/lib/gitlab/background_migration/migrate_stage_index_spec.rb b/spec/lib/gitlab/background_migration/migrate_stage_index_spec.rb
new file mode 100644
index 00000000000..f8107dd40b9
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/migrate_stage_index_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::MigrateStageIndex, :migration, schema: 20180420080616 do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:pipelines) { table(:ci_pipelines) }
+ let(:stages) { table(:ci_stages) }
+ let(:jobs) { table(:ci_builds) }
+
+ before do
+ namespaces.create(id: 10, name: 'gitlab-org', path: 'gitlab-org')
+ projects.create!(id: 11, namespace_id: 10, name: 'gitlab', path: 'gitlab')
+ pipelines.create!(id: 12, project_id: 11, ref: 'master', sha: 'adf43c3a')
+
+ stages.create(id: 100, project_id: 11, pipeline_id: 12, name: 'build')
+ stages.create(id: 101, project_id: 11, pipeline_id: 12, name: 'test')
+
+ jobs.create!(id: 121, commit_id: 12, project_id: 11,
+ stage_idx: 2, stage_id: 100)
+ jobs.create!(id: 122, commit_id: 12, project_id: 11,
+ stage_idx: 2, stage_id: 100)
+ jobs.create!(id: 123, commit_id: 12, project_id: 11,
+ stage_idx: 10, stage_id: 100)
+ jobs.create!(id: 124, commit_id: 12, project_id: 11,
+ stage_idx: 3, stage_id: 101)
+ end
+
+ it 'correctly migrates stages indices' do
+ expect(stages.all.pluck(:position)).to all(be_nil)
+
+ described_class.new.perform(100, 101)
+
+ expect(stages.all.pluck(:position)).to eq [2, 3]
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb
index dc12ba076bc..0edc3f315bb 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb
@@ -17,7 +17,7 @@ describe Gitlab::Ci::Pipeline::Chain::Create do
context 'when pipeline is ready to be saved' do
before do
- pipeline.stages.build(name: 'test', project: project)
+ pipeline.stages.build(name: 'test', position: 0, project: project)
step.perform!
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb
index 8312fa47cfa..4d7d6951a51 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb
@@ -35,11 +35,6 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
it 'populates pipeline with stages' do
expect(pipeline.stages).to be_one
expect(pipeline.stages.first).not_to be_persisted
- end
-
- it 'populates pipeline with builds' do
- expect(pipeline.builds).to be_one
- expect(pipeline.builds.first).not_to be_persisted
expect(pipeline.stages.first.builds).to be_one
expect(pipeline.stages.first.builds.first).not_to be_persisted
end
@@ -151,8 +146,8 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
step.perform!
expect(pipeline.stages.size).to eq 1
- expect(pipeline.builds.size).to eq 1
- expect(pipeline.builds.first.name).to eq 'rspec'
+ expect(pipeline.stages.first.builds.size).to eq 1
+ expect(pipeline.stages.first.builds.first.name).to eq 'rspec'
end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb
index eb1b285c7bd..05ce3412fd8 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb
@@ -24,7 +24,8 @@ describe Gitlab::Ci::Pipeline::Seed::Stage do
describe '#attributes' do
it 'returns hash attributes of a stage' do
expect(subject.attributes).to be_a Hash
- expect(subject.attributes).to include(:name, :project)
+ expect(subject.attributes)
+ .to include(:name, :position, :pipeline, :project)
end
end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index da1a6229ccf..9924641f829 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -234,59 +234,72 @@ describe Gitlab::Git::Repository, seed_helper: true do
it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RefService, :tag_names
end
- shared_examples 'archive check' do |extenstion|
- 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
+ describe '#archive_metadata' do
+ let(:storage_path) { '/tmp' }
+ let(:cache_key) { File.join(repository.gl_repository, SeedRepo::LastCommit::ID) }
- describe '#archive_prefix' do
- let(:project_name) { 'project-name'}
+ let(:append_sha) { true }
+ let(:ref) { 'master' }
+ let(:format) { nil }
- before do
- expect(repository).to receive(:name).once.and_return(project_name)
- end
+ let(:expected_extension) { 'tar.gz' }
+ let(:expected_filename) { "#{expected_prefix}.#{expected_extension}" }
+ let(:expected_path) { File.join(storage_path, cache_key, expected_filename) }
+ let(:expected_prefix) { "gitlab-git-test-#{ref}-#{SeedRepo::LastCommit::ID}" }
- it 'returns parameterised string for a ref containing slashes' do
- prefix = repository.archive_prefix('test/branch', 'SHA', append_sha: nil)
+ subject(:metadata) { repository.archive_metadata(ref, storage_path, format, append_sha: append_sha) }
- expect(prefix).to eq("#{project_name}-test-branch-SHA")
+ it 'sets RepoPath to the repository path' do
+ expect(metadata['RepoPath']).to eq(repository.path)
end
- it 'returns correct string for a ref containing dots' do
- prefix = repository.archive_prefix('test.branch', 'SHA', append_sha: nil)
-
- expect(prefix).to eq("#{project_name}-test.branch-SHA")
+ it 'sets CommitId to the commit SHA' do
+ expect(metadata['CommitId']).to eq(SeedRepo::LastCommit::ID)
end
- it 'returns string with sha when append_sha is false' do
- prefix = repository.archive_prefix('test.branch', 'SHA', append_sha: false)
-
- expect(prefix).to eq("#{project_name}-test.branch")
+ it 'sets ArchivePrefix to the expected prefix' do
+ expect(metadata['ArchivePrefix']).to eq(expected_prefix)
end
- end
- describe '#archive' do
- let(:metadata) { repository.archive_metadata('master', '/tmp', append_sha: true) }
+ it 'sets ArchivePath to the expected globally-unique path' do
+ # This is really important from a security perspective. Think carefully
+ # before changing it: https://gitlab.com/gitlab-org/gitlab-ce/issues/45689
+ expect(expected_path).to include(File.join(repository.gl_repository, SeedRepo::LastCommit::ID))
- it_should_behave_like 'archive check', '.tar.gz'
- end
-
- describe '#archive_zip' do
- let(:metadata) { repository.archive_metadata('master', '/tmp', 'zip', append_sha: true) }
+ expect(metadata['ArchivePath']).to eq(expected_path)
+ end
- it_should_behave_like 'archive check', '.zip'
- end
+ context 'append_sha varies archive path and filename' do
+ where(:append_sha, :ref, :expected_prefix) do
+ sha = SeedRepo::LastCommit::ID
- describe '#archive_bz2' do
- let(:metadata) { repository.archive_metadata('master', '/tmp', 'tbz2', append_sha: true) }
+ true | 'master' | "gitlab-git-test-master-#{sha}"
+ true | sha | "gitlab-git-test-#{sha}-#{sha}"
+ false | 'master' | "gitlab-git-test-master"
+ false | sha | "gitlab-git-test-#{sha}"
+ nil | 'master' | "gitlab-git-test-master-#{sha}"
+ nil | sha | "gitlab-git-test-#{sha}"
+ end
- it_should_behave_like 'archive check', '.tar.bz2'
- end
+ with_them do
+ it { expect(metadata['ArchivePrefix']).to eq(expected_prefix) }
+ it { expect(metadata['ArchivePath']).to eq(expected_path) }
+ end
+ end
- describe '#archive_fallback' do
- let(:metadata) { repository.archive_metadata('master', '/tmp', 'madeup', append_sha: true) }
+ context 'format varies archive path and filename' do
+ where(:format, :expected_extension) do
+ nil | 'tar.gz'
+ 'madeup' | 'tar.gz'
+ 'tbz2' | 'tar.bz2'
+ 'zip' | 'zip'
+ end
- it_should_behave_like 'archive check', '.tar.gz'
+ with_them do
+ it { expect(metadata['ArchivePrefix']).to eq(expected_prefix) }
+ it { expect(metadata['ArchivePath']).to eq(expected_path) }
+ end
+ end
end
describe '#size' do
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 31141807cb2..62da967cf96 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -232,6 +232,7 @@ Ci::Stage:
- id
- name
- status
+- position
- lock_version
- project_id
- pipeline_id
diff --git a/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb
index 3cfdae794f6..7be8be54d5e 100644
--- a/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb
@@ -4,22 +4,10 @@ describe Gitlab::Kubernetes::Helm::BaseCommand do
let(:application) { create(:clusters_applications_helm) }
let(:base_command) { described_class.new(application.name) }
- describe '#generate_script' do
- let(:helm_version) { Gitlab::Kubernetes::Helm::HELM_VERSION }
- let(:command) do
- <<~HEREDOC
- set -eo pipefail
- apk add -U ca-certificates openssl >/dev/null
- wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v#{helm_version}-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
- mv /tmp/linux-amd64/helm /usr/bin/
- HEREDOC
- end
-
- subject { base_command.generate_script }
+ subject { base_command }
- it 'should return a command that prepares the environment for helm-cli' do
- expect(subject).to eq(command)
- end
+ it_behaves_like 'helm commands' do
+ let(:commands) { '' }
end
describe '#pod_resource' do
diff --git a/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb
index e6920b0a76f..89e36a298f8 100644
--- a/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb
@@ -2,23 +2,9 @@ require 'spec_helper'
describe Gitlab::Kubernetes::Helm::InitCommand do
let(:application) { create(:clusters_applications_helm) }
- let(:init_command) { described_class.new(application.name) }
+ let(:commands) { 'helm init >/dev/null' }
- describe '#generate_script' do
- let(:command) do
- <<~MSG.chomp
- set -eo pipefail
- apk add -U ca-certificates openssl >/dev/null
- wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
- mv /tmp/linux-amd64/helm /usr/bin/
- helm init >/dev/null
- MSG
- end
+ subject { described_class.new(application.name) }
- subject { init_command.generate_script }
-
- it 'should return the appropriate command' do
- is_expected.to eq(command)
- end
- end
+ it_behaves_like 'helm commands'
end
diff --git a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb
index 137b8f718de..547f3f1752c 100644
--- a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb
@@ -12,50 +12,36 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
)
end
- describe '#generate_script' do
- let(:command) do
- <<~MSG
- set -eo pipefail
- apk add -U ca-certificates openssl >/dev/null
- wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
- mv /tmp/linux-amd64/helm /usr/bin/
- helm init --client-only >/dev/null
- helm install #{application.chart} --name #{application.name} --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null
- MSG
- end
-
- subject { install_command.generate_script }
+ subject { install_command }
- it 'should return appropriate command' do
- is_expected.to eq(command)
+ it_behaves_like 'helm commands' do
+ let(:commands) do
+ <<~EOS
+ helm init --client-only >/dev/null
+ helm install #{application.chart} --name #{application.name} --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null
+ EOS
end
+ end
- context 'with an application with a repository' do
- let(:ci_runner) { create(:ci_runner) }
- let(:application) { create(:clusters_applications_runner, runner: ci_runner) }
- let(:install_command) do
- described_class.new(
- application.name,
- chart: application.chart,
- values: application.values,
- repository: application.repository
- )
- end
-
- let(:command) do
- <<~MSG
- set -eo pipefail
- apk add -U ca-certificates openssl >/dev/null
- wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
- mv /tmp/linux-amd64/helm /usr/bin/
- helm init --client-only >/dev/null
- helm repo add #{application.name} #{application.repository}
- helm install #{application.chart} --name #{application.name} --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null
- MSG
- end
+ context 'with an application with a repository' do
+ let(:ci_runner) { create(:ci_runner) }
+ let(:application) { create(:clusters_applications_runner, runner: ci_runner) }
+ let(:install_command) do
+ described_class.new(
+ application.name,
+ chart: application.chart,
+ values: application.values,
+ repository: application.repository
+ )
+ end
- it 'should return appropriate command' do
- is_expected.to eq(command)
+ it_behaves_like 'helm commands' do
+ let(:commands) do
+ <<~EOS
+ helm init --client-only >/dev/null
+ helm repo add #{application.name} #{application.repository}
+ helm install #{application.chart} --name #{application.name} --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null
+ EOS
end
end
end
diff --git a/spec/lib/gitlab/pages_client_spec.rb b/spec/lib/gitlab/pages_client_spec.rb
new file mode 100644
index 00000000000..da6d26f4aee
--- /dev/null
+++ b/spec/lib/gitlab/pages_client_spec.rb
@@ -0,0 +1,172 @@
+require 'spec_helper'
+
+describe Gitlab::PagesClient do
+ subject { described_class }
+
+ describe '.token' do
+ it 'returns the token as it is on disk' do
+ pending 'add omnibus support for generating the secret file https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests/2466'
+ expect(subject.token).to eq(File.read('.gitlab_pages_secret'))
+ end
+ end
+
+ describe '.read_or_create_token' do
+ subject { described_class.read_or_create_token }
+ let(:token_path) { 'tmp/tests/gitlab-pages-secret' }
+ before do
+ allow(described_class).to receive(:token_path).and_return(token_path)
+ FileUtils.rm_f(token_path)
+ end
+
+ it 'uses the existing token file if it exists' do
+ secret = 'existing secret'
+ File.write(token_path, secret)
+
+ subject
+ expect(described_class.token).to eq(secret)
+ end
+
+ it 'creates one if none exists' do
+ pending 'add omnibus support for generating the secret file https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests/2466'
+
+ old_token = described_class.token
+ # sanity check
+ expect(File.exist?(token_path)).to eq(false)
+
+ subject
+ expect(described_class.token.bytesize).to eq(64)
+ expect(described_class.token).not_to eq(old_token)
+ end
+ end
+
+ describe '.write_token' do
+ let(:token_path) { 'tmp/tests/gitlab-pages-secret' }
+ before do
+ allow(described_class).to receive(:token_path).and_return(token_path)
+ FileUtils.rm_f(token_path)
+ end
+
+ it 'writes the secret' do
+ new_secret = 'hello new secret'
+ expect(File.exist?(token_path)).to eq(false)
+
+ described_class.send(:write_token, new_secret)
+
+ expect(File.read(token_path)).to eq(new_secret)
+ end
+
+ it 'does nothing if the file already exists' do
+ existing_secret = 'hello secret'
+ File.write(token_path, existing_secret)
+
+ described_class.send(:write_token, 'new secret')
+
+ expect(File.read(token_path)).to eq(existing_secret)
+ end
+ end
+
+ describe '.load_certificate' do
+ subject { described_class.load_certificate }
+ before do
+ allow(described_class).to receive(:config).and_return(config)
+ end
+
+ context 'with no certificate in the config' do
+ let(:config) { double(:config, certificate: '') }
+
+ it 'does not set @certificate' do
+ subject
+
+ expect(described_class.certificate).to be_nil
+ end
+ end
+
+ context 'with a certificate path in the config' do
+ let(:certificate_path) { 'tmp/tests/fake-certificate' }
+ let(:config) { double(:config, certificate: certificate_path) }
+
+ it 'sets @certificate' do
+ certificate_data = "--- BEGIN CERTIFICATE ---\nbla\n--- END CERTIFICATE ---\n"
+ File.write(certificate_path, certificate_data)
+ subject
+
+ expect(described_class.certificate).to eq(certificate_data)
+ end
+ end
+ end
+
+ describe '.request_kwargs' do
+ let(:token) { 'secret token' }
+ let(:auth_header) { 'Bearer c2VjcmV0IHRva2Vu' }
+ before do
+ allow(described_class).to receive(:token).and_return(token)
+ end
+
+ context 'without timeout' do
+ it { expect(subject.send(:request_kwargs, nil)[:metadata]['authorization']).to eq(auth_header) }
+ end
+
+ context 'with timeout' do
+ let(:timeout) { 1.second }
+
+ it 'still sets the authorization header' do
+ expect(subject.send(:request_kwargs, timeout)[:metadata]['authorization']).to eq(auth_header)
+ end
+
+ it 'sets a deadline value' do
+ now = Time.now
+ deadline = subject.send(:request_kwargs, timeout)[:deadline]
+
+ expect(deadline).to be_between(now, now + 2 * timeout)
+ end
+ end
+ end
+
+ describe '.stub' do
+ before do
+ allow(described_class).to receive(:address).and_return('unix:/foo/bar')
+ end
+
+ it { expect(subject.send(:stub, :health_check)).to be_a(Grpc::Health::V1::Health::Stub) }
+ end
+
+ describe '.address' do
+ subject { described_class.send(:address) }
+
+ before do
+ allow(described_class).to receive(:config).and_return(config)
+ end
+
+ context 'with a unix: address' do
+ let(:config) { double(:config, address: 'unix:/foo/bar') }
+
+ it { expect(subject).to eq('unix:/foo/bar') }
+ end
+
+ context 'with a tcp:// address' do
+ let(:config) { double(:config, address: 'tcp://localhost:1234') }
+
+ it { expect(subject).to eq('localhost:1234') }
+ end
+ end
+
+ describe '.grpc_creds' do
+ subject { described_class.send(:grpc_creds) }
+
+ before do
+ allow(described_class).to receive(:config).and_return(config)
+ end
+
+ context 'with a unix: address' do
+ let(:config) { double(:config, address: 'unix:/foo/bar') }
+
+ it { expect(subject).to eq(:this_channel_is_insecure) }
+ end
+
+ context 'with a tcp:// address' do
+ let(:config) { double(:config, address: 'tcp://localhost:1234') }
+
+ it { expect(subject).to be_a(GRPC::Core::ChannelCredentials) }
+ end
+ end
+end
diff --git a/spec/lib/omni_auth/strategies/jwt_spec.rb b/spec/lib/omni_auth/strategies/jwt_spec.rb
new file mode 100644
index 00000000000..23485fbcb18
--- /dev/null
+++ b/spec/lib/omni_auth/strategies/jwt_spec.rb
@@ -0,0 +1,87 @@
+require 'spec_helper'
+
+describe OmniAuth::Strategies::Jwt do
+ include Rack::Test::Methods
+ include DeviseHelpers
+
+ context '.decoded' do
+ let(:strategy) { described_class.new({}) }
+ let(:timestamp) { Time.now.to_i }
+ let(:jwt_config) { Devise.omniauth_configs[:jwt] }
+ let(:key) { JWT.encode(claims, jwt_config.strategy.secret) }
+
+ let(:claims) do
+ {
+ id: 123,
+ name: "user_example",
+ email: "user@example.com",
+ iat: timestamp
+ }
+ end
+
+ before do
+ allow_any_instance_of(OmniAuth::Strategy).to receive(:options).and_return(jwt_config.strategy)
+ allow_any_instance_of(Rack::Request).to receive(:params).and_return({ 'jwt' => key })
+ end
+
+ it 'decodes the user information' do
+ result = strategy.decoded
+
+ expect(result["id"]).to eq(123)
+ expect(result["name"]).to eq("user_example")
+ expect(result["email"]).to eq("user@example.com")
+ expect(result["iat"]).to eq(timestamp)
+ end
+
+ context 'required claims is missing' do
+ let(:claims) do
+ {
+ id: 123,
+ email: "user@example.com",
+ iat: timestamp
+ }
+ end
+
+ it 'raises error' do
+ expect { strategy.decoded }.to raise_error(OmniAuth::Strategies::JWT::ClaimInvalid)
+ end
+ end
+
+ context 'when valid_within is specified but iat attribute is missing in response' do
+ let(:claims) do
+ {
+ id: 123,
+ name: "user_example",
+ email: "user@example.com"
+ }
+ end
+
+ before do
+ jwt_config.strategy.valid_within = Time.now.to_i
+ end
+
+ it 'raises error' do
+ expect { strategy.decoded }.to raise_error(OmniAuth::Strategies::JWT::ClaimInvalid)
+ end
+ end
+
+ context 'when timestamp claim is too skewed from present' do
+ let(:claims) do
+ {
+ id: 123,
+ name: "user_example",
+ email: "user@example.com",
+ iat: timestamp - 10.minutes.to_i
+ }
+ end
+
+ before do
+ jwt_config.strategy.valid_within = 2.seconds
+ end
+
+ it 'raises error' do
+ expect { strategy.decoded }.to raise_error(OmniAuth::Strategies::JWT::ClaimInvalid)
+ end
+ end
+ end
+end
diff --git a/spec/migrations/schedule_stages_index_migration_spec.rb b/spec/migrations/schedule_stages_index_migration_spec.rb
new file mode 100644
index 00000000000..710264da375
--- /dev/null
+++ b/spec/migrations/schedule_stages_index_migration_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20180420080616_schedule_stages_index_migration')
+
+describe ScheduleStagesIndexMigration, :sidekiq, :migration do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:pipelines) { table(:ci_pipelines) }
+ let(:stages) { table(:ci_stages) }
+
+ before do
+ stub_const("#{described_class}::BATCH_SIZE", 1)
+
+ namespaces.create(id: 12, name: 'gitlab-org', path: 'gitlab-org')
+ projects.create!(id: 123, namespace_id: 12, name: 'gitlab', path: 'gitlab')
+ pipelines.create!(id: 1, project_id: 123, ref: 'master', sha: 'adf43c3a')
+ stages.create!(id: 121, project_id: 123, pipeline_id: 1, name: 'build')
+ stages.create!(id: 122, project_id: 123, pipeline_id: 1, name: 'test')
+ stages.create!(id: 123, project_id: 123, pipeline_id: 1, name: 'deploy')
+ end
+
+ it 'schedules delayed background migrations in batches' do
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ expect(stages.all).to all(have_attributes(position: be_nil))
+
+ migrate!
+
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, 121, 121)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(10.minutes, 122, 122)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(15.minutes, 123, 123)
+ expect(BackgroundMigrationWorker.jobs.size).to eq 3
+ end
+ end
+ end
+end
diff --git a/spec/models/active_session_spec.rb b/spec/models/active_session_spec.rb
new file mode 100644
index 00000000000..129b2f92683
--- /dev/null
+++ b/spec/models/active_session_spec.rb
@@ -0,0 +1,216 @@
+require 'rails_helper'
+
+RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
+ let(:user) do
+ create(:user).tap do |user|
+ user.current_sign_in_at = Time.current
+ end
+ end
+
+ let(:session) { double(:session, id: '6919a6f1bb119dd7396fadc38fd18d0d') }
+
+ let(:request) do
+ double(:request, {
+ user_agent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_1_3 like Mac OS X) AppleWebKit/600.1.4 ' \
+ '(KHTML, like Gecko) Mobile/12B466 [FBDV/iPhone7,2]',
+ ip: '127.0.0.1',
+ session: session
+ })
+ end
+
+ describe '#current?' do
+ it 'returns true if the active session matches the current session' do
+ active_session = ActiveSession.new(session_id: '6919a6f1bb119dd7396fadc38fd18d0d')
+
+ expect(active_session.current?(session)).to be true
+ end
+
+ it 'returns false if the active session does not match the current session' do
+ active_session = ActiveSession.new(session_id: '59822c7d9fcdfa03725eff41782ad97d')
+
+ expect(active_session.current?(session)).to be false
+ end
+
+ it 'returns false if the session id is nil' do
+ active_session = ActiveSession.new(session_id: nil)
+ session = double(:session, id: nil)
+
+ expect(active_session.current?(session)).to be false
+ end
+ end
+
+ describe '.list' do
+ it 'returns all sessions by user' do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", Marshal.dump({ session_id: 'a' }))
+ redis.set("session:user:gitlab:#{user.id}:59822c7d9fcdfa03725eff41782ad97d", Marshal.dump({ session_id: 'b' }))
+ redis.set("session:user:gitlab:9999:5c8611e4f9c69645ad1a1492f4131358", '')
+
+ redis.sadd(
+ "session:lookup:user:gitlab:#{user.id}",
+ %w[
+ 6919a6f1bb119dd7396fadc38fd18d0d
+ 59822c7d9fcdfa03725eff41782ad97d
+ ]
+ )
+ end
+
+ expect(ActiveSession.list(user)).to match_array [{ session_id: 'a' }, { session_id: 'b' }]
+ end
+
+ it 'does not return obsolete entries and cleans them up' do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", Marshal.dump({ session_id: 'a' }))
+
+ redis.sadd(
+ "session:lookup:user:gitlab:#{user.id}",
+ %w[
+ 6919a6f1bb119dd7396fadc38fd18d0d
+ 59822c7d9fcdfa03725eff41782ad97d
+ ]
+ )
+ end
+
+ expect(ActiveSession.list(user)).to eq [{ session_id: 'a' }]
+
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.sscan_each("session:lookup:user:gitlab:#{user.id}").to_a).to eq ['6919a6f1bb119dd7396fadc38fd18d0d']
+ end
+ end
+
+ it 'returns an empty array if the use does not have any active session' do
+ expect(ActiveSession.list(user)).to eq []
+ end
+ end
+
+ describe '.set' do
+ it 'sets a new redis entry for the user session and a lookup entry' do
+ ActiveSession.set(user, request)
+
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.scan_each.to_a).to match_array [
+ "session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d",
+ "session:lookup:user:gitlab:#{user.id}"
+ ]
+ end
+ end
+
+ it 'adds timestamps and information from the request' do
+ Timecop.freeze(Time.zone.parse('2018-03-12 09:06')) do
+ ActiveSession.set(user, request)
+
+ session = ActiveSession.list(user)
+
+ expect(session.count).to eq 1
+ expect(session.first).to have_attributes(
+ ip_address: '127.0.0.1',
+ browser: 'Mobile Safari',
+ os: 'iOS',
+ device_name: 'iPhone 6',
+ device_type: 'smartphone',
+ created_at: Time.zone.parse('2018-03-12 09:06'),
+ updated_at: Time.zone.parse('2018-03-12 09:06'),
+ session_id: '6919a6f1bb119dd7396fadc38fd18d0d'
+ )
+ end
+ end
+
+ it 'keeps the created_at from the login on consecutive requests' do
+ now = Time.zone.parse('2018-03-12 09:06')
+
+ Timecop.freeze(now) do
+ ActiveSession.set(user, request)
+
+ Timecop.freeze(now + 1.minute) do
+ ActiveSession.set(user, request)
+
+ session = ActiveSession.list(user)
+
+ expect(session.first).to have_attributes(
+ created_at: Time.zone.parse('2018-03-12 09:06'),
+ updated_at: Time.zone.parse('2018-03-12 09:07')
+ )
+ end
+ end
+ end
+ end
+
+ describe '.destroy' do
+ it 'removes the entry associated with the currently killed user session' do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", '')
+ redis.set("session:user:gitlab:#{user.id}:59822c7d9fcdfa03725eff41782ad97d", '')
+ redis.set("session:user:gitlab:9999:5c8611e4f9c69645ad1a1492f4131358", '')
+ end
+
+ ActiveSession.destroy(user, request.session.id)
+
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.scan_each(match: "session:user:gitlab:*")).to match_array [
+ "session:user:gitlab:#{user.id}:59822c7d9fcdfa03725eff41782ad97d",
+ "session:user:gitlab:9999:5c8611e4f9c69645ad1a1492f4131358"
+ ]
+ end
+ end
+
+ it 'removes the lookup entry' do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", '')
+ redis.sadd("session:lookup:user:gitlab:#{user.id}", '6919a6f1bb119dd7396fadc38fd18d0d')
+ end
+
+ ActiveSession.destroy(user, request.session.id)
+
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.scan_each(match: "session:lookup:user:gitlab:#{user.id}").to_a).to be_empty
+ end
+ end
+
+ it 'removes the devise session' do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", '')
+ redis.set("session:gitlab:6919a6f1bb119dd7396fadc38fd18d0d", '')
+ end
+
+ ActiveSession.destroy(user, request.session.id)
+
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.scan_each(match: "session:gitlab:*").to_a).to be_empty
+ end
+ end
+
+ it 'does not remove the devise session if the active session could not be found' do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set("session:gitlab:6919a6f1bb119dd7396fadc38fd18d0d", '')
+ end
+
+ other_user = create(:user)
+
+ ActiveSession.destroy(other_user, request.session.id)
+
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.scan_each(match: "session:gitlab:*").to_a).not_to be_empty
+ end
+ end
+ end
+
+ describe '.cleanup' do
+ it 'removes obsolete lookup entries' do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", '')
+ redis.sadd("session:lookup:user:gitlab:#{user.id}", '6919a6f1bb119dd7396fadc38fd18d0d')
+ redis.sadd("session:lookup:user:gitlab:#{user.id}", '59822c7d9fcdfa03725eff41782ad97d')
+ end
+
+ ActiveSession.cleanup(user)
+
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.smembers("session:lookup:user:gitlab:#{user.id}")).to eq ['6919a6f1bb119dd7396fadc38fd18d0d']
+ end
+ end
+
+ it 'does not bail if there are no lookup entries' do
+ ActiveSession.cleanup(user)
+ end
+ end
+end
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb
index 586d073eb5e..a00db1d2bfc 100644
--- a/spec/models/ci/stage_spec.rb
+++ b/spec/models/ci/stage_spec.rb
@@ -51,7 +51,7 @@ describe Ci::Stage, :models do
end
end
- describe 'update_status' do
+ describe '#update_status' do
context 'when stage objects needs to be updated' do
before do
create(:ci_build, :success, stage_id: stage.id)
@@ -87,4 +87,36 @@ describe Ci::Stage, :models do
end
end
end
+
+ describe '#index' do
+ context 'when stage has been imported and does not have position index set' do
+ before do
+ stage.update_column(:position, nil)
+ end
+
+ context 'when stage has statuses' do
+ before do
+ create(:ci_build, :running, stage_id: stage.id, stage_idx: 10)
+ end
+
+ it 'recalculates index before updating status' do
+ expect(stage.reload.position).to be_nil
+
+ stage.update_status
+
+ expect(stage.reload.position).to eq 10
+ end
+ end
+
+ context 'when stage does not have statuses' do
+ it 'fallbacks to zero' do
+ expect(stage.reload.position).to be_nil
+
+ stage.update_status
+
+ expect(stage.reload.position).to eq 0
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb
index 2705421e540..fb51c0172ab 100644
--- a/spec/models/diff_note_spec.rb
+++ b/spec/models/diff_note_spec.rb
@@ -85,12 +85,35 @@ describe DiffNote do
end
describe "#diff_file" do
- it "returns the correct diff file" do
- diff_file = subject.diff_file
+ context 'when the discussion was created in the diff' do
+ it 'returns correct diff file' do
+ diff_file = subject.diff_file
- expect(diff_file.old_path).to eq(position.old_path)
- expect(diff_file.new_path).to eq(position.new_path)
- expect(diff_file.diff_refs).to eq(position.diff_refs)
+ expect(diff_file.old_path).to eq(position.old_path)
+ expect(diff_file.new_path).to eq(position.new_path)
+ expect(diff_file.diff_refs).to eq(position.diff_refs)
+ end
+ end
+
+ context 'when discussion is outdated or not created in the diff' do
+ let(:diff_refs) { project.commit(sample_commit.id).diff_refs }
+ let(:position) do
+ Gitlab::Diff::Position.new(
+ old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: nil,
+ new_line: 14,
+ diff_refs: diff_refs
+ )
+ end
+
+ it 'returns the correct diff file' do
+ diff_file = subject.diff_file
+
+ expect(diff_file.old_path).to eq(position.old_path)
+ expect(diff_file.new_path).to eq(position.new_path)
+ expect(diff_file.diff_refs).to eq(position.diff_refs)
+ end
end
end
diff --git a/spec/models/lfs_object_spec.rb b/spec/models/lfs_object_spec.rb
index ba06ff42d87..6e35511e848 100644
--- a/spec/models/lfs_object_spec.rb
+++ b/spec/models/lfs_object_spec.rb
@@ -62,9 +62,7 @@ describe LfsObject do
.with('LfsObjectUploader', described_class.name, :file, kind_of(Numeric))
.once
- lfs_object = create(:lfs_object)
- lfs_object.file = fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "`/png")
- lfs_object.save!
+ create(:lfs_object, :with_file)
end
end
end
diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb
index 5a3b5b1f517..ffc78015f94 100644
--- a/spec/models/members/group_member_spec.rb
+++ b/spec/models/members/group_member_spec.rb
@@ -28,52 +28,12 @@ describe GroupMember do
end
end
- describe 'notifications' do
- describe "#after_create" do
- it "sends email to user" do
- membership = build(:group_member)
+ it_behaves_like 'members notifications', :group
- allow(membership).to receive(:notification_service)
- .and_return(double('NotificationService').as_null_object)
- expect(membership).to receive(:notification_service)
+ describe '#real_source_type' do
+ subject { create(:group_member).real_source_type }
- membership.save
- end
- end
-
- describe "#after_update" do
- before do
- @group_member = create :group_member
- allow(@group_member).to receive(:notification_service)
- .and_return(double('NotificationService').as_null_object)
- end
-
- it "sends email to user" do
- expect(@group_member).to receive(:notification_service)
- @group_member.update_attribute(:access_level, GroupMember::MASTER)
- end
-
- it "does not send an email when the access level has not changed" do
- expect(@group_member).not_to receive(:notification_service)
- @group_member.update_attribute(:access_level, GroupMember::OWNER)
- end
- end
-
- describe '#after_accept_request' do
- it 'calls NotificationService.accept_group_access_request' do
- member = create(:group_member, user: build(:user), requested_at: Time.now)
-
- expect_any_instance_of(NotificationService).to receive(:new_group_member)
-
- member.__send__(:after_accept_request)
- end
- end
-
- describe '#real_source_type' do
- subject { create(:group_member).real_source_type }
-
- it { is_expected.to eq 'Group' }
- end
+ it { is_expected.to eq 'Group' }
end
describe '#update_two_factor_requirement' do
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index b8b0e63f92e..574eb468e4c 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -123,15 +123,5 @@ describe ProjectMember do
it { expect(@project_2.users).to be_empty }
end
- describe 'notifications' do
- describe '#after_accept_request' do
- it 'calls NotificationService.new_project_member' do
- member = create(:project_member, user: create(:user), requested_at: Time.now)
-
- expect_any_instance_of(NotificationService).to receive(:new_project_member)
-
- member.__send__(:after_accept_request)
- end
- end
- end
+ it_behaves_like 'members notifications', :project
end
diff --git a/spec/models/notification_setting_spec.rb b/spec/models/notification_setting_spec.rb
index 2a0d102d3fe..12681a147b4 100644
--- a/spec/models/notification_setting_spec.rb
+++ b/spec/models/notification_setting_spec.rb
@@ -40,7 +40,12 @@ RSpec.describe NotificationSetting do
expect(notification_setting.new_issue).to eq(true)
expect(notification_setting.close_issue).to eq(true)
expect(notification_setting.merge_merge_request).to eq(true)
- expect(notification_setting.close_merge_request).to eq(false)
+
+ # In Rails 5 assigning a value which is not explicitly `true` or `false` ("nil" in this case)
+ # to a boolean column transforms it to `true`.
+ # In Rails 4 it transforms the value to `false` with deprecation warning.
+ # Replace `eq(Gitlab.rails5?)` with `eq(true)` when removing rails5? code.
+ expect(notification_setting.close_merge_request).to eq(Gitlab.rails5?)
expect(notification_setting.reopen_merge_request).to eq(false)
end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index e45fe7db1e7..630b9e0519f 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -1224,15 +1224,15 @@ describe Repository do
end
end
- shared_examples 'repo exists check' do
+ describe '#exists?' do
it 'returns true when a repository exists' do
- expect(repository.exists?).to eq(true)
+ expect(repository.exists?).to be(true)
end
it 'returns false if no full path can be constructed' do
allow(repository).to receive(:full_path).and_return(nil)
- expect(repository.exists?).to eq(false)
+ expect(repository.exists?).to be(false)
end
context 'with broken storage', :broken_storage do
@@ -1242,16 +1242,6 @@ describe Repository do
end
end
- describe '#exists?' do
- context 'when repository_exists is disabled' do
- it_behaves_like 'repo exists check'
- end
-
- context 'when repository_exists is enabled', :skip_gitaly_mock do
- it_behaves_like 'repo exists check'
- end
- end
-
describe '#has_visible_content?' do
before do
# If raw_repository.has_visible_content? gets called more than once then
diff --git a/spec/requests/api/discussions_spec.rb b/spec/requests/api/discussions_spec.rb
index 4a44b219a67..ef34192f888 100644
--- a/spec/requests/api/discussions_spec.rb
+++ b/spec/requests/api/discussions_spec.rb
@@ -2,32 +2,53 @@ require 'spec_helper'
describe API::Discussions do
let(:user) { create(:user) }
- let!(:project) { create(:project, :public, namespace: user.namespace) }
+ let!(:project) { create(:project, :public, :repository, namespace: user.namespace) }
let(:private_user) { create(:user) }
before do
- project.add_reporter(user)
+ project.add_developer(user)
end
- context "when noteable is an Issue" do
+ context 'when noteable is an Issue' do
let!(:issue) { create(:issue, project: project, author: user) }
let!(:issue_note) { create(:discussion_note_on_issue, noteable: issue, project: project, author: user) }
- it_behaves_like "discussions API", 'projects', 'issues', 'iid' do
+ it_behaves_like 'discussions API', 'projects', 'issues', 'iid' do
let(:parent) { project }
let(:noteable) { issue }
let(:note) { issue_note }
end
end
- context "when noteable is a Snippet" do
+ context 'when noteable is a Snippet' do
let!(:snippet) { create(:project_snippet, project: project, author: user) }
let!(:snippet_note) { create(:discussion_note_on_snippet, noteable: snippet, project: project, author: user) }
- it_behaves_like "discussions API", 'projects', 'snippets', 'id' do
+ it_behaves_like 'discussions API', 'projects', 'snippets', 'id' do
let(:parent) { project }
let(:noteable) { snippet }
let(:note) { snippet_note }
end
end
+
+ context 'when noteable is a Merge Request' do
+ let!(:noteable) { create(:merge_request_with_diffs, source_project: project, target_project: project, author: user) }
+ let!(:note) { create(:discussion_note_on_merge_request, noteable: noteable, project: project, author: user) }
+ let!(:diff_note) { create(:diff_note_on_merge_request, noteable: noteable, project: project, author: user) }
+ let(:parent) { project }
+
+ it_behaves_like 'discussions API', 'projects', 'merge_requests', 'iid'
+ it_behaves_like 'diff discussions API', 'projects', 'merge_requests', 'iid'
+ it_behaves_like 'resolvable discussions API', 'projects', 'merge_requests', 'iid'
+ end
+
+ context 'when noteable is a Commit' do
+ let!(:noteable) { create(:commit, project: project, author: user) }
+ let!(:note) { create(:discussion_note_on_commit, commit_id: noteable.id, project: project, author: user) }
+ let!(:diff_note) { create(:diff_note_on_commit, commit_id: noteable.id, project: project, author: user) }
+ let(:parent) { project }
+
+ it_behaves_like 'discussions API', 'projects', 'repository/commits', 'id'
+ it_behaves_like 'diff discussions API', 'projects', 'repository/commits', 'id'
+ end
end
diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb
index 3ffdfdc0e9a..0a2963452e4 100644
--- a/spec/requests/api/jobs_spec.rb
+++ b/spec/requests/api/jobs_spec.rb
@@ -281,7 +281,7 @@ describe API::Jobs do
get_artifact_file(artifact)
expect(response).to have_gitlab_http_status(200)
- expect(response.headers)
+ expect(response.headers.to_h)
.to include('Content-Type' => 'application/json',
'Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
end
@@ -311,7 +311,7 @@ describe API::Jobs do
it 'returns specific job artifacts' do
expect(response).to have_gitlab_http_status(200)
- expect(response.headers).to include(download_headers)
+ expect(response.headers.to_h).to include(download_headers)
expect(response.body).to match_file(job.artifacts_file.file.file)
end
end
@@ -462,7 +462,7 @@ describe API::Jobs do
end
it { expect(response).to have_http_status(:ok) }
- it { expect(response.headers).to include(download_headers) }
+ it { expect(response.headers.to_h).to include(download_headers) }
end
context 'when artifacts are stored remotely' do
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index 27f5dff7901..9ae39f2ef44 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -1348,7 +1348,7 @@ describe API::Runner do
it 'download artifacts' do
expect(response).to have_http_status(200)
- expect(response.headers).to include download_headers
+ expect(response.headers.to_h).to include download_headers
end
end
@@ -1363,7 +1363,7 @@ describe API::Runner do
it 'uses workhorse send-url' do
expect(response).to have_gitlab_http_status(200)
- expect(response.headers).to include(
+ expect(response.headers.to_h).to include(
'Gitlab-Workhorse-Send-Data' => /send-url:/)
end
end
diff --git a/spec/requests/api/v3/builds_spec.rb b/spec/requests/api/v3/builds_spec.rb
index 00f067889a0..485d7c2cc43 100644
--- a/spec/requests/api/v3/builds_spec.rb
+++ b/spec/requests/api/v3/builds_spec.rb
@@ -232,7 +232,7 @@ describe API::V3::Builds do
it 'returns specific job artifacts' do
expect(response).to have_http_status(200)
- expect(response.headers).to include(download_headers)
+ expect(response.headers.to_h).to include(download_headers)
expect(response.body).to match_file(build.artifacts_file.file.file)
end
end
@@ -332,7 +332,7 @@ describe API::V3::Builds do
end
it { expect(response).to have_http_status(200) }
- it { expect(response.headers).to include(download_headers) }
+ it { expect(response.headers.to_h).to include(download_headers) }
end
context 'when artifacts are stored remotely' do
diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb
index 6bed8e812c0..cd1a6cfc427 100644
--- a/spec/requests/openid_connect_spec.rb
+++ b/spec/requests/openid_connect_spec.rb
@@ -153,4 +153,13 @@ describe 'OpenID Connect requests' do
end
end
end
+
+ context 'OpenID configuration information' do
+ it 'correctly returns the configuration' do
+ get '/.well-known/openid-configuration'
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to have_key('issuer')
+ end
+ end
end
diff --git a/spec/services/applications/create_service_spec.rb b/spec/services/applications/create_service_spec.rb
index 47a2a9d6403..9c43b56744b 100644
--- a/spec/services/applications/create_service_spec.rb
+++ b/spec/services/applications/create_service_spec.rb
@@ -1,13 +1,17 @@
-require 'spec_helper'
+require "spec_helper"
describe ::Applications::CreateService do
let(:user) { create(:user) }
let(:params) { attributes_for(:application) }
- let(:request) { ActionController::TestRequest.new(remote_ip: '127.0.0.1') }
+ let(:request) do
+ if Gitlab.rails5?
+ ActionController::TestRequest.new({ remote_ip: "127.0.0.1" }, ActionController::TestSession.new)
+ else
+ ActionController::TestRequest.new(remote_ip: "127.0.0.1")
+ end
+ end
subject { described_class.new(user, params) }
- it 'creates an application' do
- expect { subject.execute(request) }.to change { Doorkeeper::Application.count }.by(1)
- end
+ it { expect { subject.execute(request) }.to change { Doorkeeper::Application.count }.by(1) }
end
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index 8de0bdf92e2..5bc6031388e 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -6,7 +6,9 @@ describe Ci::RetryBuildService do
set(:pipeline) { create(:ci_pipeline, project: project) }
let(:stage) do
- Ci::Stage.create!(project: project, pipeline: pipeline, name: 'test')
+ create(:ci_stage_entity, project: project,
+ pipeline: pipeline,
+ name: 'test')
end
let(:build) { create(:ci_build, pipeline: pipeline, stage_id: stage.id) }
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index c38ddf4612b..e8568bf8bb3 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -219,7 +219,7 @@ describe MergeRequests::MergeService do
service.execute(merge_request)
- expect(merge_request.merge_error).to include(error_message)
+ expect(merge_request.merge_error).to include('Something went wrong during merge')
expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message))
end
@@ -231,7 +231,7 @@ describe MergeRequests::MergeService do
service.execute(merge_request)
- expect(merge_request.merge_error).to include(error_message)
+ expect(merge_request.merge_error).to include('Something went wrong during merge pre-receive hook')
expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message))
end
diff --git a/spec/services/notes/resolve_service_spec.rb b/spec/services/notes/resolve_service_spec.rb
new file mode 100644
index 00000000000..b54d40a7a5c
--- /dev/null
+++ b/spec/services/notes/resolve_service_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe Notes::ResolveService do
+ let(:merge_request) { create(:merge_request) }
+ let(:note) { create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.project) }
+ let(:user) { merge_request.author }
+
+ describe '#execute' do
+ it "resolves the note" do
+ described_class.new(merge_request.project, user).execute(note)
+ note.reload
+
+ expect(note.resolved?).to be true
+ expect(note.resolved_by).to eq(user)
+ end
+
+ it "sends notifications if all discussions are resolved" do
+ expect_any_instance_of(MergeRequests::ResolvedDiscussionNotificationService).to receive(:execute).with(merge_request)
+
+ described_class.new(merge_request.project, user).execute(note)
+ end
+ end
+end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 55bbe954491..48ef5f3c115 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -96,6 +96,37 @@ describe NotificationService, :mailer do
it_should_behave_like 'participating by assignee notification'
end
+ describe '#async' do
+ let(:async) { notification.async }
+ set(:key) { create(:personal_key) }
+
+ it 'returns an Async object with the correct parent' do
+ expect(async).to be_a(described_class::Async)
+ expect(async.parent).to eq(notification)
+ end
+
+ context 'when receiving a public method' do
+ it 'schedules a MailScheduler::NotificationServiceWorker' do
+ expect(MailScheduler::NotificationServiceWorker)
+ .to receive(:perform_async).with('new_key', key)
+
+ async.new_key(key)
+ end
+ end
+
+ context 'when receiving a private method' do
+ it 'raises NoMethodError' do
+ expect { async.notifiable?(key) }.to raise_error(NoMethodError)
+ end
+ end
+
+ context 'when recieving a non-existent method' do
+ it 'raises NoMethodError' do
+ expect { async.foo(key) }.to raise_error(NoMethodError)
+ end
+ end
+ end
+
describe 'Keys' do
describe '#new_key' do
let(:key_options) { {} }
@@ -982,6 +1013,8 @@ describe NotificationService, :mailer do
let(:merge_request) { create :merge_request, source_project: project, assignee: create(:user), description: 'cc @participant' }
before do
+ project.add_master(merge_request.author)
+ project.add_master(merge_request.assignee)
build_team(merge_request.target_project)
add_users_with_subscription(merge_request.target_project, merge_request)
update_custom_notification(:new_merge_request, @u_guest_custom, resource: project)
@@ -1093,15 +1126,18 @@ describe NotificationService, :mailer do
end
describe '#reassigned_merge_request' do
+ let(:current_user) { create(:user) }
+
before do
update_custom_notification(:reassign_merge_request, @u_guest_custom, resource: project)
update_custom_notification(:reassign_merge_request, @u_custom_global)
end
it do
- notification.reassigned_merge_request(merge_request, merge_request.author)
+ notification.reassigned_merge_request(merge_request, current_user, merge_request.author)
should_email(merge_request.assignee)
+ should_email(merge_request.author)
should_email(@u_watcher)
should_email(@u_participant_mentioned)
should_email(@subscriber)
@@ -1116,7 +1152,7 @@ describe NotificationService, :mailer do
end
it 'adds "assigned" reason for new assignee' do
- notification.reassigned_merge_request(merge_request, merge_request.author)
+ notification.reassigned_merge_request(merge_request, current_user, merge_request.author)
email = find_email_for(merge_request.assignee)
@@ -1126,7 +1162,7 @@ describe NotificationService, :mailer do
it_behaves_like 'participating notifications' do
let(:participant) { create(:user, username: 'user-participant') }
let(:issuable) { merge_request }
- let(:notification_trigger) { notification.reassigned_merge_request(merge_request, @u_disabled) }
+ let(:notification_trigger) { notification.reassigned_merge_request(merge_request, current_user, merge_request.author) }
end
end
diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb
index 1b6caeab15d..347ac13828c 100644
--- a/spec/services/projects/update_pages_service_spec.rb
+++ b/spec/services/projects/update_pages_service_spec.rb
@@ -29,25 +29,10 @@ describe Projects::UpdatePagesService do
end
describe 'pages artifacts' do
- context 'with expiry date' do
- before do
- build.artifacts_expire_in = "2 days"
- build.save!
- end
-
- it "doesn't delete artifacts" do
- expect(execute).to eq(:success)
-
- expect(build.reload.artifacts?).to eq(true)
- end
- end
-
- context 'without expiry date' do
- it "does delete artifacts" do
- expect(execute).to eq(:success)
+ it "doesn't delete artifacts after deploying" do
+ expect(execute).to eq(:success)
- expect(build.reload.artifacts?).to eq(false)
- end
+ expect(build.reload.artifacts?).to eq(true)
end
end
@@ -100,25 +85,10 @@ describe Projects::UpdatePagesService do
end
describe 'pages artifacts' do
- context 'with expiry date' do
- before do
- build.artifacts_expire_in = "2 days"
- build.save!
- end
-
- it "doesn't delete artifacts" do
- expect(execute).to eq(:success)
-
- expect(build.artifacts?).to eq(true)
- end
- end
-
- context 'without expiry date' do
- it "does delete artifacts" do
- expect(execute).to eq(:success)
+ it "doesn't delete artifacts after deploying" do
+ expect(execute).to eq(:success)
- expect(build.reload.artifacts?).to eq(false)
- end
+ expect(build.artifacts?).to eq(true)
end
end
@@ -153,11 +123,13 @@ describe Projects::UpdatePagesService do
expect(execute).not_to eq(:success)
end
- it 'fails for empty file fails' do
- build.job_artifacts_archive.update_attributes(file: empty_file)
+ context 'when using empty file' do
+ let(:file) { empty_file }
- expect { execute }
- .to raise_error(Projects::UpdatePagesService::FailedToExtractError)
+ it 'fails to extract' do
+ expect { execute }
+ .to raise_error(Projects::UpdatePagesService::FailedToExtractError)
+ end
end
context 'when timeout happens by DNS error' do
@@ -171,13 +143,12 @@ describe Projects::UpdatePagesService do
build.reload
expect(deploy_status).to be_failed
- expect(build.artifacts?).to be_truthy
end
end
context 'when failed to extract zip artifacts' do
before do
- allow_any_instance_of(described_class)
+ expect_any_instance_of(described_class)
.to receive(:extract_zip_archive!)
.and_raise(Projects::UpdatePagesService::FailedToExtractError)
end
@@ -188,21 +159,19 @@ describe Projects::UpdatePagesService do
build.reload
expect(deploy_status).to be_failed
- expect(build.artifacts?).to be_truthy
end
end
context 'when missing artifacts metadata' do
before do
- allow(build).to receive(:artifacts_metadata?).and_return(false)
+ expect(build).to receive(:artifacts_metadata?).and_return(false)
end
- it 'does not raise an error and remove artifacts as failed job' do
+ it 'does not raise an error as failed job' do
execute
build.reload
expect(deploy_status).to be_failed
- expect(build.artifacts?).to be_falsey
end
end
end
diff --git a/spec/services/repository_archive_clean_up_service_spec.rb b/spec/services/repository_archive_clean_up_service_spec.rb
index 2d7fa3f80f7..ab1c638fc39 100644
--- a/spec/services/repository_archive_clean_up_service_spec.rb
+++ b/spec/services/repository_archive_clean_up_service_spec.rb
@@ -1,15 +1,47 @@
require 'spec_helper'
describe RepositoryArchiveCleanUpService do
- describe '#execute' do
- subject(:service) { described_class.new }
+ subject(:service) { described_class.new }
+ describe '#execute (new archive locations)' do
+ let(:sha) { "0" * 40 }
+
+ it 'removes outdated archives and directories in a new-style path' do
+ in_directory_with_files("project-999/#{sha}", %w[tar tar.bz2 tar.gz zip], 3.hours) do |dirname, files|
+ service.execute
+
+ files.each { |filename| expect(File.exist?(filename)).to be_falsy }
+ expect(File.directory?(dirname)).to be_falsy
+ expect(File.directory?(File.dirname(dirname))).to be_falsy
+ end
+ end
+
+ it 'does not remove directories when they contain outdated non-archives' do
+ in_directory_with_files("project-999/#{sha}", %w[tar conf rb], 2.hours) do |dirname, files|
+ service.execute
+
+ expect(File.directory?(dirname)).to be_truthy
+ end
+ end
+
+ it 'does not remove in-date archives in a new-style path' do
+ in_directory_with_files("project-999/#{sha}", %w[tar tar.bz2 tar.gz zip], 1.hour) do |dirname, files|
+ service.execute
+
+ files.each { |filename| expect(File.exist?(filename)).to be_truthy }
+ end
+ end
+ end
+
+ describe '#execute (legacy archive locations)' do
context 'when the downloads directory does not exist' do
it 'does not remove any archives' do
path = '/invalid/path/'
stub_repository_downloads_path(path)
+ allow(File).to receive(:directory?).and_call_original
expect(File).to receive(:directory?).with(path).and_return(false)
+
expect(service).not_to receive(:clean_up_old_archives)
expect(service).not_to receive(:clean_up_empty_directories)
@@ -19,7 +51,7 @@ describe RepositoryArchiveCleanUpService do
context 'when the downloads directory exists' do
shared_examples 'invalid archive files' do |dirname, extensions, mtime|
- it 'does not remove files and directoy' do
+ it 'does not remove files and directory' do
in_directory_with_files(dirname, extensions, mtime) do |dir, files|
service.execute
@@ -43,7 +75,7 @@ describe RepositoryArchiveCleanUpService do
end
context 'with files older than 2 hours inside invalid directories' do
- it_behaves_like 'invalid archive files', 'john_doe/sample.git', %w[conf rb tar tar.gz], 2.hours
+ it_behaves_like 'invalid archive files', 'john/doe/sample.git', %w[conf rb tar tar.gz], 2.hours
end
context 'with files newer than 2 hours that matches valid archive extensions' do
@@ -58,24 +90,24 @@ describe RepositoryArchiveCleanUpService do
it_behaves_like 'invalid archive files', 'sample.git', %w[conf rb tar tar.gz], 1.hour
end
end
+ end
- def in_directory_with_files(dirname, extensions, mtime)
- Dir.mktmpdir do |tmpdir|
- stub_repository_downloads_path(tmpdir)
- dir = File.join(tmpdir, dirname)
- files = create_temporary_files(dir, extensions, mtime)
+ def in_directory_with_files(dirname, extensions, mtime)
+ Dir.mktmpdir do |tmpdir|
+ stub_repository_downloads_path(tmpdir)
+ dir = File.join(tmpdir, dirname)
+ files = create_temporary_files(dir, extensions, mtime)
- yield(dir, files)
- end
+ yield(dir, files)
end
+ end
- def stub_repository_downloads_path(path)
- allow(Gitlab.config.gitlab).to receive(:repository_downloads_path).and_return(path)
- end
+ def stub_repository_downloads_path(path)
+ allow(Gitlab.config.gitlab).to receive(:repository_downloads_path).and_return(path)
+ end
- def create_temporary_files(dir, extensions, mtime)
- FileUtils.mkdir_p(dir)
- FileUtils.touch(extensions.map { |ext| File.join(dir, "sample.#{ext}") }, mtime: Time.now - mtime)
- end
+ def create_temporary_files(dir, extensions, mtime)
+ FileUtils.mkdir_p(dir)
+ FileUtils.touch(extensions.map { |ext| File.join(dir, "sample.#{ext}") }, mtime: Time.now - mtime)
end
end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 893804f1470..e28b0ea5cf2 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -909,13 +909,7 @@ describe SystemNoteService do
it 'sets the note text' do
noteable.update_attribute(:time_estimate, 277200)
- expect(subject.note).to eq "changed time estimate to 1w 4d 5h,"
- end
-
- it 'appends a comma to separate the note from the update_at time' do
- noteable.update_attribute(:time_estimate, 277200)
-
- expect(subject.note).to end_with(',')
+ expect(subject.note).to eq "changed time estimate to 1w 4d 5h"
end
end
diff --git a/spec/support/commit_trailers_spec_helper.rb b/spec/support/commit_trailers_spec_helper.rb
index add359946db..efa317fd2f9 100644
--- a/spec/support/commit_trailers_spec_helper.rb
+++ b/spec/support/commit_trailers_spec_helper.rb
@@ -8,7 +8,7 @@ module CommitTrailersSpecHelper
expect(wrapper.attribute('data-user').value).to eq user.id.to_s
end
- def expect_to_have_mailto_link(doc, email:, trailer:)
+ def expect_to_have_mailto_link_with_avatar(doc, email:, trailer:)
wrapper = find_user_wrapper(doc, trailer)
expect_to_have_links_with_url_and_avatar(wrapper, "mailto:#{CGI.escape_html(email)}", email)
diff --git a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
index 5b0b609f7f2..5a569d233bc 100644
--- a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
+++ b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
@@ -79,7 +79,7 @@ RSpec.shared_examples 'a creatable merge request' do
end
end
- it 'updates the branches when selecting a new target project' do
+ it 'updates the branches when selecting a new target project', :js do
target_project_member = target_project.owner
CreateBranchService.new(target_project, target_project_member)
.execute('a-brand-new-branch-to-test', 'master')
@@ -92,7 +92,7 @@ RSpec.shared_examples 'a creatable merge request' do
first('.js-target-branch').click
- within('.dropdown-target-branch .dropdown-content') do
+ within('.js-target-branch-dropdown .dropdown-content') do
expect(page).to have_content('a-brand-new-branch-to-test')
end
end
diff --git a/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb b/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb
new file mode 100644
index 00000000000..b29bb3c2fc0
--- /dev/null
+++ b/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb
@@ -0,0 +1,52 @@
+RSpec.shared_examples 'Master manages access requests' do
+ let(:user) { create(:user) }
+ let(:master) { create(:user) }
+
+ before do
+ entity.request_access(user)
+ entity.respond_to?(:add_owner) ? entity.add_owner(master) : entity.add_master(master)
+ sign_in(master)
+ end
+
+ it 'master can see access requests' do
+ visit members_page_path
+
+ expect_visible_access_request(entity, user)
+ end
+
+ it 'master can grant access', :js do
+ visit members_page_path
+
+ expect_visible_access_request(entity, user)
+
+ accept_confirm { click_on 'Grant access' }
+
+ expect_no_visible_access_request(entity, user)
+
+ page.within('.members-list') do
+ expect(page).to have_content user.name
+ end
+ end
+
+ it 'master can deny access', :js do
+ visit members_page_path
+
+ expect_visible_access_request(entity, user)
+
+ accept_confirm { click_on 'Deny access' }
+
+ expect_no_visible_access_request(entity, user)
+ expect(page).not_to have_content user.name
+ end
+
+ def expect_visible_access_request(entity, user)
+ expect(entity.requesters.exists?(user_id: user)).to be_truthy
+ expect(page).to have_content "Users requesting access to #{entity.name} 1"
+ expect(page).to have_content user.name
+ end
+
+ def expect_no_visible_access_request(entity, user)
+ expect(entity.requesters.exists?(user_id: user)).to be_falsy
+ expect(page).not_to have_content "Users requesting access to #{entity.name}"
+ end
+end
diff --git a/spec/support/shared_examples/helm_generated_script.rb b/spec/support/shared_examples/helm_generated_script.rb
new file mode 100644
index 00000000000..56e86a87ab9
--- /dev/null
+++ b/spec/support/shared_examples/helm_generated_script.rb
@@ -0,0 +1,19 @@
+shared_examples 'helm commands' do
+ describe '#generate_script' do
+ let(:helm_setup) do
+ <<~EOS
+ set -eo pipefail
+ ALPINE_VERSION=$(cat /etc/alpine-release | cut -d '.' -f 1,2)
+ echo http://mirror.clarkson.edu/alpine/v$ALPINE_VERSION/main >> /etc/apk/repositories
+ echo http://mirror1.hs-esslingen.de/pub/Mirrors/alpine/v$ALPINE_VERSION/main >> /etc/apk/repositories
+ apk add -U ca-certificates openssl >/dev/null
+ wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
+ mv /tmp/linux-amd64/helm /usr/bin/
+ EOS
+ end
+
+ it 'should return appropriate command' do
+ expect(subject.generate_script).to eq(helm_setup + commands)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/members_notifications_shared_example.rb b/spec/support/shared_examples/models/members_notifications_shared_example.rb
new file mode 100644
index 00000000000..76611e54306
--- /dev/null
+++ b/spec/support/shared_examples/models/members_notifications_shared_example.rb
@@ -0,0 +1,63 @@
+RSpec.shared_examples 'members notifications' do |entity_type|
+ let(:notification_service) { double('NotificationService').as_null_object }
+
+ before do
+ allow(member).to receive(:notification_service).and_return(notification_service)
+ end
+
+ describe "#after_create" do
+ let(:member) { build(:"#{entity_type}_member") }
+
+ it "sends email to user" do
+ expect(notification_service).to receive(:"new_#{entity_type}_member").with(member)
+
+ member.save
+ end
+ end
+
+ describe "#after_update" do
+ let(:member) { create(:"#{entity_type}_member", :developer) }
+
+ it "calls NotificationService.update_#{entity_type}_member" do
+ expect(notification_service).to receive(:"update_#{entity_type}_member").with(member)
+
+ member.update_attribute(:access_level, Member::MASTER)
+ end
+
+ it "does not send an email when the access level has not changed" do
+ expect(notification_service).not_to receive(:"update_#{entity_type}_member")
+
+ member.touch
+ end
+ end
+
+ describe '#accept_request' do
+ let(:member) { create(:"#{entity_type}_member", :access_request) }
+
+ it "calls NotificationService.new_#{entity_type}_member" do
+ expect(notification_service).to receive(:"new_#{entity_type}_member").with(member)
+
+ member.accept_request
+ end
+ end
+
+ describe "#accept_invite!" do
+ let(:member) { create(:"#{entity_type}_member", :invited) }
+
+ it "calls NotificationService.accept_#{entity_type}_invite" do
+ expect(notification_service).to receive(:"accept_#{entity_type}_invite").with(member)
+
+ member.accept_invite!(build(:user))
+ end
+ end
+
+ describe "#decline_invite!" do
+ let(:member) { create(:"#{entity_type}_member", :invited) }
+
+ it "calls NotificationService.decline_#{entity_type}_invite" do
+ expect(notification_service).to receive(:"decline_#{entity_type}_invite").with(member)
+
+ member.decline_invite!
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/diff_discussions.rb b/spec/support/shared_examples/requests/api/diff_discussions.rb
new file mode 100644
index 00000000000..85a4bd8ca27
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/diff_discussions.rb
@@ -0,0 +1,57 @@
+shared_examples 'diff discussions API' do |parent_type, noteable_type, id_name|
+ describe "GET /#{parent_type}/:id/#{noteable_type}/:noteable_id/discussions" do
+ it "includes diff discussions" do
+ get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions", user)
+
+ discussion = json_response.find { |record| record['id'] == diff_note.discussion_id }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(discussion).not_to be_nil
+ expect(discussion['individual_note']).to eq(false)
+ expect(discussion['notes'].first['body']).to eq(diff_note.note)
+ end
+ end
+
+ describe "GET /#{parent_type}/:id/#{noteable_type}/:noteable_id/discussions/:discussion_id" do
+ it "returns a discussion by id" do
+ get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions/#{diff_note.discussion_id}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['id']).to eq(diff_note.discussion_id)
+ expect(json_response['notes'].first['body']).to eq(diff_note.note)
+ expect(json_response['notes'].first['position']).to eq(diff_note.position.to_h.stringify_keys)
+ end
+ end
+
+ describe "POST /#{parent_type}/:id/#{noteable_type}/:noteable_id/discussions" do
+ it "creates a new diff note" do
+ position = diff_note.position.to_h
+
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions", user), body: 'hi!', position: position
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['notes'].first['body']).to eq('hi!')
+ expect(json_response['notes'].first['type']).to eq('DiffNote')
+ expect(json_response['notes'].first['position']).to eq(position.stringify_keys)
+ end
+
+ it "returns a 400 bad request error when position is invalid" do
+ position = diff_note.position.to_h.merge(new_line: '100000')
+
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions", user), body: 'hi!', position: position
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+ end
+
+ describe "POST /#{parent_type}/:id/#{noteable_type}/:noteable_id/discussions/:discussion_id/notes" do
+ it 'adds a new note to the diff discussion' do
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "discussions/#{diff_note.discussion_id}/notes", user), body: 'hi!'
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['body']).to eq('hi!')
+ expect(json_response['type']).to eq('DiffNote')
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/resolvable_discussions.rb b/spec/support/shared_examples/requests/api/resolvable_discussions.rb
new file mode 100644
index 00000000000..408ad08cc48
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/resolvable_discussions.rb
@@ -0,0 +1,87 @@
+shared_examples 'resolvable discussions API' do |parent_type, noteable_type, id_name|
+ describe "PUT /#{parent_type}/:id/#{noteable_type}/:noteable_id/discussions/:discussion_id" do
+ it "resolves discussion if resolved is true" do
+ put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "discussions/#{note.discussion_id}", user), resolved: true
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['notes'].size).to eq(1)
+ expect(json_response['notes'][0]['resolved']).to eq(true)
+ end
+
+ it "unresolves discussion if resolved is false" do
+ put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "discussions/#{note.discussion_id}", user), resolved: false
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['notes'].size).to eq(1)
+ expect(json_response['notes'][0]['resolved']).to eq(false)
+ end
+
+ it "returns a 400 bad request error if resolved parameter is not passed" do
+ put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "discussions/#{note.discussion_id}", user)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+
+ it "returns a 401 unauthorized error if user is not authenticated" do
+ put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "discussions/#{note.discussion_id}"), resolved: true
+
+ expect(response).to have_gitlab_http_status(401)
+ end
+
+ it "returns a 403 error if user resolves discussion of someone else" do
+ put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "discussions/#{note.discussion_id}", private_user), resolved: true
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+
+ context 'when user does not have access to read the discussion' do
+ before do
+ parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ it 'responds with 404' do
+ put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "discussions/#{note.discussion_id}", private_user), resolved: true
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ describe "PUT /#{parent_type}/:id/#{noteable_type}/:noteable_id/discussions/:discussion_id/notes/:note_id" do
+ it 'returns resolved note when resolved parameter is true' do
+ put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "discussions/#{note.discussion_id}/notes/#{note.id}", user), resolved: true
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['resolved']).to eq(true)
+ end
+
+ it 'returns a 404 error when note id not found' do
+ put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "discussions/#{note.discussion_id}/notes/12345", user),
+ body: 'Hello!'
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns a 400 bad request error if neither body nor resolved parameter is given' do
+ put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "discussions/#{note.discussion_id}/notes/#{note.id}", user)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+
+ it "returns a 403 error if user resolves note of someone else" do
+ put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "discussions/#{note.discussion_id}/notes/#{note.id}", private_user), resolved: true
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+end
diff --git a/spec/uploaders/lfs_object_uploader_spec.rb b/spec/uploaders/lfs_object_uploader_spec.rb
index a2fb3886610..9f28510c3e4 100644
--- a/spec/uploaders/lfs_object_uploader_spec.rb
+++ b/spec/uploaders/lfs_object_uploader_spec.rb
@@ -46,8 +46,7 @@ describe LfsObjectUploader do
end
describe 'remote file' do
- let(:remote) { described_class::Store::REMOTE }
- let(:lfs_object) { create(:lfs_object, file_store: remote) }
+ let(:lfs_object) { create(:lfs_object, :object_storage, :with_file) }
context 'with object storage enabled' do
before do
@@ -57,16 +56,11 @@ describe LfsObjectUploader do
it 'can store file remotely' do
allow(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async)
- store_file(lfs_object)
+ lfs_object
- expect(lfs_object.file_store).to eq remote
+ expect(lfs_object.file_store).to eq(described_class::Store::REMOTE)
expect(lfs_object.file.path).not_to be_blank
end
end
end
-
- def store_file(lfs_object)
- lfs_object.file = fixture_file_upload(Rails.root.join("spec/fixtures/dk.png"), "`/png")
- lfs_object.save!
- end
end
diff --git a/spec/workers/mail_scheduler/issue_due_worker_spec.rb b/spec/workers/mail_scheduler/issue_due_worker_spec.rb
index 48ac1b8a1a4..1026ae5b4bf 100644
--- a/spec/workers/mail_scheduler/issue_due_worker_spec.rb
+++ b/spec/workers/mail_scheduler/issue_due_worker_spec.rb
@@ -12,8 +12,8 @@ describe MailScheduler::IssueDueWorker do
create(:issue, :opened, project: project, due_date: 2.days.from_now) # due on another day
create(:issue, :opened, due_date: Date.tomorrow) # different project
- expect_any_instance_of(NotificationService).to receive(:issue_due).with(issue1)
- expect_any_instance_of(NotificationService).to receive(:issue_due).with(issue2)
+ expect(worker.notification_service).to receive(:issue_due).with(issue1)
+ expect(worker.notification_service).to receive(:issue_due).with(issue2)
worker.perform(project.id)
end
diff --git a/spec/workers/mail_scheduler/notification_service_worker_spec.rb b/spec/workers/mail_scheduler/notification_service_worker_spec.rb
new file mode 100644
index 00000000000..f725c8763a0
--- /dev/null
+++ b/spec/workers/mail_scheduler/notification_service_worker_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe MailScheduler::NotificationServiceWorker do
+ let(:worker) { described_class.new }
+ let(:method) { 'new_key' }
+ set(:key) { create(:personal_key) }
+
+ def serialize(*args)
+ ActiveJob::Arguments.serialize(args)
+ end
+
+ describe '#perform' do
+ it 'deserializes arguments from global IDs' do
+ expect(worker.notification_service).to receive(method).with(key)
+
+ worker.perform(method, *serialize(key))
+ end
+
+ context 'when the arguments cannot be deserialized' do
+ it 'does nothing' do
+ expect(worker.notification_service).not_to receive(method)
+
+ worker.perform(method, key.to_global_id.to_s.succ)
+ end
+ end
+
+ context 'when the method is not a public method' do
+ it 'raises NoMethodError' do
+ expect { worker.perform('notifiable?', *serialize(key)) }.to raise_error(NoMethodError)
+ end
+ end
+ end
+
+ describe '.perform_async' do
+ it 'serializes arguments as global IDs when scheduling' do
+ Sidekiq::Testing.fake! do
+ described_class.perform_async(method, key)
+
+ expect(described_class.jobs.count).to eq(1)
+ expect(described_class.jobs.first).to include('args' => [method, *serialize(key)])
+ end
+ end
+ end
+end
diff --git a/spec/workers/namespaceless_project_destroy_worker_spec.rb b/spec/workers/namespaceless_project_destroy_worker_spec.rb
index 479d9396eca..eec110dfbfb 100644
--- a/spec/workers/namespaceless_project_destroy_worker_spec.rb
+++ b/spec/workers/namespaceless_project_destroy_worker_spec.rb
@@ -22,13 +22,11 @@ describe NamespacelessProjectDestroyWorker do
end
end
- # Only possible with schema 20180222043024 and lower.
- # Project#namespace_id has not null constraint since then
- context 'project has no namespace', :migration, schema: 20180222043024 do
- let!(:project) do
- project = build(:project, namespace_id: nil)
- project.save(validate: false)
- project
+ context 'project has no namespace' do
+ let!(:project) { create(:project) }
+
+ before do
+ allow_any_instance_of(Project).to receive(:namespace).and_return(nil)
end
context 'project not a fork of another project' do
@@ -61,8 +59,7 @@ describe NamespacelessProjectDestroyWorker do
let!(:parent_project) { create(:project) }
let(:project) do
namespaceless_project = fork_project(parent_project)
- namespaceless_project.namespace_id = nil
- namespaceless_project.save(validate: false)
+ namespaceless_project.save
namespaceless_project
end
diff --git a/vendor/assets/javascripts/peek.performance_bar.js b/vendor/assets/javascripts/peek.performance_bar.js
deleted file mode 100644
index 6ed86dce2f2..00000000000
--- a/vendor/assets/javascripts/peek.performance_bar.js
+++ /dev/null
@@ -1,182 +0,0 @@
-var PerformanceBar, ajaxStart, renderPerformanceBar, updateStatus;
-
-PerformanceBar = (function() {
- PerformanceBar.prototype.appInfo = null;
-
- PerformanceBar.prototype.width = null;
-
- PerformanceBar.formatTime = function(value) {
- if (value >= 1000) {
- return ((value / 1000).toFixed(3)) + "s";
- } else {
- return (value.toFixed(0)) + "ms";
- }
- };
-
- function PerformanceBar(options) {
- var k, v;
- if (options == null) {
- options = {};
- }
- this.el = $('#peek-view-performance-bar .performance-bar');
- for (k in options) {
- v = options[k];
- this[k] = v;
- }
- if (this.width == null) {
- this.width = this.el.width();
- }
- if (this.timing == null) {
- this.timing = window.performance.timing;
- }
- }
-
- PerformanceBar.prototype.render = function(serverTime) {
- var networkTime, perfNetworkTime;
- if (serverTime == null) {
- serverTime = 0;
- }
- this.el.empty();
- this.addBar('frontend', '#90d35b', 'domLoading', 'domInteractive');
- perfNetworkTime = this.timing.responseEnd - this.timing.requestStart;
- if (serverTime && serverTime <= perfNetworkTime) {
- networkTime = perfNetworkTime - serverTime;
- this.addBar('latency / receiving', '#f1faff', this.timing.requestStart + serverTime, this.timing.requestStart + serverTime + networkTime);
- this.addBar('app', '#90afcf', this.timing.requestStart, this.timing.requestStart + serverTime, this.appInfo);
- } else {
- this.addBar('backend', '#c1d7ee', 'requestStart', 'responseEnd');
- }
- this.addBar('tcp / ssl', '#45688e', 'connectStart', 'connectEnd');
- this.addBar('redirect', '#0c365e', 'redirectStart', 'redirectEnd');
- this.addBar('dns', '#082541', 'domainLookupStart', 'domainLookupEnd');
- return this.el;
- };
-
- PerformanceBar.prototype.isLoaded = function() {
- return this.timing.domInteractive;
- };
-
- PerformanceBar.prototype.start = function() {
- return this.timing.navigationStart;
- };
-
- PerformanceBar.prototype.end = function() {
- return this.timing.domInteractive;
- };
-
- PerformanceBar.prototype.total = function() {
- return this.end() - this.start();
- };
-
- PerformanceBar.prototype.addBar = function(name, color, start, end, info) {
- var bar, left, offset, time, title, width;
- if (typeof start === 'string') {
- start = this.timing[start];
- }
- if (typeof end === 'string') {
- end = this.timing[end];
- }
- if (!((start != null) && (end != null))) {
- return;
- }
- time = end - start;
- offset = start - this.start();
- left = this.mapH(offset);
- width = this.mapH(time);
- title = name + ": " + (PerformanceBar.formatTime(time));
- bar = $('<li></li>', {
- 'data-title': title,
- 'data-toggle': 'tooltip',
- 'data-container': 'body'
- });
- bar.css({
- width: width + "px",
- left: left + "px",
- background: color
- });
- return this.el.append(bar);
- };
-
- PerformanceBar.prototype.mapH = function(offset) {
- return offset * (this.width / this.total());
- };
-
- return PerformanceBar;
-
-})();
-
-renderPerformanceBar = function() {
- var bar, resp, span, time;
- resp = $('#peek-server_response_time');
- time = Math.round(resp.data('time') * 1000);
- bar = new PerformanceBar;
- bar.render(time);
- span = $('<span>', {
- 'data-toggle': 'tooltip',
- 'data-title': 'Total navigation time for this page.',
- 'data-container': 'body'
- }).text(PerformanceBar.formatTime(bar.total()));
- return updateStatus(span);
-};
-
-updateStatus = function(html) {
- return $('#serverstats').html(html);
-};
-
-ajaxStart = null;
-
-$(document).on('pjax:start page:fetch turbolinks:request-start', function(event) {
- return ajaxStart = event.timeStamp;
-});
-
-$(document).on('pjax:end page:load turbolinks:load', function(event, xhr) {
- var ajaxEnd, serverTime, total;
- if (ajaxStart == null) {
- return;
- }
- ajaxEnd = event.timeStamp;
- total = ajaxEnd - ajaxStart;
- serverTime = xhr ? parseInt(xhr.getResponseHeader('X-Runtime')) : 0;
- return setTimeout(function() {
- var bar, now, span, tech;
- now = new Date().getTime();
- bar = new PerformanceBar({
- timing: {
- requestStart: ajaxStart,
- responseEnd: ajaxEnd,
- domLoading: ajaxEnd,
- domInteractive: now
- },
- isLoaded: function() {
- return true;
- },
- start: function() {
- return ajaxStart;
- },
- end: function() {
- return now;
- }
- });
- bar.render(serverTime);
- if ($.fn.pjax != null) {
- tech = 'PJAX';
- } else {
- tech = 'Turbolinks';
- }
- span = $('<span>', {
- 'data-toggle': 'tooltip',
- 'data-title': tech + " navigation time",
- 'data-container': 'body'
- }).text(PerformanceBar.formatTime(total));
- updateStatus(span);
- return ajaxStart = null;
- }, 0);
-});
-
-$(function() {
- if (window.performance) {
- return renderPerformanceBar();
- } else {
- return $('#peek-view-performance-bar').remove();
- }
-});
diff --git a/yarn.lock b/yarn.lock
index 7aca6b0d427..1e6ffa5f524 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -58,17 +58,14 @@
version "1.18.0"
resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.18.0.tgz#7829f0e6de0647dace54c1fcd597ee3424afb233"
+"@sindresorhus/is@^0.7.0":
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd"
+
"@types/jquery@^2.0.40":
version "2.0.48"
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-2.0.48.tgz#3e90d8cde2d29015e5583017f7830cb3975b2eef"
-JSONStream@^1.0.3:
- version "1.3.2"
- resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.2.tgz#c102371b6ec3a7cf3b847ca00c20bb0fce4c6dea"
- dependencies:
- jsonparse "^1.2.0"
- through ">=2.2.7 <3"
-
abbrev@1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
@@ -96,13 +93,6 @@ acorn-jsx@^3.0.0:
dependencies:
acorn "^3.0.4"
-acorn-node@^1.2.0:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.3.0.tgz#5f86d73346743810ef1269b901dbcbded020861b"
- dependencies:
- acorn "^5.4.1"
- xtend "^4.0.1"
-
acorn@^3.0.4:
version "3.3.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
@@ -111,7 +101,7 @@ acorn@^4.0.3:
version "4.0.13"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787"
-acorn@^5.0.0, acorn@^5.2.1, acorn@^5.3.0, acorn@^5.4.1:
+acorn@^5.0.0, acorn@^5.2.1, acorn@^5.3.0:
version "5.4.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.4.1.tgz#fdc58d9d17f4a4e98d102ded826a9b9759125102"
@@ -222,9 +212,9 @@ ansi-styles@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
-ansi-styles@^3.2.0:
- version "3.2.0"
- resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88"
+ansi-styles@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
dependencies:
color-convert "^1.9.0"
@@ -368,7 +358,7 @@ assert-plus@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234"
-assert@^1.1.1, assert@^1.4.0:
+assert@^1.1.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91"
dependencies:
@@ -382,12 +372,6 @@ ast-types@0.x.x:
version "0.11.1"
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.11.1.tgz#5bb3a8d5ba292c3f4ae94d46df37afc30300b990"
-astw@^2.0.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/astw/-/astw-2.2.0.tgz#7bd41784d32493987aeb239b6b4e1c57a873b917"
- dependencies:
- acorn "^4.0.3"
-
async-each@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
@@ -400,16 +384,12 @@ async@1.x, async@^1.4.0, async@^1.5.2:
version "1.5.2"
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
-async@^2.1.2, async@^2.1.4, async@^2.4.1:
+async@^2.0.0, async@^2.1.2, async@^2.1.4, async@^2.4.1:
version "2.6.0"
resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4"
dependencies:
lodash "^4.14.0"
-async@~0.9.0:
- version "0.9.2"
- resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
-
async@~2.1.2:
version "2.1.5"
resolved "https://registry.yarnpkg.com/async/-/async-2.1.5.tgz#e587c68580994ac67fc56ff86d3ac56bdbe810bc"
@@ -662,13 +642,14 @@ babel-plugin-check-es2015-constants@^6.22.0:
dependencies:
babel-runtime "^6.22.0"
-babel-plugin-istanbul@^4.1.5:
- version "4.1.5"
- resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.5.tgz#6760cdd977f411d3e175bb064f2bc327d99b2b6e"
+babel-plugin-istanbul@^4.1.6:
+ version "4.1.6"
+ resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz#36c59b2192efce81c5b378321b74175add1c9a45"
dependencies:
+ babel-plugin-syntax-object-rest-spread "^6.13.0"
find-up "^2.1.0"
- istanbul-lib-instrument "^1.7.5"
- test-exclude "^4.1.1"
+ istanbul-lib-instrument "^1.10.1"
+ test-exclude "^4.2.1"
babel-plugin-rewire@^1.1.0:
version "1.1.0"
@@ -698,7 +679,7 @@ babel-plugin-syntax-exponentiation-operator@^6.8.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de"
-babel-plugin-syntax-object-rest-spread@^6.8.0:
+babel-plugin-syntax-object-rest-spread@^6.13.0, babel-plugin-syntax-object-rest-spread@^6.8.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5"
@@ -1025,7 +1006,7 @@ babel-register@^6.26.0:
mkdirp "^0.5.1"
source-map-support "^0.4.15"
-babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0:
+babel-runtime@^6.0.0, babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
dependencies:
@@ -1284,23 +1265,6 @@ brorand@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
-browser-pack@^6.0.1:
- version "6.0.4"
- resolved "https://registry.yarnpkg.com/browser-pack/-/browser-pack-6.0.4.tgz#9a73beb3b48f9e36868be007b64400102c04a99f"
- dependencies:
- JSONStream "^1.0.3"
- combine-source-map "~0.8.0"
- defined "^1.0.0"
- safe-buffer "^5.1.1"
- through2 "^2.0.0"
- umd "^3.0.0"
-
-browser-resolve@^1.11.0, browser-resolve@^1.7.0:
- version "1.11.2"
- resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.2.tgz#8ff09b0a2c421718a1051c260b32e48f442938ce"
- dependencies:
- resolve "1.1.7"
-
browserify-aes@^1.0.0, browserify-aes@^1.0.4:
version "1.1.1"
resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.1.1.tgz#38b7ab55edb806ff2dcda1a7f1620773a477c49f"
@@ -1353,64 +1317,6 @@ browserify-zlib@^0.1.4:
dependencies:
pako "~0.2.0"
-browserify-zlib@~0.2.0:
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f"
- dependencies:
- pako "~1.0.5"
-
-browserify@^14.5.0:
- version "14.5.0"
- resolved "https://registry.yarnpkg.com/browserify/-/browserify-14.5.0.tgz#0bbbce521acd6e4d1d54d8e9365008efb85a9cc5"
- dependencies:
- JSONStream "^1.0.3"
- assert "^1.4.0"
- browser-pack "^6.0.1"
- browser-resolve "^1.11.0"
- browserify-zlib "~0.2.0"
- buffer "^5.0.2"
- cached-path-relative "^1.0.0"
- concat-stream "~1.5.1"
- console-browserify "^1.1.0"
- constants-browserify "~1.0.0"
- crypto-browserify "^3.0.0"
- defined "^1.0.0"
- deps-sort "^2.0.0"
- domain-browser "~1.1.0"
- duplexer2 "~0.1.2"
- events "~1.1.0"
- glob "^7.1.0"
- has "^1.0.0"
- htmlescape "^1.1.0"
- https-browserify "^1.0.0"
- inherits "~2.0.1"
- insert-module-globals "^7.0.0"
- labeled-stream-splicer "^2.0.0"
- module-deps "^4.0.8"
- os-browserify "~0.3.0"
- parents "^1.0.1"
- path-browserify "~0.0.0"
- process "~0.11.0"
- punycode "^1.3.2"
- querystring-es3 "~0.2.0"
- read-only-stream "^2.0.0"
- readable-stream "^2.0.2"
- resolve "^1.1.4"
- shasum "^1.0.0"
- shell-quote "^1.6.1"
- stream-browserify "^2.0.0"
- stream-http "^2.0.0"
- string_decoder "~1.0.0"
- subarg "^1.0.0"
- syntax-error "^1.1.1"
- through2 "^2.0.0"
- timers-browserify "^1.0.1"
- tty-browserify "~0.0.0"
- url "~0.11.0"
- util "~0.10.1"
- vm-browserify "~0.0.1"
- xtend "^4.0.0"
-
browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6:
version "1.7.7"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-1.7.7.tgz#0bd76704258be829b2398bb50e4b62d1a166b0b9"
@@ -1438,13 +1344,6 @@ buffer@^4.3.0:
ieee754 "^1.1.4"
isarray "^1.0.0"
-buffer@^5.0.2:
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.1.0.tgz#c913e43678c7cb7c8bd16afbcddb6c5505e8f9fe"
- dependencies:
- base64-js "^1.0.2"
- ieee754 "^1.1.4"
-
buildmail@4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/buildmail/-/buildmail-4.0.1.tgz#877f7738b78729871c9a105e3b837d2be11a7a72"
@@ -1505,9 +1404,17 @@ cache-base@^1.0.1:
union-value "^1.0.0"
unset-value "^1.0.0"
-cached-path-relative@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.0.1.tgz#d09c4b52800aa4c078e2dd81a869aac90d2e54e7"
+cacheable-request@^2.1.1:
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-2.1.4.tgz#0d808801b6342ad33c91df9d0b44dc09b91e5c3d"
+ dependencies:
+ clone-response "1.0.2"
+ get-stream "3.0.0"
+ http-cache-semantics "3.8.1"
+ keyv "3.0.0"
+ lowercase-keys "1.0.0"
+ normalize-url "2.0.1"
+ responselike "1.0.2"
caller-path@^0.1.0:
version "0.1.0"
@@ -1588,13 +1495,13 @@ chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
strip-ansi "^3.0.0"
supports-color "^2.0.0"
-chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1:
- version "2.3.1"
- resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.1.tgz#523fe2678aec7b04e8041909292fe8b17059b796"
+chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.3.2, chalk@^2.4.1:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e"
dependencies:
- ansi-styles "^3.2.0"
+ ansi-styles "^3.2.1"
escape-string-regexp "^1.0.5"
- supports-color "^5.2.0"
+ supports-color "^5.3.0"
chardet@^0.4.0:
version "0.4.2"
@@ -1723,6 +1630,12 @@ cliui@^3.2.0:
strip-ansi "^3.0.1"
wrap-ansi "^2.0.0"
+clone-response@1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b"
+ dependencies:
+ mimic-response "^1.0.0"
+
clone@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149"
@@ -1794,24 +1707,6 @@ combine-lists@^1.0.0:
dependencies:
lodash "^4.5.0"
-combine-source-map@~0.7.1:
- version "0.7.2"
- resolved "https://registry.yarnpkg.com/combine-source-map/-/combine-source-map-0.7.2.tgz#0870312856b307a87cc4ac486f3a9a62aeccc09e"
- dependencies:
- convert-source-map "~1.1.0"
- inline-source-map "~0.6.0"
- lodash.memoize "~3.0.3"
- source-map "~0.5.3"
-
-combine-source-map@~0.8.0:
- version "0.8.0"
- resolved "https://registry.yarnpkg.com/combine-source-map/-/combine-source-map-0.8.0.tgz#a58d0df042c186fcf822a8e8015f5450d2d79a8b"
- dependencies:
- convert-source-map "~1.1.0"
- inline-source-map "~0.6.0"
- lodash.memoize "~3.0.3"
- source-map "~0.5.3"
-
combined-stream@1.0.6, combined-stream@^1.0.5, combined-stream@~1.0.5:
version "1.0.6"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818"
@@ -1878,14 +1773,6 @@ concat-stream@^1.5.0, concat-stream@^1.5.2:
readable-stream "^2.2.2"
typedarray "^0.0.6"
-concat-stream@~1.5.0, concat-stream@~1.5.1:
- version "1.5.2"
- resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.2.tgz#708978624d856af41a5a741defdd261da752c266"
- dependencies:
- inherits "~2.0.1"
- readable-stream "~2.0.0"
- typedarray "~0.0.5"
-
configstore@^3.0.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.1.tgz#094ee662ab83fad9917678de114faaea8fcdca90"
@@ -1926,7 +1813,7 @@ consolidate@^0.14.0:
dependencies:
bluebird "^3.1.1"
-constants-browserify@^1.0.0, constants-browserify@~1.0.0:
+constants-browserify@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
@@ -1946,10 +1833,6 @@ convert-source-map@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5"
-convert-source-map@~1.1.0:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.1.3.tgz#4829c877e9fe49b3161f3bf3673888e204699860"
-
cookie-signature@1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
@@ -1999,9 +1882,10 @@ core-util-is@1.0.2, core-util-is@~1.0.0:
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
cosmiconfig@^2.1.0, cosmiconfig@^2.1.1:
- version "2.1.1"
- resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-2.1.1.tgz#817f2c2039347a1e9bf7d090c0923e53f749ca82"
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-2.2.2.tgz#6173cebd56fac042c1f4390edf7af6c07c7cb892"
dependencies:
+ is-directory "^0.3.1"
js-yaml "^3.4.3"
minimist "^1.2.0"
object-assign "^4.1.0"
@@ -2068,7 +1952,7 @@ cryptiles@3.x.x:
dependencies:
boom "5.x.x"
-crypto-browserify@^3.0.0, crypto-browserify@^3.11.0:
+crypto-browserify@^3.11.0:
version "3.12.0"
resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"
dependencies:
@@ -2373,7 +2257,7 @@ decode-uri-component@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
-decompress-response@^3.2.0:
+decompress-response@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3"
dependencies:
@@ -2474,15 +2358,6 @@ depd@1.1.1, depd@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359"
-deps-sort@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/deps-sort/-/deps-sort-2.0.0.tgz#091724902e84658260eb910748cccd1af6e21fb5"
- dependencies:
- JSONStream "^1.0.3"
- shasum "^1.0.0"
- subarg "^1.0.0"
- through2 "^2.0.0"
-
des.js@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc"
@@ -2515,13 +2390,6 @@ detect-port-alt@1.1.5:
address "^1.0.1"
debug "^2.6.0"
-detective@^4.0.0:
- version "4.7.1"
- resolved "https://registry.yarnpkg.com/detective/-/detective-4.7.1.tgz#0eca7314338442febb6d65da54c10bb1c82b246e"
- dependencies:
- acorn "^5.2.1"
- defined "^1.0.0"
-
di@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c"
@@ -2596,7 +2464,7 @@ dom-serializer@0:
domelementtype "~1.1.1"
entities "~1.1.1"
-domain-browser@^1.1.1, domain-browser@~1.1.0:
+domain-browser@^1.1.1:
version "1.1.7"
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc"
@@ -2635,12 +2503,6 @@ dropzone@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/dropzone/-/dropzone-4.2.0.tgz#fbe7acbb9918e0706489072ef663effeef8a79f3"
-duplexer2@^0.1.2, duplexer2@~0.1.0, duplexer2@~0.1.2:
- version "0.1.4"
- resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1"
- dependencies:
- readable-stream "^2.0.2"
-
duplexer3@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
@@ -2840,7 +2702,7 @@ es6-set@~0.1.5:
es6-symbol "3.1.1"
event-emitter "~0.3.5"
-es6-symbol@3, es6-symbol@3.1.1, es6-symbol@^3.1, es6-symbol@~3.1, es6-symbol@~3.1.1:
+es6-symbol@3, es6-symbol@3.1.1, es6-symbol@^3.1, es6-symbol@^3.1.1, es6-symbol@~3.1, es6-symbol@~3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77"
dependencies:
@@ -3104,7 +2966,7 @@ eventemitter3@1.x.x:
version "1.2.0"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508"
-events@^1.0.0, events@~1.1.0:
+events@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
@@ -3498,7 +3360,7 @@ fresh@0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
-from2@^2.1.0:
+from2@^2.1.0, from2@^2.1.1:
version "2.3.0"
resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af"
dependencies:
@@ -3598,7 +3460,7 @@ get-stdin@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
-get-stream@^3.0.0:
+get-stream@3.0.0, get-stream@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
@@ -3653,7 +3515,7 @@ glob@^5.0.15:
once "^1.3.0"
path-is-absolute "^1.0.0"
-glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.0, glob@^7.1.1, glob@^7.1.2:
+glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
dependencies:
@@ -3750,23 +3612,26 @@ got@^6.7.1:
unzip-response "^2.0.1"
url-parse-lax "^1.0.0"
-got@^7.1.0:
- version "7.1.0"
- resolved "https://registry.yarnpkg.com/got/-/got-7.1.0.tgz#05450fd84094e6bbea56f451a43a9c289166385a"
+got@^8.0.3:
+ version "8.3.0"
+ resolved "https://registry.yarnpkg.com/got/-/got-8.3.0.tgz#6ba26e75f8a6cc4c6b3eb1fe7ce4fec7abac8533"
dependencies:
- decompress-response "^3.2.0"
+ "@sindresorhus/is" "^0.7.0"
+ cacheable-request "^2.1.1"
+ decompress-response "^3.3.0"
duplexer3 "^0.1.4"
get-stream "^3.0.0"
- is-plain-obj "^1.1.0"
- is-retry-allowed "^1.0.0"
- is-stream "^1.0.0"
+ into-stream "^3.1.0"
+ is-retry-allowed "^1.1.0"
isurl "^1.0.0-alpha5"
lowercase-keys "^1.0.0"
- p-cancelable "^0.3.0"
- p-timeout "^1.1.1"
- safe-buffer "^5.0.1"
- timed-out "^4.0.0"
- url-parse-lax "^1.0.0"
+ mimic-response "^1.0.0"
+ p-cancelable "^0.4.0"
+ p-timeout "^2.0.1"
+ pify "^3.0.0"
+ safe-buffer "^5.1.1"
+ timed-out "^4.0.1"
+ url-parse-lax "^3.0.0"
url-to-options "^1.0.1"
graceful-fs@^4.1.11, graceful-fs@^4.1.2:
@@ -3869,6 +3734,10 @@ has-symbol-support-x@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/has-symbol-support-x/-/has-symbol-support-x-1.3.0.tgz#588bd6927eaa0e296afae24160659167fc2be4f8"
+has-symbols@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44"
+
has-to-string-tag-x@^1.2.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/has-to-string-tag-x/-/has-to-string-tag-x-1.3.0.tgz#78e3d98c3c0ec9413e970eb8d766249a1e13058f"
@@ -3906,7 +3775,7 @@ has-values@^1.0.0:
is-number "^3.0.0"
kind-of "^4.0.0"
-has@^1.0.0, has@^1.0.1:
+has@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28"
dependencies:
@@ -4015,10 +3884,6 @@ html-entities@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.0.tgz#41948caf85ce82fed36e4e6a0ed371a6664379e2"
-htmlescape@^1.1.0:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/htmlescape/-/htmlescape-1.1.1.tgz#3a03edc2214bca3b66424a3e7959349509cb0351"
-
htmlparser2@^3.8.2, htmlparser2@^3.9.0:
version "3.9.2"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338"
@@ -4030,6 +3895,10 @@ htmlparser2@^3.8.2, htmlparser2@^3.9.0:
inherits "^2.0.1"
readable-stream "^2.0.2"
+http-cache-semantics@3.8.1:
+ version "3.8.1"
+ resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2"
+
http-deceiver@^1.2.7:
version "1.2.7"
resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87"
@@ -4098,10 +3967,6 @@ https-browserify@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82"
-https-browserify@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
-
https-proxy-agent@1:
version "1.0.0"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-1.0.0.tgz#35f7da6c48ce4ddbfa264891ac593ee5ff8671e6"
@@ -4211,12 +4076,6 @@ ini@^1.3.4, ini@~1.3.0:
version "1.3.5"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
-inline-source-map@~0.6.0:
- version "0.6.2"
- resolved "https://registry.yarnpkg.com/inline-source-map/-/inline-source-map-0.6.2.tgz#f9393471c18a79d1724f863fa38b586370ade2a5"
- dependencies:
- source-map "~0.5.3"
-
inquirer@3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9"
@@ -4254,19 +4113,6 @@ inquirer@^0.12.0:
strip-ansi "^3.0.0"
through "^2.3.6"
-insert-module-globals@^7.0.0:
- version "7.0.1"
- resolved "https://registry.yarnpkg.com/insert-module-globals/-/insert-module-globals-7.0.1.tgz#c03bf4e01cb086d5b5e5ace8ad0afe7889d638c3"
- dependencies:
- JSONStream "^1.0.3"
- combine-source-map "~0.7.1"
- concat-stream "~1.5.1"
- is-buffer "^1.1.0"
- lexical-scope "^1.2.0"
- process "~0.11.0"
- through2 "^2.0.0"
- xtend "^4.0.0"
-
internal-ip@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-1.2.0.tgz#ae9fbf93b984878785d50a8de1b356956058cf5c"
@@ -4277,6 +4123,13 @@ interpret@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.1.tgz#d579fb7f693b858004947af39fa0db49f795602c"
+into-stream@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/into-stream/-/into-stream-3.1.0.tgz#96fb0a936c12babd6ff1752a17d05616abd094c6"
+ dependencies:
+ from2 "^2.1.1"
+ p-is-promise "^1.1.0"
+
invariant@^2.2.0, invariant@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360"
@@ -4332,7 +4185,7 @@ is-binary-path@^1.0.0:
dependencies:
binary-extensions "^1.0.0"
-is-buffer@^1.1.0, is-buffer@^1.1.5:
+is-buffer@^1.1.5:
version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
@@ -4378,6 +4231,10 @@ is-descriptor@^1.0.0, is-descriptor@^1.0.2:
is-data-descriptor "^1.0.0"
kind-of "^6.0.2"
+is-directory@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1"
+
is-dotfile@^1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1"
@@ -4515,7 +4372,7 @@ is-path-inside@^1.0.0:
dependencies:
path-is-inside "^1.0.1"
-is-plain-obj@^1.0.0, is-plain-obj@^1.1.0:
+is-plain-obj@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
@@ -4563,7 +4420,7 @@ is-resolvable@^1.0.0:
dependencies:
tryit "^1.0.1"
-is-retry-allowed@^1.0.0:
+is-retry-allowed@^1.0.0, is-retry-allowed@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34"
@@ -4611,7 +4468,7 @@ is-wsl@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d"
-isarray@0.0.1, isarray@~0.0.1:
+isarray@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
@@ -4665,13 +4522,29 @@ istanbul-lib-coverage@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.1.tgz#73bfb998885299415c93d38a3e9adf784a77a9da"
+istanbul-lib-coverage@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.0.tgz#f7d8f2e42b97e37fe796114cb0f9d68b5e3a4341"
+
istanbul-lib-hook@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.1.0.tgz#8538d970372cb3716d53e55523dd54b557a8d89b"
dependencies:
append-transform "^0.4.0"
-istanbul-lib-instrument@^1.7.5, istanbul-lib-instrument@^1.9.1:
+istanbul-lib-instrument@^1.10.1:
+ version "1.10.1"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.1.tgz#724b4b6caceba8692d3f1f9d0727e279c401af7b"
+ dependencies:
+ babel-generator "^6.18.0"
+ babel-template "^6.16.0"
+ babel-traverse "^6.18.0"
+ babel-types "^6.18.0"
+ babylon "^6.18.0"
+ istanbul-lib-coverage "^1.2.0"
+ semver "^5.3.0"
+
+istanbul-lib-instrument@^1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.9.1.tgz#250b30b3531e5d3251299fdd64b0b2c9db6b558e"
dependencies:
@@ -4772,13 +4645,20 @@ js-tokens@^3.0.0, js-tokens@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
-js-yaml@3.x, js-yaml@^3.4.3, js-yaml@^3.5.1, js-yaml@^3.7.0:
+js-yaml@3.x, js-yaml@^3.5.1, js-yaml@^3.7.0:
version "3.9.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.9.1.tgz#08775cebdfdd359209f0d2acd383c8f86a6904a0"
dependencies:
argparse "^1.0.7"
esprima "^4.0.0"
+js-yaml@^3.4.3:
+ version "3.11.0"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef"
+ dependencies:
+ argparse "^1.0.7"
+ esprima "^4.0.0"
+
js-yaml@~3.7.0:
version "3.7.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.7.0.tgz#5c967ddd837a9bfdca5f2de84253abe8a1c03b80"
@@ -4798,6 +4678,10 @@ jsesc@~0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
+json-buffer@3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898"
+
json-loader@^0.5.4:
version "0.5.7"
resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d"
@@ -4816,12 +4700,6 @@ json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1:
dependencies:
jsonify "~0.0.0"
-json-stable-stringify@~0.0.0:
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz#611c23e814db375527df851193db59dd2af27f45"
- dependencies:
- jsonify "~0.0.0"
-
json-stringify-safe@5.0.x, json-stringify-safe@~5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
@@ -4838,10 +4716,6 @@ jsonify@~0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
-jsonparse@^1.2.0:
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
-
jsonpointer@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9"
@@ -4876,9 +4750,9 @@ karma-chrome-launcher@^2.2.0:
fs-access "^1.0.0"
which "^1.2.1"
-karma-coverage-istanbul-reporter@^1.4.1:
- version "1.4.1"
- resolved "https://registry.yarnpkg.com/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-1.4.1.tgz#2b42d145ddbb4868d85d52888c495a21c61d873c"
+karma-coverage-istanbul-reporter@^1.4.2:
+ version "1.4.2"
+ resolved "https://registry.yarnpkg.com/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-1.4.2.tgz#a8d0c8815c7d6f6cea15a394a7c4b39ef151a939"
dependencies:
istanbul-api "^1.1.14"
minimatch "^3.0.4"
@@ -4901,23 +4775,23 @@ karma-sourcemap-loader@^0.3.7:
dependencies:
graceful-fs "^4.1.2"
-karma-webpack@2.0.7:
- version "2.0.7"
- resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-2.0.7.tgz#dc3a492b478f10e8e3ccb9f58171b623f7070a1f"
+karma-webpack@3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-3.0.0.tgz#bf009c5b73c667c11c015717e9e520f581317c44"
dependencies:
- async "~0.9.0"
- loader-utils "^0.2.5"
- lodash "^3.8.0"
+ async "^2.0.0"
+ babel-runtime "^6.0.0"
+ loader-utils "^1.0.0"
+ lodash "^4.0.0"
source-map "^0.5.6"
- webpack-dev-middleware "^1.12.0"
+ webpack-dev-middleware "^2.0.6"
-karma@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/karma/-/karma-2.0.0.tgz#a02698dd7f0f05ff5eb66ab8f65582490b512e58"
+karma@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/karma/-/karma-2.0.2.tgz#4d2db9402850a66551fa784b0164fb0824ed8c4b"
dependencies:
bluebird "^3.3.0"
body-parser "^1.16.1"
- browserify "^14.5.0"
chokidar "^1.4.1"
colors "^1.1.0"
combine-lists "^1.0.0"
@@ -4942,7 +4816,7 @@ karma@^2.0.0:
socket.io "2.0.4"
source-map "^0.6.1"
tmp "0.0.33"
- useragent "^2.1.12"
+ useragent "2.2.1"
katex@^0.8.3:
version "0.8.3"
@@ -4950,6 +4824,12 @@ katex@^0.8.3:
dependencies:
match-at "^0.1.0"
+keyv@3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.0.0.tgz#44923ba39e68b12a7cec7df6c3268c031f2ef373"
+ dependencies:
+ json-buffer "3.0.0"
+
killable@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.0.tgz#da8b84bd47de5395878f95d64d02f2449fe05e6b"
@@ -4974,14 +4854,6 @@ kind-of@^6.0.0, kind-of@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
-labeled-stream-splicer@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/labeled-stream-splicer/-/labeled-stream-splicer-2.0.0.tgz#a52e1d138024c00b86b1c0c91f677918b8ae0a59"
- dependencies:
- inherits "^2.0.1"
- isarray "~0.0.1"
- stream-splicer "^2.0.0"
-
latest-version@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15"
@@ -5011,12 +4883,6 @@ levn@^0.3.0, levn@~0.3.0:
prelude-ls "~1.1.2"
type-check "~0.3.2"
-lexical-scope@^1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/lexical-scope/-/lexical-scope-1.2.0.tgz#fcea5edc704a4b3a8796cdca419c3a0afaf22df4"
- dependencies:
- astw "^2.0.0"
-
libbase64@0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/libbase64/-/libbase64-0.1.0.tgz#62351a839563ac5ff5bd26f12f60e9830bb751e6"
@@ -5062,7 +4928,7 @@ loader-runner@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2"
-loader-utils@^0.2.15, loader-utils@^0.2.5:
+loader-utils@^0.2.15:
version "0.2.16"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.16.tgz#f08632066ed8282835dff88dfb52704765adee6d"
dependencies:
@@ -5162,10 +5028,6 @@ lodash.memoize@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
-lodash.memoize@~3.0.3:
- version "3.0.4"
- resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-3.0.4.tgz#2dcbd2c287cbc0a55cc42328bd0c736150d53e3f"
-
lodash.mergewith@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz#150cf0a16791f5903b8891eab154609274bdea55"
@@ -5193,10 +5055,6 @@ lodash@4.17.4:
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
-lodash@^3.8.0:
- version "3.10.1"
- resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
-
lodash@^4.0.0, lodash@^4.11.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0:
version "4.17.5"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
@@ -5238,6 +5096,13 @@ loglevel@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.4.1.tgz#95b383f91a3c2756fd4ab093667e4309161f2bcd"
+loglevelnext@^1.0.1:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/loglevelnext/-/loglevelnext-1.0.5.tgz#36fc4f5996d6640f539ff203ba819641680d75a2"
+ dependencies:
+ es6-symbol "^3.1.1"
+ object.assign "^4.1.0"
+
longest@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
@@ -5248,18 +5113,22 @@ loose-envify@^1.0.0:
dependencies:
js-tokens "^3.0.0"
-loud-rejection@^1.0.0:
+loud-rejection@^1.0.0, loud-rejection@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f"
dependencies:
currently-unhandled "^0.4.1"
signal-exit "^3.0.0"
-lowercase-keys@^1.0.0:
+lowercase-keys@1.0.0, lowercase-keys@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306"
-lru-cache@4.1.x, lru-cache@^4.0.1, lru-cache@^4.1.1:
+lru-cache@2.2.x:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.2.4.tgz#6c658619becf14031d0d0b594b16042ce4dc063d"
+
+lru-cache@^4.0.1, lru-cache@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55"
dependencies:
@@ -5418,6 +5287,24 @@ micromatch@^3.1.4:
snapdragon "^0.8.1"
to-regex "^3.0.1"
+micromatch@^3.1.8:
+ version "3.1.10"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
+ dependencies:
+ arr-diff "^4.0.0"
+ array-unique "^0.3.2"
+ braces "^2.3.1"
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ extglob "^2.0.4"
+ fragment-cache "^0.2.1"
+ kind-of "^6.0.2"
+ nanomatch "^1.2.9"
+ object.pick "^1.3.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.2"
+
miller-rabin@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"
@@ -5443,6 +5330,10 @@ mime@^1.3.4, mime@^1.4.1, mime@^1.5.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
+mime@^2.1.0:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-2.3.1.tgz#b1621c54d63b97c47d3cfe7f7215f7d64517c369"
+
mimic-fn@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18"
@@ -5475,7 +5366,7 @@ minimist@0.0.8:
version "0.0.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
-minimist@^1.1.0, minimist@^1.1.3, minimist@^1.2.0:
+minimist@^1.1.3, minimist@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
@@ -5511,26 +5402,6 @@ mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkd
dependencies:
minimist "0.0.8"
-module-deps@^4.0.8:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/module-deps/-/module-deps-4.1.1.tgz#23215833f1da13fd606ccb8087b44852dcb821fd"
- dependencies:
- JSONStream "^1.0.3"
- browser-resolve "^1.7.0"
- cached-path-relative "^1.0.0"
- concat-stream "~1.5.0"
- defined "^1.0.0"
- detective "^4.0.0"
- duplexer2 "^0.1.2"
- inherits "^2.0.1"
- parents "^1.0.0"
- readable-stream "^2.0.2"
- resolve "^1.1.3"
- stream-combiner2 "^1.1.1"
- subarg "^1.0.0"
- through2 "^2.0.0"
- xtend "^4.0.0"
-
moment@2.x, moment@^2.18.1:
version "2.19.2"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.2.tgz#8a7f774c95a64550b4c7ebd496683908f9419dbe"
@@ -5799,6 +5670,14 @@ normalize-range@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
+normalize-url@2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-2.0.1.tgz#835a9da1551fa26f70e92329069a23aa6574d7e6"
+ dependencies:
+ prepend-http "^2.0.0"
+ query-string "^5.0.1"
+ sort-keys "^2.0.0"
+
normalize-url@^1.4.0:
version "1.9.1"
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c"
@@ -5855,7 +5734,7 @@ object-copy@^0.1.0:
define-property "^0.2.5"
kind-of "^3.0.3"
-object-keys@^1.0.8:
+object-keys@^1.0.11, object-keys@^1.0.8:
version "1.0.11"
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d"
@@ -5865,6 +5744,15 @@ object-visit@^1.0.0:
dependencies:
isobject "^3.0.0"
+object.assign@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
+ dependencies:
+ define-properties "^1.1.2"
+ function-bind "^1.1.1"
+ has-symbols "^1.0.0"
+ object-keys "^1.0.11"
+
object.omit@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa"
@@ -5946,10 +5834,6 @@ os-browserify@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.2.1.tgz#63fc4ccee5d2d7763d26bbf8601078e6c2e0044f"
-os-browserify@~0.3.0:
- version "0.3.0"
- resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
-
os-homedir@^1.0.0, os-homedir@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
@@ -5979,14 +5863,18 @@ osenv@^0.1.4:
os-homedir "^1.0.0"
os-tmpdir "^1.0.0"
-p-cancelable@^0.3.0:
- version "0.3.0"
- resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.3.0.tgz#b9e123800bcebb7ac13a479be195b507b98d30fa"
+p-cancelable@^0.4.0:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.4.1.tgz#35f363d67d52081c8d9585e37bcceb7e0bbcb2a0"
p-finally@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
+p-is-promise@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e"
+
p-limit@^1.0.0, p-limit@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.2.0.tgz#0e92b6bedcb59f022c13d0f1949dc82d15909f1c"
@@ -6003,9 +5891,9 @@ p-map@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.1.1.tgz#05f5e4ae97a068371bc2a5cc86bfbdbc19c4ae7a"
-p-timeout@^1.1.1:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-1.2.0.tgz#9820f99434c5817868b4f34809ee5291660d5b6c"
+p-timeout@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-2.0.1.tgz#d8dd1979595d2dc0139e1fe46b8b646cb3cdf038"
dependencies:
p-finally "^1.0.0"
@@ -6050,7 +5938,7 @@ pako@~0.2.0:
version "0.2.9"
resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75"
-pako@~1.0.2, pako@~1.0.5:
+pako@~1.0.2:
version "1.0.6"
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258"
@@ -6062,12 +5950,6 @@ parallel-transform@^1.1.0:
inherits "^2.0.3"
readable-stream "^2.1.5"
-parents@^1.0.0, parents@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/parents/-/parents-1.0.1.tgz#fedd4d2bf193a77745fe71e371d73c3307d9c751"
- dependencies:
- path-platform "~0.11.15"
-
parse-asn1@^5.0.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.0.tgz#37c4f9b7ed3ab65c74817b5f2480937fbf97c712"
@@ -6117,7 +5999,7 @@ pascalcase@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
-path-browserify@0.0.0, path-browserify@~0.0.0:
+path-browserify@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a"
@@ -6151,10 +6033,6 @@ path-parse@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1"
-path-platform@~0.11.15:
- version "0.11.15"
- resolved "https://registry.yarnpkg.com/path-platform/-/path-platform-0.11.15.tgz#e864217f74c36850f0852b78dc7bf7d4a5721bf2"
-
path-proxy@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/path-proxy/-/path-proxy-1.0.0.tgz#18e8a36859fc9d2f1a53b48dee138543c020de5e"
@@ -6528,7 +6406,7 @@ postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0
source-map "^0.5.6"
supports-color "^3.2.3"
-postcss@^6.0.1, postcss@^6.0.14, postcss@^6.0.8:
+postcss@^6.0.1, postcss@^6.0.14:
version "6.0.19"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.19.tgz#76a78386f670b9d9494a655bf23ac012effd1555"
dependencies:
@@ -6536,6 +6414,14 @@ postcss@^6.0.1, postcss@^6.0.14, postcss@^6.0.8:
source-map "^0.6.1"
supports-color "^5.2.0"
+postcss@^6.0.8:
+ version "6.0.21"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.21.tgz#8265662694eddf9e9a5960db6da33c39e4cd069d"
+ dependencies:
+ chalk "^2.3.2"
+ source-map "^0.6.1"
+ supports-color "^5.3.0"
+
prelude-ls@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
@@ -6544,14 +6430,22 @@ prepend-http@^1.0.0, prepend-http@^1.0.1:
version "1.0.4"
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
+prepend-http@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
+
preserve@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
-prettier@1.11.1, prettier@^1.7.0:
+prettier@1.11.1:
version "1.11.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.11.1.tgz#61e43fc4cd44e68f2b0dfc2c38cd4bb0fccdcc75"
+prettier@^1.7.0:
+ version "1.12.1"
+ resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.12.1.tgz#c1ad20e803e7749faf905a409d2367e06bbe7325"
+
prismjs@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.6.0.tgz#118d95fb7a66dba2272e343b345f5236659db365"
@@ -6651,7 +6545,7 @@ punycode@1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
-punycode@1.4.1, punycode@^1.2.4, punycode@^1.3.2, punycode@^1.4.1:
+punycode@1.4.1, punycode@^1.2.4, punycode@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
@@ -6686,7 +6580,15 @@ query-string@^4.1.0:
object-assign "^4.1.0"
strict-uri-encode "^1.0.0"
-querystring-es3@^0.2.0, querystring-es3@~0.2.0:
+query-string@^5.0.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.1.1.tgz#a78c012b71c17e05f2e3fa2319dd330682efb3cb"
+ dependencies:
+ decode-uri-component "^0.2.0"
+ object-assign "^4.1.0"
+ strict-uri-encode "^1.0.0"
+
+querystring-es3@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
@@ -6785,12 +6687,6 @@ react-error-overlay@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-4.0.0.tgz#d198408a85b4070937a98667f500c832f86bd5d4"
-read-only-stream@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/read-only-stream/-/read-only-stream-2.0.0.tgz#2724fd6a8113d73764ac288d4386270c1dbf17f0"
- dependencies:
- readable-stream "^2.0.2"
-
read-pkg-up@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02"
@@ -6842,7 +6738,7 @@ readable-stream@1.1.x, "readable-stream@1.x >=1.1.9":
isarray "0.0.1"
string_decoder "~0.10.x"
-readable-stream@~2.0.0, readable-stream@~2.0.5, readable-stream@~2.0.6:
+readable-stream@~2.0.5, readable-stream@~2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e"
dependencies:
@@ -6941,7 +6837,7 @@ regex-cache@^0.4.2:
dependencies:
is-equal-shallow "^0.1.3"
-regex-not@^1.0.0:
+regex-not@^1.0.0, regex-not@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
dependencies:
@@ -7150,16 +7046,28 @@ resolve-url@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
-resolve@1.1.7, resolve@1.1.x:
+resolve@1.1.x:
version "1.1.7"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
-resolve@^1.1.3, resolve@^1.1.4, resolve@^1.1.6, resolve@^1.2.0, resolve@^1.4.0:
+resolve@^1.1.6, resolve@^1.2.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36"
dependencies:
path-parse "^1.0.5"
+resolve@^1.4.0:
+ version "1.7.1"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.7.1.tgz#aadd656374fd298aee895bc026b8297418677fd3"
+ dependencies:
+ path-parse "^1.0.5"
+
+responselike@1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7"
+ dependencies:
+ lowercase-keys "^1.0.0"
+
restore-cursor@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541"
@@ -7387,20 +7295,13 @@ setprototypeof@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656"
-sha.js@^2.4.0, sha.js@^2.4.8, sha.js@~2.4.4:
+sha.js@^2.4.0, sha.js@^2.4.8:
version "2.4.10"
resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.10.tgz#b1fde5cd7d11a5626638a07c604ab909cfa31f9b"
dependencies:
inherits "^2.0.1"
safe-buffer "^5.0.1"
-shasum@^1.0.0:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/shasum/-/shasum-1.0.2.tgz#e7012310d8f417f4deb5712150e5678b87ae565f"
- dependencies:
- json-stable-stringify "~0.0.0"
- sha.js "~2.4.4"
-
shebang-command@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
@@ -7411,7 +7312,7 @@ shebang-regex@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
-shell-quote@1.6.1, shell-quote@^1.6.1:
+shell-quote@1.6.1:
version "1.6.1"
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.6.1.tgz#f4781949cce402697127430ea3b3c5476f481767"
dependencies:
@@ -7583,6 +7484,12 @@ sort-keys@^1.0.0:
dependencies:
is-plain-obj "^1.0.0"
+sort-keys@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128"
+ dependencies:
+ is-plain-obj "^1.0.0"
+
source-list-map@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085"
@@ -7734,20 +7641,13 @@ statuses@~1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e"
-stream-browserify@^2.0.0, stream-browserify@^2.0.1:
+stream-browserify@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db"
dependencies:
inherits "~2.0.1"
readable-stream "^2.0.2"
-stream-combiner2@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/stream-combiner2/-/stream-combiner2-1.1.1.tgz#fb4d8a1420ea362764e21ad4780397bebcb41cbe"
- dependencies:
- duplexer2 "~0.1.0"
- readable-stream "^2.0.2"
-
stream-combiner@~0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14"
@@ -7761,7 +7661,7 @@ stream-each@^1.1.0:
end-of-stream "^1.1.0"
stream-shift "^1.0.0"
-stream-http@^2.0.0, stream-http@^2.3.1:
+stream-http@^2.3.1:
version "2.8.0"
resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.0.tgz#fd86546dac9b1c91aff8fc5d287b98fafb41bc10"
dependencies:
@@ -7775,13 +7675,6 @@ stream-shift@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952"
-stream-splicer@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/stream-splicer/-/stream-splicer-2.0.0.tgz#1b63be438a133e4b671cc1935197600175910d83"
- dependencies:
- inherits "^2.0.1"
- readable-stream "^2.0.2"
-
streamroller@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-0.7.0.tgz#a1d1b7cf83d39afb0d63049a5acbf93493bdf64b"
@@ -7814,7 +7707,7 @@ string_decoder@^0.10.25, string_decoder@~0.10.x:
version "0.10.31"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
-string_decoder@~1.0.0, string_decoder@~1.0.3:
+string_decoder@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab"
dependencies:
@@ -7867,12 +7760,6 @@ style-loader@^0.20.2:
loader-utils "^1.1.0"
schema-utils "^0.4.3"
-subarg@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/subarg/-/subarg-1.0.0.tgz#f62cf17581e996b48fc965699f54c06ae268b8d2"
- dependencies:
- minimist "^1.1.0"
-
supports-color@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
@@ -7895,6 +7782,12 @@ supports-color@^5.1.0, supports-color@^5.2.0:
dependencies:
has-flag "^3.0.0"
+supports-color@^5.3.0:
+ version "5.4.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54"
+ dependencies:
+ has-flag "^3.0.0"
+
svg4everybody@2.1.9:
version "2.1.9"
resolved "https://registry.yarnpkg.com/svg4everybody/-/svg4everybody-2.1.9.tgz#5bd9f6defc133859a044646d4743fabc28db7e2d"
@@ -7911,12 +7804,6 @@ svgo@^0.7.0:
sax "~1.2.1"
whet.extend "~0.9.9"
-syntax-error@^1.1.1:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/syntax-error/-/syntax-error-1.4.0.tgz#2d9d4ff5c064acb711594a3e3b95054ad51d907c"
- dependencies:
- acorn-node "^1.2.0"
-
table@^3.7.8:
version "3.8.3"
resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f"
@@ -7963,12 +7850,12 @@ term-size@^1.2.0:
dependencies:
execa "^0.7.0"
-test-exclude@^4.1.1:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.1.1.tgz#4d84964b0966b0087ecc334a2ce002d3d9341e26"
+test-exclude@^4.2.1:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.2.1.tgz#dfa222f03480bca69207ca728b37d74b45f724fa"
dependencies:
arrify "^1.0.1"
- micromatch "^2.3.11"
+ micromatch "^3.1.8"
object-assign "^4.1.0"
read-pkg-up "^1.0.1"
require-main-filename "^1.0.1"
@@ -7996,7 +7883,7 @@ through2@^2.0.0:
readable-stream "^2.1.5"
xtend "~4.0.1"
-through@2, "through@>=2.2.7 <3", through@^2.3.6, through@~2.3, through@~2.3.1:
+through@2, through@^2.3.6, through@~2.3, through@~2.3.1:
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
@@ -8018,11 +7905,11 @@ timeago.js@^3.0.2:
dependencies:
"@types/jquery" "^2.0.40"
-timed-out@^4.0.0:
+timed-out@^4.0.0, timed-out@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f"
-timers-browserify@^1.0.1, timers-browserify@^1.4.2:
+timers-browserify@^1.4.2:
version "1.4.2"
resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-1.4.2.tgz#c9c58b575be8407375cb5e2462dacee74359f41d"
dependencies:
@@ -8085,6 +7972,15 @@ to-regex@^3.0.1:
extend-shallow "^2.0.1"
regex-not "^1.0.0"
+to-regex@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
+ dependencies:
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ regex-not "^1.0.2"
+ safe-regex "^1.1.0"
+
touch@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b"
@@ -8125,10 +8021,6 @@ tty-browserify@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
-tty-browserify@~0.0.0:
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.1.tgz#3f05251ee17904dfd0677546670db9651682b811"
-
tunnel-agent@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
@@ -8156,7 +8048,7 @@ type-is@~1.6.15:
media-typer "0.3.0"
mime-types "~2.1.18"
-typedarray@^0.0.6, typedarray@~0.0.5:
+typedarray@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
@@ -8189,10 +8081,6 @@ ultron@~1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c"
-umd@^3.0.0:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/umd/-/umd-3.0.1.tgz#8ae556e11011f63c2596708a8837259f01b3d60e"
-
unc-path-regex@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa"
@@ -8294,6 +8182,10 @@ urix@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
+url-join@^2.0.2:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/url-join/-/url-join-2.0.5.tgz#5af22f18c052a000a48d7b82c5e9c2e2feeda728"
+
url-loader@^0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-0.6.2.tgz#a007a7109620e9d988d14bce677a1decb9a993f7"
@@ -8308,6 +8200,12 @@ url-parse-lax@^1.0.0:
dependencies:
prepend-http "^1.0.1"
+url-parse-lax@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c"
+ dependencies:
+ prepend-http "^2.0.0"
+
url-parse@1.0.x:
version "1.0.5"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.0.5.tgz#0854860422afdcfefeb6c965c662d4800169927b"
@@ -8326,7 +8224,7 @@ url-to-options@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9"
-url@^0.11.0, url@~0.11.0:
+url@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
dependencies:
@@ -8347,18 +8245,18 @@ user-home@^2.0.0:
dependencies:
os-homedir "^1.0.0"
-useragent@^2.1.12:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.3.0.tgz#217f943ad540cb2128658ab23fc960f6a88c9972"
+useragent@2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.2.1.tgz#cf593ef4f2d175875e8bb658ea92e18a4fd06d8e"
dependencies:
- lru-cache "4.1.x"
+ lru-cache "2.2.x"
tmp "0.0.x"
util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
-util@0.10.3, util@^0.10.3, util@~0.10.1:
+util@0.10.3, util@^0.10.3:
version "0.10.3"
resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9"
dependencies:
@@ -8407,7 +8305,7 @@ visibilityjs@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/visibilityjs/-/visibilityjs-1.2.4.tgz#bff8663da62c8c10ad4ee5ae6a1ae6fac4259d63"
-vm-browserify@0.0.4, vm-browserify@~0.0.1:
+vm-browserify@0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73"
dependencies:
@@ -8429,12 +8327,12 @@ vue-eslint-parser@^2.0.1:
lodash "^4.17.4"
vue-hot-reload-api@^2.2.0:
- version "2.2.4"
- resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.2.4.tgz#683bd1d026c0d3b3c937d5875679e9a87ec6cd8f"
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.0.tgz#97976142405d13d8efae154749e88c4e358cf926"
vue-loader@^14.1.1:
- version "14.1.1"
- resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-14.1.1.tgz#331f197fcea790d6b8662c29b850806e7eb29342"
+ version "14.2.2"
+ resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-14.2.2.tgz#c8cf3c2e29b6fb2ee595248a2aa6005038a125b3"
dependencies:
consolidate "^0.14.0"
hash-sum "^1.0.2"
@@ -8450,26 +8348,26 @@ vue-loader@^14.1.1:
vue-style-loader "^4.0.1"
vue-template-es2015-compiler "^1.6.0"
-vue-resource@^1.3.5:
- version "1.3.5"
- resolved "https://registry.yarnpkg.com/vue-resource/-/vue-resource-1.3.5.tgz#021d8713e9d86a77e83169dfdd8eab6047369a71"
+vue-resource@^1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/vue-resource/-/vue-resource-1.5.0.tgz#ba0c6ef7af2eeace03cf24a91f529471be974c72"
dependencies:
- got "^7.1.0"
+ got "^8.0.3"
vue-router@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.0.1.tgz#d9b05ad9c7420ba0f626d6500d693e60092cc1e9"
vue-style-loader@^4.0.1:
- version "4.0.2"
- resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-4.0.2.tgz#e89aa4702a0c6b9630d8de70b1cbddb06b9ad254"
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-4.1.0.tgz#7588bd778e2c9f8d87bfc3c5a4a039638da7a863"
dependencies:
hash-sum "^1.0.2"
loader-utils "^1.0.2"
-vue-template-compiler@^2.5.13:
- version "2.5.13"
- resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.5.13.tgz#12a2aa0ecd6158ac5e5f14d294b0993f399c3d38"
+vue-template-compiler@^2.5.16:
+ version "2.5.16"
+ resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.5.16.tgz#93b48570e56c720cdf3f051cc15287c26fbd04cb"
dependencies:
de-indent "^1.0.2"
he "^1.1.0"
@@ -8482,9 +8380,9 @@ vue-virtual-scroll-list@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/vue-virtual-scroll-list/-/vue-virtual-scroll-list-1.2.5.tgz#bcbd010f7cdb035eba8958ebf807c6214d9a167a"
-vue@^2.5.13:
- version "2.5.13"
- resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.13.tgz#95bd31e20efcf7a7f39239c9aa6787ce8cf578e1"
+vue@^2.5.16:
+ version "2.5.16"
+ resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.16.tgz#07edb75e8412aaeed871ebafa99f4672584a0085"
vuex@^3.0.1:
version "3.0.1"
@@ -8521,7 +8419,7 @@ webpack-bundle-analyzer@^2.10.0:
opener "^1.4.3"
ws "^4.0.0"
-webpack-dev-middleware@1.12.2, webpack-dev-middleware@^1.12.0:
+webpack-dev-middleware@1.12.2:
version "1.12.2"
resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.12.2.tgz#f8fc1120ce3b4fc5680ceecb43d777966b21105e"
dependencies:
@@ -8531,6 +8429,18 @@ webpack-dev-middleware@1.12.2, webpack-dev-middleware@^1.12.0:
range-parser "^1.0.3"
time-stamp "^2.0.0"
+webpack-dev-middleware@^2.0.6:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-2.0.6.tgz#a51692801e8310844ef3e3790e1eacfe52326fd4"
+ dependencies:
+ loud-rejection "^1.6.0"
+ memory-fs "~0.4.1"
+ mime "^2.1.0"
+ path-is-absolute "^1.0.0"
+ range-parser "^1.0.3"
+ url-join "^2.0.2"
+ webpack-log "^1.0.1"
+
webpack-dev-server@^2.11.2:
version "2.11.2"
resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.11.2.tgz#1f4f4c78bf1895378f376815910812daf79a216f"
@@ -8563,6 +8473,15 @@ webpack-dev-server@^2.11.2:
webpack-dev-middleware "1.12.2"
yargs "6.6.0"
+webpack-log@^1.0.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-1.2.0.tgz#a4b34cda6b22b518dbb0ab32e567962d5c72a43d"
+ dependencies:
+ chalk "^2.1.0"
+ log-symbols "^2.1.0"
+ loglevelnext "^1.0.1"
+ uuid "^3.1.0"
+
webpack-sources@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.0.1.tgz#c7356436a4d13123be2e2426a05d1dad9cbe65cf"
@@ -8721,7 +8640,7 @@ xregexp@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943"
-xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1:
+xtend@^4.0.0, xtend@~4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"