summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhil Hughes <me@iamphill.com>2018-05-03 12:39:22 +0100
committerPhil Hughes <me@iamphill.com>2018-05-03 12:39:22 +0100
commit93f0eb07c993fa5e424b0e803e972c93e6e444c5 (patch)
tree688ef47370dc56b81058f1fbee7261d807e7cbbd
parentefed5aed411237ea94e9530e3033119d75f44886 (diff)
parent9b8332d324c931798c30adf5517abda96212d5c3 (diff)
downloadgitlab-ce-93f0eb07c993fa5e424b0e803e972c93e6e444c5.tar.gz
Merge branch '44846-improve-web-ide-left-panel-and-modes' into ide-sidebar-commit-box
-rw-r--r--CHANGELOG.md36
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_PAGES_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile6
-rw-r--r--Gemfile.lock10
-rw-r--r--Gemfile.rails5.lock21
-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/environments/components/environment_actions.vue18
-rw-r--r--app/assets/javascripts/environments/components/environment_external_url.vue14
-rw-r--r--app/assets/javascripts/environments/components/environment_monitoring.vue15
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.vue3
-rw-r--r--app/assets/javascripts/environments/components/environment_terminal_button.vue18
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue26
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/form.vue11
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/success_message.vue39
-rw-r--r--app/assets/javascripts/ide/components/repo_commit_section.vue20
-rw-r--r--app/assets/javascripts/ide/stores/actions.js6
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js4
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js4
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js5
-rw-r--r--app/assets/javascripts/ide/stores/state.js1
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js6
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue2
-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/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/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/identicon.vue2
-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/pages/diff.scss6
-rw-r--r--app/assets/stylesheets/pages/notes.scss4
-rw-r--r--app/assets/stylesheets/pages/repo.scss19
-rw-r--r--app/assets/stylesheets/pages/repo.scss.orig786
-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/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/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/identity.rb8
-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/merge_request.rb2
-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/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/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.haml2
-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/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/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/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/change-font-for-tables-inside-diff-discussions.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/ide-improve-commit-panel.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/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/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/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-environment-item-action-buttons-icons.yml5
-rw-r--r--changelogs/unreleased/update-timeline-icon-for-description-edit.yml5
-rw-r--r--changelogs/unreleased/winh-new-mergerequest-branch-picker.yml5
-rw-r--r--changelogs/unreleased/zj-fork-opt-out.yml5
-rw-r--r--changelogs/unreleased/zj-namespace-service-mandatory.yml5
-rw-r--r--changelogs/unreleased/zj-repo-checksum-opt-out.yml5
-rw-r--r--config/application.rb1
-rw-r--r--config/gitlab.yml.example4
-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/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/api/discussions.md585
-rw-r--r--doc/api/groups.md4
-rw-r--r--doc/api/notes.md9
-rw-r--r--doc/development/README.md1
-rw-r--r--doc/development/diffs.md115
-rw-r--r--doc/development/fe_guide/icons.md2
-rw-r--r--doc/development/testing_guide/frontend_testing.md74
-rw-r--r--doc/topics/autodevops/index.md17
-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/pages/index.md36
-rw-r--r--doc/workflow/lfs/manage_large_binaries_with_git_lfs.md11
-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/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/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/gitlab_projects.rb3
-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.rb56
-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/redis/shared_state.rb2
-rw-r--r--lib/gitlab/shell.rb44
-rw-r--r--lib/omni_auth/strategies/jwt.rb62
-rw-r--r--package.json9
-rw-r--r--qa/Gemfile.lock2
-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/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/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/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/javascripts/ide/components/commit_sidebar/empty_state_spec.js29
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/success_message_spec.js35
-rw-r--r--spec/javascripts/ide/stores/actions/file_spec.js14
-rw-r--r--spec/javascripts/ide/stores/mutations_spec.js10
-rw-r--r--spec/javascripts/lib/utils/text_utility_spec.js8
-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_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/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/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/merge_request_spec.rb2
-rw-r--r--spec/models/notification_setting_spec.rb7
-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/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--yarn.lock557
301 files changed, 10136 insertions, 5256 deletions
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/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 ac39a106c48..f374f6662e9 100644
--- a/GITLAB_PAGES_VERSION
+++ b/GITLAB_PAGES_VERSION
@@ -1 +1 @@
-0.9.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 f34d801465d..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'
@@ -415,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 2e9cb1a956f..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)
@@ -1029,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)
@@ -1076,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
@@ -1117,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)
diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock
index a0330cbdd02..3056b97ccd5 100644
--- a/Gemfile.rails5.lock
+++ b/Gemfile.rails5.lock
@@ -162,6 +162,7 @@ GEM
activerecord (>= 3.2.0, < 5.2)
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
+ device_detector (1.0.1)
devise (4.4.1)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
@@ -304,12 +305,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)
@@ -375,7 +376,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)
@@ -554,9 +555,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)
@@ -602,8 +600,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 +748,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)
@@ -1035,6 +1031,7 @@ DEPENDENCIES
database_cleaner (~> 1.5.0)
deckar01-task_list (= 2.0.0)
default_value_for (~> 3.0.5)
+ device_detector
devise (~> 4.2)
devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0)
@@ -1082,7 +1079,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
@@ -1123,7 +1120,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)
@@ -1134,7 +1130,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 +1161,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/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue
index 16bd2f5feb3..ab9e22037d0 100644
--- a/app/assets/javascripts/environments/components/environment_actions.vue
+++ b/app/assets/javascripts/environments/components/environment_actions.vue
@@ -1,5 +1,5 @@
<script>
- import playIconSvg from 'icons/_icon_play.svg';
+ import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
@@ -8,9 +8,9 @@
directives: {
tooltip,
},
-
components: {
loadingIcon,
+ Icon,
},
props: {
actions: {
@@ -19,20 +19,16 @@
default: () => [],
},
},
-
data() {
return {
- playIconSvg,
isLoading: false,
};
},
-
computed: {
title() {
return 'Deploy to...';
},
},
-
methods: {
onClickAction(endpoint) {
this.isLoading = true;
@@ -65,7 +61,10 @@
:disabled="isLoading"
>
<span>
- <span v-html="playIconSvg"></span>
+ <icon
+ name="play"
+ :size="12"
+ />
<i
class="fa fa-caret-down"
aria-hidden="true"
@@ -86,7 +85,10 @@
:class="{ disabled: isActionDisabled(action) }"
:disabled="isActionDisabled(action)"
>
- <span v-html="playIconSvg"></span>
+ <icon
+ name="play"
+ :size="12"
+ />
<span>
{{ action.name }}
</span>
diff --git a/app/assets/javascripts/environments/components/environment_external_url.vue b/app/assets/javascripts/environments/components/environment_external_url.vue
index c9a68cface6..ea6f1168c68 100644
--- a/app/assets/javascripts/environments/components/environment_external_url.vue
+++ b/app/assets/javascripts/environments/components/environment_external_url.vue
@@ -1,4 +1,5 @@
<script>
+ import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
import { s__ } from '../../locale';
@@ -6,6 +7,9 @@
* Renders the external url link in environments table.
*/
export default {
+ components: {
+ Icon,
+ },
directives: {
tooltip,
},
@@ -15,7 +19,6 @@
required: true,
},
},
-
computed: {
title() {
return s__('Environments|Open');
@@ -34,10 +37,9 @@
:aria-label="title"
:href="externalUrl"
>
- <i
- class="fa fa-external-link"
- aria-hidden="true"
- >
- </i>
+ <icon
+ name="external-link"
+ :size="12"
+ />
</a>
</template>
diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue
index 081537cf218..deada134b27 100644
--- a/app/assets/javascripts/environments/components/environment_monitoring.vue
+++ b/app/assets/javascripts/environments/components/environment_monitoring.vue
@@ -2,20 +2,22 @@
/**
* Renders the Monitoring (Metrics) link in environments table.
*/
+ import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
+ components: {
+ Icon,
+ },
directives: {
tooltip,
},
-
props: {
monitoringUrl: {
type: String,
required: true,
},
},
-
computed: {
title() {
return 'Monitoring';
@@ -33,10 +35,9 @@
:title="title"
:aria-label="title"
>
- <i
- class="fa fa-area-chart"
- aria-hidden="true"
- >
- </i>
+ <icon
+ name="chart"
+ :size="12"
+ />
</a>
</template>
diff --git a/app/assets/javascripts/environments/components/environment_rollback.vue b/app/assets/javascripts/environments/components/environment_rollback.vue
index 605a88e997e..c822fb1574c 100644
--- a/app/assets/javascripts/environments/components/environment_rollback.vue
+++ b/app/assets/javascripts/environments/components/environment_rollback.vue
@@ -12,7 +12,6 @@
components: {
loadingIcon,
},
-
props: {
retryUrl: {
type: String,
@@ -24,13 +23,11 @@
default: true,
},
},
-
data() {
return {
isLoading: false,
};
},
-
methods: {
onClick() {
this.isLoading = true;
diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.vue b/app/assets/javascripts/environments/components/environment_terminal_button.vue
index 407d5333c0e..e8469d088ef 100644
--- a/app/assets/javascripts/environments/components/environment_terminal_button.vue
+++ b/app/assets/javascripts/environments/components/environment_terminal_button.vue
@@ -3,14 +3,16 @@
* Renders a terminal button to open a web terminal.
* Used in environments table.
*/
- import terminalIconSvg from 'icons/_icon_terminal.svg';
+ import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
+ components: {
+ Icon,
+ },
directives: {
tooltip,
},
-
props: {
terminalPath: {
type: String,
@@ -18,13 +20,6 @@
default: '',
},
},
-
- data() {
- return {
- terminalIconSvg,
- };
- },
-
computed: {
title() {
return 'Terminal';
@@ -40,7 +35,10 @@
:title="title"
:aria-label="title"
:href="terminalPath"
- v-html="terminalIconSvg"
>
+ <icon
+ name="terminal"
+ :size="12"
+ />
</a>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue b/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue
index ed12b5373b4..d0a60d647e5 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue
@@ -1,38 +1,27 @@
<script>
import { mapState } from 'vuex';
-import Icon from '~/vue_shared/components/icon.vue';
-import tooltip from '~/vue_shared/directives/tooltip';
export default {
- components: {
- Icon,
- },
- directives: {
- tooltip,
- },
computed: {
- ...mapState(['lastCommitMsg', 'noChangesStateSvgPath', 'committedStateSvgPath']),
- statusSvg() {
- return this.lastCommitMsg ? this.committedStateSvgPath : this.noChangesStateSvgPath;
- },
+ ...mapState(['lastCommitMsg', 'noChangesStateSvgPath']),
},
};
</script>
<template>
<div
+ v-if="!lastCommitMsg"
class="multi-file-commit-panel-section ide-commit-empty-state js-empty-state"
>
<div
class="ide-commit-empty-state-container"
>
<div class="svg-content svg-80">
- <img :src="statusSvg" />
+ <img :src="noChangesStateSvgPath" />
</div>
<div class="append-right-default prepend-left-default">
<div
class="text-content text-center"
- v-if="!lastCommitMsg"
>
<h4>
{{ __('No changes') }}
@@ -41,15 +30,6 @@ export default {
{{ __('Edit files in the editor and commit changes here') }}
</p>
</div>
- <div
- class="text-content text-center"
- v-else
- >
- <h4>
- {{ __('All changes are committed') }}
- </h4>
- <p v-html="lastCommitMsg"></p>
- </div>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
index 385cc6d51d5..f42b4265f8e 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
@@ -4,6 +4,7 @@ import { sprintf, __ } from '~/locale';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import CommitMessageField from './message_field.vue';
import Actions from './actions.vue';
+import SuccessMessage from './success_message.vue';
import { activityBarViews, MAX_WINDOW_HEIGHT_COMPACT, COMMIT_ITEM_PADDING } from '../../constants';
export default {
@@ -11,6 +12,7 @@ export default {
Actions,
LoadingButton,
CommitMessageField,
+ SuccessMessage,
},
data() {
return {
@@ -19,10 +21,13 @@ export default {
};
},
computed: {
- ...mapState(['changedFiles', 'stagedFiles', 'currentActivityView']),
+ ...mapState(['lastCommitMsg', 'changedFiles', 'stagedFiles', 'currentActivityView']),
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
...mapGetters(['hasChanges']),
...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']),
+ someUncommitedChanges() {
+ return !!(this.changedFiles.length || this.stagedFiles.length);
+ },
overviewText() {
return sprintf(
__(
@@ -120,6 +125,10 @@ export default {
@submit.prevent.stop="commitChanges"
ref="formEl"
>
+ <success-message
+ v-if="lastCommitMsg && !someUncommitedChanges"
+ :committed-state-svg-path="committedStateSvgPath"
+ />
<commit-message-field
:text="commitMessage"
@input="updateCommitMessage"
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue b/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue
new file mode 100644
index 00000000000..628a17eddca
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue
@@ -0,0 +1,39 @@
+<script>
+import { mapState } from 'vuex';
+
+export default {
+ props: {
+ committedStateSvgPath: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState(['lastCommitMsg']),
+ },
+};
+</script>
+
+<template>
+ <div
+ class="multi-file-commit-panel-success-message"
+ aria-live="assertive"
+ >
+ <div class="svg-content svg-80">
+ <img
+ :src="committedStateSvgPath"
+ alt=""
+ />
+ </div>
+ <div class="append-right-default prepend-left-default">
+ <div
+ class="text-content text-center"
+ >
+ <h4>
+ {{ __('All changes are committed') }}
+ </h4>
+ <p v-html="lastCommitMsg"></p>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
index 55ea1a510f1..e71ce83d4ea 100644
--- a/app/assets/javascripts/ide/components/repo_commit_section.vue
+++ b/app/assets/javascripts/ide/components/repo_commit_section.vue
@@ -19,8 +19,22 @@ export default {
tooltip,
},
computed: {
- ...mapState(['changedFiles', 'stagedFiles']),
+ ...mapState([
+ 'changedFiles',
+ 'stagedFiles',
+ 'rightPanelCollapsed',
+ 'lastCommitMsg',
+ 'unusedSeal',
+ ]),
+ ...mapState('commit', ['commitMessage', 'submitCommitLoading']),
...mapGetters(['lastOpenedFile', 'hasChanges']),
+ ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']),
+ showStageUnstageArea() {
+ return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal);
+ },
+ someUncommitedChanges() {
+ return !!(this.changedFiles.length || this.stagedFiles.length);
+ },
},
watch: {
hasChanges() {
@@ -71,7 +85,7 @@ export default {
</template>
</deprecated-modal>
<template
- v-if="changedFiles.length || stagedFiles.length"
+ v-if="showStageUnstageArea"
>
<commit-files-list
class="is-first"
@@ -93,7 +107,7 @@ export default {
/>
</template>
<empty-state
- v-else
+ v-if="unusedSeal"
/>
</div>
</template>
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index 9900eabb358..500c158f5b5 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -161,6 +161,12 @@ export const updateTempFlagForEntry = ({ commit, dispatch, state }, { file, temp
export const toggleFileFinder = ({ commit }, fileFindVisible) =>
commit(types.TOGGLE_FILE_FINDER, fileFindVisible);
+export const burstUnusedSeal = ({ state, commit }) => {
+ if (state.unusedSeal) {
+ commit(types.BURST_UNUSED_SEAL);
+ }
+};
+
export * from './actions/tree';
export * from './actions/file';
export * from './actions/project';
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index 6b2fba74b09..885584e77ac 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -116,7 +116,7 @@ export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) =
});
};
-export const changeFileContent = ({ state, commit }, { path, content }) => {
+export const changeFileContent = ({ commit, dispatch, state }, { path, content }) => {
const file = state.entries[path];
commit(types.UPDATE_FILE_CONTENT, { path, content });
@@ -127,6 +127,8 @@ export const changeFileContent = ({ state, commit }, { path, content }) => {
} else if (!file.changed && indexOfChangedFile !== -1) {
commit(types.REMOVE_FILE_FROM_CHANGED, path);
}
+
+ dispatch('burstUnusedSeal', {}, { root: true });
};
export const setFileLanguage = ({ getters, commit }, { fileLanguage }) => {
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index 79ee2ea3619..ab00d12089d 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -177,6 +177,10 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
}
commit(rootTypes.CLEAR_STAGED_CHANGES, null, { root: true });
+
+ setTimeout(() => {
+ commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true });
+ }, 5000);
})
.then(() => {
if (rootGetters.lastOpenedFile) {
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index aa96e7f519f..08e22e5ad7e 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -63,3 +63,4 @@ export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB';
export const UPDATE_ACTIVITY_BAR_VIEW = 'UPDATE_ACTIVITY_BAR_VIEW';
export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG';
export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';
+export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL';
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index 27c84ec695f..a257e2ef025 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -143,6 +143,11 @@ export default {
}),
});
},
+ [types.BURST_UNUSED_SEAL](state) {
+ Object.assign(state, {
+ unusedSeal: false,
+ });
+ },
...projectMutations,
...mergeRequestMutation,
...fileMutations,
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index 908df02f964..fd31fcd14ab 100644
--- a/app/assets/javascripts/ide/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -21,5 +21,6 @@ export default () => ({
viewer: 'editor',
delayViewerUpdated: false,
currentActivityView: activityBarViews.edit,
+ unusedSeal: true,
fileFindVisible: false,
});
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index b54ecd2d543..5e786ee6935 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -74,7 +74,11 @@ export function capitalizeFirstCharacter(text) {
* @param {*} replace
* @returns {String}
*/
-export const stripHtml = (string, replace = '') => string.replace(/<[^>]*>/g, replace);
+export const stripHtml = (string, replace = '') => {
+ if (!string) return string;
+
+ return string.replace(/<[^>]*>/g, replace);
+};
/**
* Converts snake_case string to camelCase
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/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/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_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/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/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/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 11052be40a8..70ce5de6a6c 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -44,6 +44,12 @@
}
}
+ .note-text {
+ table {
+ font-family: $font-family-sans-serif;
+ }
+ }
+
table {
width: 100%;
font-family: $monospace_font;
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 cecb3075a5e..ca34fbb0783 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -485,6 +485,7 @@
margin-bottom: 0;
border-bottom: 1px solid $white-dark;
padding: $gl-btn-padding $gl-padding;
+ min-height: 56px;
}
.multi-file-commit-panel-header-title {
@@ -613,6 +614,24 @@
}
}
+.multi-file-commit-panel-bottom {
+ position: relative;
+
+ .multi-file-commit-panel-success-message {
+ position: absolute;
+ top: 1px;
+ left: 3px;
+ bottom: 0;
+ right: 0;
+ z-index: 10;
+ background: $gray-light;
+ overflow: auto;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ }
+}
+
.dirty-diff {
// !important need to override monaco inline style
width: 4px !important;
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/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/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/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/identity.rb b/app/models/identity.rb
index 1011b9f1109..3fd0c5e751d 100644
--- a/app/models/identity.rb
+++ b/app/models/identity.rb
@@ -1,12 +1,16 @@
class Identity < ActiveRecord::Base
+ def self.uniqueness_scope
+ :provider
+ end
+
include Sortable
include CaseSensitivity
belongs_to :user
validates :provider, presence: true
- validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider, case_sensitive: false }
- validates :user_id, uniqueness: { scope: :provider }
+ validates :extern_uid, allow_blank: true, uniqueness: { scope: uniqueness_scope, case_sensitive: false }
+ validates :user_id, uniqueness: { scope: uniqueness_scope }
before_save :ensure_normalized_extern_uid, if: :extern_uid_changed?
after_destroy :clear_user_synced_attributes, if: :user_synced_attributes_metadata_from_provider?
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/merge_request.rb b/app/models/merge_request.rb
index 8f964a488aa..63c6ada86e1 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -323,7 +323,7 @@ class MergeRequest < ActiveRecord::Base
# updates `merge_jid` with the MergeWorker#jid.
# This helps tracking enqueued and ongoing merge jobs.
def merge_async(user_id, params)
- jid = MergeWorker.perform_async(id, user_id, params)
+ jid = MergeWorker.perform_async(id, user_id, params.to_h)
update_column(:merge_jid, jid)
end
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/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 e1cee584929..f90b8b8c0a4 100644
--- a/app/views/admin/runners/_runner.html.haml
+++ b/app/views/admin/runners/_runner.html.haml
@@ -31,7 +31,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 37269862de6..d04cf48b05c 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -108,4 +108,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/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 a911449672b..cb0cccb8f8a 100644
--- a/app/views/peek/_bar.html.haml
+++ b/app/views/peek/_bar.html.haml
@@ -3,5 +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 }
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/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/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/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/change-font-for-tables-inside-diff-discussions.yml b/changelogs/unreleased/change-font-for-tables-inside-diff-discussions.yml
new file mode 100644
index 00000000000..f2810fab208
--- /dev/null
+++ b/changelogs/unreleased/change-font-for-tables-inside-diff-discussions.yml
@@ -0,0 +1,5 @@
+---
+title: Change font for tables inside diff discussions
+merge_request: 18660
+author: George Tsiolis
+type: changed
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/ide-improve-commit-panel.yml b/changelogs/unreleased/ide-improve-commit-panel.yml
new file mode 100644
index 00000000000..f6237db3039
--- /dev/null
+++ b/changelogs/unreleased/ide-improve-commit-panel.yml
@@ -0,0 +1,5 @@
+---
+title: Improve interaction on WebIDE commit panel
+merge_request:
+author:
+type: changed
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/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/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/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-environment-item-action-buttons-icons.yml b/changelogs/unreleased/update-environment-item-action-buttons-icons.yml
new file mode 100644
index 00000000000..7f06022be3e
--- /dev/null
+++ b/changelogs/unreleased/update-environment-item-action-buttons-icons.yml
@@ -0,0 +1,5 @@
+---
+title: Update environment item action buttons icons
+merge_request: 18632
+author: George Tsiolis
+type: changed
diff --git a/changelogs/unreleased/update-timeline-icon-for-description-edit.yml b/changelogs/unreleased/update-timeline-icon-for-description-edit.yml
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-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-fork-opt-out.yml b/changelogs/unreleased/zj-fork-opt-out.yml
new file mode 100644
index 00000000000..56bf6b8b0f6
--- /dev/null
+++ b/changelogs/unreleased/zj-fork-opt-out.yml
@@ -0,0 +1,5 @@
+---
+title: Gitaly handles repository forks by default
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/zj-namespace-service-mandatory.yml b/changelogs/unreleased/zj-namespace-service-mandatory.yml
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/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 6aad8e93dcc..7eb44b8059e 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -534,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',
@@ -825,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/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/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 2a1bcb8cde2..0d24c5a5d4f 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 5853b428430..10cd1bff125 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -322,6 +322,7 @@ ActiveRecord::Schema.define(version: 20180425131009) 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
@@ -486,6 +487,7 @@ ActiveRecord::Schema.define(version: 20180425131009) 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/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/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/development/README.md b/doc/development/README.md
index 99c6641e637..3c77e99b8cf 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -41,6 +41,7 @@ comments: false
- [Avoid modules with instance variables](module_with_instance_variables.md) if possible
- [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/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/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/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/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 7c0cd2c40d2..882ddf4d2c5 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -495,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
@@ -561,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/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/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/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
index 0e29740b15f..0d592a6d43e 100644
--- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
+++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
@@ -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 8aad320e376..75d56b82424 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -286,6 +286,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
@@ -601,6 +605,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
@@ -642,6 +648,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
@@ -655,6 +666,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/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/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/gitlab_projects.rb b/lib/gitlab/git/gitlab_projects.rb
index 099709620b3..68373460d23 100644
--- a/lib/gitlab/git/gitlab_projects.rb
+++ b/lib/gitlab/git/gitlab_projects.rb
@@ -63,7 +63,8 @@ module Gitlab
end
def fork_repository(new_shard_name, new_repository_relative_path)
- Gitlab::GitalyClient.migrate(:fork_repository) do |is_enabled|
+ Gitlab::GitalyClient.migrate(:fork_repository,
+ status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
gitaly_fork_repository(new_shard_name, new_repository_relative_path)
else
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 0d07fb85213..84d37f77fbb 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -391,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)
@@ -413,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
@@ -436,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
@@ -1179,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
@@ -1546,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/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/package.json b/package.json
index 8d0473069c3..e95add1d7e9 100644
--- a/package.json
+++ b/package.json
@@ -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/Gemfile.lock b/qa/Gemfile.lock
index 565adac7499..1bc424335f8 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -47,7 +47,7 @@ GEM
mini_portile2 (2.3.0)
minitest (5.11.1)
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)
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/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/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/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/javascripts/ide/components/commit_sidebar/empty_state_spec.js b/spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js
index 5df2d46ca6b..16d0b354a30 100644
--- a/spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js
@@ -11,7 +11,6 @@ describe('IDE commit panel empty state', () => {
const Component = Vue.extend(emptyState);
Vue.set(store.state, 'noChangesStateSvgPath', 'no-changes');
- Vue.set(store.state, 'committedStateSvgPath', 'committed-state');
vm = createComponentWithStore(Component, store);
@@ -24,35 +23,7 @@ describe('IDE commit panel empty state', () => {
resetStore(vm.$store);
});
- describe('statusSvg', () => {
- it('uses noChangesStateSvgPath when commit message is empty', () => {
- expect(vm.statusSvg).toBe('no-changes');
- expect(vm.$el.querySelector('img').getAttribute('src')).toBe('no-changes');
- });
-
- it('uses committedStateSvgPath when commit message exists', done => {
- vm.$store.state.lastCommitMsg = 'testing';
-
- Vue.nextTick(() => {
- expect(vm.statusSvg).toBe('committed-state');
- expect(vm.$el.querySelector('img').getAttribute('src')).toBe('committed-state');
-
- done();
- });
- });
- });
-
it('renders no changes text when last commit message is empty', () => {
expect(vm.$el.textContent).toContain('No changes');
});
-
- it('renders last commit message when it exists', done => {
- vm.$store.state.lastCommitMsg = 'testing commit message';
-
- Vue.nextTick(() => {
- expect(vm.$el.textContent).toContain('testing commit message');
-
- done();
- });
- });
});
diff --git a/spec/javascripts/ide/components/commit_sidebar/success_message_spec.js b/spec/javascripts/ide/components/commit_sidebar/success_message_spec.js
new file mode 100644
index 00000000000..e1a432b81be
--- /dev/null
+++ b/spec/javascripts/ide/components/commit_sidebar/success_message_spec.js
@@ -0,0 +1,35 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import successMessage from '~/ide/components/commit_sidebar/success_message.vue';
+import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+import { resetStore } from '../../helpers';
+
+describe('IDE commit panel successful commit state', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(successMessage);
+
+ vm = createComponentWithStore(Component, store, {
+ committedStateSvgPath: 'committed-state',
+ });
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders last commit message when it exists', done => {
+ vm.$store.state.lastCommitMsg = 'testing commit message';
+
+ Vue.nextTick(() => {
+ expect(vm.$el.textContent).toContain('testing commit message');
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js
index 07b6bb11b0b..3de48cd0d2d 100644
--- a/spec/javascripts/ide/stores/actions/file_spec.js
+++ b/spec/javascripts/ide/stores/actions/file_spec.js
@@ -398,6 +398,20 @@ describe('IDE store file actions', () => {
})
.catch(done.fail);
});
+
+ it('bursts unused seal', done => {
+ store
+ .dispatch('changeFileContent', {
+ path: tmpFile.path,
+ content: 'content',
+ })
+ .then(() => {
+ expect(store.state.unusedSeal).toBe(false);
+
+ done();
+ })
+ .catch(done.fail);
+ });
});
describe('discardFileChanges', () => {
diff --git a/spec/javascripts/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js
index ede5e5e18e5..972713c5ad2 100644
--- a/spec/javascripts/ide/stores/mutations_spec.js
+++ b/spec/javascripts/ide/stores/mutations_spec.js
@@ -138,4 +138,14 @@ describe('Multi-file store mutations', () => {
expect(localState.fileFindVisible).toBe(true);
});
});
+
+ describe('BURST_UNUSED_SEAL', () => {
+ it('updates unusedSeal', () => {
+ expect(localState.unusedSeal).toBe(true);
+
+ mutations.BURST_UNUSED_SEAL(localState);
+
+ expect(localState.unusedSeal).toBe(false);
+ });
+ });
});
diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js
index ae00fb76714..eab5c24406a 100644
--- a/spec/javascripts/lib/utils/text_utility_spec.js
+++ b/spec/javascripts/lib/utils/text_utility_spec.js
@@ -75,6 +75,14 @@ describe('text_utility', () => {
'This is a text with html .',
);
});
+
+ it('passes through with null string input', () => {
+ expect(textUtils.stripHtml(null, ' ')).toEqual(null);
+ });
+
+ it('passes through with undefined string input', () => {
+ expect(textUtils.stripHtml(undefined, ' ')).toEqual(undefined);
+ });
});
describe('convertToCamelCase', () => {
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_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/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/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/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index becb146422e..5a9aa7c7d1b 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -1213,7 +1213,7 @@ describe MergeRequest do
it 'enqueues MergeWorker job and updates merge_jid' do
merge_request = create(:merge_request)
user_id = double(:user_id)
- params = double(:params)
+ params = {}
merge_jid = 'hash-123'
expect(MergeWorker).to receive(:perform_async).with(merge_request.id, user_id, params) do
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/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 17c7a511857..6c1c13c9d80 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -1330,7 +1330,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
@@ -1345,7 +1345,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/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/yarn.lock b/yarn.lock
index 395e2cfb0ed..1e6ffa5f524 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -66,13 +66,6 @@
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"
@@ -100,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"
@@ -115,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"
@@ -226,12 +212,6 @@ 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"
- dependencies:
- color-convert "^1.9.0"
-
ansi-styles@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
@@ -378,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:
@@ -392,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"
@@ -410,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"
@@ -672,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"
@@ -708,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"
@@ -1035,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:
@@ -1294,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"
@@ -1363,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"
@@ -1448,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"
@@ -1527,10 +1416,6 @@ cacheable-request@^2.1.1:
normalize-url "2.0.1"
responselike "1.0.2"
-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"
-
caller-path@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f"
@@ -1610,17 +1495,9 @@ 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"
- dependencies:
- ansi-styles "^3.2.0"
- escape-string-regexp "^1.0.5"
- supports-color "^5.2.0"
-
-chalk@^2.3.2:
- version "2.4.0"
- resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.0.tgz#a060a297a6b57e15b61ca63ce84995daa0fe6e52"
+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.1"
escape-string-regexp "^1.0.5"
@@ -1830,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"
@@ -1914,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"
@@ -1962,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"
@@ -1982,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"
@@ -2105,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:
@@ -2511,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"
@@ -2552,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"
@@ -2633,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"
@@ -2672,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"
@@ -2877,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:
@@ -3141,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"
@@ -3690,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:
@@ -3909,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"
@@ -3946,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:
@@ -4055,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"
@@ -4142,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"
@@ -4255,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"
@@ -4298,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"
@@ -4383,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"
@@ -4666,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"
@@ -4720,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:
@@ -4882,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"
@@ -4904,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"
@@ -4942,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"
@@ -4967,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"
@@ -5008,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"
@@ -5046,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"
@@ -5083,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"
@@ -5134,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:
@@ -5234,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"
@@ -5265,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"
@@ -5310,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"
@@ -5320,7 +5113,7 @@ 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:
@@ -5331,7 +5124,11 @@ 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:
@@ -5490,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"
@@ -5515,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"
@@ -5547,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"
@@ -5583,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"
@@ -5935,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"
@@ -5945,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"
@@ -6026,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"
@@ -6134,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"
@@ -6146,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"
@@ -6201,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"
@@ -6235,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"
@@ -6751,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"
@@ -6794,7 +6588,7 @@ query-string@^5.0.1:
object-assign "^4.1.0"
strict-uri-encode "^1.0.0"
-querystring-es3@^0.2.0, querystring-es3@~0.2.0:
+querystring-es3@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
@@ -6893,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"
@@ -6950,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:
@@ -7049,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:
@@ -7258,11 +7046,11 @@ 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.1.6, resolve@^1.2.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36"
dependencies:
@@ -7507,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"
@@ -7531,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:
@@ -7860,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"
@@ -7887,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:
@@ -7901,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"
@@ -7940,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:
@@ -7993,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"
@@ -8043,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"
@@ -8095,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"
@@ -8128,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"
@@ -8154,7 +7909,7 @@ 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:
@@ -8217,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"
@@ -8257,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"
@@ -8288,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"
@@ -8321,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"
@@ -8426,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"
@@ -8464,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:
@@ -8485,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:
@@ -8545,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:
@@ -8659,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:
@@ -8669,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"
@@ -8701,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"
@@ -8859,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"