summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDouglas Barbosa Alexandre <dbalexandre@gmail.com>2016-04-08 15:48:09 -0300
committerDouglas Barbosa Alexandre <dbalexandre@gmail.com>2016-04-08 15:48:09 -0300
commit7afeace35474249c9aeb898f3a56180745106169 (patch)
tree36bd5104fb42d739b54378fc2fb5e0cc9714e538
parent877f56c9370eef9affef3cc3438c5d4fe11b123c (diff)
parent0d216c194c5a7b72c98f0f06b4fc7fd0ba358c0e (diff)
downloadgitlab-ce-7afeace35474249c9aeb898f3a56180745106169.tar.gz
Merge branch 'master' into decouple-member-notification
-rw-r--r--.gitlab-ci.yml79
-rw-r--r--.scss-lint.yml106
-rw-r--r--CHANGELOG96
-rw-r--r--CONTRIBUTING.md2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile4
-rw-r--r--Gemfile.lock30
-rw-r--r--app/assets/javascripts/awards_handler.coffee4
-rw-r--r--app/assets/javascripts/dispatcher.js.coffee8
-rw-r--r--app/assets/javascripts/gl_dropdown.js.coffee226
-rw-r--r--app/assets/javascripts/issuable_context.js.coffee2
-rw-r--r--app/assets/javascripts/issue.js.coffee17
-rw-r--r--app/assets/javascripts/issues.js.coffee15
-rw-r--r--app/assets/javascripts/labels_select.js.coffee22
-rw-r--r--app/assets/javascripts/lib/notify.js.coffee30
-rw-r--r--app/assets/javascripts/lib/url_utility.js.coffee31
-rw-r--r--app/assets/javascripts/merge_request.js.coffee16
-rw-r--r--app/assets/javascripts/merge_request_widget.js.coffee78
-rw-r--r--app/assets/javascripts/milestone_select.js.coffee17
-rw-r--r--app/assets/javascripts/notes.js.coffee38
-rw-r--r--app/assets/javascripts/right_sidebar.js.coffee55
-rw-r--r--app/assets/javascripts/search_autocomplete.js.coffee305
-rw-r--r--app/assets/javascripts/todos.js.coffee10
-rw-r--r--app/assets/javascripts/users_select.js.coffee23
-rw-r--r--app/assets/javascripts/zen_mode.js.coffee2
-rw-r--r--app/assets/stylesheets/behaviors.scss4
-rw-r--r--app/assets/stylesheets/framework/calendar.scss6
-rw-r--r--app/assets/stylesheets/framework/common.scss11
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss60
-rw-r--r--app/assets/stylesheets/framework/files.scss1
-rw-r--r--app/assets/stylesheets/framework/filters.scss2
-rw-r--r--app/assets/stylesheets/framework/forms.scss36
-rw-r--r--app/assets/stylesheets/framework/header.scss28
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss35
-rw-r--r--app/assets/stylesheets/framework/mobile.scss4
-rw-r--r--app/assets/stylesheets/framework/nav.scss14
-rw-r--r--app/assets/stylesheets/framework/selects.scss2
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss8
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap_variables.scss4
-rw-r--r--app/assets/stylesheets/framework/typography.scss8
-rw-r--r--app/assets/stylesheets/framework/variables.scss48
-rw-r--r--app/assets/stylesheets/framework/zen.scss101
-rw-r--r--app/assets/stylesheets/pages/awards.scss2
-rw-r--r--app/assets/stylesheets/pages/ci_projects.scss2
-rw-r--r--app/assets/stylesheets/pages/commit.scss10
-rw-r--r--app/assets/stylesheets/pages/commits.scss2
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss8
-rw-r--r--app/assets/stylesheets/pages/diff.scss9
-rw-r--r--app/assets/stylesheets/pages/editor.scss2
-rw-r--r--app/assets/stylesheets/pages/events.scss4
-rw-r--r--app/assets/stylesheets/pages/lint.scss4
-rw-r--r--app/assets/stylesheets/pages/login.scss2
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss36
-rw-r--r--app/assets/stylesheets/pages/note_form.scss127
-rw-r--r--app/assets/stylesheets/pages/notes.scss163
-rw-r--r--app/assets/stylesheets/pages/projects.scss4
-rw-r--r--app/assets/stylesheets/pages/search.scss142
-rw-r--r--app/assets/stylesheets/pages/status.scss2
-rw-r--r--app/assets/stylesheets/pages/todos.scss8
-rw-r--r--app/controllers/admin/application_settings_controller.rb1
-rw-r--r--app/controllers/admin/projects_controller.rb2
-rw-r--r--app/controllers/groups/milestones_controller.rb31
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb2
-rw-r--r--app/controllers/projects/application_controller.rb4
-rw-r--r--app/controllers/projects/badges_controller.rb14
-rw-r--r--app/controllers/projects/branches_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests_controller.rb22
-rw-r--r--app/controllers/projects/project_members_controller.rb11
-rw-r--r--app/controllers/projects/refs_controller.rb2
-rw-r--r--app/controllers/projects/wikis_controller.rb14
-rw-r--r--app/controllers/projects_controller.rb18
-rw-r--r--app/controllers/sessions_controller.rb15
-rw-r--r--app/helpers/application_settings_helper.rb4
-rw-r--r--app/helpers/events_helper.rb8
-rw-r--r--app/helpers/search_helper.rb57
-rw-r--r--app/mailers/notify.rb4
-rw-r--r--app/models/application_setting.rb2
-rw-r--r--app/models/commit.rb4
-rw-r--r--app/models/commit_range.rb4
-rw-r--r--app/models/concerns/issuable.rb3
-rw-r--r--app/models/external_issue.rb2
-rw-r--r--app/models/issue.rb4
-rw-r--r--app/models/label.rb2
-rw-r--r--app/models/merge_request.rb13
-rw-r--r--app/models/milestone.rb4
-rw-r--r--app/models/note.rb2
-rw-r--r--app/models/project.rb16
-rw-r--r--app/models/project_services/builds_email_service.rb11
-rw-r--r--app/models/project_services/gitlab_issue_tracker_service.rb2
-rw-r--r--app/models/project_services/jira_service.rb2
-rw-r--r--app/models/repository.rb27
-rw-r--r--app/models/snippet.rb4
-rw-r--r--app/models/user.rb2
-rw-r--r--app/services/git_push_service.rb18
-rw-r--r--app/services/issues/move_service.rb28
-rw-r--r--app/services/milestones/create_service.rb2
-rw-r--r--app/services/projects/import_service.rb2
-rw-r--r--app/services/projects/unlink_fork_service.rb19
-rw-r--r--app/services/system_hooks_service.rb22
-rw-r--r--app/services/system_note_service.rb2
-rw-r--r--app/services/todo_service.rb26
-rw-r--r--app/uploaders/file_uploader.rb17
-rw-r--r--app/views/admin/application_settings/_form.html.haml7
-rw-r--r--app/views/admin/builds/_build.html.haml2
-rw-r--r--app/views/admin/dashboard/index.html.haml2
-rw-r--r--app/views/admin/deploy_keys/index.html.haml2
-rw-r--r--app/views/admin/groups/_group.html.haml28
-rw-r--r--app/views/admin/groups/index.html.haml45
-rw-r--r--app/views/admin/labels/index.html.haml12
-rw-r--r--app/views/admin/runners/index.html.haml2
-rw-r--r--app/views/dashboard/todos/_todo.html.haml7
-rw-r--r--app/views/events/_event.html.haml2
-rw-r--r--app/views/events/event/_created_project.html.haml18
-rw-r--r--app/views/groups/milestones/new.html.haml8
-rw-r--r--app/views/layouts/_search.html.haml40
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml2
-rw-r--r--app/views/layouts/nav/_project_settings.html.haml7
-rw-r--r--app/views/layouts/project.html.haml6
-rw-r--r--app/views/profiles/two_factor_auths/new.html.haml2
-rw-r--r--app/views/projects/_md_preview.html.haml17
-rw-r--r--app/views/projects/_zen.html.haml20
-rw-r--r--app/views/projects/badges/index.html.haml24
-rw-r--r--app/views/projects/buttons/_dropdown.html.haml17
-rw-r--r--app/views/projects/ci/builds/_build.html.haml2
-rw-r--r--app/views/projects/diffs/_image.html.haml7
-rw-r--r--app/views/projects/merge_requests/widget/_heading.html.haml24
-rw-r--r--app/views/projects/merge_requests/widget/_show.html.haml19
-rw-r--r--app/views/projects/notes/_edit_form.html.haml7
-rw-r--r--app/views/projects/notes/_form.html.haml4
-rw-r--r--app/views/projects/notes/_hints.html.haml15
-rw-r--r--app/views/projects/notes/_note.html.haml29
-rw-r--r--app/views/projects/notes/_notes_with_form.html.haml31
-rw-r--r--app/views/projects/notes/discussions/_active.html.haml16
-rw-r--r--app/views/projects/notes/discussions/_commit.html.haml15
-rw-r--r--app/views/projects/notes/discussions/_outdated.html.haml15
-rw-r--r--app/views/search/results/_note.html.haml18
-rw-r--r--app/views/shared/issuable/_filter.html.haml4
-rw-r--r--app/views/shared/issuable/_label_dropdown.html.haml2
-rw-r--r--app/views/shared/issuable/_milestone_dropdown.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml15
-rw-r--r--app/views/users/show.html.haml2
-rw-r--r--app/views/votes/_votes_block.html.haml2
-rw-r--r--app/workers/project_cache_worker.rb3
-rw-r--r--app/workers/project_destroy_worker.rb2
-rw-r--r--config/gitlab.yml.example7
-rw-r--r--config/initializers/1_settings.rb3
-rw-r--r--config/initializers/premailer.rb3
-rw-r--r--config/mail_room.yml2
-rw-r--r--config/routes.rb8
-rw-r--r--db/migrate/20160324020319_remove_todos_for_deleted_issues.rb17
-rw-r--r--db/migrate/20160329144452_add_index_on_pending_delete_projects.rb6
-rw-r--r--db/migrate/20160331133914_remove_todos_for_deleted_merge_requests.rb17
-rw-r--r--db/migrate/20160331223143_remove_twitter_sharing_enabled_from_application_settings.rb5
-rw-r--r--db/schema.rb5
-rw-r--r--doc/administration/auth/ldap.md12
-rw-r--r--doc/api/issues.md17
-rw-r--r--doc/api/merge_requests.md19
-rw-r--r--doc/api/milestones.md20
-rw-r--r--doc/api/projects.md166
-rw-r--r--doc/api/settings.md3
-rw-r--r--doc/api/users.md6
-rw-r--r--doc/ci/ssh_keys/README.md2
-rw-r--r--doc/ci/yaml/README.md2
-rw-r--r--doc/development/README.md2
-rw-r--r--doc/development/instrumentation.md37
-rw-r--r--doc/development/testing.md134
-rw-r--r--doc/development/ui_guide.md4
-rw-r--r--doc/incoming_email/README.md108
-rw-r--r--doc/install/installation.md12
-rw-r--r--doc/integration/ldap.md2
-rw-r--r--doc/integration/saml.md71
-rw-r--r--doc/markdown/markdown.md8
-rw-r--r--doc/permissions/permissions.md5
-rw-r--r--doc/update/8.6-to-8.7.md154
-rw-r--r--features/dashboard/dashboard.feature8
-rw-r--r--features/dashboard/todos.feature5
-rw-r--r--features/steps/dashboard/dashboard.rb19
-rw-r--r--features/steps/dashboard/issues.rb9
-rw-r--r--features/steps/dashboard/todos.rb10
-rw-r--r--features/steps/project/merge_requests.rb8
-rw-r--r--features/steps/project/wiki.rb2
-rw-r--r--features/steps/shared/diff_note.rb4
-rw-r--r--features/steps/shared/note.rb4
-rw-r--r--fixtures/emojis/digests.json8597
-rw-r--r--lib/api/branches.rb2
-rw-r--r--lib/api/entities.rb15
-rw-r--r--lib/api/issues.rb10
-rw-r--r--lib/api/merge_requests.rb14
-rw-r--r--lib/api/milestones.rb22
-rw-r--r--lib/api/projects.rb28
-rw-r--r--lib/api/users.rb6
-rw-r--r--lib/award_emoji.rb19
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb140
-rw-r--r--lib/banzai/filter/commit_range_reference_filter.rb2
-rw-r--r--lib/banzai/filter/commit_reference_filter.rb2
-rw-r--r--lib/banzai/filter/external_issue_reference_filter.rb41
-rw-r--r--lib/banzai/filter/gollum_tags_filter.rb14
-rw-r--r--lib/banzai/filter/image_link_filter.rb27
-rw-r--r--lib/banzai/filter/label_reference_filter.rb2
-rw-r--r--lib/banzai/filter/merge_request_reference_filter.rb2
-rw-r--r--lib/banzai/filter/milestone_reference_filter.rb2
-rw-r--r--lib/banzai/filter/reference_filter.rb148
-rw-r--r--lib/banzai/filter/snippet_reference_filter.rb2
-rw-r--r--lib/banzai/filter/user_reference_filter.rb27
-rw-r--r--lib/banzai/filter/wiki_link_filter.rb56
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb1
-rw-r--r--lib/banzai/pipeline/wiki_pipeline.rb6
-rw-r--r--lib/gitlab/badge/build.rb46
-rw-r--r--lib/gitlab/current_settings.rb1
-rw-r--r--lib/gitlab/email/message/repository_push.rb2
-rw-r--r--lib/gitlab/email/receiver.rb15
-rw-r--r--lib/gitlab/fogbugz_import/client.rb2
-rw-r--r--lib/gitlab/gfm/reference_rewriter.rb9
-rw-r--r--lib/gitlab/gfm/uploads_rewriter.rb51
-rw-r--r--lib/gitlab/incoming_email.rb16
-rw-r--r--lib/gitlab/ldap/access.rb5
-rw-r--r--lib/gitlab/metrics.rb26
-rw-r--r--lib/gitlab/metrics/metric.rb22
-rw-r--r--lib/gitlab/note_data_builder.rb2
-rw-r--r--lib/gitlab/routing.rb13
-rw-r--r--lib/gitlab/saml/auth_hash.rb19
-rw-r--r--lib/gitlab/saml/config.rb21
-rw-r--r--lib/gitlab/saml/user.rb26
-rw-r--r--lib/gitlab/url_builder.rb16
-rw-r--r--lib/tasks/gemojione.rake48
-rw-r--r--lib/tasks/gitlab/check.rake15
-rw-r--r--spec/controllers/admin/projects_controller_spec.rb23
-rw-r--r--spec/controllers/admin/users_controller_spec.rb15
-rw-r--r--spec/controllers/groups/milestones_controller_spec.rb6
-rw-r--r--spec/controllers/projects/branches_controller_spec.rb14
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb2
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb49
-rw-r--r--spec/controllers/projects_controller_spec.rb22
-rw-r--r--spec/controllers/sessions_controller_spec.rb101
-rw-r--r--spec/factories/file_uploader.rb20
-rw-r--r--spec/factories/forked_project_links.rb5
-rw-r--r--spec/factories_spec.rb16
-rw-r--r--spec/features/atom/users_spec.rb2
-rw-r--r--spec/features/issues/filter_issues_spec.rb119
-rw-r--r--spec/features/issues_spec.rb2
-rw-r--r--spec/features/markdown_spec.rb3
-rw-r--r--spec/features/merge_requests/create_new_mr_spec.rb28
-rw-r--r--spec/features/notes_on_merge_requests_spec.rb4
-rw-r--r--spec/features/projects/badges/list_spec.rb34
-rw-r--r--spec/features/search_spec.rb43
-rw-r--r--spec/fixtures/emails/reply_without_subaddressing_and_key_inside_references.eml42
-rw-r--r--spec/fixtures/emails/valid_reply.eml4
-rw-r--r--spec/javascripts/fixtures/zen_mode.html.haml2
-rw-r--r--spec/javascripts/issue_spec.js.coffee6
-rw-r--r--spec/lib/award_emoji_spec.rb19
-rw-r--r--spec/lib/banzai/filter/gollum_tags_filter_spec.rb8
-rw-r--r--spec/lib/banzai/filter/image_link_filter_spec.rb24
-rw-r--r--spec/lib/banzai/filter/issue_reference_filter_spec.rb8
-rw-r--r--spec/lib/banzai/pipeline/wiki_pipeline_spec.rb6
-rw-r--r--spec/lib/extracts_path_spec.rb2
-rw-r--r--spec/lib/gitlab/badge/build_spec.rb103
-rw-r--r--spec/lib/gitlab/closing_issue_extractor_spec.rb117
-rw-r--r--spec/lib/gitlab/email/receiver_spec.rb23
-rw-r--r--spec/lib/gitlab/fogbugz_import/client_spec.rb24
-rw-r--r--spec/lib/gitlab/gfm/uploads_rewriter_spec.rb66
-rw-r--r--spec/lib/gitlab/incoming_email_spec.rb26
-rw-r--r--spec/lib/gitlab/ldap/access_spec.rb27
-rw-r--r--spec/lib/gitlab/metrics_spec.rb47
-rw-r--r--spec/lib/gitlab/saml/user_spec.rb166
-rw-r--r--spec/mailers/notify_spec.rb66
-rw-r--r--spec/mailers/shared/notify.rb78
-rw-r--r--spec/models/application_setting_spec.rb1
-rw-r--r--spec/models/concerns/issuable_spec.rb1
-rw-r--r--spec/models/hooks/system_hook_spec.rb54
-rw-r--r--spec/models/project_services/builds_email_service_spec.rb24
-rw-r--r--spec/models/project_spec.rb9
-rw-r--r--spec/models/repository_spec.rb54
-rw-r--r--spec/models/user_spec.rb7
-rw-r--r--spec/requests/api/milestones_spec.rb19
-rw-r--r--spec/requests/api/projects_spec.rb72
-rw-r--r--spec/services/git_push_service_spec.rb36
-rw-r--r--spec/services/issues/move_service_spec.rb14
-rw-r--r--spec/services/projects/import_service_spec.rb17
-rw-r--r--spec/services/projects/unlink_fork_service_spec.rb32
-rw-r--r--spec/services/todo_service_spec.rb77
-rw-r--r--spec/support/carrierwave.rb7
-rw-r--r--spec/support/filter_spec_helper.rb2
-rw-r--r--spec/support/markdown_feature.rb2
-rw-r--r--spec/support/matchers/markdown_matchers.rb9
-rw-r--r--spec/support/test_env.rb1
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb4
-rw-r--r--spec/workers/merge_worker_spec.rb2
-rw-r--r--spec/workers/project_cache_worker_spec.rb27
288 files changed, 13810 insertions, 1571 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 53f115c92c8..1dc49ca336d 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -2,7 +2,6 @@ image: "ruby:2.1"
services:
- mysql:latest
- - postgres:latest
- redis:latest
cache:
@@ -35,134 +34,86 @@ spec:feature:
script:
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature
- tags:
- - ruby
- - mysql
spec:api:
stage: test
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:api
- tags:
- - ruby
- - mysql
spec:models:
stage: test
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:models
- tags:
- - ruby
- - mysql
spec:lib:
stage: test
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:lib
- tags:
- - ruby
- - mysql
spec:services:
stage: test
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:services
- tags:
- - ruby
- - mysql
spec:other:
stage: test
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:other
- tags:
- - ruby
- - mysql
spinach:project:half:
stage: test
script:
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:half
- tags:
- - ruby
- - mysql
spinach:project:rest:
stage: test
script:
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:rest
- tags:
- - ruby
- - mysql
spinach:other:
stage: test
script:
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:other
- tags:
- - ruby
- - mysql
teaspoon:
stage: test
script:
- RAILS_ENV=test bundle exec teaspoon
- tags:
- - ruby
- - mysql
rubocop:
stage: test
script:
- bundle exec rubocop
- tags:
- - ruby
- - mysql
scss-lint:
stage: test
script:
- bundle exec rake scss_lint
- tags:
- - ruby
brakeman:
stage: test
script:
- bundle exec rake brakeman
- tags:
- - ruby
- - mysql
flog:
stage: test
script:
- bundle exec rake flog
- tags:
- - ruby
- - mysql
flay:
stage: test
script:
- bundle exec rake flay
- tags:
- - ruby
- - mysql
bundler:audit:
stage: test
only:
- master
script:
- - "bundle exec bundle-audit update"
- - "bundle exec bundle-audit check --ignore OSVDB-115941"
- tags:
- - ruby
- - mysql
+ - "bundle exec bundle-audit check --update --ignore OSVDB-115941"
# Ruby 2.2 jobs
@@ -178,9 +129,6 @@ spec:feature:ruby22:
key: "ruby22"
paths:
- vendor
- tags:
- - ruby
- - mysql
spec:api:ruby22:
stage: test
@@ -193,9 +141,6 @@ spec:api:ruby22:
key: "ruby22"
paths:
- vendor
- tags:
- - ruby
- - mysql
spec:models:ruby22:
stage: test
@@ -208,9 +153,6 @@ spec:models:ruby22:
key: "ruby22"
paths:
- vendor
- tags:
- - ruby
- - mysql
spec:lib:ruby22:
stage: test
@@ -223,9 +165,6 @@ spec:lib:ruby22:
key: "ruby22"
paths:
- vendor
- tags:
- - ruby
- - mysql
spec:services:ruby22:
stage: test
@@ -238,9 +177,6 @@ spec:services:ruby22:
key: "ruby22"
paths:
- vendor
- tags:
- - ruby
- - mysql
spec:other:ruby22:
stage: test
@@ -253,9 +189,6 @@ spec:other:ruby22:
key: "ruby22"
paths:
- vendor
- tags:
- - ruby
- - mysql
spinach:project:half:ruby22:
stage: test
@@ -269,9 +202,6 @@ spinach:project:half:ruby22:
key: "ruby22"
paths:
- vendor
- tags:
- - ruby
- - mysql
spinach:project:rest:ruby22:
stage: test
@@ -285,9 +215,6 @@ spinach:project:rest:ruby22:
key: "ruby22"
paths:
- vendor
- tags:
- - ruby
- - mysql
spinach:other:ruby22:
stage: test
@@ -301,10 +228,6 @@ spinach:other:ruby22:
key: "ruby22"
paths:
- vendor
- tags:
- - ruby
- - mysql
-
notify:slack:
stage: notifications
diff --git a/.scss-lint.yml b/.scss-lint.yml
index 937d3407b60..835a4a88c44 100644
--- a/.scss-lint.yml
+++ b/.scss-lint.yml
@@ -7,21 +7,44 @@ exclude:
- 'app/assets/stylesheets/pages/emojis.scss'
linters:
+ # Reports when you use improper spacing around ! (the "bang") in !default,
+ # !global, !important, and !optional flags.
BangFormat:
enabled: false
+ # Whether or not to prefer `border: 0` over `border: none`.
BorderZero:
enabled: false
+ # Reports when you define a rule set using a selector with chained classes
+ # (a.k.a. adjoining classes).
+ ChainedClasses:
+ enabled: false
+
+ # Prefer hexadecimal color codes over color keywords.
+ # (e.g. `color: green` is a color keyword)
ColorKeyword:
enabled: false
+ # Prefer color literals (keywords or hexadecimal codes) to be used only in
+ # variable declarations. They should be referred to via variables everywhere
+ # else.
ColorVariable:
enabled: false
+ # Which form of comments to prefer in CSS.
Comment:
enabled: false
+
+ # Reports @debug statements (which you probably left behind accidentally).
+ DebugStatement:
+ enabled: false
+ # Rule sets should be ordered as follows:
+ # - @extend declarations
+ # - @include declarations without inner @content
+ # - properties, @include declarations with inner @content
+ # - nested rule sets.
DeclarationOrder:
enabled: false
@@ -32,15 +55,25 @@ linters:
DisableLinterReason:
enabled: true
+ # Reports when you define the same property twice in a single rule set.
DuplicateProperty:
enabled: false
+ # Separate rule, function, and mixin declarations with empty lines.
EmptyLineBetweenBlocks:
enabled: false
+ # Reports when you have an empty rule set.
EmptyRule:
enabled: false
+ # Reports when you have an @extend directive.
+ ExtendDirective:
+ enabled: false
+
+ # Files should always have a final newline. This results in better diffs
+ # when adding lines to the file, since SCM systems such as git won't
+ # think that you touched the last line.
FinalNewline:
enabled: false
@@ -53,12 +86,17 @@ linters:
HexNotation:
enabled: true
+ # Avoid using ID selectors.
IdSelector:
enabled: false
+ # The basenames of @imported SCSS partials should not begin with an
+ # underscore and should not include the filename extension.
ImportPath:
enabled: false
+ # Avoid using !important in properties. It is usually indicative of a
+ # misunderstanding of CSS specificity and can lead to brittle code.
ImportantRule:
enabled: false
@@ -67,33 +105,51 @@ linters:
enabled: true
width: 2
+ # Don't write leading zeros for numeric values with a decimal point.
LeadingZero:
enabled: false
+ # Reports when you define the same selector twice in a single sheet.
MergeableSelector:
enabled: false
+ # Functions, mixins, variables, and placeholders should be declared
+ # with all lowercase letters and hyphens instead of underscores.
NameFormat:
enabled: false
+ # Avoid nesting selectors too deeply.
NestingDepth:
enabled: false
+ # Always use placeholder selectors in @extend.
PlaceholderInExtend:
enabled: false
+ # Sort properties in a strict order.
PropertySortOrder:
enabled: false
+ # Reports when you use an unknown or disabled CSS property
+ # (ignoring vendor-prefixed properties).
PropertySpelling:
enabled: false
+ # Configure which units are allowed for property values.
+ PropertyUnits:
+ enabled: false
+
+ # Pseudo-elements, like ::before, and ::first-letter, should be declared
+ # with two colons. Pseudo-classes, like :hover and :first-child, should
+ # be declared with one colon.
PseudoElement:
enabled: false
+ # Avoid qualifying elements in selectors (also known as "tag-qualifying").
QualifyingElement:
enabled: false
+ # Don't write selectors with a depth of applicability greater than 3.
SelectorDepth:
enabled: false
@@ -113,9 +169,12 @@ linters:
enabled: true
allow_single_line_rule_sets: true
+ # Split selectors onto separate lines after each comma, and have each
+ # individual selector occupy a single line.
SingleLinePerSelector:
enabled: false
+ # Commas in lists should be followed by a space.
SpaceAfterComma:
enabled: false
@@ -128,28 +187,75 @@ linters:
# colon.
SpaceAfterPropertyName:
enabled: true
+
+ # Variables should be formatted with a single space separating the colon
+ # from the variable's value.
+ SpaceAfterVariableColon:
+ enabled: false
+
+ # Variables should be formatted with no space between the name and the
+ # colon.
+ SpaceAfterVariableName:
+ enabled: false
+ # Operators should be formatted with a single space on both sides of an
+ # infix operator.
SpaceAroundOperator:
enabled: false
+ # Opening braces should be preceded by a single space.
SpaceBeforeBrace:
+ enabled: true
+
+ # Parentheses should not be padded with spaces.
+ SpaceBetweenParens:
enabled: false
+ # Enforces that string literals should be written with a consistent form
+ # of quotes (single or double).
StringQuotes:
enabled: false
+ # Property values, @extend, @include, and @import directives, and variable
+ # declarations should always end with a semicolon.
TrailingSemicolon:
enabled: false
+ # Reports lines containing trailing whitespace.
TrailingWhitespace:
enabled: false
+ # Don't write trailing zeros for numeric values with a decimal point.
+ TrailingZero:
+ enabled: false
+
+ # Don't use the `all` keyword to specify transition properties.
+ TransitionAll:
+ enabled: false
+
+ # Numeric values should not contain unnecessary fractional portions.
UnnecessaryMantissa:
enabled: false
+ # Do not use parent selector references (&) when they would otherwise
+ # be unnecessary.
UnnecessaryParentReference:
enabled: false
+
+ # URLs should be valid and not contain protocols or domain names.
+ UrlFormat:
+ enabled: false
+
+ # URLs should always be enclosed within quotes.
+ UrlQuotes:
+ enabled: false
+
+ # Properties, like color and font, are easier to read and maintain
+ # when defined using variables rather than literals.
+ VariableForProperty:
+ enabled: false
+ # Avoid vendor prefixes. Or rather: don't write them yourself.
VendorPrefix:
enabled: false
diff --git a/CHANGELOG b/CHANGELOG
index 88edf1e8484..c072cada732 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,15 +1,27 @@
Please view this file on the master branch, on stable branches it's out of date.
v 8.7.0 (unreleased)
- - Don't attempt to look up an avatar in repo if repo directory does not exist (Stan hu)
+ - All images in discussions and wikis now link to their source files !3464 (Connor Shea).
+ - Return status code 303 after a branch DELETE operation to avoid project deletion (Stan Hu)
+ - Improved Markdown rendering performance !3389 (Yorick Peterse)
+ - Don't attempt to look up an avatar in repo if repo directory does not exist (Stan Hu)
+ - Expose project badges in project settings
- Preserve time notes/comments have been updated at when moving issue
- Make HTTP(s) label consistent on clone bar (Stan Hu)
- Expose label description in API (Mariusz Jachimowicz)
- - Allow back dating on issues when created through the API
+ - Allow back dating on issues when created through the API
+ - Fix Error 500 after renaming a project path (Stan Hu)
- Fix avatar stretching by providing a cropping feature
+ - API: Expose `subscribed` for issues and merge requests (Robert Schilling)
+ - Allow SAML to handle external users based on user's information !3530
+ - Add endpoints to archive or unarchive a project !3372
- Add links to CI setup documentation from project settings and builds pages
- Handle nil descriptions in Slack issue messages (Stan Hu)
+ - Add default scope to projects to exclude projects pending deletion
+ - Ensure empty recipients are rejected in BuildsEmailService
+ - API: Ability to filter milestones by state `active` and `closed` (Robert Schilling)
- Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.)
+ - Better errors handling when creating milestones inside groups
- Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.)
- Gracefully handle notes on deleted commits in merge requests (Stan Hu)
- Decouple membership and notifications
@@ -17,6 +29,61 @@ v 8.7.0 (unreleased)
v 8.6.2 (unreleased)
- Comments on confidential issues don't show up in activity feed to non-members
- Fix NoMethodError when visiting CI root path at `/ci`
+ - Fix creation of merge requests for orphaned branches (Stan Hu)
+ - Fall back to `In-Reply-To` and `References` headers when sub-addressing is not available (David Padilla)
+ - Remove "Congratulations!" tweet button on newly-created project. (Connor Shea)
+ - Fix admin/projects when using visibility levels on search (PotHix)
+ - Build status notifications
+ - API: Expose user location (Robert Schilling)
+ - ClosingIssueExtractor regex now also works with colons. e.g. "Fixes: #1234" !3591
+ - Update number of Todos in the sidebar when it's marked as "Done". !3600
+
+v 8.6.5
+ - Fix importing from GitHub Enterprise. !3529
+ - Perform the language detection after updating merge requests in `GitPushService`, leading to faster visual feedback for the end-user. !3533
+ - Check permissions when user attempts to import members from another project. !3535
+ - Only update repository language if it is not set to improve performance. !3556
+ - Return status code 303 after a branch DELETE operation to avoid project deletion (Stan Hu). !3583
+ - Unblock user when active_directory is disabled and it can be found !3550
+ - Fix a 2FA authentication spoofing vulnerability.
+
+v 8.6.4
+ - Don't attempt to fetch any tags from a forked repo (Stan Hu)
+
+v 8.6.3
+ - Mentions on confidential issues doesn't create todos for non-members. !3374
+ - Destroy related todos when an Issue/MR is deleted. !3376
+ - Fix error 500 when target is nil on todo list. !3376
+ - Fix copying uploads when moving issue to another project. !3382
+ - Ensuring Merge Request API returns boolean values for work_in_progress (Abhi Rao). !3432
+ - Fix raw/rendered diff producing different results on merge requests. !3450
+ - Fix commit comment alignment (Stan Hu). !3466
+ - Fix Error 500 when searching for a comment in a project snippet. !3468
+ - Allow temporary email as notification email. !3477
+ - Fix issue with dropdowns not selecting values. !3478
+ - Update gitlab-shell version and doc to 2.6.12. gitlab-org/gitlab-ee!280
+
+v 8.6.2
+ - Fix dropdown alignment. !3298
+ - Fix issuable sidebar overlaps on tablet. !3299
+ - Make dropdowns pixel perfect. !3337
+ - Fix order of steps to prevent PostgreSQL errors when running migration. !3355
+ - Fix bold text in issuable sidebar. !3358
+ - Fix error with anonymous token in applications settings. !3362
+ - Fix the milestone 'upcoming' filter. !3364 + !3368
+ - Fix comments on confidential issues showing up in activity feed to non-members. !3375
+ - Fix `NoMethodError` when visiting CI root path at `/ci`. !3377
+ - Add a tooltip to new branch button in issue page. !3380
+ - Fix an issue hiding the password form when signed-in with a linked account. !3381
+ - Add links to CI setup documentation from project settings and builds pages. !3384
+ - Fix an issue with width of project select dropdown. !3386
+ - Remove redundant `require`s from Banzai files. !3391
+ - Fix error 500 with cancel button on issuable edit form. !3392 + !3417
+ - Fix background when editing a highlighted note. !3423
+ - Remove tabstop from the WIP toggle links. !3426
+ - Ensure private project snippets are not viewable by unauthorized people.
+ - Gracefully handle notes on deleted commits in merge requests (Stan Hu). !3402
+ - Fixed issue with notification settings not saving. !3452
v 8.6.1
- Add option to reload the schema before restoring a database backup. !2807
@@ -89,6 +156,7 @@ v 8.6.0
- Add main language of a project in the list of projects (Tiago Botelho)
- Add #upcoming filter to Milestone filter (Tiago Botelho)
- Add ability to show archived projects on dashboard, explore and group pages
+ - Remove fork link closes all merge requests opened on source project (Florent Baldino)
- Move group activity to separate page
- Create external users which are excluded of internal and private projects unless access was explicitly granted
- Continue parameters are checked to ensure redirection goes to the same instance
@@ -97,6 +165,12 @@ v 8.6.0
- Trigger a todo for mentions on commits page
- Let project owners and admins soft delete issues and merge requests
+v 8.5.10
+ - Fix a 2FA authentication spoofing vulnerability.
+
+v 8.5.9
+ - Don't attempt to fetch any tags from a forked repo (Stan Hu).
+
v 8.5.8
- Bump Git version requirement to 2.7.4
@@ -238,6 +312,15 @@ v 8.5.0
- Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul)
- Add Todos
+v 8.4.8
+ - Fix a 2FA authentication spoofing vulnerability.
+
+v 8.4.7
+ - Don't attempt to fetch any tags from a forked repo (Stan Hu).
+
+v 8.4.6
+ - Bump Git version requirement to 2.7.4
+
v 8.4.5
- No CE-specific changes
@@ -351,6 +434,15 @@ v 8.4.0
- Add IP check against DNSBLs at account sign-up
- Added cache:key to .gitlab-ci.yml allowing to fine tune the caching
+v 8.3.7
+ - Fix a 2FA authentication spoofing vulnerability.
+
+v 8.3.6
+ - Don't attempt to fetch any tags from a forked repo (Stan Hu).
+
+v 8.3.5
+ - Bump Git version requirement to 2.7.4
+
v 8.3.4
- Use gitlab-workhorse 0.5.4 (fixes API routing bug)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 511336f384c..1f26a5d7eaf 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -448,7 +448,7 @@ merge request:
- multi-line method chaining style **Option B**: dot `.` on previous line
- string literal quoting style **Option A**: single quoted by default
1. [Rails](https://github.com/bbatsov/rails-style-guide)
-1. [Testing](https://github.com/thoughtbot/guides/tree/master/style/testing)
+1. [Testing](doc/development/testing.md)
1. [CoffeeScript](https://github.com/thoughtbot/guides/tree/master/style/coffeescript)
1. [SCSS styleguide][scss-styleguide]
1. [Shell commands](doc/development/shell_commands.md) created by GitLab
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 39e898a4f95..7486fdbc50b 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-0.7.1
+0.7.2
diff --git a/Gemfile b/Gemfile
index 006e53e0c10..0279b4ac47e 100644
--- a/Gemfile
+++ b/Gemfile
@@ -214,7 +214,7 @@ gem 'jquery-rails', '~> 4.0.0'
gem 'jquery-scrollto-rails', '~> 1.4.3'
gem 'jquery-ui-rails', '~> 5.0.0'
gem 'raphael-rails', '~> 2.1.2'
-gem 'request_store', '~> 1.2.0'
+gem 'request_store', '~> 1.3.0'
gem 'select2-rails', '~> 3.5.9'
gem 'virtus', '~> 1.0.1'
gem 'net-ssh', '~> 3.0.1'
@@ -290,7 +290,7 @@ group :development, :test do
gem 'rubocop', '~> 0.38.0', require: false
gem 'scss_lint', '~> 0.47.0', require: false
gem 'coveralls', '~> 0.8.2', require: false
- gem 'simplecov', '~> 0.10.0', require: false
+ gem 'simplecov', '~> 0.11.0', require: false
gem 'flog', require: false
gem 'flay', require: false
gem 'bundler-audit', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index da27c62acbf..1ba8d748db1 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -99,7 +99,7 @@ GEM
bullet (5.0.0)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.9.0)
- bundler-audit (0.4.0)
+ bundler-audit (0.5.0)
bundler (~> 1.2)
thor (~> 0.18)
byebug (8.2.1)
@@ -126,9 +126,9 @@ GEM
coderay (1.1.0)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
- coffee-rails (4.1.0)
+ coffee-rails (4.1.1)
coffee-script (>= 2.2.0)
- railties (>= 4.0.0, < 5.0)
+ railties (>= 4.0.0, < 5.1.x)
coffee-script (2.4.1)
coffee-script-source
execjs
@@ -136,10 +136,9 @@ GEM
colorize (0.7.7)
concurrent-ruby (1.0.0)
connection_pool (2.2.0)
- coveralls (0.8.9)
+ coveralls (0.8.13)
json (~> 1.8)
- rest-client (>= 1.6.8, < 2)
- simplecov (~> 0.10.0)
+ simplecov (~> 0.11.0)
term-ansicolor (~> 1.3)
thor (~> 0.19.1)
tins (~> 1.6.0)
@@ -176,8 +175,6 @@ GEM
diff-lcs (1.2.5)
diffy (3.0.7)
docile (1.1.5)
- domain_name (0.5.25)
- unf (>= 0.0.5, < 1.0.0)
doorkeeper (2.2.2)
railties (>= 3.2)
dropzonejs-rails (0.7.2)
@@ -421,8 +418,6 @@ GEM
nokogiri (~> 1.6.0)
ruby_parser (~> 3.5)
htmlentities (4.3.4)
- http-cookie (1.0.2)
- domain_name (~> 0.5)
http_parser.rb (0.5.3)
httparty (0.13.7)
json (~> 1.8)
@@ -480,7 +475,6 @@ GEM
nested_form (0.3.2)
net-ldap (0.12.1)
net-ssh (3.0.1)
- netrc (0.11.0)
newrelic_rpm (3.14.1.311)
nokogiri (1.6.7.2)
mini_portile2 (~> 2.0.0.rc2)
@@ -652,15 +646,11 @@ GEM
redis-store (~> 1.1.0)
redis-store (1.1.7)
redis (>= 2.2)
- request_store (1.2.1)
+ request_store (1.3.0)
rerun (0.11.0)
listen (~> 3.0)
responders (2.1.1)
railties (>= 4.2.0, < 5.1)
- rest-client (1.8.0)
- http-cookie (>= 1.0.2, < 2.0)
- mime-types (>= 1.16, < 3.0)
- netrc (~> 0.7)
rinku (1.7.3)
rotp (2.1.1)
rouge (1.10.1)
@@ -754,7 +744,7 @@ GEM
rufus-scheduler (>= 2.0.24)
sidekiq (>= 4.0.0)
simple_oauth (0.1.9)
- simplecov (0.10.0)
+ simplecov (0.11.2)
docile (~> 1.1.0)
json (~> 1.8)
simplecov-html (~> 0.10.0)
@@ -845,7 +835,7 @@ GEM
underscore-rails (1.8.3)
unf (0.1.4)
unf_ext
- unf_ext (0.0.7.1)
+ unf_ext (0.0.7.2)
unicode-display_width (1.0.2)
unicorn (4.9.0)
kgio (~> 2.6)
@@ -1011,7 +1001,7 @@ DEPENDENCIES
redcarpet (~> 3.3.3)
redis-namespace
redis-rails (~> 4.0.0)
- request_store (~> 1.2.0)
+ request_store (~> 1.3.0)
rerun (~> 0.11.0)
responders (~> 2.0)
rouge (~> 1.10.1)
@@ -1032,7 +1022,7 @@ DEPENDENCIES
shoulda-matchers (~> 2.8.0)
sidekiq (~> 4.0)
sidekiq-cron (~> 0.4.0)
- simplecov (~> 0.10.0)
+ simplecov (~> 0.11.0)
sinatra (~> 1.4.4)
six (~> 0.2.0)
slack-notifier (~> 1.2.0)
diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee
index 47b080406d4..6a670d5e887 100644
--- a/app/assets/javascripts/awards_handler.coffee
+++ b/app/assets/javascripts/awards_handler.coffee
@@ -1,5 +1,5 @@
class @AwardsHandler
- constructor: (@post_emoji_url, @noteable_type, @noteable_id, @aliases) ->
+ constructor: (@get_emojis_url, @post_emoji_url, @noteable_type, @noteable_id, @aliases) ->
$(".js-add-award").on "click", (event) =>
event.stopPropagation()
event.preventDefault()
@@ -34,7 +34,7 @@ class @AwardsHandler
$("#emoji_search").focus()
else
$('.js-add-award').addClass "is-loading"
- $.get "/emojis", (response) =>
+ $.get @get_emojis_url, (response) =>
$('.js-add-award').removeClass "is-loading"
$(".js-award-holder").append response
setTimeout =>
diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee
index f5e1ca9860d..70fd6f50e9c 100644
--- a/app/assets/javascripts/dispatcher.js.coffee
+++ b/app/assets/javascripts/dispatcher.js.coffee
@@ -146,15 +146,11 @@ class Dispatcher
when 'project_members', 'deploy_keys', 'hooks', 'services', 'protected_branches'
shortcut_handler = new ShortcutsNavigation()
-
# If we haven't installed a custom shortcut handler, install the default one
if not shortcut_handler
new Shortcuts()
initSearch: ->
- opts = $('.search-autocomplete-opts')
- path = opts.data('autocomplete-path')
- project_id = opts.data('autocomplete-project-id')
- project_ref = opts.data('autocomplete-project-ref')
- new SearchAutocomplete(path, project_id, project_ref)
+ # Only when search form is present
+ new SearchAutocomplete() if $('.search').length
diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee
index dd0465b9358..e8d25591f63 100644
--- a/app/assets/javascripts/gl_dropdown.js.coffee
+++ b/app/assets/javascripts/gl_dropdown.js.coffee
@@ -1,8 +1,13 @@
class GitLabDropdownFilter
BLUR_KEYCODES = [27, 40]
+ ARROW_KEY_CODES = [38, 40]
HAS_VALUE_CLASS = "has-value"
constructor: (@input, @options) ->
+ {
+ @filterInputBlur = true
+ } = @options
+
$inputContainer = @input.parent()
$clearButton = $inputContainer.find('.js-dropdown-input-clear')
@@ -18,22 +23,26 @@ class GitLabDropdownFilter
# Key events
timeout = ""
@input.on "keyup", (e) =>
+ keyCode = e.which
+
+ return if ARROW_KEY_CODES.indexOf(keyCode) >= 0
+
if @input.val() isnt "" and !$inputContainer.hasClass HAS_VALUE_CLASS
$inputContainer.addClass HAS_VALUE_CLASS
else if @input.val() is "" and $inputContainer.hasClass HAS_VALUE_CLASS
$inputContainer.removeClass HAS_VALUE_CLASS
- if e.keyCode is 13 and @input.val() isnt ""
+ if keyCode is 13 and @input.val() isnt ""
if @options.enterCallback
@options.enterCallback()
return
clearTimeout timeout
timeout = setTimeout =>
- blur_field = @shouldBlur e.keyCode
+ blur_field = @shouldBlur keyCode
search_text = @input.val()
- if blur_field
+ if blur_field and @filterInputBlur
@input.blur()
if @options.remote
@@ -92,38 +101,61 @@ class GitLabDropdown
LOADING_CLASS = "is-loading"
PAGE_TWO_CLASS = "is-page-two"
ACTIVE_CLASS = "is-active"
+ currentIndex = -1
+
+ FILTER_INPUT = '.dropdown-input .dropdown-input-field'
constructor: (@el, @options) ->
- self = @
@dropdown = $(@el).parent()
+
+ # Set Defaults
+ {
+ # If no input is passed create a default one
+ @filterInput = @getElement(FILTER_INPUT)
+ @highlight = false
+ @filterInputBlur = true
+ @enterCallback = true
+ } = @options
+
+ self = @
+
+ # If selector was passed
+ if _.isString(@filterInput)
+ @filterInput = @getElement(@filterInput)
+
search_fields = if @options.search then @options.search.fields else [];
if @options.data
- # Remote data
- @remote = new GitLabDropdownRemote @options.data, {
- dataType: @options.dataType,
- beforeSend: @toggleLoading.bind(@)
- success: (data) =>
- @fullData = data
+ # If data is an array
+ if _.isArray @options.data
+ @fullData = @options.data
+ @parseData @options.data
+ else
+ # Remote data
+ @remote = new GitLabDropdownRemote @options.data, {
+ dataType: @options.dataType,
+ beforeSend: @toggleLoading.bind(@)
+ success: (data) =>
+ @fullData = data
- @parseData @fullData
- }
+ @parseData @fullData
+ }
- # Init filiterable
+ # Init filterable
if @options.filterable
- @input = @dropdown.find('.dropdown-input .dropdown-input-field')
-
- @filter = new GitLabDropdownFilter @input,
+ @filter = new GitLabDropdownFilter @filterInput,
+ filterInputBlur: @filterInputBlur
remote: @options.filterRemote
query: @options.data
keys: @options.search.fields
data: =>
return @fullData
callback: (data) =>
+ currentIndex = -1
@parseData data
- @highlightRow 1
enterCallback: =>
- @selectFirstRow()
+ if @enterCallback
+ @selectRowAtIndex 0
# Event listeners
@@ -145,10 +177,15 @@ class GitLabDropdown
selector = ".dropdown-page-one .dropdown-content a"
@dropdown.on "click", selector, (e) ->
- selected = self.rowClicked $(@)
+ $el = $(@)
+ selected = self.rowClicked $el
if self.options.clicked
- self.options.clicked(selected)
+ self.options.clicked(selected, $el, e)
+
+ # Finds an element inside wrapper element
+ getElement: (selector) ->
+ @dropdown.find selector
toggleLoading: ->
$('.dropdown-menu', @dropdown).toggleClass LOADING_CLASS
@@ -188,14 +225,19 @@ class GitLabDropdown
return true
opened: =>
+ @addArrowKeyEvent()
+
contentHtml = $('.dropdown-content', @dropdown).html()
if @remote && contentHtml is ""
@remote.execute()
if @options.filterable
- @dropdown.find(".dropdown-input-field").focus()
+ @filterInput.focus()
+
+ @dropdown.trigger('shown.gl.dropdown')
hidden: (e) =>
+ @removeArrayKeyEvent()
if @options.filterable
@dropdown
.find(".dropdown-input-field")
@@ -209,6 +251,8 @@ class GitLabDropdown
if @options.hidden
@options.hidden.call(@,e)
+ @dropdown.trigger('hidden.gl.dropdown')
+
# Render the full menu
renderMenu: (html) ->
@@ -233,13 +277,19 @@ class GitLabDropdown
renderItem: (data) ->
html = ""
+ # Divider
return "<li class='divider'></li>" if data is "divider"
+ # Separator is a full-width divider
+ return "<li class='separator'></li>" if data is "separator"
+
+ # Header
+ return "<li class='dropdown-header'>#{data.header}</li>" if data.header?
+
if @options.renderRow
# Call the render function
html = @options.renderRow(data)
else
- selected = if @options.isSelected then @options.isSelected(data) else false
if not selected
value = if @options.id then @options.id(data) else data.id
fieldName = @options.fieldName
@@ -247,35 +297,54 @@ class GitLabDropdown
if field.length
selected = true
- url = if @options.url then @options.url(data) else "#"
- text = if @options.text then @options.text(data) else ""
+ # Set URL
+ if @options.url?
+ url = @options.url(data)
+ else
+ url = if data.url? then data.url else '#'
+
+ # Set Text
+ if @options.text?
+ text = @options.text(data)
+ else
+ text = if data.text? then data.text else ''
+
cssClass = "";
if selected
cssClass = "is-active"
- html = "<li>"
- html += "<a href='#{url}' class='#{cssClass}'>"
- html += text
- html += "</a>"
- html += "</li>"
+ if @highlight
+ text = @highlightTextMatches(text, @filterInput.val())
+
+ html = "<li>
+ <a href='#{url}' class='#{cssClass}'>
+ #{text}
+ </a>
+ </li>"
return html
+ highlightTextMatches: (text, term) ->
+ occurrences = fuzzaldrinPlus.match(text, term)
+ text.split('').map((character, i) ->
+ if i in occurrences then "<b>#{character}</b>" else character
+ ).join('')
+
noResults: ->
- html = "<li>"
- html += "<a href='#' class='dropdown-menu-empty-link is-focused'>"
- html += "No matching results."
- html += "</a>"
- html += "</li>"
+ html = "<li class='dropdown-menu-empty-link'>
+ <a href='#' class='is-focused'>
+ No matching results.
+ </a>
+ </li>"
highlightRow: (index) ->
- if @input.val() isnt ""
+ if @filterInput.val() isnt ""
selector = '.dropdown-content li:first-child a'
if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one .dropdown-content li:first-child a"
- $(selector).addClass 'is-focused'
+ @getElement(selector).addClass 'is-focused'
rowClicked: (el) ->
fieldName = @options.fieldName
@@ -284,7 +353,7 @@ class GitLabDropdown
selectedObject = @renderedData[selectedIndex]
value = if @options.id then @options.id(selectedObject, el) else selectedObject.id
field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']")
-
+
if el.hasClass(ACTIVE_CLASS)
el.removeClass(ACTIVE_CLASS)
field.remove()
@@ -292,6 +361,8 @@ class GitLabDropdown
# Toggle the dropdown label
if @options.toggleLabel
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel
+ else
+ selectedObject
else
if !value?
field.remove()
@@ -307,24 +378,93 @@ class GitLabDropdown
if @options.toggleLabel
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject)
if value?
- if !field.length
+ if !field.length and fieldName
# Create hidden input for form
input = "<input type='hidden' name='#{fieldName}' value='#{value}' />"
if @options.inputId?
input = $(input)
.attr('id', @options.inputId)
@dropdown.before input
+ else
+ field.val value
return selectedObject
- selectFirstRow: ->
- selector = '.dropdown-content li:first-child a'
+ selectRowAtIndex: (index) ->
+ selector = ".dropdown-content li:not(.divider):eq(#{index}) a"
+
if @dropdown.find(".dropdown-toggle-page").length
- selector = ".dropdown-page-one .dropdown-content li:first-child a"
+ selector = ".dropdown-page-one #{selector}"
# simulate a click on the first link
- $(selector).trigger "click"
+ $(selector, @dropdown).trigger "click"
+
+ addArrowKeyEvent: ->
+ ARROW_KEY_CODES = [38, 40]
+ $input = @dropdown.find(".dropdown-input-field")
+
+ selector = '.dropdown-content li:not(.divider)'
+ if @dropdown.find(".dropdown-toggle-page").length
+ selector = ".dropdown-page-one #{selector}"
+
+ $('body').on 'keydown', (e) =>
+ currentKeyCode = e.which
+
+ if ARROW_KEY_CODES.indexOf(currentKeyCode) >= 0
+ e.preventDefault()
+ e.stopImmediatePropagation()
+
+ PREV_INDEX = currentIndex
+ $listItems = $(selector, @dropdown)
+
+ # if @options.filterable
+ # $input.blur()
+
+ if currentKeyCode is 40
+ # Move down
+ currentIndex += 1 if currentIndex < ($listItems.length - 1)
+ else if currentKeyCode is 38
+ # Move up
+ currentIndex -= 1 if currentIndex > 0
+
+ @highlightRowAtIndex($listItems, currentIndex) if currentIndex isnt PREV_INDEX
+
+ return false
+
+ if currentKeyCode is 13
+ @selectRowAtIndex currentIndex
+
+ removeArrayKeyEvent: ->
+ $('body').off 'keydown'
+
+ highlightRowAtIndex: ($listItems, index) ->
+ # Remove the class for the previously focused row
+ $('.is-focused', @dropdown).removeClass 'is-focused'
+
+ # Update the class for the row at the specific index
+ $listItem = $listItems.eq(index)
+ $listItem.find('a:first-child').addClass "is-focused"
+
+ # Dropdown content scroll area
+ $dropdownContent = $listItem.closest('.dropdown-content')
+ dropdownScrollTop = $dropdownContent.scrollTop()
+ dropdownContentHeight = $dropdownContent.outerHeight()
+ dropdownContentTop = $dropdownContent.prop('offsetTop')
+ dropdownContentBottom = dropdownContentTop + dropdownContentHeight
+
+ # Get the offset bottom of the list item
+ listItemHeight = $listItem.outerHeight()
+ listItemTop = $listItem.prop('offsetTop')
+ listItemBottom = listItemTop + listItemHeight
+
+ if listItemBottom > dropdownContentBottom + dropdownScrollTop
+ # Scroll the dropdown content down
+ $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom)
+ else if listItemTop < dropdownContentTop + dropdownScrollTop
+ # Scroll the dropdown content up
+ $dropdownContent.scrollTop(listItemTop - dropdownContentTop)
$.fn.glDropdown = (opts) ->
return @.each ->
- new GitLabDropdown @, opts
+ if (!$.data @, 'glDropdown')
+ $.data(@, 'glDropdown', new GitLabDropdown @, opts)
diff --git a/app/assets/javascripts/issuable_context.js.coffee b/app/assets/javascripts/issuable_context.js.coffee
index 6fc924d3d66..2f19513a831 100644
--- a/app/assets/javascripts/issuable_context.js.coffee
+++ b/app/assets/javascripts/issuable_context.js.coffee
@@ -9,7 +9,7 @@ class @IssuableContext
$(".issuable-sidebar .inline-update").on "change", ".js-assignee", ->
$(this).submit()
- $(document).on "click",".edit-link", (e) ->
+ $(document).off("click", ".edit-link").on "click",".edit-link", (e) ->
$block = $(@).parents('.block')
$selectbox = $block.find('.selectbox')
if $selectbox.is(':visible')
diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee
index d663e34871c..946d83b7bdd 100644
--- a/app/assets/javascripts/issue.js.coffee
+++ b/app/assets/javascripts/issue.js.coffee
@@ -6,25 +6,10 @@ class @Issue
constructor: ->
# Prevent duplicate event bindings
@disableTaskList()
- @fixAffixScroll()
if $('a.btn-close').length
@initTaskList()
@initIssueBtnEventListeners()
- fixAffixScroll: ->
- fixAffix = ->
- $discussion = $('.issuable-discussion')
- $sidebar = $('.issuable-sidebar')
- if $sidebar.hasClass('no-affix')
- $sidebar.removeClass(['affix-top','affix'])
- discussionHeight = $discussion.height()
- sidebarHeight = $sidebar.height()
- if sidebarHeight > discussionHeight
- $discussion.height(sidebarHeight + 50)
- $sidebar.addClass('no-affix')
- $(window).on('resize', fixAffix)
- fixAffix()
-
initTaskList: ->
$('.detail-page-description .js-task-list-container').taskList('enable')
$(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList
@@ -49,7 +34,7 @@ class @Issue
issueStatus = if isClose then 'close' else 'open'
new Flash(issueFailMessage, 'alert')
success: (data, textStatus, jqXHR) ->
- if data.saved
+ if 'id' of data
$(document).trigger('issuable:change');
if isClose
$('a.btn-close').addClass('hidden')
diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee
index b1479bfb449..0d9f2094c2a 100644
--- a/app/assets/javascripts/issues.js.coffee
+++ b/app/assets/javascripts/issues.js.coffee
@@ -26,6 +26,20 @@
$(".selected_issue").bind "change", Issues.checkChanged
+ # Update state filters if present in page
+ updateStateFilters: ->
+ stateFilters = $('.issues-state-filters')
+ newParams = {}
+ paramKeys = ['author_id', 'label_name', 'milestone_title', 'assignee_id', 'issue_search']
+
+ for paramKey in paramKeys
+ newParams[paramKey] = gl.utils.getUrlParameter(paramKey) or ''
+
+ if stateFilters.length
+ stateFilters.find('a').each ->
+ initialUrl = $(this).attr 'href'
+ $(this).attr 'href', gl.utils.mergeUrlParams(newParams, initialUrl)
+
# Make sure we trigger ajax request only after user stop typing
initSearch: ->
@timer = null
@@ -54,6 +68,7 @@
# Change url so if user reload a page - search results are saved
history.replaceState {page: issuesUrl}, document.title, issuesUrl
Issues.reload()
+ Issues.updateStateFilters()
dataType: "json"
checkChanged: ->
diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee
index e69a9e3e4b1..d1fe116397a 100644
--- a/app/assets/javascripts/labels_select.js.coffee
+++ b/app/assets/javascripts/labels_select.js.coffee
@@ -16,6 +16,7 @@ class @LabelsSelect
abilityName = $dropdown.data('ability-name')
$selectbox = $dropdown.closest('.selectbox')
$block = $selectbox.closest('.block')
+ $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span')
$value = $block.find('.value')
$loading = $block.find('.block-loading').fadeOut()
@@ -142,6 +143,7 @@ class @LabelsSelect
if not selected.length
data[abilityName].label_ids = ['']
$loading.fadeIn()
+ $dropdown.trigger('loading.gl.dropdown')
$.ajax(
type: 'PUT'
url: issueUpdateURL
@@ -149,15 +151,20 @@ class @LabelsSelect
data: data
).done (data) ->
$loading.fadeOut()
+ $dropdown.trigger('loaded.gl.dropdown')
$selectbox.hide()
data.issueURLSplit = issueURLSplit
- if not data.labels.length
- template = labelNoneHTMLTemplate()
- else
+ labelCount = 0
+ if data.labels.length
template = labelHTMLTemplate(data)
- href = $value
- .show()
- .html(template)
+ labelCount = data.labels.length
+ else
+ template = labelNoneHTMLTemplate()
+ $value
+ .removeAttr('style')
+ .html(template)
+ $sidebarCollapsedValue.text(labelCount)
+
$value
.find('a')
.each((i) ->
@@ -226,7 +233,8 @@ class @LabelsSelect
hidden: ->
$selectbox.hide()
- $value.show()
+ # display:block overrides the hide-collapse rule
+ $value.removeAttr('style')
if $dropdown.hasClass 'js-multiselect'
saveLabelData()
diff --git a/app/assets/javascripts/lib/notify.js.coffee b/app/assets/javascripts/lib/notify.js.coffee
new file mode 100644
index 00000000000..3f9ca39912c
--- /dev/null
+++ b/app/assets/javascripts/lib/notify.js.coffee
@@ -0,0 +1,30 @@
+((w) ->
+ notificationGranted = (message, opts, onclick) ->
+ notification = new Notification(message, opts)
+
+ if onclick
+ notification.onclick = onclick
+
+ notifyPermissions = ->
+ if 'Notification' of window
+ Notification.requestPermission()
+
+ notifyMe = (message, body, icon, onclick) ->
+ opts =
+ body: body
+ icon: icon
+ # Let's check if the browser supports notifications
+ if !('Notification' of window)
+ # do nothing
+ else if Notification.permission == 'granted'
+ # If it's okay let's create a notification
+ notificationGranted message, opts, onclick
+ else if Notification.permission != 'denied'
+ Notification.requestPermission (permission) ->
+ # If the user accepts, let's create a notification
+ if permission == 'granted'
+ notificationGranted message, opts, onclick
+
+ w.notify = notifyMe
+ w.notifyPermissions = notifyPermissions
+) window
diff --git a/app/assets/javascripts/lib/url_utility.js.coffee b/app/assets/javascripts/lib/url_utility.js.coffee
new file mode 100644
index 00000000000..abd556e0b4e
--- /dev/null
+++ b/app/assets/javascripts/lib/url_utility.js.coffee
@@ -0,0 +1,31 @@
+((w) ->
+
+ w.gl ?= {}
+ w.gl.utils ?= {}
+
+ w.gl.utils.getUrlParameter = (sParam) ->
+ sPageURL = decodeURIComponent(window.location.search.substring(1))
+ sURLVariables = sPageURL.split('&')
+ sParameterName = undefined
+ i = 0
+ while i < sURLVariables.length
+ sParameterName = sURLVariables[i].split('=')
+ if sParameterName[0] is sParam
+ return if sParameterName[1] is undefined then true else sParameterName[1]
+ i++
+
+ # #
+ # @param {Object} params - url keys and value to merge
+ # @param {String} url
+ # #
+ w.gl.utils.mergeUrlParams = (params, url) ->
+ newUrl = decodeURIComponent(url)
+ for paramName, paramValue of params
+ pattern = new RegExp "\\b(#{paramName}=).*?(&|$)"
+ if url.search(pattern) >= 0
+ newUrl = newUrl.replace pattern, "$1#{paramValue}$2"
+ else
+ newUrl = "#{newUrl}#{(if newUrl.indexOf('?') > 0 then '&' else '?')}#{paramName}=#{paramValue}"
+ newUrl
+
+) window
diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee
index 6af5a48a0bb..1f46e331427 100644
--- a/app/assets/javascripts/merge_request.js.coffee
+++ b/app/assets/javascripts/merge_request.js.coffee
@@ -15,8 +15,6 @@ class @MergeRequest
this.$('.show-all-commits').on 'click', =>
this.showAllCommits()
- @fixAffixScroll();
-
@initTabs()
# Prevent duplicate event bindings
@@ -30,20 +28,6 @@ class @MergeRequest
$: (selector) ->
this.$el.find(selector)
- fixAffixScroll: ->
- fixAffix = ->
- $discussion = $('.issuable-discussion')
- $sidebar = $('.issuable-sidebar')
- if $sidebar.hasClass('no-affix')
- $sidebar.removeClass(['affix-top','affix'])
- discussionHeight = $discussion.height()
- sidebarHeight = $sidebar.height()
- if sidebarHeight > discussionHeight
- $discussion.height(sidebarHeight + 50)
- $sidebar.addClass('no-affix')
- $(window).on('resize', fixAffix)
- fixAffix()
-
initTabs: ->
if @opts.action != 'new'
# `MergeRequests#new` has no tab-persisting or lazy-loading behavior
diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee
index 738ffc8343b..84a8887fbce 100644
--- a/app/assets/javascripts/merge_request_widget.js.coffee
+++ b/app/assets/javascripts/merge_request_widget.js.coffee
@@ -2,13 +2,20 @@ class @MergeRequestWidget
# Initialize MergeRequestWidget behavior
#
# check_enable - Boolean, whether to check automerge status
- # url_to_automerge_check - String, URL to use to check automerge status
- # current_status - String, current automerge status
- # ci_enable - Boolean, whether a CI service is enabled
- # url_to_ci_check - String, URL to use to check CI status
+ # merge_check_url - String, URL to use to check automerge status
+ # ci_status_url - String, URL to use to check CI status
#
+
constructor: (@opts) ->
- modal = $('#modal_merge_info').modal(show: false)
+ $('#modal_merge_info').modal(show: false)
+ @firstCICheck = true
+ @readyForCICheck = true
+ clearInterval @fetchBuildStatusInterval
+
+ @pollCIStatus()
+ notifyPermissions()
+
+ setOpts: (@opts) ->
mergeInProgress: (deleteSourceBranch = false)->
$.ajax
@@ -27,18 +34,61 @@ class @MergeRequestWidget
dataType: 'json'
getMergeStatus: ->
- $.get @opts.url_to_automerge_check, (data) ->
+ $.get @opts.merge_check_url, (data) ->
$('.mr-state-widget').replaceWith(data)
- getCiStatus: ->
- if @opts.ci_enable
- $.get @opts.url_to_ci_check, (data) =>
- this.showCiState data.status
+ ciLabelForStatus: (status) ->
+ if status == 'success'
+ 'passed'
+ else
+ status
+
+ pollCIStatus: ->
+ @fetchBuildStatusInterval = setInterval ( =>
+ return if not @readyForCICheck
+
+ @getCIStatus(true)
+
+ @readyForCICheck = false
+ ), 10000
+
+ getCIStatus: (showNotification) ->
+ _this = @
+ $('.ci-widget-fetching').show()
+
+ $.getJSON @opts.ci_status_url, (data) =>
+ @readyForCICheck = true
+
+ if @firstCICheck
+ @firstCICheck = false
+ @opts.ci_status = data.status
+
+ if @opts.ci_status is ''
+ @opts.ci_status = data.status
+ return
+
+ if data.status isnt @opts.ci_status
+ @showCIStatus data.status
if data.coverage
- this.showCiCoverage data.coverage
- , 'json'
+ @showCICoverage data.coverage
+
+ if showNotification
+ message = @opts.ci_message.replace('{{status}}', @ciLabelForStatus(data.status))
+ message = message.replace('{{sha}}', data.sha)
+ message = message.replace('{{title}}', data.title)
+
+ notify(
+ "Build #{@ciLabelForStatus(data.status)}",
+ message,
+ @opts.gitlab_icon,
+ ->
+ @close()
+ Turbolinks.visit _this.opts.builds_path
+ )
+
+ @opts.ci_status = data.status
- showCiState: (state) ->
+ showCIStatus: (state) ->
$('.ci_widget').hide()
allowed_states = ["failed", "canceled", "running", "pending", "success", "skipped", "not_found"]
if state in allowed_states
@@ -52,7 +102,7 @@ class @MergeRequestWidget
$('.ci_widget.ci-error').show()
@setMergeButtonClass('btn-danger')
- showCiCoverage: (coverage) ->
+ showCICoverage: (coverage) ->
text = 'Coverage ' + coverage + '%'
$('.ci_widget:visible .ci-coverage').text(text)
diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee
index d61d03791fa..f73127f49f0 100644
--- a/app/assets/javascripts/milestone_select.js.coffee
+++ b/app/assets/javascripts/milestone_select.js.coffee
@@ -18,6 +18,7 @@ class @MilestoneSelect
abilityName = $dropdown.data('ability-name')
$selectbox = $dropdown.closest('.selectbox')
$block = $selectbox.closest('.block')
+ $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon')
$value = $block.find('.value')
$loading = $block.find('.block-loading').fadeOut()
@@ -80,7 +81,9 @@ class @MilestoneSelect
milestone.name is selectedMilestone
hidden: ->
$selectbox.hide()
- $value.show()
+
+ # display:block overrides the hide-collapse rule
+ $value.removeAttr('style')
clicked: (selected) ->
if $dropdown.hasClass 'js-filter-bulk-update'
return
@@ -88,11 +91,9 @@ class @MilestoneSelect
if $dropdown.hasClass('js-filter-submit')
if selected.name?
selectedMilestone = selected.name
- else if selected.title?
- selectedMilestone = selected.title
else
selectedMilestone = ''
- $dropdown.parents('form').submit()
+ Issues.filterResults $dropdown.closest('form')
else
selected = $selectbox
.find('input[type="hidden"]')
@@ -102,20 +103,22 @@ class @MilestoneSelect
data[abilityName].milestone_id = selected
$loading
.fadeIn()
+ $dropdown.trigger('loading.gl.dropdown')
$.ajax(
type: 'PUT'
url: issueUpdateURL
data: data
).done (data) ->
+ $dropdown.trigger('loaded.gl.dropdown')
$loading.fadeOut()
$selectbox.hide()
- $milestoneLink = $value
- .show()
- .find('a')
+ $value.removeAttr('style')
if data.milestone?
data.milestone.namespace = _this.currentProject.namespace
data.milestone.path = _this.currentProject.path
$value.html(milestoneLinkTemplate(data.milestone))
+ $sidebarCollapsedValue.find('span').text(data.milestone.title)
else
$value.html(milestoneLinkNoneTemplate)
+ $sidebarCollapsedValue.find('span').text('No')
)
diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee
index ff06c57f2b5..86e3b860fcb 100644
--- a/app/assets/javascripts/notes.js.coffee
+++ b/app/assets/javascripts/notes.js.coffee
@@ -251,13 +251,11 @@ class @Notes
Sets some hidden fields in the form.
###
setupMainTargetNoteForm: ->
-
# find the form
form = $(".js-new-note-form")
- # insert the form after the button
- form.clone().replaceAll $(".js-main-target-form")
- form = form.prev("form")
+ # Set a global clone of the form for later cloning
+ @formClone = form.clone()
# show the form
@setupNoteForm(form)
@@ -266,9 +264,7 @@ class @Notes
form.removeClass "js-new-note-form"
form.addClass "js-main-target-form"
- # remove unnecessary fields and buttons
form.find("#note_line_code").remove()
- form.find(".js-close-discussion-note-form").remove()
###
General note form setup.
@@ -297,7 +293,14 @@ class @Notes
else
previewButton.removeClass("turn-on").addClass "turn-off"
+ textarea.on 'focus', ->
+ $(this).closest('.md-area').addClass 'is-focused'
+
+ textarea.on 'blur', ->
+ $(this).closest('.md-area').removeClass 'is-focused'
+
autosize(textarea)
+
new Autosave textarea, [
"Note"
form.find("#note_commit_id").val()
@@ -307,7 +310,6 @@ class @Notes
]
# remove notify commit author checkbox for non-commit notes
- form.find(".js-notify-commit-author").remove() if form.find("#note_noteable_type").val() isnt "Commit"
GitLab.GfmAutoComplete.setup()
new DropzoneInput(form)
form.show()
@@ -455,15 +457,15 @@ class @Notes
Shows the note form below the notes.
###
replyToDiscussionNote: (e) =>
- form = $(".js-new-note-form")
+ form = @formClone.clone()
replyLink = $(e.target).closest(".js-discussion-reply-button")
replyLink.hide()
# insert the form after the button
- form.clone().insertAfter replyLink
+ replyLink.after form
# show the form
- @setupDiscussionNoteForm(replyLink, replyLink.next("form"))
+ @setupDiscussionNoteForm(replyLink, form)
###
Shows the diff or discussion form and does some setup on it.
@@ -488,7 +490,9 @@ class @Notes
.text(form.find('.js-close-discussion-note-form').data('cancel-text'))
@setupNoteForm form
form.find(".js-note-text").focus()
- form.addClass "js-discussion-note-form"
+ form
+ .removeClass('js-main-target-form')
+ .addClass("discussion-form js-discussion-note-form")
###
Called when clicking on the "add a comment" button on the side of a diff line.
@@ -498,9 +502,8 @@ class @Notes
###
addDiffNote: (e) =>
e.preventDefault()
- link = e.currentTarget
- form = $(".js-new-note-form")
- row = $(link).closest("tr")
+ $link = $(e.currentTarget)
+ row = $link.closest("tr")
nextRow = row.next()
hasNotes = nextRow.is(".notes_holder")
addForm = false
@@ -509,7 +512,7 @@ class @Notes
# In parallel view, look inside the correct left/right pane
if @isParallelView()
- lineType = $(link).data("lineType")
+ lineType = $link.data("lineType")
targetContent += "." + lineType
rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\"></td><td class=\"notes_content parallel old\"></td><td class=\"notes_line\"></td><td class=\"notes_content parallel new\"></td></tr>"
@@ -531,11 +534,11 @@ class @Notes
addForm = true
if addForm
- newForm = form.clone()
+ newForm = @formClone.clone()
newForm.appendTo row.next().find(targetContent)
# show the form
- @setupDiscussionNoteForm $(link), newForm
+ @setupDiscussionNoteForm $link, newForm
###
Called in response to "cancel" on a diff note form.
@@ -560,7 +563,6 @@ class @Notes
cancelDiscussionForm: (e) =>
e.preventDefault()
- form = $(".js-new-note-form")
form = $(e.target).closest(".js-discussion-note-form")
@removeDiscussionNoteForm(form)
diff --git a/app/assets/javascripts/right_sidebar.js.coffee b/app/assets/javascripts/right_sidebar.js.coffee
new file mode 100644
index 00000000000..67403554340
--- /dev/null
+++ b/app/assets/javascripts/right_sidebar.js.coffee
@@ -0,0 +1,55 @@
+class @Sidebar
+ constructor: (currentUser) ->
+ @addEventListeners()
+
+ addEventListeners: ->
+ $('aside').on('click', '.sidebar-collapsed-icon', @sidebarCollapseClicked)
+ $('.dropdown').on('hidden.gl.dropdown', @sidebarDropdownHidden)
+ $('.dropdown').on('loading.gl.dropdown', @sidebarDropdownLoading)
+ $('.dropdown').on('loaded.gl.dropdown', @sidebarDropdownLoaded)
+
+ sidebarDropdownLoading: (e) ->
+ $sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon')
+ img = $sidebarCollapsedIcon.find('img')
+ i = $sidebarCollapsedIcon.find('i')
+ $loading = $('<i class="fa fa-spinner fa-spin"></i>')
+ if img.length
+ img.before($loading)
+ img.hide()
+ else if i.length
+ i.before($loading)
+ i.hide()
+
+ sidebarDropdownLoaded: (e) ->
+ $sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon')
+ img = $sidebarCollapsedIcon.find('img')
+ $sidebarCollapsedIcon.find('i.fa-spin').remove()
+ i = $sidebarCollapsedIcon.find('i')
+ if img.length
+ img.show()
+ else
+ i.show()
+
+
+ sidebarCollapseClicked: (e) ->
+ e.preventDefault()
+ $block = $(@).closest('.block')
+
+ $('aside')
+ .find('.gutter-toggle')
+ .trigger('click')
+ $editLink = $block.find('.edit-link')
+
+ if $editLink.length
+ $editLink.trigger('click')
+ $block.addClass('collapse-after-update')
+ $('.page-with-sidebar').addClass('with-overlay')
+
+ sidebarDropdownHidden: (e) ->
+ $block = $(@).closest('.block')
+ if $block.hasClass('collapse-after-update')
+ $block.removeClass('collapse-after-update')
+ $('.page-with-sidebar').removeClass('with-overlay')
+ $('aside')
+ .find('.gutter-toggle')
+ .trigger('click') \ No newline at end of file
diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee
index c1801365266..6a7b4ad1db7 100644
--- a/app/assets/javascripts/search_autocomplete.js.coffee
+++ b/app/assets/javascripts/search_autocomplete.js.coffee
@@ -1,11 +1,296 @@
class @SearchAutocomplete
- constructor: (search_autocomplete_path, project_id, project_ref) ->
- project_id = '' unless project_id
- project_ref = '' unless project_ref
- query = "?project_id=" + project_id + "&project_ref=" + project_ref
-
- $("#search").autocomplete
- source: search_autocomplete_path + query
- minLength: 1
- select: (event, ui) ->
- location.href = ui.item.url
+
+ KEYCODE =
+ ESCAPE: 27
+ BACKSPACE: 8
+ ENTER: 13
+
+ constructor: (opts = {}) ->
+ {
+ @wrap = $('.search')
+
+ @optsEl = @wrap.find('.search-autocomplete-opts')
+ @autocompletePath = @optsEl.data('autocomplete-path')
+ @projectId = @optsEl.data('autocomplete-project-id') || ''
+ @projectRef = @optsEl.data('autocomplete-project-ref') || ''
+
+ } = opts
+
+ # Dropdown Element
+ @dropdown = @wrap.find('.dropdown')
+ @dropdownContent = @dropdown.find('.dropdown-content')
+
+ @locationBadgeEl = @getElement('.search-location-badge')
+ @locationText = @getElement('.location-text')
+ @scopeInputEl = @getElement('#scope')
+ @searchInput = @getElement('.search-input')
+ @projectInputEl = @getElement('#search_project_id')
+ @groupInputEl = @getElement('#group_id')
+ @searchCodeInputEl = @getElement('#search_code')
+ @repositoryInputEl = @getElement('#repository_ref')
+ @clearInput = @getElement('.js-clear-input')
+
+ @saveOriginalState()
+
+ # Only when user is logged in
+ @createAutocomplete() if gon.current_user_id
+
+ @searchInput.addClass('disabled')
+
+ @saveTextLength()
+
+ @bindEvents()
+
+ # Finds an element inside wrapper element
+ getElement: (selector) ->
+ @wrap.find(selector)
+
+ saveOriginalState: ->
+ @originalState = @serializeState()
+
+ saveTextLength: ->
+ @lastTextLength = @searchInput.val().length
+
+ createAutocomplete: ->
+ @searchInput.glDropdown
+ filterInputBlur: false
+ filterable: true
+ filterRemote: true
+ highlight: true
+ enterCallback: false
+ filterInput: 'input#search'
+ search:
+ fields: ['text']
+ data: @getData.bind(@)
+ selectable: true
+ clicked: @onClick.bind(@)
+
+ getData: (term, callback) ->
+ _this = @
+
+ # Do not trigger request if input is empty
+ return if @searchInput.val() is ''
+
+ # Prevent multiple ajax calls
+ return if @loadingSuggestions
+
+ @loadingSuggestions = true
+
+ jqXHR = $.get(@autocompletePath, {
+ project_id: @projectId
+ project_ref: @projectRef
+ term: term
+ }, (response) ->
+ # Hide dropdown menu if no suggestions returns
+ if !response.length
+ _this.disableAutocomplete()
+ return
+
+ data = []
+
+ # List results
+ firstCategory = true
+ for suggestion in response
+
+ # Add group header before list each group
+ if lastCategory isnt suggestion.category
+ data.push 'separator' if !firstCategory
+
+ firstCategory = false if firstCategory
+
+ data.push
+ header: suggestion.category
+
+ lastCategory = suggestion.category
+
+ data.push
+ id: "#{suggestion.category.toLowerCase()}-#{suggestion.id}"
+ category: suggestion.category
+ text: suggestion.label
+ url: suggestion.url
+
+ # Add option to proceed with the search
+ if data.length
+ data.push('separator')
+ data.push
+ text: "Result name contains \"#{term}\""
+ url: "/search?\
+ search=#{term}\
+ &project_id=#{_this.projectInputEl.val()}\
+ &group_id=#{_this.groupInputEl.val()}"
+
+ callback(data)
+ ).always ->
+ _this.loadingSuggestions = false
+
+ serializeState: ->
+ {
+ # Search Criteria
+ search_project_id: @projectInputEl.val()
+ group_id: @groupInputEl.val()
+ search_code: @searchCodeInputEl.val()
+ repository_ref: @repositoryInputEl.val()
+ scope: @scopeInputEl.val()
+
+ # Location badge
+ _location: @locationText.text()
+ }
+
+ bindEvents: ->
+ $(document).on 'click', @onDocumentClick
+ @searchInput.on 'keydown', @onSearchInputKeyDown
+ @searchInput.on 'keyup', @onSearchInputKeyUp
+ @searchInput.on 'click', @onSearchInputClick
+ @searchInput.on 'focus', @onSearchInputFocus
+ @clearInput.on 'click', @onClearInputClick
+
+ onDocumentClick: (e) =>
+ # If clicking outside the search box
+ # And search input is not focused
+ # And we are not clicking inside a suggestion
+ if not $.contains(@dropdown[0], e.target) and @isFocused and not $(e.target).parents('ul').length
+ @onSearchInputBlur()
+
+ enableAutocomplete: ->
+ # No need to enable anything if user is not logged in
+ return if !gon.current_user_id
+
+ _this = @
+ @loadingSuggestions = false
+
+ @dropdown.addClass('open')
+ @searchInput.removeClass('disabled')
+
+ onSearchInputKeyDown: =>
+ # Saves last length of the entered text
+ @saveTextLength()
+
+ onSearchInputKeyUp: (e) =>
+ switch e.keyCode
+ when KEYCODE.BACKSPACE
+ # when trying to remove the location badge
+ if @lastTextLength is 0 and @badgePresent()
+ @removeLocationBadge()
+
+ # When removing the last character and no badge is present
+ if @lastTextLength is 1
+ @disableAutocomplete()
+
+ # When removing any character from existin value
+ if @lastTextLength > 1
+ @enableAutocomplete()
+
+ when KEYCODE.ESCAPE
+ @restoreOriginalState()
+
+ else
+ # Handle the case when deleting the input value other than backspace
+ # e.g. Pressing ctrl + backspace or ctrl + x
+ if @searchInput.val() is ''
+ @disableAutocomplete()
+ else
+ # We should display the menu only when input is not empty
+ @enableAutocomplete()
+
+ @wrap.toggleClass 'has-value', !!e.target.value
+
+ # Avoid falsy value to be returned
+ return
+
+ onSearchInputClick: (e) =>
+ # Prevents closing the dropdown menu
+ e.stopImmediatePropagation()
+
+ onSearchInputFocus: =>
+ @isFocused = true
+ @wrap.addClass('search-active')
+
+ onClearInputClick: (e) =>
+ e.preventDefault()
+ @searchInput.val('').focus()
+
+ onSearchInputBlur: (e) =>
+ @isFocused = false
+ @wrap.removeClass('search-active')
+
+ # If input is blank then restore state
+ if @searchInput.val() is ''
+ @restoreOriginalState()
+
+ addLocationBadge: (item) ->
+ category = if item.category? then "#{item.category}: " else ''
+ value = if item.value? then item.value else ''
+
+ html = "<span class='location-badge'>
+ <i class='location-text'>#{category}#{value}</i>
+ </span>"
+ @locationBadgeEl.html(html)
+ @wrap.addClass('has-location-badge')
+
+ restoreOriginalState: ->
+ inputs = Object.keys @originalState
+
+ for input in inputs
+ @getElement("##{input}").val(@originalState[input])
+
+
+ if @originalState._location is ''
+ @locationBadgeEl.empty()
+ else
+ @addLocationBadge(
+ value: @originalState._location
+ )
+
+ @dropdown.removeClass 'open'
+
+ badgePresent: ->
+ @locationBadgeEl.children().length
+
+ resetSearchState: ->
+ inputs = Object.keys @originalState
+
+ for input in inputs
+
+ # _location isnt a input
+ break if input is '_location'
+
+ @getElement("##{input}").val('')
+
+ removeLocationBadge: ->
+ @locationBadgeEl.empty()
+
+ # Reset state
+ @resetSearchState()
+
+ @wrap.removeClass('has-location-badge')
+
+ disableAutocomplete: ->
+ @searchInput.addClass('disabled')
+ @dropdown.removeClass('open')
+ @restoreMenu()
+
+ restoreMenu: ->
+ html = "<ul>
+ <li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li>
+ </ul>"
+ @dropdownContent.html(html)
+
+ onClick: (item, $el, e) ->
+ if location.pathname.indexOf(item.url) isnt -1
+ e.preventDefault()
+ if not @badgePresent
+ if item.category is 'Projects'
+ @projectInputEl.val(item.id)
+ @addLocationBadge(
+ value: 'This project'
+ )
+
+ if item.category is 'Groups'
+ @groupInputEl.val(item.id)
+ @addLocationBadge(
+ value: 'This group'
+ )
+
+ $el.removeClass('is-active')
+ @disableAutocomplete()
+ @searchInput.val('').focus()
diff --git a/app/assets/javascripts/todos.js.coffee b/app/assets/javascripts/todos.js.coffee
index b6b4bd90e6a..886da72e261 100644
--- a/app/assets/javascripts/todos.js.coffee
+++ b/app/assets/javascripts/todos.js.coffee
@@ -6,10 +6,12 @@ class @Todos
clearListeners: ->
$('.done-todo').off('click')
$('.js-todos-mark-all').off('click')
+ $('.todo').off('click')
initBtnListeners: ->
$('.done-todo').on('click', @doneClicked)
$('.js-todos-mark-all').on('click', @allDoneClicked)
+ $('.todo').on('click', @goToTodoUrl)
doneClicked: (e) =>
e.preventDefault()
@@ -54,3 +56,11 @@ class @Todos
updateBadges: (data) ->
$('.todos-pending .badge, .todos-pending-count').text data.count
$('.todos-done .badge').text data.done_count
+
+ goToTodoUrl: (e)->
+ todoLink = $(this).data('url')
+ if e.metaKey
+ e.preventDefault()
+ window.open(todoLink,'_blank')
+ else
+ Turbolinks.visit(todoLink)
diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee
index aec13e54c98..eee9b6e690e 100644
--- a/app/assets/javascripts/users_select.js.coffee
+++ b/app/assets/javascripts/users_select.js.coffee
@@ -19,6 +19,7 @@ class @UsersSelect
$block = $selectbox.closest('.block')
abilityName = $dropdown.data('ability-name')
$value = $block.find('.value')
+ $collapsedSidebar = $block.find('.sidebar-collapsed-user')
$loading = $block.find('.block-loading').fadeOut()
$block.on('click', '.js-assign-yourself', (e) =>
@@ -32,12 +33,14 @@ class @UsersSelect
data[abilityName].assignee_id = selected
$loading
.fadeIn()
+ $dropdown.trigger('loading.gl.dropdown')
$.ajax(
type: 'PUT'
dataType: 'json'
url: issueURL
data: data
).done (data) ->
+ $dropdown.trigger('loaded.gl.dropdown')
$loading.fadeOut()
$selectbox.hide()
@@ -51,11 +54,22 @@ class @UsersSelect
name: 'Unassigned'
username: ''
avatar: ''
+ $value.html(assigneeTemplate(user))
+ $collapsedSidebar.html(collapsedAssigneeTemplate(user))
- $value.html(noAssigneeTemplate(user))
- $value.find('a').attr('href')
- noAssigneeTemplate = _.template(
+ collapsedAssigneeTemplate = _.template(
+ '<% if( avatar ) { %>
+ <a class="author_link" href="/u/<%= username %>">
+ <img width="24" class="avatar avatar-inline s24" alt="" src="<%= avatar %>">
+ <span class="author">Toni Boehm</span>
+ </a>
+ <% } else { %>
+ <i class="fa fa-user"></i>
+ <% } %>'
+ )
+
+ assigneeTemplate = _.template(
'<% if (username) { %>
<a class="author_link " href="/u/<%= username %>">
<% if( avatar ) { %>
@@ -131,7 +145,8 @@ class @UsersSelect
hidden: (e) ->
$selectbox.hide()
- $value.show()
+ # display:block overrides the hide-collapse rule
+ $value.removeAttr('style')
clicked: (user) ->
page = $('body').data 'page'
diff --git a/app/assets/javascripts/zen_mode.js.coffee b/app/assets/javascripts/zen_mode.js.coffee
index e1c5446eaac..99f35ecfb0f 100644
--- a/app/assets/javascripts/zen_mode.js.coffee
+++ b/app/assets/javascripts/zen_mode.js.coffee
@@ -42,7 +42,7 @@ class @ZenMode
$(e.currentTarget).trigger('zen_mode:leave')
$(document).on 'zen_mode:enter', (e) =>
- @enter(e.target.parentNode)
+ @enter($(e.target).closest('.md-area').find('.zen-backdrop'))
$(document).on 'zen_mode:leave', (e) =>
@exit()
diff --git a/app/assets/stylesheets/behaviors.scss b/app/assets/stylesheets/behaviors.scss
index 469f4f296ae..542a53f0377 100644
--- a/app/assets/stylesheets/behaviors.scss
+++ b/app/assets/stylesheets/behaviors.scss
@@ -13,10 +13,10 @@
// Toggle between two states.
.js-toggler-container {
- .turn-on { display: block; }
+ .turn-on { display: block; }
.turn-off { display: none; }
&.on {
- .turn-on { display: none; }
+ .turn-on { display: none; }
.turn-off { display: block; }
}
}
diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss
index e3192823a1a..0b3af592d4a 100644
--- a/app/assets/stylesheets/framework/calendar.scss
+++ b/app/assets/stylesheets/framework/calendar.scss
@@ -1,3 +1,9 @@
+.calender-block {
+ @media (min-width: $screen-sm-min) and (max-width: $screen-lg-min) {
+ overflow-x: scroll;
+ }
+}
+
.user-calendar-activities {
.calendar_onclick_hr {
padding: 0;
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 9b676d759e0..2ade341c9dd 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -121,17 +121,10 @@ p.time {
text-shadow: none;
}
-.thin_area{
+.thin_area {
height: 150px;
}
-// Fixes alignment on notes.
-.new_note {
- label {
- text-align: left;
- }
-}
-
// Fix issue with notes & lists creating a bunch of bottom borders.
li.note {
img { max-width: 100% }
@@ -148,7 +141,7 @@ li.note {
}
}
-.wiki_content code, .readme code{
+.wiki_content code, .readme code {
background-color: inherit;
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 6c870ed927e..82dc1acbd01 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -42,7 +42,7 @@
font-size: 15px;
text-align: left;
border: 1px solid $dropdown-toggle-border-color;
- border-radius: 2px;
+ border-radius: $dropdown-border-radius;
outline: 0;
text-overflow: ellipsis;
white-space: nowrap;
@@ -75,12 +75,12 @@
width: 240px;
margin-top: 2px;
margin-bottom: 0;
- padding: 10px;
- font-size: 14px;
+ font-size: 15px;
font-weight: normal;
+ padding: 10px 0;
background-color: $dropdown-bg;
border: 1px solid $dropdown-border-color;
- border-radius: $border-radius-base;
+ border-radius: $dropdown-border-radius;
box-shadow: 0 2px 4px $dropdown-shadow-color;
&.is-loading {
@@ -101,9 +101,17 @@
li {
text-align: left;
list-style: none;
+ padding: 0 10px;
}
.divider {
+ height: 1px;
+ margin: 8px 10px;
+ padding: 0;
+ background-color: $dropdown-divider-color;
+ }
+
+ .separator {
width: 100%;
height: 1px;
margin-top: 8px;
@@ -141,6 +149,17 @@
line-height: 16px;
}
}
+
+ .dropdown-header {
+ color: $dropdown-header-color;
+ font-size: 13px;
+ line-height: 22px;
+ padding: 0 10px 10px;
+ }
+
+ .separator + .dropdown-header {
+ padding-top: 2px;
+ }
}
.dropdown-menu-paging {
@@ -158,6 +177,10 @@
.dropdown-menu-back {
display: block;
}
+
+ .dropdown-content {
+ padding: 0 10px;
+ }
}
}
@@ -193,7 +216,7 @@
}
.dropdown-select {
- width: 300px;
+ width: $dropdown-width;
}
.dropdown-menu-align-right {
@@ -222,20 +245,11 @@
}
}
-.dropdown-header {
- padding-left: 5px;
- padding-right: 5px;
- color: $dropdown-header-color;
- font-size: 13px;
- line-height: 22px;
-}
.dropdown-title {
position: relative;
- margin-bottom: 10px;
- padding-left: 30px;
- padding-right: 30px;
- padding-bottom: 10px;
+ padding: 0 0 15px;
+ margin: 0 10px 10px;
font-weight: 600;
line-height: 1;
text-align: center;
@@ -261,21 +275,26 @@
}
.dropdown-menu-close {
- right: 0;
+ right: 7px;
+ width: 20px;
+ height: 20px;
+ top: -1px;
}
.dropdown-menu-back {
- left: 0;
+ left: 7px;
+ top: 2px;
}
.dropdown-input {
position: relative;
margin-bottom: 10px;
+ padding: 0 10px;
.fa {
position: absolute;
top: 10px;
- right: 10px;
+ right: 20px;
color: #c7c7c7;
font-size: 12px;
pointer-events: none;
@@ -285,6 +304,9 @@
display: none;
cursor: pointer;
pointer-events: all;
+ right: 22px;
+ top: 9px;
+ font-size: 14px;
}
&.has-value {
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index a26ace5cc19..b15f4e7bd5e 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -20,6 +20,7 @@
margin: 0;
text-align: left;
padding: 10px $gl-padding;
+ word-wrap: break-word;
.file-actions {
float: right;
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index b05c5df1bd8..9209347f9bc 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -3,7 +3,7 @@
vertical-align: top;
}
-@media (min-width: $screen-sm-min) {
+@media (min-width: $screen-sm-min) {
.issues-filters,
.issues_bulk_update {
.dropdown-menu-toggle {
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 4cb4129b71b..54cb5461113 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -6,40 +6,6 @@ input {
border-radius: $border-radius-base;
}
-input[type='search'] {
- background-color: white;
- padding-left: 10px;
-}
-
-input[type='search'].search-input {
- background-repeat: no-repeat;
- background-position: 10px;
- background-size: 16px;
- background-position-x: 30%;
- padding-left: 10px;
- background-color: $gray-light;
-
- &.search-input[value=""] {
- background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAFu0lEQVRIia1WTahkVxH+quqce7vf6zdvJpHoIlkYJ2SiJiIokmQjgoGgIAaEIYuYXWICgojiwkmC4taFwhjcyIDusogEIwwiSSCKPwsdwzAg0SjJ9Izzk5n3+nXfe8+pqizOvd395scfsJqi6dPnnDr11Vc/NJ1OwUTosqJLCmYCHCAC2mSHs+ojZv6AO46Y+20AhIneJsafhPhXVZSXDk7qi+aOLhtQNuBmQtcarAKjTXpn2+l3u2yPunvZSABRucjcAV/eMZuM48/Go/g1d19kc4wq+e8MZjWkbI/P5t2P3RFFbv7SQdyBlBUx8N8OTuqjMcof+N94yMPrY2DMm/ytnb32J0QrY+6AqsHM4Q64O9SKDmerKDD3Oy/tNL9vk342CC8RuU6n0ymCMHb22scu7zQngtASOjUHE1BX4UUAv4b7Ow6qiXCXuz/UdvogAAweDY943/b4cAz0ZlYHXeMsnT07RVb7wMUr8ykI4H5HVkMd5Rcb4/jNURVOL5qErAaAUUdCCIJ5kx5q2nw8m39ImEAAsjpE6PStB0YfMcd1wqqG3Xn7A3PfZyyKnNjaqD4fmE/fCNKshirIyY1xvI+Av6g5QIAIIWX7cJPssboSiBBEeKmsZne0Sb8kzAUWNYyq8NvbDo0fZ6beqxuLmqOOMr/lwOh+YXpXtbjERGja9JyZ9+HxpXKb9Gj5oywRESbj+Cj1ENG1QViTGBl1FbC1We1tbVRfHWIoQkhqH9xbpE92XUbb6VJZ1R4crjRz1JWcDMJvLdoMcyAEhjuwHo8Bfndg3mbszhOY+adVlMtD3po51OwzIQiEaams7oeJhxRw1FFOVpFRRUYIhMBAFRnjOsC8IFHHUA4TQQhgAqpAiIFfGbxkIqj54ayGbL7UoOqHCniAEKHLNr26l+D9wQJzeUwMAnfHvEnLECzZRwRV++d60ptjW9VLZeolEJG6GwCCE0CFVNB+Ay0NEqoQYG4YYFu7B8IEVRt3uRzy/osIoLV9QZimWXGHUMFdmI6M64DUF2Je88R9VZqCSP+QlcF5k+4tCzSsXaqjINuK6UyE0+s/mk6/qFq8oAIL9pqMLhkGsNrOyoOIlszust3aJv0U9+kFdwjTGwWl1YdF+KWlQSZ0Se/psj8yGVdg5tJyfH96EBWmLtoEMwMzMFt031NzGWLLzKhC+KV7H5ZeeaMOPxemma2x68puc0LN3+/u6LJiePS6MKHvn4wu6cPzJj0hsioeMfDrEvjv5r6W9gBvjKJujuKzQ0URIZj75NylvT+mbHfXQa4rwAMaVRTMm/SFyzvNy0yF6+4AM+1ubcSnqkAIUjQKl1RKSbE5jt+vovx1MBqF0WW7/d1Z80ab9BtmuJ3Xk5cJKds9TZt/uLPXvtiTrQ+dIwqfAejUvM1os6FNikXKUHfQ+ekUsXT5u85enJ0CaBSkkGEo1syUQ+DfMdE/4GA1uzupf9zdbzhOmLsF4efHVXjaHHAzmDtGdQRd/Nc5wAEJjNki3XfhyvwVNz80xANrht3LsENY9cBBdN1L9GUyyvFRFZ42t75sBvCQRykbRlU4tT2pPxoCvzx09d4GmPs200M6wKdWSDGK8mppYSWdhAlt0qeaLv+IadXU9/Evq4FAZ8ej+LmtcTxaRX4NWI0Uag5Vg1p5MYg8BnlhXIdPHDow+vTWZvVMVttXDLqkTzZdPj6Qii6cP1cSvIdl3iQkNYyi9HH0I22y+93tY3DcQkTZgQtM+POoCr8x97eylkmtrgKuztrvXJ21x/aNKuqIkZ/fntRfCdcTfhUTAIhRzoDojJD0aSNLLwMzmpT7+JaLtyf1MwDo6qz9djFaUq3t9MlFmy/c1OCSceY9fMsVaL9mvH9ocXdkdWxv1scAePG0THAhMOaLdOw/Gvxfxb1w4eCapyIENUcV5M3/u8FitAxZ25P6GAHT3UX39Srw+QOb1ZffA98Dl2Wy1BYkAAAAAElFTkSuQmCC');
- }
-
- &.search-input::-webkit-input-placeholder {
- text-align: center;
- }
-
- &.search-input:-moz-placeholder { /* Firefox 18- */
- text-align: center;
- }
-
- &.search-input::-moz-placeholder { /* Firefox 19+ */
- text-align: center;
- }
-
- &.search-input:-ms-input-placeholder {
- text-align: center;
- }
-}
-
input[type='text'].danger {
background: #f2dede!important;
border-color: #d66;
@@ -125,7 +91,7 @@ label {
}
.form-control::-webkit-input-placeholder {
- color: #7f8fa4;
+ color: $gl-placeholder-color;
}
.input-group {
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 6a68bb5c115..b3397d16016 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -36,7 +36,7 @@ header {
padding: 0;
.nav > li > a {
- color: #7f8fa4;
+ color: $gl-icon-color;
font-size: 18px;
padding: 0;
margin: ($header-height - 28) / 2 0;
@@ -62,7 +62,7 @@ header {
background-color: #eee;
}
&.active {
- color: #7f8fa4;
+ color: $gl-icon-color;
}
}
}
@@ -81,14 +81,14 @@ header {
font-size: 19px;
line-height: $header-height;
font-weight: normal;
- color: #4c4e54;
+ color: $gl-text-color;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: top;
white-space: nowrap;
a {
- color: #4c4e54;
+ color: $gl-text-color;
&:hover {
text-decoration: underline;
}
@@ -117,26 +117,6 @@ header {
}
}
- .search {
- margin-right: 10px;
- margin-left: 10px;
- margin-top: ($header-height - 36) / 2;
-
- form {
- margin: 0;
- padding: 0;
- }
-
- .search-input {
- width: 220px;
-
- &:focus {
- @include box-shadow(none);
- outline: none;
- }
- }
- }
-
.impersonation i {
color: $red-normal;
}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 8328aac4e7a..c8f86d60e3b 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -1,9 +1,7 @@
.div-dropzone-wrapper {
.div-dropzone {
position: relative;
- padding: 0;
- border: 0;
- margin-bottom: 5px;
+ margin-bottom: -5px;
.div-dropzone-focus {
border-color: #66afe9 !important;
@@ -25,12 +23,10 @@
.div-dropzone-spinner {
position: absolute;
- top: 100%;
- left: 100%;
- margin-top: -1.1em;
- margin-left: -1.1em;
+ bottom: 10px;
+ right: 5px;
opacity: 0;
- font-size: 30px;
+ font-size: 20px;
transition: opacity 200ms ease-in-out;
}
@@ -65,17 +61,30 @@
position: relative;
}
+.md-header {
+ .nav-links {
+ .active {
+ a {
+ border-bottom-color: #000;
+ }
+ }
+
+ a {
+ padding-top: 0;
+ line-height: 1;
+ }
+ }
+}
+
.referenced-users {
color: #4c4e54;
padding-top: 10px;
}
.md-preview-holder {
- background: #fff;
- border: 1px solid #ddd;
- min-height: 169px;
- padding: 5px;
- box-shadow: none;
+ min-height: 167px;
+ padding: 10px 0;
+ overflow-x: auto;
}
.markdown-area {
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index 5ea4f9a49db..66180f38a4f 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -107,7 +107,7 @@
}
.page-title {
- .note_created_ago, .new-issue-link {
+ .note-created-ago, .new-issue-link {
display: none;
}
}
@@ -116,7 +116,7 @@
display: none;
}
- aside:not(.right-sidebar){
+ aside:not(.right-sidebar) {
display: none;
}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index 95bdd6d1ea3..94f5a12ff6a 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -56,6 +56,17 @@
}
}
+ .nav-search {
+ display: inline-block;
+ width: 50%;
+ padding: 11px 0;
+
+ /* Small devices (phones, tablets, 768px and lower) */
+ @media (max-width: $screen-sm-min) {
+ width: 100%;
+ }
+ }
+
.nav-links {
display: inline-block;
width: 50%;
@@ -100,6 +111,7 @@
> form {
display: inline-block;
+ margin-top: -1px;
}
.icon-label {
@@ -110,7 +122,7 @@
height: 34px;
display: inline-block;
position: relative;
- top: 1px;
+ top: 2px;
margin-right: $gl-padding-top;
/* Medium devices (desktops, 992px and up) */
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index e82d052f45a..b2fab387e17 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -51,7 +51,7 @@
padding: 10px 15px;
}
-.select2-drop{
+.select2-drop {
color: #7f8fa4;
}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 9d188317783..18189e985c4 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -288,6 +288,10 @@
@media (min-width: $screen-sm-min) {
padding-right: $sidebar_collapsed_width;
}
+
+ .sidebar-collapsed-icon {
+ cursor: pointer;
+ }
}
.right-sidebar-expanded {
@@ -300,4 +304,8 @@
@media (min-width: $screen-md-min) {
padding-right: $gutter_width;
}
+
+ &.with-overlay {
+ padding-right: $sidebar_collapsed_width;
+ }
}
diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
index f63ac033234..c72af5dad0a 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
@@ -56,8 +56,8 @@ $component-active-bg: $brand-info;
//##
$input-color: $text-color;
-$input-border: #e7e9ed;
-$input-border-focus: #7f8fa4;
+$input-border: $border-color;
+$input-border-focus: $focus-border-color;
$legend-color: $text-color;
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index b1886fbe67b..7b2aada5a0d 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -138,6 +138,12 @@
}
}
+ a.no-attachment-icon {
+ &:before {
+ display: none;
+ }
+ }
+
/* Link to current header. */
h1, h2, h3, h4, h5, h6 {
position: relative;
@@ -244,7 +250,7 @@ a > code {
* Textareas intended for GFM
*
*/
-textarea.js-gfm-input {
+.js-gfm-input {
font-family: $monospace_font;
color: $gl-text-color;
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 61e0dd4d672..8d3ad934a50 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -11,6 +11,7 @@ $gutter_inner_width: 258px;
* UI elements
*/
$border-color: #efeff1;
+$focus-border-color: #3aabf0;
$table-border-color: #eef0f2;
$background-color: #faf9f9;
@@ -26,6 +27,7 @@ $gl-text-orange: #d90;
$gl-link-color: #3084bb;
$gl-dark-link-color: #333;
$gl-placeholder-color: #8f8f8f;
+$gl-icon-color: $gl-placeholder-color;
$gl-gray: $gl-text-color;
$gl-header-color: $gl-title-color;
@@ -66,7 +68,7 @@ $header-height: 58px;
$fixed-layout-width: 1280px;
$gl-avatar-size: 40px;
$error-exclamation-point: #e62958;
-$border-radius-default: 3px;
+$border-radius-default: 2px;
$btn-transparent-color: #8f8f8f;
$ssh-key-icon-color: #8f8f8f;
$ssh-key-icon-size: 18px;
@@ -102,9 +104,9 @@ $orange-light: rgba(252, 109, 38, 0.80);
$orange-normal: #e75e40;
$orange-dark: #ce5237;
-$red-light: #f06559;
-$red-normal: #e52c5a;
-$red-dark: #d22852;
+$red-light: #e52c5a;
+$red-normal: #d22852;
+$red-dark: darken($red-normal, 5%);
$border-white-light: #f1f2f4;
$border-white-normal: #d6dae2;
@@ -126,9 +128,9 @@ $border-orange-light: #fc6d26;
$border-orange-normal: #ce5237;
$border-orange-dark: #c14e35;
-$border-red-light: #f24f41;
-$border-red-normal: #d22852;
-$border-red-dark: #ca264f;
+$border-red-light: #d22852;
+$border-red-normal: #ca264f;
+$border-red-dark: darken($border-red-normal, 5%);
$help-well-bg: #fafafa;
$help-well-border: #e5e5e5;
@@ -166,6 +168,8 @@ $regular_font: 'Source Sans Pro', "Helvetica Neue", Helvetica, Arial, sans-serif
/*
* Dropdowns
*/
+$dropdown-border-radius: 2px;
+$dropdown-width: 300px;
$dropdown-bg: #fff;
$dropdown-link-color: #555;
$dropdown-link-hover-bg: $row-hover;
@@ -176,8 +180,8 @@ $dropdown-divider-color: rgba(#000, .1);
$dropdown-header-color: #959494;
$dropdown-title-btn-color: #bfbfbf;
$dropdown-input-color: #555;
-$dropdown-input-focus-border: rgb(58, 171, 240);
-$dropdown-input-focus-shadow: rgba(#000, .2);
+$dropdown-input-focus-border: $focus-border-color;
+$dropdown-input-focus-shadow: rgba($dropdown-input-focus-border, .4);
$dropdown-loading-bg: rgba(#fff, .6);
$dropdown-toggle-bg: #fff;
@@ -193,3 +197,29 @@ $dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color;
$award-emoji-menu-bg: #fff;
$award-emoji-menu-border: #f1f2f4;
$award-emoji-new-btn-icon-color: #dcdcdc;
+
+/*
+ * Search Box
+ */
+$search-input-border-color: rgba(#4688f1, .8);
+$search-input-focus-shadow-color: $dropdown-input-focus-shadow;
+$search-input-width: 244px;
+$location-badge-color: #aaa;
+$location-badge-bg: $gray-normal;
+$location-badge-active-bg: #4f91f8;
+$location-icon-color: #e7e9ed;
+$location-icon-active-color: #807e7e;
+
+/*
+ * Notes
+ */
+$notes-light-color: #8e8e8e;
+$notes-action-color: #c3c3c3;
+$notes-role-color: #8e8e8e;
+$notes-role-border-color: #e4e4e4;
+
+$note-disabled-comment-color: #b2b2b2;
+$note-form-border-color: #e5e5e5;
+$note-toolbar-color: #959494;
+
+$zen-control-hover-color: #111;
diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss
index 02e24ec7c4d..f870ea0d87f 100644
--- a/app/assets/stylesheets/framework/zen.scss
+++ b/app/assets/stylesheets/framework/zen.scss
@@ -1,61 +1,62 @@
-.zennable {
- a.js-zen-enter {
- color: $gl-gray;
- position: absolute;
+.zen-backdrop {
+ &.fullscreen {
+ background-color: white;
+ position: fixed;
top: 0;
- right: 4px;
- line-height: 56px;
- }
+ bottom: 0;
+ left: 0;
+ right: 0;
+ z-index: 1031;
- a.js-zen-leave {
- display: none;
- color: $gl-text-color;
- position: absolute;
- top: 10px;
- right: 10px;
- padding: 5px;
- font-size: 36px;
+ textarea {
+ border: none;
+ box-shadow: none;
+ border-radius: 0;
+ color: #000;
+ font-size: 20px;
+ line-height: 26px;
+ padding: 30px;
+ display: block;
+ outline: none;
+ resize: none;
+ height: 100vh;
+ max-width: 900px;
+ margin: 0 auto;
+ }
- &:hover {
- color: #111;
+ .zen-control-leave {
+ display: block;
+ position: absolute;
+ top: 0;
}
}
+}
- .zen-backdrop {
- &.fullscreen {
- background-color: white;
- position: fixed;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
- z-index: 1031;
+.zen-cotrol {
+ padding: 0;
+ color: #555;
+ background: none;
+ border: 0;
+}
- textarea {
- border: none;
- box-shadow: none;
- border-radius: 0;
- color: #000;
- font-size: 20px;
- line-height: 26px;
- padding: 30px;
- display: block;
- outline: none;
- resize: none;
- height: 100vh;
- max-width: 900px;
- margin: 0 auto;
- }
+.zen-control-full {
+ color: $note-toolbar-color;
- a.js-zen-enter {
- display: none;
- }
+ &:hover {
+ color: $gl-link-color;
+ text-decoration: none;
+ }
+}
- a.js-zen-leave {
- display: block;
- position: absolute;
- top: 0;
- }
- }
+.zen-control-leave {
+ display: none;
+ color: $gl-text-color;
+ position: absolute;
+ right: 10px;
+ padding: 5px;
+ font-size: 36px;
+
+ &:hover {
+ color: $zen-control-hover-color;
}
}
diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss
index 28994e60baa..37bf38fa65d 100644
--- a/app/assets/stylesheets/pages/awards.scss
+++ b/app/assets/stylesheets/pages/awards.scss
@@ -37,7 +37,7 @@
height: 300px;
overflow-y: scroll;
- input.emoji-search{
+ input.emoji-search {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAFu0lEQVRIia1WTahkVxH+quqce7vf6zdvJpHoIlkYJ2SiJiIokmQjgoGgIAaEIYuYXWICgojiwkmC4taFwhjcyIDusogEIwwiSSCKPwsdwzAg0SjJ9Izzk5n3+nXfe8+pqizOvd395scfsJqi6dPnnDr11Vc/NJ1OwUTosqJLCmYCHCAC2mSHs+ojZv6AO46Y+20AhIneJsafhPhXVZSXDk7qi+aOLhtQNuBmQtcarAKjTXpn2+l3u2yPunvZSABRucjcAV/eMZuM48/Go/g1d19kc4wq+e8MZjWkbI/P5t2P3RFFbv7SQdyBlBUx8N8OTuqjMcof+N94yMPrY2DMm/ytnb32J0QrY+6AqsHM4Q64O9SKDmerKDD3Oy/tNL9vk342CC8RuU6n0ymCMHb22scu7zQngtASOjUHE1BX4UUAv4b7Ow6qiXCXuz/UdvogAAweDY943/b4cAz0ZlYHXeMsnT07RVb7wMUr8ykI4H5HVkMd5Rcb4/jNURVOL5qErAaAUUdCCIJ5kx5q2nw8m39ImEAAsjpE6PStB0YfMcd1wqqG3Xn7A3PfZyyKnNjaqD4fmE/fCNKshirIyY1xvI+Av6g5QIAIIWX7cJPssboSiBBEeKmsZne0Sb8kzAUWNYyq8NvbDo0fZ6beqxuLmqOOMr/lwOh+YXpXtbjERGja9JyZ9+HxpXKb9Gj5oywRESbj+Cj1ENG1QViTGBl1FbC1We1tbVRfHWIoQkhqH9xbpE92XUbb6VJZ1R4crjRz1JWcDMJvLdoMcyAEhjuwHo8Bfndg3mbszhOY+adVlMtD3po51OwzIQiEaams7oeJhxRw1FFOVpFRRUYIhMBAFRnjOsC8IFHHUA4TQQhgAqpAiIFfGbxkIqj54ayGbL7UoOqHCniAEKHLNr26l+D9wQJzeUwMAnfHvEnLECzZRwRV++d60ptjW9VLZeolEJG6GwCCE0CFVNB+Ay0NEqoQYG4YYFu7B8IEVRt3uRzy/osIoLV9QZimWXGHUMFdmI6M64DUF2Je88R9VZqCSP+QlcF5k+4tCzSsXaqjINuK6UyE0+s/mk6/qFq8oAIL9pqMLhkGsNrOyoOIlszust3aJv0U9+kFdwjTGwWl1YdF+KWlQSZ0Se/psj8yGVdg5tJyfH96EBWmLtoEMwMzMFt031NzGWLLzKhC+KV7H5ZeeaMOPxemma2x68puc0LN3+/u6LJiePS6MKHvn4wu6cPzJj0hsioeMfDrEvjv5r6W9gBvjKJujuKzQ0URIZj75NylvT+mbHfXQa4rwAMaVRTMm/SFyzvNy0yF6+4AM+1ubcSnqkAIUjQKl1RKSbE5jt+vovx1MBqF0WW7/d1Z80ab9BtmuJ3Xk5cJKds9TZt/uLPXvtiTrQ+dIwqfAejUvM1os6FNikXKUHfQ+ekUsXT5u85enJ0CaBSkkGEo1syUQ+DfMdE/4GA1uzupf9zdbzhOmLsF4efHVXjaHHAzmDtGdQRd/Nc5wAEJjNki3XfhyvwVNz80xANrht3LsENY9cBBdN1L9GUyyvFRFZ42t75sBvCQRykbRlU4tT2pPxoCvzx09d4GmPs200M6wKdWSDGK8mppYSWdhAlt0qeaLv+IadXU9/Evq4FAZ8ej+LmtcTxaRX4NWI0Uag5Vg1p5MYg8BnlhXIdPHDow+vTWZvVMVttXDLqkTzZdPj6Qii6cP1cSvIdl3iQkNYyi9HH0I22y+93tY3DcQkTZgQtM+POoCr8x97eylkmtrgKuztrvXJ21x/aNKuqIkZ/fntRfCdcTfhUTAIhRzoDojJD0aSNLLwMzmpT7+JaLtyf1MwDo6qz9djFaUq3t9MlFmy/c1OCSceY9fMsVaL9mvH9ocXdkdWxv1scAePG0THAhMOaLdOw/Gvxfxb1w4eCapyIENUcV5M3/u8FitAxZ25P6GAHT3UX39Srw+QOb1ZffA98Dl2Wy1BYkAAAAAElFTkSuQmCC");
background-repeat: no-repeat;
background-position: right 5px center;
diff --git a/app/assets/stylesheets/pages/ci_projects.scss b/app/assets/stylesheets/pages/ci_projects.scss
index 2a7b5cfc7fd..67a9d7d2cf7 100644
--- a/app/assets/stylesheets/pages/ci_projects.scss
+++ b/app/assets/stylesheets/pages/ci_projects.scss
@@ -42,7 +42,7 @@
}
}
- .loading{
+ .loading {
font-size: 20px;
}
diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss
index 971656feb42..358d2f4ab9d 100644
--- a/app/assets/stylesheets/pages/commit.scss
+++ b/app/assets/stylesheets/pages/commit.scss
@@ -1,15 +1,15 @@
-.commit-title{
+.commit-title {
display: block;
}
-.commit-author, .commit-committer{
+.commit-author, .commit-committer {
display: block;
color: #999;
font-weight: normal;
font-style: italic;
}
-.commit-author strong, .commit-committer strong{
+.commit-author strong, .commit-committer strong {
font-weight: bold;
font-style: normal;
}
@@ -20,6 +20,8 @@
margin: 0;
padding: 0;
margin-top: 10px;
+ word-break: normal;
+ white-space: pre-wrap;
}
.commit-info-row {
@@ -74,7 +76,7 @@
color: $gl-text-red;
}
}
- .edit-file{
+ .edit-file {
a {
color: $gl-text-color;
}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index b6011fe7679..8272615768d 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -1,4 +1,4 @@
-.commits-compare-switch{
+.commits-compare-switch {
@include btn-default;
@include btn-white;
background: image-url("switch_icon.png") no-repeat center center;
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index d3eda1a57e6..5917f089720 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -33,8 +33,12 @@
.description {
margin-top: 6px;
- p:last-child {
- margin-bottom: 0;
+ p {
+ overflow-x: auto;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
}
}
}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index f1368d74b3b..97f4485beb8 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -59,9 +59,14 @@
border-collapse: separate;
margin: 0;
padding: 0;
+
.line_holder td {
line-height: $code_line_height;
font-size: $code_font_size;
+
+ span {
+ white-space: pre;
+ }
}
}
@@ -104,6 +109,10 @@
display: table-cell;
}
}
+
+ .text-file.diff-wrap-lines table .line_holder td span {
+ white-space: pre-wrap;
+ }
}
.image {
background: #ddd;
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index 43be5e38ba8..0f0592a0ab8 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -1,5 +1,5 @@
.file-editor {
- #editor{
+ #editor {
border: none;
@include border-radius(0);
height: 500px;
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 84eefd01cfe..c66efe978cd 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -43,10 +43,6 @@
.md {
color: #7f8fa4;
font-size: $gl-font-size;
-
- iframe.twitter-share-button {
- vertical-align: bottom;
- }
}
pre {
diff --git a/app/assets/stylesheets/pages/lint.scss b/app/assets/stylesheets/pages/lint.scss
index 6d2bd33b28b..6926448519e 100644
--- a/app/assets/stylesheets/pages/lint.scss
+++ b/app/assets/stylesheets/pages/lint.scss
@@ -1,9 +1,9 @@
.ci-body {
- .incorrect-syntax{
+ .incorrect-syntax {
font-size: 19px;
color: red;
}
- .correct-syntax{
+ .correct-syntax {
font-size: 19px;
color: #47a447;
}
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index 777bcbca5c3..403171d4532 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -36,7 +36,7 @@
}
}
- .login-box{
+ .login-box {
background: #fafafa;
border-radius: 10px;
box-shadow: 0 0 2px #ccc;
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 7ff63ca20b6..1c6a4208974 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -195,42 +195,6 @@
line-height: 31px;
}
-.disabled-comment-area {
- padding: 16px 0;
-
- .disabled-profile {
- width: 40px;
- height: 40px;
- background: $border-gray-dark;
- border-radius: 20px;
- display: inline-block;
- margin-right: 10px;
- }
-
- .disabled-comment {
- background: $gray-light;
- display: inline-block;
- vertical-align: top;
- height: 200px;
- border-radius: 4px;
- border: 1px solid $border-gray-normal;
- padding-top: 90px;
- text-align: center;
- right: 20px;
- position: absolute;
- left: 70px;
- margin-bottom: 20px;
-
- span {
- color: #b2b2b2;
-
- a {
- color: $md-link-color;
- }
- }
- }
-}
-
.builds {
.table-holder {
overflow-x: scroll;
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 655f88b0c2c..a909776b437 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -1,10 +1,6 @@
/**
* Note Form
*/
-
-.comment-btn {
- @extend .btn-create;
-}
.reply-btn {
@extend .btn-primary;
margin: 10px $gl-padding;
@@ -17,16 +13,17 @@
}
.diff-file,
.discussion {
- .new_note {
+ .new-note {
margin: 0;
border: none;
}
}
-.new_note {
+
+.new-note {
display: none;
}
-.new_note, .note-edit-form {
+.new-note, .note-edit-form {
.note-form-actions {
margin-top: $gl-padding;
}
@@ -40,21 +37,18 @@
img {
max-width: 100%;
}
+}
- .note_text {
- width: 100%;
- }
+.note-textarea {
+ padding: 10px 0;
+ font-family: $regular_font;
+ border: 0;
- .comment-hints {
- margin-top: -12px;
+ &:focus {
+ outline: 0;
}
}
-/* loading indicator */
-.notes-busy {
- margin: 18px;
-}
-
.note-image-attach {
@extend .col-md-4;
margin-left: 45px;
@@ -62,38 +56,29 @@
}
.common-note-form {
- margin: 0;
- background: #fff;
- padding: $gl-padding;
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
- margin-bottom: -$gl-padding;
-}
-
-.note-form-actions {
- .note-form-option {
- margin-top: 8px;
- margin-left: 30px;
- @extend .pull-left;
- }
-
- .js-notify-commit-author {
- float: left;
- }
-
- .write-preview-btn {
- // makes the "absolute" position for links relative to this
- position: relative;
-
- // preview/edit buttons
- > a {
- position: absolute;
- right: 5px;
- top: 8px;
+ .md-area {
+ padding: $gl-padding-top $gl-padding;
+ border: 1px solid $note-form-border-color;
+ border-radius: $border-radius-base;
+
+ &.is-focused {
+ border-color: $focus-border-color;
+ box-shadow: 0 0 2px rgba(#000, .2),
+ 0 0 4px rgba($focus-border-color, .4);
+
+ .comment-toolbar,
+ .nav-links {
+ border-color: $focus-border-color;
+ }
}
}
}
+.discussion-form {
+ padding: $gl-padding-top $gl-padding;
+ background-color: #fff;
+}
+
.note-edit-form {
display: none;
font-size: 15px;
@@ -152,11 +137,49 @@
}
}
-.comment-hints {
- color: #999;
- background: #fff;
- padding: 7px;
- margin-top: -7px;
- border: 1px solid $border-color;
- font-size: 13px;
+.comment-toolbar {
+ padding-top: $gl-padding-top;
+ color: $note-toolbar-color;
+ border-top: 1px solid $border-color;
+}
+
+.toolbar-button {
+ padding: 0;
+ background: none;
+ border: 0;
+ font-size: 14px;
+ line-height: 16px;
+
+ &:hover,
+ &:focus {
+ color: $gl-link-color;
+ outline: 0;
+ }
+
+ @media (min-width: $screen-md-min) {
+ float: left;
+ margin-right: $gl-padding;
+
+ &:last-child {
+ float: right;
+ margin-right: 0;
+ }
+ }
+}
+
+.toolbar-button-icon {
+ position: relative;
+ top: 1px;
+ margin-right: 3px;
+ color: inherit;
+ font-size: 16px;
+}
+
+.toolbar-text {
+ font-size: 14px;
+ line-height: 16px;
+
+ @media (min-width: $screen-md-min) {
+ float: left;
+ }
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 4bd2016bdcf..aca86457c70 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -20,9 +20,15 @@ ul.notes {
.timeline-content {
margin-left: 55px;
+
+ &.timeline-content-form {
+ @media (max-width: $screen-sm-max) {
+ margin-left: 0;
+ }
+ }
}
- .note_created_ago, .note-updated-at {
+ .note-created-ago, .note-updated-at {
white-space: nowrap;
}
@@ -39,53 +45,6 @@ ul.notes {
}
}
- .discussion-header,
- .note-header {
- @extend .cgray;
-
- a:hover {
- text-decoration: none;
- }
-
- .avatar {
- float: left;
- margin-right: 10px;
- }
-
- .discussion-last-update,
- .note-last-update {
- &:before {
- content: "\00b7";
- }
-
- a {
- color: $gl-gray;
-
- &:hover {
- text-decoration: underline;
- }
- }
- }
- .author {
- color: #4c4e54;
- margin-right: 3px;
-
- &:hover {
- color: $gl-link-color;
- }
- }
- .author-username {
- }
-
- .note-role {
- float: right;
- margin-top: 1px;
- border: 1px solid #bbb;
- background-color: transparent;
- color: $gl-gray;
- }
- }
-
.discussion-body {
padding-top: 15px;
}
@@ -123,7 +82,7 @@ ul.notes {
// On diffs code should wrap nicely and not overflow
pre {
code {
- white-space: pre-wrap;
+ white-space: pre;
}
}
@@ -196,42 +155,90 @@ ul.notes {
&.notes_content {
background-color: #fff;
border-width: 1px 0;
- padding-top: 0;
+ padding: 0;
vertical-align: top;
- &.parallel{
+ &.parallel {
border-width: 1px;
}
}
}
}
+.discussion-header,
+.note-header {
+ a {
+ color: inherit;
+
+ &:hover {
+ color: $gl-link-color;
+ text-decoration: none;
+ }
+ }
+
+ .author_link {
+ font-weight: 600;
+ }
+}
+
+.note-headline-light,
+.discussion-headline-light {
+ color: $notes-light-color;
+}
+
/**
* Actions for Discussions/Notes
*/
-.discussion,
-.note {
- .discussion-actions,
- .note-actions {
- float: right;
- margin-left: 10px;
+.discussion-actions,
+.note-actions {
+ float: right;
+ margin-left: 10px;
+ color: $notes-action-color;
+}
- a {
- margin-left: 5px;
- color: $gl-gray;
+.note-action-button,
+.discussion-action-button {
+ display: inline-block;
+ margin-left: 10px;
+ line-height: 24px;
- i.fa {
- font-size: 16px;
- line-height: 16px;
- }
+ .fa {
+ position: relative;
+ top: 1px;
+ font-size: 17px;
+ }
- &:hover {
- @extend .cgray;
- &.danger { @extend .cred; }
- }
- }
+ .fa-trash-o {
+ top: 0;
+ font-size: 16px;
}
}
+
+.discussion-toggle-button {
+ line-height: 20px;
+ font-size: 13px;
+
+ .fa {
+ margin-right: 3px;
+ font-size: 10px;
+ line-height: 18px;
+ vertical-align: top;
+ }
+}
+
+.note-role {
+ position: relative;
+ top: -2px;
+ display: inline-block;
+ padding-left: 4px;
+ padding-right: 4px;
+ color: $notes-role-color;
+ font-size: 12px;
+ line-height: 20px;
+ border: 1px solid $notes-role-border-color;
+ border-radius: $border-radius-base;
+}
+
.diff-file .note .note-actions {
right: 0;
top: 0;
@@ -280,3 +287,21 @@ ul.notes {
}
}
}
+
+.disabled-comment {
+ margin-left: -$gl-padding-top;
+ margin-right: -$gl-padding-top;
+ background-color: $gray-light;
+ border-radius: $border-radius-base;
+ border: 1px solid $border-gray-normal;
+ color: $note-disabled-comment-color;
+ line-height: 200px;
+
+ .disabled-comment-text {
+ line-height: normal;
+ }
+
+ a {
+ color: $gl-link-color;
+ }
+}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 4e6aa8cd1a6..fcca9d4faf5 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -315,7 +315,7 @@ pre.light-well {
}
.git-empty {
- margin: 0 7px;
+ margin: 0 7px 7px;
h5 {
color: #5c5d5e;
@@ -401,7 +401,7 @@ pre.light-well {
}
.commit_short_id {
- margin-right: 5px;
+ margin: 0 5px;
color: $gl-link-color;
font-weight: 600;
}
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index b6e45024644..f0f3744c6fa 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -21,3 +21,145 @@
}
}
+.search {
+ margin-right: 10px;
+ margin-left: 10px;
+ margin-top: ($header-height - 35) / 2;
+
+ form {
+ @extend .form-control;
+ margin: 0;
+ padding: 4px;
+ width: $search-input-width;
+ line-height: 24px;
+ }
+
+ .location-text {
+ font-style: normal;
+ }
+
+ .search-input {
+ border: none;
+ font-size: 14px;
+ outline: none;
+ padding: 0;
+ margin-left: 5px;
+ line-height: 25px;
+ width: 98%;
+ }
+
+ .location-badge {
+ line-height: 25px;
+ padding: 0 5px;
+ border-radius: $border-radius-default;
+ font-size: 14px;
+ font-style: normal;
+ color: $location-badge-color;
+ display: inline-block;
+ background-color: $location-badge-bg;
+ vertical-align: top;
+ }
+
+ .search-input-container {
+ display: -webkit-flex;
+ display: flex;
+ position: relative;
+ }
+
+ .search-location-badge, .search-input-wrap {
+ // Fallback if flexbox is not supported
+ display: inline-block;
+ }
+
+ .search-input-wrap {
+ width: 100%;
+
+ .search-icon, .clear-icon {
+ position: absolute;
+ right: 5px;
+ top: 0;
+ color: $location-icon-color;
+
+ &:before {
+ font-family: FontAwesome;
+ font-weight: normal;
+ font-style: normal;
+ }
+ }
+
+ .search-icon {
+ @extend .fa-search;
+ @include transition(color .15s);
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ }
+
+ .clear-icon {
+ @extend .fa-times;
+ display: none;
+ }
+
+ // Rewrite position. Dropdown menu should be relative to .search-input-container
+ .dropdown {
+ position: static;
+ }
+
+ .dropdown-header {
+ text-transform: uppercase;
+ font-size: 11px;
+ }
+
+ // Custom dropdown positioning
+ .dropdown-menu {
+ top: 30px;
+ left: -5px;
+ padding: 0;
+
+ ul {
+ padding: 10px 0;
+ }
+ }
+
+ .dropdown-content {
+ max-height: 350px;
+ }
+ }
+
+ &.search-active {
+ form {
+ @extend .form-control:focus;
+ border-color: $dropdown-input-focus-border;
+ box-shadow: 0 0 4px $search-input-focus-shadow-color;
+ }
+
+ .location-badge {
+ @include transition(all .15s);
+ background-color: $location-badge-active-bg;
+ color: $white-light;
+ }
+
+ .search-input-wrap {
+ i {
+ color: $location-icon-active-color;
+ }
+ }
+ }
+
+ &.has-value {
+ .search-icon {
+ display: none;
+ }
+
+ .clear-icon {
+ cursor: pointer;
+ display: block;
+ }
+ }
+
+ &.has-location-badge {
+ .search-input-wrap {
+ width: 78%;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index 5e5e38a0ba6..dbb6daf0d70 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -1,4 +1,4 @@
-.container-fluid .content {
+.container-fluid {
.ci-status {
padding: 2px 7px;
margin-right: 5px;
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index f983e9829e6..e83fa9e3d52 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -6,13 +6,19 @@
.navbar-nav {
li {
.badge.todos-pending-count {
- background-color: #7f8fa4;
+ background-color: $gl-icon-color;
margin-top: -5px;
font-weight: normal;
}
}
}
+.todo {
+ &:hover {
+ cursor: pointer;
+ }
+}
+
.todo-item {
.todo-title {
@include str-truncated(calc(100% - 174px));
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index ed9f6031389..f010436bd36 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -52,7 +52,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:require_two_factor_authentication,
:two_factor_grace_period,
:gravatar_enabled,
- :twitter_sharing_enabled,
:sign_in_text,
:help_page_text,
:home_page_url,
diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb
index 4089091d569..c6b3105544a 100644
--- a/app/controllers/admin/projects_controller.rb
+++ b/app/controllers/admin/projects_controller.rb
@@ -5,7 +5,7 @@ class Admin::ProjectsController < Admin::ApplicationController
def index
@projects = Project.all
@projects = @projects.in_namespace(params[:namespace_id]) if params[:namespace_id].present?
- @projects = @projects.where("visibility_level IN (?)", params[:visibility_levels]) if params[:visibility_levels].present?
+ @projects = @projects.where("projects.visibility_level IN (?)", params[:visibility_levels]) if params[:visibility_levels].present?
@projects = @projects.with_push if params[:with_push].present?
@projects = @projects.abandoned if params[:abandoned].present?
@projects = @projects.non_archived unless params[:with_archived].present?
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index b23c3022fb5..9d5a28e8d4d 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -18,14 +18,14 @@ class Groups::MilestonesController < Groups::ApplicationController
end
def create
- project_ids = params[:milestone][:project_ids]
+ project_ids = params[:milestone][:project_ids].reject(&:blank?)
title = milestone_params[:title]
- @projects.where(id: project_ids).each do |project|
- Milestones::CreateService.new(project, current_user, milestone_params).execute
+ if create_milestones(project_ids)
+ redirect_to milestone_path(title)
+ else
+ render_new_with_error(project_ids.empty?)
end
-
- redirect_to milestone_path(title)
end
def show
@@ -41,6 +41,27 @@ class Groups::MilestonesController < Groups::ApplicationController
private
+ def create_milestones(project_ids)
+ return false unless project_ids.present?
+
+ ActiveRecord::Base.transaction do
+ @projects.where(id: project_ids).each do |project|
+ Milestones::CreateService.new(project, current_user, milestone_params).execute
+ end
+ end
+
+ true
+ rescue ActiveRecord::ActiveRecordError => e
+ flash.now[:alert] = "An error occurred while creating the milestone: #{e.message}"
+ false
+ end
+
+ def render_new_with_error(empty_project_ids)
+ @milestone = Milestone.new(milestone_params)
+ @milestone.errors.add(:project_id, "Please select at least one project.") if empty_project_ids
+ render :new
+ end
+
def authorize_admin_milestones!
return render_404 unless can?(current_user, :admin_milestones, group)
end
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index 21135f7d607..d28e96c3f18 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -55,7 +55,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
end
else
saml_user = Gitlab::Saml::User.new(oauth)
- saml_user.save
+ saml_user.save if saml_user.changed?
@user = saml_user.gl_user
continue_login_process
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index 657ee94cfd7..74150ad606b 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -68,7 +68,9 @@ class Projects::ApplicationController < ApplicationController
end
def require_non_empty_project
- redirect_to namespace_project_path(@project.namespace, @project) if @project.empty_repo?
+ # Be sure to return status code 303 to avoid a double DELETE:
+ # http://api.rubyonrails.org/classes/ActionController/Redirecting.html
+ redirect_to namespace_project_path(@project.namespace, @project), status: 303 if @project.empty_repo?
end
def require_branch_head
diff --git a/app/controllers/projects/badges_controller.rb b/app/controllers/projects/badges_controller.rb
index 6ff47c4033a..824aa41db51 100644
--- a/app/controllers/projects/badges_controller.rb
+++ b/app/controllers/projects/badges_controller.rb
@@ -1,12 +1,20 @@
class Projects::BadgesController < Projects::ApplicationController
- before_action :no_cache_headers
+ layout 'project_settings'
+ before_action :authorize_admin_project!, only: [:index]
+ before_action :no_cache_headers, except: [:index]
+
+ def index
+ @ref = params[:ref] || @project.default_branch || 'master'
+ @build_badge = Gitlab::Badge::Build.new(@project, @ref)
+ end
def build
+ badge = Gitlab::Badge::Build.new(project, params[:ref])
+
respond_to do |format|
format.html { render_404 }
format.svg do
- image = Ci::ImageForBuildService.new.execute(project, ref: params[:ref])
- send_file(image.path, filename: image.name, disposition: 'inline', type: 'image/svg+xml')
+ send_data(badge.data, type: badge.type, disposition: 'inline')
end
end
end
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index c0a53734921..d09e7375b67 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -48,7 +48,7 @@ class Projects::BranchesController < Projects::ApplicationController
respond_to do |format|
format.html do
redirect_to namespace_project_branches_path(@project.namespace,
- @project)
+ @project), status: 303
end
format.js { render status: status[:return_code] }
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 6189de09f27..49064f5d505 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -57,8 +57,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
respond_to do |format|
format.html
format.json { render json: @merge_request }
- format.diff { render text: @merge_request.to_diff(current_user) }
- format.patch { render text: @merge_request.to_patch(current_user) }
+ format.diff { render text: @merge_request.to_diff }
+ format.patch { render text: @merge_request.to_patch }
end
end
@@ -154,7 +154,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request.target_project, @merge_request])
end
format.json do
- render json: @merge_request.to_json(include: [:milestone, :labels, :assignee])
+ render json: @merge_request.to_json(include: [:milestone, :labels, assignee: { methods: :avatar_url }])
end
end
else
@@ -224,14 +224,22 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def ci_status
- ci_service = @merge_request.source_project.ci_service
- status = ci_service.commit_status(merge_request.last_commit.sha, merge_request.source_branch)
+ ci_commit = @merge_request.ci_commit
+ if ci_commit
+ status = ci_commit.status
+ coverage = ci_commit.try(:coverage)
+ else
+ ci_service = @merge_request.source_project.ci_service
+ status = ci_service.commit_status(merge_request.last_commit.sha, merge_request.source_branch) if ci_service
- if ci_service.respond_to?(:commit_coverage)
- coverage = ci_service.commit_coverage(merge_request.last_commit.sha, merge_request.source_branch)
+ if ci_service.respond_to?(:commit_coverage)
+ coverage = ci_service.commit_coverage(merge_request.last_commit.sha, merge_request.source_branch)
+ end
end
response = {
+ title: merge_request.title,
+ sha: merge_request.last_commit_short_sha,
status: status,
coverage: coverage
}
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index e7bddc4a6f1..e457db2f0b7 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -94,9 +94,14 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
def apply_import
- giver = Project.find(params[:source_project_id])
- status = @project.team.import(giver, current_user)
- notice = status ? "Successfully imported" : "Import failed"
+ source_project = Project.find(params[:source_project_id])
+
+ if can?(current_user, :read_project_member, source_project)
+ status = @project.team.import(source_project, current_user)
+ notice = status ? "Successfully imported" : "Import failed"
+ else
+ return render_404
+ end
redirect_to(namespace_project_project_members_path(project.namespace, project),
notice: notice)
diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb
index 00df1c9c965..d79f16e6a5a 100644
--- a/app/controllers/projects/refs_controller.rb
+++ b/app/controllers/projects/refs_controller.rb
@@ -24,6 +24,8 @@ class Projects::RefsController < Projects::ApplicationController
namespace_project_find_file_path(@project.namespace, @project, @id)
when "graphs_commits"
commits_namespace_project_graph_path(@project.namespace, @project, @id)
+ when "badges"
+ namespace_project_badges_path(@project.namespace, @project, ref: @id)
else
namespace_project_commits_path(@project.namespace, @project, @id)
end
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 02ceb8f4334..9f3a4a69721 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -88,6 +88,20 @@ class Projects::WikisController < Projects::ApplicationController
)
end
+ def markdown_preview
+ text = params[:text]
+
+ ext = Gitlab::ReferenceExtractor.new(@project, current_user, current_user)
+ ext.analyze(text)
+
+ render json: {
+ body: view_context.markdown(text, pipeline: :wiki, project_wiki: @project_wiki),
+ references: {
+ users: ext.users.map(&:username)
+ }
+ }
+ end
+
def git_access
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index ec5318f2d2c..41a4c41cf80 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -40,6 +40,9 @@ class ProjectsController < Projects::ApplicationController
def update
status = ::Projects::UpdateService.new(@project, current_user, project_params).execute
+ # Refresh the repo in case anything changed
+ @repository = project.repository
+
respond_to do |format|
if status
flash[:notice] = "Project '#{@project.name}' was successfully updated."
@@ -71,7 +74,7 @@ class ProjectsController < Projects::ApplicationController
def remove_fork
return access_denied! unless can?(current_user, :remove_fork_project, @project)
- if @project.unlink_fork
+ if ::Projects::UnlinkForkService.new(@project, current_user).execute
flash[:notice] = 'The fork relationship has been removed.'
end
end
@@ -143,7 +146,7 @@ class ProjectsController < Projects::ApplicationController
participants = ::Projects::ParticipantsService.new(@project, current_user).execute(note_type, note_id)
@suggestions = {
- emojis: autocomplete_emojis,
+ emojis: AwardEmoji.urls,
issues: autocomplete.issues,
mergerequests: autocomplete.merge_requests,
members: participants
@@ -240,17 +243,6 @@ class ProjectsController < Projects::ApplicationController
)
end
- def autocomplete_emojis
- Rails.cache.fetch("autocomplete-emoji-#{Gemojione::VERSION}") do
- Emoji.emojis.map do |name, emoji|
- {
- name: name,
- path: view_context.image_url("#{emoji["unicode"]}.png")
- }
- end
- end
- end
-
def repo_exists?
project.repository_exists? && !project.empty_repo?
end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 65677a3dd3c..c29f4609e93 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -5,7 +5,8 @@ class SessionsController < Devise::SessionsController
skip_before_action :check_2fa_requirement, only: [:destroy]
prepend_before_action :check_initial_setup, only: [:new]
- prepend_before_action :authenticate_with_two_factor, only: [:create]
+ prepend_before_action :authenticate_with_two_factor,
+ if: :two_factor_enabled?, only: [:create]
prepend_before_action :store_redirect_path, only: [:new]
before_action :auto_sign_in_with_provider, only: [:new]
@@ -56,10 +57,10 @@ class SessionsController < Devise::SessionsController
end
def find_user
- if user_params[:login]
- User.by_login(user_params[:login])
- elsif user_params[:otp_attempt] && session[:otp_user_id]
+ if session[:otp_user_id]
User.find(session[:otp_user_id])
+ elsif user_params[:login]
+ User.by_login(user_params[:login])
end
end
@@ -83,11 +84,13 @@ class SessionsController < Devise::SessionsController
end
end
+ def two_factor_enabled?
+ find_user.try(:two_factor_enabled?)
+ end
+
def authenticate_with_two_factor
user = self.resource = find_user
- return unless user && user.two_factor_enabled?
-
if user_params[:otp_attempt].present? && session[:otp_user_id]
if valid_otp_attempt?(user)
# Remove any lingering user data from login
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 23693629a4c..60a0ff32c9c 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -3,10 +3,6 @@ module ApplicationSettingsHelper
current_application_settings.gravatar_enabled?
end
- def twitter_sharing_enabled?
- current_application_settings.twitter_sharing_enabled?
- end
-
def signup_enabled?
current_application_settings.signup_enabled?
end
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index d3e5e3aa8b9..592bad8ba24 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -214,4 +214,12 @@ module EventsHelper
end
end
end
+
+ def event_row_class(event)
+ if event.body?
+ "event-block"
+ else
+ "event-inline"
+ end
+ end
end
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 494dad0b41e..8a97a74ad73 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -1,4 +1,5 @@
module SearchHelper
+
def search_autocomplete_opts(term)
return unless current_user
@@ -23,45 +24,44 @@ module SearchHelper
# Autocomplete results for various settings pages
def default_autocomplete
[
- { label: "Profile settings", url: profile_path },
- { label: "SSH Keys", url: profile_keys_path },
- { label: "Dashboard", url: root_path },
- { label: "Admin Section", url: admin_root_path },
+ { category: "Settings", label: "Profile settings", url: profile_path },
+ { category: "Settings", label: "SSH Keys", url: profile_keys_path },
+ { category: "Settings", label: "Dashboard", url: root_path },
+ { category: "Settings", label: "Admin Section", url: admin_root_path },
]
end
# Autocomplete results for internal help pages
def help_autocomplete
[
- { label: "help: API Help", url: help_page_path("api", "README") },
- { label: "help: Markdown Help", url: help_page_path("markdown", "markdown") },
- { label: "help: Permissions Help", url: help_page_path("permissions", "permissions") },
- { label: "help: Public Access Help", url: help_page_path("public_access", "public_access") },
- { label: "help: Rake Tasks Help", url: help_page_path("raketasks", "README") },
- { label: "help: SSH Keys Help", url: help_page_path("ssh", "README") },
- { label: "help: System Hooks Help", url: help_page_path("system_hooks", "system_hooks") },
- { label: "help: Webhooks Help", url: help_page_path("web_hooks", "web_hooks") },
- { label: "help: Workflow Help", url: help_page_path("workflow", "README") },
+ { category: "Help", label: "API Help", url: help_page_path("api", "README") },
+ { category: "Help", label: "Markdown Help", url: help_page_path("markdown", "markdown") },
+ { category: "Help", label: "Permissions Help", url: help_page_path("permissions", "permissions") },
+ { category: "Help", label: "Public Access Help", url: help_page_path("public_access", "public_access") },
+ { category: "Help", label: "Rake Tasks Help", url: help_page_path("raketasks", "README") },
+ { category: "Help", label: "SSH Keys Help", url: help_page_path("ssh", "README") },
+ { category: "Help", label: "System Hooks Help", url: help_page_path("system_hooks", "system_hooks") },
+ { category: "Help", label: "Webhooks Help", url: help_page_path("web_hooks", "web_hooks") },
+ { category: "Help", label: "Workflow Help", url: help_page_path("workflow", "README") },
]
end
# Autocomplete results for the current project, if it's defined
def project_autocomplete
if @project && @project.repository.exists? && @project.repository.root_ref
- prefix = search_result_sanitize(@project.name_with_namespace)
ref = @ref || @project.repository.root_ref
[
- { label: "#{prefix} - Files", url: namespace_project_tree_path(@project.namespace, @project, ref) },
- { label: "#{prefix} - Commits", url: namespace_project_commits_path(@project.namespace, @project, ref) },
- { label: "#{prefix} - Network", url: namespace_project_network_path(@project.namespace, @project, ref) },
- { label: "#{prefix} - Graph", url: namespace_project_graph_path(@project.namespace, @project, ref) },
- { label: "#{prefix} - Issues", url: namespace_project_issues_path(@project.namespace, @project) },
- { label: "#{prefix} - Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) },
- { label: "#{prefix} - Milestones", url: namespace_project_milestones_path(@project.namespace, @project) },
- { label: "#{prefix} - Snippets", url: namespace_project_snippets_path(@project.namespace, @project) },
- { label: "#{prefix} - Members", url: namespace_project_project_members_path(@project.namespace, @project) },
- { label: "#{prefix} - Wiki", url: namespace_project_wikis_path(@project.namespace, @project) },
+ { category: "Current Project", label: "Files", url: namespace_project_tree_path(@project.namespace, @project, ref) },
+ { category: "Current Project", label: "Commits", url: namespace_project_commits_path(@project.namespace, @project, ref) },
+ { category: "Current Project", label: "Network", url: namespace_project_network_path(@project.namespace, @project, ref) },
+ { category: "Current Project", label: "Graph", url: namespace_project_graph_path(@project.namespace, @project, ref) },
+ { category: "Current Project", label: "Issues", url: namespace_project_issues_path(@project.namespace, @project) },
+ { category: "Current Project", label: "Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) },
+ { category: "Current Project", label: "Milestones", url: namespace_project_milestones_path(@project.namespace, @project) },
+ { category: "Current Project", label: "Snippets", url: namespace_project_snippets_path(@project.namespace, @project) },
+ { category: "Current Project", label: "Members", url: namespace_project_project_members_path(@project.namespace, @project) },
+ { category: "Current Project", label: "Wiki", url: namespace_project_wikis_path(@project.namespace, @project) },
]
else
[]
@@ -72,7 +72,9 @@ module SearchHelper
def groups_autocomplete(term, limit = 5)
current_user.authorized_groups.search(term).limit(limit).map do |group|
{
- label: "group: #{search_result_sanitize(group.name)}",
+ category: "Groups",
+ id: group.id,
+ label: "#{search_result_sanitize(group.name)}",
url: group_path(group)
}
end
@@ -83,7 +85,10 @@ module SearchHelper
current_user.authorized_projects.search_by_title(term).
sorted_by_stars.non_archived.limit(limit).map do |p|
{
- label: "project: #{search_result_sanitize(p.name_with_namespace)}",
+ category: "Projects",
+ id: p.id,
+ value: "#{search_result_sanitize(p.name)}",
+ label: "#{search_result_sanitize(p.name_with_namespace)}",
url: namespace_project_path(p.namespace, p)
}
end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 8cbc9eefc7b..826e5f96fa1 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -110,6 +110,10 @@ class Notify < BaseMailer
headers['Reply-To'] = address
+ fallback_reply_message_id = "<reply-#{reply_key}@#{Gitlab.config.gitlab.host}>".freeze
+ headers['References'] ||= ''
+ headers['References'] << ' ' << fallback_reply_message_id
+
@reply_by_email = true
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index c4879598c4e..052cd874733 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -12,7 +12,6 @@
# updated_at :datetime
# home_page_url :string(255)
# default_branch_protection :integer default(2)
-# twitter_sharing_enabled :boolean default(TRUE)
# restricted_visibility_levels :text
# version_check_enabled :boolean default(TRUE)
# max_attachment_size :integer default(10), not null
@@ -140,7 +139,6 @@ class ApplicationSetting < ActiveRecord::Base
default_branch_protection: Settings.gitlab['default_branch_protection'],
signup_enabled: Settings.gitlab['signup_enabled'],
signin_enabled: Settings.gitlab['signin_enabled'],
- twitter_sharing_enabled: Settings.gitlab['twitter_sharing_enabled'],
gravatar_enabled: Settings.gravatar['enabled'],
sign_in_text: Settings.extra['sign_in_text'],
restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
diff --git a/app/models/commit.rb b/app/models/commit.rb
index d0dbe009d0d..d09876a07d9 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -74,14 +74,14 @@ class Commit
#
# This pattern supports cross-project references.
def self.reference_pattern
- %r{
+ @reference_pattern ||= %r{
(?:#{Project.reference_pattern}#{reference_prefix})?
(?<commit>\h{7,40})
}x
end
def self.link_reference_pattern
- super("commit", /(?<commit>\h{7,40})/)
+ @link_reference_pattern ||= super("commit", /(?<commit>\h{7,40})/)
end
def to_reference(from_project = nil)
diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb
index 289dbc57287..51673897d98 100644
--- a/app/models/commit_range.rb
+++ b/app/models/commit_range.rb
@@ -43,14 +43,14 @@ class CommitRange
#
# This pattern supports cross-project references.
def self.reference_pattern
- %r{
+ @reference_pattern ||= %r{
(?:#{Project.reference_pattern}#{reference_prefix})?
(?<commit_range>#{STRICT_PATTERN})
}x
end
def self.link_reference_pattern
- super("compare", /(?<commit_range>#{PATTERN})/)
+ @link_reference_pattern ||= super("compare", /(?<commit_range>#{PATTERN})/)
end
# Initialize a CommitRange
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index cf5b2c71675..afa2ca039ae 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -19,6 +19,7 @@ module Issuable
has_many :notes, as: :noteable, dependent: :destroy
has_many :label_links, as: :target, dependent: :destroy
has_many :labels, through: :label_links
+ has_many :todos, as: :target, dependent: :destroy
validates :author, presence: true
validates :title, presence: true, length: { within: 0..255 }
@@ -41,7 +42,7 @@ module Issuable
scope :join_project, -> { joins(:project) }
scope :references_project, -> { references(:project) }
- scope :non_archived, -> { join_project.merge(Project.non_archived.only(:where)) }
+ scope :non_archived, -> { join_project.where(projects: { archived: false }) }
delegate :name,
:email,
diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb
index 2ca79df0a29..b8585d4e577 100644
--- a/app/models/external_issue.rb
+++ b/app/models/external_issue.rb
@@ -31,7 +31,7 @@ class ExternalIssue
# Pattern used to extract `JIRA-123` issue references from text
def self.reference_pattern
- %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)}
+ @reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)}
end
def to_reference(_from_project = nil)
diff --git a/app/models/issue.rb b/app/models/issue.rb
index ed960cb39f4..e064b0f8b95 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -73,14 +73,14 @@ class Issue < ActiveRecord::Base
#
# This pattern supports cross-project references.
def self.reference_pattern
- %r{
+ @reference_pattern ||= %r{
(#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}(?<issue>\d+)
}x
end
def self.link_reference_pattern
- super("issues", /(?<issue>\d+)/)
+ @link_reference_pattern ||= super("issues", /(?<issue>\d+)/)
end
def to_reference(from_project = nil)
diff --git a/app/models/label.rb b/app/models/label.rb
index 500d5a35521..55c01cae762 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -56,7 +56,7 @@ class Label < ActiveRecord::Base
# This pattern supports cross-project references.
#
def self.reference_pattern
- %r{
+ @reference_pattern ||= %r{
(#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}
(?:
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 7c61a7ae18c..bf185cb5dd8 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -135,6 +135,7 @@ class MergeRequest < ActiveRecord::Base
scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) }
scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
scope :of_projects, ->(ids) { where(target_project_id: ids) }
+ scope :from_project, ->(project) { where(source_project_id: project.id) }
scope :merged, -> { with_state(:merged) }
scope :closed_and_merged, -> { with_states(:closed, :merged) }
@@ -149,14 +150,14 @@ class MergeRequest < ActiveRecord::Base
#
# This pattern supports cross-project references.
def self.reference_pattern
- %r{
+ @reference_pattern ||= %r{
(#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}(?<merge_request>\d+)
}x
end
def self.link_reference_pattern
- super("merge_requests", /(?<merge_request>\d+)/)
+ @link_reference_pattern ||= super("merge_requests", /(?<merge_request>\d+)/)
end
# Returns all the merge requests from an ActiveRecord:Relation.
@@ -331,15 +332,15 @@ class MergeRequest < ActiveRecord::Base
# Returns the raw diff for this merge request
#
# see "git diff"
- def to_diff(current_user)
- target_project.repository.diff_text(target_branch, source_sha)
+ def to_diff
+ target_project.repository.diff_text(diff_base_commit.sha, source_sha)
end
# Returns the commit as a series of email patches.
#
# see "git format-patch"
- def to_patch(current_user)
- target_project.repository.format_patch(target_branch, source_sha)
+ def to_patch
+ target_project.repository.format_patch(diff_base_commit.sha, source_sha)
end
def hook_attrs
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index bbd59eab9ae..986184dd301 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -79,7 +79,7 @@ class Milestone < ActiveRecord::Base
end
def self.link_reference_pattern
- super("milestones", /(?<milestone>\d+)/)
+ @link_reference_pattern ||= super("milestones", /(?<milestone>\d+)/)
end
def self.upcoming
@@ -89,7 +89,7 @@ class Milestone < ActiveRecord::Base
def to_reference(from_project = nil)
escaped_title = self.title.gsub("]", "\\]")
- h = Gitlab::Application.routes.url_helpers
+ h = Gitlab::Routing.url_helpers
url = h.namespace_project_milestone_url(self.project.namespace, self.project, self)
"[#{escaped_title}](#{url})"
diff --git a/app/models/note.rb b/app/models/note.rb
index b0c33f2eec5..87ced65c650 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -311,7 +311,7 @@ class Note < ActiveRecord::Base
for_merge_request? && for_diff_line?
end
- def for_project_snippet?
+ def for_snippet?
noteable_type == "Snippet"
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 2f9621809b6..c3d42705902 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -207,6 +207,8 @@ class Project < ActiveRecord::Base
mount_uploader :avatar, AvatarUploader
# Scopes
+ default_scope { where(pending_delete: false) }
+
scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) }
scope :sorted_by_stars, -> { reorder('projects.star_count DESC') }
scope :sorted_by_names, -> { joins(:namespace).reorder('namespaces.name ASC, projects.name ASC') }
@@ -470,7 +472,7 @@ class Project < ActiveRecord::Base
end
def web_url
- Gitlab::Application.routes.url_helpers.namespace_project_url(self.namespace, self)
+ Gitlab::Routing.url_helpers.namespace_project_url(self.namespace, self)
end
def web_url_without_protocol
@@ -591,7 +593,7 @@ class Project < ActiveRecord::Base
if avatar.present?
[gitlab_config.url, avatar.url].join
elsif avatar_in_git
- Gitlab::Application.routes.url_helpers.namespace_project_avatar_url(namespace, self)
+ Gitlab::Routing.url_helpers.namespace_project_avatar_url(namespace, self)
end
end
@@ -930,16 +932,6 @@ class Project < ActiveRecord::Base
self.builds_enabled = true
end
- def unlink_fork
- if forked?
- forked_from_project.lfs_objects.find_each do |lfs_object|
- lfs_object.projects << self
- end
-
- forked_project_link.destroy
- end
- end
-
def any_runners?(&block)
if runners.active.any?(&block)
return true
diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb
index f6313255cbb..f9f04838766 100644
--- a/app/models/project_services/builds_email_service.rb
+++ b/app/models/project_services/builds_email_service.rb
@@ -50,12 +50,15 @@ class BuildsEmailService < Service
def execute(push_data)
return unless supported_events.include?(push_data[:object_kind])
+ return unless should_build_be_notified?(push_data)
- if should_build_be_notified?(push_data)
+ recipients = all_recipients(push_data)
+
+ if recipients.any?
BuildEmailWorker.perform_async(
push_data[:build_id],
- all_recipients(push_data),
- push_data,
+ recipients,
+ push_data
)
end
end
@@ -84,7 +87,7 @@ class BuildsEmailService < Service
end
def all_recipients(data)
- all_recipients = recipients.split(',')
+ all_recipients = recipients.split(',').compact.reject(&:blank?)
if add_pusher? && data[:user][:email]
all_recipients << "#{data[:user][:email]}"
diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb
index 05436cd0f79..eaa5654b9c6 100644
--- a/app/models/project_services/gitlab_issue_tracker_service.rb
+++ b/app/models/project_services/gitlab_issue_tracker_service.rb
@@ -20,7 +20,7 @@
#
class GitlabIssueTrackerService < IssueTrackerService
- include Gitlab::Application.routes.url_helpers
+ include Gitlab::Routing.url_helpers
prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index aba37921c09..1ed42c4f3e7 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -21,7 +21,7 @@
class JiraService < IssueTrackerService
include HTTParty
- include Gitlab::Application.routes.url_helpers
+ include Gitlab::Routing.url_helpers
DEFAULT_API_VERSION = 2
diff --git a/app/models/repository.rb b/app/models/repository.rb
index c07e8072043..8dead3a5884 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -72,7 +72,7 @@ class Repository
return @has_visible_content unless @has_visible_content.nil?
@has_visible_content = cache.fetch(:has_visible_content?) do
- raw_repository.branch_count > 0
+ branch_count > 0
end
end
@@ -173,7 +173,7 @@ class Repository
end
def branch_names
- cache.fetch(:branch_names) { raw_repository.branch_names }
+ cache.fetch(:branch_names) { branches.map(&:name) }
end
def tag_names
@@ -191,7 +191,7 @@ class Repository
end
def branch_count
- @branch_count ||= cache.fetch(:branch_count) { raw_repository.branch_count }
+ @branch_count ||= cache.fetch(:branch_count) { branches.size }
end
def tag_count
@@ -239,7 +239,7 @@ class Repository
def expire_branches_cache
cache.expire(:branch_names)
- @branches = nil
+ @local_branches = nil
end
def expire_cache(branch_name = nil, revision = nil)
@@ -331,10 +331,14 @@ class Repository
# Runs code after a repository has been created.
def after_create
expire_exists_cache
+ expire_root_ref_cache
+ expire_emptiness_caches
end
# Runs code just before a repository is deleted.
def before_delete
+ expire_exists_cache
+
expire_cache if exists?
expire_root_ref_cache
@@ -362,6 +366,11 @@ class Repository
expire_tag_count_cache
end
+ def before_import
+ expire_emptiness_caches
+ expire_exists_cache
+ end
+
# Runs code after a repository has been forked/imported.
def after_import
expire_emptiness_caches
@@ -612,10 +621,14 @@ class Repository
refs_contains_sha('tag', sha)
end
- def branches
- @branches ||= raw_repository.branches
+ def local_branches
+ @local_branches ||= rugged.branches.each(:local).map do |branch|
+ Gitlab::Git::Branch.new(branch.name, branch.target)
+ end
end
+ alias_method :branches, :local_branches
+
def tags
@tags ||= raw_repository.tags
end
@@ -818,7 +831,7 @@ class Repository
end
def fetch_ref(source_path, source_ref, target_ref)
- args = %W(#{Gitlab.config.git.bin_path} fetch -f #{source_path} #{source_ref}:#{target_ref})
+ args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
Gitlab::Popen.popen(args, path_to_repo)
end
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index b9e835a4486..b96e3937281 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -56,14 +56,14 @@ class Snippet < ActiveRecord::Base
#
# This pattern supports cross-project references.
def self.reference_pattern
- %r{
+ @reference_pattern ||= %r{
(#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}(?<snippet>\d+)
}x
end
def self.link_reference_pattern
- super("snippets", /(?<snippet>\d+)/)
+ @link_reference_pattern ||= super("snippets", /(?<snippet>\d+)/)
end
def to_reference(from_project = nil)
diff --git a/app/models/user.rb b/app/models/user.rb
index af6b86bfa70..f68379fd050 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -412,6 +412,8 @@ class User < ActiveRecord::Base
end
def owns_notification_email
+ return if self.temp_oauth_email?
+
self.errors.add(:notification_email, "is not an email you own") unless self.all_emails.include?(self.notification_email)
end
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index c007d648dd6..dc74c02760b 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -43,23 +43,27 @@ class GitPushService < BaseService
@push_commits = @project.repository.commits_between(params[:oldrev], params[:newrev])
process_commit_messages
end
- # Checks if the main language has changed in the project and if so
- # it updates it accordingly
- update_main_language
# Update merge requests that may be affected by this push. A new branch
# could cause the last commit of a merge request to change.
update_merge_requests
+ # Checks if the main language has changed in the project and if so
+ # it updates it accordingly
+ update_main_language
+
perform_housekeeping
end
def update_main_language
- current_language = @project.repository.main_language
+ # Performance can be bad so for now only check main_language once
+ # See https://gitlab.com/gitlab-org/gitlab-ce/issues/14937
+ return if @project.main_language.present?
- unless current_language == @project.main_language
- return @project.update_attributes(main_language: current_language)
- end
+ return unless is_default_branch?
+ return unless push_to_new_branch? || push_to_existing_branch?
+ current_language = @project.repository.main_language
+ @project.update_attributes(main_language: current_language)
true
end
diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb
index a5efb21fab6..82e7090f1ea 100644
--- a/app/services/issues/move_service.rb
+++ b/app/services/issues/move_service.rb
@@ -43,7 +43,7 @@ module Issues
def create_new_issue
new_params = { id: nil, iid: nil, label_ids: [], milestone: nil,
project: @new_project, author: @old_issue.author,
- description: unfold_references(@old_issue.description) }
+ description: rewrite_content(@old_issue.description) }
new_params = @old_issue.serializable_hash.merge(new_params)
CreateService.new(@new_project, @current_user, new_params).execute
@@ -53,7 +53,7 @@ module Issues
@old_issue.notes.find_each do |note|
new_note = note.dup
new_params = { project: @new_project, noteable: @new_issue,
- note: unfold_references(new_note.note),
+ note: rewrite_content(new_note.note),
created_at: note.created_at,
updated_at: note.updated_at }
@@ -61,6 +61,18 @@ module Issues
end
end
+ def rewrite_content(content)
+ return unless content
+
+ rewriters = [Gitlab::Gfm::ReferenceRewriter,
+ Gitlab::Gfm::UploadsRewriter]
+
+ rewriters.inject(content) do |text, klass|
+ rewriter = klass.new(text, @old_project, @current_user)
+ rewriter.rewrite(@new_project)
+ end
+ end
+
def close_issue
close_service = CloseService.new(@old_project, @current_user)
close_service.execute(@old_issue, notifications: false, system_note: false)
@@ -78,20 +90,12 @@ module Issues
direction: :to)
end
- def unfold_references(content)
- return unless content
-
- rewriter = Gitlab::Gfm::ReferenceRewriter.new(content, @old_project,
- @current_user)
- rewriter.rewrite(@new_project)
+ def mark_as_moved
+ @old_issue.update(moved_to: @new_issue)
end
def notify_participants
notification_service.issue_moved(@old_issue, @new_issue, @current_user)
end
-
- def mark_as_moved
- @old_issue.update(moved_to: @new_issue)
- end
end
end
diff --git a/app/services/milestones/create_service.rb b/app/services/milestones/create_service.rb
index b8e08c9f1eb..3b90399af64 100644
--- a/app/services/milestones/create_service.rb
+++ b/app/services/milestones/create_service.rb
@@ -3,7 +3,7 @@ module Milestones
def execute
milestone = project.milestones.new(params)
- if milestone.save
+ if milestone.save!
event_service.open_milestone(milestone, current_user)
end
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index 2015897dd19..ef15ef6a473 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -46,6 +46,8 @@ module Projects
def import_data
return unless has_importer?
+ project.repository.before_import
+
unless importer.execute
raise Error, 'The remote data could not be imported.'
end
diff --git a/app/services/projects/unlink_fork_service.rb b/app/services/projects/unlink_fork_service.rb
new file mode 100644
index 00000000000..315c3e16292
--- /dev/null
+++ b/app/services/projects/unlink_fork_service.rb
@@ -0,0 +1,19 @@
+module Projects
+ class UnlinkForkService < BaseService
+ def execute
+ return unless @project.forked?
+
+ @project.forked_from_project.lfs_objects.find_each do |lfs_object|
+ lfs_object.projects << @project
+ end
+
+ merge_requests = @project.forked_from_project.merge_requests.opened.from_project(@project)
+
+ merge_requests.each do |mr|
+ MergeRequests::CloseService.new(@project, @current_user).execute(mr)
+ end
+
+ @project.forked_project_link.destroy
+ end
+ end
+end
diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb
index ea2b26ccb52..f0615ec7420 100644
--- a/app/services/system_hooks_service.rb
+++ b/app/services/system_hooks_service.rb
@@ -95,17 +95,19 @@ class SystemHooksService
end
def project_member_data(model)
+ project = model.project || Project.unscoped.find(model.source_id)
+
{
- project_name: model.project.name,
- project_path: model.project.path,
- project_path_with_namespace: model.project.path_with_namespace,
- project_id: model.project.id,
- user_username: model.user.username,
- user_name: model.user.name,
- user_email: model.user.email,
- user_id: model.user.id,
- access_level: model.human_access,
- project_visibility: Project.visibility_levels.key(model.project.visibility_level_field).downcase
+ project_name: project.name,
+ project_path: project.path,
+ project_path_with_namespace: project.path_with_namespace,
+ project_id: project.id,
+ user_username: model.user.username,
+ user_name: model.user.name,
+ user_email: model.user.email,
+ user_id: model.user.id,
+ access_level: model.human_access,
+ project_visibility: Project.visibility_levels.key(project.visibility_level_field).downcase
}
end
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index e022a046c48..658b086496f 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -224,7 +224,7 @@ class SystemNoteService
#
# "Started branch `issue-branch-button-201`"
def self.new_issue_branch(issue, project, author, branch)
- h = Gitlab::Application.routes.url_helpers
+ h = Gitlab::Routing.url_helpers
link = h.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch)
body = "Started branch [`#{branch}`](#{link})"
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index f2662922e90..42c5bca90fd 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -123,7 +123,7 @@ class TodoService
def handle_note(note, author)
# Skip system notes, and notes on project snippet
- return if note.system? || note.for_project_snippet?
+ return if note.system? || note.for_snippet?
project = note.project
target = note.noteable
@@ -170,14 +170,30 @@ class TodoService
end
def filter_mentioned_users(project, target, author)
- mentioned_users = target.mentioned_users.select do |user|
- user.can?(:read_project, project)
- end
-
+ mentioned_users = target.mentioned_users
+ mentioned_users = reject_users_without_access(mentioned_users, project, target)
mentioned_users.delete(author)
mentioned_users.uniq
end
+ def reject_users_without_access(users, project, target)
+ if target.is_a?(Note) && target.for_issue?
+ target = target.noteable
+ end
+
+ if target.is_a?(Issue)
+ select_users(users, :read_issue, target)
+ else
+ select_users(users, :read_project, project)
+ end
+ end
+
+ def select_users(users, ability, subject)
+ users.select do |user|
+ user.can?(ability.to_sym, subject)
+ end
+ end
+
def pending_todos(user, criteria = {})
valid_keys = [:project_id, :target_id, :target_type, :commit_id]
user.todos.pending.where(criteria.slice(*valid_keys))
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index 86d24469e05..1af9e9b0edb 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -1,14 +1,15 @@
# encoding: utf-8
class FileUploader < CarrierWave::Uploader::Base
include UploaderHelper
+ MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)}
storage :file
attr_accessor :project, :secret
- def initialize(project, secret = self.class.generate_secret)
+ def initialize(project, secret = nil)
@project = project
- @secret = secret
+ @secret = secret || self.class.generate_secret
end
def base_dir
@@ -23,14 +24,14 @@ class FileUploader < CarrierWave::Uploader::Base
File.join(base_dir, 'tmp', @project.path_with_namespace, @secret)
end
- def self.generate_secret
- SecureRandom.hex
- end
-
def secure_url
File.join("/uploads", @secret, file.filename)
end
+ def to_markdown
+ to_h[:markdown]
+ end
+
def to_h
filename = image? ? self.file.basename : self.file.filename
escaped_filename = filename.gsub("]", "\\]")
@@ -45,4 +46,8 @@ class FileUploader < CarrierWave::Uploader::Base
markdown: markdown
}
end
+
+ def self.generate_secret
+ SecureRandom.hex
+ end
end
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 0350995d03d..de86dacbb12 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -77,13 +77,6 @@
= f.check_box :gravatar_enabled
Gravatar enabled
.form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :twitter_sharing_enabled do
- = f.check_box :twitter_sharing_enabled, :'aria-describedby' => 'twitter_help_block'
- Twitter enabled
- %span.help-block#twitter_help_block Show users a button to share their newly created public or internal projects on twitter
- .form-group
= f.label :default_projects_limit, class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :default_projects_limit, class: 'form-control'
diff --git a/app/views/admin/builds/_build.html.haml b/app/views/admin/builds/_build.html.haml
index 588ad767426..3571eefd570 100644
--- a/app/views/admin/builds/_build.html.haml
+++ b/app/views/admin/builds/_build.html.haml
@@ -15,7 +15,7 @@
%td
- if project
- = link_to project.name_with_namespace, admin_namespace_project_path(project.namespace, project), class: "monospace"
+ = link_to project.name_with_namespace, admin_namespace_project_path(project.namespace, project)
%td
= link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace"
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 3274ba5377b..6dd2fef395d 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -1,4 +1,4 @@
-.admin-dashboard
+.admin-dashboard.prepend-top-default
.row
.col-md-4
%h4 Statistics
diff --git a/app/views/admin/deploy_keys/index.html.haml b/app/views/admin/deploy_keys/index.html.haml
index 41c43899978..149593e7f46 100644
--- a/app/views/admin/deploy_keys/index.html.haml
+++ b/app/views/admin/deploy_keys/index.html.haml
@@ -1,5 +1,5 @@
- page_title "Deploy Keys"
-.panel.panel-default
+.panel.panel-default.prepend-top-default
.panel-heading
Public deploy keys (#{@deploy_keys.count})
.controls
diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml
new file mode 100644
index 00000000000..9025aaac097
--- /dev/null
+++ b/app/views/admin/groups/_group.html.haml
@@ -0,0 +1,28 @@
+- css_class = '' unless local_assigns[:css_class]
+- css_class += ' no-description' if group.description.blank?
+
+%li.group-row{ class: css_class }
+ .controls.hidden-xs
+ = link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: 'btn btn-grouped btn-sm'
+ = link_to 'Destroy', [:admin, group], data: {confirm: "REMOVE #{group.name}? Are you sure?"}, method: :delete, class: 'btn btn-grouped btn-sm btn-remove'
+
+ .stats
+ %span
+ = icon('bookmark')
+ = number_with_delimiter(group.projects.count)
+
+ %span
+ = icon('users')
+ = number_with_delimiter(group.users.count)
+
+ %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)}
+ = visibility_level_icon(group.visibility_level, fw: false)
+
+ = image_tag group_icon(group), class: 'avatar s40 hidden-xs'
+ .title
+ = link_to [:admin, group], class: 'group-name' do
+ = group.name
+
+ - if group.description.present?
+ .description
+ = markdown(group.description, pipeline: :description)
diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml
index 6bdc885a312..775072a7441 100644
--- a/app/views/admin/groups/index.html.haml
+++ b/app/views/admin/groups/index.html.haml
@@ -1,20 +1,19 @@
- page_title "Groups"
%h3.page-title
Groups (#{number_with_delimiter(@groups.total_count)})
- = link_to 'New Group', new_admin_group_path, class: "btn btn-new pull-right"
%p.light
Group allows you to keep projects organized.
Use groups for uniting related projects.
-%hr
-= form_tag admin_groups_path, method: :get, class: 'form-inline' do
- = hidden_field_tag :sort, @sort
- .form-group
- = text_field_tag :name, params[:name], class: "form-control"
- = button_tag "Search", class: "btn submit btn-primary"
+.top-area
+ .nav-search
+ = form_tag admin_groups_path, method: :get, class: 'form-inline' do
+ = hidden_field_tag :sort, @sort
+ = text_field_tag :name, params[:name], class: "form-control"
+ = button_tag "Search", class: "btn submit btn-primary"
- .pull-right
+ .nav-controls
.dropdown.inline
%a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
%span.light
@@ -33,34 +32,10 @@
= sort_title_recently_updated
= link_to admin_groups_path(sort: sort_value_oldest_updated) do
= sort_title_oldest_updated
+ = link_to 'New Group', new_admin_group_path, class: "btn btn-new"
-%hr
-
-%ul.bordered-list
+%ul.content-list
- @groups.each do |group|
- %li
- .clearfix
- .pull-right.prepend-top-10
- = link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: "btn btn-sm"
- = link_to 'Destroy', [:admin, group], data: {confirm: "REMOVE #{group.name}? Are you sure?"}, method: :delete, class: "btn btn-sm btn-remove"
-
- %h4
- = link_to [:admin, group] do
- %span{ class: visibility_level_color(group.visibility_level) }
- = visibility_level_icon(group.visibility_level)
-
- %i.fa.fa-folder
- = group.name
-
- &rarr;
- %span.monospace
- %strong #{group.path}/
- .clearfix
- %p
- = truncate group.description, length: 150
- .clearfix
- %p.light
- #{pluralize(group.members.size, 'member')}, #{pluralize(group.projects.count, 'project')}
-
+ = render 'group', group: group
= paginate @groups, theme: "gitlab"
diff --git a/app/views/admin/labels/index.html.haml b/app/views/admin/labels/index.html.haml
index 3c57e3dc174..05d6b9ed238 100644
--- a/app/views/admin/labels/index.html.haml
+++ b/app/views/admin/labels/index.html.haml
@@ -1,8 +1,10 @@
- page_title "Labels"
-= link_to new_admin_label_path, class: "pull-right btn btn-nr btn-new" do
- New label
-%h3.page-title
- Labels
+
+%div
+ = link_to new_admin_label_path, class: "pull-right btn btn-nr btn-new" do
+ New label
+ %h3.page-title
+ Labels
%hr
.labels
@@ -13,4 +15,4 @@
- else
.light-well
.nothing-here-block There are no labels yet
-
+
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index c407972cd08..2dad64b8d0f 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -1,4 +1,4 @@
-%p.lead
+%p.lead.prepend-top-default
%span
To register a new runner you should enter the following registration token.
With this token the runner will request a unique runner token and use that for future communication.
diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml
index e3a4d64df01..aa0aff86d4d 100644
--- a/app/views/dashboard/todos/_todo.html.haml
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -1,4 +1,4 @@
-%li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo) }
+%li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo), data:{url: todo_target_path(todo)} }
.todo-item.todo-block
= image_tag avatar_icon(todo.author_email, 40), class: 'avatar s40', alt:''
@@ -10,7 +10,10 @@
(removed)
%span.todo-label
= todo_action_name(todo)
- = todo_target_link(todo)
+ - if todo.target
+ = todo_target_link(todo)
+ - else
+ (removed)
&middot; #{time_ago_with_tooltip(todo.created_at)}
diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml
index 42c2764e7e2..4d20dd5830e 100644
--- a/app/views/events/_event.html.haml
+++ b/app/views/events/_event.html.haml
@@ -1,5 +1,5 @@
- if event.visible_to_user?(current_user)
- .event-item{class: "#{event.body? ? "event-block" : "event-inline" }"}
+ .event-item{ class: event_row_class(event) }
.event-item-timestamp
#{time_ago_with_tooltip(event.created_at)}
diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml
index 8cf36c711b4..5a2a469ba62 100644
--- a/app/views/events/event/_created_project.html.haml
+++ b/app/views/events/event/_created_project.html.haml
@@ -7,21 +7,3 @@
= link_to_project event.project
- else
= event.project_name
-
-- if !event.project.private? && twitter_sharing_enabled?
- .event-body{"data-user-is" => event.author_id}
- .event-note
- .md
- %p
- Congratulations! Why not share your accomplishment with the world?
-
- %a.twitter-share-button{ |
- href: "https://twitter.com/share", |
- "data-url" => event.project.web_url, |
- "data-text" => "I just #{event.action_name} a new project on GitLab! GitLab is version control on your server.", |
- "data-size" => "medium", |
- "data-related" => "gitlab", |
- "data-hashtags" => "gitlab", |
- "data-count" => "none"}
- Tweet
- %script{src: "//platform.twitter.com/widgets.js"}
diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml
index a8e1ed77da9..4290e0bf72e 100644
--- a/app/views/groups/milestones/new.html.haml
+++ b/app/views/groups/milestones/new.html.haml
@@ -10,6 +10,14 @@
= form_for @milestone, url: group_milestones_path(@group), html: { class: 'form-horizontal milestone-form gfm-form js-quick-submit js-requires-input' } do |f|
.row
+ - if @milestone.errors.any?
+ #error_explanation
+ .alert.alert-danger
+ %ul
+ - @milestone.errors.full_messages.each do |msg|
+ %li
+ = msg
+
.col-md-6
.form-group
= f.label :title, "Title", class: "control-label"
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index 54af2c3063c..6b208c3d0bb 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -1,10 +1,33 @@
-.search
- = form_tag search_path, method: :get, class: 'navbar-form pull-left' do |f|
- = search_field_tag "search", nil, placeholder: 'Search', class: "search-input form-control", spellcheck: false, tabindex: "1"
+- if controller.controller_path =~ /^groups/ && @group.persisted?
+ - label = 'This group'
+- if controller.controller_path =~ /^projects/ && @project.persisted?
+ - label = 'This project'
+
+.search.search-form{class: "#{'has-location-badge' if label.present?}"}
+ = form_tag search_path, method: :get, class: 'navbar-form' do |f|
+ .search-input-container
+ .search-location-badge
+ - if label.present?
+ %span.location-badge
+ %i.location-text
+ = label
+ .search-input-wrap
+ .dropdown{ data: {url: search_autocomplete_path } }
+ = search_field_tag "search", nil, placeholder: 'Search', class: "search-input dropdown-menu-toggle", spellcheck: false, tabindex: "1", autocomplete: 'off', data: { toggle: 'dropdown' }
+ .dropdown-menu.dropdown-select
+ = dropdown_content do
+ %ul
+ %li
+ %a.is-focused.dropdown-menu-empty-link
+ Loading...
+ = dropdown_loading
+ %i.search-icon
+ %i.clear-icon.js-clear-input
+
= hidden_field_tag :group_id, @group.try(:id)
- - if @project && @project.persisted?
- = hidden_field_tag :project_id, @project.id
+ = hidden_field_tag :project_id, @project && @project.persisted? ? @project.id : '', id: 'search_project_id'
+ - if @project && @project.persisted?
- if current_controller?(:issues)
= hidden_field_tag :scope, 'issues'
- elsif current_controller?(:merge_requests)
@@ -21,10 +44,3 @@
= hidden_field_tag :repository_ref, @ref
= button_tag 'Go' if ENV['RAILS_ENV'] == 'test'
.search-autocomplete-opts.hide{:'data-autocomplete-path' => search_autocomplete_path, :'data-autocomplete-project-id' => @project.try(:id), :'data-autocomplete-project-ref' => @ref }
-
-:javascript
- $('.search-input').on('keyup', function(e) {
- if (e.keyCode == 27) {
- $('.search-input').blur();
- }
- });
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index 4a0069f18f8..5cef652da14 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -9,7 +9,7 @@
= icon('bell fw')
%span
Todos
- %span.count= number_with_delimiter(todos_pending_count)
+ %span.count.todos-pending-count= number_with_delimiter(todos_pending_count)
= nav_link(path: 'dashboard#activity') do
= link_to activity_dashboard_path, class: 'shortcuts-activity', title: 'Activity' do
= icon('dashboard fw')
diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml
index dc3050f02e5..d429a928464 100644
--- a/app/views/layouts/nav/_project_settings.html.haml
+++ b/app/views/layouts/nav/_project_settings.html.haml
@@ -51,8 +51,13 @@
= icon('code fw')
%span
Variables
- = nav_link path: 'triggers#index' do
+ = nav_link(controller: :triggers) do
= link_to namespace_project_triggers_path(@project.namespace, @project), title: 'Triggers' do
= icon('retweet fw')
%span
Triggers
+ = nav_link(controller: :badges) do
+ = link_to namespace_project_badges_path(@project.namespace, @project), title: 'Badges' do
+ = icon('star-half-empty fw')
+ %span
+ Badges
diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml
index ab527e8e438..a7ef31acd3d 100644
--- a/app/views/layouts/project.html.haml
+++ b/app/views/layouts/project.html.haml
@@ -5,10 +5,14 @@
- content_for :scripts_body_top do
- project = @target_project || @project
+ - if @project_wiki
+ - markdown_preview_path = namespace_project_wikis_markdown_preview_path(project.namespace, project)
+ - else
+ - markdown_preview_path = markdown_preview_namespace_project_path(project.namespace, project)
- if current_user
:javascript
window.project_uploads_path = "#{namespace_project_uploads_path project.namespace,project}";
- window.markdown_preview_path = "#{markdown_preview_namespace_project_path(project.namespace, project)}";
+ window.markdown_preview_path = "#{markdown_preview_path}";
- content_for :scripts_body do
= render "layouts/init_auto_complete" if current_user
diff --git a/app/views/profiles/two_factor_auths/new.html.haml b/app/views/profiles/two_factor_auths/new.html.haml
index 5d342ef58e5..69fc81cb45c 100644
--- a/app/views/profiles/two_factor_auths/new.html.haml
+++ b/app/views/profiles/two_factor_auths/new.html.haml
@@ -8,8 +8,6 @@
Increase your account's security by enabling two-factor authentication (2FA).
.col-lg-9
%p
- Status: #{current_user.two_factor_enabled? ? 'enabled' : 'disabled'}
- %p
Download the Google Authenticator application from App Store for iOS or Google Play for Android and scan this code.
More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}.
.row.append-bottom-10
diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml
index 1fb37ef6621..4920910fee1 100644
--- a/app/views/projects/_md_preview.html.haml
+++ b/app/views/projects/_md_preview.html.haml
@@ -1,18 +1,19 @@
.md-area
- .md-header.clearfix
+ .md-header
%ul.nav-links
%li.active
- %a.js-md-write-button(href="#md-write-holder" tabindex="-1")
+ %a.js-md-write-button{ href: "#md-write-holder" }
Write
%li
- %a.js-md-preview-button(href="#md-preview-holder" tabindex="-1")
+ %a.js-md-preview-button{ href: "#md-preview-holder" }
Preview
+ %li.pull-right
+ %button.zen-cotrol.zen-control-full.js-zen-enter{ type: 'button' }
+ Go full screen
- %div
- .md-write-holder
- = yield
- .md.md-preview-holder.hide
- .js-md-preview{class: (preview_class if defined?(preview_class))}
+ .md-write-holder
+ = yield
+ .md.md-preview-holder.js-md-preview.hide{class: (preview_class if defined?(preview_class))}
- if defined?(referenced_users) && referenced_users
%div.referenced-users.hide
diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml
index e701253d7de..bddff5cdcbc 100644
--- a/app/views/projects/_zen.html.haml
+++ b/app/views/projects/_zen.html.haml
@@ -1,12 +1,8 @@
-.zennable
- .zen-backdrop
- - classes << ' js-gfm-input js-autosize markdown-area'
- - if defined?(f) && f
- = f.text_area attr, class: classes
- - else
- = text_area_tag attr, nil, class: classes
- %a.js-zen-enter(tabindex="-1" href="#")
- = icon('expand')
- Edit in fullscreen
- %a.js-zen-leave(tabindex="-1" href="#")
- = icon('compress')
+.zen-backdrop
+ - classes << ' js-gfm-input js-autosize markdown-area'
+ - if defined?(f) && f
+ = f.text_area attr, class: classes, placeholder: "Write a comment or drag your files here..."
+ - else
+ = text_area_tag attr, nil, class: classes, placeholder: "Write a comment or drag your files here..."
+ %a.zen-cotrol.zen-control-leave.js-zen-leave{ href: "#" }
+ = icon('compress')
diff --git a/app/views/projects/badges/index.html.haml b/app/views/projects/badges/index.html.haml
new file mode 100644
index 00000000000..c22384ddf46
--- /dev/null
+++ b/app/views/projects/badges/index.html.haml
@@ -0,0 +1,24 @@
+- page_title 'Badges'
+- badges_path = namespace_project_badges_path(@project.namespace, @project)
+- header_title project_title(@project, 'Badges', badges_path)
+
+.prepend-top-10
+ .panel.panel-default
+ .panel-heading
+ %b Builds badge &middot;
+ = @build_badge.to_html
+ .pull-right
+ = render 'shared/ref_switcher', destination: 'badges'
+ .panel-body
+ .row
+ .col-md-2.text-center
+ Markdown
+ .col-md-10.code.js-syntax-highlight
+ = highlight('.md', @build_badge.to_markdown)
+ .row
+ %hr
+ .row
+ .col-md-2.text-center
+ HTML
+ .col-md-10.code.js-syntax-highlight
+ = highlight('.html', @build_badge.to_html)
diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml
index e7c85edff96..1e4c46fca2f 100644
--- a/app/views/projects/buttons/_dropdown.html.haml
+++ b/app/views/projects/buttons/_dropdown.html.haml
@@ -3,25 +3,32 @@
%a.btn.dropdown-toggle{href: '#', "data-toggle" => "dropdown"}
= icon('plus')
%ul.dropdown-menu.dropdown-menu-right.project-home-dropdown
- - if can?(current_user, :create_issue, @project)
+ - can_create_issue = can?(current_user, :create_issue, @project)
+ - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
+ - can_create_snippet = can?(current_user, :create_snippet, @project)
+
+ - if can_create_issue
%li
= link_to url_for_new_issue(@project, only_path: true) do
= icon('exclamation-circle fw')
New issue
- - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
+
- if merge_project
%li
= link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project) do
= icon('tasks fw')
New merge request
- - if can?(current_user, :create_snippet, @project)
+
+ - if can_create_snippet
%li
= link_to new_namespace_project_snippet_path(@project.namespace, @project) do
= icon('file-text-o fw')
New snippet
- - if can?(current_user, :push_code, @project)
+ - if can_create_issue || merge_project || can_create_snippet
%li.divider
+
+ - if can?(current_user, :push_code, @project)
%li
= link_to namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master') do
= icon('file fw')
@@ -35,13 +42,11 @@
= icon('tags fw')
New tag
- elsif current_user && current_user.already_forked?(@project)
- %li.divider
%li
= link_to namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master') do
= icon('file fw')
New file
- elsif can?(current_user, :fork_project, @project)
- %li.divider
%li
- continue_params = { to: namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master'),
notice: edit_in_new_fork_notice,
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index d22d1da8402..2cf9115e4dd 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -39,7 +39,7 @@
%td
= build.name
- .pull-right
+ .label-container
- if build.tags.any?
- build.tags.each do |tag|
%span.label.label-primary
diff --git a/app/views/projects/diffs/_image.html.haml b/app/views/projects/diffs/_image.html.haml
index 8367112a9cb..2731219ccad 100644
--- a/app/views/projects/diffs/_image.html.haml
+++ b/app/views/projects/diffs/_image.html.haml
@@ -1,7 +1,10 @@
- diff = diff_file.diff
- file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, diff.new_path))
-- old_commit_id = diff_refs.first.id
-- old_file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(old_commit_id, diff.old_path))
+// diff_refs will be nil for orphaned commits (e.g. first commit in repo)
+- if diff_refs
+ - old_commit_id = diff_refs.first.id
+ - old_file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(old_commit_id, diff.old_path))
+
- if diff.renamed_file || diff.new_file || diff.deleted_file
.image
%span.wrap
diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml
index b05ab869215..2ec0d20a879 100644
--- a/app/views/projects/merge_requests/widget/_heading.html.haml
+++ b/app/views/projects/merge_requests/widget/_heading.html.haml
@@ -1,15 +1,17 @@
- if @ci_commit
.mr-widget-heading
- .ci_widget{class: "ci-#{@ci_commit.status}"}
- = ci_status_icon(@ci_commit)
- %span
- Build
- = ci_status_label(@ci_commit)
- for
- = succeed "." do
- = link_to @ci_commit.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @ci_commit.sha), class: "monospace"
- %span.ci-coverage
- = link_to "View details", builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'builds'}
+ - %w[success skipped canceled failed running pending].each do |status|
+ .ci_widget{ class: "ci-#{status}", style: ("display:none" unless @ci_commit.status == status) }
+ = ci_icon_for_status(status)
+ %span
+ CI build
+ = ci_label_for_status(status)
+ for
+ - commit = @merge_request.last_commit
+ = succeed "." do
+ = link_to @ci_commit.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @ci_commit.sha), class: "monospace"
+ %span.ci-coverage
+ = link_to "View details", builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'builds'}
- elsif @merge_request.has_ci?
- # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX
@@ -43,5 +45,5 @@
:javascript
$(function() {
- merge_request_widget.getCiStatus();
+ merge_request_widget.getCIStatus(false);
});
diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml
index a489d4f9b24..92d95358937 100644
--- a/app/views/projects/merge_requests/widget/_show.html.haml
+++ b/app/views/projects/merge_requests/widget/_show.html.haml
@@ -9,12 +9,19 @@
:javascript
var merge_request_widget;
-
- merge_request_widget = new MergeRequestWidget({
- url_to_automerge_check: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
+ var opts = {
+ merge_check_url: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
check_enable: #{@merge_request.unchecked? ? "true" : "false"},
- url_to_ci_check: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
+ ci_status_url: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
+ gitlab_icon: "#{asset_path 'gitlab_logo.png'}",
+ ci_status: "",
+ ci_message: "Build {{status}} for \"{{title}}\"",
ci_enable: #{@project.ci_service ? "true" : "false"},
- current_status: "#{@merge_request.gitlab_merge_status}",
- });
+ builds_path: "#{builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}"
+ };
+ if(typeof merge_request_widget === 'undefined') {
+ merge_request_widget = new MergeRequestWidget(opts);
+ } else {
+ merge_request_widget.setOpts(opts);
+ }
diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml
index 2999befffc6..23e4f93eab5 100644
--- a/app/views/projects/notes/_edit_form.html.haml
+++ b/app/views/projects/notes/_edit_form.html.haml
@@ -1,10 +1,11 @@
.note-edit-form
- = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, html: { class: 'edit-note js-quick-submit' } do |f|
+ = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, html: { class: 'edit-note common-note-form js-quick-submit' } do |f|
= note_target_fields(note)
= render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do
- = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-task-list-field'
+ = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text js-task-list-field'
= render 'projects/notes/hints'
.note-form-actions.clearfix
= f.submit 'Save Comment', class: 'btn btn-nr btn-save btn-grouped js-comment-button'
- = link_to 'Cancel', '#', class: 'btn btn-nr btn-cancel note-edit-cancel'
+ %button.btn.btn-nr.btn-cancel.note-edit-cancel{ type: 'button' }
+ Cancel
diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml
index f675f092da1..c446ecec2c3 100644
--- a/app/views/projects/notes/_form.html.haml
+++ b/app/views/projects/notes/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new_note js-new-note-form js-quick-submit common-note-form gfm-form" }, authenticity_token: true do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form gfm-form" }, authenticity_token: true do |f|
= hidden_field_tag :view, diff_view
= hidden_field_tag :line_type
= note_target_fields(@note)
@@ -8,7 +8,7 @@
= f.hidden_field :noteable_type
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
- = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text'
+ = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text'
= render 'projects/notes/hints'
.error-alert
diff --git a/app/views/projects/notes/_hints.html.haml b/app/views/projects/notes/_hints.html.haml
index 6e7929bdab0..0b002043408 100644
--- a/app/views/projects/notes/_hints.html.haml
+++ b/app/views/projects/notes/_hints.html.haml
@@ -1,9 +1,8 @@
-.comment-hints.clearfix
- .pull-left
+.comment-toolbar.clearfix
+ .toolbar-text
+ Styling with
= link_to 'Markdown', help_page_path('markdown', 'markdown'), target: '_blank', tabindex: -1
- tip:
- = random_markdown_tip
- .pull-right
- = link_to '#', class: 'markdown-selector', tabindex: -1 do
- = icon('paperclip')
- Attach a file
+ is supported
+ %button.toolbar-button.markdown-selector{ type: 'button', tabindex: '-1' }
+ = icon('file-image-o', class: 'toolbar-button-icon')
+ Attach a file
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index 2cf32e6093d..a681d6dece4 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -5,28 +5,21 @@
= image_tag avatar_icon(note.author), alt: '', class: 'avatar s40'
.timeline-content
.note-header
+ = link_to_member(note.project, note.author, avatar: false)
+ .inline.note-headline-light
+ = "#{note.author.to_reference} commented"
+ %a{ href: "##{dom_id(note)}" }
+ = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
- if note_editable?(note)
.note-actions
- = link_to '#', title: 'Edit comment', class: 'js-note-edit' do
+ - access = note.project.team.human_max_access(note.author.id)
+ - if access
+ %span.note-role
+ = access
+ = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
= icon('pencil-square-o')
-
- = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'js-note-delete danger' do
+ = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete' do
= icon('trash-o')
-
- - unless note.system
- - access = note.project.team.human_max_access(note.author.id)
- - if access
- %span.note-role.label
- = access
-
- = link_to_member(note.project, note.author, avatar: false)
-
- %span.author-username
- = '@' + note.author.username
-
- %span.note-last-update
- %a{name: dom_id(note), href: "##{dom_id(note)}", title: 'Link here'}
- = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note_created_ago')
.note-body{class: note_editable?(note) ? 'js-task-list-container' : ''}
.note-text
= preserve do
diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml
index 910eb6cf66e..cc42aab5c52 100644
--- a/app/views/projects/notes/_notes_with_form.html.haml
+++ b/app/views/projects/notes/_notes_with_form.html.haml
@@ -1,20 +1,21 @@
%ul#notes-list.notes.main-notes-list.timeline
= render "projects/notes/notes"
-.js-notes-busy
-
-.js-main-target-form
-- if can? current_user, :create_note, @project
- = render "projects/notes/form", view: diff_view
-- else
- .disabled-comment-area
- .disabled-profile
- .disabled-comment
- %span
- Please
- = link_to "register",new_user_session_path
- or
- = link_to "login",new_user_session_path
- to post a comment
+%ul.notes.timeline
+ %li.timeline-entry
+ - if can? current_user, :create_note, @project
+ .timeline-icon.hidden-xs.hidden-sm
+ %a.author_link{ href: user_path(current_user) }
+ = image_tag avatar_icon(current_user), alt: current_user.to_reference, class: 'avatar s40'
+ .timeline-content.timeline-content-form
+ = render "projects/notes/form", view: diff_view
+ - else
+ .disabled-comment.text-center
+ .disabled-comment-text.inline
+ Please
+ = link_to "register",new_user_session_path
+ or
+ = link_to "login",new_user_session_path
+ to post a comment
:javascript
var notes = new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}")
diff --git a/app/views/projects/notes/discussions/_active.html.haml b/app/views/projects/notes/discussions/_active.html.haml
index 4f15a99d061..cd8a5f0bd02 100644
--- a/app/views/projects/notes/discussions/_active.html.haml
+++ b/app/views/projects/notes/discussions/_active.html.haml
@@ -1,22 +1,20 @@
- note = discussion_notes.first
.discussion.js-toggle-container{ class: note.discussion_id }
.discussion-header
+ = link_to_member(@project, note.author, avatar: false)
+ .inline.discussion-headline-light
+ = "#{note.author.to_reference} started a discussion"
+ = link_to diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) do
+ on the diff
.discussion-actions
- = link_to "#", class: "js-toggle-button" do
+ = link_to "#", class: "discussion-action-button discussion-toggle-button js-toggle-button" do
%i.fa.fa-chevron-up
Show/hide discussion
- %div
- = link_to_member(@project, note.author, avatar: false)
- started a discussion
- = link_to diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) do
- %strong on the diff
.last-update.hide.js-toggle-content
- last_note = discussion_notes.last
last updated by
= link_to_member(@project, last_note.author, avatar: false)
-
- %span.discussion-last-update
- #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')}
+ #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')}
.discussion-body.js-toggle-content
= render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note
diff --git a/app/views/projects/notes/discussions/_commit.html.haml b/app/views/projects/notes/discussions/_commit.html.haml
index f67ec8db942..46f2ba4bbcf 100644
--- a/app/views/projects/notes/discussions/_commit.html.haml
+++ b/app/views/projects/notes/discussions/_commit.html.haml
@@ -3,21 +3,20 @@
- commit_description = commit ? 'commit' : 'a deleted commit'
.discussion.js-toggle-container{ class: note.discussion_id }
.discussion-header
+ = link_to_member(@project, note.author, avatar: false)
+ .inline.discussion-headline-light
+ = "#{note.author.to_reference} started a discussion on #{commit_description}"
+ - if commit
+ = link_to(commit.short_id, namespace_project_commit_path(note.project.namespace, note.project, note.noteable), class: 'monospace')
.discussion-actions
- = link_to "#", class: "js-toggle-button" do
+ = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do
%i.fa.fa-chevron-up
Show/hide discussion
- %div
- = link_to_member(@project, note.author, avatar: false)
- %p started a discussion on #{commit_description}
- - if commit
- = link_to(commit.short_id, namespace_project_commit_path(note.project.namespace, note.project, note.noteable), class: 'monospace')
.last-update.hide.js-toggle-content
- last_note = discussion_notes.last
last updated by
= link_to_member(@project, last_note.author, avatar: false)
- %span.discussion-last-update
- #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')}
+ #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')}
.discussion-body.js-toggle-content
- if note.for_diff_line?
= render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note
diff --git a/app/views/projects/notes/discussions/_outdated.html.haml b/app/views/projects/notes/discussions/_outdated.html.haml
index 218b0da3977..f8e000b424f 100644
--- a/app/views/projects/notes/discussions/_outdated.html.haml
+++ b/app/views/projects/notes/discussions/_outdated.html.haml
@@ -1,19 +1,18 @@
- note = discussion_notes.first
.discussion.js-toggle-container{ class: note.discussion_id }
.discussion-header
+ = link_to_member(@project, note.author, avatar: false)
+ .inline.discussion-headline-light
+ = "#{note.author.to_reference} started a discussion"
+ on the outdated diff
.discussion-actions
- = link_to "#", class: "js-toggle-button" do
+ = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do
%i.fa.fa-chevron-down
Show/hide discussion
- %div
- = link_to_member(@project, note.author, avatar: false)
- started a discussion on the
- %strong outdated diff
- %div
+ .last-update.hide.js-toggle-content
- last_note = discussion_notes.last
last updated by
= link_to_member(@project, last_note.author, avatar: false)
- %span.discussion-last-update
- #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')}
+ #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')}
.discussion-body.js-toggle-content.hide
= render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note
diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml
index 5fcba2b7e93..9544e3d3e17 100644
--- a/app/views/search/results/_note.html.haml
+++ b/app/views/search/results/_note.html.haml
@@ -1,24 +1,20 @@
- project = note.project
+- note_url = Gitlab::UrlBuilder.new(:note).build(note.id)
+- noteable_identifier = note.noteable.try(:iid) || note.noteable.id
.search-result-row
%h5.note-search-caption.str-truncated
%i.fa.fa-comment
= link_to_member(project, note.author, avatar: false)
commented on
+ = link_to project.name_with_namespace, project
+ &middot;
- if note.for_commit?
- = link_to project do
- = project.name_with_namespace
- &middot;
- = link_to namespace_project_commit_path(project.namespace, project, note.commit_id, anchor: dom_id(note)) do
- Commit #{truncate_sha(note.commit_id)}
+ = link_to "Commit #{truncate_sha(note.commit_id)}", note_url
- else
- = link_to project do
- = project.name_with_namespace
- &middot;
- %span #{note.noteable_type.titleize} ##{note.noteable.iid}
+ %span #{note.noteable_type.titleize} ##{noteable_identifier}
&middot;
- = link_to [project.namespace.becomes(Namespace), project, note.noteable, anchor: dom_id(note)] do
- = note.noteable.title
+ = link_to note.noteable.title, note_url
.note-search-result
.term
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index c99da92be9f..921eaefd79a 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -7,13 +7,13 @@
class: "check_all_issues left"
.issues-other-filters
.filter-item.inline
- - if params[:author_id]
+ - if params[:author_id].present?
= hidden_field_tag(:author_id, params[:author_id])
= dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit",
placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: "author_id", default_label: "Author" } })
.filter-item.inline
- - if params[:assignee_id]
+ - if params[:assignee_id].present?
= hidden_field_tag(:assignee_id, params[:assignee_id])
= dropdown_tag(user_dropdown_label(params[:assignee_id], "Assignee"), options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: (@project.id if @project), selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } })
diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml
index 006a34a11e3..fd5e58c1f1f 100644
--- a/app/views/shared/issuable/_label_dropdown.html.haml
+++ b/app/views/shared/issuable/_label_dropdown.html.haml
@@ -1,4 +1,4 @@
-- if params[:label_name]
+- if params[:label_name].present?
= hidden_field_tag(:label_name, params[:label_name])
.dropdown
%button.dropdown-menu-toggle.js-label-select.js-filter-submit{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}}
diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml
index e52d2e39e6b..2fcf40ece99 100644
--- a/app/views/shared/issuable/_milestone_dropdown.html.haml
+++ b/app/views/shared/issuable/_milestone_dropdown.html.haml
@@ -1,4 +1,4 @@
-- if params[:milestone_title]
+- if params[:milestone_title].present?
= hidden_field_tag(:milestone_title, params[:milestone_title])
= dropdown_tag(milestone_dropdown_label(params[:milestone_title]), options: { title: "Filter by milestone", toggle_class: 'js-milestone-select js-filter-submit', filter: true, dropdown_class: "dropdown-menu-selectable",
placeholder: "Search milestones", footer_content: @project.present?, data: { show_no: true, show_any: true, show_upcoming: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: @project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 451c64da2c4..94affa4b59a 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -1,5 +1,6 @@
%aside.right-sidebar{ class: sidebar_gutter_collapsed_class }
.issuable-sidebar
+ - can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
.block.issuable-sidebar-header
%span.issuable-count.hide-collapsed.pull-left
= issuable.iid
@@ -29,7 +30,7 @@
.title.hide-collapsed
Assignee
= icon('spinner spin', class: 'block-loading')
- - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
+ - if can_edit_issuable
= link_to 'Edit', '#', class: 'edit-link pull-right'
.value.bold.hide-collapsed
- if issuable.assignee
@@ -41,9 +42,10 @@
= issuable.assignee.to_reference
- else
%span.assign-yourself
- No assignee -
- %a.js-assign-yourself{ href: '#' }
- assign yourself
+ No assignee
+ - if can_edit_issuable
+ %a.js-assign-yourself{ href: '#' }
+ \- assign yourself
.selectbox.hide-collapsed
= f.hidden_field 'assignee_id', value: issuable.assignee_id, id: 'issue_assignee_id'
@@ -60,7 +62,7 @@
.title.hide-collapsed
Milestone
= icon('spinner spin', class: 'block-loading')
- - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
+ - if can_edit_issuable
= link_to 'Edit', '#', class: 'edit-link pull-right'
.value.bold.hide-collapsed
- if issuable.milestone
@@ -82,7 +84,7 @@
.title.hide-collapsed
Labels
= icon('spinner spin', class: 'block-loading')
- - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
+ - if can_edit_issuable
= link_to 'Edit', '#', class: 'edit-link pull-right'
.value.bold.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels.any?) }
- if issuable.labels.any?
@@ -150,3 +152,4 @@
new LabelsSelect();
new IssuableContext('#{current_user.to_json(only: [:username, :id, :name])}');
new Subscription('.subscription')
+ new Sidebar(); \ No newline at end of file
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index bca816f22cb..0c4b6a5618b 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -87,7 +87,7 @@
%div{ class: container_class }
.tab-content
#activity.tab-pane
- .gray-content-block.white.second-block
+ .gray-content-block.calender-block.white.second-block.hidden-xs
%div{ class: container_class }
.user-calendar{data: {href: user_calendar_path}}
%h4.center.light
diff --git a/app/views/votes/_votes_block.html.haml b/app/views/votes/_votes_block.html.haml
index 02647229776..8ffcdc4a327 100644
--- a/app/views/votes/_votes_block.html.haml
+++ b/app/views/votes/_votes_block.html.haml
@@ -15,12 +15,14 @@
- if current_user
:javascript
+ var get_emojis_url = "#{emojis_path}";
var post_emoji_url = "#{award_toggle_namespace_project_notes_path(@project.namespace, @project)}";
var noteable_type = "#{votable.class.name.underscore}";
var noteable_id = "#{votable.id}";
var aliases = #{AwardEmoji.aliases.to_json};
window.awards_handler = new AwardsHandler(
+ get_emojis_url,
post_emoji_url,
noteable_type,
noteable_id,
diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb
index 55cb6af232e..ccefd0f71a0 100644
--- a/app/workers/project_cache_worker.rb
+++ b/app/workers/project_cache_worker.rb
@@ -5,6 +5,9 @@ class ProjectCacheWorker
def perform(project_id)
project = Project.find(project_id)
+
+ return unless project.repository.exists?
+
project.update_repository_size
project.update_commit_count
diff --git a/app/workers/project_destroy_worker.rb b/app/workers/project_destroy_worker.rb
index d06e4480292..b51c6a266c9 100644
--- a/app/workers/project_destroy_worker.rb
+++ b/app/workers/project_destroy_worker.rb
@@ -5,7 +5,7 @@ class ProjectDestroyWorker
def perform(project_id, user_id, params)
begin
- project = Project.find(project_id)
+ project = Project.unscoped.find(project_id)
rescue ActiveRecord::RecordNotFound
return
end
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 500b745f55e..35c7c425a5a 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -80,7 +80,7 @@ production: &base
# This happens when the commit is pushed or merged into the default branch of a project.
# When not specified the default issue_closing_pattern as specified below will be used.
# Tip: you can test your closing pattern at http://rubular.com.
- # issue_closing_pattern: '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)'
+ # issue_closing_pattern: '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing))(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)'
## Default project features settings
default_projects_features:
@@ -106,7 +106,7 @@ production: &base
enabled: false
# The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
- # The `%{key}` placeholder is added after the user part, after a `+` character, before the `@`.
+ # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
address: "gitlab-incoming+%{key}@gmail.com"
# Email account username
@@ -345,6 +345,8 @@ production: &base
#
# - { name: 'saml',
# label: 'Our SAML Provider',
+ # groups_attribute: 'Groups',
+ # external_groups: ['Contractors', 'Freelancers'],
# args: {
# assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
# idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
@@ -352,6 +354,7 @@ production: &base
# issuer: 'https://gitlab.example.com',
# name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
# } }
+ #
# - { name: 'crowd',
# args: {
# crowd_server_url: 'CROWD SERVER URL',
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 626268d7648..72c4d8d61ce 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -174,10 +174,9 @@ end
Settings.gitlab['time_zone'] ||= nil
Settings.gitlab['signup_enabled'] ||= true if Settings.gitlab['signup_enabled'].nil?
Settings.gitlab['signin_enabled'] ||= true if Settings.gitlab['signin_enabled'].nil?
-Settings.gitlab['twitter_sharing_enabled'] ||= true if Settings.gitlab['twitter_sharing_enabled'].nil?
Settings.gitlab['restricted_visibility_levels'] = Settings.send(:verify_constant_array, Gitlab::VisibilityLevel, Settings.gitlab['restricted_visibility_levels'], [])
Settings.gitlab['username_changing_enabled'] = true if Settings.gitlab['username_changing_enabled'].nil?
-Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)' if Settings.gitlab['issue_closing_pattern'].nil?
+Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing))(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)' if Settings.gitlab['issue_closing_pattern'].nil?
Settings.gitlab['default_projects_features'] ||= {}
Settings.gitlab['webhook_timeout'] ||= 10
Settings.gitlab['max_attachment_size'] ||= 10
diff --git a/config/initializers/premailer.rb b/config/initializers/premailer.rb
index a44316bc3a4..b9176688bc4 100644
--- a/config/initializers/premailer.rb
+++ b/config/initializers/premailer.rb
@@ -3,5 +3,6 @@ Premailer::Rails.config.merge!(
generate_text_part: false,
preserve_styles: true,
remove_comments: true,
- remove_ids: true
+ remove_ids: true,
+ remove_scripts: false
)
diff --git a/config/mail_room.yml b/config/mail_room.yml
index aed55f74eab..60257329f3e 100644
--- a/config/mail_room.yml
+++ b/config/mail_room.yml
@@ -17,7 +17,7 @@ if File.exists?(config_file)
config['start_tls'] = false if config['start_tls'].nil?
config['mailbox'] = "inbox" if config['mailbox'].nil?
- if config['enabled'] && config['address'] && config['address'].include?('%{key}')
+ if config['enabled'] && config['address']
redis_url = Gitlab::RedisConfig.new(rails_env).url
%>
-
diff --git a/config/routes.rb b/config/routes.rb
index 7f03fbf6af9..fade07c0500 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -576,6 +576,7 @@ Rails.application.routes.draw do
# Order matters to give priority to these matches
get '/wikis/git_access', to: 'wikis#git_access'
get '/wikis/pages', to: 'wikis#pages', as: 'wiki_pages'
+ post '/wikis/markdown_preview', to:'wikis#markdown_preview'
post '/wikis', to: 'wikis#create'
get '/wikis/*id/history', to: 'wikis#history', as: 'wiki_history', constraints: WIKI_SLUG_ID
@@ -751,10 +752,11 @@ Rails.application.routes.draw do
end
resources :runner_projects, only: [:create, :destroy]
- resources :badges, only: [], path: 'badges/*ref',
- constraints: { ref: Gitlab::Regex.git_reference_regex } do
+ resources :badges, only: [:index] do
collection do
- get :build, constraints: { format: /svg/ }
+ scope '*ref', constraints: { ref: Gitlab::Regex.git_reference_regex } do
+ get :build, constraints: { format: /svg/ }
+ end
end
end
end
diff --git a/db/migrate/20160324020319_remove_todos_for_deleted_issues.rb b/db/migrate/20160324020319_remove_todos_for_deleted_issues.rb
new file mode 100644
index 00000000000..1fff9759d1e
--- /dev/null
+++ b/db/migrate/20160324020319_remove_todos_for_deleted_issues.rb
@@ -0,0 +1,17 @@
+class RemoveTodosForDeletedIssues < ActiveRecord::Migration
+ def up
+ execute <<-SQL
+ DELETE FROM todos
+ WHERE todos.target_type = 'Issue'
+ AND NOT EXISTS (
+ SELECT *
+ FROM issues
+ WHERE issues.id = todos.target_id
+ AND issues.deleted_at IS NULL
+ )
+ SQL
+ end
+
+ def down
+ end
+end
diff --git a/db/migrate/20160329144452_add_index_on_pending_delete_projects.rb b/db/migrate/20160329144452_add_index_on_pending_delete_projects.rb
new file mode 100644
index 00000000000..275554e736e
--- /dev/null
+++ b/db/migrate/20160329144452_add_index_on_pending_delete_projects.rb
@@ -0,0 +1,6 @@
+class AddIndexOnPendingDeleteProjects < ActiveRecord::Migration
+ def change
+ add_index :projects, :pending_delete
+ end
+end
+
diff --git a/db/migrate/20160331133914_remove_todos_for_deleted_merge_requests.rb b/db/migrate/20160331133914_remove_todos_for_deleted_merge_requests.rb
new file mode 100644
index 00000000000..54cea964ff2
--- /dev/null
+++ b/db/migrate/20160331133914_remove_todos_for_deleted_merge_requests.rb
@@ -0,0 +1,17 @@
+class RemoveTodosForDeletedMergeRequests < ActiveRecord::Migration
+ def up
+ execute <<-SQL
+ DELETE FROM todos
+ WHERE todos.target_type = 'MergeRequest'
+ AND NOT EXISTS (
+ SELECT *
+ FROM merge_requests
+ WHERE merge_requests.id = todos.target_id
+ AND merge_requests.deleted_at IS NULL
+ )
+ SQL
+ end
+
+ def down
+ end
+end
diff --git a/db/migrate/20160331223143_remove_twitter_sharing_enabled_from_application_settings.rb b/db/migrate/20160331223143_remove_twitter_sharing_enabled_from_application_settings.rb
new file mode 100644
index 00000000000..0d736e323b6
--- /dev/null
+++ b/db/migrate/20160331223143_remove_twitter_sharing_enabled_from_application_settings.rb
@@ -0,0 +1,5 @@
+class RemoveTwitterSharingEnabledFromApplicationSettings < ActiveRecord::Migration
+ def change
+ remove_column :application_settings, :twitter_sharing_enabled, :boolean
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index e946ecd3f2b..4c7673511fb 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20160328121138) do
+ActiveRecord::Schema.define(version: 20160331133914) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -417,9 +417,9 @@ ActiveRecord::Schema.define(version: 20160328121138) do
t.string "state"
t.integer "iid"
t.integer "updated_by_id"
+ t.integer "moved_to_id"
t.boolean "confidential", default: false
t.datetime "deleted_at"
- t.integer "moved_to_id"
end
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
@@ -757,6 +757,7 @@ ActiveRecord::Schema.define(version: 20160328121138) do
add_index "projects", ["namespace_id"], name: "index_projects_on_namespace_id", using: :btree
add_index "projects", ["path"], name: "index_projects_on_path", using: :btree
add_index "projects", ["path"], name: "index_projects_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"}
+ add_index "projects", ["pending_delete"], name: "index_projects_on_pending_delete", using: :btree
add_index "projects", ["runners_token"], name: "index_projects_on_runners_token", using: :btree
add_index "projects", ["star_count"], name: "index_projects_on_star_count", using: :btree
add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree
diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md
index 237700bbcd9..10096779844 100644
--- a/doc/administration/auth/ldap.md
+++ b/doc/administration/auth/ldap.md
@@ -261,13 +261,13 @@ tree and traverse it.
- Run the following check command to make sure that the LDAP settings are
correct and GitLab can see your users:
- ```bash
- # For Omnibus installations
- sudo gitlab-rake gitlab:ldap:check
+ ```bash
+ # For Omnibus installations
+ sudo gitlab-rake gitlab:ldap:check
- # For installations from source
- sudo -u git -H bundle exec rake gitlab:ldap:check RAILS_ENV=production
- ```
+ # For installations from source
+ sudo -u git -H bundle exec rake gitlab:ldap:check RAILS_ENV=production
+ ```
### Connection Refused
diff --git a/doc/api/issues.md b/doc/api/issues.md
index cc6355d34ef..1c635a6cdcf 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -76,8 +76,9 @@ Example response:
"title" : "Consequatur vero maxime deserunt laboriosam est voluptas dolorem.",
"created_at" : "2016-01-04T15:31:51.081Z",
"iid" : 6,
- "labels" : []
- },
+ "labels" : [],
+ "subscribed" : false
+ }
]
```
@@ -152,7 +153,8 @@ Example response:
"id" : 41,
"title" : "Ut commodi ullam eos dolores perferendis nihil sunt.",
"updated_at" : "2016-01-04T15:31:46.176Z",
- "created_at" : "2016-01-04T15:31:46.176Z"
+ "created_at" : "2016-01-04T15:31:46.176Z",
+ "subscribed" : false
}
]
```
@@ -213,7 +215,8 @@ Example response:
"id" : 41,
"title" : "Ut commodi ullam eos dolores perferendis nihil sunt.",
"updated_at" : "2016-01-04T15:31:46.176Z",
- "created_at" : "2016-01-04T15:31:46.176Z"
+ "created_at" : "2016-01-04T15:31:46.176Z",
+ "subscribed": false
}
```
@@ -267,7 +270,8 @@ Example response:
},
"description" : null,
"updated_at" : "2016-01-07T12:44:33.959Z",
- "milestone" : null
+ "milestone" : null,
+ "subscribed" : true
}
```
@@ -323,7 +327,8 @@ Example response:
],
"id" : 85,
"assignee" : null,
- "milestone" : null
+ "milestone" : null,
+ "subscribed" : true
}
```
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index b20a6300b7a..20db73ea6c0 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -66,7 +66,8 @@ Parameters:
"due_date": null
},
"merge_when_build_succeeds": true,
- "merge_status": "can_be_merged"
+ "merge_status": "can_be_merged",
+ "subscribed" : false
}
]
```
@@ -128,7 +129,8 @@ Parameters:
"due_date": null
},
"merge_when_build_succeeds": true,
- "merge_status": "can_be_merged"
+ "merge_status": "can_be_merged",
+ "subscribed" : true
}
```
@@ -227,6 +229,7 @@ Parameters:
},
"merge_when_build_succeeds": true,
"merge_status": "can_be_merged",
+ "subscribed" : true,
"changes": [
{
"old_path": "VERSION",
@@ -304,7 +307,8 @@ Parameters:
"due_date": null
},
"merge_when_build_succeeds": true,
- "merge_status": "can_be_merged"
+ "merge_status": "can_be_merged",
+ "subscribed" : true
}
```
@@ -373,7 +377,8 @@ Parameters:
"due_date": null
},
"merge_when_build_succeeds": true,
- "merge_status": "can_be_merged"
+ "merge_status": "can_be_merged",
+ "subscribed" : true
}
```
@@ -466,7 +471,8 @@ Parameters:
"due_date": null
},
"merge_when_build_succeeds": true,
- "merge_status": "can_be_merged"
+ "merge_status": "can_be_merged",
+ "subscribed" : true
}
```
@@ -530,7 +536,8 @@ Parameters:
"due_date": null
},
"merge_when_build_succeeds": true,
- "merge_status": "can_be_merged"
+ "merge_status": "can_be_merged",
+ "subscribed" : true
}
```
diff --git a/doc/api/milestones.md b/doc/api/milestones.md
index a6828728264..e4202025f80 100644
--- a/doc/api/milestones.md
+++ b/doc/api/milestones.md
@@ -7,8 +7,24 @@ Returns a list of project milestones.
```
GET /projects/:id/milestones
GET /projects/:id/milestones?iid=42
+GET /projects/:id/milestones?state=active
+GET /projects/:id/milestones?state=closed
```
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `iid` | integer | optional | Return only the milestone having the given `iid` |
+| `state` | string | optional | Return only `active` or `closed` milestones` |
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/milestones
+```
+
+Example Response:
+
```json
[
{
@@ -25,10 +41,6 @@ GET /projects/:id/milestones?iid=42
]
```
-Parameters:
-
-- `id` (required) - The ID of a project
-- `iid` (optional) - Return the milestone having the given `iid`
## Get single milestone
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 3703f4b327a..3a909a2bc87 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -491,6 +491,172 @@ Parameters:
- `id` (required) - The ID of the project to be forked
+### Archive a project
+
+Archives the project if the user is either admin or the project owner of this project. This action is
+idempotent, thus archiving an already archived project will not change the project.
+
+Status code 201 with the project as body is given when successful, in case the user doesn't
+have the proper access rights, code 403 is returned. Status 404 is returned if the project
+doesn't exist, or is hidden to the user.
+
+```
+POST /projects/:id/archive
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of the project |
+
+```bash
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/archive"
+```
+
+Example response:
+
+```json
+{
+ "id": 3,
+ "description": null,
+ "default_branch": "master",
+ "public": false,
+ "visibility_level": 0,
+ "ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git",
+ "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
+ "web_url": "http://example.com/diaspora/diaspora-project-site",
+ "tag_list": [
+ "example",
+ "disapora project"
+ ],
+ "owner": {
+ "id": 3,
+ "name": "Diaspora",
+ "created_at": "2013-09-30T13: 46: 02Z"
+ },
+ "name": "Diaspora Project Site",
+ "name_with_namespace": "Diaspora / Diaspora Project Site",
+ "path": "diaspora-project-site",
+ "path_with_namespace": "diaspora/diaspora-project-site",
+ "issues_enabled": true,
+ "open_issues_count": 1,
+ "merge_requests_enabled": true,
+ "builds_enabled": true,
+ "wiki_enabled": true,
+ "snippets_enabled": false,
+ "created_at": "2013-09-30T13: 46: 02Z",
+ "last_activity_at": "2013-09-30T13: 46: 02Z",
+ "creator_id": 3,
+ "namespace": {
+ "created_at": "2013-09-30T13: 46: 02Z",
+ "description": "",
+ "id": 3,
+ "name": "Diaspora",
+ "owner_id": 1,
+ "path": "diaspora",
+ "updated_at": "2013-09-30T13: 46: 02Z"
+ },
+ "permissions": {
+ "project_access": {
+ "access_level": 10,
+ "notification_level": 3
+ },
+ "group_access": {
+ "access_level": 50,
+ "notification_level": 3
+ }
+ },
+ "archived": true,
+ "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png",
+ "shared_runners_enabled": true,
+ "forks_count": 0,
+ "star_count": 0,
+ "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b"
+}
+```
+
+### Unarchive a project
+
+Unarchives the project if the user is either admin or the project owner of this project. This action is
+idempotent, thus unarchiving an non-archived project will not change the project.
+
+Status code 201 with the project as body is given when successful, in case the user doesn't
+have the proper access rights, code 403 is returned. Status 404 is returned if the project
+doesn't exist, or is hidden to the user.
+
+```
+POST /projects/:id/archive
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of the project |
+
+```bash
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/unarchive"
+```
+
+Example response:
+
+```json
+{
+ "id": 3,
+ "description": null,
+ "default_branch": "master",
+ "public": false,
+ "visibility_level": 0,
+ "ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git",
+ "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
+ "web_url": "http://example.com/diaspora/diaspora-project-site",
+ "tag_list": [
+ "example",
+ "disapora project"
+ ],
+ "owner": {
+ "id": 3,
+ "name": "Diaspora",
+ "created_at": "2013-09-30T13: 46: 02Z"
+ },
+ "name": "Diaspora Project Site",
+ "name_with_namespace": "Diaspora / Diaspora Project Site",
+ "path": "diaspora-project-site",
+ "path_with_namespace": "diaspora/diaspora-project-site",
+ "issues_enabled": true,
+ "open_issues_count": 1,
+ "merge_requests_enabled": true,
+ "builds_enabled": true,
+ "wiki_enabled": true,
+ "snippets_enabled": false,
+ "created_at": "2013-09-30T13: 46: 02Z",
+ "last_activity_at": "2013-09-30T13: 46: 02Z",
+ "creator_id": 3,
+ "namespace": {
+ "created_at": "2013-09-30T13: 46: 02Z",
+ "description": "",
+ "id": 3,
+ "name": "Diaspora",
+ "owner_id": 1,
+ "path": "diaspora",
+ "updated_at": "2013-09-30T13: 46: 02Z"
+ },
+ "permissions": {
+ "project_access": {
+ "access_level": 10,
+ "notification_level": 3
+ },
+ "group_access": {
+ "access_level": 50,
+ "notification_level": 3
+ }
+ },
+ "archived": false,
+ "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png",
+ "shared_runners_enabled": true,
+ "forks_count": 0,
+ "star_count": 0,
+ "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b"
+}
+```
+
### Remove project
Removes a project including all associated resources (issues, merge requests etc.)
diff --git a/doc/api/settings.md b/doc/api/settings.md
index 001de76c7af..1e745115dc8 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -26,7 +26,6 @@ Example response:
"default_branch_protection" : 2,
"restricted_visibility_levels" : [],
"signin_enabled" : true,
- "twitter_sharing_enabled" : true,
"after_sign_out_path" : null,
"max_attachment_size" : 10,
"user_oauth_applications" : true,
@@ -57,7 +56,6 @@ PUT /application/settings
| `sign_in_text` | string | no | Text on login page |
| `home_page_url` | string | no | Redirect to this URL when not logged in |
| `default_branch_protection` | integer | no | Determine if developers can push to master. Can take `0` _(not protected, both developers and masters can push new commits, force push or delete the branch)_, `1` _(partially protected, developers can push new commits, but cannot force push or delete the branch, masters can do anything)_ or `2` _(fully protected, developers cannot push new commits, force push or delete the branch, masters can do anything)_ as a parameter. Default is `1`. |
-| `twitter_sharing_enabled` | boolean | no | Allow users to share project creation on Twitter |
| `restricted_visibility_levels` | array of integers | no | Selected levels cannot be used by non-admin users for projects or snippets. Can take `0` _(Private)_, `1` _(Internal)_ and `2` _(Public)_ as a parameter. Default is null which means there is no restriction. |
| `max_attachment_size` | integer | no | Limit attachment size in MB |
| `session_expire_delay` | integer | no | Session duration in minutes. GitLab restart is required to apply changes |
@@ -85,7 +83,6 @@ Example response:
"updated_at": "2015-06-30T13:22:42.210Z",
"home_page_url": "",
"default_branch_protection": 2,
- "twitter_sharing_enabled": true,
"restricted_visibility_levels": [],
"max_attachment_size": 10,
"session_expire_delay": 10080,
diff --git a/doc/api/users.md b/doc/api/users.md
index 383e7c76ab0..7d2b4897cff 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -69,6 +69,7 @@ GET /users
"state": "blocked",
"created_at": "2012-05-23T08:01:01Z",
"bio": null,
+ "location": null,
"skype": "",
"linkedin": "",
"twitter": "",
@@ -126,6 +127,7 @@ Parameters:
"created_at": "2012-05-23T08:00:58Z",
"is_admin": false,
"bio": null,
+ "location": null,
"skype": "",
"linkedin": "",
"twitter": "",
@@ -154,6 +156,7 @@ Parameters:
"confirmed_at": "2012-05-23T08:00:58Z",
"last_sign_in_at": "2015-03-23T08:00:58Z",
"bio": null,
+ "location": null,
"skype": "",
"linkedin": "",
"twitter": "",
@@ -191,6 +194,7 @@ Parameters:
- `extern_uid` (optional) - External UID
- `provider` (optional) - External provider name
- `bio` (optional) - User's biography
+- `location` (optional) - User's location
- `admin` (optional) - User is admin - true or false (default)
- `can_create_group` (optional) - User can create groups - true or false
- `confirm` (optional) - Require confirmation - true (default) or false
@@ -218,6 +222,7 @@ Parameters:
- `extern_uid` - External UID
- `provider` - External provider name
- `bio` - User's biography
+- `location` (optional) - User's location
- `admin` (optional) - User is admin - true or false (default)
- `can_create_group` (optional) - User can create groups - true or false
- `external` (optional) - Flags the user as external - true or false(default)
@@ -260,6 +265,7 @@ GET /user
"state": "active",
"created_at": "2012-05-23T08:00:58Z",
"bio": null,
+ "location": null,
"skype": "",
"linkedin": "",
"twitter": "",
diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md
index 210f9c3e849..d790015aca1 100644
--- a/doc/ci/ssh_keys/README.md
+++ b/doc/ci/ssh_keys/README.md
@@ -57,7 +57,7 @@ before_script:
# WARNING: Use this only with the Docker executor, if you use it with shell
# you will overwrite your user's SSH config.
- mkdir -p ~/.ssh
- - '[[ -f /.dockerinit ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config`
+ - '[[ -f /.dockerinit ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
```
As a final step, add the _public_ key from the one you created earlier to the
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 4316f3c1f64..7da9b31e30d 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -38,7 +38,7 @@ services:
- postgres
before_script:
- - bundle_install
+ - bundle install
stages:
- build
diff --git a/doc/development/README.md b/doc/development/README.md
index 1b281809afc..2f4e7845ccc 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -4,9 +4,11 @@
- [CI setup](ci_setup.md) for testing GitLab
- [Gotchas](gotchas.md) to avoid
- [How to dump production data to staging](db_dump.md)
+- [Instrumentation](instrumentation.md)
- [Migration Style Guide](migration_style_guide.md) for creating safe migrations
- [Rake tasks](rake_tasks.md) for development
- [Shell commands](shell_commands.md) in the GitLab codebase
- [Sidekiq debugging](sidekiq_debugging.md)
- [SQL guidelines](sql.md) for SQL guidelines
+- [Testing standards and style guidelines](testing.md)
- [UI guide](ui_guide.md) for building GitLab with existing css styles and elements
diff --git a/doc/development/instrumentation.md b/doc/development/instrumentation.md
new file mode 100644
index 00000000000..c0192bd6709
--- /dev/null
+++ b/doc/development/instrumentation.md
@@ -0,0 +1,37 @@
+# Instrumenting Ruby Code
+
+GitLab Performance Monitoring allows instrumenting of custom blocks of Ruby
+code. This can be used to measure the time spent in a specific part of a larger
+chunk of code. The resulting data is written to a separate series.
+
+To start measuring a block of Ruby code you should use
+`Gitlab::Metrics.measure` and give it a name for the series to store the data
+in:
+
+```ruby
+Gitlab::Metrics.measure(:user_logins) do
+ ...
+end
+```
+
+The first argument of this method is the series name and should be plural. This
+name will be prefixed with `rails_` or `sidekiq_` depending on whether the code
+was run in the Rails application or one of the Sidekiq workers. In the
+above example the final series names would be as follows:
+
+- rails_user_logins
+- sidekiq_user_logins
+
+Series names should be plural as this keeps the naming style in line with the
+other series names.
+
+By default metrics measured using a block contain a single value, "duration",
+which contains the number of milliseconds it took to execute the block. Custom
+values can be added by passing a Hash as the 2nd argument. Custom tags can be
+added by passing a Hash as the 3rd argument. A simple example is as follows:
+
+```ruby
+Gitlab::Metrics.measure(:example_series, { number: 10 }, { class: self.class.to_s }) do
+ ...
+end
+```
diff --git a/doc/development/testing.md b/doc/development/testing.md
new file mode 100644
index 00000000000..23417845f16
--- /dev/null
+++ b/doc/development/testing.md
@@ -0,0 +1,134 @@
+# Testing Standards and Style Guidelines
+
+This guide outlines standards and best practices for automated testing of GitLab
+CE and EE.
+
+It is meant to be an _extension_ of the [thoughtbot testing
+styleguide](https://github.com/thoughtbot/guides/tree/master/style/testing). If
+this guide defines a rule that contradicts the thoughtbot guide, this guide
+takes precedence. Some guidelines may be repeated verbatim to stress their
+importance.
+
+## Factories
+
+GitLab uses [factory_girl] as a test fixture replacement.
+
+- Factory definitions live in `spec/factories/`, named using the pluralization
+ of their corresponding model (`User` factories are defined in `users.rb`).
+- There should be only one top-level factory definition per file.
+- Make use of [Traits] to clean up definitions and usages.
+- When defining a factory, don't define attributes that are not required for the
+ resulting record to pass validation.
+- When instantiating from a factory, don't supply extraneous attributes that
+ aren't required by the test.
+- Factories don't have to be limited to `ActiveRecord` objects.
+ [See example](https://gitlab.com/gitlab-org/gitlab-ce/commit/0b8cefd3b2385a21cfed779bd659978c0402766d).
+
+[factory_girl]: https://github.com/thoughtbot/factory_girl
+[Traits]: http://www.rubydoc.info/gems/factory_girl/file/GETTING_STARTED.md#Traits
+
+## JavaScript
+
+GitLab uses [Teaspoon] to run its [Jasmine] JavaScript specs. They can be run on
+the command line via `bundle exec teaspoon`, or via a web browser at
+`http://localhost:3000/teaspoon` when the Rails server is running.
+
+- JavaScript tests live in `spec/javascripts/`, matching the folder structure of
+ `app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js.coffee` has a corresponding
+ `spec/javascripts/behaviors/autosize_spec.js.coffee` file.
+- Haml fixtures required for JavaScript tests live in
+ `spec/javascripts/fixtures`. They should contain the bare minimum amount of
+ markup necessary for the test.
+
+ > **Warning:** Keep in mind that a Rails view may change and
+ invalidate your test, but everything will still pass because your fixture
+ doesn't reflect the latest view.
+
+- Keep in mind that in a CI environment, these tests are run in a headless
+ browser and you will not have access to certain APIs, such as
+ [`Notification`](https://developer.mozilla.org/en-US/docs/Web/API/notification),
+ which will have to be stubbed.
+
+[Teaspoon]: https://github.com/modeset/teaspoon
+[Jasmine]: https://github.com/jasmine/jasmine
+
+## RSpec
+
+### General Guidelines
+
+- Use a single, top-level `describe ClassName` block.
+- Use `described_class` instead of repeating the class name being described.
+- Use `.method` to describe class methods and `#method` to describe instance
+ methods.
+- Use `context` to test branching logic.
+- Don't `describe` symbols (see [Gotchas](gotchas.md#dont-describe-symbols)).
+- Prefer `not_to` to `to_not`.
+- Try to match the ordering of tests to the ordering within the class.
+- Try to follow the [Four-Phase Test][four-phase-test] pattern, using newlines
+ to separate phases.
+
+[four-phase-test]: https://robots.thoughtbot.com/four-phase-test
+
+### `let` variables
+
+GitLab's RSpec suite has made extensive use of `let` variables to reduce
+duplication. However, this sometimes [comes at the cost of clarity][lets-not],
+so we need to set some guidelines for their use going forward:
+
+- `let` variables are preferable to instance variables. Local variables are
+ preferable to `let` variables.
+- Use `let` to reduce duplication throughout an entire spec file.
+- Don't use `let` to define variables used by a single test; define them as
+ local variables inside the test's `it` block.
+- Don't define a `let` variable inside the top-level `describe` block that's
+ only used in a more deeply-nested `context` or `describe` block. Keep the
+ definition as close as possible to where it's used.
+- Try to avoid overriding the definition of one `let` variable with another.
+- Don't define a `let` variable that's only used by the definition of another.
+ Use a helper method instead.
+
+[lets-not]: https://robots.thoughtbot.com/lets-not
+
+### Test speed
+
+GitLab has a massive test suite that, without parallelization, can take more
+than an hour to run. It's important that we make an effort to write tests that
+are accurate and effective _as well as_ fast.
+
+Here are some things to keep in mind regarding test performance:
+
+- `double` and `spy` are faster than `FactoryGirl.build(...)`
+- `FactoryGirl.build(...)` and `.build_stubbed` are faster than `.create`.
+- Don't `create` an object when `build`, `build_stubbed`, `attributes_for`,
+ `spy`, or `double` will do. Database persistence is slow!
+- Use `create(:empty_project)` instead of `create(:project)` when you don't need
+ the underlying repository. Filesystem operations are slow!
+- Don't mark a feature as requiring JavaScript (through `@javascript` in
+ Spinach or `js: true` in RSpec) unless it's _actually_ required for the test
+ to be valid. Headless browser testing is slow!
+
+### Features / Integration
+
+- Feature specs live in `spec/features/` and should be named
+ `ROLE_ACTION_spec.rb`, such as `user_changes_password_spec.rb`.
+- Use only one `feature` block per feature spec file.
+- Use scenario titles that describe the success and failure paths.
+- Avoid scenario titles that add no information, such as "successfully."
+- Avoid scenario titles that repeat the feature title.
+
+## Spinach (feature) tests
+
+GitLab [moved from Cucumber to Spinach](https://github.com/gitlabhq/gitlabhq/pull/1426)
+for its feature/integration tests in September 2012.
+
+As of March 2016, we are [trying to avoid adding new Spinach
+tests](https://gitlab.com/gitlab-org/gitlab-ce/issues/14121) going forward,
+opting for [RSpec feature](#features-integration) specs.
+
+Adding new Spinach scenarios is acceptable _only if_ the new scenario requires
+no more than one new `step` definition. If more than that is required, the
+test should be re-implemented using RSpec instead.
+
+---
+
+[Return to Development documentation](README.md)
diff --git a/doc/development/ui_guide.md b/doc/development/ui_guide.md
index 2f01defc11d..a3e260a5f89 100644
--- a/doc/development/ui_guide.md
+++ b/doc/development/ui_guide.md
@@ -1,9 +1,5 @@
# UI Guide for building GitLab
-## Best practices for creating new pages in GitLab
-
-TODO: write some best practices when develop GitLab features.
-
## GitLab UI development kit
We created a page inside GitLab where you can check commonly used html and css elements.
diff --git a/doc/incoming_email/README.md b/doc/incoming_email/README.md
index 4cfb8402943..5a9a1582877 100644
--- a/doc/incoming_email/README.md
+++ b/doc/incoming_email/README.md
@@ -1,36 +1,99 @@
# Reply by email
-GitLab can be set up to allow users to comment on issues and merge requests by replying to notification emails.
+GitLab can be set up to allow users to comment on issues and merge requests by
+replying to notification emails.
-## Get a mailbox
+## Requirement
-Reply by email requires an IMAP-enabled email account, with a provider or server that supports [email sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing). Sub-addressing is a feature where any email to `user+some_arbitrary_tag@example.com` will end up in the mailbox for `user@example.com`, and is supported by providers such as Gmail, Google Apps, Yahoo! Mail, Outlook.com and iCloud, as well as the Postfix mail server which you can run on-premises.
+Reply by email requires an IMAP-enabled email account. GitLab allows you to use
+three strategies for this feature:
+- using email sub-addressing
+- using a dedicated email address
+- using a catch-all mailbox
-If you want to use Gmail / Google Apps with Reply by email, make sure you have [IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018) and [allow less secure apps to access the account](https://support.google.com/accounts/answer/6010255).
+### Email sub-addressing
-To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these instructions](./postfix.md).
+**If your provider or server supports email sub-addressing, we recommend using it.**
+
+[Sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing) is
+a feature where any email to `user+some_arbitrary_tag@example.com` will end up
+in the mailbox for `user@example.com`, and is supported by providers such as
+Gmail, Google Apps, Yahoo! Mail, Outlook.com and iCloud, as well as the Postfix
+mail server which you can run on-premises.
+
+### Dedicated email address
+
+This solution is really simple to set up: you just have to create an email
+address dedicated to receive your users' replies to GitLab notifications.
+
+### Catch-all mailbox
+
+A [catch-all mailbox](https://en.wikipedia.org/wiki/Catch-all) for a domain will
+"catch all" the emails addressed to the domain that do not exist in the mail
+server.
+
+## How it works?
+
+### 1. GitLab sends a notification email
+
+When GitLab sends a notification and Reply by email is enabled, the `Reply-To`
+header is set to the address defined in your GitLab configuration, with the
+`%{key}` placeholder (if present) replaced by a specific "reply key". In
+addition, this "reply key" is also added to the `References` header.
+
+### 2. You reply to the notification email
+
+When you reply to the notification email, your email client will:
+
+- send the email to the `Reply-To` address it got from the notification email
+- set the `In-Reply-To` header to the value of the `Message-ID` header from the
+ notification email
+- set the `References` header to the value of the `Message-ID` plus the value of
+ the notification email's `References` header.
+
+### 3. GitLab receives your reply to the notification email
+
+When GitLab receives your reply, it will look for the "reply key" in the
+following headers, in this order:
+
+1. the `To` header
+1. the `References` header
+
+If it finds a reply key, it will be able to leave your reply as a comment on
+the entity the notification was about (issue, merge request, commit...).
+
+For more details about the `Message-ID`, `In-Reply-To`, and `References headers`,
+please consult [RFC 5322](https://tools.ietf.org/html/rfc5322#section-3.6.4).
## Set it up
+If you want to use Gmail / Google Apps with Reply by email, make sure you have
+[IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018)
+and [allowed less secure apps to access the account](https://support.google.com/accounts/answer/6010255).
+
+To set up a basic Postfix mail server with IMAP access on Ubuntu, follow
+[these instructions](./postfix.md).
+
### Omnibus package installations
-1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the feature and fill in the details for your specific IMAP server and email account:
+1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the
+ feature and fill in the details for your specific IMAP server and email account:
```ruby
# Configuration for Postfix mail server, assumes mailbox incoming@gitlab.example.com
gitlab_rails['incoming_email_enabled'] = true
-
- # The email address including a placeholder for the key that references the item being replied to.
- # The `%{key}` placeholder is added after the user part, before the `@`.
+
+ # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
+ # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
gitlab_rails['incoming_email_address'] = "incoming+%{key}@gitlab.example.com"
-
+
# Email account username
# With third party providers, this is usually the full email address.
# With self-hosted email servers, this is usually the user part of the email address.
gitlab_rails['incoming_email_email'] = "incoming"
# Email account password
gitlab_rails['incoming_email_password'] = "[REDACTED]"
-
+
# IMAP server host
gitlab_rails['incoming_email_host'] = "gitlab.example.com"
# IMAP server port
@@ -47,18 +110,18 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
```ruby
# Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com
gitlab_rails['incoming_email_enabled'] = true
-
+
# The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
- # The `%{key}` placeholder is added after the user part, after a `+` character, before the `@`.
+ # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
gitlab_rails['incoming_email_address'] = "gitlab-incoming+%{key}@gmail.com"
-
+
# Email account username
# With third party providers, this is usually the full email address.
# With self-hosted email servers, this is usually the user part of the email address.
gitlab_rails['incoming_email_email'] = "gitlab-incoming@gmail.com"
# Email account password
gitlab_rails['incoming_email_password'] = "[REDACTED]"
-
+
# IMAP server host
gitlab_rails['incoming_email_host'] = "imap.gmail.com"
# IMAP server port
@@ -72,8 +135,6 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
gitlab_rails['incoming_email_mailbox_name'] = "inbox"
```
- As mentioned, the part after `+` in the address is ignored, and any email sent here will end up in the mailbox for `incoming@gitlab.example.com`/`gitlab-incoming@gmail.com`.
-
1. Reconfigure GitLab and restart mailroom for the changes to take effect:
```sh
@@ -97,7 +158,8 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
cd /home/git/gitlab
```
-1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature and fill in the details for your specific IMAP server and email account:
+1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature
+ and fill in the details for your specific IMAP server and email account:
```sh
sudo editor config/gitlab.yml
@@ -109,7 +171,7 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
enabled: true
# The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
- # The `%{key}` placeholder is added after the user part, after a `+` character, before the `@`.
+ # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
address: "incoming+%{key}@gitlab.example.com"
# Email account username
@@ -138,7 +200,7 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
enabled: true
# The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
- # The `%{key}` placeholder is added after the user part, after a `+` character, before the `@`.
+ # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
address: "gitlab-incoming+%{key}@gmail.com"
# Email account username
@@ -161,8 +223,6 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
mailbox: "inbox"
```
- As mentioned, the part after `+` in the address is ignored, and any email sent here will end up in the mailbox for `incoming@gitlab.example.com`/`gitlab-incoming@gmail.com`.
-
1. Enable `mail_room` in the init script at `/etc/default/gitlab`:
```sh
@@ -195,8 +255,8 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
incoming_email:
enabled: true
- # The email address including a placeholder for the key that references the item being replied to.
- # The `%{key}` placeholder is added after the user part, before the `@`.
+ # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
+ # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
address: "gitlab-incoming+%{key}@gmail.com"
# Email account username
diff --git a/doc/install/installation.md b/doc/install/installation.md
index bffbc776500..d97dc7d1311 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -227,9 +227,9 @@ sudo usermod -aG redis git
### Clone the Source
# Clone GitLab repository
- sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-6-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-7-stable gitlab
-**Note:** You can change `8-6-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `8-7-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
@@ -283,9 +283,13 @@ sudo usermod -aG redis git
# Copy the example Rack attack config
sudo -u git -H cp config/initializers/rack_attack.rb.example config/initializers/rack_attack.rb
- # Configure Git global settings for git user, used when editing via web editor
+ # Configure Git global settings for git user
+ # 'autocrlf' is needed for the web editor
sudo -u git -H git config --global core.autocrlf input
+ # Disable 'git gc --auto' because GitLab already runs 'git gc' when needed
+ sudo -u git -H git config --global gc.auto 0
+
# Configure Redis connection settings
sudo -u git -H cp config/resque.yml.example config/resque.yml
@@ -348,7 +352,7 @@ GitLab Shell is an SSH access and repository management software developed speci
cd /home/git
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git
cd gitlab-workhorse
- sudo -u git -H git checkout v0.7.1
+ sudo -u git -H git checkout v0.7.2
sudo -u git -H make
### Initialize Database and Activate Advanced Features
diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md
index fb20308c49c..30f0c15dacc 100644
--- a/doc/integration/ldap.md
+++ b/doc/integration/ldap.md
@@ -1,3 +1,3 @@
# GitLab LDAP integration
-This document was moved under [`administration/auth/ldap`](administration/auth/ldap.md).
+This document was moved under [`administration/auth/ldap`](../administration/auth/ldap.md).
diff --git a/doc/integration/saml.md b/doc/integration/saml.md
index 1c3dc707f6d..8a7205caaa4 100644
--- a/doc/integration/saml.md
+++ b/doc/integration/saml.md
@@ -131,8 +131,75 @@ On the sign in page there should now be a SAML button below the regular sign in
Click the icon to begin the authentication process. If everything goes well the user
will be returned to GitLab and will be signed in.
+## External Groups
+
+>**Note:**
+This setting is only available on GitLab 8.7 and above.
+
+SAML login includes support for external groups. You can define in the SAML
+settings which groups, to which your users belong in your IdP, you wish to be
+marked as [external](../permissions/permissions.md).
+
+### Requirements
+
+First you need to tell GitLab where to look for group information. For this you
+need to make sure that your IdP server sends a specific `AttributeStament` along
+with the regular SAML response. Here is an example:
+
+```xml
+<saml:AttributeStatement>
+ <saml:Attribute Name="Groups">
+ <saml:AttributeValue xsi:type="xs:string">SecurityGroup</saml:AttributeValue>
+ <saml:AttributeValue xsi:type="xs:string">Developers</saml:AttributeValue>
+ <saml:AttributeValue xsi:type="xs:string">Designers</saml:AttributeValue>
+ </saml:Attribute>
+</saml:AttributeStatement>
+```
+
+The name of the attribute can be anything you like, but it must contain the groups
+to which a user belongs. In order to tell GitLab where to find these groups, you need
+to add a `groups_attribute:` element to your SAML settings. You will also need to
+tell GitLab which groups are external via the `external_groups:` element:
+
+```yaml
+{ name: 'saml',
+ label: 'Our SAML Provider',
+ groups_attribute: 'Groups',
+ external_groups: ['Freelancers', 'Interns'],
+ args: {
+ assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
+ idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
+ idp_sso_target_url: 'https://login.example.com/idp',
+ issuer: 'https://gitlab.example.com',
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
+ } }
+```
+
## Customization
+### `auto_sign_in_with_provider`
+
+You can add this setting to your GitLab configuration to automatically redirect you
+to your SAML server for authentication, thus removing the need to click a button
+before actually signing in.
+
+For omnibus package:
+
+```ruby
+gitlab_rails['omniauth_auto_sign_in_with_provider'] = 'saml'
+```
+
+For installations from source:
+
+```yaml
+omniauth:
+ auto_sign_in_with_provider: saml
+```
+
+Please keep in mind that every sign in attempt will be redirected to the SAML server,
+so you will not be able to sign in using local credentials. Make sure that at least one
+of the SAML users has admin permissions.
+
### `attribute_statements`
>**Note:**
@@ -205,6 +272,10 @@ To bypass this you can add `skip_before_action :verify_authenticity_token` to th
where it can then be seen in the usual logs, or as a flash message in the login
screen.
+That file is located at `/opt/gitlab/embedded/service/gitlab-rails/app/controllers`
+for Omnibus installations and by default on `/home/git/gitlab/app/controllers` for
+installations from source.
+
### Invalid audience
This error means that the IdP doesn't recognize GitLab as a valid sender and
diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md
index e6eb1cf3819..4f199b6af6f 100644
--- a/doc/markdown/markdown.md
+++ b/doc/markdown/markdown.md
@@ -31,7 +31,7 @@
_GitLab uses the [Redcarpet Ruby library][redcarpet] for Markdown processing._
-For GitLab we developed something we call "GitLab Flavored Markdown" (GFM). It extends the standard Markdown in a few significant ways to add some useful functionality.
+GitLab uses "GitLab Flavored Markdown" (GFM). It extends the standard Markdown in a few significant ways to add some useful functionality. It was inspired by [GitHub Flavored Markdown](https://help.github.com/articles/basic-writing-and-formatting-syntax/).
You can use GFM in
@@ -47,10 +47,10 @@ You can also use other rich text files in GitLab. You might have to install a de
GFM honors the markdown specification in how [paragraphs and line breaks are handled](https://daringfireball.net/projects/markdown/syntax#p).
-A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines.
+A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines.
Line-breaks, or softreturns, are rendered if you end a line with two or more spaces
- Roses are red [followed by two or more spaces]
+ Roses are red [followed by two or more spaces]
Violets are blue
Sugar is sweet
@@ -67,7 +67,7 @@ It is not reasonable to italicize just _part_ of a word, especially when you're
perform_complicated_task
do_this_and_do_that_and_another_thing
-perform_complicated_task
+perform_complicated_task
do_this_and_do_that_and_another_thing
## URL auto-linking
diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md
index 3d375e47c8e..6219693b8a8 100644
--- a/doc/permissions/permissions.md
+++ b/doc/permissions/permissions.md
@@ -52,10 +52,11 @@ documentation](../workflow/add-user/add-user.md).
| Switch visibility level | | | | | ✓ |
| Transfer project to another namespace | | | | | ✓ |
| Remove project | | | | | ✓ |
-| Force push to protected branches | | | | | |
-| Remove protected branches | | | | | |
+| Force push to protected branches [^2] | | | | | |
+| Remove protected branches [^2] | | | | | |
[^1]: If **Allow guest to access builds** is enabled in CI settings
+[^2]: Not allowed for Guest, Reporter, Developer, Master, or Owner
## Group
diff --git a/doc/update/8.6-to-8.7.md b/doc/update/8.6-to-8.7.md
new file mode 100644
index 00000000000..57847d2d9fd
--- /dev/null
+++ b/doc/update/8.6-to-8.7.md
@@ -0,0 +1,154 @@
+# From 8.6 to 8.7
+
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag in the version dropdown at the
+top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
+### 1. Stop server
+
+ sudo service gitlab stop
+
+### 2. Backup
+
+```bash
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 3. Get latest code
+
+```bash
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
+```
+
+For GitLab Community Edition:
+
+```bash
+sudo -u git -H git checkout 8-7-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+sudo -u git -H git checkout 8-7-stable-ee
+```
+
+### 4. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout v2.7.0
+```
+
+### 5. Update gitlab-workhorse
+
+Install and compile gitlab-workhorse. This requires
+[Go 1.5](https://golang.org/dl) which should already be on your system from
+GitLab 8.1.
+
+```bash
+cd /home/git/gitlab-workhorse
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout v0.7.2
+sudo -u git -H make
+```
+
+### 6. Install libs, migrations, etc.
+
+```bash
+cd /home/git/gitlab
+
+# MySQL installations (note: the line below states '--without postgres')
+sudo -u git -H bundle install --without postgres development test --deployment
+
+# PostgreSQL installations (note: the line below states '--without mysql')
+sudo -u git -H bundle install --without mysql development test --deployment
+
+# Optional: clean up old gems
+sudo -u git -H bundle clean
+
+# Run database migrations
+sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+
+# Clean up assets and cache
+sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
+
+```
+
+### 7. Update configuration files
+
+#### Git configuration
+
+Disable `git gc --auto` because GitLab runs `git gc` for us already.
+
+```sh
+sudo -u git -H git config --global gc.auto 0
+```
+
+#### Nginx configuration
+
+Ensure you're still up-to-date with the latest NGINX configuration changes:
+
+```sh
+# For HTTPS configurations
+git diff origin/8-6-stable:lib/support/nginx/gitlab-ssl origin/8-7-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/8-6-stable:lib/support/nginx/gitlab origin/8-7-stable:lib/support/nginx/gitlab
+```
+
+If you are using Apache instead of NGINX please see the updated [Apache templates].
+Also note that because Apache does not support upstreams behind Unix sockets you
+will need to let gitlab-workhorse listen on a TCP port. You can do this
+via [/etc/default/gitlab].
+
+[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
+[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-7-stable/lib/support/init.d/gitlab.default.example#L37
+
+#### Init script
+
+Ensure you're still up-to-date with the latest init script changes:
+
+ sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+
+### 8. Start application
+
+ sudo service gitlab start
+ sudo service nginx restart
+
+### 9. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+ sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+
+To make sure you didn't miss anything run a more thorough check:
+
+ sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+
+If all items are green, then congratulations, the upgrade is complete!
+
+## Things went south? Revert to previous version (8.6)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 8.5 to 8.6](8.5-to-8.6.md), except for the
+database migration (the backup is already migrated to the previous version).
+
+### 2. Restore from the backup
+
+```bash
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
+```
+
+If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
diff --git a/features/dashboard/dashboard.feature b/features/dashboard/dashboard.feature
index c3b3577c449..db73309804c 100644
--- a/features/dashboard/dashboard.feature
+++ b/features/dashboard/dashboard.feature
@@ -6,6 +6,7 @@ Feature: Dashboard
And project "Shop" has push event
And project "Shop" has CI enabled
And project "Shop" has CI build
+ And project "Shop" has labels: "bug", "feature", "enhancement"
And I visit dashboard page
Scenario: I should see projects list
@@ -51,6 +52,13 @@ Feature: Dashboard
Then The list should be sorted by "Oldest updated"
@javascript
+ Scenario: Filtering Issues by label
+ Given project "Shop" has issue "Bugfix1" with label "feature"
+ When I visit dashboard issues page
+ And I filter the list by label "feature"
+ Then I should see "Bugfix1" in issues list
+
+ @javascript
Scenario: Visiting Project's issues after sorting
Given I visit dashboard issues page
And I sort the list by "Oldest updated"
diff --git a/features/dashboard/todos.feature b/features/dashboard/todos.feature
index 1e7b1b50d64..8677b450813 100644
--- a/features/dashboard/todos.feature
+++ b/features/dashboard/todos.feature
@@ -36,3 +36,8 @@ Feature: Dashboard Todos
Scenario: I filter by action
Given I filter by "Mentioned"
Then I should not see todos related to "Assignments" in the list
+
+ @javascript
+ Scenario: I click on a todo row
+ Given I click on the todo
+ Then I should be directed to the corresponding page
diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb
index 5062e348844..b5980b35102 100644
--- a/features/steps/dashboard/dashboard.rb
+++ b/features/steps/dashboard/dashboard.rb
@@ -87,4 +87,23 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps
step 'I should see 1 project at group list' do
expect(find('span.last_activity/span')).to have_content('1')
end
+
+ step 'I filter the list by label "feature"' do
+ page.within ".labels-filter" do
+ find('.dropdown').click
+ click_link "feature"
+ end
+ end
+
+ step 'I should see "Bugfix1" in issues list' do
+ page.within "ul.content-list" do
+ expect(page).to have_content "Bugfix1"
+ end
+ end
+
+ step 'project "Shop" has issue "Bugfix1" with label "feature"' do
+ project = Project.find_by(name: "Shop")
+ issue = create(:issue, title: "Bugfix1", project: project, assignee: current_user)
+ issue.labels << project.labels.find_by(title: 'feature')
+ end
end
diff --git a/features/steps/dashboard/issues.rb b/features/steps/dashboard/issues.rb
index 93aa77589be..e21af72a777 100644
--- a/features/steps/dashboard/issues.rb
+++ b/features/steps/dashboard/issues.rb
@@ -42,11 +42,10 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps
end
step 'I click "All" link' do
- find('.js-author-search').click
- find('.dropdown-content a', match: :first).click
-
- find('.js-assignee-search').click
- find('.dropdown-content a', match: :first).click
+ find(".js-author-search").click
+ find(".dropdown-menu-author li a", match: :first).click
+ find(".js-assignee-search").click
+ find(".dropdown-menu-assignee li a", match: :first).click
end
def should_see(issue)
diff --git a/features/steps/dashboard/todos.rb b/features/steps/dashboard/todos.rb
index 963e4f21365..a6e574f12a9 100644
--- a/features/steps/dashboard/todos.rb
+++ b/features/steps/dashboard/todos.rb
@@ -26,6 +26,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
end
step 'I should see todos assigned to me' do
+ page.within('.nav-sidebar') { expect(page).to have_content 'Todos 4' }
expect(page).to have_content 'To do 4'
expect(page).to have_content 'Done 0'
@@ -41,6 +42,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
click_link 'Done'
end
+ page.within('.nav-sidebar') { expect(page).to have_content 'Todos 3' }
expect(page).to have_content 'To do 3'
expect(page).to have_content 'Done 1'
should_not_see_todo "John Doe assigned you merge request !#{merge_request.iid}"
@@ -88,6 +90,14 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
should_not_see_todo "John Doe assigned you issue ##{issue.iid}"
end
+ step 'I click on the todo' do
+ find('.todo:nth-child(1)').click
+ end
+
+ step 'I should be directed to the corresponding page' do
+ page.should have_css('.identifier', text: 'Merge Request !1')
+ end
+
def should_see_todo(position, title, body, pending = true)
page.within(".todo:nth-child(#{position})") do
expect(page).to have_content title
diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb
index 91fe19dd477..a4f02b590ea 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -326,7 +326,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I should see a discussion has started on diff' do
page.within(".notes .discussion") do
- page.should have_content "#{current_user.name} started a discussion"
+ page.should have_content "#{current_user.name} #{current_user.to_reference} started a discussion"
page.should have_content sample_commit.line_code_path
page.should have_content "Line is wrong"
end
@@ -334,7 +334,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I should see a discussion by user "John Doe" has started on diff' do
page.within(".notes .discussion") do
- page.should have_content "#{user_exists("John Doe").name} started a discussion"
+ page.should have_content "#{user_exists("John Doe").name} #{user_exists("John Doe").to_reference} started a discussion"
page.should have_content sample_commit.line_code_path
page.should have_content "Line is wrong"
end
@@ -350,7 +350,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I should see a discussion has started on commit diff' do
page.within(".notes .discussion") do
- page.should have_content "#{current_user.name} started a discussion on commit"
+ page.should have_content "#{current_user.name} #{current_user.to_reference} started a discussion on commit"
page.should have_content sample_commit.line_code_path
page.should have_content "Line is wrong"
end
@@ -358,7 +358,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I should see a discussion has started on commit' do
page.within(".notes .discussion") do
- page.should have_content "#{current_user.name} started a discussion on commit"
+ page.should have_content "#{current_user.name} #{current_user.to_reference} started a discussion on commit"
page.should have_content "One comment to rule them all"
end
end
diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb
index 223b7277b51..9f6aed1c5b9 100644
--- a/features/steps/project/wiki.rb
+++ b/features/steps/project/wiki.rb
@@ -85,7 +85,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
end
step 'I have an existing Wiki page with images linked on page' do
- wiki.create_page("pictures", "Look at this [image](image.jpg)\n\n ![image](image.jpg)", :markdown, "first commit")
+ wiki.create_page("pictures", "Look at this [image](image.jpg)\n\n ![alt text](image.jpg)", :markdown, "first commit")
@wiki_page = wiki.find_page("pictures")
end
diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb
index 906b66a4a63..32c3e99f450 100644
--- a/features/steps/shared/diff_note.rb
+++ b/features/steps/shared/diff_note.rb
@@ -125,7 +125,7 @@ module SharedDiffNote
step 'I should only see one diff form' do
page.within(diff_file_selector) do
- expect(page).to have_css("form.new_note", count: 1)
+ expect(page).to have_css("form.new-note", count: 1)
end
end
@@ -161,7 +161,7 @@ module SharedDiffNote
step 'I should see a temporary diff comment form' do
page.within(diff_file_selector) do
- expect(page).to have_css(".js-temp-notes-holder form.new_note")
+ expect(page).to have_css(".js-temp-notes-holder form.new-note")
end
end
diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb
index fb0462d6e04..a3c3887ab46 100644
--- a/features/steps/shared/note.rb
+++ b/features/steps/shared/note.rb
@@ -2,7 +2,7 @@ module SharedNote
include Spinach::DSL
step 'I delete a comment' do
- page.within('.notes') do
+ page.within('.main-notes-list') do
find('.note').hover
find(".js-note-delete").click
end
@@ -128,7 +128,7 @@ module SharedNote
end
step 'I edit the last comment with a +1' do
- page.within(".notes") do
+ page.within(".main-notes-list") do
find(".note").hover
find('.js-note-edit').click
end
diff --git a/fixtures/emojis/digests.json b/fixtures/emojis/digests.json
new file mode 100644
index 00000000000..18d6e93e0f4
--- /dev/null
+++ b/fixtures/emojis/digests.json
@@ -0,0 +1,8597 @@
+[
+ {
+ "name": "100",
+ "unicode": "1F4AF",
+ "digest": "6d57c7cc93335f853e1a5670233f121bc94730dbd82b2b3c5c5a509e092ef0fd"
+ },
+ {
+ "name": "1234",
+ "unicode": "1F522",
+ "digest": "727763fd9f18fd5df59e9f78e678ea4ec753e674d70f15d4e77c7802067d660b"
+ },
+ {
+ "name": "8ball",
+ "unicode": "1F3B1",
+ "digest": "1aecf21951452ba24e921ec71b3d313b7ddc2e185b0339c9e0eebc85be4f031d"
+ },
+ {
+ "name": "a",
+ "unicode": "1F170",
+ "digest": "2272113a5bcb7faf8db7c1bd35df576d32f2f7cbd881463934ad3382eb87c723"
+ },
+ {
+ "name": "ab",
+ "unicode": "1F18E",
+ "digest": "6f8a237751fdc84db4121f408272d9a23258515449610e4c6c54f50f6e995627"
+ },
+ {
+ "name": "abc",
+ "unicode": "1F524",
+ "digest": "652a2381a7b587d8a52d5178e2d7d6c8600b33d36160fa69677943da374105bc"
+ },
+ {
+ "name": "abcd",
+ "unicode": "1F521",
+ "digest": "35ade4fd3d75294ebb72c24490aa32745604edc6cabe095b90634cd3ce78c07b"
+ },
+ {
+ "name": "accept",
+ "unicode": "1F251",
+ "digest": "8212ed158cc447c92813273fc915e84d3d5c4c48d1b38e498c088bad27ab8145"
+ },
+ {
+ "name": "aerial_tramway",
+ "unicode": "1F6A1",
+ "digest": "8039d7f67e6e5b211066cab6cf2142afc3aca5c830a357369362c9b484029563"
+ },
+ {
+ "name": "airplane",
+ "unicode": "2708",
+ "digest": "18f4dfac323555d8cdabb79148874c0185ce98e1a08e69414d236b23e502a854"
+ },
+ {
+ "name": "airplane_arriving",
+ "unicode": "1F6EC",
+ "digest": "9a1c81d97512e5d0e3acec40290d00f616ec182140909859e366a734b9f840bb"
+ },
+ {
+ "name": "airplane_departure",
+ "unicode": "1F6EB",
+ "digest": "e3c5ff4038db998c1897cb237d0b865da0bc60331c758f204e45a979d5fab445"
+ },
+ {
+ "name": "airplane_northeast",
+ "unicode": "1F6EA",
+ "digest": "fdddc2cd3618ec6661612581b8b93553cb086b0bb197e96aedf1bee8055e7bb4"
+ },
+ {
+ "name": "airplane_small",
+ "unicode": "1F6E9",
+ "digest": "f98b44422d6bf505b50330805ecf68013d035341f0b6487c3c05ad913eb5abd3"
+ },
+ {
+ "name": "airplane_small_up",
+ "unicode": "1F6E8",
+ "digest": "029752b29a757c087dec60f45ea242e974fc181129e20390d5d4a2f90442091a"
+ },
+ {
+ "name": "airplane_up",
+ "unicode": "1F6E7",
+ "digest": "ec45d4dbfce1f75dc59339417b1dcf5f1e1359cd9d04ff233babf359a3330e77"
+ },
+ {
+ "name": "alarm_clock",
+ "unicode": "23F0",
+ "digest": "84ddd7b3b857c165410b7b44863e5354ca0f3591c3bfe56231f12c9f7531a96f"
+ },
+ {
+ "name": "alembic",
+ "unicode": "2697",
+ "digest": "45698914a21683f06931d807af171bcb6984e5ebce66012bba71b467565bd69d"
+ },
+ {
+ "name": "alien",
+ "unicode": "1F47D",
+ "digest": "94dbe4e90614c654145aba93610c43e3ab86df8ca07391bd4e56383f9329c008"
+ },
+ {
+ "name": "ambulance",
+ "unicode": "1F691",
+ "digest": "82ef36bcd13c88a4b2397c918b8048adc6bf045ed2532ff568e0dfd1b1b29c3c"
+ },
+ {
+ "name": "amphora",
+ "unicode": "1F3FA",
+ "digest": "d3758d88aa1fc3be01894102f57479d3a49790510d38ad3d06a2774962010608"
+ },
+ {
+ "name": "anchor",
+ "unicode": "2693",
+ "digest": "27c6034f769d9f020362fc5b227b9279651cc940861e727d1f6ccd59af98f851"
+ },
+ {
+ "name": "angel",
+ "unicode": "1F47C",
+ "digest": "c1b8ad2adc7686e7fbbe4ec357071e7228a5e0762e001bb589e2f97ff258d5c7"
+ },
+ {
+ "name": "angel_tone1",
+ "unicode": "1F47C-1F3FB",
+ "digest": "90b701c43311b1096c4a012d9905a186f1a16829ea2707921a8418c28617d751"
+ },
+ {
+ "name": "angel_tone2",
+ "unicode": "1F47C-1F3FC",
+ "digest": "d6bcaf1b76e25d486d4ab9b159cf727782d508543d1ae27c8d2c12d2f13d6eb0"
+ },
+ {
+ "name": "angel_tone3",
+ "unicode": "1F47C-1F3FD",
+ "digest": "3069285e6218c8083cb0085aa10017bcdea033e321d97ba339a84892074b903a"
+ },
+ {
+ "name": "angel_tone4",
+ "unicode": "1F47C-1F3FE",
+ "digest": "dbb87019752d9caa94ce086858c1e3225b62e221ad599f5106548fda2456fc2b"
+ },
+ {
+ "name": "angel_tone5",
+ "unicode": "1F47C-1F3FF",
+ "digest": "f77703df97720c27a128b5f3c0948b9e04a6b6b81ea5306468154f9bf56225db"
+ },
+ {
+ "name": "anger",
+ "unicode": "1F4A2",
+ "digest": "2253b7ff0894f247bc6f04d841a748c56d6c94684880c13df42387691ff20e75"
+ },
+ {
+ "name": "anger_left",
+ "unicode": "1F5EE",
+ "digest": "f2711991e8b386b2d5b12f296ce20a9b4b00ef91d6d67af2cf4e06abf2faa1dc"
+ },
+ {
+ "name": "anger_right",
+ "unicode": "1F5EF",
+ "digest": "24b572d64c519251a3ae8844e8d66fd6955752aff99aebe7dc20179505a466c4"
+ },
+ {
+ "name": "angry",
+ "unicode": "1F620",
+ "digest": "c4188ba70df99d8ccef5706d711176725d3dd50d62f065a177d68d85c7828107"
+ },
+ {
+ "name": "anguished",
+ "unicode": "1F627",
+ "digest": "9c2347308133ae50dc04da62042fff847f4c477b2956b8aa976f0413899e38bc"
+ },
+ {
+ "name": "ant",
+ "unicode": "1F41C",
+ "digest": "d2af2ed1cfe15d649aa329d965764a1e8726941d833841781a5b66d7dd0b0921"
+ },
+ {
+ "name": "apple",
+ "unicode": "1F34E",
+ "digest": "a9babee24f454934a5e1fb8d781cbce354dfd88e8a8e01f02e8b30071fd40460"
+ },
+ {
+ "name": "aquarius",
+ "unicode": "2652",
+ "digest": "1a168c252678847d1f9ef450887489e3bdc207ecae4b6fb05e92295ff861ae2c"
+ },
+ {
+ "name": "aries",
+ "unicode": "2648",
+ "digest": "bde262a8795e12f8b0ebb3f0f8c3a56104062fcee8d5d678cf4bb445a7daf698"
+ },
+ {
+ "name": "arrow_backward",
+ "unicode": "25C0",
+ "digest": "ddae36d1febf5c246e51d599e2898a8aa30cd47f88b5bcb469e3ca9d22538b97"
+ },
+ {
+ "name": "arrow_double_down",
+ "unicode": "23EC",
+ "digest": "906f42b5f788128ed90d2d162cf03e6e595a50ad05e0aa5f64e925637379d0cd"
+ },
+ {
+ "name": "arrow_double_up",
+ "unicode": "23EB",
+ "digest": "2129a57402980de6fc6f59ad8354525c2dbcd66d1b78f4de091181ddc81e0693"
+ },
+ {
+ "name": "arrow_down",
+ "unicode": "2B07",
+ "digest": "370e4f41565d5dab245c20e45c502505a56d26c2392283781b841eb3e905edb2"
+ },
+ {
+ "name": "arrow_down_small",
+ "unicode": "1F53D",
+ "digest": "98a2b183f2daec425160bbfce1d2b940b8baa0d5032fdacfa9453e39bed5651b"
+ },
+ {
+ "name": "arrow_forward",
+ "unicode": "25B6",
+ "digest": "348627b8e0f55cf1e9ab19c9de1d170371b2c4cb4dda9a2aa8e0c558db08b18a"
+ },
+ {
+ "name": "arrow_heading_down",
+ "unicode": "2935",
+ "digest": "96c64953fc3134711247bef320f252c48993ebc90494925b7fee42ffce2a2ec2"
+ },
+ {
+ "name": "arrow_heading_up",
+ "unicode": "2934",
+ "digest": "94f94e74176cc050703b3584f3f700debf86e4e61b893a441825a21fa3f8ce74"
+ },
+ {
+ "name": "arrow_left",
+ "unicode": "2B05",
+ "digest": "4553be62a63d7550deac4f7dbeffce6006f769ae6cddfb8c795671672011ba0b"
+ },
+ {
+ "name": "arrow_lower_left",
+ "unicode": "2199",
+ "digest": "10f83c252110d705cdcfebc35a70c341ad288730d0c0729479e3a96e263d5120"
+ },
+ {
+ "name": "arrow_lower_right",
+ "unicode": "2198",
+ "digest": "ee33abd4c96c19e9b80a2fc1500ba8ecaa6668c49310cc816a496e8c61af3850"
+ },
+ {
+ "name": "arrow_right",
+ "unicode": "27A1",
+ "digest": "2611e9138a2651916f414015d0287f5f0af266514d96a42915d32b04fb652a90"
+ },
+ {
+ "name": "arrow_right_hook",
+ "unicode": "21AA",
+ "digest": "628b06384a2963a4fe81e9fbf4e22511f697878d9b9db7d2fc98f8aadbe8f4f9"
+ },
+ {
+ "name": "arrow_up",
+ "unicode": "2B06",
+ "digest": "c09e5f41c01028b45707c525d30d3d6731ec57b7447f0d7ba4ad6c1404449e5c"
+ },
+ {
+ "name": "arrow_up_down",
+ "unicode": "2195",
+ "digest": "e7fd92d24a01702f76c7fcc0de998bc81fbfb93711d076984f6da91d1dccd84c"
+ },
+ {
+ "name": "arrow_up_small",
+ "unicode": "1F53C",
+ "digest": "bc48dad74bc1d0c5579cbf5e3d005314b0d21bc5b5ebbba2b05136e33f49296d"
+ },
+ {
+ "name": "arrow_upper_left",
+ "unicode": "2196",
+ "digest": "792a9709f03843024e53d201cb4769c59b656c3bf0dff2306e8e605493a66b93"
+ },
+ {
+ "name": "arrow_upper_right",
+ "unicode": "2197",
+ "digest": "ee934b0c9cff270efd30a6cafc15253d405efd2c93b4785ac2ed4ea6420266a6"
+ },
+ {
+ "name": "arrows_clockwise",
+ "unicode": "1F503",
+ "digest": "914f4120513730d7a19c9f8c4e59223a90568de0b25a225b712b31fa9697ef4f"
+ },
+ {
+ "name": "arrows_counterclockwise",
+ "unicode": "1F504",
+ "digest": "86d87597e4e3db6dbba9907ee82412db0cbab1ea875bd0be6505dd886dc19b90"
+ },
+ {
+ "name": "art",
+ "unicode": "1F3A8",
+ "digest": "dfc6b0da780199df86507d65b0499ba1706c266ae7badcb0e7fb5b85af7c9578"
+ },
+ {
+ "name": "articulated_lorry",
+ "unicode": "1F69B",
+ "digest": "4c4de240ebd175f7b53453eda4e51f2e57d0db2a98d317f804116e14e47cff1d"
+ },
+ {
+ "name": "ascending_notes",
+ "unicode": "1F39C",
+ "digest": "33432042771d456338dda5d98e49322d3600f2cc9049963480c7c38d9de1ef0a"
+ },
+ {
+ "name": "asterisk",
+ "unicode": "002A-20E3",
+ "digest": "0b7f27f545b616677c83d40ff957337477b2881459b4d3c839ae55e23797419f"
+ },
+ {
+ "name": "astonished",
+ "unicode": "1F632",
+ "digest": "58632b97e274ade5183752db2b3c5c4fe29effcd5a9720a8d01fa809b97023dc"
+ },
+ {
+ "name": "athletic_shoe",
+ "unicode": "1F45F",
+ "digest": "1fc55d85a4d6751f9e60467801b051d2fb3341bdcc33b8d3695d5143359edb43"
+ },
+ {
+ "name": "atm",
+ "unicode": "1F3E7",
+ "digest": "bf827ef6c349f5b6912d821457975a4720d1750529d907e94ece429b7a388d7e"
+ },
+ {
+ "name": "atom",
+ "unicode": "269B",
+ "digest": "cbce1725602efbb77a935cfae5407e4d75489ee988910296c7f6140665afc669"
+ },
+ {
+ "name": "b",
+ "unicode": "1F171",
+ "digest": "9116256b3189977e37f6da7ddedf82bb29b0358829a4e8718fd59e51d9b86b3c"
+ },
+ {
+ "name": "baby",
+ "unicode": "1F476",
+ "digest": "66596bea11015154e0b1752b85f349f4286c6643ee6f51ee5e60e0d625c4ae9a"
+ },
+ {
+ "name": "baby_bottle",
+ "unicode": "1F37C",
+ "digest": "ed42994b4a539b8bfeccde0f3c7e9c7f54d6696ff48ce7e48171bbab51002348"
+ },
+ {
+ "name": "baby_chick",
+ "unicode": "1F424",
+ "digest": "ea2cfa0e5c2cbff5fffdb52cc04dfe7872834bd7cfeaa45e0541b8faffcbd0e9"
+ },
+ {
+ "name": "baby_symbol",
+ "unicode": "1F6BC",
+ "digest": "65df04dff8739b86f7663ae9c0648927341f360a986655e109721b0e16013b75"
+ },
+ {
+ "name": "baby_tone1",
+ "unicode": "1F476-1F3FB",
+ "digest": "bc747527a2d723cf99ef3fc2539c19d29634c92ff417736982d3bf87d65d06eb"
+ },
+ {
+ "name": "baby_tone2",
+ "unicode": "1F476-1F3FC",
+ "digest": "b82bba7a666b7d070751726e54acc7fb8f96e2dfc09e9610d61cfd20947aef9c"
+ },
+ {
+ "name": "baby_tone3",
+ "unicode": "1F476-1F3FD",
+ "digest": "7f45dfd4ea2ae8515d419ffa13e7ee5c625b024b4e521ace5344c414bb929da0"
+ },
+ {
+ "name": "baby_tone4",
+ "unicode": "1F476-1F3FE",
+ "digest": "80b1854626616f15426649cc6415e4911a55c8f761422fe48a08af9e8ac6a7cb"
+ },
+ {
+ "name": "baby_tone5",
+ "unicode": "1F476-1F3FF",
+ "digest": "9f890804d19a61bee76a29644c818045dd96cf69d67cfbca2d11f4ad376b27da"
+ },
+ {
+ "name": "back",
+ "unicode": "1F519",
+ "digest": "1dc73947b8f56e033777ca3f747407923bd16b07e53a6c78b09950ca474b7e7a"
+ },
+ {
+ "name": "badminton",
+ "unicode": "1F3F8",
+ "digest": "3f95180c1175d0248ebf4b8650cf86566c39e0486d828078244080194c14d4fe"
+ },
+ {
+ "name": "baggage_claim",
+ "unicode": "1F6C4",
+ "digest": "7c1a69511aa2a93984d601da4d1cef1cb4cefbbf127b1486278da8c01345bbf3"
+ },
+ {
+ "name": "balloon",
+ "unicode": "1F388",
+ "digest": "a10c2b0865179cdbdef339494ec9b2a109451a356e53738d6a9dd43232500956"
+ },
+ {
+ "name": "ballot_box",
+ "unicode": "1F5F3",
+ "digest": "0455ea75612efe78354315b4c345953d2d559bb471d5b01c1adc1d6b74ed693a"
+ },
+ {
+ "name": "ballot_box_check",
+ "unicode": "1F5F9",
+ "digest": "fc3ba16c009d963a4a0ea20a348ac98eee3c4c18c481df19a5ada0d1de7fcc15"
+ },
+ {
+ "name": "ballot_box_with_check",
+ "unicode": "2611",
+ "digest": "5f5cec7fe462557d31e8d2b836534c1e76d546cc0061236fa2af3667972b84aa"
+ },
+ {
+ "name": "ballot_box_x",
+ "unicode": "1F5F5",
+ "digest": "861dcfc2361298262587b5d0e163fed96a55c44636361f5b4a9ab1d6502b8928"
+ },
+ {
+ "name": "ballot_x",
+ "unicode": "1F5F4",
+ "digest": "0b73b89847eb82bcad5664644c8af237e0aef6c3d8c94b7a5df94e05d0ebf4e1"
+ },
+ {
+ "name": "bamboo",
+ "unicode": "1F38D",
+ "digest": "feb0cf2f1012a1c0649b8c66f7e96e2d8bcdefe879c5a52dab3e25c51009e3b2"
+ },
+ {
+ "name": "banana",
+ "unicode": "1F34C",
+ "digest": "aa9a1e6db00efa94a7f414c570eff7fc29011be64031a24d03b7f37b617cfd2d"
+ },
+ {
+ "name": "bangbang",
+ "unicode": "203C",
+ "digest": "bdd350766ccd1c0138f6294f7ebfa3e9867b02bda40a743f7062e52c68358765"
+ },
+ {
+ "name": "bank",
+ "unicode": "1F3E6",
+ "digest": "c9648c93049cf8e7884242e58ae3145383d2e5034c9090e0d34c53f5bbce397f"
+ },
+ {
+ "name": "bar_chart",
+ "unicode": "1F4CA",
+ "digest": "942277f72a5b754b13454dab62c85b1ff3447544f38ec76a285f3be32f6f5d12"
+ },
+ {
+ "name": "barber",
+ "unicode": "1F488",
+ "digest": "e1526eea685aafc56fb83d07f8ff63c9967600e447b0e5f831a17d6153f2062d"
+ },
+ {
+ "name": "baseball",
+ "unicode": "26BE",
+ "digest": "3d028b16a898f3a15874bc9d3891f9fbf59ea1c226c5c774eddb58a712c489ae"
+ },
+ {
+ "name": "basketball",
+ "unicode": "1F3C0",
+ "digest": "b2f5a3904d505db066337a24fc840ef75b49ef4c5f152227d8e632ff82285b12"
+ },
+ {
+ "name": "basketball_player",
+ "unicode": "26F9",
+ "digest": "e94beb69f631667479a80095bf313ceb3aa109d6ebb80f182722360a6d2a214e"
+ },
+ {
+ "name": "basketball_player_tone1",
+ "unicode": "26F9-1F3FB",
+ "digest": "6fc77cf2f26ee18e9a3faea500d4277839f77633f31ee618a68c301f1ad32d90"
+ },
+ {
+ "name": "basketball_player_tone2",
+ "unicode": "26F9-1F3FC",
+ "digest": "6ee9060c24d92708e12a854fb0bdf5c717c90b8c0350d8aa40c278b41bfa12fc"
+ },
+ {
+ "name": "basketball_player_tone3",
+ "unicode": "26F9-1F3FD",
+ "digest": "752e90dbfa7c7a9ae3f37de924e22f3c3d5a7e54dd41c8e8eb99cabb0dad73cf"
+ },
+ {
+ "name": "basketball_player_tone4",
+ "unicode": "26F9-1F3FE",
+ "digest": "38bedc3074e6243454d568d9b665f5764f1a3d983875651ce7a1cdb53da9f6c8"
+ },
+ {
+ "name": "basketball_player_tone5",
+ "unicode": "26F9-1F3FF",
+ "digest": "25ee1e84670d3db96d3ad098c859abd6b3448f55f668ce0c195ee2337a215de7"
+ },
+ {
+ "name": "bath",
+ "unicode": "1F6C0",
+ "digest": "ae6301a6354630cd9dc06a5137f23f826d019c8298b2b012b6ff31b773a910b6"
+ },
+ {
+ "name": "bath_tone1",
+ "unicode": "1F6C0-1F3FB",
+ "digest": "fce7ae2e7ef3f7f44f36c2ad49348b4cf7fce0b0c17e1a90a1e85734cee95b2a"
+ },
+ {
+ "name": "bath_tone2",
+ "unicode": "1F6C0-1F3FC",
+ "digest": "4d1c9444f16467488fe939fdad279d6855d28be564e5dcc1990451c4b9ae8c95"
+ },
+ {
+ "name": "bath_tone3",
+ "unicode": "1F6C0-1F3FD",
+ "digest": "9a59a4360effb48af4cbb1a953655ef61e69375407038b4d0bd8068fbaf3cc16"
+ },
+ {
+ "name": "bath_tone4",
+ "unicode": "1F6C0-1F3FE",
+ "digest": "01aafa8a53a08018b9fbf28ec6b3b918d6bd0dee7a891196f32f81f60d114f0e"
+ },
+ {
+ "name": "bath_tone5",
+ "unicode": "1F6C0-1F3FF",
+ "digest": "2733e81ccaee21231c2e47e3310b431e9bd784bf34f0db609f8eadcee359500d"
+ },
+ {
+ "name": "bathtub",
+ "unicode": "1F6C1",
+ "digest": "9515e3bb9ab41350305e64fc6877aae82d51e1ba8ce8b2b4b8ffaeda960820cd"
+ },
+ {
+ "name": "battery",
+ "unicode": "1F50B",
+ "digest": "7d4d475c1d5b1be55c319953e3363ff864fe4fcd921a8aa649b9a547c0894deb"
+ },
+ {
+ "name": "beach",
+ "unicode": "1F3D6",
+ "digest": "52855d75cfa4476ccc23c58b4afcb76ee48abb22a9a6081210c8accefdf33099"
+ },
+ {
+ "name": "beach_umbrella",
+ "unicode": "26F1",
+ "digest": "cefe8e195d21d3e0769d3bfe15170db9e57c86db9d31cacb19fcdc8d2191b661"
+ },
+ {
+ "name": "bear",
+ "unicode": "1F43B",
+ "digest": "b5ac126875c20c82b9e3140b143233944a2e4132d781d0b575e83673988523cb"
+ },
+ {
+ "name": "bed",
+ "unicode": "1F6CF",
+ "digest": "1919245d7a76799aad0533eb72db2cbaa1f32ee8231a0c1989d3f233f2d42370"
+ },
+ {
+ "name": "bee",
+ "unicode": "1F41D",
+ "digest": "69ada63403c8dabae39c63ba143143aeb59b66faae6aa82d8342337925a9e6b5"
+ },
+ {
+ "name": "beer",
+ "unicode": "1F37A",
+ "digest": "b71dd6efdb4ce7d9d71fdbf82a2ccf83841fb0cceb119ee7da1e575d3bfa853c"
+ },
+ {
+ "name": "beers",
+ "unicode": "1F37B",
+ "digest": "994108cebfe0c614c05967af4e3864d8adbbfcf7cccef1cbd42a47b7dfabf80c"
+ },
+ {
+ "name": "beetle",
+ "unicode": "1F41E",
+ "digest": "ec351ce238a81711eef00e5be1de2e198423cf524b60e531d435902b44420edc"
+ },
+ {
+ "name": "beginner",
+ "unicode": "1F530",
+ "digest": "13288d9fc221dc02f4181b998104e13c3c5c98d3c4e650186bef59a46d39f6f0"
+ },
+ {
+ "name": "bell",
+ "unicode": "1F514",
+ "digest": "784b9a82814ce14a264e54b3a8f8e706f3c7b763646d9f8174c4aa84ad41ef09"
+ },
+ {
+ "name": "bellhop",
+ "unicode": "1F6CE",
+ "digest": "c15455f1b52ac26404b5c13a0e1070212ed1830026422873f4f6335e26e31259"
+ },
+ {
+ "name": "bento",
+ "unicode": "1F371",
+ "digest": "d59314b17a8646d4a78fefb7b79f289f33d4aaea893fed4cad0b890df63395e7"
+ },
+ {
+ "name": "bicyclist",
+ "unicode": "1F6B4",
+ "digest": "e7359d615d40325bb08a145cfebde2ecef448deeb21695a34b55d3ccb971447f"
+ },
+ {
+ "name": "bicyclist_tone1",
+ "unicode": "1F6B4-1F3FB",
+ "digest": "e45808faa32f4ffb881d3569c0b8e2c69d4a64665f4d1fae24d7a1e5f1d3ea4b"
+ },
+ {
+ "name": "bicyclist_tone2",
+ "unicode": "1F6B4-1F3FC",
+ "digest": "92a3494270d1da6a117e92402c7898d4a7fffbe3d6143fb9ae445c4827c0c8a4"
+ },
+ {
+ "name": "bicyclist_tone3",
+ "unicode": "1F6B4-1F3FD",
+ "digest": "6fdf1db2bbd08d06b643b08f0f29daeaa20e0b8c8abec21132191f435cc05e42"
+ },
+ {
+ "name": "bicyclist_tone4",
+ "unicode": "1F6B4-1F3FE",
+ "digest": "d9c27848e1bcc8197c858e1ef12a537f4ed6c77fb211b6731388dc88c2bb7a61"
+ },
+ {
+ "name": "bicyclist_tone5",
+ "unicode": "1F6B4-1F3FF",
+ "digest": "4892af1a8a0229a813d7b8e3d88481c2365e3e1a5ce2e0e27ce432c5336da810"
+ },
+ {
+ "name": "bike",
+ "unicode": "1F6B2",
+ "digest": "e726f97b5432f46ed51328c0930d1d63b3a2d7b67c5c2303a5ca997083cfcac1"
+ },
+ {
+ "name": "bikini",
+ "unicode": "1F459",
+ "digest": "7612fcb72c005ae7172260825f588d6995f2bc919cb3d283dd4591f6872a1855"
+ },
+ {
+ "name": "biohazard",
+ "unicode": "2623",
+ "digest": "81f8309318051255ed4dc18855a3cd3f8657a6f3b2d368caa531a57ce0e34235"
+ },
+ {
+ "name": "bird",
+ "unicode": "1F426",
+ "digest": "3f219e5aa18e2f1febfd368ec133786cd2eab357db79984cb8ba07fed0eec7cd"
+ },
+ {
+ "name": "birthday",
+ "unicode": "1F382",
+ "digest": "9eb1adb0170ab851042cb3da8b64f02f4e4b63e7a07db405b55b50f5bbd3cacf"
+ },
+ {
+ "name": "black_circle",
+ "unicode": "26AB",
+ "digest": "c2ba672994ad0f99d7fdc449f3fee45a2dca68a58f9fe95825b38465a30ef44e"
+ },
+ {
+ "name": "black_joker",
+ "unicode": "1F0CF",
+ "digest": "1eb85b8e2b93dec221a97a1c309dee3683408f6166e1a1a1bd83cf2f64f007dd"
+ },
+ {
+ "name": "black_large_square",
+ "unicode": "2B1B",
+ "digest": "0ff2112227c38ed8c30b0bddf2300e87d2a244cd7fe81886a1cb1a287a7e8bb6"
+ },
+ {
+ "name": "black_medium_small_square",
+ "unicode": "25FE",
+ "digest": "f1010aa694084ad4655a9d4ce5a1711eaab21029e31bf8798253f0ad644e8abb"
+ },
+ {
+ "name": "black_medium_square",
+ "unicode": "25FC",
+ "digest": "06bf48ffbc84e71bbb90aa0f6c3f9f53533c6fd063ff168cefdb0a050dcf8302"
+ },
+ {
+ "name": "black_nib",
+ "unicode": "2712",
+ "digest": "c1361df4a5ae9f2ed121d26928021e96c6865331861e1960700d39cb1bd49355"
+ },
+ {
+ "name": "black_small_square",
+ "unicode": "25AA",
+ "digest": "d430ec419869fa1b5ba980ddeecb4c5ad5050a2b3421e45048cc184a6fc46899"
+ },
+ {
+ "name": "black_square_button",
+ "unicode": "1F532",
+ "digest": "85b6587b6b2c3544ddb7bc07207b0740e437744ba134835836153899ae396135"
+ },
+ {
+ "name": "blossom",
+ "unicode": "1F33C",
+ "digest": "029bbe385e07e2017dd918d685e107678c9c0e919a3bd1521b7a0d7c9172da05"
+ },
+ {
+ "name": "blowfish",
+ "unicode": "1F421",
+ "digest": "b5ee9f6ffabb74e3024067f016d17a631ee98536cb9c7269d55fa867f95a54fb"
+ },
+ {
+ "name": "blue_book",
+ "unicode": "1F4D8",
+ "digest": "6fbf227fb9facc1957bb9dfb31749cbfe66c3afe8081347f2471fd64ef2e6b3a"
+ },
+ {
+ "name": "blue_car",
+ "unicode": "1F699",
+ "digest": "e61ef2299d11fc01e9d6c496d188a7211633946706f6e771c412368346ca16f4"
+ },
+ {
+ "name": "blue_heart",
+ "unicode": "1F499",
+ "digest": "1af8d04173e0a984360786f6031220000dd548b8c912a68fd51f2ba490a9e16a"
+ },
+ {
+ "name": "blush",
+ "unicode": "1F60A",
+ "digest": "d615cda0f7c185ed8a92008204043ef769f3b7fb5424d595aeaaf3827bcdbd73"
+ },
+ {
+ "name": "boar",
+ "unicode": "1F417",
+ "digest": "c23a06db0337597e361ae581eacd4faf9926c6b7db0510d3599eb2e2a73315cb"
+ },
+ {
+ "name": "bomb",
+ "unicode": "1F4A3",
+ "digest": "0099e7435eba35f4f3ad273993293693a8b5cd110567c95ed83e5b4e2d0978ff"
+ },
+ {
+ "name": "book",
+ "unicode": "1F4D6",
+ "digest": "152408f2ff9949b7cbe57f623e4f875aa8dd0b02317e03cc914e1ea3712b3fc7"
+ },
+ {
+ "name": "book2",
+ "unicode": "1F56E",
+ "digest": "26d6b66a1957e7750b3e22eb2e46d0cc85932977bbb81d3d8482ec1ec58ee12b"
+ },
+ {
+ "name": "bookmark",
+ "unicode": "1F516",
+ "digest": "a2e0c6f5466c1b2fc148b20f6afcf4a878f4df55b0181f61fffa3ff727dcb251"
+ },
+ {
+ "name": "bookmark_tabs",
+ "unicode": "1F4D1",
+ "digest": "16135d62ff440722bd1ce8f84219be6a5eb3120a1597bfda4aeed4a2d9e7d7b2"
+ },
+ {
+ "name": "books",
+ "unicode": "1F4DA",
+ "digest": "ba019e4174639440caec424b30dfa016fe71a6f7436fe63025a2e3609ebfc012"
+ },
+ {
+ "name": "boom",
+ "unicode": "1F4A5",
+ "digest": "ec26246935c99749950612d69c06435ccdc126f14426a48a7599c5b6b91d9d58"
+ },
+ {
+ "name": "boot",
+ "unicode": "1F462",
+ "digest": "7ed639d52e285b0f46064dd4e1f4a8fb5814e1b2dc47c6f93cb349a6ac7ea97a"
+ },
+ {
+ "name": "bouquet",
+ "unicode": "1F490",
+ "digest": "b699f13af218560344f3571436f87b6f8c5c9f0fa0308836937667241b3fc7aa"
+ },
+ {
+ "name": "bouquet2",
+ "unicode": "1F395",
+ "digest": "1643ec51ff26fc1ac0c67859e202386398650bf2a996c82b68e1b73fa52abf7d"
+ },
+ {
+ "name": "bow",
+ "unicode": "1F647",
+ "digest": "5e260c38cfc80cd2f20ef78d982126dbf90934f7afa12c96d0b7b413beb6d4e0"
+ },
+ {
+ "name": "bow_and_arrow",
+ "unicode": "1F3F9",
+ "digest": "1c23469256331ea4ff03c036f89f0e63ad3228c51faecba50129da99b7eaddf3"
+ },
+ {
+ "name": "bow_tone1",
+ "unicode": "1F647-1F3FB",
+ "digest": "d3ec7ef70b355ba310d6fae7130a4e4cd11526b6e219474b5678a2b3ba1077f0"
+ },
+ {
+ "name": "bow_tone2",
+ "unicode": "1F647-1F3FC",
+ "digest": "c2905c0feba15fbc533cc6b36038eeda30f729182aa544f1d9164f5ccfed64d5"
+ },
+ {
+ "name": "bow_tone3",
+ "unicode": "1F647-1F3FD",
+ "digest": "298fc646d96c307eaa137c80b403d8355539ed8af13d3954a4ccacef67d341fa"
+ },
+ {
+ "name": "bow_tone4",
+ "unicode": "1F647-1F3FE",
+ "digest": "27db8401aa62a2544b24ff839b332958b5e8c3ab3fd7a289d3c62c654705da60"
+ },
+ {
+ "name": "bow_tone5",
+ "unicode": "1F647-1F3FF",
+ "digest": "168cdf834edb54723cf1c32311d4117c288132c5f76d6c415726c7484158c52a"
+ },
+ {
+ "name": "bowling",
+ "unicode": "1F3B3",
+ "digest": "0e888bcd1a5cc1ea7b07cea255ccb04dcdc87b0337b74cdc96a708aad7975768"
+ },
+ {
+ "name": "boy",
+ "unicode": "1F466",
+ "digest": "f349ab3e1015b4ccda5faab6a355f9c38e36e7c1cd667084563a14a2b11036ea"
+ },
+ {
+ "name": "boy_tone1",
+ "unicode": "1F466-1F3FB",
+ "digest": "4d04a5e45c9f9749de580321a212e14304b4ffcd229fa971fb59d97e6124262f"
+ },
+ {
+ "name": "boy_tone2",
+ "unicode": "1F466-1F3FC",
+ "digest": "0c9d6b6b1b3da68b9ef1f0f01efa4d170a48cfc66de4f577f8669c160b81cc97"
+ },
+ {
+ "name": "boy_tone3",
+ "unicode": "1F466-1F3FD",
+ "digest": "7dbecace78edb2aceffce6cb4d49ca132b93d80c26a8f1526a18832a2f23454a"
+ },
+ {
+ "name": "boy_tone4",
+ "unicode": "1F466-1F3FE",
+ "digest": "49f9c633afa8ff81068c78717e0012f8936fb3dcdb8b57342410f57f0635ae7c"
+ },
+ {
+ "name": "boy_tone5",
+ "unicode": "1F466-1F3FF",
+ "digest": "17e2ec379c7b542e6c2c5deef992af5f1fbaa3e288d1f71c8c984fb91a698cd4"
+ },
+ {
+ "name": "boys_symbol",
+ "unicode": "1F6C9",
+ "digest": "47fadbcb876ca436264ce2f3ebd1472bd68f55cc2b4833bf054335be9dc7a0f2"
+ },
+ {
+ "name": "bread",
+ "unicode": "1F35E",
+ "digest": "43697495538bfed11ed75213af8b1bdc14ef359d9b472cd7f9130fcb0a198680"
+ },
+ {
+ "name": "bride_with_veil",
+ "unicode": "1F470",
+ "digest": "37e75fbb2b0d06c900d51269b99107c60b61453dbf218b54df3011a455cd6dc3"
+ },
+ {
+ "name": "bride_with_veil_tone1",
+ "unicode": "1F470-1F3FB",
+ "digest": "44072e54e0618d2675a5bfd6572108590e51e8e733381e091e8754ee96c2cf20"
+ },
+ {
+ "name": "bride_with_veil_tone2",
+ "unicode": "1F470-1F3FC",
+ "digest": "f0acd961e108db9d9dd5d1b06e708b2eb6a7ef7235d6c8678b9319077faf4fa8"
+ },
+ {
+ "name": "bride_with_veil_tone3",
+ "unicode": "1F470-1F3FD",
+ "digest": "3f7adddb41ead3cd07098799ab2a5b8e8842344307d9045264403fb685f20555"
+ },
+ {
+ "name": "bride_with_veil_tone4",
+ "unicode": "1F470-1F3FE",
+ "digest": "5f7199fd99319651f3a7b3553cc5387c59b65cac1eb020441e19b5c12c807dc7"
+ },
+ {
+ "name": "bride_with_veil_tone5",
+ "unicode": "1F470-1F3FF",
+ "digest": "4b1f6c33dd72a3a11c764bb00e7be7441b39c7af78aae52141276a279d63ab78"
+ },
+ {
+ "name": "bridge_at_night",
+ "unicode": "1F309",
+ "digest": "f81cc36de8edbdf3fe4d55932d5c6c8ad429487ec1f7af044611b6dc950ee09c"
+ },
+ {
+ "name": "briefcase",
+ "unicode": "1F4BC",
+ "digest": "a3c3e802191f3e131683dac1fcd81e294dea72af8e65c94972990924c79c5619"
+ },
+ {
+ "name": "broken_heart",
+ "unicode": "1F494",
+ "digest": "4dee349274c2ea44d1c0395cbd39356b88897b0c45040aa40d8cb2607ee67420"
+ },
+ {
+ "name": "bug",
+ "unicode": "1F41B",
+ "digest": "bac4660ee8dcbef0023691804ee3fad3ea3d4bac20d847a5913cee6e7dca826c"
+ },
+ {
+ "name": "bulb",
+ "unicode": "1F4A1",
+ "digest": "af5394230f95781c7eb8054b1a13732a6e6170318599c79e9ca2a816a5b821a2"
+ },
+ {
+ "name": "bullettrain_front",
+ "unicode": "1F685",
+ "digest": "59afcd289500bd4148b1b91f560a5ce8ac9e1b52eddb8fec857ff5d171f017fb"
+ },
+ {
+ "name": "bullettrain_side",
+ "unicode": "1F684",
+ "digest": "79ff8f579081a2f1c3b05311a18ca432adb026a7860875cea4a5460e49b2a474"
+ },
+ {
+ "name": "bullhorn",
+ "unicode": "1F56B",
+ "digest": "a4ca5cbfe299e8ccd148d17055d2d395cf8515e416bf771044c9a670509a8254"
+ },
+ {
+ "name": "bullhorn_waves",
+ "unicode": "1F56C",
+ "digest": "92493636cf086205d1e12cc19e613b84152ef10b8cd0215619a0fc813bfc9a7c"
+ },
+ {
+ "name": "burrito",
+ "unicode": "1F32F",
+ "digest": "4babb1af1136ab2334d26495b0be779d0bcc9516fd956fc07ffde427d11122f0"
+ },
+ {
+ "name": "bus",
+ "unicode": "1F68C",
+ "digest": "476e7a5e92f64038e5012205395efead51f1c10b3edb25380f38da97e2412edd"
+ },
+ {
+ "name": "busstop",
+ "unicode": "1F68F",
+ "digest": "3bcf82872ab6abb0278238c71bd004a40c46696bdda05f54c153d45d6fe88f15"
+ },
+ {
+ "name": "bust_in_silhouette",
+ "unicode": "1F464",
+ "digest": "2230844993ab011fe2756a1aa3873ff7d5f7d888bddec408ba0b32e4f6003570"
+ },
+ {
+ "name": "busts_in_silhouette",
+ "unicode": "1F465",
+ "digest": "d1c3cb6d437616834425a53621c0bc0a6b368d745dd9da2300a3db4543d57660"
+ },
+ {
+ "name": "cactus",
+ "unicode": "1F335",
+ "digest": "e87588e6548d201db903dc0523b3ccc83c6b559981d743eae1504ce668cd8be4"
+ },
+ {
+ "name": "cake",
+ "unicode": "1F370",
+ "digest": "3947783d128018f5e396602d0492cb5c31e8e8df98af01eda7cade71aea8d989"
+ },
+ {
+ "name": "calculator",
+ "unicode": "1F5A9",
+ "digest": "01b47b5c69c12b65fa4f4c0d580f2a98280d6116f4ad2cf8be378759008bcc3c"
+ },
+ {
+ "name": "calendar",
+ "unicode": "1F4C6",
+ "digest": "00bb700dd88efbc43bc64263491cdf77965130b1dc23f31e682905c3dfe4040c"
+ },
+ {
+ "name": "calendar_spiral",
+ "unicode": "1F5D3",
+ "digest": "1dd5da98bb435c0c3f632bc0a5c9fdde694de7aee752bf4bb85def086e788a2a"
+ },
+ {
+ "name": "calling",
+ "unicode": "1F4F2",
+ "digest": "2375828085f2efd17b8a5ebb3cfec1e420190913328a7a0dd9ff0f67c7249ffb"
+ },
+ {
+ "name": "camel",
+ "unicode": "1F42B",
+ "digest": "9ff789ab50b51cd9e7fdc7fbe8d6f913fda95dfd425949f97974548652a53ce1"
+ },
+ {
+ "name": "camera",
+ "unicode": "1F4F7",
+ "digest": "d95192b9ba0f566d8874099125def031e15297d1306989ea9b6a49f7b9b56661"
+ },
+ {
+ "name": "camera_with_flash",
+ "unicode": "1F4F8",
+ "digest": "4db6fb3fdb9a004537dff97f4197c7ed87c9c978ba9ac562ed8bb7c1fa260d38"
+ },
+ {
+ "name": "camping",
+ "unicode": "1F3D5",
+ "digest": "f0855dc78bf6f3d06b3c2fc19180c8ff23d9e22871658fcc26a8fde08d328a0a"
+ },
+ {
+ "name": "cancellation_x",
+ "unicode": "1F5D9",
+ "digest": "cea2f7a48543207615ee06755ded62c2a95a7eaf7d7b68a3fc25e74d94e2c92c"
+ },
+ {
+ "name": "cancer",
+ "unicode": "264B",
+ "digest": "b990f85e9f62017d99526244eaef5c5e56f8808698011e85d44de1d2ed87f1a2"
+ },
+ {
+ "name": "candle",
+ "unicode": "1F56F",
+ "digest": "5eefd555951e65298583009a307acc6fb6d02c88325ef3adf231717e75e5a333"
+ },
+ {
+ "name": "candy",
+ "unicode": "1F36C",
+ "digest": "f14203c408173fbb94b4ee69d6de67226a17dc51b0cbd776f62623ee03fd2eb3"
+ },
+ {
+ "name": "capital_abcd",
+ "unicode": "1F520",
+ "digest": "2a7cc876218b8c244b9802448ee25ce5004671a4f00ea950a636d8c3b766dbef"
+ },
+ {
+ "name": "capricorn",
+ "unicode": "2651",
+ "digest": "03a5fd064c10f47c7fd0ae318c573bb559c269b1b2d61b45aa5b8ce9b5fbd9df"
+ },
+ {
+ "name": "card_box",
+ "unicode": "1F5C3",
+ "digest": "7d760ae1d44e6f4b2aac00895ca86b5743f8b5ca157ec2bd21ce2665e50ad23a"
+ },
+ {
+ "name": "card_index",
+ "unicode": "1F4C7",
+ "digest": "150950903eccb468981c58b87ed7c1ba44e17f52627d695f660ce96b3d9d6e8e"
+ },
+ {
+ "name": "carousel_horse",
+ "unicode": "1F3A0",
+ "digest": "d6862085550fa139a147dceb1b2b9f950a08dcd01cecd8b8697f9c7992ca054e"
+ },
+ {
+ "name": "cartridge",
+ "unicode": "1F5AD",
+ "digest": "0b1625eea118060b51a70905c1eb3313ed632e989f70943eca16aa29fe8a34f2"
+ },
+ {
+ "name": "cat",
+ "unicode": "1F431",
+ "digest": "002208c0c9165971853ee05cd05513175a913376a462a345a939d73401c6acb7"
+ },
+ {
+ "name": "cat2",
+ "unicode": "1F408",
+ "digest": "fbdb726cc035f83784dcfe2d9adb85f8aeec429064aed5c5ca0b8be406068aa5"
+ },
+ {
+ "name": "cd",
+ "unicode": "1F4BF",
+ "digest": "bd4d4eef2cc0b1e4ee1f5280f922743e76f27d35836987801b2b48969eac17d8"
+ },
+ {
+ "name": "celtic_cross",
+ "unicode": "1F548",
+ "digest": "187aac988d7e02085a15f31c4cc0ff25127be5b088e354e65c7b1152bffb40ff"
+ },
+ {
+ "name": "chains",
+ "unicode": "26D3",
+ "digest": "a6a915d9c361e1564e13cf2d33ad5df3d684aa349b8dc5909e6343d67401beb9"
+ },
+ {
+ "name": "champagne",
+ "unicode": "1F37E",
+ "digest": "77395d3afe5cc10bfdc381120bae2ae4aefdaa96c529536413873a696c5fa713"
+ },
+ {
+ "name": "chart",
+ "unicode": "1F4B9",
+ "digest": "9fd5f8cd99988bbe0fabc89a0b23e28d1468641d2f9468e82b7148a1948d8236"
+ },
+ {
+ "name": "chart_with_downwards_trend",
+ "unicode": "1F4C9",
+ "digest": "6fe456d76c0a996c12049057b5d60129098a9deddfa2d133cff5c4400e4595a0"
+ },
+ {
+ "name": "chart_with_upwards_trend",
+ "unicode": "1F4C8",
+ "digest": "e83cc4cf4228bd77e030a19755b11cf75cf671f40973c23e240afa54d9de478e"
+ },
+ {
+ "name": "checkered_flag",
+ "unicode": "1F3C1",
+ "digest": "77501c2c66af31f72f5c05f21e87598cd59740b5cfc02926c66dc755bab3c3cf"
+ },
+ {
+ "name": "cheese",
+ "unicode": "1F9C0",
+ "digest": "5897036ba97b557868bb314fcee83b9d8a609c8447b270a0b3d34a29ce7496d1"
+ },
+ {
+ "name": "cherries",
+ "unicode": "1F352",
+ "digest": "5a0ba73039e4b56e3d16a1c70ad992f41af7a16f6d5ba4b5337bdf338276f0ff"
+ },
+ {
+ "name": "cherry_blossom",
+ "unicode": "1F338",
+ "digest": "b40533225291f539ffe97e4ab1d70d07e179b2f9345b2814355164d0407cf3bf"
+ },
+ {
+ "name": "chestnut",
+ "unicode": "1F330",
+ "digest": "6a2a37899d28326daf36965b343b2646492c2c0cee8871321cc17315d6252a9a"
+ },
+ {
+ "name": "chicken",
+ "unicode": "1F414",
+ "digest": "13d770684a11ea10c0ae7570a98c5dfafd4bfb78ac3f72f46729aef9060b85c0"
+ },
+ {
+ "name": "children_crossing",
+ "unicode": "1F6B8",
+ "digest": "654d2502c1edc57c5ab4237df76db3121f6b8735eb13d30bffd305605a083445"
+ },
+ {
+ "name": "chipmunk",
+ "unicode": "1F43F",
+ "digest": "1ae3c838450afcbbe8a96992481dde252e343ab83546d0789ebed81a78ca9188"
+ },
+ {
+ "name": "chocolate_bar",
+ "unicode": "1F36B",
+ "digest": "2486b7265048eb2294d6be0a0a8a4d6067df95721ace9d131d8f715a27ba8cf0"
+ },
+ {
+ "name": "christmas_tree",
+ "unicode": "1F384",
+ "digest": "454c08870eaa84283c19731ed3b10c4868d2e2f0cc44f2feba0de9ba4cc9c4e1"
+ },
+ {
+ "name": "church",
+ "unicode": "26EA",
+ "digest": "b62e838ffb0dfefeced1707359437b6815e0721783b549212282e08617402f6f"
+ },
+ {
+ "name": "cinema",
+ "unicode": "1F3A6",
+ "digest": "6df56f6a0008d0352740d1e045ffdb702e80c2a6d88b6db1a8bcd27eb3c12dcc"
+ },
+ {
+ "name": "circus_tent",
+ "unicode": "1F3AA",
+ "digest": "f8b7a7f4cf4f9efd20423acc30abb3a28e2a5183b3e39f5cc88e7e0ed7757d64"
+ },
+ {
+ "name": "city_dusk",
+ "unicode": "1F306",
+ "digest": "8779066dc9386d05c951b1df1753983c2937a5f3b84d5fc09ed0b172d4ef914e"
+ },
+ {
+ "name": "city_sunset",
+ "unicode": "1F307",
+ "digest": "c2530d12204eb518c5a3c8d7deba11170b1412fdf406aea05a69d4c026210d1b"
+ },
+ {
+ "name": "cityscape",
+ "unicode": "1F3D9",
+ "digest": "15251a708d50fc721bd67d8abb2a517c0bade196df3b736e21d79191d749241f"
+ },
+ {
+ "name": "cl",
+ "unicode": "1F191",
+ "digest": "104591d8e7b980cf38dcf8326d36c845384b7a4e6d94c49f36e9946484712a95"
+ },
+ {
+ "name": "clap",
+ "unicode": "1F44F",
+ "digest": "ed6ef8bb78ca1fa295b87222c440c6d5ba4f154f2752bf0d428941260d66aaac"
+ },
+ {
+ "name": "clap_tone1",
+ "unicode": "1F44F-1F3FB",
+ "digest": "57a1fd1fa2578c30b8a47abb84e81af5f5bbc6c301a5daf0c53d4d07b017e777"
+ },
+ {
+ "name": "clap_tone2",
+ "unicode": "1F44F-1F3FC",
+ "digest": "2ad4dcd513e55486f21151bf3792e1febf116574d238545b07b4290901430fdd"
+ },
+ {
+ "name": "clap_tone3",
+ "unicode": "1F44F-1F3FD",
+ "digest": "2d8c705d4fcc162fb65cd51e2c6683f1129ebc72fba13343533f64ede1c62687"
+ },
+ {
+ "name": "clap_tone4",
+ "unicode": "1F44F-1F3FE",
+ "digest": "40ffd41b2b4f59d0040e9d20497e57c4e47f18aeae43fcae02be5c2f50069102"
+ },
+ {
+ "name": "clap_tone5",
+ "unicode": "1F44F-1F3FF",
+ "digest": "be55df1ac7600ba086c2ef6ea223ebc62271fa47876c53ade1a1c0151fdc994c"
+ },
+ {
+ "name": "clapper",
+ "unicode": "1F3AC",
+ "digest": "a8748398f56fd2c1e6e87fe0c77edec444df7c7dd462d43dbcea6d8de97c81c5"
+ },
+ {
+ "name": "classical_building",
+ "unicode": "1F3DB",
+ "digest": "6a607b0666141b51d6e944b04f3f6188a5c026396e6105f1d2a5e6b6350cd66b"
+ },
+ {
+ "name": "clipboard",
+ "unicode": "1F4CB",
+ "digest": "4ca1a0b864a962b111d6bdb65373b779f3fff571ffd32d029666f9b708e1ab73"
+ },
+ {
+ "name": "clock",
+ "unicode": "1F570",
+ "digest": "c48314ccde8bf01acc2b1bc9a6b5aa7d796fc0c8769f80398bc74545fcef31ed"
+ },
+ {
+ "name": "clock1",
+ "unicode": "1F550",
+ "digest": "c0550fa0c385920cbdb775bdaaa5e812097a484c4a32e35ebbafe3a364a4a438"
+ },
+ {
+ "name": "clock10",
+ "unicode": "1F559",
+ "digest": "25651ac5520505f326457364428de3679cc22ca57278d4c54cc4b60420fa7b74"
+ },
+ {
+ "name": "clock1030",
+ "unicode": "1F565",
+ "digest": "dbf682bac968fc5a3959af2b96eaaa5ee78306f6341c43c1345b94bc561a3d04"
+ },
+ {
+ "name": "clock11",
+ "unicode": "1F55A",
+ "digest": "333732dd6c3184f257964bcf5a20a6111f9adb04560b5d12dc613636e846df5b"
+ },
+ {
+ "name": "clock1130",
+ "unicode": "1F566",
+ "digest": "005999cb37998adea1645d7df63b2705a42db3b4f1a734891d79af3e833764ff"
+ },
+ {
+ "name": "clock12",
+ "unicode": "1F55B",
+ "digest": "6690e591bec1751e1c5472e0bf52f66779b2113e5b8c6c578e65dbb83d091b16"
+ },
+ {
+ "name": "clock1230",
+ "unicode": "1F567",
+ "digest": "549f3921bcff7f330c5a41e6756d8c15601f1f8278b35b369148771c60be2a6f"
+ },
+ {
+ "name": "clock130",
+ "unicode": "1F55C",
+ "digest": "9332ef07a9dde8ccaa1e58a3e97edee0601a1152fc6d351b782816c838d2a408"
+ },
+ {
+ "name": "clock2",
+ "unicode": "1F551",
+ "digest": "9d1ec8fbdae627880e1c067c10d6a40f1e4494a246c77224b3cd7b287554c4b4"
+ },
+ {
+ "name": "clock230",
+ "unicode": "1F55D",
+ "digest": "3578a39c28695d4e617a648a1eb44e0bb5a8a11dcbe04fa2eb2aea0a60589067"
+ },
+ {
+ "name": "clock3",
+ "unicode": "1F552",
+ "digest": "c2e2a27301b6ac27dc359be590448eb1e65fe87211f1af30a473d8bde4f3db47"
+ },
+ {
+ "name": "clock330",
+ "unicode": "1F55E",
+ "digest": "7a77cf8cf9a98f4767a2dca1d3795be45938eee185db81120d85cedebe128899"
+ },
+ {
+ "name": "clock4",
+ "unicode": "1F553",
+ "digest": "0945c4199400d546350cfff25bc9e9160789d1cf9890b3318bdc462ac6cc9782"
+ },
+ {
+ "name": "clock430",
+ "unicode": "1F55F",
+ "digest": "9fdb6f1fa076c4c6a395dbf6db27499ee447b3558f3aa64d913686c360e428a8"
+ },
+ {
+ "name": "clock5",
+ "unicode": "1F554",
+ "digest": "855b3500eb6d20bb6e51d3a6c9d1a5131c06404c6c149841c7cca52201036428"
+ },
+ {
+ "name": "clock530",
+ "unicode": "1F560",
+ "digest": "a6ebd9f884d45a1f43650351a1f1da9724bc044d7da2f6d99ffb3d1fa0c31c5d"
+ },
+ {
+ "name": "clock6",
+ "unicode": "1F555",
+ "digest": "e38f9fc4f87f12ee602dcf2285d59dbc343fc0fc37662992cfe9866c20f58e87"
+ },
+ {
+ "name": "clock630",
+ "unicode": "1F561",
+ "digest": "735954a650791fc38c845c43998023e652d36e55534850e43952878b8804b2f1"
+ },
+ {
+ "name": "clock7",
+ "unicode": "1F556",
+ "digest": "2c4244ec4019e9624e6ea5a751bb735ab87bead33b1ea160265c81bba3c2f736"
+ },
+ {
+ "name": "clock730",
+ "unicode": "1F562",
+ "digest": "0bcf20e30be1bb23394696770301867e307f8e5014e0ed7d75ed96efe34d625d"
+ },
+ {
+ "name": "clock8",
+ "unicode": "1F557",
+ "digest": "af454047a1765ef1c8355969302a826d4c47f5c61a6ec47fdec3510a8003b0d8"
+ },
+ {
+ "name": "clock830",
+ "unicode": "1F563",
+ "digest": "e48b81dac055dc6d5f7832cf34368329c573d03b35bfe076fed1c6e6d48a82e7"
+ },
+ {
+ "name": "clock9",
+ "unicode": "1F558",
+ "digest": "f2a3d1bc029dc0e6406cdaa96542e77503e4cfb79d99c69cb454b8cf635a73fc"
+ },
+ {
+ "name": "clock930",
+ "unicode": "1F564",
+ "digest": "bb1b2b83052e8e6fb97c48c13bce0d950907e044eb2dabf21d7fed321f75110b"
+ },
+ {
+ "name": "clockwise_arrows",
+ "unicode": "1F5D8",
+ "digest": "67027b7e1a4d800a3ce7d731c21c098d1109d217159a27665eebb7e080fc2622"
+ },
+ {
+ "name": "closed_book",
+ "unicode": "1F4D5",
+ "digest": "afd6dae5fa0f59330fc2adb922e92b3410a33a80a2667651718c7dac588010bc"
+ },
+ {
+ "name": "closed_lock_with_key",
+ "unicode": "1F510",
+ "digest": "d0ed5c00f939111ce86f9c741b733b22e04ebbd871aa33da3eb0f46a6f38b707"
+ },
+ {
+ "name": "closed_umbrella",
+ "unicode": "1F302",
+ "digest": "3ef08b299f9170007a5433fe82d0953bf0f75b6685d0ce58972f9af032dc471a"
+ },
+ {
+ "name": "cloud",
+ "unicode": "2601",
+ "digest": "d1e7932551e85c6e86bfb3b41f0c936a6d0953bf9f9119b8cca3eaed22ac0c01"
+ },
+ {
+ "name": "cloud_lightning",
+ "unicode": "1F329",
+ "digest": "fc9c85cc95f9c456635692c974f72b6d40e14943824b8129a21c47265c3416f4"
+ },
+ {
+ "name": "cloud_rain",
+ "unicode": "1F327",
+ "digest": "f4406e62ed98f6141ab70736f6d5c540023e805396db0346ee6b7082c3f5e8e2"
+ },
+ {
+ "name": "cloud_snow",
+ "unicode": "1F328",
+ "digest": "948990cd13dd927917208c026089519fcf8e258a8a284684ace67c9a2f9a8149"
+ },
+ {
+ "name": "cloud_tornado",
+ "unicode": "1F32A",
+ "digest": "44753516d0bd05d47cfa6eb922aba570ba6a87f805f325772b2cff071460ead1"
+ },
+ {
+ "name": "clubs",
+ "unicode": "2663",
+ "digest": "5fd19fadd3b0887a6a59819ffbbe33a061055c043200700c31be30e14a5d36d5"
+ },
+ {
+ "name": "cocktail",
+ "unicode": "1F378",
+ "digest": "cf096ebe15b4053702d490cd96f04d565b4993529bcd6d8d50cb821200d1cd92"
+ },
+ {
+ "name": "coffee",
+ "unicode": "2615",
+ "digest": "6ea6128e353d9f74aee99caaaaa30c53f996fb242bf3bffb0fa92e6b4d373e57"
+ },
+ {
+ "name": "coffin",
+ "unicode": "26B0",
+ "digest": "b59772d7aa262c4d7433f9cdf76d50011f4c63421b730c8ab4a08675f730c39f"
+ },
+ {
+ "name": "cold_sweat",
+ "unicode": "1F630",
+ "digest": "f0d0057bf01db8d930f6e4632c5bf8d0b1bc709bcfb6463a1f1973b5f1d70a83"
+ },
+ {
+ "name": "comet",
+ "unicode": "2604",
+ "digest": "00252ec55d1846d95c8d4c704b35251232d9810029fc215a7da08262dd1f3541"
+ },
+ {
+ "name": "compression",
+ "unicode": "1F5DC",
+ "digest": "432fbe66e5e3c38ebfeb4eb03465667a1e1be868b4afe510ec95eadda6481bde"
+ },
+ {
+ "name": "computer",
+ "unicode": "1F4BB",
+ "digest": "99777be010488867c7872b2e235be7c35b1a6f28d92baa921b61ced5491c0257"
+ },
+ {
+ "name": "computer_old",
+ "unicode": "1F5B3",
+ "digest": "b27c30d74f205a8a3bd00a55ca17da7cf6ae3b65ae33e949755a4c6bd69a9fd3"
+ },
+ {
+ "name": "confetti_ball",
+ "unicode": "1F38A",
+ "digest": "e77d0c0970d3d12e123e548639fc0fa3ce41668667e4be55baefc09dfaa22cb0"
+ },
+ {
+ "name": "confounded",
+ "unicode": "1F616",
+ "digest": "0f51db64149151d3d7ae5dce08c9af3d064123524fa36fe1f51a78cbd966b6ea"
+ },
+ {
+ "name": "confused",
+ "unicode": "1F615",
+ "digest": "ed23587432c1be98356156784ca4fe0b374b7b3b371660d45cfb0a1efd44e322"
+ },
+ {
+ "name": "congratulations",
+ "unicode": "3297",
+ "digest": "2a46d640bf24fd4dc7649baf4b28c4adb30eda8d24d70eda07036c85b48195e0"
+ },
+ {
+ "name": "construction",
+ "unicode": "1F6A7",
+ "digest": "73fac9fb5eb91954b0f998f9d05fb953241eed988c134fa42477393159fa34fa"
+ },
+ {
+ "name": "construction_site",
+ "unicode": "1F3D7",
+ "digest": "0ff52e6adf1927d356b27be5fef6bad2ad842be05e3a0bd16a17efe78e5676d9"
+ },
+ {
+ "name": "construction_worker",
+ "unicode": "1F477",
+ "digest": "2be436fa7ad0a31e328fc6f776044bd1eec35c99541ced891792e3bef738d0a0"
+ },
+ {
+ "name": "construction_worker_tone1",
+ "unicode": "1F477-1F3FB",
+ "digest": "172cebc84f91237a85292c5ab0a105cc3abbb96e7423c4ae81feffd00bdb3b26"
+ },
+ {
+ "name": "construction_worker_tone2",
+ "unicode": "1F477-1F3FC",
+ "digest": "3e9b96ddfd639eefda99ad3a0ad26a28a0f2c8be72988c2bdbd648e6104638b6"
+ },
+ {
+ "name": "construction_worker_tone3",
+ "unicode": "1F477-1F3FD",
+ "digest": "11f83c565168dce5ac2387b873769d85ec4087171d6e92fc766c209ea06cd4f3"
+ },
+ {
+ "name": "construction_worker_tone4",
+ "unicode": "1F477-1F3FE",
+ "digest": "09e320e78e3a2940f0c5a0ef9a235ab72c51e053fd8ff433843fdb62571c8e70"
+ },
+ {
+ "name": "construction_worker_tone5",
+ "unicode": "1F477-1F3FF",
+ "digest": "7ac2a1a0038e7aefea889380be604a98255823587e90799165f7db39dd03a0cc"
+ },
+ {
+ "name": "control_knobs",
+ "unicode": "1F39B",
+ "digest": "9f10e578b410ff6aa7cc7fe806a0f1181893765303c0ca3867b652f1392a8a22"
+ },
+ {
+ "name": "contruction_site",
+ "unicode": "1F3D7",
+ "digest": "0ff52e6adf1927d356b27be5fef6bad2ad842be05e3a0bd16a17efe78e5676d9"
+ },
+ {
+ "name": "convenience_store",
+ "unicode": "1F3EA",
+ "digest": "1ff4351e4a4503f58ed5d35074a2112c681337e35ffe55332187481685573606"
+ },
+ {
+ "name": "cookie",
+ "unicode": "1F36A",
+ "digest": "5c78ce2e721b0a3767d6ce0b59c1e88fdf94a7edc94e98c4d6b7aadb5b2aeea7"
+ },
+ {
+ "name": "cool",
+ "unicode": "1F192",
+ "digest": "54a96697a5070388ce8364a5ee2e0d78a53acc8b4f6755b1359fd67252cc41e8"
+ },
+ {
+ "name": "cop",
+ "unicode": "1F46E",
+ "digest": "16bee252c2a133bcf57f6d7b8372a61364744a2f662acb90e2005732555135fa"
+ },
+ {
+ "name": "cop_tone1",
+ "unicode": "1F46E-1F3FB",
+ "digest": "2fc52f3ed735e327d12dadb15f9feb7b7f720fc6857b551548a2a84809053817"
+ },
+ {
+ "name": "cop_tone2",
+ "unicode": "1F46E-1F3FC",
+ "digest": "6208f3174ced4f07ba3820ba838b247d7438d69d86eb04927333e7436e56af7e"
+ },
+ {
+ "name": "cop_tone3",
+ "unicode": "1F46E-1F3FD",
+ "digest": "2427d30bdfe127be4d8c3870472cae191eece142c784a5c2809df938f43e7c53"
+ },
+ {
+ "name": "cop_tone4",
+ "unicode": "1F46E-1F3FE",
+ "digest": "6e73f8abdf816f3cb2728b971a5a8d006a236c1d71b2ee1788ab60329f406323"
+ },
+ {
+ "name": "cop_tone5",
+ "unicode": "1F46E-1F3FF",
+ "digest": "4b146465cc95ade7e9ca722e31a1b06311214dae8f7f4d95c6329d56c45b451f"
+ },
+ {
+ "name": "copyright",
+ "unicode": "00A9",
+ "digest": "8143583821085dfc8ac21079fe220288ba3a3b6ca3014dc5dc98b18da77589c1"
+ },
+ {
+ "name": "corn",
+ "unicode": "1F33D",
+ "digest": "0160502226b5f9af81763545f288dbbb20632039d7509f347c751cfdb49dc5b5"
+ },
+ {
+ "name": "couch",
+ "unicode": "1F6CB",
+ "digest": "a93fffed194b404200495abda8772bb35539cfc8499eb0a9bf09c508afad6676"
+ },
+ {
+ "name": "couple",
+ "unicode": "1F46B",
+ "digest": "97fe611a613216a1788f9bd88a9deb4714ee123a66b5fd3d0ac916fbb4da7304"
+ },
+ {
+ "name": "couple_mm",
+ "unicode": "1F468-2764-1F468",
+ "digest": "3ae6fbf3ba168256ea85c756ac1e7b83fdb8b780d33f06128ed80706ff627eea"
+ },
+ {
+ "name": "couple_with_heart",
+ "unicode": "1F491",
+ "digest": "d9701173a5e8dff052ab6a15a42494dbb61dc7146d3734c82916abc9c05f76db"
+ },
+ {
+ "name": "couple_ww",
+ "unicode": "1F469-2764-1F469",
+ "digest": "d2a2ec29c1a1234ea0aa1d9fc6707cf8be8bb36ea8b92523ffa1c3071bcf0b06"
+ },
+ {
+ "name": "couplekiss",
+ "unicode": "1F48F",
+ "digest": "e722730de82397da7c8f88d79319b391e8f01fbe4a9133850cc92ad34e77bd82"
+ },
+ {
+ "name": "cow",
+ "unicode": "1F42E",
+ "digest": "dcc1efef2f02588806a156ed43da959c587d4c576ff6badec77f820ed3ba507f"
+ },
+ {
+ "name": "cow2",
+ "unicode": "1F404",
+ "digest": "dcf59f92fd0a37b2ca720bcda606defa4357b58d8f4ad15c1288ad8d814b2bc7"
+ },
+ {
+ "name": "crab",
+ "unicode": "1F980",
+ "digest": "59d34a4e92326ebeab188d9e33b25c20f4d54d187c274713fa3256b03b9e662a"
+ },
+ {
+ "name": "crayon",
+ "unicode": "1F58D",
+ "digest": "0f3351c2e68a8d47d27b45a9901be6160de0f9a291bd8680df84d0fc679bcb31"
+ },
+ {
+ "name": "credit_card",
+ "unicode": "1F4B3",
+ "digest": "708c0e7008e06e5d1b3b4e68a7e0ada9f4ae22ab6c28285d81a340f913fd9a84"
+ },
+ {
+ "name": "crescent_moon",
+ "unicode": "1F319",
+ "digest": "0959f838a410e8bfeebf00aa9658df56e515dbd2361142021071e17244662bfc"
+ },
+ {
+ "name": "cricket",
+ "unicode": "1F3CF",
+ "digest": "00eb11254e887c71db5e8945ad211e9e0280f1e02f4b77a4799b64bba2bbe9b3"
+ },
+ {
+ "name": "crocodile",
+ "unicode": "1F40A",
+ "digest": "99abcb42264d40d2450aaca8c3759a019bfd600a311cf3027243f1ca200d4639"
+ },
+ {
+ "name": "cross",
+ "unicode": "271D",
+ "digest": "a6e3c345cf6aa2ce690b66454066b53ef5b1dab2ed635e21f1586b1dffc5df42"
+ },
+ {
+ "name": "cross_heavy",
+ "unicode": "1F547",
+ "digest": "2e37c26b9bad0beb019c7f3e7a3892352d0ad9ca1b90c4333d42e8d56680be70"
+ },
+ {
+ "name": "cross_white",
+ "unicode": "1F546",
+ "digest": "3452e667010d7e49a51d7e1f4ba8ed4f303e33ed43255a051e9a18832a1efba6"
+ },
+ {
+ "name": "crossbones",
+ "unicode": "1F571",
+ "digest": "f5e7ce293c1a3282711073e68f033a3876e8428d1218cb2f8294630f9124e584"
+ },
+ {
+ "name": "crossed_flags",
+ "unicode": "1F38C",
+ "digest": "d4da057db289bec83f0106a94c89bd0cd9b52c7c7f8bc69bc8cbce480d53e12b"
+ },
+ {
+ "name": "crossed_swords",
+ "unicode": "2694",
+ "digest": "f159978583fa77c73ba6de85d35c4195cbd55963e537bd2bfd8f98ab8ff3559a"
+ },
+ {
+ "name": "crown",
+ "unicode": "1F451",
+ "digest": "e6fe2a28b7d80749ca121cabbe89321dcecdd760a122e73fb1562ea9bb40e90d"
+ },
+ {
+ "name": "cruise_ship",
+ "unicode": "1F6F3",
+ "digest": "90519c46ddfb63e71bc76661953da9041e5f0b97e9f8a7a8696518b4d529f3dd"
+ },
+ {
+ "name": "cry",
+ "unicode": "1F622",
+ "digest": "2d6a096796222c29b050f74db6b5aff9b9f61390c5eb56e45d1801918751002f"
+ },
+ {
+ "name": "crying_cat_face",
+ "unicode": "1F63F",
+ "digest": "df057d4e3e5c5c87caedf87ea3a6f936811b93f228f46bb7018d2bb5afaa6d35"
+ },
+ {
+ "name": "crystal_ball",
+ "unicode": "1F52E",
+ "digest": "7de438f88134c32c4db67d705e5fecf2a6187a87f56ebbb5bcc5ba09626e2935"
+ },
+ {
+ "name": "cupid",
+ "unicode": "1F498",
+ "digest": "7cb3f7d1ddf9678982197ef0e65735fb465ae8e3652d611f37d3bcccf4d7e2c1"
+ },
+ {
+ "name": "curly_loop",
+ "unicode": "27B0",
+ "digest": "881a43ae406cb74b2ef136bf970db9928bcdc3bbbb7393e90d2c597fe1dd9a96"
+ },
+ {
+ "name": "currency_exchange",
+ "unicode": "1F4B1",
+ "digest": "c4d76e9e61fac8d3c0cb9e07f1fbf1a7fcac6f4d4c78776ff7f04fc9391ce689"
+ },
+ {
+ "name": "curry",
+ "unicode": "1F35B",
+ "digest": "ebe41ee864c873e3a371888c0087b11dbcb124335812895002ed81fe2b6ba571"
+ },
+ {
+ "name": "custard",
+ "unicode": "1F36E",
+ "digest": "afc192f405c30e2d529ec0f4b31a7faf474bcd01fded5294dc38880b8bb22155"
+ },
+ {
+ "name": "customs",
+ "unicode": "1F6C3",
+ "digest": "5abb98151a79cebc1032c0ea149617093e42f41e50574a790a91074cabaa4c3a"
+ },
+ {
+ "name": "cyclone",
+ "unicode": "1F300",
+ "digest": "ae77e15bf2f312f03dbc5c7813d304005bbb549953482db9beb91810c585dc0e"
+ },
+ {
+ "name": "dagger",
+ "unicode": "1F5E1",
+ "digest": "377060a7ce930566a4732b361be98e8a193a546846dfbba2a00abeeef41d1976"
+ },
+ {
+ "name": "dancer",
+ "unicode": "1F483",
+ "digest": "e050db55afbb968e02219a58c7e82b824848d299a4df64f0d08d4e1872816203"
+ },
+ {
+ "name": "dancer_tone1",
+ "unicode": "1F483-1F3FB",
+ "digest": "350f6b2e4589fdd436173163035621b8da0bd49c7b9ec9f39593aae5e0ed0641"
+ },
+ {
+ "name": "dancer_tone2",
+ "unicode": "1F483-1F3FC",
+ "digest": "a9efc84ec80582f286147ca34162a27fd5989f4030084acdbc309d4368660f5b"
+ },
+ {
+ "name": "dancer_tone3",
+ "unicode": "1F483-1F3FD",
+ "digest": "ef187f44278fdb8605c80f5cf199e0b3de8a49085dada2e215bb91e1d7d3be5d"
+ },
+ {
+ "name": "dancer_tone4",
+ "unicode": "1F483-1F3FE",
+ "digest": "5195bc352dc9d24cc5505a167c756038e55c05048c61799ea1bfdf2debe44ac2"
+ },
+ {
+ "name": "dancer_tone5",
+ "unicode": "1F483-1F3FF",
+ "digest": "55cb7eee9fa11a16a3932800a19e334546f7396df6aadde22e58fe3185926b16"
+ },
+ {
+ "name": "dancers",
+ "unicode": "1F46F",
+ "digest": "39e7dfd9dafeee20f2968960b1179ee4bf3f2b63a3035fc1944024d0ae8b5de1"
+ },
+ {
+ "name": "dango",
+ "unicode": "1F361",
+ "digest": "2a1b50abe5dc72335344878d9b701028ccad651964d9e3affeedbf3c2bfd652a"
+ },
+ {
+ "name": "dark_sunglasses",
+ "unicode": "1F576",
+ "digest": "6bb1e911a93d5eb0581d3ce8f8929125d3d8fc04e086f3263cfd25af1348ce6c"
+ },
+ {
+ "name": "dart",
+ "unicode": "1F3AF",
+ "digest": "6f28741543a4c1eead21856128ffea1fcf772954fe6af40844dfde47f092ed32"
+ },
+ {
+ "name": "dash",
+ "unicode": "1F4A8",
+ "digest": "25aef37611f1c2f2e96518bf8aeba80580dca9634c8505d390c147388adf6746"
+ },
+ {
+ "name": "date",
+ "unicode": "1F4C5",
+ "digest": "de591b8fad608be761b839beefe9e4c2316320bcf0c44c543a1bc4b89923d938"
+ },
+ {
+ "name": "deciduous_tree",
+ "unicode": "1F333",
+ "digest": "ff31a52096ac1eae770f7f71b6d802198add2c8b4d9d7c9327071b6d6ab86c7b"
+ },
+ {
+ "name": "department_store",
+ "unicode": "1F3EC",
+ "digest": "c1e200d5fdd792121acabdb17bbcfe8e28a63757cfd895c72d4909f14de95ac2"
+ },
+ {
+ "name": "descending_notes",
+ "unicode": "1F39D",
+ "digest": "f09c6a2e094b13bf91cc07b7b776e43348ccef9f91247ca36cc02e7d91098af0"
+ },
+ {
+ "name": "desert",
+ "unicode": "1F3DC",
+ "digest": "e45815250bfc5411de516f87efa218874bcda4b0420b4c17182efc22ba0ce80d"
+ },
+ {
+ "name": "desktop",
+ "unicode": "1F5A5",
+ "digest": "ba46323e695918e7253f1013cb991efb09790581c74c07c38bc5e10a20b8e8de"
+ },
+ {
+ "name": "desktop_window",
+ "unicode": "1F5D4",
+ "digest": "d5b6c4a847e2a96f97f50fd353a22cb121915cb1d7bbc0f02df38769819b6b7e"
+ },
+ {
+ "name": "diamond_shape_with_a_dot_inside",
+ "unicode": "1F4A0",
+ "digest": "4e0e6364b8682dec9a9e20676161c9c9c0faf0a5fdd5402ca2668b18f2bb850a"
+ },
+ {
+ "name": "diamonds",
+ "unicode": "2666",
+ "digest": "42b13b2ed8e5fc63fbe81263c06cc203ba18a45ed5cc2a4fdbf617d219a0d3b4"
+ },
+ {
+ "name": "disappointed",
+ "unicode": "1F61E",
+ "digest": "7f1a619fef84960a9f312d17a58aa58105a4f20a4072efb10227892ab22475d8"
+ },
+ {
+ "name": "disappointed_relieved",
+ "unicode": "1F625",
+ "digest": "a389f5e0a4b619dbc406217967fb1f8f3d0e49b3f790e554ae0ececadbf98967"
+ },
+ {
+ "name": "dividers",
+ "unicode": "1F5C2",
+ "digest": "bf4c303452a4c0b4986925041dbec5b7e478060d560630b7c5bc2f997fcad668"
+ },
+ {
+ "name": "dizzy",
+ "unicode": "1F4AB",
+ "digest": "d6fba9b906f0eabd46686e416273a2ca6634249374385f2abf7ed284f0eef995"
+ },
+ {
+ "name": "dizzy_face",
+ "unicode": "1F635",
+ "digest": "b55e20c1551a2912bb5ec64a66c788c9d6f21594cc1da66032188f3814b03f40"
+ },
+ {
+ "name": "do_not_litter",
+ "unicode": "1F6AF",
+ "digest": "126f8c4085e0a8de8241f211f96c3f42c3e3400ea7d8fdf79a14443c3eceb972"
+ },
+ {
+ "name": "document",
+ "unicode": "1F5CE",
+ "digest": "2cbca96cc69306a10f1a9b6505723e027239439d899f6b395dc43f3c37d2d777"
+ },
+ {
+ "name": "document_text",
+ "unicode": "1F5B9",
+ "digest": "29407b12409c9673f3d89ef1f86ee50cbc7ed53b1870e33b4a29bbc609017f72"
+ },
+ {
+ "name": "dog",
+ "unicode": "1F436",
+ "digest": "c7b729de8a0967b1f38c3fa5ded94e77e329588caeaaf43abfd1090f420e62bf"
+ },
+ {
+ "name": "dog2",
+ "unicode": "1F415",
+ "digest": "e1897ca60bb3d2662cbe7933352e2b9c50739adf5901d3328797bf399575b97a"
+ },
+ {
+ "name": "dollar",
+ "unicode": "1F4B5",
+ "digest": "7db1e57f799439df1295d42b5249393f1e8cacc8df54caf30499c967a7282742"
+ },
+ {
+ "name": "dolls",
+ "unicode": "1F38E",
+ "digest": "398e7ff5780328700aadded7ce8c50757b1096af5cec66cc4d813a6714686b6d"
+ },
+ {
+ "name": "dolphin",
+ "unicode": "1F42C",
+ "digest": "27385af08848d93acdd13f72751074c2cbccb5ab3c6047e334598af74ed4862d"
+ },
+ {
+ "name": "door",
+ "unicode": "1F6AA",
+ "digest": "3365d7834086328ecbf1da0037f1cf1d0eb49534e173f7962a9e8f4b2ab87e26"
+ },
+ {
+ "name": "doughnut",
+ "unicode": "1F369",
+ "digest": "b4b99fdfe8d07b49cbdd78f8c57e4424819a4ffc8a3ba4867da44cbb3b3a5cca"
+ },
+ {
+ "name": "dove",
+ "unicode": "1F54A",
+ "digest": "4e2e9c47e5632efe6ccf945d61dbc2f1155a2e52905e17f307b502a2c951bdb8"
+ },
+ {
+ "name": "dragon",
+ "unicode": "1F409",
+ "digest": "d7d016568b54d67017681a075fb799d4a2a790ecfa2946d02dbcee629eb4975d"
+ },
+ {
+ "name": "dragon_face",
+ "unicode": "1F432",
+ "digest": "4d0025f1df63b62448477a8f08a50704e15caafb10fea476b529113f41797ab9"
+ },
+ {
+ "name": "dress",
+ "unicode": "1F457",
+ "digest": "02d56ed227280eaf5ad92830ee304afb81f74bb5a13c855397bcd04dd7fa51fb"
+ },
+ {
+ "name": "dromedary_camel",
+ "unicode": "1F42A",
+ "digest": "5afe8a0b73f9f4560264020b1e02a566149dbc38c15a00d2fb5cd90b32d09a75"
+ },
+ {
+ "name": "droplet",
+ "unicode": "1F4A7",
+ "digest": "a92c419792cbd3ba90ed21547362134cfac3e17a5304ee4e3872c9f7b561f834"
+ },
+ {
+ "name": "dvd",
+ "unicode": "1F4C0",
+ "digest": "1ba23e2f01ced5e192e4c1d2f766d9bce400470e81c81410139fd3c0739422df"
+ },
+ {
+ "name": "e-mail",
+ "unicode": "1F4E7",
+ "digest": "12135310cfedc091d120426f5b132df82b538c5fcad458bf6b21588f353c3adb"
+ },
+ {
+ "name": "ear",
+ "unicode": "1F442",
+ "digest": "70ba1103a34e68590d91a3b6f8acdbad3b1c65e46e31e26ee1cb855c1e21095e"
+ },
+ {
+ "name": "ear_of_rice",
+ "unicode": "1F33E",
+ "digest": "ddd5f3cc83dbdafd9115861eecd0128e52165bb1dd0049df06ffc564b650d384"
+ },
+ {
+ "name": "ear_tone1",
+ "unicode": "1F442-1F3FB",
+ "digest": "72977be94f5d287a09d175f98fba8b7955ae13aa12ce8e029c0ca875c02ee820"
+ },
+ {
+ "name": "ear_tone2",
+ "unicode": "1F442-1F3FC",
+ "digest": "5ff2e46cb3be7f13b8b94092246b58dab4c2a9ee2a5a46e0b84cf35a6928141f"
+ },
+ {
+ "name": "ear_tone3",
+ "unicode": "1F442-1F3FD",
+ "digest": "19b523f5ada2acaea94b922059c458a3303f4da1dd4c197cf25d31a0e6ecc4b2"
+ },
+ {
+ "name": "ear_tone4",
+ "unicode": "1F442-1F3FE",
+ "digest": "6a5cca9f49c539ef7d0883a2f39652f33ee2d3b25dca0234e4ba027ebbb2b466"
+ },
+ {
+ "name": "ear_tone5",
+ "unicode": "1F442-1F3FF",
+ "digest": "a0a56e8abd36e9be6e2448bcee6f56ecb8bf62d728b19ab6e8f9c6338e226b67"
+ },
+ {
+ "name": "earth_africa",
+ "unicode": "1F30D",
+ "digest": "d4921b543d7cf0c7344fa50c5e4d5a76c208d900be852adc1ee82ed4e8861a39"
+ },
+ {
+ "name": "earth_americas",
+ "unicode": "1F30E",
+ "digest": "61691e6aa9b8d90fc7f75fbc6cc7add5c36022d38f3e05c9d7c54dc44cf865bb"
+ },
+ {
+ "name": "earth_asia",
+ "unicode": "1F30F",
+ "digest": "262904cb552c7f5cf828a11071b3d430a74824b7464e8759ef93ee23b1705767"
+ },
+ {
+ "name": "egg",
+ "unicode": "1F373",
+ "digest": "a7dd617cad489c481ffd14937d9ed491cdd5756903e00473f42600c2fbefb600"
+ },
+ {
+ "name": "eggplant",
+ "unicode": "1F346",
+ "digest": "e5402e8ae5b7f9699ed86b97c242f7939d5731c5a364a2d5b9d04ea5d293cda1"
+ },
+ {
+ "name": "eight",
+ "unicode": "0038-20E3",
+ "digest": "34e293d3228e4643a0132d592f96db91b651fe6ced056ac3c8a3fd49c5ed3416"
+ },
+ {
+ "name": "eight_pointed_black_star",
+ "unicode": "2734",
+ "digest": "c3c2da75731a9a0f4f0a8d1f9cffef75c35e19b7f5d4081da33ac12b46be5fc2"
+ },
+ {
+ "name": "eight_spoked_asterisk",
+ "unicode": "2733",
+ "digest": "cc69618c1074d2b00e6f2c49df5e2c5ff6f4c0fae305505eb8c9daa65a0ea340"
+ },
+ {
+ "name": "electric_plug",
+ "unicode": "1F50C",
+ "digest": "732e1d1675233a0b4643cb73d0c352f8a5a56a11ee90d26627ad1e43c2e4a8e5"
+ },
+ {
+ "name": "elephant",
+ "unicode": "1F418",
+ "digest": "08df3910c4d5d8f49a72c47dd938195e495bde8fd8b3e7b17098a2c1afc41634"
+ },
+ {
+ "name": "end",
+ "unicode": "1F51A",
+ "digest": "05844ab9dcb43deff86f04617af6ea09215595de1415dcfaae018bced57938fe"
+ },
+ {
+ "name": "envelope",
+ "unicode": "2709",
+ "digest": "aad272511d0db910437ba25cf1fb9c806d47aad92a232edb87055916daf4676a"
+ },
+ {
+ "name": "envelope_back",
+ "unicode": "1F582",
+ "digest": "bc60b6d375feee00758a94a05b42eeb165f4084b20eb3e6012b72faa221f7e75"
+ },
+ {
+ "name": "envelope_flying",
+ "unicode": "1F585",
+ "digest": "9d6b6ca4c08006062a6f11948de3e15b13cf5c458967e39a9358665a8e13e214"
+ },
+ {
+ "name": "envelope_stamped",
+ "unicode": "1F583",
+ "digest": "f6102aea7283ddc136bfeb09589573420b9279105045fc6b965c1633c1297468"
+ },
+ {
+ "name": "envelope_stamped_pen",
+ "unicode": "1F586",
+ "digest": "80ea471318d1e04f8e525ff236b3cd4a4c864e66c6246b6aad77d92f56895f33"
+ },
+ {
+ "name": "envelope_with_arrow",
+ "unicode": "1F4E9",
+ "digest": "c1ba19b5e7cf64c547ac46eee139e6af70700d49ab511a96e6828c30feb116bc"
+ },
+ {
+ "name": "euro",
+ "unicode": "1F4B6",
+ "digest": "f571952583ffecfa5777065e4d1b680c423d25bc80e567a48fb5d7a1c1b5e735"
+ },
+ {
+ "name": "european_castle",
+ "unicode": "1F3F0",
+ "digest": "db82e383975d079a7bb006e7868035088d75c33bd4031cf8466b71089b65426f"
+ },
+ {
+ "name": "european_post_office",
+ "unicode": "1F3E4",
+ "digest": "d9b38e0f0ca3ad8895b40c767bdbb2b142ccaf03a86c2f275f57a31ed478801a"
+ },
+ {
+ "name": "evergreen_tree",
+ "unicode": "1F332",
+ "digest": "60d8b2d86b20255341f7ecad6d0f178ba9db5fa6b3de92f1b439cdb19f2fc0b1"
+ },
+ {
+ "name": "exclamation",
+ "unicode": "2757",
+ "digest": "cd900ecf82de2b26f0d7783dac4b3232ae94d2cddad5bfacea2eaf65b7ac0a09"
+ },
+ {
+ "name": "expressionless",
+ "unicode": "1F611",
+ "digest": "2ec9466b2d629907ce4c3e24e57f7ee556d2258ff011d972e14d0ae969a40c51"
+ },
+ {
+ "name": "eye",
+ "unicode": "1F441",
+ "digest": "790841e8fce647173eec3c5019440ad9c7e916c535f92acb3132bd92df148cad"
+ },
+ {
+ "name": "eye_in_speech_bubble",
+ "unicode": "1F441-1F5E8",
+ "digest": "bcde5a89a7653bff302685d9d632dd2723796a7ac73125fb7b9493d1ca848e0a"
+ },
+ {
+ "name": "eyeglasses",
+ "unicode": "1F453",
+ "digest": "fd140bef19c420bafe59368d35dd58a58a53e7145b104bae94be10f90679213b"
+ },
+ {
+ "name": "eyes",
+ "unicode": "1F440",
+ "digest": "57ed1f87ebe2485ea32ea69abdb8c5f7ccdcc149b33e74230d801f0883c68c5d"
+ },
+ {
+ "name": "factory",
+ "unicode": "1F3ED",
+ "digest": "6e6b35ae013e5dd26852c9a95d05c39e89c1c1950a33f47e7b951c34af18f37c"
+ },
+ {
+ "name": "fallen_leaf",
+ "unicode": "1F342",
+ "digest": "28ba8628065ffa973b525dd1455691c828d49c2b8c814af387880c13f6707f7e"
+ },
+ {
+ "name": "family",
+ "unicode": "1F46A",
+ "digest": "b5307f86e54cfea581e8406f4b95c801e250a893a9d208cc9a69a6d910b90932"
+ },
+ {
+ "name": "family_mmb",
+ "unicode": "1F468-1F468-1F466",
+ "digest": "49a753c3fcd4420800dd1cda585dae6bfa81615ad4862b477246456f86dc9e82"
+ },
+ {
+ "name": "family_mmbb",
+ "unicode": "1F468-1F468-1F466-1F466",
+ "digest": "882a3a0048efd666b0ab3a07b9f08041aa3a2acdab02664d0feff30bbfa70d68"
+ },
+ {
+ "name": "family_mmg",
+ "unicode": "1F468-1F468-1F467",
+ "digest": "45dd75c19d260a658c8ac93cf878976b96d2000f0efc9c59e72dacc80afb08fa"
+ },
+ {
+ "name": "family_mmgb",
+ "unicode": "1F468-1F468-1F467-1F466",
+ "digest": "910f44a348a951d36ee1f1484d237085bec5083c3875a4d908831dfc64530eaf"
+ },
+ {
+ "name": "family_mmgg",
+ "unicode": "1F468-1F468-1F467-1F467",
+ "digest": "012e75ad0d1b16c2ce63bf80a1ebfb1fc194229cfaf1241039599b82832f6aee"
+ },
+ {
+ "name": "family_mwbb",
+ "unicode": "1F468-1F469-1F466-1F466",
+ "digest": "049a32f61c54f093d2124e25f8b2ec7eac13161e2f2ebf6dc067797698cbe831"
+ },
+ {
+ "name": "family_mwg",
+ "unicode": "1F468-1F469-1F467",
+ "digest": "ba32c637caba634bda99ccba2a1a2a4b6f33aaaed933c30c7d5a51e8de1790d0"
+ },
+ {
+ "name": "family_mwgb",
+ "unicode": "1F468-1F469-1F467-1F466",
+ "digest": "198faba987f45429329b93bbce4f111329f284558bf0eecfa1424186b5f009fe"
+ },
+ {
+ "name": "family_mwgg",
+ "unicode": "1F468-1F469-1F467-1F467",
+ "digest": "3fa2e57cba314dcff04cf8186914823e1e081aabf34fa7437b05c58015df400c"
+ },
+ {
+ "name": "family_wwb",
+ "unicode": "1F469-1F469-1F466",
+ "digest": "b9592fc110a25a478569075deaa520308ef74579cd47aa44df9836599d68143f"
+ },
+ {
+ "name": "family_wwbb",
+ "unicode": "1F469-1F469-1F466-1F466",
+ "digest": "88f398997835fcf5153f17f6baf0deeb2a9c25ce2f8422192c18ac23e90b3193"
+ },
+ {
+ "name": "family_wwg",
+ "unicode": "1F469-1F469-1F467",
+ "digest": "c8d859d3c957fe0d535efccde295fe99bab76e3d28ab5a49c8e736608461cb2e"
+ },
+ {
+ "name": "family_wwgb",
+ "unicode": "1F469-1F469-1F467-1F466",
+ "digest": "006506e4a3d0c82642a0c8481ce95e5e3b969e20fe2def0a16dd686afddbc705"
+ },
+ {
+ "name": "family_wwgg",
+ "unicode": "1F469-1F469-1F467-1F467",
+ "digest": "2553f0deab133aad09b99411d9dd68b56fede30f55ee1f354358767765e36673"
+ },
+ {
+ "name": "fast_forward",
+ "unicode": "23E9",
+ "digest": "1baaed10969b60c083da754ee056bb71df36182cc65af40640acfb76f6b39200"
+ },
+ {
+ "name": "fax",
+ "unicode": "1F4E0",
+ "digest": "b0a392192d03bd5d1ad5ee8eea933cf64725b1776819537bbed27561d78192e7"
+ },
+ {
+ "name": "fearful",
+ "unicode": "1F628",
+ "digest": "7c4cc4de3357c2a6d6e779342b09dabb3ef832a32f2778a0ba074b446f588e8f"
+ },
+ {
+ "name": "feet",
+ "unicode": "1F43E",
+ "digest": "cae13fb54ec64dbcf86ea25bebe2b79877e2d4f5d810b867f095f1d3dfc7f144"
+ },
+ {
+ "name": "ferris_wheel",
+ "unicode": "1F3A1",
+ "digest": "a710a8a0fb039d953313b75330db37e3228d856593547b1f04dc83c00168b987"
+ },
+ {
+ "name": "ferry",
+ "unicode": "26F4",
+ "digest": "21ea239b5adb68dc1ce6c5a1993b0a0b835ef6cc7a0a27cb890838d8475504f6"
+ },
+ {
+ "name": "field_hockey",
+ "unicode": "1F3D1",
+ "digest": "1e46c7f0b5b79c90a5d211ea14cd7e358b1a26a3c8294439253f2b08d0e5c92e"
+ },
+ {
+ "name": "file_cabinet",
+ "unicode": "1F5C4",
+ "digest": "c0b7bdab6c98909eb0fbf1ac89da0008bb00ddb1cb57fe64b4a5ac993eeb18c9"
+ },
+ {
+ "name": "file_folder",
+ "unicode": "1F4C1",
+ "digest": "d98f93c6d7283df0c45f08d3d31ecf5b91b6db1b735959f19e42bfada500a0d1"
+ },
+ {
+ "name": "film_frames",
+ "unicode": "1F39E",
+ "digest": "754a0a60e978f8299a0c4f8959e1f9260f01683e15ae943db430036f01a79b18"
+ },
+ {
+ "name": "finger_pointing_down",
+ "unicode": "1F597",
+ "digest": "0c542ac3141e8f2e74767acd0eb399c2d68c779cb78bf16d437ad3b1f8134ad9"
+ },
+ {
+ "name": "finger_pointing_down2",
+ "unicode": "1F59F",
+ "digest": "c5b128a232cbf518544802a2ae1459368274297163721fa05d0103cf95b2b1ee"
+ },
+ {
+ "name": "finger_pointing_left",
+ "unicode": "1F598",
+ "digest": "d178ece691e2091be08db77fda9cf05462934628557358a8cb6222587b291f7e"
+ },
+ {
+ "name": "finger_pointing_right",
+ "unicode": "1F599",
+ "digest": "a412a47544d8f401f9181f8826c5fa3d6b42a1d76f6926963c2d9cd2a01be06d"
+ },
+ {
+ "name": "finger_pointing_up",
+ "unicode": "1F59E",
+ "digest": "32c2ccab52aa318a47c816d1bcf9c076e667c9ef3e64ce37d7ba7e827238690d"
+ },
+ {
+ "name": "fire",
+ "unicode": "1F525",
+ "digest": "b44311874681135acbb5e7226febe4365c732da3a9617f10d7074a3b1ade1641"
+ },
+ {
+ "name": "fire_engine",
+ "unicode": "1F692",
+ "digest": "3ae03fa34a7088ada95458eb4ee3e97691b3489149f6bbc168086f0483ed3bb2"
+ },
+ {
+ "name": "fire_engine_oncoming",
+ "unicode": "1F6F1",
+ "digest": "e2482c450136d373f74dfafddf502e0b675eb5d2e1e1c645f163db0e4d15fbb6"
+ },
+ {
+ "name": "fireworks",
+ "unicode": "1F386",
+ "digest": "3dee83a27c406960253ca1460eb88a599c7b81506051b69605a421b17fe8282c"
+ },
+ {
+ "name": "first_quarter_moon",
+ "unicode": "1F313",
+ "digest": "8fa066362d77bd889090bbe0904ca47f34704e29781c67133c6eaa521c3e1972"
+ },
+ {
+ "name": "first_quarter_moon_with_face",
+ "unicode": "1F31B",
+ "digest": "8877edb366f8eaa00fd83200acf5a17c3b84d246a250519d565dda3aea866ec3"
+ },
+ {
+ "name": "fish",
+ "unicode": "1F41F",
+ "digest": "9ce742108794cc15e59f7719623ae938efbd8155c93ad72585a32f4e32ea9414"
+ },
+ {
+ "name": "fish_cake",
+ "unicode": "1F365",
+ "digest": "1b5b14509287e30da9b8d7abcec376b247f9095aea4bf3fc320349f061a4c321"
+ },
+ {
+ "name": "fishing_pole_and_fish",
+ "unicode": "1F3A3",
+ "digest": "35db56776db1fcec7c8479922d57d54da2577cfe44a894bfd78c51c950c450fb"
+ },
+ {
+ "name": "fist",
+ "unicode": "270A",
+ "digest": "6b80ac2e4d8b830ae06f7c1626d456460094e4ba20c20fb82dabb6b3d2ce7605"
+ },
+ {
+ "name": "fist_tone1",
+ "unicode": "270A-1F3FB",
+ "digest": "d7c79f4f988dd68f064baa5a3a568ab299f8d409db45c8463f39b80e5dd6081f"
+ },
+ {
+ "name": "fist_tone2",
+ "unicode": "270A-1F3FC",
+ "digest": "d1108194e2d962f9ccd00131876d769a8e003117a460d18b2ccbf93e0a0ea346"
+ },
+ {
+ "name": "fist_tone3",
+ "unicode": "270A-1F3FD",
+ "digest": "12f5644b632c95a5c2e41cc9af299e286e266db8b3860091ef5be5f0c4ccc026"
+ },
+ {
+ "name": "fist_tone4",
+ "unicode": "270A-1F3FE",
+ "digest": "521a3ac573381f3bc37a08ddd2d122767aaa0b6b7a38050d3671a12343351816"
+ },
+ {
+ "name": "fist_tone5",
+ "unicode": "270A-1F3FF",
+ "digest": "604e5a234da1b9160e506b3c9026faf9e04268fced7b44baa1ef5e3d4efa83a4"
+ },
+ {
+ "name": "five",
+ "unicode": "0035-20E3",
+ "digest": "0cbd6cd11eb6c2d67749112750d125f4f0a07b53bb7bfb1de0986d943ea9d632"
+ },
+ {
+ "name": "flag_ac",
+ "unicode": "1F1E6-1F1E8",
+ "digest": "d9db1edeb709824a1083c2bba79ca5f683ed0edded35918bb167d1ee7396c8da"
+ },
+ {
+ "name": "flag_ad",
+ "unicode": "1F1E6-1F1E9",
+ "digest": "04a8c1745d9b8b20e903302379f2557e8082f72e33878db4cb2cd6b33eb97952"
+ },
+ {
+ "name": "flag_ae",
+ "unicode": "1F1E6-1F1EA",
+ "digest": "868324ac2e7bea1547f5de95f39633b77b8d62f3b3433b3d1a4ee96d169a09cd"
+ },
+ {
+ "name": "flag_af",
+ "unicode": "1F1E6-1F1EB",
+ "digest": "9a94458519e9db5d6cf1557e54fdf62d7e48aaf7de25744a093ec8f284656226"
+ },
+ {
+ "name": "flag_ag",
+ "unicode": "1F1E6-1F1EC",
+ "digest": "ea59fabc2bd9024df06a59a34412f52bebfeb03eb6abd73d8fe153e3a68e28f4"
+ },
+ {
+ "name": "flag_ai",
+ "unicode": "1F1E6-1F1EE",
+ "digest": "75676ded736ad2ebb921e9fd8ebfef49819a35c3dcf005bbc3b7e8c6e75178f2"
+ },
+ {
+ "name": "flag_al",
+ "unicode": "1F1E6-1F1F1",
+ "digest": "77b835dcff399b609e2479cbf10f08344c8fc277370ba8e4540165ca15563847"
+ },
+ {
+ "name": "flag_am",
+ "unicode": "1F1E6-1F1F2",
+ "digest": "3b820c628dd5a93137f7288a43553778f60b0beea4c0a239d063893c0723e73d"
+ },
+ {
+ "name": "flag_ao",
+ "unicode": "1F1E6-1F1F4",
+ "digest": "d26439d4ecbe8b67bb1ae9753454505358ebb6b802624f19800471e53ee27187"
+ },
+ {
+ "name": "flag_aq",
+ "unicode": "1F1E6-1F1F6",
+ "digest": "6b0b4e800d88ab289ae4b6d449bfa115e92543958b477d13ad348468a74e4616"
+ },
+ {
+ "name": "flag_ar",
+ "unicode": "1F1E6-1F1F7",
+ "digest": "ca76db601dd3f5794f1caace8ab5641fe3786b86e4ae030706162f0ce07d27b3"
+ },
+ {
+ "name": "flag_as",
+ "unicode": "1F1E6-1F1F8",
+ "digest": "170e1dde0e3fd2e0f2149de5cc8845e15580cc0412e81a643d61bd387de16141"
+ },
+ {
+ "name": "flag_at",
+ "unicode": "1F1E6-1F1F9",
+ "digest": "0ab3675a16b4988e87c81e87453c160d6616c7be76247f54c471dc63aa8b42ba"
+ },
+ {
+ "name": "flag_au",
+ "unicode": "1F1E6-1F1FA",
+ "digest": "b6f17d3dfd3547c069a0b6cddd4cf44fb8ce1d1d300e24284fb292ac142537e3"
+ },
+ {
+ "name": "flag_aw",
+ "unicode": "1F1E6-1F1FC",
+ "digest": "7857bc907f04dfb7ccc4401c05034ad8afb6383a022db77973cfcafa4d6c16c8"
+ },
+ {
+ "name": "flag_ax",
+ "unicode": "1F1E6-1F1FD",
+ "digest": "ab8f1fd4af7c220a54d478cec5a9f7f3beb5fc83439c448f3ac9848af8391ac1"
+ },
+ {
+ "name": "flag_az",
+ "unicode": "1F1E6-1F1FF",
+ "digest": "187cc7b6d39800c5910a34409db1e6b1d8aac808c72a93e922a419d9b054fd0b"
+ },
+ {
+ "name": "flag_ba",
+ "unicode": "1F1E7-1F1E6",
+ "digest": "cd22c744213087384cf79ed314742026787212c9ceb6999ed166534670f7864a"
+ },
+ {
+ "name": "flag_bb",
+ "unicode": "1F1E7-1F1E7",
+ "digest": "44ff0a48ac2d2180374baa58b1b7c64f26d0d151a48811eb08ffa20758104512"
+ },
+ {
+ "name": "flag_bd",
+ "unicode": "1F1E7-1F1E9",
+ "digest": "c18793d2b963458607a0bab94c57e62c8278fce870e96fd8dda78067a8fbde18"
+ },
+ {
+ "name": "flag_be",
+ "unicode": "1F1E7-1F1EA",
+ "digest": "6e6ccfca064a43b93c8acc04a9425f95af204198022ca20b9ee6c491e99ad950"
+ },
+ {
+ "name": "flag_bf",
+ "unicode": "1F1E7-1F1EB",
+ "digest": "d69c0394a1c7cb6323f54f024b7d740c728f229ca5e1b54ac374d5024f5470a5"
+ },
+ {
+ "name": "flag_bg",
+ "unicode": "1F1E7-1F1EC",
+ "digest": "413a270caf4a9155e84bdba6c9512277f5642246f6ba8d701383a5eeb02f7e95"
+ },
+ {
+ "name": "flag_bh",
+ "unicode": "1F1E7-1F1ED",
+ "digest": "9243ed65d7f24c824c2a3207335a2d4ad25251258547c16d0b7b7cbb9df6f8de"
+ },
+ {
+ "name": "flag_bi",
+ "unicode": "1F1E7-1F1EE",
+ "digest": "63056519030524b2d2dcd47448267d817205dbd6b98075c97f011a8f1d4d1a4b"
+ },
+ {
+ "name": "flag_bj",
+ "unicode": "1F1E7-1F1EF",
+ "digest": "93b245eed85d22260d27d1a8c77f51fb3439309e09b2aeca6cd504dbea77b509"
+ },
+ {
+ "name": "flag_bl",
+ "unicode": "1F1E7-1F1F1",
+ "digest": "5e1e478deaf02bbaa26595e4cefc5f5c9bec6105ce521b7b9ab4fa5e7a452c14"
+ },
+ {
+ "name": "flag_black",
+ "unicode": "1F3F4",
+ "digest": "df131e5c28e9f51dea53fe7f33551f91d420f7d686b7a62980f0154c6b5357a6"
+ },
+ {
+ "name": "flag_bm",
+ "unicode": "1F1E7-1F1F2",
+ "digest": "9dcd9e60faebe7f93eb19157e99f2ad654a8145c61738de96e6ecd11a246764a"
+ },
+ {
+ "name": "flag_bn",
+ "unicode": "1F1E7-1F1F3",
+ "digest": "078af6ca481a77871ba005e251a46ce63951c27b1b0cd33b9c1d0d31d349bc1a"
+ },
+ {
+ "name": "flag_bo",
+ "unicode": "1F1E7-1F1F4",
+ "digest": "92516d04e922a3bcbabe2e7619194bc972c09ba97576e8155f9829c397a71d8c"
+ },
+ {
+ "name": "flag_bq",
+ "unicode": "1F1E7-1F1F6",
+ "digest": "7832df5267a2bb8dddb83aeb11162ce79aeebdb718f2ac0e54adcf3d87936171"
+ },
+ {
+ "name": "flag_br",
+ "unicode": "1F1E7-1F1F7",
+ "digest": "aabcc1c082124045ed214f7d9778d8e2ed791ebb8433defea91db458658abeec"
+ },
+ {
+ "name": "flag_bs",
+ "unicode": "1F1E7-1F1F8",
+ "digest": "f628f39003608e181696634929522884165e27ccef55270293f92eeef991635f"
+ },
+ {
+ "name": "flag_bt",
+ "unicode": "1F1E7-1F1F9",
+ "digest": "af24a8ab34815da04c3e5af49a47449e0de93b068957cbda695816d0f830ca12"
+ },
+ {
+ "name": "flag_bv",
+ "unicode": "1F1E7-1F1FB",
+ "digest": "ff0037f6eed95d4bb5f2b502902360e1ff41426e2896daf3e0730cef1f8f7e41"
+ },
+ {
+ "name": "flag_bw",
+ "unicode": "1F1E7-1F1FC",
+ "digest": "3e3241ecb97946cc3e467b083d113a57dd305595e1512d4da18cc403e8689c1d"
+ },
+ {
+ "name": "flag_by",
+ "unicode": "1F1E7-1F1FE",
+ "digest": "bdd21885c6fac475241884a44149b887297772e17617ee59dd9fe8518d52cf3d"
+ },
+ {
+ "name": "flag_bz",
+ "unicode": "1F1E7-1F1FF",
+ "digest": "21c16e1da641af004576000bf1db44b9a1e0fccfddc775e96022721c2f18eeea"
+ },
+ {
+ "name": "flag_ca",
+ "unicode": "1F1E8-1F1E6",
+ "digest": "0d00e459084d58d3ea9c60488a9e51bf45f71b77f1600f190225d5ca6ca6c796"
+ },
+ {
+ "name": "flag_cc",
+ "unicode": "1F1E8-1F1E8",
+ "digest": "86ab27164603ef0f1f83fe898eda6fbb7bc5709f2518f5577f00817860806a7b"
+ },
+ {
+ "name": "flag_cd",
+ "unicode": "1F1E8-1F1E9",
+ "digest": "fdc2796530ada4bd0bae37ace4bbe707b321b287dcd64568f8e01d3a9df56066"
+ },
+ {
+ "name": "flag_cf",
+ "unicode": "1F1E8-1F1EB",
+ "digest": "5943bec02bede0931e21e7c34a68f375499f60a34883cc1edf2f21e9834b15ce"
+ },
+ {
+ "name": "flag_cg",
+ "unicode": "1F1E8-1F1EC",
+ "digest": "54498482e2772371e148e05cfb7c5eb55f6a22cd528662abdea10bad47d157da"
+ },
+ {
+ "name": "flag_ch",
+ "unicode": "1F1E8-1F1ED",
+ "digest": "53d6d35aeeebb0b4b1ad858dc3691e649ac73d30b3be76f96d5fe9605fa99386"
+ },
+ {
+ "name": "flag_ci",
+ "unicode": "1F1E8-1F1EE",
+ "digest": "3a173a3058a5c0174dc88750852cafec264e901ce82a6c69db122c8c0ea71a3a"
+ },
+ {
+ "name": "flag_ck",
+ "unicode": "1F1E8-1F1F0",
+ "digest": "42f395ff53c618b72b8a224cd4343d1a32f5ad82ced56bf590170a5ff0d5134c"
+ },
+ {
+ "name": "flag_cl",
+ "unicode": "1F1E8-1F1F1",
+ "digest": "9d6255feb690596904d800e72d5acdb5cda941c5a741b031ea39a3c7650ac46f"
+ },
+ {
+ "name": "flag_cm",
+ "unicode": "1F1E8-1F1F2",
+ "digest": "ffc99d14e0a8b46a980331090ed9f36f31a87f1b0f8dd8c09007a31c6127c69e"
+ },
+ {
+ "name": "flag_cn",
+ "unicode": "1F1E8-1F1F3",
+ "digest": "869a98c52bdc33591f87e2aab6cb4f13e98bb19136250ff25805d0312a8b7c8a"
+ },
+ {
+ "name": "flag_co",
+ "unicode": "1F1E8-1F1F4",
+ "digest": "6aa458440eb2500ad307fea40fd8f1171a1506a6e32af144a4fd51545bb56151"
+ },
+ {
+ "name": "flag_cp",
+ "unicode": "1F1E8-1F1F5",
+ "digest": "62627702e3e3768808c12f153a527ffcc492ad74d8cdc1858cfde971efd0c8ee"
+ },
+ {
+ "name": "flag_cr",
+ "unicode": "1F1E8-1F1F7",
+ "digest": "0f3b54d8330c5bb136647547dafc598bda755697cfd6b7d872a2443ba7b5cad4"
+ },
+ {
+ "name": "flag_cu",
+ "unicode": "1F1E8-1F1FA",
+ "digest": "69bc973002475bb3d9b54cb0ba9ec9cb85f144c1cf54689da0ee8f414ebb0d83"
+ },
+ {
+ "name": "flag_cv",
+ "unicode": "1F1E8-1F1FB",
+ "digest": "af2e135cf3c1b03a5937c068a75061b5cd332e95902fd0f8dffb2ac2dc89692a"
+ },
+ {
+ "name": "flag_cw",
+ "unicode": "1F1E8-1F1FC",
+ "digest": "df4b2228a82f766c5c64c13c1388482a68549e59dd843671ee0eb43506e33411"
+ },
+ {
+ "name": "flag_cx",
+ "unicode": "1F1E8-1F1FD",
+ "digest": "db12e513345a7be53954167d359ede0b3effbfb292508ee4d726123e3a8f83d7"
+ },
+ {
+ "name": "flag_cy",
+ "unicode": "1F1E8-1F1FE",
+ "digest": "0cea41d4820746e2c6eb408f7ec7419afba9f7396401d92e6c1d77382f721d0b"
+ },
+ {
+ "name": "flag_cz",
+ "unicode": "1F1E8-1F1FF",
+ "digest": "a1c2405916963be306f761539123486a2845af53716c9dfe94ad5420e14d36c4"
+ },
+ {
+ "name": "flag_de",
+ "unicode": "1F1E9-1F1EA",
+ "digest": "74a80b64437bc4e31bdd7cbb753ecd2d719bf34c506cbac535db83a644174cce"
+ },
+ {
+ "name": "flag_dg",
+ "unicode": "1F1E9-1F1EC",
+ "digest": "13cb5ea872f94a9c3fb579cef417e2d1ed38e8cbe95059576380cacd59bc4b9d"
+ },
+ {
+ "name": "flag_dj",
+ "unicode": "1F1E9-1F1EF",
+ "digest": "5b479654c28d3eeb70055c5e25dc46ccaba9eeea7537cc45ca9dbb8186b743b6"
+ },
+ {
+ "name": "flag_dk",
+ "unicode": "1F1E9-1F1F0",
+ "digest": "dee7fa9644a9b447417518a353e7edcbb37b2af8bc7d13a6ed71d7210c43ca3c"
+ },
+ {
+ "name": "flag_dm",
+ "unicode": "1F1E9-1F1F2",
+ "digest": "2e339190a8a0a238140f42e329f6646af5be75763a787ea268488a2e0440dc4c"
+ },
+ {
+ "name": "flag_do",
+ "unicode": "1F1E9-1F1F4",
+ "digest": "be5dafcd32d7197a96d37299a91835a8009299452f05a66d91c5fdec17448230"
+ },
+ {
+ "name": "flag_dz",
+ "unicode": "1F1E9-1F1FF",
+ "digest": "cf525d56bac45fe689f92d441274fc0ecbed4f95591d2c066598f72b1ee8d618"
+ },
+ {
+ "name": "flag_ea",
+ "unicode": "1F1EA-1F1E6",
+ "digest": "1acb13950f7c3692f9a36e618d8ec10a73ead5d7fa80fb52b6b2a18e3d456002"
+ },
+ {
+ "name": "flag_ec",
+ "unicode": "1F1EA-1F1E8",
+ "digest": "4d9d35450efc6026651ccc2278e70fb90b001ca5e5eecd31361b1e4e23253dbd"
+ },
+ {
+ "name": "flag_ee",
+ "unicode": "1F1EA-1F1EA",
+ "digest": "86ec7b2f618fe71dddec3d5a621b56b878d683780f1e0ad446f965326d42df48"
+ },
+ {
+ "name": "flag_eg",
+ "unicode": "1F1EA-1F1EC",
+ "digest": "f06d36a6fec15af4c1a76de30e8469847dde2728bb5a48956b4e466098b778a4"
+ },
+ {
+ "name": "flag_eh",
+ "unicode": "1F1EA-1F1ED",
+ "digest": "eb63f5b92c62c98dc008dfa7ad8830aa17fa23964f812a28055bd8b6f5960c5b"
+ },
+ {
+ "name": "flag_er",
+ "unicode": "1F1EA-1F1F7",
+ "digest": "e901195f7b37b22a6872d36713de0ec176f6424c209e261e5c849ce318c772f6"
+ },
+ {
+ "name": "flag_es",
+ "unicode": "1F1EA-1F1F8",
+ "digest": "27ab5cc6c2e9f26ccdfa632887533eebcd9b514f80cec9e721cf8e5e2544339c"
+ },
+ {
+ "name": "flag_et",
+ "unicode": "1F1EA-1F1F9",
+ "digest": "6cdb3718c9b3ec713258dd36781db58b7da53f3017445056c1a76233e3b4a7de"
+ },
+ {
+ "name": "flag_eu",
+ "unicode": "1F1EA-1F1FA",
+ "digest": "363f60e8a747166d5cec8d70bfdf266411eec2ff07933b6187975075caadfd74"
+ },
+ {
+ "name": "flag_fi",
+ "unicode": "1F1EB-1F1EE",
+ "digest": "1a1959cb551a0e8bdaee8c04657fb7387a4d83173f7759f89468da12e1818a9e"
+ },
+ {
+ "name": "flag_fj",
+ "unicode": "1F1EB-1F1EF",
+ "digest": "f26dc36ea9c1f32d9bb54874ea384e7118b6e2585be69245fdd73acd8304ae78"
+ },
+ {
+ "name": "flag_fk",
+ "unicode": "1F1EB-1F1F0",
+ "digest": "0479e233499b704f91a9b13d083e66296efe2f28ed917ab1496b223bfb09adb8"
+ },
+ {
+ "name": "flag_fm",
+ "unicode": "1F1EB-1F1F2",
+ "digest": "142ea7b4b4a7004329925b495da43ab82351cbaac383c8da6e614b39ba58d05e"
+ },
+ {
+ "name": "flag_fo",
+ "unicode": "1F1EB-1F1F4",
+ "digest": "f1c800d4f4d39e2aead9a11ed500f16108d6bc48bd24bd2a1af7b966d8e76752"
+ },
+ {
+ "name": "flag_fr",
+ "unicode": "1F1EB-1F1F7",
+ "digest": "6f52f36b5199c65ab1cad13ff4e77d2d8b48a8ff79b92166976674ffdc7829ee"
+ },
+ {
+ "name": "flag_ga",
+ "unicode": "1F1EC-1F1E6",
+ "digest": "50a0d5a07466e419b74a4d532738f7958de9baa37df6191be4f3755dccc3b326"
+ },
+ {
+ "name": "flag_gb",
+ "unicode": "1F1EC-1F1E7",
+ "digest": "220f7da6d5a231b766c79f2e1b7d3fdb74ec0c0c17558cc00a8a8ccdf2afc2e0"
+ },
+ {
+ "name": "flag_gd",
+ "unicode": "1F1EC-1F1E9",
+ "digest": "3e162b0d13f4ceea7f663b1d425f13863d104e80df75a640f526e276bcd04081"
+ },
+ {
+ "name": "flag_ge",
+ "unicode": "1F1EC-1F1EA",
+ "digest": "35897f8254675d2efe9e3070c88af9ef214f08440e6ee75ebe81d28cdb57ea2b"
+ },
+ {
+ "name": "flag_gf",
+ "unicode": "1F1EC-1F1EB",
+ "digest": "3a34df321635f71a0f2cc4e1eda58d85c29230c77456362345196351bf56533d"
+ },
+ {
+ "name": "flag_gg",
+ "unicode": "1F1EC-1F1EC",
+ "digest": "c972f8d190b4e9ca8890df41503d202ffd73981833d3f3750f563302167bcd66"
+ },
+ {
+ "name": "flag_gh",
+ "unicode": "1F1EC-1F1ED",
+ "digest": "9c3d3569bd411389fa0af7c6938d4325cedeb9c0e8f059dc1d5a74c6b8d6d01b"
+ },
+ {
+ "name": "flag_gi",
+ "unicode": "1F1EC-1F1EE",
+ "digest": "ede638bc6fedc30a01821025d87ec19297500da9c04a7a155984fca186118649"
+ },
+ {
+ "name": "flag_gl",
+ "unicode": "1F1EC-1F1F1",
+ "digest": "a2ce3371eff1da8331671925f707232aa593ac7400d59555c9ca689729ce24ec"
+ },
+ {
+ "name": "flag_gm",
+ "unicode": "1F1EC-1F1F2",
+ "digest": "932bf6eb75ddd4278268dd2f09d8fffcfef89f8fd6b6e86a08a414cd3ceec94d"
+ },
+ {
+ "name": "flag_gn",
+ "unicode": "1F1EC-1F1F3",
+ "digest": "ebf543713895adaa09d64897f24bd461191191b8fcbbcede52bdaf4bd2dc67a8"
+ },
+ {
+ "name": "flag_gp",
+ "unicode": "1F1EC-1F1F5",
+ "digest": "2e6c48d80c571b34f31fa9b3622dcc51e1707c0118e991e9c177742ff02a8a96"
+ },
+ {
+ "name": "flag_gq",
+ "unicode": "1F1EC-1F1F6",
+ "digest": "b0f5810180d12fc48faf75e73f882dc59072d7bf957f8455bf7e1e336539dc41"
+ },
+ {
+ "name": "flag_gr",
+ "unicode": "1F1EC-1F1F7",
+ "digest": "8d60d6f8910f5179d851dbea0798b56a492c6be85f3d55e1a1126cd1d6663a3b"
+ },
+ {
+ "name": "flag_gs",
+ "unicode": "1F1EC-1F1F8",
+ "digest": "7b07915af0e2364ebc386a162d44846f3a7986fdd24e20ad2bc56d64a103fe9c"
+ },
+ {
+ "name": "flag_gt",
+ "unicode": "1F1EC-1F1F9",
+ "digest": "0c78108ede45bf34917b409a0867f5ec8253c74b694beda083f3e8d04d7a10d8"
+ },
+ {
+ "name": "flag_gu",
+ "unicode": "1F1EC-1F1FA",
+ "digest": "909f1bc98fa1507adb787eb3875503b21ea937d6ae8bb152153916c2da5e13bb"
+ },
+ {
+ "name": "flag_gw",
+ "unicode": "1F1EC-1F1FC",
+ "digest": "f5f34410c7b22d5ed9994b47d0e7a9d9a6a1f05c4d3142f7fef3e4409725f5e6"
+ },
+ {
+ "name": "flag_gy",
+ "unicode": "1F1EC-1F1FE",
+ "digest": "4939cf52ab34a924a31032b42668960a2c7d8d4f998b16b065c247110df334be"
+ },
+ {
+ "name": "flag_hk",
+ "unicode": "1F1ED-1F1F0",
+ "digest": "bde0916df6d62f6b1cf8f85a8a39526c97fc6ef6fedb0b0cae2adb127a08eafe"
+ },
+ {
+ "name": "flag_hm",
+ "unicode": "1F1ED-1F1F2",
+ "digest": "603e6c9bff9a0dc941970a313fe98fbf53ff5a57028f1a2766420be4211711cc"
+ },
+ {
+ "name": "flag_hn",
+ "unicode": "1F1ED-1F1F3",
+ "digest": "2953ad0909bc32c02615f6ad5a4e5f331ba794a41632b1f0fc366e1c640cc2b9"
+ },
+ {
+ "name": "flag_hr",
+ "unicode": "1F1ED-1F1F7",
+ "digest": "41c9ffc4f0faaa2d77e5cffb781329e7d2489ce879bd8eb9c503621e834abc50"
+ },
+ {
+ "name": "flag_ht",
+ "unicode": "1F1ED-1F1F9",
+ "digest": "6a56c3d71b4f858e1774aa2134a9f5584087fec968e9ee8bb1046d2ec93bf059"
+ },
+ {
+ "name": "flag_hu",
+ "unicode": "1F1ED-1F1FA",
+ "digest": "72f5809818d4cab8c0cee73df7f67b820fb8471eea4199911a5917ac099795e8"
+ },
+ {
+ "name": "flag_ic",
+ "unicode": "1F1EE-1F1E8",
+ "digest": "7e2a7667fcd05f927af47e64c5790c104a9956dd9f1a45f03cb0fdcc85d866d3"
+ },
+ {
+ "name": "flag_id",
+ "unicode": "1F1EE-1F1E9",
+ "digest": "4721f616fae2e443e52f1e9cc96e4835bddca16a2d75d7d5afea57cdee866b7f"
+ },
+ {
+ "name": "flag_ie",
+ "unicode": "1F1EE-1F1EA",
+ "digest": "84b19833e6c9fb43187f8a28d85045a3df58816f20a07edab90474323174b1f3"
+ },
+ {
+ "name": "flag_il",
+ "unicode": "1F1EE-1F1F1",
+ "digest": "c99d4bd8c2541cf3a7392c4faf4477d96bc47065dd1423b9e06450483e69b34f"
+ },
+ {
+ "name": "flag_im",
+ "unicode": "1F1EE-1F1F2",
+ "digest": "5eeb12c0315b527ce61649a38b64d76af726a73b2d381d1a1ddd1366bafb1bfc"
+ },
+ {
+ "name": "flag_in",
+ "unicode": "1F1EE-1F1F3",
+ "digest": "ecc3cfcff3368fe0875a51a8be9f4dfd449a187e5beb41a2b34241736247f73b"
+ },
+ {
+ "name": "flag_io",
+ "unicode": "1F1EE-1F1F4",
+ "digest": "26243d60e04ba3bc9eb8f008bfc77b2a64bcf1a3d0073eb0449a8c8121618c9c"
+ },
+ {
+ "name": "flag_iq",
+ "unicode": "1F1EE-1F1F6",
+ "digest": "a1fb5e59575081920b3be5290f654d57a9be099deb56d4ed69eba81a2b531cb3"
+ },
+ {
+ "name": "flag_ir",
+ "unicode": "1F1EE-1F1F7",
+ "digest": "ab89488b934af1d4bdae7ed16dfc74fffe658bb8e95d5161b48cdd06de44ae85"
+ },
+ {
+ "name": "flag_is",
+ "unicode": "1F1EE-1F1F8",
+ "digest": "55db1fc9e6c56d4c9bcb9a46e5e4300cf2a0c32fa91dc24b487a1d56c8097268"
+ },
+ {
+ "name": "flag_it",
+ "unicode": "1F1EE-1F1F9",
+ "digest": "36fc993fb00ab607578a4d0e573e988e17b9459a68a000a48de905a8238589d0"
+ },
+ {
+ "name": "flag_je",
+ "unicode": "1F1EF-1F1EA",
+ "digest": "c608dbfd1259330e2f8c40dc5d12ffd0489396f4fc5f3ca57bcb2f0d9d05c20c"
+ },
+ {
+ "name": "flag_jm",
+ "unicode": "1F1EF-1F1F2",
+ "digest": "a8224b68b2d324f848d75e4376875ef76a8174e6ba32790d9ca622fe1eabfd5f"
+ },
+ {
+ "name": "flag_jo",
+ "unicode": "1F1EF-1F1F4",
+ "digest": "2403563dc2ab4ed0e7e3a0761cc09f96801550bba6b177b54d651d8804ad987d"
+ },
+ {
+ "name": "flag_jp",
+ "unicode": "1F1EF-1F1F5",
+ "digest": "aea8eebd0a0139818cb7629d9c9a8e55160b458eb8ffeee2f36c5cff4b507fd3"
+ },
+ {
+ "name": "flag_ke",
+ "unicode": "1F1F0-1F1EA",
+ "digest": "9c8365f74858743bcdce4a9cf6a6f4110faf2dc6433e5dc7d98c24bb3b32a36d"
+ },
+ {
+ "name": "flag_kg",
+ "unicode": "1F1F0-1F1EC",
+ "digest": "0c72bdb1d64b1e3be3d9516a50655a6162d8501851d2cf2fadb8c6ef7740df4e"
+ },
+ {
+ "name": "flag_kh",
+ "unicode": "1F1F0-1F1ED",
+ "digest": "49e41e488732d789e395091e144cd6215c6818ba2073e5e22ea21203a737d03c"
+ },
+ {
+ "name": "flag_ki",
+ "unicode": "1F1F0-1F1EE",
+ "digest": "9d7f168adbcf5f4cfe28470addfdb0a8b231438d593edb70f633981bfa4c7638"
+ },
+ {
+ "name": "flag_km",
+ "unicode": "1F1F0-1F1F2",
+ "digest": "9318c28957fa7a19eba5ec452c1cbce01a5a83d41d29d081614d3abb0585d478"
+ },
+ {
+ "name": "flag_kn",
+ "unicode": "1F1F0-1F1F3",
+ "digest": "eac7e7d0f023dee5c0c8559bc2c9a96273adda54ce47598025120b30d8d6ebc1"
+ },
+ {
+ "name": "flag_kp",
+ "unicode": "1F1F0-1F1F5",
+ "digest": "d4d53db6f8363174de6db864c056267ba8a7d7e87b5527f2f42bb9b8ac3f362b"
+ },
+ {
+ "name": "flag_kr",
+ "unicode": "1F1F0-1F1F7",
+ "digest": "5c7e61ab4a2aae70cbe51f0ca4718516002bc943b35d870bd853a0c98c4e0ed5"
+ },
+ {
+ "name": "flag_kw",
+ "unicode": "1F1F0-1F1FC",
+ "digest": "5d229cd99d25f4285bd30d98cfcc3cd8346648897476e2905a1811ceeef48d37"
+ },
+ {
+ "name": "flag_ky",
+ "unicode": "1F1F0-1F1FE",
+ "digest": "9ce3d8dfc273d3a400960876c434b702f93df92c6c00682dbed2ec8e3966d8a8"
+ },
+ {
+ "name": "flag_kz",
+ "unicode": "1F1F0-1F1FF",
+ "digest": "a6f0be0a767fa4824495d568d9fc2bd8d4c1a26f363873d3b65362e9383e2a50"
+ },
+ {
+ "name": "flag_la",
+ "unicode": "1F1F1-1F1E6",
+ "digest": "ab2ae96da87f7b53ab212f8dcd897a591cff9ea6666270097a8e739ee0b8f8cb"
+ },
+ {
+ "name": "flag_lb",
+ "unicode": "1F1F1-1F1E7",
+ "digest": "0c3fcab22e9fae1c78658290aff97de785d0b6adb5e3702d00073ce774b7ed54"
+ },
+ {
+ "name": "flag_lc",
+ "unicode": "1F1F1-1F1E8",
+ "digest": "e154b0b3a1635a36e0d9ad518c0ea12259320e5f1ebbda982248486492065d28"
+ },
+ {
+ "name": "flag_li",
+ "unicode": "1F1F1-1F1EE",
+ "digest": "bbc393a89e73cc8c29a0a9297428d07aa1d4717ea9b7d4dd9d69f21ac7d0605d"
+ },
+ {
+ "name": "flag_lk",
+ "unicode": "1F1F1-1F1F0",
+ "digest": "376bd501d113a844971ca1006ab31aa086cd55d74842ea5f3dedaba997b58693"
+ },
+ {
+ "name": "flag_lr",
+ "unicode": "1F1F1-1F1F7",
+ "digest": "9a6ebe1c9d9a53079ee77292a5ad0965f96409b0417f92876a1c3bd463d6a9bc"
+ },
+ {
+ "name": "flag_ls",
+ "unicode": "1F1F1-1F1F8",
+ "digest": "e2f4b05414f6e0c3d629a92b0534d4145475f0214a83a62c902fe0884c833c89"
+ },
+ {
+ "name": "flag_lt",
+ "unicode": "1F1F1-1F1F9",
+ "digest": "d5e2f8b2ffa820a33ea6d612fccd61e32467d25154342f5be134d3520e48387f"
+ },
+ {
+ "name": "flag_lu",
+ "unicode": "1F1F1-1F1FA",
+ "digest": "f43277103292195b51981d08e2dde68eab660a65c7875f510e09a8b2370f1b5c"
+ },
+ {
+ "name": "flag_lv",
+ "unicode": "1F1F1-1F1FB",
+ "digest": "e1288ac5c80d6e9d577d652e34be247ca39bf9d3d7cfc8a6cae13c1f9ac9dc47"
+ },
+ {
+ "name": "flag_ly",
+ "unicode": "1F1F1-1F1FE",
+ "digest": "5122294b769a174e3b6e3d238bb846b3e760929f5bb3c1a708d8a429f3f32f68"
+ },
+ {
+ "name": "flag_ma",
+ "unicode": "1F1F2-1F1E6",
+ "digest": "615a6447ff284de7689b4fd7b04fdda308f65dbbec958cfb96d2977514981d16"
+ },
+ {
+ "name": "flag_mc",
+ "unicode": "1F1F2-1F1E8",
+ "digest": "08b48b28938acbfc0fbc15c25ee14dbad7164c5165d03df2eee370755ee7b4cf"
+ },
+ {
+ "name": "flag_md",
+ "unicode": "1F1F2-1F1E9",
+ "digest": "93d61de68f821e1e08b30e63d91e8b4a657766475128538894cf9da9a3b4e3c0"
+ },
+ {
+ "name": "flag_me",
+ "unicode": "1F1F2-1F1EA",
+ "digest": "ee55c0eb78241aec2baf1822a47fa46d63209ceae3db7617ae886b823ae229ff"
+ },
+ {
+ "name": "flag_mf",
+ "unicode": "1F1F2-1F1EB",
+ "digest": "62627702e3e3768808c12f153a527ffcc492ad74d8cdc1858cfde971efd0c8ee"
+ },
+ {
+ "name": "flag_mg",
+ "unicode": "1F1F2-1F1EC",
+ "digest": "86ec8140e2c4854f52cff74757baf0cbb75a4aacca8be6af8c8f9c939a7b866c"
+ },
+ {
+ "name": "flag_mh",
+ "unicode": "1F1F2-1F1ED",
+ "digest": "8311ea3422c9d5e94b55e19b03bedd6fe6e2a191b7657e15ac75a48932958a5b"
+ },
+ {
+ "name": "flag_mk",
+ "unicode": "1F1F2-1F1F0",
+ "digest": "5c6f504f88c5a875c06ac8b26fa6e81a9d79c42a1c7d1fad9a5d4c8ad06ca502"
+ },
+ {
+ "name": "flag_ml",
+ "unicode": "1F1F2-1F1F1",
+ "digest": "d08a4973db40cf28e58ca3c80e8bd4e50d68ba1080b31917aeefdb0e210b5c50"
+ },
+ {
+ "name": "flag_mm",
+ "unicode": "1F1F2-1F1F2",
+ "digest": "5e95089514ca09bb93afb481b317477c9d053adcf450e0b711d78ed1078c7470"
+ },
+ {
+ "name": "flag_mn",
+ "unicode": "1F1F2-1F1F3",
+ "digest": "7a0ca72715dd2a36eeeed2f8c888497cb752f0000af8f07d6930743caf6e4273"
+ },
+ {
+ "name": "flag_mo",
+ "unicode": "1F1F2-1F1F4",
+ "digest": "d2c7c2191bc1bc83d85f2270968cb4de5cf26a11f70e166a8b32c108287ef729"
+ },
+ {
+ "name": "flag_mp",
+ "unicode": "1F1F2-1F1F5",
+ "digest": "89ad06121fd7981338fe188464491bea371f85125bfb4fc01fb5cad606613b1e"
+ },
+ {
+ "name": "flag_mq",
+ "unicode": "1F1F2-1F1F6",
+ "digest": "98176f3af823b26a3657a17c5073ee22367898b40bd3973de76329aa87ca5a2e"
+ },
+ {
+ "name": "flag_mr",
+ "unicode": "1F1F2-1F1F7",
+ "digest": "cc3e705ad84f83fe2d544385c39564743024dab26595d62469b35fdb791f6015"
+ },
+ {
+ "name": "flag_ms",
+ "unicode": "1F1F2-1F1F8",
+ "digest": "465e3d5700b557f2589bd6e34a0c6b12c634a6ed4dcfbee3c1c841c5de3413f0"
+ },
+ {
+ "name": "flag_mt",
+ "unicode": "1F1F2-1F1F9",
+ "digest": "e610ba22d8d8ad750ed10dff8e1b4d89bc34f066c3424bfa77dbdc1a5d79743a"
+ },
+ {
+ "name": "flag_mu",
+ "unicode": "1F1F2-1F1FA",
+ "digest": "3daf015d3b95218677dafbb282b7804686aa68875a6bd1d70c165b7b149e19cb"
+ },
+ {
+ "name": "flag_mv",
+ "unicode": "1F1F2-1F1FB",
+ "digest": "d30e4bfd04f08177de92f3c175600aaafa89b9668bbe2b83f35f07a74382065c"
+ },
+ {
+ "name": "flag_mw",
+ "unicode": "1F1F2-1F1FC",
+ "digest": "f364b1c8bfda3f86b5e26422eedc571ba11e312dcc634197631a6840cb22aede"
+ },
+ {
+ "name": "flag_mx",
+ "unicode": "1F1F2-1F1FD",
+ "digest": "eafb02ec0be9cefab7cef7c426c7d860d98e4947f4da04054154dc86d8f487c4"
+ },
+ {
+ "name": "flag_my",
+ "unicode": "1F1F2-1F1FE",
+ "digest": "9a690b357bc6b970781bd122c1e546ade3ccb73d930c2af1008b82027e36c7cf"
+ },
+ {
+ "name": "flag_mz",
+ "unicode": "1F1F2-1F1FF",
+ "digest": "36d0548ebfef9e0443ec1d0597ebfa6e95c25b997381f30c8c74008820743bb9"
+ },
+ {
+ "name": "flag_na",
+ "unicode": "1F1F3-1F1E6",
+ "digest": "4989dc9452b0bdfa101cfd3b7c83ef1195a7e45128b9ed00193fe712a6d02fca"
+ },
+ {
+ "name": "flag_nc",
+ "unicode": "1F1F3-1F1E8",
+ "digest": "7fc9d865eebf729d5496c4cd7576476ec599f65b379d4a6df66b4e399553c2eb"
+ },
+ {
+ "name": "flag_ne",
+ "unicode": "1F1F3-1F1EA",
+ "digest": "d3f10fb44ec44a04112bc66d05f0a44c6ec46dae73cfd3fe26cdc8b32ec06713"
+ },
+ {
+ "name": "flag_nf",
+ "unicode": "1F1F3-1F1EB",
+ "digest": "d390e0d52215a025380af221ba9e955e5886edbb4c9f4b124f2fb60a8e019e42"
+ },
+ {
+ "name": "flag_ng",
+ "unicode": "1F1F3-1F1EC",
+ "digest": "e69d1bb8f1db4a0c295c90dda23d8f97c2dea59f9a2da2ecb0e9a1dc4dbea101"
+ },
+ {
+ "name": "flag_ni",
+ "unicode": "1F1F3-1F1EE",
+ "digest": "dbaccc942637469b0ee75bd5f956958c3c5a89d8f69b69c96f02ab6594124894"
+ },
+ {
+ "name": "flag_nl",
+ "unicode": "1F1F3-1F1F1",
+ "digest": "bda2eb0315763c3c19d37c664dab1ee4280f20888a0ca57677fd33cfa4240910"
+ },
+ {
+ "name": "flag_no",
+ "unicode": "1F1F3-1F1F4",
+ "digest": "42b49dec756a220781ea271ca8fbcaba524dc3b38d5d8f999bfaa40ef9ebd302"
+ },
+ {
+ "name": "flag_np",
+ "unicode": "1F1F3-1F1F5",
+ "digest": "b5259257db079235310d5d9537d2b5b61ae0326bc8920ba13084b009844e2957"
+ },
+ {
+ "name": "flag_nr",
+ "unicode": "1F1F3-1F1F7",
+ "digest": "1bd7d1fe2c3a5e98cfd4dff6e8d6dd6d3c74f0051ad615587d77d2291a9784cc"
+ },
+ {
+ "name": "flag_nu",
+ "unicode": "1F1F3-1F1FA",
+ "digest": "e2a7a398e07d2232147cc0917d72d18b519246d3d314e9f6f03dcf98d312d4ce"
+ },
+ {
+ "name": "flag_nz",
+ "unicode": "1F1F3-1F1FF",
+ "digest": "ce8b1cb87dae3a3ec865575b57a0b4987a7f4bd3f170e7b210dd764fc2588cd4"
+ },
+ {
+ "name": "flag_om",
+ "unicode": "1F1F4-1F1F2",
+ "digest": "29da72505a276a8a372a00c197388ebc5098c221cab26b3ff755bd62b10f740f"
+ },
+ {
+ "name": "flag_pa",
+ "unicode": "1F1F5-1F1E6",
+ "digest": "180b673c9aceea43a8b55823a82d80600257e4982d0757d129860e3d8a14f458"
+ },
+ {
+ "name": "flag_pe",
+ "unicode": "1F1F5-1F1EA",
+ "digest": "b61823ea2cd91e371e40832df5764558b81d44fac41030827a3f6d2564643c00"
+ },
+ {
+ "name": "flag_pf",
+ "unicode": "1F1F5-1F1EB",
+ "digest": "e560421911f4af90c73a0dbdf8f42e69316003799304c9394fb127e3b83326fa"
+ },
+ {
+ "name": "flag_pg",
+ "unicode": "1F1F5-1F1EC",
+ "digest": "880e87db2ce0eac38db037683a5db46fd6ce30623cf56ae4a93a747103570044"
+ },
+ {
+ "name": "flag_ph",
+ "unicode": "1F1F5-1F1ED",
+ "digest": "49aae2f56bfd1385741dc76857aa1f1459778b2d39a1c955e469c5367585bfd5"
+ },
+ {
+ "name": "flag_pk",
+ "unicode": "1F1F5-1F1F0",
+ "digest": "64379dbfc932df3a07935b5cfa11ca151f761d3728939e982604e12c663cd646"
+ },
+ {
+ "name": "flag_pl",
+ "unicode": "1F1F5-1F1F1",
+ "digest": "3b688b074c2735d3dea0b7ab74b80eba243ce50cb05d68e585c9d701c1f14617"
+ },
+ {
+ "name": "flag_pm",
+ "unicode": "1F1F5-1F1F2",
+ "digest": "a13a69ee3131501dd8138173cfb669a35ee8039d84aa665e69dd7f0d0aa3e717"
+ },
+ {
+ "name": "flag_pn",
+ "unicode": "1F1F5-1F1F3",
+ "digest": "d7ae3985cf66024e4a3001e79a8efbb3e75571f2b0abbd0fb87fc1efc795a2b3"
+ },
+ {
+ "name": "flag_pr",
+ "unicode": "1F1F5-1F1F7",
+ "digest": "4910dc984bc908158506b770f28af56150cbb4509a4291947dfa2479b9e4b308"
+ },
+ {
+ "name": "flag_ps",
+ "unicode": "1F1F5-1F1F8",
+ "digest": "b2bca7619fced25de94d7bd398537857460348a552e7d73d189aef3f428e6a13"
+ },
+ {
+ "name": "flag_pt",
+ "unicode": "1F1F5-1F1F9",
+ "digest": "177282613b4b8b4d9551f1da6a1c3f66f1b96cf67c71c7d164213b26b3237395"
+ },
+ {
+ "name": "flag_pw",
+ "unicode": "1F1F5-1F1FC",
+ "digest": "2ff42a14bdc7df76b5f989dca381f94765032b26ae47d47b97844abde458cefe"
+ },
+ {
+ "name": "flag_py",
+ "unicode": "1F1F5-1F1FE",
+ "digest": "80169b69a46c4c67d0090dc2c6bf05d1a14f133ac7ae56f811547e8e8f70d81b"
+ },
+ {
+ "name": "flag_qa",
+ "unicode": "1F1F6-1F1E6",
+ "digest": "589b44b975aa97426afb8db7f8b355491fca246b693903485824bf0f5a6953a2"
+ },
+ {
+ "name": "flag_re",
+ "unicode": "1F1F7-1F1EA",
+ "digest": "77d242261742831a142c9ec74cd17d76b1e6d1af751ff3c6a356646744bc798a"
+ },
+ {
+ "name": "flag_ro",
+ "unicode": "1F1F7-1F1F4",
+ "digest": "d7d17026ea81f27456983722540f9a23343a3a1b22e7697c4fba118ce8b4719e"
+ },
+ {
+ "name": "flag_rs",
+ "unicode": "1F1F7-1F1F8",
+ "digest": "e466a18cc0368e623d3fe33a036c1e88db91ae24f7510e17caacc85c41f1bac8"
+ },
+ {
+ "name": "flag_ru",
+ "unicode": "1F1F7-1F1FA",
+ "digest": "86bf53a62dfc4c434d910f43df70f430fc67c0070fe3fc466c4fbfd6a5d8e646"
+ },
+ {
+ "name": "flag_rw",
+ "unicode": "1F1F7-1F1FC",
+ "digest": "38ec5a01896c9747a8dbf865d5e8584770e587253b7af3d3b9c36cd993f67518"
+ },
+ {
+ "name": "flag_sa",
+ "unicode": "1F1F8-1F1E6",
+ "digest": "a44d0b145f2a0b68eace24ecfd27519e9525ec764836728bc9c1fe96ccb811a0"
+ },
+ {
+ "name": "flag_sb",
+ "unicode": "1F1F8-1F1E7",
+ "digest": "8ffa24c5cb92be4dbe43f6cd85b61b9608a3101bd78ebccff4fe99c209b3e241"
+ },
+ {
+ "name": "flag_sc",
+ "unicode": "1F1F8-1F1E8",
+ "digest": "227d090ac2cbf317e594567b6114b5063a13cfe33abf990d37b200debcfadabb"
+ },
+ {
+ "name": "flag_sd",
+ "unicode": "1F1F8-1F1E9",
+ "digest": "350f3332e8ea1138e54facc870dd0fea5f2ab7d3fd4baa02ed8627ae79642f6c"
+ },
+ {
+ "name": "flag_se",
+ "unicode": "1F1F8-1F1EA",
+ "digest": "c1b09f36c263727de83b54376f05e083a17a61941af9a1640b826629256a280d"
+ },
+ {
+ "name": "flag_sg",
+ "unicode": "1F1F8-1F1EC",
+ "digest": "e6fc26920dfc07e4fd3c8d897de9c607e0bf48a3b64a13630c858d707a8e7660"
+ },
+ {
+ "name": "flag_sh",
+ "unicode": "1F1F8-1F1ED",
+ "digest": "f2c22ab0eb49e3104c35f1c0268b1e63c3a67f41b0cfa9861b189525988e53b6"
+ },
+ {
+ "name": "flag_si",
+ "unicode": "1F1F8-1F1EE",
+ "digest": "1ef0b10e498f71591322f9d8ec122d39838f479370cf7ee922560986ef6c4f2e"
+ },
+ {
+ "name": "flag_sj",
+ "unicode": "1F1F8-1F1EF",
+ "digest": "ce913b007f84a9cba2add8d754aa791901624c60e4200de426dfa25271cb0f78"
+ },
+ {
+ "name": "flag_sk",
+ "unicode": "1F1F8-1F1F0",
+ "digest": "d8f8fc4024c82f906effe98facbef9d543fb3708b1134dc502c74dc4a442b30a"
+ },
+ {
+ "name": "flag_sl",
+ "unicode": "1F1F8-1F1F1",
+ "digest": "dd7fd0452498d8d1c894cf0d5a662ddff9c5bcc02148bdc3dc7e6f25d0bb586e"
+ },
+ {
+ "name": "flag_sm",
+ "unicode": "1F1F8-1F1F2",
+ "digest": "2b499606aee2b5cbf4037338753c80a4c8f75f4abcef2c8657bd9337e602bbd3"
+ },
+ {
+ "name": "flag_sn",
+ "unicode": "1F1F8-1F1F3",
+ "digest": "03b46a9d8b129da13f60c23b820b04fba52050ca58a41b859ad57d5c3cc2515d"
+ },
+ {
+ "name": "flag_so",
+ "unicode": "1F1F8-1F1F4",
+ "digest": "ea416b6a05ddc5b16291ebe5101735360b08c834d55ac82c663ac1dd3e459048"
+ },
+ {
+ "name": "flag_sr",
+ "unicode": "1F1F8-1F1F7",
+ "digest": "012179fbcbcb7343e7b09d33e283fb63c7964a6eca35ccb9407d468e495a9874"
+ },
+ {
+ "name": "flag_ss",
+ "unicode": "1F1F8-1F1F8",
+ "digest": "6723150482c640643c9dd7e33ea749f4a8b46aceacbd4f5e11aa33b3ee13aab7"
+ },
+ {
+ "name": "flag_st",
+ "unicode": "1F1F8-1F1F9",
+ "digest": "0947fcec2e3cb1b0e9943c3d00891e8ee226e8d0532e9b1fe807ddf2e8fbc49d"
+ },
+ {
+ "name": "flag_sv",
+ "unicode": "1F1F8-1F1FB",
+ "digest": "ce7e583db833c4b10e2f7a2d09b97bb522c02e96ea0b3f3a48a955f7d8f970d8"
+ },
+ {
+ "name": "flag_sx",
+ "unicode": "1F1F8-1F1FD",
+ "digest": "c01fb238c7ba439f24a5ef821b6457f2a0fd0b99a1b2d02395bed87f0a4a88e5"
+ },
+ {
+ "name": "flag_sy",
+ "unicode": "1F1F8-1F1FE",
+ "digest": "a77d87ef98c96140c59998d10d94837e2a056dd3ac5c7522e89e5c62eac69e69"
+ },
+ {
+ "name": "flag_sz",
+ "unicode": "1F1F8-1F1FF",
+ "digest": "2904ad01040a9107ad556ec4c2561781d96746005cca250babb1127b8ba21050"
+ },
+ {
+ "name": "flag_ta",
+ "unicode": "1F1F9-1F1E6",
+ "digest": "eda84db90e1a8854e8ff3c15b3b38ee65f7d6532b76970a6fbac304c30d8c959"
+ },
+ {
+ "name": "flag_tc",
+ "unicode": "1F1F9-1F1E8",
+ "digest": "4628fdf6dc598a2846beefe97f7d4c6812f4961394cec132924b44bbe79b3322"
+ },
+ {
+ "name": "flag_td",
+ "unicode": "1F1F9-1F1E9",
+ "digest": "125ff31e4285cb2a5493a52a2703ebe8e7138b918ec4dae3d0f8693632372df6"
+ },
+ {
+ "name": "flag_tf",
+ "unicode": "1F1F9-1F1EB",
+ "digest": "489d591e11764ac341f2234020f7879db782b8f673fc9aae425fd713e4082334"
+ },
+ {
+ "name": "flag_tg",
+ "unicode": "1F1F9-1F1EC",
+ "digest": "4ceedfcfcc22cd14d9add9d86d6748447995f19f7095fa4be883e21eb1aa86bc"
+ },
+ {
+ "name": "flag_th",
+ "unicode": "1F1F9-1F1ED",
+ "digest": "2798cc660af1c5dc4891c30aded3a53d7cfa0af128cc495df8141907b165902d"
+ },
+ {
+ "name": "flag_tj",
+ "unicode": "1F1F9-1F1EF",
+ "digest": "0483506fc5b5f2d4fc18ea3cd2f8a5da985d68fe4bf90bd3fd05e67e38f32398"
+ },
+ {
+ "name": "flag_tk",
+ "unicode": "1F1F9-1F1F0",
+ "digest": "d5d4a8c6ce3207731b7c154a9d8d8fa2af055a48f03b3cbbcfd3317d3b8a75f2"
+ },
+ {
+ "name": "flag_tl",
+ "unicode": "1F1F9-1F1F1",
+ "digest": "7a2ba8f91a6b627c60c88244223a9b9d0c12707f50b174f9c2eca07dd3440df7"
+ },
+ {
+ "name": "flag_tm",
+ "unicode": "1F1F9-1F1F2",
+ "digest": "adcf5f23adcf983ce626b44559482f8728251eab34b3ff5d8b125112f3a1010f"
+ },
+ {
+ "name": "flag_tn",
+ "unicode": "1F1F9-1F1F3",
+ "digest": "5ee690ee1f3c3c0cba9b36efdef902894ec59cefbc60c4baa341efd3d7bb9ba2"
+ },
+ {
+ "name": "flag_to",
+ "unicode": "1F1F9-1F1F4",
+ "digest": "cde8672ca25b0e3a423865283fab9bc3ab10f472e04979b3b2f8032b71e96300"
+ },
+ {
+ "name": "flag_tr",
+ "unicode": "1F1F9-1F1F7",
+ "digest": "3d83c03ed084cfc81fa633310382acd7213e1eaa19d0ed97d142e7824032b55d"
+ },
+ {
+ "name": "flag_tt",
+ "unicode": "1F1F9-1F1F9",
+ "digest": "d66d272ac27e2b398289d6b60128ccd3508aeb1f4a00a3920c5e6a21bfe357ed"
+ },
+ {
+ "name": "flag_tv",
+ "unicode": "1F1F9-1F1FB",
+ "digest": "8716527383854cf1569f737d0f0f9ad77b46747255f24e02f5b2fbc850c2e35c"
+ },
+ {
+ "name": "flag_tw",
+ "unicode": "1F1F9-1F1FC",
+ "digest": "fb17b97e18e4423c5f60d60ec3ec60b917be579fc4dd9b5b23236786dcb35108"
+ },
+ {
+ "name": "flag_tz",
+ "unicode": "1F1F9-1F1FF",
+ "digest": "a8a8cf57ae5227cb54620bf31d2d6e154d2067d6d049b8db64bc4e538222948b"
+ },
+ {
+ "name": "flag_ua",
+ "unicode": "1F1FA-1F1E6",
+ "digest": "03aca4b3ffd60d944a5793eb7530f8d8ae527782f642f6606194e46ee314b12c"
+ },
+ {
+ "name": "flag_ug",
+ "unicode": "1F1FA-1F1EC",
+ "digest": "70226a1585e88390b3b815b8b79a0ddb36d2961c6b465c4ff72aa444abfe982e"
+ },
+ {
+ "name": "flag_um",
+ "unicode": "1F1FA-1F1F2",
+ "digest": "aa83bf051149acf907140a860de5de1700710e4164ae5549ad1040b24d0a142b"
+ },
+ {
+ "name": "flag_us",
+ "unicode": "1F1FA-1F1F8",
+ "digest": "32ba2aa09a30514247e91d60762791b582f547a37d9151f98b700dff50f355ea"
+ },
+ {
+ "name": "flag_uy",
+ "unicode": "1F1FA-1F1FE",
+ "digest": "0e01b3f1df4bdf6d616dacc9c5825151b941bf074be750e8b24a07ea5d5bcacb"
+ },
+ {
+ "name": "flag_uz",
+ "unicode": "1F1FA-1F1FF",
+ "digest": "903029ce83812a2134f24b65db35b183443a440ea5fecaa6ef7dcaaf65b2519c"
+ },
+ {
+ "name": "flag_va",
+ "unicode": "1F1FB-1F1E6",
+ "digest": "fd3c1c5d0ac030e838f807288912c98a3e258f87901e252e46942a4dab9f8cb7"
+ },
+ {
+ "name": "flag_vc",
+ "unicode": "1F1FB-1F1E8",
+ "digest": "7cd554ea8ca817b5366701160274587ab44167ae5a89c430bbaf237ea18b7421"
+ },
+ {
+ "name": "flag_ve",
+ "unicode": "1F1FB-1F1EA",
+ "digest": "72930094fb088c1facabea07616035ec4771374358a90c3045219d087b350dd8"
+ },
+ {
+ "name": "flag_vg",
+ "unicode": "1F1FB-1F1EC",
+ "digest": "78a59afd368b7a8312bfdb2f49927ff09e6b8f46aab0136c0453e3319e81df49"
+ },
+ {
+ "name": "flag_vi",
+ "unicode": "1F1FB-1F1EE",
+ "digest": "e070879f9605a9bae66bb84f2abf5a40c8b264baee65cd4f7a6720b826739f29"
+ },
+ {
+ "name": "flag_vn",
+ "unicode": "1F1FB-1F1F3",
+ "digest": "100ddf06e0f239b170f4d6cb459450bf4945281ee818f7d3c061828b80562219"
+ },
+ {
+ "name": "flag_vu",
+ "unicode": "1F1FB-1F1FA",
+ "digest": "59fc9d16818295bba4f7f551598f85378cd07f2bd7e31a4eef2589aaa3847563"
+ },
+ {
+ "name": "flag_wf",
+ "unicode": "1F1FC-1F1EB",
+ "digest": "62627702e3e3768808c12f153a527ffcc492ad74d8cdc1858cfde971efd0c8ee"
+ },
+ {
+ "name": "flag_white",
+ "unicode": "1F3F3",
+ "digest": "96307e3a28e92d1e7147a06f154ffc291ee3cd1765cf8b7bfb06412294112559"
+ },
+ {
+ "name": "flag_ws",
+ "unicode": "1F1FC-1F1F8",
+ "digest": "0c95271d0f4b23f0d215ee0fba05cf08ecb70665d4c028e17463ecda2754b164"
+ },
+ {
+ "name": "flag_xk",
+ "unicode": "1F1FD-1F1F0",
+ "digest": "713aa7d228e96f4a06d58d1fb8c2a55296c3e56842f8177ca936f3e09f50da1e"
+ },
+ {
+ "name": "flag_ye",
+ "unicode": "1F1FE-1F1EA",
+ "digest": "3bb65bae9c913357bcae8b8b5878efc9e194ca308442ab69639c29716b49f078"
+ },
+ {
+ "name": "flag_yt",
+ "unicode": "1F1FE-1F1F9",
+ "digest": "f86c86f4c194610a3af78971fcf221ad97b9499d08f6d64476e417a2f52a611e"
+ },
+ {
+ "name": "flag_za",
+ "unicode": "1F1FF-1F1E6",
+ "digest": "4dd4fa49a01fdcfc7c1c099a7869e0e9acba83a6a3debf6c8505ada4c796b872"
+ },
+ {
+ "name": "flag_zm",
+ "unicode": "1F1FF-1F1F2",
+ "digest": "ab6790d89875447de3d1c7f4713b102761bc3e9afdd714b818689e175ca03011"
+ },
+ {
+ "name": "flag_zw",
+ "unicode": "1F1FF-1F1FC",
+ "digest": "9d39b934fe922174b2250f2cd1b174a548d2904091d3298f35b7cc59fbceb181"
+ },
+ {
+ "name": "flags",
+ "unicode": "1F38F",
+ "digest": "c3f4a66786e524a5562919afcba9486113091ed205f1342e91d2f6439845ad61"
+ },
+ {
+ "name": "flashlight",
+ "unicode": "1F526",
+ "digest": "5f641b8fd1c7f1dcd43ec3b1ef78d14ef9929d723789c5567aca8b95d3d39803"
+ },
+ {
+ "name": "fleur-de-lis",
+ "unicode": "269C",
+ "digest": "d6ddeeea355ed55103b7fc65ac1ee0dbaa79d01e0d136b265363a6b92284c073"
+ },
+ {
+ "name": "flip_phone",
+ "unicode": "1F581",
+ "digest": "be59efba4bc0759af5a726c06619090ef5071bf2541611d71691dedecee6c697"
+ },
+ {
+ "name": "floppy_black",
+ "unicode": "1F5AA",
+ "digest": "9022f51bb09c5130c6d46bb2accb159bed6f54d6fbffda6ecad62965ebc958ea"
+ },
+ {
+ "name": "floppy_disk",
+ "unicode": "1F4BE",
+ "digest": "e987961ca516032a90942ef6c398836f2da68a5981714bd172acfe7b0e369d0a"
+ },
+ {
+ "name": "floppy_white",
+ "unicode": "1F5AB",
+ "digest": "ec79c400117c4506ef8cf3eebef6c42dd37e60b3079d3e98b6ccd06e517e2af0"
+ },
+ {
+ "name": "flower_playing_cards",
+ "unicode": "1F3B4",
+ "digest": "451f361050b96ba9ed8dc5b64c8a90c1316fd9b83fb818152881a54e100eea6c"
+ },
+ {
+ "name": "flushed",
+ "unicode": "1F633",
+ "digest": "39cf51f9dec2a910c66ecd39a7bd616fea09d67e81801e57e84f03ed1e917750"
+ },
+ {
+ "name": "fog",
+ "unicode": "1F32B",
+ "digest": "da6fdb9b682ed9a3368adcd7531f1a29e22755a620e3cca163fc3f33a6a78107"
+ },
+ {
+ "name": "foggy",
+ "unicode": "1F301",
+ "digest": "b599f3178db289c6e30017f3f0a9d30b00a75417057c7a10c0c9eedac78edbf1"
+ },
+ {
+ "name": "folder",
+ "unicode": "1F5C0",
+ "digest": "8932141321911032ce8469ba85fe309b78384545c3b9946978b383670b956644"
+ },
+ {
+ "name": "folder_open",
+ "unicode": "1F5C1",
+ "digest": "74f3b484771c3d6ef61cf003de25c1a59b875afa46c057b5b1d92d9f99460685"
+ },
+ {
+ "name": "football",
+ "unicode": "1F3C8",
+ "digest": "834fe5f431d6aa8ef1186aa79e71f813393535d273483b6af4cc4bdb8380e5b4"
+ },
+ {
+ "name": "footprints",
+ "unicode": "1F463",
+ "digest": "60dc938f6769ea21b05b5afcc481d3ddacf1f565e04f33310b271d5422e7ceb9"
+ },
+ {
+ "name": "fork_and_knife",
+ "unicode": "1F374",
+ "digest": "7e07c9dc555d172fa2eaa41cefd8d46d9624be0137aff196dd003a8a82610ec3"
+ },
+ {
+ "name": "fork_knife_plate",
+ "unicode": "1F37D",
+ "digest": "b4081b9edea6cdab5112fdd17535051ba17710953013f5020c7c40f84a1e3247"
+ },
+ {
+ "name": "fountain",
+ "unicode": "26F2",
+ "digest": "0acdca5e8f6d745a8d582d96012ec8fc55b9f5447e657ebfd998a4e332d99322"
+ },
+ {
+ "name": "four",
+ "unicode": "0034-20E3",
+ "digest": "36bd4ea6e2ae689835a79f8e60466eccd62fce7e91e84ed768cffd87dac628dd"
+ },
+ {
+ "name": "four_leaf_clover",
+ "unicode": "1F340",
+ "digest": "12ee2343df25bbd9077fdc12314c1edb51c0cdb556af7e22590e8a578ef57f17"
+ },
+ {
+ "name": "frame_photo",
+ "unicode": "1F5BC",
+ "digest": "6ff21063063989c6ae7dd69f4d6a781c676f9dba380d8e6f1dbac5d53b24f349"
+ },
+ {
+ "name": "frame_tiles",
+ "unicode": "1F5BD",
+ "digest": "34a5bb044b4b3ad94b116ad106f7b6747fb8612dc0e9f8ccd4313c2920508df0"
+ },
+ {
+ "name": "frame_x",
+ "unicode": "1F5BE",
+ "digest": "2e427688fd70361c8c59787d0722ad68abe1c3f968258ee99c0c77ce4b8a8e15"
+ },
+ {
+ "name": "free",
+ "unicode": "1F193",
+ "digest": "c1d9172a656717f78d941303c5da8790c6cd9827838d8f7dc3719afb53bcab80"
+ },
+ {
+ "name": "fried_shrimp",
+ "unicode": "1F364",
+ "digest": "c0c19e95f2c38f6cf870920bf3c2d4d69c36ea6e7dc9a5c45c3e8b285269d40a"
+ },
+ {
+ "name": "fries",
+ "unicode": "1F35F",
+ "digest": "0f546534684de29d319cbcbab4162acb321c4f8f3202fe17d69e1894ab7c8195"
+ },
+ {
+ "name": "frog",
+ "unicode": "1F438",
+ "digest": "6a417757fa6ee39e7a277cbd53c690ff88af0b1d76728d56f9bc645cb628aeb7"
+ },
+ {
+ "name": "frowning",
+ "unicode": "1F626",
+ "digest": "fb39f5c2aea98054adb02a3a0ac34a2e38d83f32cd590e9d2449e06a9702f2f5"
+ },
+ {
+ "name": "frowning2",
+ "unicode": "2639",
+ "digest": "7bb6c682a6c9f98bf3a5ae986e317fd26d1af497c857500deec2f06b6a3af5da"
+ },
+ {
+ "name": "fuelpump",
+ "unicode": "26FD",
+ "digest": "9cbb2646c93b255bd3de87dc01aa1193ab96e39a3013975d250472ab8aae61d6"
+ },
+ {
+ "name": "full_moon",
+ "unicode": "1F315",
+ "digest": "0b4f08ef2089397ead034b444a60e6e9810073454581b52a46b2369e3b9cd5f9"
+ },
+ {
+ "name": "full_moon_with_face",
+ "unicode": "1F31D",
+ "digest": "a371cb9e1f28a7db739dd058234642a2e333dff4b6df9882df85a6d984e4b5e8"
+ },
+ {
+ "name": "game_die",
+ "unicode": "1F3B2",
+ "digest": "6584909a4348c350c04417421b63eace1245087f7d239051b30a0cd37fe929f9"
+ },
+ {
+ "name": "gear",
+ "unicode": "2699",
+ "digest": "b0ff5fd007daa366a9eecb7422dbeb8a973e123a04267b88fef96c7453238294"
+ },
+ {
+ "name": "gem",
+ "unicode": "1F48E",
+ "digest": "d75d854f35975e4e291c3b9fcaf8437467f6d7eb27b29e2d7c0f0038fc666fe2"
+ },
+ {
+ "name": "gemini",
+ "unicode": "264A",
+ "digest": "392abe62872736a0bf92979a8c25a814985d0ff0a08dc7ab2a5c058aeda7e685"
+ },
+ {
+ "name": "ghost",
+ "unicode": "1F47B",
+ "digest": "f084b14483476e2d07563840f8c33b46da9c17f791da07fde3acffeb77342947"
+ },
+ {
+ "name": "gift",
+ "unicode": "1F381",
+ "digest": "c9a2ae6ea05c02e78e9567dcbd971701a2f869eb46c62d85cef23d0834388d8c"
+ },
+ {
+ "name": "gift_heart",
+ "unicode": "1F49D",
+ "digest": "e0c5aacf1ce89117d86b148f10a02dc18fe0cd22a75fbf6f0f88f2fad3ca80fe"
+ },
+ {
+ "name": "girl",
+ "unicode": "1F467",
+ "digest": "0758cbc4cbc7d72d6df8f66fc3a6b2b283c6634b053e59d61c6cac44cf8bffda"
+ },
+ {
+ "name": "girl_tone1",
+ "unicode": "1F467-1F3FB",
+ "digest": "7afdece55cb64e8056e2202de8c17b66ddb616f224ac374ec9a160d06b3138cc"
+ },
+ {
+ "name": "girl_tone2",
+ "unicode": "1F467-1F3FC",
+ "digest": "c160aa65fee70ad52930d01246ac9f282ff6abf1d93c5cc5b299fc257ee81db1"
+ },
+ {
+ "name": "girl_tone3",
+ "unicode": "1F467-1F3FD",
+ "digest": "b8a5687cd637855a41b8c7dc686f0e69fda379875408cd269f1b330a805c72f4"
+ },
+ {
+ "name": "girl_tone4",
+ "unicode": "1F467-1F3FE",
+ "digest": "a9cf743936b733634f323790a1abe3a410601b6841484baebea484b392f4e98e"
+ },
+ {
+ "name": "girl_tone5",
+ "unicode": "1F467-1F3FF",
+ "digest": "c902170e67b81eee35eeefb6a5c62c6109cb423dcae88d4e036ddd50b240c072"
+ },
+ {
+ "name": "girls_symbol",
+ "unicode": "1F6CA",
+ "digest": "2c55aee81defd7a1620ffeaad8d9bcc1835f19237c72c79633aec45671ddb9ff"
+ },
+ {
+ "name": "globe_with_meridians",
+ "unicode": "1F310",
+ "digest": "945646de3d8f057760fe374494a253d9a6aa8a132309154b0a5bdbffb5b20c3f"
+ },
+ {
+ "name": "goat",
+ "unicode": "1F410",
+ "digest": "f99cbc6755d119cb5c1dce08cabd20871f98d009bb773da4a146dae60476a235"
+ },
+ {
+ "name": "golf",
+ "unicode": "26F3",
+ "digest": "74a7876d185f8ff6a6533e4db2e1eb787119b2f8d8b07c36d99ec3163fb48485"
+ },
+ {
+ "name": "golfer",
+ "unicode": "1F3CC",
+ "digest": "6458295a5e4a6e4323c32a7f1f7182fb2d3918083839efc380d995860ce360b1"
+ },
+ {
+ "name": "grapes",
+ "unicode": "1F347",
+ "digest": "7f6873d65180ab476f49d207ac2d1f7dbaf6c8b0b561d50b64325e192cf97a86"
+ },
+ {
+ "name": "green_apple",
+ "unicode": "1F34F",
+ "digest": "effc3fe60f2ab704a034c794bfccfa023b41332f8f16ca44cc8ea41698f03873"
+ },
+ {
+ "name": "green_book",
+ "unicode": "1F4D7",
+ "digest": "6652c4d2ccfa4a287a5d45007bd06cadc16d34b0a1ca4b6b13b46f976c8d8319"
+ },
+ {
+ "name": "green_heart",
+ "unicode": "1F49A",
+ "digest": "f4bcb660a1d3cf3692238359d8b9de9a725a9af81f166253e487d61b8ccf9d86"
+ },
+ {
+ "name": "grey_exclamation",
+ "unicode": "2755",
+ "digest": "ac8cdab7496d133e7bc9475f2fdb0cf59b3ccba20f2f156c8b693e72b5948078"
+ },
+ {
+ "name": "grey_question",
+ "unicode": "2754",
+ "digest": "c173e1b2a16ab62b0abd7a58deb7a6df709b072d30d001627b92d0123a3a3e4a"
+ },
+ {
+ "name": "grimacing",
+ "unicode": "1F62C",
+ "digest": "8c54b73f5d2c1c6347e2c0ab01616519e0fb34490daa9c36664d442c6851c57e"
+ },
+ {
+ "name": "grin",
+ "unicode": "1F601",
+ "digest": "916eabdabd8b7ca698e638bbbd14affff97464ec11a3b59c0cb96cd7705600d8"
+ },
+ {
+ "name": "grinning",
+ "unicode": "1F600",
+ "digest": "3d8665c03f272ca3063e96145989926355a7ac315ed1a032d30fcefa6f0c3923"
+ },
+ {
+ "name": "guardsman",
+ "unicode": "1F482",
+ "digest": "ebbd29fa138005232d64fca4a8ec015d097fa14e6ded57b35ac257b4570b3c36"
+ },
+ {
+ "name": "guardsman_tone1",
+ "unicode": "1F482-1F3FB",
+ "digest": "b6082c8fee5dbc3ce2540f3939d5e344b5366c9f07827345facaba438e7017ff"
+ },
+ {
+ "name": "guardsman_tone2",
+ "unicode": "1F482-1F3FC",
+ "digest": "2b813afe1c2bbdaf9a47493393a0e6c400a16e453ed25a9a9c0035197927b56e"
+ },
+ {
+ "name": "guardsman_tone3",
+ "unicode": "1F482-1F3FD",
+ "digest": "49b2fa1ad0bc50a5ef6d73fb140aa1876506b9ebb9d45782ccb8dbb6818f8dde"
+ },
+ {
+ "name": "guardsman_tone4",
+ "unicode": "1F482-1F3FE",
+ "digest": "a584e1e3a8ad7be4871a6bdb7996d4f649abeaa77eb5d1cae998058d8b23ca0f"
+ },
+ {
+ "name": "guardsman_tone5",
+ "unicode": "1F482-1F3FF",
+ "digest": "e853b67ee13fda99e98f47083529ca80c404df1b19352c78b9c69850eb8f2c76"
+ },
+ {
+ "name": "guitar",
+ "unicode": "1F3B8",
+ "digest": "8c041b961649cc5917f56f2fb543f9a5280724647ed2fc67bc94a05eff9da805"
+ },
+ {
+ "name": "gun",
+ "unicode": "1F52B",
+ "digest": "d7f5aa657cc0ba04d878511820632b89c305a9b4d6c4a4b90ff691dad9906607"
+ },
+ {
+ "name": "haircut",
+ "unicode": "1F487",
+ "digest": "369dbab1b138c31d3eca04c950fdab4ec9f085272268c241f100d44e7b0f229e"
+ },
+ {
+ "name": "haircut_tone1",
+ "unicode": "1F487-1F3FB",
+ "digest": "c56f32d7c1d8a92d22429133f87f31a159818939cfdc570cb48b6d243cc58cf2"
+ },
+ {
+ "name": "haircut_tone2",
+ "unicode": "1F487-1F3FC",
+ "digest": "e916e040ffb8e869e930d1256343af2ad2bbaa683f01a11564d0777019944bec"
+ },
+ {
+ "name": "haircut_tone3",
+ "unicode": "1F487-1F3FD",
+ "digest": "f07cdfbea964ac42a9a050f832107ef0f2fa8115b27689f93d1be954de07b7c1"
+ },
+ {
+ "name": "haircut_tone4",
+ "unicode": "1F487-1F3FE",
+ "digest": "32ec7f5e999f7c43676768c8320ffaa346c713d340a94b948b1f564b345a2d11"
+ },
+ {
+ "name": "haircut_tone5",
+ "unicode": "1F487-1F3FF",
+ "digest": "5aad997d09e7975700927906d41a10bae774356ccddbe5197980bde670272262"
+ },
+ {
+ "name": "hamburger",
+ "unicode": "1F354",
+ "digest": "24ebae9a69cf283ab198499cb38d0cdcd82bac74c8e8d1e769ad78eb320a4294"
+ },
+ {
+ "name": "hammer",
+ "unicode": "1F528",
+ "digest": "a43a66b0efdc4cd2c84fd0ccc2cb8e9ede1f89c5d62eefa6ae521d3aed9d81b3"
+ },
+ {
+ "name": "hammer_pick",
+ "unicode": "2692",
+ "digest": "2e4fe33406ca03fbb0df1596d63e903d8ee6bd78ecc3ec38a67dd2cecbc584e2"
+ },
+ {
+ "name": "hamster",
+ "unicode": "1F439",
+ "digest": "f47da088ff5792532a382b6e3a47d2dd7c5e6fc19abd5ff6c5ba3ce420b4192e"
+ },
+ {
+ "name": "hand_splayed",
+ "unicode": "1F590",
+ "digest": "a43e52f7cdec5e9d51497888b0988d7bbd42846ad7e492b196293fbce576d197"
+ },
+ {
+ "name": "hand_splayed_reverse",
+ "unicode": "1F591",
+ "digest": "ff0af0fe9def7388adca6836e5958492282b1afae99f1b6e1e65d11ba68b96db"
+ },
+ {
+ "name": "hand_splayed_tone1",
+ "unicode": "1F590-1F3FB",
+ "digest": "73cceec7117280d330f8a149979190f0f355dd8d0a92821be89fb70344bb8dfe"
+ },
+ {
+ "name": "hand_splayed_tone2",
+ "unicode": "1F590-1F3FC",
+ "digest": "b06fac698128f4c3a7b8ea56e8bc4de088bb5461aa0f9c84553f16b43d347145"
+ },
+ {
+ "name": "hand_splayed_tone3",
+ "unicode": "1F590-1F3FD",
+ "digest": "a94ee9a2f8cdec6d2f7dd6887d1c7b8e064fcad63030c2c7c001742d72b5603e"
+ },
+ {
+ "name": "hand_splayed_tone4",
+ "unicode": "1F590-1F3FE",
+ "digest": "501792b4126c6f32e755accee0fc8b4d1915e1d36c4ceaa40f3bd0066efe76c3"
+ },
+ {
+ "name": "hand_splayed_tone5",
+ "unicode": "1F590-1F3FF",
+ "digest": "22ed533d587cf44f286e2d6ad77be20b4b5f133c422af4ca51e9af86a75002d8"
+ },
+ {
+ "name": "hand_victory",
+ "unicode": "1F594",
+ "digest": "2d512ced4e8a438f2a346aed67310d3080f9828c748ade1be95943c32ba1c735"
+ },
+ {
+ "name": "handbag",
+ "unicode": "1F45C",
+ "digest": "f1e2822c67f659b52c76821dd9db001332215a8566fc1846c89b6019c9758038"
+ },
+ {
+ "name": "hard_disk",
+ "unicode": "1F5B4",
+ "digest": "df8549d4281f5ae70fb6792a02c078e651764b0276aa43b7407236bd38fc21b4"
+ },
+ {
+ "name": "hash",
+ "unicode": "0023-20E3",
+ "digest": "5bd5c7180485fa71accdec5378bdc196ce0602f594f91e4eadc1e7514d5d0f90"
+ },
+ {
+ "name": "hatched_chick",
+ "unicode": "1F425",
+ "digest": "7995c3eb503a8b9662694eba80a9b551216473a31928091e35cd6ebc21cee083"
+ },
+ {
+ "name": "hatching_chick",
+ "unicode": "1F423",
+ "digest": "22905b42fa65dbc9aad8940d2db13691cacc62014f54e0960978ee0002178e1b"
+ },
+ {
+ "name": "head_bandage",
+ "unicode": "1F915",
+ "digest": "d690b740ff4f58e89dfc764c6411a4e84cfedffd7694eb5efa839a642dbabd08"
+ },
+ {
+ "name": "headphones",
+ "unicode": "1F3A7",
+ "digest": "219da138032c01c97a94f02b211049418191a3beb3d159804b9033f5916fd3c8"
+ },
+ {
+ "name": "hear_no_evil",
+ "unicode": "1F649",
+ "digest": "8120060238eaca645809dd113862a144f10395afcb3837ab60c0f04009b49a2f"
+ },
+ {
+ "name": "heart",
+ "unicode": "2764",
+ "digest": "a646a25a36f431cadc7e56afd1a4d1b7cbae5292a25d7783bd31462d0d3d719b"
+ },
+ {
+ "name": "heart_decoration",
+ "unicode": "1F49F",
+ "digest": "a83989669347c98cb74065d4f0befedbc37f82c91214e773245cb6810ab359b4"
+ },
+ {
+ "name": "heart_exclamation",
+ "unicode": "2763",
+ "digest": "9751c89dcf10805f2011949ff3ddcb6bcb13de8c32ae5de9e03955e8a4235df2"
+ },
+ {
+ "name": "heart_eyes",
+ "unicode": "1F60D",
+ "digest": "335ea73efca4824e623a5a51ccdb494c8b1f5f10b4139b39b250a2a771876b0d"
+ },
+ {
+ "name": "heart_eyes_cat",
+ "unicode": "1F63B",
+ "digest": "9346b85afb80f7b498cc255426ea15a287f81d8fb3c26dab61337635f439d3ce"
+ },
+ {
+ "name": "heart_tip",
+ "unicode": "1F394",
+ "digest": "2178829e2c85accda55d2f685544587f6de5c8398a127ae1e08ff1c4ab282204"
+ },
+ {
+ "name": "heartbeat",
+ "unicode": "1F493",
+ "digest": "cd6921ce55c155873220a09416d695c4bcca1556007066d6d185e93d6561e825"
+ },
+ {
+ "name": "heartpulse",
+ "unicode": "1F497",
+ "digest": "f869357b9e678d9671ec38c569fc88efec48006c159b69297277cee795dc4dc9"
+ },
+ {
+ "name": "hearts",
+ "unicode": "2665",
+ "digest": "17dc9b2941561f58ca0f04d0754b1eff3490b63b17241580b3d4aa4638fa85e8"
+ },
+ {
+ "name": "heavy_check_mark",
+ "unicode": "2714",
+ "digest": "b5fa24f6e0f1dcbd6278e9125154522f2efd79e6dd0836ccb792a1f3aeeff2b2"
+ },
+ {
+ "name": "heavy_division_sign",
+ "unicode": "2797",
+ "digest": "59a6983d788f347c64eecb3df6f7d3b36779d92df6cc811820993ff9e18d77e1"
+ },
+ {
+ "name": "heavy_dollar_sign",
+ "unicode": "1F4B2",
+ "digest": "d2e89c54b3fdeda4d1fd4d29454b69dcf750181110894e6e71a40df99c95bfe8"
+ },
+ {
+ "name": "heavy_minus_sign",
+ "unicode": "2796",
+ "digest": "dd5ab3722fe49cfdbc5e1fbab5b342dc960de7b412d4fba59d66e06ce3dc3bcd"
+ },
+ {
+ "name": "heavy_multiplication_x",
+ "unicode": "2716",
+ "digest": "7d77742f91377785675802f40bd8dde9bd1feeb513735760a58ea9bee8a65d44"
+ },
+ {
+ "name": "heavy_plus_sign",
+ "unicode": "2795",
+ "digest": "9aa9dcdbba120a4b485c21f67589609b789c6e3edf08479ff8268fa0db973ad7"
+ },
+ {
+ "name": "helicopter",
+ "unicode": "1F681",
+ "digest": "b259ea8d2bdca36766075894da650b1d3ff4c8602259cd0d30cb8214cd585340"
+ },
+ {
+ "name": "helmet_with_cross",
+ "unicode": "26D1",
+ "digest": "affbe9dd87b87ff9235b4858c59c2a73e9ed30dd5221e5b666b8d7747378a9c4"
+ },
+ {
+ "name": "herb",
+ "unicode": "1F33F",
+ "digest": "3c452106b1966f643751bf161fa7d1762a33e6fff381b2109bb53b55c4fdd129"
+ },
+ {
+ "name": "hibiscus",
+ "unicode": "1F33A",
+ "digest": "268963a1f3cdad9050d9ae31c558e010f33812e3b09bbf9088ba876c033d8b2f"
+ },
+ {
+ "name": "high_brightness",
+ "unicode": "1F506",
+ "digest": "d607f6269d95dd16c2a7932e49ac09e44f4c19e0a34f6c0f21ecb945a2316361"
+ },
+ {
+ "name": "high_heel",
+ "unicode": "1F460",
+ "digest": "5c320d5954bf4f4dacacddd562c1598ab101731077a6656ac5d2bfd41405483e"
+ },
+ {
+ "name": "hockey",
+ "unicode": "1F3D2",
+ "digest": "008904c1b8db139215492a6d96c09f2c3eeda769f858a9bbae13f8c54d439d0e"
+ },
+ {
+ "name": "hole",
+ "unicode": "1F573",
+ "digest": "36bbafa5e89b1410ec74919aaf60b09ac3525a421cb5b475b9bb2f20357db8de"
+ },
+ {
+ "name": "homes",
+ "unicode": "1F3D8",
+ "digest": "9980d6dd6cbd23b820747ecac4cb10974dd24b0c94b4acfe21fa87793ad065c9"
+ },
+ {
+ "name": "honey_pot",
+ "unicode": "1F36F",
+ "digest": "94cb1624491076b5cb145e7a309f91a7be3d4c0bed712af6a51d641eb73edee7"
+ },
+ {
+ "name": "horse",
+ "unicode": "1F434",
+ "digest": "624ad9dc9ed7af3f6e1a2f9d4ed483702ae64ed5fbcf5e9918af6bfef24e76f9"
+ },
+ {
+ "name": "horse_racing",
+ "unicode": "1F3C7",
+ "digest": "c2702b7225e9839a789dda7c43f0cc86dced2b4d5d3787116106396633362de6"
+ },
+ {
+ "name": "horse_racing_tone1",
+ "unicode": "1F3C7-1F3FB",
+ "digest": "a7ed284f9d5cd8a4fe4a09cb91c3f99e5db99c7e31c5f525c14de97b06857d92"
+ },
+ {
+ "name": "horse_racing_tone2",
+ "unicode": "1F3C7-1F3FC",
+ "digest": "20b4d61b21ee6ba860b029f0ad0e38f5ecb6dd2c774f7b7801fba07ed33f96be"
+ },
+ {
+ "name": "horse_racing_tone3",
+ "unicode": "1F3C7-1F3FD",
+ "digest": "dd65f7bb96ee44507d26e524202d567d2d7679d571245299a2a84f68bd5def4c"
+ },
+ {
+ "name": "horse_racing_tone4",
+ "unicode": "1F3C7-1F3FE",
+ "digest": "36afaad218a4c820b19c7c9bbbc187119d47b41273d8f48ab14cc3e32dd7c21f"
+ },
+ {
+ "name": "horse_racing_tone5",
+ "unicode": "1F3C7-1F3FF",
+ "digest": "2e0efd501a4471428533ce7909972a49ff045369261c27e4abb97ee2aede2f47"
+ },
+ {
+ "name": "hospital",
+ "unicode": "1F3E5",
+ "digest": "df5c774fa36b2601e6960a7b81cdfac71c1d2d71f04dea88068d1c9043e313bb"
+ },
+ {
+ "name": "hot_pepper",
+ "unicode": "1F336",
+ "digest": "62e4dade3c793f6d83530bd1f60f3e3e26c1e10a41786c3a15f5aec0ff2b8e76"
+ },
+ {
+ "name": "hotdog",
+ "unicode": "1F32D",
+ "digest": "58b829e26b5c4642942898d9c7873cb08e048fd7deaacba8292899d5d895cb2b"
+ },
+ {
+ "name": "hotel",
+ "unicode": "1F3E8",
+ "digest": "428120a35b38a217901e10d704751eb8fdbc9f805e6eccd8aab070f4311b2085"
+ },
+ {
+ "name": "hotsprings",
+ "unicode": "2668",
+ "digest": "df4f946218445f97a6f28c6abe4c1d1dac56ff97a8cd81df59f1b3c320e0092f"
+ },
+ {
+ "name": "hourglass",
+ "unicode": "231B",
+ "digest": "07aece9413e6898717b4f0757e073d7a593f3e8044c56855127033b796207ccb"
+ },
+ {
+ "name": "hourglass_flowing_sand",
+ "unicode": "23F3",
+ "digest": "92dbc68e9d16fb9f706236367e1882f0d2b6817b83ca490820a000021f2c6483"
+ },
+ {
+ "name": "house",
+ "unicode": "1F3E0",
+ "digest": "a6221fc84a9b0e11ae71bfa1e0020982b55ff8c89a374a6d755dba710b4e058c"
+ },
+ {
+ "name": "house_abandoned",
+ "unicode": "1F3DA",
+ "digest": "e404631e3a296bdeae3de7510da8934c32327bc0fa0f7ae4e676b61932165668"
+ },
+ {
+ "name": "house_with_garden",
+ "unicode": "1F3E1",
+ "digest": "22d0d911da96b7ae3bf6692d3cf3590afbca959fc99c13e7a088f7194f43a35d"
+ },
+ {
+ "name": "hugging",
+ "unicode": "1F917",
+ "digest": "68ed6c4e0eae9071cf67770a39e07a2290b4f7763170f765b3cd3ac67ae43240"
+ },
+ {
+ "name": "hushed",
+ "unicode": "1F62F",
+ "digest": "69faa8e0b170ee8cf41977ca4a5154406360ed9699d5c62ecdaa01f50e8e4276"
+ },
+ {
+ "name": "ice_cream",
+ "unicode": "1F368",
+ "digest": "d48ec98a8789148b96c30f19595201a0f85ed899659d97d1d3596091162909ff"
+ },
+ {
+ "name": "ice_skate",
+ "unicode": "26F8",
+ "digest": "6fb044d9fbe62605f6728062c35c345ddd3ae4cc51203c925b0e69f1b3ef2dbf"
+ },
+ {
+ "name": "icecream",
+ "unicode": "1F366",
+ "digest": "abd5774157575dd304dc1a393244757853972c863861a654ca29b2d528e48b28"
+ },
+ {
+ "name": "id",
+ "unicode": "1F194",
+ "digest": "860ffb36d37d84e2c1cf0ab991b95c1cf73e458bef0e4d85bb0c1e26115cb2d1"
+ },
+ {
+ "name": "ideograph_advantage",
+ "unicode": "1F250",
+ "digest": "37892a5642cd49ef7828646f36f48b5a83dc02437624c05da428579256118030"
+ },
+ {
+ "name": "imp",
+ "unicode": "1F47F",
+ "digest": "f8c93d03bd9f1d5ef86738541e11695d6811bf6fef06759eba98321b6d038814"
+ },
+ {
+ "name": "inbox_tray",
+ "unicode": "1F4E5",
+ "digest": "066a2d75633eb50329496f6866b5b0645c2e48135a03118f1bf53244f8529043"
+ },
+ {
+ "name": "incoming_envelope",
+ "unicode": "1F4E8",
+ "digest": "ef6e5c5aa679d174181dae77113717f26e295778dde1e2c3bdf1d64de8a4af8c"
+ },
+ {
+ "name": "info",
+ "unicode": "1F6C8",
+ "digest": "59c35e77d5ee663c5d56f7d8af845ce8aeb9935e526ae4a06e02ae70e71212ca"
+ },
+ {
+ "name": "information_desk_person",
+ "unicode": "1F481",
+ "digest": "acae6d272e348aee87dd60360f16ac58cea7cb4e1ea962cc1655005c7f4aed27"
+ },
+ {
+ "name": "information_desk_person_tone1",
+ "unicode": "1F481-1F3FB",
+ "digest": "709ebb0481ca981d76ece2d4fc68db693ddf18b9c1aaa0b6ac5d3c42e71bf07f"
+ },
+ {
+ "name": "information_desk_person_tone2",
+ "unicode": "1F481-1F3FC",
+ "digest": "d5bc3563bc721d66b73850db93ac827be3715e7ca6420dc0051396ffe26bef47"
+ },
+ {
+ "name": "information_desk_person_tone3",
+ "unicode": "1F481-1F3FD",
+ "digest": "af67fd4ef2fc402bec2d446b2e8ff5e9f636b5a9bbb6639587cdb88bd780d265"
+ },
+ {
+ "name": "information_desk_person_tone4",
+ "unicode": "1F481-1F3FE",
+ "digest": "fd3174d1adfe13e8c0d6b6ae9c3a26ea35bb40f98f0728f91d1798809a74933b"
+ },
+ {
+ "name": "information_desk_person_tone5",
+ "unicode": "1F481-1F3FF",
+ "digest": "4b773c443830a02de8b4d6471077b5d1387b560b537cabba7cdc667110cbde69"
+ },
+ {
+ "name": "information_source",
+ "unicode": "2139",
+ "digest": "50cd8bf46d20b7c18d5f00a69fc79452aa32934245ba8d0929e51632d73876bd"
+ },
+ {
+ "name": "innocent",
+ "unicode": "1F607",
+ "digest": "a3510fd51c17093ebe2371cfde7611aa44aed2d120a0e5500cfaae0f1d3486a4"
+ },
+ {
+ "name": "interrobang",
+ "unicode": "2049",
+ "digest": "1f843ff672486154f9f3df549bb1b528a5eac8d15264f447649ba57f45ee4d00"
+ },
+ {
+ "name": "iphone",
+ "unicode": "1F4F1",
+ "digest": "be6f96c02ddae557f700fd20fe7b3f94c9e1c928acb82b2b8b214d231273fece"
+ },
+ {
+ "name": "island",
+ "unicode": "1F3DD",
+ "digest": "17f02b309b62ed9542b1d8943168302846040e420f413e56d799bb5fba7064fa"
+ },
+ {
+ "name": "izakaya_lantern",
+ "unicode": "1F3EE",
+ "digest": "ddb20f475aa119c3a64a55dff40f7a9dbc3a14f7ffc6cfbac89210c652f10d02"
+ },
+ {
+ "name": "jack_o_lantern",
+ "unicode": "1F383",
+ "digest": "62a701ac472619bcb3859e0d9a61b98c7f5c32150d2d04ca8c3e8fc3bec4dbd5"
+ },
+ {
+ "name": "japan",
+ "unicode": "1F5FE",
+ "digest": "2535300fff2b2e4b75fc73c187be6c0ea4bc4753e443db498ea55e268e627ab7"
+ },
+ {
+ "name": "japanese_castle",
+ "unicode": "1F3EF",
+ "digest": "70645aa05599e23a9ac4327e4a2e78bffe7ea06c38ec1935c15ae420619c5c1c"
+ },
+ {
+ "name": "japanese_goblin",
+ "unicode": "1F47A",
+ "digest": "59b6901dc6eedc6509c25b4eef6702bf461ded06c5ff12fe2a02a5b3301577c0"
+ },
+ {
+ "name": "japanese_ogre",
+ "unicode": "1F479",
+ "digest": "dab7e68cd4cbf99c13d64792c7104c4f0a846bc63aa12950fa8fab028dca301d"
+ },
+ {
+ "name": "jeans",
+ "unicode": "1F456",
+ "digest": "ddd032ac77cdfe49152a0e0a0eaaaea9f183590fb1f493ec30e9e39f679e3914"
+ },
+ {
+ "name": "jet_up",
+ "unicode": "1F6E6",
+ "digest": "3708e5e034b1c64d1268d66527e13c369aa0f8903bce9172bef773b2d1940948"
+ },
+ {
+ "name": "joy",
+ "unicode": "1F602",
+ "digest": "f90cfbcb14f906f8d786b61f022c978f381fc99ca422805f605631314e101805"
+ },
+ {
+ "name": "joy_cat",
+ "unicode": "1F639",
+ "digest": "6ca24a94490de66d1ca2cbc080bcd805f54ca295051d8e6588cae3fe6658c80a"
+ },
+ {
+ "name": "joystick",
+ "unicode": "1F579",
+ "digest": "ec172df88ef8e8a5512d6d906c13296875b7057ed0cca79f4ac8cddd9e1de34b"
+ },
+ {
+ "name": "kaaba",
+ "unicode": "1F54B",
+ "digest": "30f1a27a148399bbb811586eff795eff858701c42055c23e4d5bef7ae77f5f32"
+ },
+ {
+ "name": "key",
+ "unicode": "1F511",
+ "digest": "c68ed648350d3976c8d27a709020c8873ecf553929e66453acff96231684a1a2"
+ },
+ {
+ "name": "key2",
+ "unicode": "1F5DD",
+ "digest": "87a7d42531d7a11dcb11b0d6d1be611ee8cec35b5d22226a8ac6083fedef4f5d"
+ },
+ {
+ "name": "keyboard",
+ "unicode": "1F5AE",
+ "digest": "3b254cbf19946df3af05e501d11653d89fcda91684b7248d86186f842b83bf16"
+ },
+ {
+ "name": "keyboard_mouse",
+ "unicode": "1F5A6",
+ "digest": "95b523e55d8afeaeb06442bbe20e47f49643bb0c32d89a8cdbbccdead20532b3"
+ },
+ {
+ "name": "keyboard_with_jacks",
+ "unicode": "1F398",
+ "digest": "e29a0d0b8018d13458469edca13c60a882a2817957c1aa11b050684c995a47ee"
+ },
+ {
+ "name": "keycap_ten",
+ "unicode": "1F51F",
+ "digest": "7593aa7ffe7192a2e35c6ccec76522f6243777783c9152c7c03419835ea58c03"
+ },
+ {
+ "name": "kimono",
+ "unicode": "1F458",
+ "digest": "e92bea044fe013f1993c2229d86e9cca9d43f14aab00564ce6ff559bdc5ce93a"
+ },
+ {
+ "name": "kiss",
+ "unicode": "1F48B",
+ "digest": "c060eb09af2a0d0f77d307b995c15719b0e59c9162a490b8a553fac9b779c8f0"
+ },
+ {
+ "name": "kiss_mm",
+ "unicode": "1F468-2764-1F48B-1F468",
+ "digest": "381364ad988ec07cc3708fd60f71838092224009088fff587069b4e8ab01ee63"
+ },
+ {
+ "name": "kiss_ww",
+ "unicode": "1F469-2764-1F48B-1F469",
+ "digest": "7705ca707b73f44c856ea324bdfe30ed05244c8d192d1111f6e1d62ab3f2f8a5"
+ },
+ {
+ "name": "kissing",
+ "unicode": "1F617",
+ "digest": "3142617e8b9488689bd9efc67c0e4cc71a1870df8ffc308f949eedc5c3684051"
+ },
+ {
+ "name": "kissing_cat",
+ "unicode": "1F63D",
+ "digest": "ed26cee8c438ba41365b55c48457cdad3e8d43bf90db3128ac5b277718b82ed3"
+ },
+ {
+ "name": "kissing_closed_eyes",
+ "unicode": "1F61A",
+ "digest": "22d3369d21b4c2cb4c0c2cab9551cd848dd4f9adecfa64977d3f1a80fc0c8b53"
+ },
+ {
+ "name": "kissing_heart",
+ "unicode": "1F618",
+ "digest": "1f089b07447bdcc1baada6a2a9607d4ef4f2de9a6093fcab47a553a64b9acb76"
+ },
+ {
+ "name": "kissing_smiling_eyes",
+ "unicode": "1F619",
+ "digest": "e37d282861669adfa3953b9af833acfab7d55e787621d4318d77de7e3529d5c5"
+ },
+ {
+ "name": "knife",
+ "unicode": "1F52A",
+ "digest": "3fef068a6ada61630dc868e47d25e0e0550b44bc7cf530afe88ca63dc7ab2a39"
+ },
+ {
+ "name": "koala",
+ "unicode": "1F428",
+ "digest": "fe020ab9048f3c2a881474f8b1335db6bfaf37d115ff9b2d264f668d136122dd"
+ },
+ {
+ "name": "koko",
+ "unicode": "1F201",
+ "digest": "734a5cb296826a598e02be3f4ec22f318633ede2ce274914586256421e2df97b"
+ },
+ {
+ "name": "label",
+ "unicode": "1F3F7",
+ "digest": "9fe8195c3efab4d905b1cfcba0ae58cda12496030b0908de8076ff5e6777742e"
+ },
+ {
+ "name": "large_blue_circle",
+ "unicode": "1F535",
+ "digest": "ba4d0f84a9c2be9a65b25c8cfa78f30d4856d021b1853154dd1d2fd0c5bcfb6a"
+ },
+ {
+ "name": "large_blue_diamond",
+ "unicode": "1F537",
+ "digest": "d5aa5e315126859c10c83507be6b9e11cbf423f7a27145de089468cff9b94a94"
+ },
+ {
+ "name": "large_orange_diamond",
+ "unicode": "1F536",
+ "digest": "108600badd0ef267842325c0fbf326cb3504306332c64f6f5694de2b54c9438a"
+ },
+ {
+ "name": "last_quarter_moon",
+ "unicode": "1F317",
+ "digest": "68315b85bc1cb17bb82629bd1a6024a5124f3641b9878a732a8aad016c587546"
+ },
+ {
+ "name": "last_quarter_moon_with_face",
+ "unicode": "1F31C",
+ "digest": "146a419109b7f662bf87cf9de299e47d025a8758c8970b7dabf3483e1956b559"
+ },
+ {
+ "name": "laughing",
+ "unicode": "1F606",
+ "digest": "f22d3be77f1daf058d04c3cbc1fd7f76b4dc069d2d300b45e63e768b08d269c5"
+ },
+ {
+ "name": "leaves",
+ "unicode": "1F343",
+ "digest": "f65e2db125564eb04fc427a49fff175d6e2dae847bd12314d5e6a131610d5ccd"
+ },
+ {
+ "name": "ledger",
+ "unicode": "1F4D2",
+ "digest": "62df1772cec10c035ae0646e6cca4ba7d75b10636a520d091c5b42c2dc36b742"
+ },
+ {
+ "name": "left_luggage",
+ "unicode": "1F6C5",
+ "digest": "62292758715115e55ab6239805b7f99b7b35bdfa8d40da07fe391424f1f083d8"
+ },
+ {
+ "name": "left_receiver",
+ "unicode": "1F57B",
+ "digest": "8052e44951afee04c87296128744b5019ec783c9ed1a231f659af6c8ddaa50f3"
+ },
+ {
+ "name": "left_right_arrow",
+ "unicode": "2194",
+ "digest": "28a6945972451b1f4dadec5c55310b8868ffd9f3b0a07803287bc4e07a56e7d4"
+ },
+ {
+ "name": "leftwards_arrow_with_hook",
+ "unicode": "21A9",
+ "digest": "d672afc39fd50f78d7370be243173fe76ba50292f0c401305b562898939a8b7f"
+ },
+ {
+ "name": "lemon",
+ "unicode": "1F34B",
+ "digest": "e0e293a8b8c1b3c87534f5e05cf006671eb3c6d52b4d17d40f2e23bce215a8be"
+ },
+ {
+ "name": "leo",
+ "unicode": "264C",
+ "digest": "b0fd4e5f4637de530b62323521c6edcd80312d67ea4043eedd959acb6763474a"
+ },
+ {
+ "name": "leopard",
+ "unicode": "1F406",
+ "digest": "ede891be8484a17e6277431c64ec1bfd6b742544a41947ebc85005bc2d558bb1"
+ },
+ {
+ "name": "level_slider",
+ "unicode": "1F39A",
+ "digest": "49777cf160d9130d723e3bfef765c3de54033e6b059000fb0e22fb559b5ed190"
+ },
+ {
+ "name": "levitate",
+ "unicode": "1F574",
+ "digest": "3e4e9a5ac6a8dbd7909c58a9d915f16f1a0fc59cc019714ae5935f18e4704044"
+ },
+ {
+ "name": "libra",
+ "unicode": "264E",
+ "digest": "ec8e2e7a735abc9f2bddb115fc0e09f4bdc7a164679e2b57d127f58eee1155c2"
+ },
+ {
+ "name": "lifter",
+ "unicode": "1F3CB",
+ "digest": "f64db037fd21e5918e5de35d6a561ef4b44668e307ed351338de00fcf3e771e3"
+ },
+ {
+ "name": "lifter_tone1",
+ "unicode": "1F3CB-1F3FB",
+ "digest": "f9e0d161b12c4908ac3409b11c1a77ee38f33ba018f12416545876214bfb7c01"
+ },
+ {
+ "name": "lifter_tone2",
+ "unicode": "1F3CB-1F3FC",
+ "digest": "631eb6ed5bd147dc6f1f8b94149abe44d62a0f78e7809e37a4bfe127c40ed98f"
+ },
+ {
+ "name": "lifter_tone3",
+ "unicode": "1F3CB-1F3FD",
+ "digest": "406b5707a47d9066f016acf0b64fa695e3505acc2453758a0428de21efd7eb6d"
+ },
+ {
+ "name": "lifter_tone4",
+ "unicode": "1F3CB-1F3FE",
+ "digest": "d917164ed8c4bb1ffcc887ca256ec329e7fa1b9516eaf8c159f8b43fdb071ed6"
+ },
+ {
+ "name": "lifter_tone5",
+ "unicode": "1F3CB-1F3FF",
+ "digest": "f79ea93e8a40b3c895b693bf49eb4ce6e7b3f4413595e5881ea44839fd7fe8e5"
+ },
+ {
+ "name": "light_check_mark",
+ "unicode": "1F5F8",
+ "digest": "7842b0df8c2b6703bed0cce5d2790d394eec7120b2a245a76f375528f2729a7b"
+ },
+ {
+ "name": "light_rail",
+ "unicode": "1F688",
+ "digest": "7c2be55456f1332e849ff6699a26dda2e1641c280f45c9ec88dedf6d9b7b7fe2"
+ },
+ {
+ "name": "link",
+ "unicode": "1F517",
+ "digest": "cc4873f8a612dd721dddcd507a4430b4fb6c4abc15a8848456f0ffd97811b163"
+ },
+ {
+ "name": "lion_face",
+ "unicode": "1F981",
+ "digest": "935b1076815f51fafcd860a395d0a03c536acfcea61ffcf542a377da046fa7d9"
+ },
+ {
+ "name": "lips",
+ "unicode": "1F444",
+ "digest": "e3bc20f9e210fa1711271234fe61bf1c9ddf36dd6ffc5b832c6c3a769a1e59a8"
+ },
+ {
+ "name": "lips2",
+ "unicode": "1F5E2",
+ "digest": "c6ba915982ac47d8aaf14ad3605949df95588acfb4e147bf608f8c1714cdf19b"
+ },
+ {
+ "name": "lipstick",
+ "unicode": "1F484",
+ "digest": "335b912e163020df3d6d9f0a19a55d6547bd59b471c5a3e374c2968e49911ccc"
+ },
+ {
+ "name": "lock",
+ "unicode": "1F512",
+ "digest": "c20eacfb8ccd9bb85919a837c0d4650ee608edb48c85bff46945f613e95d7038"
+ },
+ {
+ "name": "lock_with_ink_pen",
+ "unicode": "1F50F",
+ "digest": "5cab25cea08e22d9c3f5de16de6d0ab658ca15cc93d7830f29b0f3e9348ec45f"
+ },
+ {
+ "name": "lollipop",
+ "unicode": "1F36D",
+ "digest": "33d2334a00bf0e15869ccc75fadc36f27f89abf0525bb71f859aad9e1dc4ad66"
+ },
+ {
+ "name": "loop",
+ "unicode": "27BF",
+ "digest": "fa1174ddc44e317d0796e07868c7ac8ac9c9274fbc8a6c3d0ec78d543c3c6bf0"
+ },
+ {
+ "name": "loud_sound",
+ "unicode": "1F50A",
+ "digest": "fb70229e13b690ffc1031d2e631123f8c908035a15218c297c1c4a3ff3624aa0"
+ },
+ {
+ "name": "loudspeaker",
+ "unicode": "1F4E2",
+ "digest": "e2d6cf9ec6412ee62f3128a1afd8c63ec74755c4833f01a4f99722407fe154d6"
+ },
+ {
+ "name": "love_hotel",
+ "unicode": "1F3E9",
+ "digest": "184670ebc4045043a7b18d576da3255d216551da522a11cde7df34524e9c7d50"
+ },
+ {
+ "name": "love_letter",
+ "unicode": "1F48C",
+ "digest": "9a4c52e2622fc7d364995ebc93ca530d972134621d117b72053a659dffc90ffc"
+ },
+ {
+ "name": "low_brightness",
+ "unicode": "1F505",
+ "digest": "c177b7fa9fdbef959cc47e7d16becd71117470b767a81ed6d15f80f464776c02"
+ },
+ {
+ "name": "m",
+ "unicode": "24C2",
+ "digest": "2eaf011e74d69613923dad424daaec4c13b592388dbcc5757b645bc058eedecb"
+ },
+ {
+ "name": "mag",
+ "unicode": "1F50D",
+ "digest": "029427bd73d2c79fffc5194ded01f6011952ec0124b7634c6230e0afa7ad7c95"
+ },
+ {
+ "name": "mag_right",
+ "unicode": "1F50E",
+ "digest": "f99de50bb59ec3bf1d4ccb8584ca09d4a7ceb5bf9f600ea8d3f84930efbf01b8"
+ },
+ {
+ "name": "mahjong",
+ "unicode": "1F004",
+ "digest": "da5d1fa980c38e092d414516161ca26046aa65ace3261999ea750f72e676ac6e"
+ },
+ {
+ "name": "mailbox",
+ "unicode": "1F4EB",
+ "digest": "14217df8f39a95fc0a0c527f97db1ca8564764034e921614decc5be705629352"
+ },
+ {
+ "name": "mailbox_closed",
+ "unicode": "1F4EA",
+ "digest": "e0c7beb205ec548a66d8afc7f103b64c6c79c08417ab550f19c36cc6d1a62bc4"
+ },
+ {
+ "name": "mailbox_with_mail",
+ "unicode": "1F4EC",
+ "digest": "6d381c0c4181be628d9409df1d85f8a9438c21ef5b92d82ef8ae1ff0079236de"
+ },
+ {
+ "name": "mailbox_with_no_mail",
+ "unicode": "1F4ED",
+ "digest": "74843d5ea9e03b48323f2252bdd000585f549b7fffe1fe181a25c38b99b5e23d"
+ },
+ {
+ "name": "man",
+ "unicode": "1F468",
+ "digest": "0275935258b4c832c3fcb06531d3e6972e2c3d46bab2973004750a9f00bd4cb6"
+ },
+ {
+ "name": "man_tone1",
+ "unicode": "1F468-1F3FB",
+ "digest": "1f6603d040f4a025f49d384170dd16b8da169663fc3282af1dc8710d9c1a7adf"
+ },
+ {
+ "name": "man_tone2",
+ "unicode": "1F468-1F3FC",
+ "digest": "d65bb03071b483946c69c61769d19b29a2af76fa7e43020e55f0bbc046492221"
+ },
+ {
+ "name": "man_tone3",
+ "unicode": "1F468-1F3FD",
+ "digest": "9af8ede7211b19a7dc0c60db083dd2bdc4897dda4d71e57feadf2e39d847f060"
+ },
+ {
+ "name": "man_tone4",
+ "unicode": "1F468-1F3FE",
+ "digest": "6555de60976aafeb024db78addb44eab2a412dd7277013f44d06757d03b6a252"
+ },
+ {
+ "name": "man_tone5",
+ "unicode": "1F468-1F3FF",
+ "digest": "b58b97a28a6adc1777acc05194cd917c730f90e37441124c384ded12e9a7d2a4"
+ },
+ {
+ "name": "man_with_gua_pi_mao",
+ "unicode": "1F472",
+ "digest": "88663173a6ccbebec5e24883c90d965447e022c6688773273110fe544d5b1607"
+ },
+ {
+ "name": "man_with_gua_pi_mao_tone1",
+ "unicode": "1F472-1F3FB",
+ "digest": "3c8bad3923a619f888e14544d357499a26a517e8fbe7a51027117b960c9eb842"
+ },
+ {
+ "name": "man_with_gua_pi_mao_tone2",
+ "unicode": "1F472-1F3FC",
+ "digest": "da125a3310fab19c9282497d53e2fc71ad07920ce60a0ef52dcdb31500023f09"
+ },
+ {
+ "name": "man_with_gua_pi_mao_tone3",
+ "unicode": "1F472-1F3FD",
+ "digest": "1d5842558847367966bf3ea473ff80fe744359bc5d969f4cc06cf2e452ed2fb6"
+ },
+ {
+ "name": "man_with_gua_pi_mao_tone4",
+ "unicode": "1F472-1F3FE",
+ "digest": "92be490f3ba602a43e2be8160d8bfd8a0691b2f81fe017b06df10f476a89ffab"
+ },
+ {
+ "name": "man_with_gua_pi_mao_tone5",
+ "unicode": "1F472-1F3FF",
+ "digest": "669f6b31bc7a8bf50b169d0600f14e00addaeb24144a1bace8b94950372839b0"
+ },
+ {
+ "name": "man_with_turban",
+ "unicode": "1F473",
+ "digest": "87d30d35ba40ee39c2df8ce19d975ce34a9c54688bafeac7377d7d481e55f1a4"
+ },
+ {
+ "name": "man_with_turban_tone1",
+ "unicode": "1F473-1F3FB",
+ "digest": "33b8b8154e0691e2ad66177dbf1e0101411fd8b3a16bf4e54c36d4a874f2a275"
+ },
+ {
+ "name": "man_with_turban_tone2",
+ "unicode": "1F473-1F3FC",
+ "digest": "1a6b83faa8d6e6a7d12a04898a6f22243287330a1faa081d2626b17dfb07174d"
+ },
+ {
+ "name": "man_with_turban_tone3",
+ "unicode": "1F473-1F3FD",
+ "digest": "5d43da5109e688ff8ca0675f33ebbaf930e206f1f01e3ee773f2844663fe572b"
+ },
+ {
+ "name": "man_with_turban_tone4",
+ "unicode": "1F473-1F3FE",
+ "digest": "bfaf7293c5ea75d0ecdc6fe5afe8f48e7b29b2e0df06ef974d3e1732f5db5dd4"
+ },
+ {
+ "name": "man_with_turban_tone5",
+ "unicode": "1F473-1F3FF",
+ "digest": "fba2404dd3d7eab5268519894cc0b386e1b17fdf14a04760c346014aa0e25acd"
+ },
+ {
+ "name": "mans_shoe",
+ "unicode": "1F45E",
+ "digest": "45dc13ac44c922b4c4b8ecb2e1a870a78e09d53da86843431ab0e9ec96ebcd97"
+ },
+ {
+ "name": "map",
+ "unicode": "1F5FA",
+ "digest": "f56116d09996d6d08fb5cdfb46622b545253f2649008170fc2011a9713fa875b"
+ },
+ {
+ "name": "maple_leaf",
+ "unicode": "1F341",
+ "digest": "40c5ee93396301911391cf6e70454b6fa8020fe5c85d3364136bcedb5d052cdb"
+ },
+ {
+ "name": "mask",
+ "unicode": "1F637",
+ "digest": "e0301cd27eb8c74c9772ff05b880215fc031ac1ae7f3177cd24ba0acb43b3834"
+ },
+ {
+ "name": "massage",
+ "unicode": "1F486",
+ "digest": "856d0fb1144ee91c58dfad74f9a2cababf6bae4b3ceba2a95c03ecd44ae3aa21"
+ },
+ {
+ "name": "massage_tone1",
+ "unicode": "1F486-1F3FB",
+ "digest": "fd53b06eb0967303c0914ebb79fd872900ec0f71b2852c7238517e192e5023e1"
+ },
+ {
+ "name": "massage_tone2",
+ "unicode": "1F486-1F3FC",
+ "digest": "7ef57359a339ae1ca4488f9a6195a352e74daf5b67d8e1ae1e91fe866921c40c"
+ },
+ {
+ "name": "massage_tone3",
+ "unicode": "1F486-1F3FD",
+ "digest": "e4fb643b6242bedb395e503ae337a88b2a255b5fda88b4aaa93396f948614a6e"
+ },
+ {
+ "name": "massage_tone4",
+ "unicode": "1F486-1F3FE",
+ "digest": "94f007c2daf9455fa8d2b10cc7ccff7db9bc9daf835ef5c3699be091938db833"
+ },
+ {
+ "name": "massage_tone5",
+ "unicode": "1F486-1F3FF",
+ "digest": "d18e800b728bf45b500f492062dc81312ca1ad7b1a0277a3d5bc150e4632ea1c"
+ },
+ {
+ "name": "meat_on_bone",
+ "unicode": "1F356",
+ "digest": "674a2a58e174b7681eef3b6c5b39c098ed9374cc610d037166c0092ee5269a97"
+ },
+ {
+ "name": "medal",
+ "unicode": "1F3C5",
+ "digest": "270d438b6e2155e944dc734ea3e4d02409e51f59db2db636398fbf96e5edb0e6"
+ },
+ {
+ "name": "mega",
+ "unicode": "1F4E3",
+ "digest": "540ab4fd5bab041a681749b85e6de598ebcbfc4fbf5c3cdbd9ca1e8256191733"
+ },
+ {
+ "name": "melon",
+ "unicode": "1F348",
+ "digest": "39dd0ecb23e2d3da6cbb7309333fed5d7e2cb38c0afc526ade78520eca11b5f4"
+ },
+ {
+ "name": "menorah",
+ "unicode": "1F54E",
+ "digest": "5f81bc2e5a34bf76481d2958fdb0b4e4540c599aa837a6453609a39023885d8c"
+ },
+ {
+ "name": "mens",
+ "unicode": "1F6B9",
+ "digest": "5ed56cff80e8ee7ed581f2a2e365915db5cb29df89e850e0add0b68db4b0c788"
+ },
+ {
+ "name": "metal",
+ "unicode": "1F918",
+ "digest": "45e5fac0b9b019cf217dcfd1380cafb0d03063454612178278dac1ca5f8476a6"
+ },
+ {
+ "name": "metal_tone1",
+ "unicode": "1F918-1F3FB",
+ "digest": "9b3596fe7c063df838f0a43fb680ce10fb88e2b73c5c3324abfa357a224c17aa"
+ },
+ {
+ "name": "metal_tone2",
+ "unicode": "1F918-1F3FC",
+ "digest": "e15a4898a0efca4354ac48d6b01ff0618ce8b110b1246a4f5d78e19b54658be6"
+ },
+ {
+ "name": "metal_tone3",
+ "unicode": "1F918-1F3FD",
+ "digest": "c159e8179cb1907c246b432d87c5253b914fd7cebb6ac05292c4e38eff4815b0"
+ },
+ {
+ "name": "metal_tone4",
+ "unicode": "1F918-1F3FE",
+ "digest": "a8a43a88028c97074321e3da56df1045db41ede58bf286c21d7ae90f222f2011"
+ },
+ {
+ "name": "metal_tone5",
+ "unicode": "1F918-1F3FF",
+ "digest": "e6611e826e867e2c73a8cadb138e4aa6365e3583dd229ff24b3e8f161904bf56"
+ },
+ {
+ "name": "metro",
+ "unicode": "1F687",
+ "digest": "532378cf385f9a7fafe2f5c8203e675be6d38798871f4c8e2c50498a1529f956"
+ },
+ {
+ "name": "microphone",
+ "unicode": "1F3A4",
+ "digest": "46da2b94e4dc233f640249103f09ec915aaa812cce90afe68fedb6774a27ad4b"
+ },
+ {
+ "name": "microphone2",
+ "unicode": "1F399",
+ "digest": "f9df32cd207808f67a895d3460a215d1ecc42e377907bcd64731c02b697d4f32"
+ },
+ {
+ "name": "microscope",
+ "unicode": "1F52C",
+ "digest": "79918f5fe0a39f31f270a481f4c6e00ea49fc09d64b1ae78770971293c2b1ed8"
+ },
+ {
+ "name": "middle_finger",
+ "unicode": "1F595",
+ "digest": "c6320b236a4a9593aeade511b52dd3114207e947458cb3b818c78737a505fdf6"
+ },
+ {
+ "name": "middle_finger_tone1",
+ "unicode": "1F595-1F3FB",
+ "digest": "93c7aa994856185519d576cb779bdcff3a33f7077eef98e70968125f92f02448"
+ },
+ {
+ "name": "middle_finger_tone2",
+ "unicode": "1F595-1F3FC",
+ "digest": "a0de802294717b80e08d9d30f5fd64eacb90b5b3b9d7a0c27d6226a22822597f"
+ },
+ {
+ "name": "middle_finger_tone3",
+ "unicode": "1F595-1F3FD",
+ "digest": "8bbbab07c838257416bbf8377904362c07019fca9d5abf9fd048ccf6370178da"
+ },
+ {
+ "name": "middle_finger_tone4",
+ "unicode": "1F595-1F3FE",
+ "digest": "d9eed8db540fdb669c6ae5ef168b77659660589f5ddd9b66062274d335a3ef04"
+ },
+ {
+ "name": "middle_finger_tone5",
+ "unicode": "1F595-1F3FF",
+ "digest": "0519c3298040e57db202294476df239edb9b23b44848bab296bc45eda7cf8664"
+ },
+ {
+ "name": "military_medal",
+ "unicode": "1F396",
+ "digest": "bd1da0004768f404c6bb4db85d4b748f766a77ab3edb74e709d0c0064509a043"
+ },
+ {
+ "name": "milky_way",
+ "unicode": "1F30C",
+ "digest": "598b4e641c1081bb03ce38a29f9711fc8616373216a833e4daa14fbe97a358f5"
+ },
+ {
+ "name": "minibus",
+ "unicode": "1F690",
+ "digest": "3d15791ca96349c3abb5bd5d1014b6b33b984db19609f56f5fd1e8d2fc551809"
+ },
+ {
+ "name": "minidisc",
+ "unicode": "1F4BD",
+ "digest": "83c4bfda4e0a80785fa1c3f2bbf3c15aca2bda8ea3727ce78bc4236e1e377a36"
+ },
+ {
+ "name": "mobile_phone_off",
+ "unicode": "1F4F4",
+ "digest": "cfe6dfd766b9e0b4768df25d6e943c9abc0e910ff5e5c7a8a0f425c786bbab8d"
+ },
+ {
+ "name": "money_mouth",
+ "unicode": "1F911",
+ "digest": "3ac2f9b5409e1426eef6966938ca04cf78aeffefd43f44b6c86af4af7836e22f"
+ },
+ {
+ "name": "money_with_wings",
+ "unicode": "1F4B8",
+ "digest": "f7f1fa502d2f6804169869aeb5ca7f0ea64bc2d6a0204f08875d65da4f8cb332"
+ },
+ {
+ "name": "moneybag",
+ "unicode": "1F4B0",
+ "digest": "442db49cda27360d2eb781489c9879730a6094c3267bb0a0a8687d84f8fed078"
+ },
+ {
+ "name": "monkey",
+ "unicode": "1F412",
+ "digest": "3141c971aacbadaba21f970a515e192740212be2a49fa1f5eb0fc4dc576e209f"
+ },
+ {
+ "name": "monkey_face",
+ "unicode": "1F435",
+ "digest": "e2397431d2befe44bf5298fa81d865d80722bf954113bceacc2aa98b84d856e2"
+ },
+ {
+ "name": "monorail",
+ "unicode": "1F69D",
+ "digest": "b546153200d6fbe8d65b1b34f62ff4a19b1b6a159eb1b536c5c2ecb56dab0ec9"
+ },
+ {
+ "name": "mood_bubble",
+ "unicode": "1F5F0",
+ "digest": "1df7061217e478d43ab9a87d4f351c4ca56705acd6b4e0b0bedfdece77635f1b"
+ },
+ {
+ "name": "mood_bubble_lightning",
+ "unicode": "1F5F1",
+ "digest": "4af3e4e53eaa328b0d20542ab31705a74bf9fd368cd0673b706838ce1681d3c9"
+ },
+ {
+ "name": "mood_lightning",
+ "unicode": "1F5F2",
+ "digest": "6784635e81ec722fd50a1c2a23b0f9679e4bf1b5ae2b5a01eeb995bc1f7a426f"
+ },
+ {
+ "name": "mortar_board",
+ "unicode": "1F393",
+ "digest": "cb59edb08f75c374088b65284e4d0f77b9bc9573de3e6a5127f865431011e54c"
+ },
+ {
+ "name": "mosque",
+ "unicode": "1F54C",
+ "digest": "a08ddb74342dea8f79063db6f98ba03eb08fe99481de8ce9123827ca7f17c7f3"
+ },
+ {
+ "name": "motorboat",
+ "unicode": "1F6E5",
+ "digest": "9dbea67bbe2e95dcc68c049a58f87390a44350b32308342615d75214af3d1cef"
+ },
+ {
+ "name": "motorcycle",
+ "unicode": "1F3CD",
+ "digest": "8429fb6dfeb873abdffcc179c32d4f23e91c9e6b27b06cd204fd2e83cc11189e"
+ },
+ {
+ "name": "motorway",
+ "unicode": "1F6E3",
+ "digest": "fc05a36c917637c135b0a60db8afcd58cee2b335070fe3888697f8026c9d11a5"
+ },
+ {
+ "name": "mount_fuji",
+ "unicode": "1F5FB",
+ "digest": "22bfffef033637b3c9b2fe7e539c74a659d2a49e594d2b33be894da00654d059"
+ },
+ {
+ "name": "mountain",
+ "unicode": "26F0",
+ "digest": "486cf4e9d5f3913d138fdb7878fe869b39caa3fca53876365957a89dc8f7edb8"
+ },
+ {
+ "name": "mountain_bicyclist",
+ "unicode": "1F6B5",
+ "digest": "b547b96951b6837df8ae3be1e846f15e7e2ac06d976e1fe7f1442dcc5d3a0942"
+ },
+ {
+ "name": "mountain_bicyclist_tone1",
+ "unicode": "1F6B5-1F3FB",
+ "digest": "68ce0d55163c7b89ee1d87b752ece127bb25ca9deb3421b31df549a00ac5f69d"
+ },
+ {
+ "name": "mountain_bicyclist_tone2",
+ "unicode": "1F6B5-1F3FC",
+ "digest": "5bfa82180bfb8bc4444cf301688aff02884895574a7ba66b398aaf20bde0f101"
+ },
+ {
+ "name": "mountain_bicyclist_tone3",
+ "unicode": "1F6B5-1F3FD",
+ "digest": "33cb64a792123b81a05080465a0ea1035a2cdfdab01c71f5f725a5f92251c3e8"
+ },
+ {
+ "name": "mountain_bicyclist_tone4",
+ "unicode": "1F6B5-1F3FE",
+ "digest": "9c3fa4e65dcb0ad69b963292e77c7a75853ae3c1d18a90670f81ffb65b5d020c"
+ },
+ {
+ "name": "mountain_bicyclist_tone5",
+ "unicode": "1F6B5-1F3FF",
+ "digest": "871de9e3fddb49b305e5f91000143878b0288c107a125c4e60acf2b6cf8b7f3f"
+ },
+ {
+ "name": "mountain_cableway",
+ "unicode": "1F6A0",
+ "digest": "f248ed5bf864f4a81e365b30d2825d2e6fc15a200c4ccf69e9f797341529f955"
+ },
+ {
+ "name": "mountain_railway",
+ "unicode": "1F69E",
+ "digest": "7dd08745ab56c95c3dfcebcca517ff231cef61b670cedf9d7c53f3244c34e30b"
+ },
+ {
+ "name": "mountain_snow",
+ "unicode": "1F3D4",
+ "digest": "9939aade3d4d972ba3af16fcc6cc2454978f5426e4c92838734a44db065ce0ff"
+ },
+ {
+ "name": "mouse",
+ "unicode": "1F42D",
+ "digest": "fb20b3a82f407a6316bbbac68d58018c3d5b93a9a6ae968f44ace18d1c5698d9"
+ },
+ {
+ "name": "mouse2",
+ "unicode": "1F401",
+ "digest": "87be4099523ec32440e6d091f1193a8ed90730b9fbecaafed4912585bfe7818c"
+ },
+ {
+ "name": "mouse_one",
+ "unicode": "1F5AF",
+ "digest": "e0d2055ccba489d24e0c0b6d2f22793efe48a734b0fd50f5af88f721b40665c0"
+ },
+ {
+ "name": "mouse_three_button",
+ "unicode": "1F5B1",
+ "digest": "6a5629fee01145211cc8f4e8f59c5f1e61affed38c650502213d76c7d8861b01"
+ },
+ {
+ "name": "movie_camera",
+ "unicode": "1F3A5",
+ "digest": "d6633b89a637b64d617c3032eed74bb82d3fa732dd9975486b2b5841b473808a"
+ },
+ {
+ "name": "moyai",
+ "unicode": "1F5FF",
+ "digest": "bf948c26cd98e2f5e48da363f2924a9d7c217232115a00cec372d0d5293402a8"
+ },
+ {
+ "name": "muscle",
+ "unicode": "1F4AA",
+ "digest": "c85147efb786bdea3e7d53e2edf6b827280cd9fa881661a6102a614bf5b3579f"
+ },
+ {
+ "name": "muscle_tone1",
+ "unicode": "1F4AA-1F3FB",
+ "digest": "38d071df2b25031b61f3605b03c34d2e5d3e35d29f3c4aada14be37e19750eb8"
+ },
+ {
+ "name": "muscle_tone2",
+ "unicode": "1F4AA-1F3FC",
+ "digest": "dcf11b76c8ffb58dc7e4f9ecd32a4c291d9772d51df2853d41081e041e7e0876"
+ },
+ {
+ "name": "muscle_tone3",
+ "unicode": "1F4AA-1F3FD",
+ "digest": "a3d5f8f2dbfc28f9713ee657428ea3292c47d0b22f11a51c13594be22b0f5204"
+ },
+ {
+ "name": "muscle_tone4",
+ "unicode": "1F4AA-1F3FE",
+ "digest": "eb220fc19be58d16cacc6b721e1011078b03256c0245756f251a4c2bcf50586c"
+ },
+ {
+ "name": "muscle_tone5",
+ "unicode": "1F4AA-1F3FF",
+ "digest": "4e18708cbd61eaad288f913c86ad2d45108dd4484bc35879c5dcdd075eeb09fd"
+ },
+ {
+ "name": "mushroom",
+ "unicode": "1F344",
+ "digest": "a2b252cd759244409d9a8066470059948e2c50b8cc86b59821c1c86b5190f640"
+ },
+ {
+ "name": "musical_keyboard",
+ "unicode": "1F3B9",
+ "digest": "dcb3e84d27bfe373e5ea7ede457908de52002f0fd6105e9f3f5525c54d2a43dd"
+ },
+ {
+ "name": "musical_note",
+ "unicode": "1F3B5",
+ "digest": "76a0f598f8e251a9dab44f2e14f2b7a6fb0c0c351e0f37862c8c99d380f1c261"
+ },
+ {
+ "name": "musical_score",
+ "unicode": "1F3BC",
+ "digest": "a132c6b35236005b45c830a42fa97b454d3061c14991c6320f34807f10ba6a4a"
+ },
+ {
+ "name": "mute",
+ "unicode": "1F507",
+ "digest": "73a99b7f9e00f92cab78cd304dee4e893a112c3a6f2285c13d44916ea547458e"
+ },
+ {
+ "name": "nail_care",
+ "unicode": "1F485",
+ "digest": "62f721d3610d1647dba4b3f53cd4f2bc4180dae298314c2cca2a6a8ab1664525"
+ },
+ {
+ "name": "nail_care_tone1",
+ "unicode": "1F485-1F3FB",
+ "digest": "11b82ed2e6b6619c9b74702fdacfb0ddc91310191c8b89f355c7c69a72673f8f"
+ },
+ {
+ "name": "nail_care_tone2",
+ "unicode": "1F485-1F3FC",
+ "digest": "5195c76bccb9149d9080347d785dae2cce947bada5b198fae8c23e42f5553154"
+ },
+ {
+ "name": "nail_care_tone3",
+ "unicode": "1F485-1F3FD",
+ "digest": "50eab0bf825c5e00db07a3f5ad26b1bb221f54efb5c55549f392b2f5aec09e5a"
+ },
+ {
+ "name": "nail_care_tone4",
+ "unicode": "1F485-1F3FE",
+ "digest": "d05a9ccfad02191c89e4cbd00aa48fdaf908c0de6681f4a587d500be448e528f"
+ },
+ {
+ "name": "nail_care_tone5",
+ "unicode": "1F485-1F3FF",
+ "digest": "62466354dcf6717a8b9e942ca2c5ad15a26aa815c213e3b01faba9a2e302ecdd"
+ },
+ {
+ "name": "name_badge",
+ "unicode": "1F4DB",
+ "digest": "0a1cb0f7d489d3356a4d3e01f9faf78449d82d8ec4595c8639a55c3606c97c40"
+ },
+ {
+ "name": "necktie",
+ "unicode": "1F454",
+ "digest": "029e1140391ef559a9316021c2db94f05653751fdf9d8f366446467a70fee6df"
+ },
+ {
+ "name": "negative_squared_cross_mark",
+ "unicode": "274E",
+ "digest": "0ba0e705fdeac99edd712db31a8846320b9d2cf53c9cb4d4bcfd22ba4e1488ea"
+ },
+ {
+ "name": "nerd",
+ "unicode": "1F913",
+ "digest": "94efd551700aae8909b8dd7a78a54a33e070d24b2e0a10534353645084614e98"
+ },
+ {
+ "name": "network",
+ "unicode": "1F5A7",
+ "digest": "1dbaa54deeb2328fd8a3f044e450c97ac3ff39627c598bb2f4312d677482ee06"
+ },
+ {
+ "name": "neutral_face",
+ "unicode": "1F610",
+ "digest": "df01da8501e1f588049c8ed66e504e9abcce83f74ce5790f4d3dc547408f77ee"
+ },
+ {
+ "name": "new",
+ "unicode": "1F195",
+ "digest": "24e80abd29750d8b297335cdd4751b6250bb820560cf0392a6cc8783d34db63a"
+ },
+ {
+ "name": "new_moon",
+ "unicode": "1F311",
+ "digest": "2d697e431eac53d6e1ea367b5da03c15fc535cd7e8c214f801fe595b768a8e11"
+ },
+ {
+ "name": "new_moon_with_face",
+ "unicode": "1F31A",
+ "digest": "ea469a4668ded071f35e5898ae229fdb5d02b0730ce233169b83e22f81292baa"
+ },
+ {
+ "name": "newspaper",
+ "unicode": "1F4F0",
+ "digest": "0aaf6747a43fb60cd15e6e64ca0eccaade331b376c6fe6712fd5e8294e9868cc"
+ },
+ {
+ "name": "newspaper2",
+ "unicode": "1F5DE",
+ "digest": "0ca6b5850091f23295c970815a8e64a52e3c3dae492029ecb1e0726c2693f9bf"
+ },
+ {
+ "name": "ng",
+ "unicode": "1F196",
+ "digest": "4994c9b795033ed788e98c4af571a1dffe28c0a1479e3b42dcae21bb08381b5f"
+ },
+ {
+ "name": "night_with_stars",
+ "unicode": "1F303",
+ "digest": "56bb4a59a897c1836ee1a49cc99f468891b790b0f8bce203c201c13bb7b8ae9a"
+ },
+ {
+ "name": "nine",
+ "unicode": "0039-20E3",
+ "digest": "7e3644a98cb6417a351530c9ce6b368e637a22c847a8c04133897dc1c5d7419f"
+ },
+ {
+ "name": "no_bell",
+ "unicode": "1F515",
+ "digest": "f4fb42836132000101624fecef8b9358736a0fc76beae460e6986aaa479204fd"
+ },
+ {
+ "name": "no_bicycles",
+ "unicode": "1F6B3",
+ "digest": "b3c258bea7d6988640e3348598c03c97632ca00a11cbf0352995b801ff4a296b"
+ },
+ {
+ "name": "no_entry",
+ "unicode": "26D4",
+ "digest": "ac807d54092efdc3aea417790a7d0c50b59800c9ea49b37f1aec6d2e453c5f6d"
+ },
+ {
+ "name": "no_entry_sign",
+ "unicode": "1F6AB",
+ "digest": "5a17d677ec1c7595a7970a1cbe0d20909341b30d3ab31471ced590f51fff1ff7"
+ },
+ {
+ "name": "no_good",
+ "unicode": "1F645",
+ "digest": "8ce921e5e13e1203cf43fdc3e7c5ec1fb2a1f9ff79f21539cff542c80af2e5fe"
+ },
+ {
+ "name": "no_good_tone1",
+ "unicode": "1F645-1F3FB",
+ "digest": "aab4d354aaac06e8348eb354487c6381e475b44651cb2716660904a36c47a1b6"
+ },
+ {
+ "name": "no_good_tone2",
+ "unicode": "1F645-1F3FC",
+ "digest": "8fb66b1a7b8f72062794281294515d47471a8c59de300b99d656c3412ca19d64"
+ },
+ {
+ "name": "no_good_tone3",
+ "unicode": "1F645-1F3FD",
+ "digest": "aeecf73fb9dca24b4002db2802fc9b5a483644c49f834c19f143d4e56ec46c1a"
+ },
+ {
+ "name": "no_good_tone4",
+ "unicode": "1F645-1F3FE",
+ "digest": "fadeb23307d5ccabbf08c848cf81c66c05b152aa32b85f86061caf14760f8eb9"
+ },
+ {
+ "name": "no_good_tone5",
+ "unicode": "1F645-1F3FF",
+ "digest": "cf26d5d6463d0febf4e1f08e343308742ffe0811cfc30c459b87d4cc812f5d04"
+ },
+ {
+ "name": "no_mobile_phones",
+ "unicode": "1F4F5",
+ "digest": "3b4ead88beca33f1e303d0a45268849be7aaaff7830b761732c7a5afc5a2de3a"
+ },
+ {
+ "name": "no_mouth",
+ "unicode": "1F636",
+ "digest": "2af81a3e07a8b7827a1e58f6f5036ccff2f6e7b0027a4f934c9fa34c6a780963"
+ },
+ {
+ "name": "no_pedestrians",
+ "unicode": "1F6B7",
+ "digest": "9f9ed90bb8f9964fa8cb0048fc092ecc0612a1994c98d19ef0b5a58607888476"
+ },
+ {
+ "name": "no_smoking",
+ "unicode": "1F6AD",
+ "digest": "fb90290ff5c917b7307a97c8ba793d20c61042525cf2f7bfd4cd2a7878aeefa5"
+ },
+ {
+ "name": "non-potable_water",
+ "unicode": "1F6B1",
+ "digest": "c4ddca2ab1a97260e9b2c2aa33fb03455c0e8174541c3a9416fc44143a3ee567"
+ },
+ {
+ "name": "nose",
+ "unicode": "1F443",
+ "digest": "308e28b15b7f734f6f184ae367789d7cf258656b24861cf8d5935127d810aa3f"
+ },
+ {
+ "name": "nose_tone1",
+ "unicode": "1F443-1F3FB",
+ "digest": "392d24b38ac3edc2d7b83945a60bbe9115a6a97658d8af35281a7cbef79449e8"
+ },
+ {
+ "name": "nose_tone2",
+ "unicode": "1F443-1F3FC",
+ "digest": "409a790339c405770492e49fdb0b5ba34087c27e2f9018ecd845ab078e61476a"
+ },
+ {
+ "name": "nose_tone3",
+ "unicode": "1F443-1F3FD",
+ "digest": "92b52b479a935f31e460257d809c531edad1a6bb4583ad18233b12c4e45202fe"
+ },
+ {
+ "name": "nose_tone4",
+ "unicode": "1F443-1F3FE",
+ "digest": "78ad2e857792e86cded6ba5620f634f7d1f79a92c82c266e48fab9bd73df3688"
+ },
+ {
+ "name": "nose_tone5",
+ "unicode": "1F443-1F3FF",
+ "digest": "dbef6813c1965d3e93f70f33f118f9950130af21c622cea97ea215a36b4fa73f"
+ },
+ {
+ "name": "note",
+ "unicode": "1F5C9",
+ "digest": "073660fdaa02ecf98d04f61f8d65d6cc447ccae3825fccaff19a2c99ebba52af"
+ },
+ {
+ "name": "note_empty",
+ "unicode": "1F5C6",
+ "digest": "06b56eeaca6349bbcf1020bea98f937450a7e086db65cd5d7497748e0fb607be"
+ },
+ {
+ "name": "notebook",
+ "unicode": "1F4D3",
+ "digest": "64bd4a3e7ca7b22fc704c7b7bd4d13540c16bc69b9d8dd76e69e6ad573ab3823"
+ },
+ {
+ "name": "notebook_with_decorative_cover",
+ "unicode": "1F4D4",
+ "digest": "4b45f28fbde1be5c214a6bc2413abc91db02bccd86f74c21b7f4a4da8b75a46f"
+ },
+ {
+ "name": "notepad",
+ "unicode": "1F5CA",
+ "digest": "85069e2d13540886457368a57295072aec44c7137d9223bfcf908ce1f0e5124e"
+ },
+ {
+ "name": "notepad_empty",
+ "unicode": "1F5C7",
+ "digest": "8be5053e74c13d8220917c5aee1f4afdecb001612886438f283b0c2a0fecf6af"
+ },
+ {
+ "name": "notepad_spiral",
+ "unicode": "1F5D2",
+ "digest": "c181b6c1cc6063ec1848e46cbbf1d8b890c53b59cdc5218311ce06889570e727"
+ },
+ {
+ "name": "notes",
+ "unicode": "1F3B6",
+ "digest": "bf3868386e17eac40ac7fbabea027042027ff061daafe406c869cdd8ce94641d"
+ },
+ {
+ "name": "nut_and_bolt",
+ "unicode": "1F529",
+ "digest": "fdb9d7408202fad7a52ff21608042c08c3b0beb195999fff233df36a29dc9e96"
+ },
+ {
+ "name": "o",
+ "unicode": "2B55",
+ "digest": "8e119dba4130bd33b3ee5c862fb4fa5a691173911ffee51cb9359fee3398e330"
+ },
+ {
+ "name": "o2",
+ "unicode": "1F17E",
+ "digest": "00d751124c25633611055bd61e74fc3f3d1779f0d09e1e707837686f613367b4"
+ },
+ {
+ "name": "ocean",
+ "unicode": "1F30A",
+ "digest": "9b1fbfd2a64f417d0c2cb91085b29a12d14e15844bc21798bdee938bb7bf6222"
+ },
+ {
+ "name": "octopus",
+ "unicode": "1F419",
+ "digest": "3fdfbc02f47ad434bdeb7f3a15cd4e8f8118ee1cd754627e358f1c2f4616f5e3"
+ },
+ {
+ "name": "oden",
+ "unicode": "1F362",
+ "digest": "afed1c5166943e5803602ffacc67652e3b29ee4222a6c36aba2daf88bd21ad3c"
+ },
+ {
+ "name": "office",
+ "unicode": "1F3E2",
+ "digest": "dc1836ef152d88fd628df18db770594f5dbc8d7f20d6ce982588b25b78b19c92"
+ },
+ {
+ "name": "oil",
+ "unicode": "1F6E2",
+ "digest": "f8b7626cb09e229203105b9c8c7f3fbb38c0650021092fc50115ad517248644a"
+ },
+ {
+ "name": "ok",
+ "unicode": "1F197",
+ "digest": "6b05bbab4a7104541c2f4bce553884d17ae0ad07589b19d6b53b6949c14f2269"
+ },
+ {
+ "name": "ok_hand",
+ "unicode": "1F44C",
+ "digest": "9981f32ef200b011a10f6bfa2066c41b6b5e7bcd6c3c21647980b640bc1fa93b"
+ },
+ {
+ "name": "ok_hand_tone1",
+ "unicode": "1F44C-1F3FB",
+ "digest": "e5933a9b64b03ce0634f15f02ff7b6424530dbdc0e283461e0c9992d0c2ca2ad"
+ },
+ {
+ "name": "ok_hand_tone2",
+ "unicode": "1F44C-1F3FC",
+ "digest": "4c04741c9f2c8731da8df3015e9aae00061a01848c2d22aab1e9853c271deed3"
+ },
+ {
+ "name": "ok_hand_tone3",
+ "unicode": "1F44C-1F3FD",
+ "digest": "216dc5a72f9e34bbb7b39f680c388bd5b52abf9b41b843342e53e285b7933076"
+ },
+ {
+ "name": "ok_hand_tone4",
+ "unicode": "1F44C-1F3FE",
+ "digest": "7139de7ec9d5a962cf87b9fbbeef3a53aa482bb840ab3b64d8d0da81bdc19886"
+ },
+ {
+ "name": "ok_hand_tone5",
+ "unicode": "1F44C-1F3FF",
+ "digest": "e18b0a1bc5d970cc63466bd6da6e9f855db37d1eada3230d19f600c1f5a402a3"
+ },
+ {
+ "name": "ok_woman",
+ "unicode": "1F646",
+ "digest": "3b2fa732d9c9addb056f136192428e99d805d4cb1c7dab724fd552c7e93197e4"
+ },
+ {
+ "name": "ok_woman_tone1",
+ "unicode": "1F646-1F3FB",
+ "digest": "017aca3797701b043a44f22e67dcad8b531a3ca14e629ae0d2fbc601ed3e49cb"
+ },
+ {
+ "name": "ok_woman_tone2",
+ "unicode": "1F646-1F3FC",
+ "digest": "036bed032bc5a616668775cda0d5640c810e2836aa28009c8e8bf2b487259c59"
+ },
+ {
+ "name": "ok_woman_tone3",
+ "unicode": "1F646-1F3FD",
+ "digest": "d9a4414caddda43d1a36828cfbecce5f2b7e5c1b67b4a47991b2ae0a34cf7ab7"
+ },
+ {
+ "name": "ok_woman_tone4",
+ "unicode": "1F646-1F3FE",
+ "digest": "942e1b9aa495c4c4de0804e4d4348422201299d649e5d65829ba4a308880df1c"
+ },
+ {
+ "name": "ok_woman_tone5",
+ "unicode": "1F646-1F3FF",
+ "digest": "e8d0fb5b999d5d63404493aa505b5af2260c76001023431d5e788773d0a9e2de"
+ },
+ {
+ "name": "older_man",
+ "unicode": "1F474",
+ "digest": "620f763325827acbeb9d57798ef55d87827d0dfc77b84d942e25bc5057f2cbfe"
+ },
+ {
+ "name": "older_man_tone1",
+ "unicode": "1F474-1F3FB",
+ "digest": "e0f35c12362eae503d1c30a345c3a4978196d351d8a1eb9d5f107c60ea4bbf52"
+ },
+ {
+ "name": "older_man_tone2",
+ "unicode": "1F474-1F3FC",
+ "digest": "671766ce9fa47c3fa009d4f138344c87d73032a1c38e48614c663f8ea5d0f673"
+ },
+ {
+ "name": "older_man_tone3",
+ "unicode": "1F474-1F3FD",
+ "digest": "6ff4885ef8c416b8970780a691fef74c8d89421ab11e0aa8c522c33e1c67fbe8"
+ },
+ {
+ "name": "older_man_tone4",
+ "unicode": "1F474-1F3FE",
+ "digest": "0ae7d4e316dcd4d27a5a6cdaabab88a4f992bd1b75f6ceaeb5b906ed1eb5269c"
+ },
+ {
+ "name": "older_man_tone5",
+ "unicode": "1F474-1F3FF",
+ "digest": "abe2757bd5e35f30d2a6daec09637ea5382a46d14d239b77282e9bf874229b57"
+ },
+ {
+ "name": "older_woman",
+ "unicode": "1F475",
+ "digest": "3ed599443eed25399aac999fc234c9e97f8fb6ec567e37a553c26e01021b097c"
+ },
+ {
+ "name": "older_woman_tone1",
+ "unicode": "1F475-1F3FB",
+ "digest": "7421c5dba67cfd1eeabb2fa8faf4aa0d615d23f191cf7d7c0ad9c1fa884edfda"
+ },
+ {
+ "name": "older_woman_tone2",
+ "unicode": "1F475-1F3FC",
+ "digest": "65edeef25648ac7f8be535df06af1286441691fa15176e99a6e83fc779aa2cde"
+ },
+ {
+ "name": "older_woman_tone3",
+ "unicode": "1F475-1F3FD",
+ "digest": "5d27bbcc5796227a9caec1c7612d3f691055655b96f7303e420839463d76c269"
+ },
+ {
+ "name": "older_woman_tone4",
+ "unicode": "1F475-1F3FE",
+ "digest": "75b858e910175fc0233503d672120fd43ac035ba3fd2052fbb44df39f6e3695c"
+ },
+ {
+ "name": "older_woman_tone5",
+ "unicode": "1F475-1F3FF",
+ "digest": "9da1cf10a605c470877d7f4a840f99344b1ec2e7b1ec7db61e930cde77025e3b"
+ },
+ {
+ "name": "om_symbol",
+ "unicode": "1F549",
+ "digest": "c8c1c9d445b1fc50a627b71bee21fba978e04532e4685ec032a0174f51fc12bb"
+ },
+ {
+ "name": "on",
+ "unicode": "1F51B",
+ "digest": "08e1159a68d3334a87ffa75b9e70826cb557d0f73a2c1d08f4c3d60476ecacc8"
+ },
+ {
+ "name": "oncoming_automobile",
+ "unicode": "1F698",
+ "digest": "6bff7f40fe223df6d16c7512532b8aa6f83e8c13e1007b63eb9aabf774c1a322"
+ },
+ {
+ "name": "oncoming_bus",
+ "unicode": "1F68D",
+ "digest": "127a357fcd96ce4b9ab11c3dba95d8ff811bab193dd8ba38efb7067a44752ce8"
+ },
+ {
+ "name": "oncoming_police_car",
+ "unicode": "1F694",
+ "digest": "57cb70e05e70c1f68ab42259f307ed9782c2b9d6e35d2dff2895aa23d7eb6b04"
+ },
+ {
+ "name": "oncoming_taxi",
+ "unicode": "1F696",
+ "digest": "174967ae4c3d5881d2408c71c020f704e933190af4caef5d2908e9ac382f35ea"
+ },
+ {
+ "name": "one",
+ "unicode": "0031-20E3",
+ "digest": "113b9d87c3e37c9c54e49cecccbfc40c15fb97fd03a51505df85e48b78702b2b"
+ },
+ {
+ "name": "open_file_folder",
+ "unicode": "1F4C2",
+ "digest": "def93715203aed464211798d773732895a19389a94a2e7ed43e7f229b2aab7da"
+ },
+ {
+ "name": "open_hands",
+ "unicode": "1F450",
+ "digest": "7c60a37ae11727c998908199b8709e52593b931843aef942f37b306b1edca12a"
+ },
+ {
+ "name": "open_hands_tone1",
+ "unicode": "1F450-1F3FB",
+ "digest": "09ffa9b3f28fc56a71e4e711bdfc87ce1a56721229377e71f1c00224523f8b9b"
+ },
+ {
+ "name": "open_hands_tone2",
+ "unicode": "1F450-1F3FC",
+ "digest": "21ecaba9f086bcb7eb07c17c2b2621bcd1ca28c57f79032d5e0eba356494cc85"
+ },
+ {
+ "name": "open_hands_tone3",
+ "unicode": "1F450-1F3FD",
+ "digest": "c7dbb8c44f78f7793b202ec215fee42b7e1e555d659fbf402383500217b89656"
+ },
+ {
+ "name": "open_hands_tone4",
+ "unicode": "1F450-1F3FE",
+ "digest": "867451d42492ab2277687447f421f744530b9ea057312326353fec39c94b18fd"
+ },
+ {
+ "name": "open_hands_tone5",
+ "unicode": "1F450-1F3FF",
+ "digest": "56335506cf68e29150cb68d7ebbb4a92aed390018966669a8144d20ae0d6cfe3"
+ },
+ {
+ "name": "open_mouth",
+ "unicode": "1F62E",
+ "digest": "f05fdf998e8b5c0b00ebd8b5ab17a67f5c0a45275f31a201af74e8ab0c2f7ba9"
+ },
+ {
+ "name": "ophiuchus",
+ "unicode": "26CE",
+ "digest": "98c61bb0c36d60c476d42d5e074297662e8d141dcab7004a5bd63c359eed3b84"
+ },
+ {
+ "name": "optical_disk",
+ "unicode": "1F5B8",
+ "digest": "df8c10028d29d65f144a6b789d1c3294e7b3293554c4c30d28d72dc7ba8d9a5d"
+ },
+ {
+ "name": "orange_book",
+ "unicode": "1F4D9",
+ "digest": "86d150ea3d62183ab7dfe2851cf7f4d1ae769b7ecbb1987b0f463e639e429598"
+ },
+ {
+ "name": "orthodox_cross",
+ "unicode": "2626",
+ "digest": "9c861285ca6d699cd2c72b6df44ec2b1e64138152f19c66e32df1ce770ff2e83"
+ },
+ {
+ "name": "outbox_tray",
+ "unicode": "1F4E4",
+ "digest": "b6a6015d5d7d528af485de23ff4518dc35408def1cc49bc6c9b01d880d613985"
+ },
+ {
+ "name": "ox",
+ "unicode": "1F402",
+ "digest": "cbcfe5c8c4d6b939e24e18e610785f171bb9410441e02c2eeb1bceb0a6246daf"
+ },
+ {
+ "name": "package",
+ "unicode": "1F4E6",
+ "digest": "4023cffce85384217a73609f457aec013876e689c44bcfff0bcc35f3e4e1ab00"
+ },
+ {
+ "name": "page",
+ "unicode": "1F5CF",
+ "digest": "cc745056525f59d9128d1d03b14770376bb09ab64b8ef4ac994ab7f38efd4783"
+ },
+ {
+ "name": "page_facing_up",
+ "unicode": "1F4C4",
+ "digest": "71a0872bf1b13c58746f9b41655227c75be107ab6083c0dce13cb16444af22e7"
+ },
+ {
+ "name": "page_with_curl",
+ "unicode": "1F4C3",
+ "digest": "cb4210464faea946c7b07db7067c7fc98920f778cf57721388f5362942ba3029"
+ },
+ {
+ "name": "pager",
+ "unicode": "1F4DF",
+ "digest": "209dbdc19aa650ecacc0569e17a9123c9a1e39df59c9b4120f3b0888b63cd6f1"
+ },
+ {
+ "name": "pages",
+ "unicode": "1F5D0",
+ "digest": "05bd47b78f089389356d9d839c736843f56b959ab4277056606ffcbb013390bc"
+ },
+ {
+ "name": "paintbrush",
+ "unicode": "1F58C",
+ "digest": "73eb33184f5f495d6c2699fafc1a8680069f82a70fbe519290c3a2ce30d1aee9"
+ },
+ {
+ "name": "palm_tree",
+ "unicode": "1F334",
+ "digest": "1589ff4b1b87296edc0118e4aa67b3b504ed85a5b8d47e7d0c3e309d0bbf8cd6"
+ },
+ {
+ "name": "panda_face",
+ "unicode": "1F43C",
+ "digest": "050ee87892f56ff485f460bc6c3846d98a0ca7083d2cf0b8ab24772b672273f2"
+ },
+ {
+ "name": "paperclip",
+ "unicode": "1F4CE",
+ "digest": "1463607a59345973f009fa53a719e2264b95743560adb99737bef29b1d133a95"
+ },
+ {
+ "name": "paperclips",
+ "unicode": "1F587",
+ "digest": "7071e031f4a100c3cb3573fbfa375360043f0276289a0818f2ffaf71b3580040"
+ },
+ {
+ "name": "park",
+ "unicode": "1F3DE",
+ "digest": "d257f0f1b1a0134573f80ba1a5f522a91c320ee7f93a1cb64877c077e7e19b50"
+ },
+ {
+ "name": "parking",
+ "unicode": "1F17F",
+ "digest": "e1d2cfd1c57ea85003ca4df066cbba4e506bf6c4d6c790e27b2f78ad8443fabf"
+ },
+ {
+ "name": "part_alternation_mark",
+ "unicode": "303D",
+ "digest": "b3cc2e803b255e858417345ba6ba52a1c22f511b483fec11b5d68c4432f759b6"
+ },
+ {
+ "name": "partly_sunny",
+ "unicode": "26C5",
+ "digest": "484990f5e1a3b14c731e7bd4b0b4a1c10cd5fb54ac7cf2751f40c8bf59d7e2b4"
+ },
+ {
+ "name": "passport_control",
+ "unicode": "1F6C2",
+ "digest": "224e8ef60d4d6587721727555de324948fb5b6c1cb5cc4b546960983d1ec85c4"
+ },
+ {
+ "name": "pause_button",
+ "unicode": "23F8",
+ "digest": "edd605ffaa39a7905ed0958b7cc69f00f5b271e579198d2df1746ad1b3648272"
+ },
+ {
+ "name": "peace",
+ "unicode": "262E",
+ "digest": "e0ee8a5c9fb18d5db6841b21527ed8fd955abdff9ffdb7b2684dca22107015fc"
+ },
+ {
+ "name": "peach",
+ "unicode": "1F351",
+ "digest": "a3f4fd5ff02e0a03104ab54456ee1a7521858ee68443856ee10e0972e5b6aaa5"
+ },
+ {
+ "name": "pear",
+ "unicode": "1F350",
+ "digest": "7a7a72568d53677cd1fff4d9e58e63327a742fa16d22a2bef03b4a6fa378d3b3"
+ },
+ {
+ "name": "pen_ballpoint",
+ "unicode": "1F58A",
+ "digest": "6becdc6f622c774bb09b7e7592bba2123ecccc9de32a35f0b18b50d7d54109cb"
+ },
+ {
+ "name": "pen_fountain",
+ "unicode": "1F58B",
+ "digest": "8c78cf0c2bd1d5e309d2d3356ff207e3fc76ca18dd6b90762cb62f6afbc95c6a"
+ },
+ {
+ "name": "pencil",
+ "unicode": "1F4DD",
+ "digest": "62b7ee5d9352114d09ee6f2c9a4c5e8b79f775a6c509e82ddfcdd61e13716249"
+ },
+ {
+ "name": "pencil2",
+ "unicode": "270F",
+ "digest": "aa2c572772187fee1f9125bb0950f5ce8a61f7dd2647258c40b4077ee5feb498"
+ },
+ {
+ "name": "pencil3",
+ "unicode": "1F589",
+ "digest": "52c1ba1228917eb491ac1745a495e0fdafba6b985a81caba250f71d1f94c725c"
+ },
+ {
+ "name": "penguin",
+ "unicode": "1F427",
+ "digest": "095de34b3f6a2521a342c21f5f2551a0092bf47429801c15b7bbf0913924f412"
+ },
+ {
+ "name": "pennant_black",
+ "unicode": "1F3F2",
+ "digest": "cd3c33bfc3c7fbe84b98d2d481d56a7bf5488ff94afadd8b5a0e454768b80269"
+ },
+ {
+ "name": "pennant_white",
+ "unicode": "1F3F1",
+ "digest": "818b1be73540f2cfeb1c514e1ee75d18715af317f0db817d9ae081b9ea33d4b0"
+ },
+ {
+ "name": "pensive",
+ "unicode": "1F614",
+ "digest": "2d9e7f1eed14dcc86674cec78e992567a40d0f223fc67d722b91eebcd1251269"
+ },
+ {
+ "name": "performing_arts",
+ "unicode": "1F3AD",
+ "digest": "a202755bab6427433975589bb8b63e61e5d7f55c6242676d8000e91eedabc55e"
+ },
+ {
+ "name": "persevere",
+ "unicode": "1F623",
+ "digest": "686ef3fc70ce8294d02a764ebd75b69f25cca6bff6b92e7905130366d22f6d8a"
+ },
+ {
+ "name": "person_frowning",
+ "unicode": "1F64D",
+ "digest": "16e8fbf22c0b4c237d0d45202fa32d1ebd04760a5b6975c9c9b477321ccb0e12"
+ },
+ {
+ "name": "person_frowning_tone1",
+ "unicode": "1F64D-1F3FB",
+ "digest": "a143b865976ce3cf307db854cfd1ca58c3832df0eee5e9b0ab307cf4f24ba3db"
+ },
+ {
+ "name": "person_frowning_tone2",
+ "unicode": "1F64D-1F3FC",
+ "digest": "4e7050d8a38019ba2293f66b9930e6a7e35dacf3b3bc9431edb586a0d9ea8054"
+ },
+ {
+ "name": "person_frowning_tone3",
+ "unicode": "1F64D-1F3FD",
+ "digest": "0750015d3ac1b5954d31e36cd59c70b6ed9f4df698082484b7ac59eb0b9964b0"
+ },
+ {
+ "name": "person_frowning_tone4",
+ "unicode": "1F64D-1F3FE",
+ "digest": "18d6cc92d0990624218d38d6eeed60bccb371d0fc9f1c889e9476b3b0c44b5e8"
+ },
+ {
+ "name": "person_frowning_tone5",
+ "unicode": "1F64D-1F3FF",
+ "digest": "4a898199cbaf083d37511f51d8a1d2560b7a20c62a1b09087831da7010fbd093"
+ },
+ {
+ "name": "person_with_blond_hair",
+ "unicode": "1F471",
+ "digest": "67d95a0801c65f62db55fa80ab35dec65c239601a44bf5f5902e4645f126770e"
+ },
+ {
+ "name": "person_with_blond_hair_tone1",
+ "unicode": "1F471-1F3FB",
+ "digest": "e79717bfe30a26eafc082a75fa7547d8f2ad3c123fb2d75a95e75f0ce7ecbd0c"
+ },
+ {
+ "name": "person_with_blond_hair_tone2",
+ "unicode": "1F471-1F3FC",
+ "digest": "c4a1961c292149ab6e1fd54a7894398599bf855de97a05ee4e836a86a400deb3"
+ },
+ {
+ "name": "person_with_blond_hair_tone3",
+ "unicode": "1F471-1F3FD",
+ "digest": "e2707d0cf778bee5b72d861ec76430eb1cf9f9820f066ee6327574d5697f445e"
+ },
+ {
+ "name": "person_with_blond_hair_tone4",
+ "unicode": "1F471-1F3FE",
+ "digest": "94da43f0b12ef4a98dabec096ff1184b0a9b5b6ee55824d257e5112cc7e88730"
+ },
+ {
+ "name": "person_with_blond_hair_tone5",
+ "unicode": "1F471-1F3FF",
+ "digest": "9e096a210ea720d32bc6a7005cd77f8b314ccf817fc3060da2e1796de39e9d60"
+ },
+ {
+ "name": "person_with_pouting_face",
+ "unicode": "1F64E",
+ "digest": "8c3199a422250d2db9a163156191ed2c6697d7f31699e2efe19e05ca26e5d225"
+ },
+ {
+ "name": "person_with_pouting_face_tone1",
+ "unicode": "1F64E-1F3FB",
+ "digest": "3e1f09bbf607381c992739ea92dd35cbd26b1bbc705a7d21b7c3156f50e9d8b3"
+ },
+ {
+ "name": "person_with_pouting_face_tone2",
+ "unicode": "1F64E-1F3FC",
+ "digest": "b5fc1cf3fdc5ff01105ee2452db90baa6a52c1e42f3795b2836c3e35197ece1f"
+ },
+ {
+ "name": "person_with_pouting_face_tone3",
+ "unicode": "1F64E-1F3FD",
+ "digest": "e8ec2539c458a8283c8c1050634c432b6363f3e64b68ba4c977994782f09b564"
+ },
+ {
+ "name": "person_with_pouting_face_tone4",
+ "unicode": "1F64E-1F3FE",
+ "digest": "5cab7a29699decd45682583446c2bf56ddcd69cd16e14db661b526a4076dfa17"
+ },
+ {
+ "name": "person_with_pouting_face_tone5",
+ "unicode": "1F64E-1F3FF",
+ "digest": "3caebd3626fd77d849859d1c99a747f80a2b59bfa5c1854494f1ce0485539a94"
+ },
+ {
+ "name": "pick",
+ "unicode": "26CF",
+ "digest": "24a3e8f592435b97272e6d134ea5503dce3012811659c4aadbad4e45d9fba679"
+ },
+ {
+ "name": "pig",
+ "unicode": "1F437",
+ "digest": "50b55fc74e8f6c89c6e04609381c99a660748908f0ef015f5da37089678ad0c3"
+ },
+ {
+ "name": "pig2",
+ "unicode": "1F416",
+ "digest": "e8189fb678608e8b9d69e11d2566f9a4765cbdff99ec8e66df30c7a2dabf742f"
+ },
+ {
+ "name": "pig_nose",
+ "unicode": "1F43D",
+ "digest": "7e299cb49a771884f5065c68733a5a1fe354a54cff009127230177f1717af4a5"
+ },
+ {
+ "name": "pill",
+ "unicode": "1F48A",
+ "digest": "53ae3379cc6721744979122569f157a5a13aa6b48e081a89f17b2d90134efe9e"
+ },
+ {
+ "name": "pineapple",
+ "unicode": "1F34D",
+ "digest": "ceda8ffa4a41594f28a4e69d03f8a1daeb2ba20740f0b8c56447cae833eea035"
+ },
+ {
+ "name": "ping_pong",
+ "unicode": "1F3D3",
+ "digest": "dd2a84716c93410a285ff759bfbc2dc31a10f90b203c7a657b908e5949e89a39"
+ },
+ {
+ "name": "piracy",
+ "unicode": "1F572",
+ "digest": "f42955ba75c598392e5e258be49968d858c876e0d6e7aa9dc795f7e8cff42be9"
+ },
+ {
+ "name": "pisces",
+ "unicode": "2653",
+ "digest": "75f11b9a094196b54a242420362fa7c0aeba7cfc497b187e1aaaba96d93684a7"
+ },
+ {
+ "name": "pizza",
+ "unicode": "1F355",
+ "digest": "ac94ae1c034f7b854ce2a483e1c219d101a84336f5065342f4824ff32ba705c4"
+ },
+ {
+ "name": "place_of_worship",
+ "unicode": "1F6D0",
+ "digest": "4fabc307b7e35f94288f6d53985485662a4814b11a9a382f0a3873d41b1290d3"
+ },
+ {
+ "name": "play_pause",
+ "unicode": "23EF",
+ "digest": "d69e8cdec33447283cf65d343b986115e27681d781b721db7894e5c587ca18ad"
+ },
+ {
+ "name": "point_down",
+ "unicode": "1F447",
+ "digest": "685f46a643be7f3033896e59a822f87d61ce50db6969bcdbacc743215a96bb7a"
+ },
+ {
+ "name": "point_down_tone1",
+ "unicode": "1F447-1F3FB",
+ "digest": "d3dd2608fe17d5649c960fcf8dbdb68466908d80fa349b7947b457da2a27ebb1"
+ },
+ {
+ "name": "point_down_tone2",
+ "unicode": "1F447-1F3FC",
+ "digest": "67ab236a14f6d63abcdb26433a66a183d223186c21ebc9f978fab50165ebe271"
+ },
+ {
+ "name": "point_down_tone3",
+ "unicode": "1F447-1F3FD",
+ "digest": "c8a2368f2cedb5bbb5cc0195b97fbf3787747637bf6e77bdc9a4edf4a3f22a04"
+ },
+ {
+ "name": "point_down_tone4",
+ "unicode": "1F447-1F3FE",
+ "digest": "6a92eab3bc8f950fa423e690f54a352887bda92f01e91c62eb3f3a9544c10cd8"
+ },
+ {
+ "name": "point_down_tone5",
+ "unicode": "1F447-1F3FF",
+ "digest": "6ad329f156414f421d6f8cf5e2a68d34b7a41f90d80e8e66b15bcbd3788126c7"
+ },
+ {
+ "name": "point_left",
+ "unicode": "1F448",
+ "digest": "cb520d6bba4c2b3bd7911315c9efce3261d048ff090437d7e24c9c5a7255043e"
+ },
+ {
+ "name": "point_left_tone1",
+ "unicode": "1F448-1F3FB",
+ "digest": "81813901bdaa8d261277f79aff9e9a21beb80a5855899941820b25f70786ec21"
+ },
+ {
+ "name": "point_left_tone2",
+ "unicode": "1F448-1F3FC",
+ "digest": "ecdc3dea0d644290aa7e0dab758c215822482a482ba35d825a33152453593c1e"
+ },
+ {
+ "name": "point_left_tone3",
+ "unicode": "1F448-1F3FD",
+ "digest": "84e73b6a37755016271c255eba164f349dbd2a2badf5d9ac1c6f4cbfcae589f0"
+ },
+ {
+ "name": "point_left_tone4",
+ "unicode": "1F448-1F3FE",
+ "digest": "d16800499b6c6ede94256796b1de8a8f723879f636849856b3bd8b7a092b5576"
+ },
+ {
+ "name": "point_left_tone5",
+ "unicode": "1F448-1F3FF",
+ "digest": "18b7108066cebf2d4090f29e595a2f01db94bd210f3b1d61dc269ec249a749b9"
+ },
+ {
+ "name": "point_right",
+ "unicode": "1F449",
+ "digest": "866180bf31e92de32aba336d5b5ce81773a29cdaadada1d93c944cf9ad9783bc"
+ },
+ {
+ "name": "point_right_tone1",
+ "unicode": "1F449-1F3FB",
+ "digest": "ebe2e4bf6bd46a5798b9a845a4ed055911c4fe58dbeacc4d39d6ea63e28e7cc9"
+ },
+ {
+ "name": "point_right_tone2",
+ "unicode": "1F449-1F3FC",
+ "digest": "b638662a67b1c6adde4f5abc789aae010b178404cdd1b71fcc982cdf8307c655"
+ },
+ {
+ "name": "point_right_tone3",
+ "unicode": "1F449-1F3FD",
+ "digest": "32c6ca2f992416ab2c36672dfbc1c0de8f102c77a13496dd8d63736a7b0261d2"
+ },
+ {
+ "name": "point_right_tone4",
+ "unicode": "1F449-1F3FE",
+ "digest": "89bd6828e9b82408a3829d49fa43332e2599f7d10bc6e5b14b750ef03267b173"
+ },
+ {
+ "name": "point_right_tone5",
+ "unicode": "1F449-1F3FF",
+ "digest": "390525048a12b0efa22de550c800e439b0deaad03f1f31155d179aef093354af"
+ },
+ {
+ "name": "point_up",
+ "unicode": "261D",
+ "digest": "31b5ca1303c1afabe1db322b24f73b23f3568c87a364f61c82f6e0c858c090e9"
+ },
+ {
+ "name": "point_up_2",
+ "unicode": "1F446",
+ "digest": "55c237054aa347c9847f3f3f577eb755db55dfcf793aa7de0f8f868574d70e8f"
+ },
+ {
+ "name": "point_up_2_tone1",
+ "unicode": "1F446-1F3FB",
+ "digest": "dc07e7732d973de96ae3b08b14c19e20b6c1aea7f5a30e7198679b750422e914"
+ },
+ {
+ "name": "point_up_2_tone2",
+ "unicode": "1F446-1F3FC",
+ "digest": "af2211fc4a1bd51d1e76f7bc43a6fa87bdd24e4295c52fdbdb01c1ca670a6cd7"
+ },
+ {
+ "name": "point_up_2_tone3",
+ "unicode": "1F446-1F3FD",
+ "digest": "917701169b3fb3e1b6e14a68e9572b25998ef2e38abac9ad8cf30100f8ea0dac"
+ },
+ {
+ "name": "point_up_2_tone4",
+ "unicode": "1F446-1F3FE",
+ "digest": "20843904764c6c3e55792cce0c55c76f72b97788c5229cad655ebf1f2873b439"
+ },
+ {
+ "name": "point_up_2_tone5",
+ "unicode": "1F446-1F3FF",
+ "digest": "1d0cca546027c717da50f90da65757af46fe7cd4e397da9b8e203446f707208d"
+ },
+ {
+ "name": "point_up_tone1",
+ "unicode": "261D-1F3FB",
+ "digest": "5ede60379dee23166c6b834d73da8b55268e330f67058843b8a3705dca6ed71a"
+ },
+ {
+ "name": "point_up_tone2",
+ "unicode": "261D-1F3FC",
+ "digest": "c94a15ef848d410aa5d32b8d0e453b59682fde6f39e6705cbb81cf0829833a81"
+ },
+ {
+ "name": "point_up_tone3",
+ "unicode": "261D-1F3FD",
+ "digest": "d319ce72876d97a3b1d4bc7c0679e546a983f02145d723a0da5ed0b73a51cfe7"
+ },
+ {
+ "name": "point_up_tone4",
+ "unicode": "261D-1F3FE",
+ "digest": "9171a27f86f27fd144347a17153fb56e30bd32e67a8f10f8c1f32a40cad4e009"
+ },
+ {
+ "name": "point_up_tone5",
+ "unicode": "261D-1F3FF",
+ "digest": "a894f87da4c3d33d5e6e74d003a33ec60c453db6507fe05d22235f807ead27d6"
+ },
+ {
+ "name": "police_car",
+ "unicode": "1F693",
+ "digest": "7999869cb75be404fc34942b6f9d8e84fa7e259aa892a1e8e1652a5f02cceea6"
+ },
+ {
+ "name": "poodle",
+ "unicode": "1F429",
+ "digest": "8a568d8688bf19b440b7c1b49fcfe6672b8f75af0031d89ab6212623430acadb"
+ },
+ {
+ "name": "poop",
+ "unicode": "1F4A9",
+ "digest": "140ce75a015ede5e764873e0ae9a56e7b2af333eddca0fe2796b14545c620258"
+ },
+ {
+ "name": "popcorn",
+ "unicode": "1F37F",
+ "digest": "12264cb16fca9317e3ba8d5924a2c8f15f790e36d2f29e7b12aaaf77e1beb73d"
+ },
+ {
+ "name": "post_office",
+ "unicode": "1F3E3",
+ "digest": "5e2d896cd646a2eecd5596af9e44ca1fa2745de5cedaf0f6d193b8243201c6cc"
+ },
+ {
+ "name": "postal_horn",
+ "unicode": "1F4EF",
+ "digest": "339aa61fa1567a1d159bb8204d15db889fbb6cc1106f6e1991b4a184d1bc1fc7"
+ },
+ {
+ "name": "postbox",
+ "unicode": "1F4EE",
+ "digest": "ef1a6543fccb9f1009cc3782c51883e51167721a0b49e8ba21e8e6049b216906"
+ },
+ {
+ "name": "potable_water",
+ "unicode": "1F6B0",
+ "digest": "4a2379835660dfa8b6780d662a10d1effab710f471eb9b5e6ade4772ba7e5aeb"
+ },
+ {
+ "name": "pouch",
+ "unicode": "1F45D",
+ "digest": "cbd47ec1a65f5c642773d8ea2e7e57f7041a2d7ed9df05fbdd7bc8743c6dece6"
+ },
+ {
+ "name": "poultry_leg",
+ "unicode": "1F357",
+ "digest": "d416e9464bd58073bd3e32eb06c0da96905609f47b9d667acdc0810e94237584"
+ },
+ {
+ "name": "pound",
+ "unicode": "1F4B7",
+ "digest": "1ac491bb8a91613b2b1faaac4e7b4bc794d2abef69ac79de17d54c824c3ef826"
+ },
+ {
+ "name": "pouting_cat",
+ "unicode": "1F63E",
+ "digest": "ba28d75401d5bb98773acd35aaf173356bae4d5a5520a226559478138364ebdf"
+ },
+ {
+ "name": "pray",
+ "unicode": "1F64F",
+ "digest": "fb0df9c1566014bd2df2a1afd59366b896f20c03ca3516e02e4be44ea556c8ea"
+ },
+ {
+ "name": "pray_tone1",
+ "unicode": "1F64F-1F3FB",
+ "digest": "c6d8cb46e65ad13a92e85f97e018176fd89513f23e899e15d1ad09e3b4009f4b"
+ },
+ {
+ "name": "pray_tone2",
+ "unicode": "1F64F-1F3FC",
+ "digest": "2cd68cbe1ba3254f173ec8136af79cae64873bd0f20480158c3e6babd5a1a442"
+ },
+ {
+ "name": "pray_tone3",
+ "unicode": "1F64F-1F3FD",
+ "digest": "d2e81863f74a87b96335fb108e7b206f28ed18185362ab4d42a3b0523801398b"
+ },
+ {
+ "name": "pray_tone4",
+ "unicode": "1F64F-1F3FE",
+ "digest": "ad1b91254b101d872325c325ebd1f2a6257cfe22e83de88e29dd16ffac191979"
+ },
+ {
+ "name": "pray_tone5",
+ "unicode": "1F64F-1F3FF",
+ "digest": "23f40a11321decbdc6a1d274b9ad571041d261d364d13d1063c306e73ad52254"
+ },
+ {
+ "name": "prayer_beads",
+ "unicode": "1F4FF",
+ "digest": "cb6f8700154f75749cf2642a25c03e255dc18428baf8b57f6bd807c92b83e28d"
+ },
+ {
+ "name": "princess",
+ "unicode": "1F478",
+ "digest": "47b93eb52d757c3c000d9760391ecb942776d883b28050d833fa11612483d8ee"
+ },
+ {
+ "name": "princess_tone1",
+ "unicode": "1F478-1F3FB",
+ "digest": "1e4073c2abdf51a61a1a85a3e063541fe96e9b9ec36ec6f7fb9c98deeb230869"
+ },
+ {
+ "name": "princess_tone2",
+ "unicode": "1F478-1F3FC",
+ "digest": "6a0a5dc447cd887798f908c15972e7a12d28d81f168b92bcb105786ac253bea0"
+ },
+ {
+ "name": "princess_tone3",
+ "unicode": "1F478-1F3FD",
+ "digest": "2f08d22fdfc7a7d66fcd87ae716b811f43077f5bb17fef87f5b7e2aa93700d70"
+ },
+ {
+ "name": "princess_tone4",
+ "unicode": "1F478-1F3FE",
+ "digest": "02129211bf7bf7ff6de35913b7069aee151532d878b8c4f7e24c012e5b09d4b4"
+ },
+ {
+ "name": "princess_tone5",
+ "unicode": "1F478-1F3FF",
+ "digest": "d676f103600b69dbfdb469469a77b9d561ec460ff862befa58ab30ddc909c9f7"
+ },
+ {
+ "name": "printer",
+ "unicode": "1F5A8",
+ "digest": "c44402c87071f8d31d3997abab53ab9f8f7c11434e747380928814ceb6b0a417"
+ },
+ {
+ "name": "prohibited",
+ "unicode": "1F6C7",
+ "digest": "bc6cdea2269a0ec39576d98dc4cda2bd9efa4dc330dde870148c6a85ad9cc63f"
+ },
+ {
+ "name": "projector",
+ "unicode": "1F4FD",
+ "digest": "fc361282f367926254c08150b02cb8fda7fa8d2c9c939d9360c78bf19a4f982e"
+ },
+ {
+ "name": "punch",
+ "unicode": "1F44A",
+ "digest": "5759db1d7093744c74b840bbb4761fb025d6633f8fa539bcb35dcf54fc05ceb6"
+ },
+ {
+ "name": "punch_tone1",
+ "unicode": "1F44A-1F3FB",
+ "digest": "793b3fa2a43c23b2c1e1b48b86ae35e8c4024cd065fac0a0a5ada87cb78d6de3"
+ },
+ {
+ "name": "punch_tone2",
+ "unicode": "1F44A-1F3FC",
+ "digest": "6fc2467e99982ab00b0c352c6f7793d34faf17b16a0312082c9bd1f0709e3938"
+ },
+ {
+ "name": "punch_tone3",
+ "unicode": "1F44A-1F3FD",
+ "digest": "bf747b29952550c5b4d3807b9ed85b5e5d4bcc3265b0e214791f7db547f861fb"
+ },
+ {
+ "name": "punch_tone4",
+ "unicode": "1F44A-1F3FE",
+ "digest": "3b6c0ccb682552f32d6744c438e3af04a1732c67a74bcafb14c723cf526fed87"
+ },
+ {
+ "name": "punch_tone5",
+ "unicode": "1F44A-1F3FF",
+ "digest": "945bae1aa3587cd1dc57d1ec4da18c67a59e0e7150dcc8735e5357b4ea1234ac"
+ },
+ {
+ "name": "purple_heart",
+ "unicode": "1F49C",
+ "digest": "e0eb886e74f22d40d059ff3a089d472af53c6c53de380f428cca140dfd046345"
+ },
+ {
+ "name": "purse",
+ "unicode": "1F45B",
+ "digest": "67d82ff9a4d76148b9d98538d4b786f880058a556e650ec3f93e1632aa42aaa7"
+ },
+ {
+ "name": "pushpin",
+ "unicode": "1F4CC",
+ "digest": "c4de129d5d8744caffeb2f499fcc0bc6b551843938f8166ffecd0de00bda66e3"
+ },
+ {
+ "name": "pushpin_black",
+ "unicode": "1F588",
+ "digest": "80ebac74edb9e8e1f8a219b32a676d318ed73b359cd8193b91b493d775307f63"
+ },
+ {
+ "name": "put_litter_in_its_place",
+ "unicode": "1F6AE",
+ "digest": "b26d3b68bd62d30ecfe75cfaf309a7a0f91e92db0aa18b0b97b97baf0609d4e6"
+ },
+ {
+ "name": "question",
+ "unicode": "2753",
+ "digest": "258e3169bae177fb0f01ed5f9b933f7f02dd2673e12a316af44a0c3729a78a2c"
+ },
+ {
+ "name": "rabbit",
+ "unicode": "1F430",
+ "digest": "9817a7454aeda77d28f63eb13c0dc0a6d9e6c9abe3dcf538b4b3477e494cddb6"
+ },
+ {
+ "name": "rabbit2",
+ "unicode": "1F407",
+ "digest": "67ba57a31b0768a2118faabdcb088f96f1441e1132397f65b6937d523ff7dabb"
+ },
+ {
+ "name": "race_car",
+ "unicode": "1F3CE",
+ "digest": "2e9828e3884c79ad7e9e1173d3470790f3f56cfa08ef4e38deff45db0728c66c"
+ },
+ {
+ "name": "racehorse",
+ "unicode": "1F40E",
+ "digest": "36aa3c7123ee7e15600657166032b21b8edeb192cf6d3ada39b5c65001f7fc40"
+ },
+ {
+ "name": "radio",
+ "unicode": "1F4FB",
+ "digest": "b1403f9a883405b909208f52c9474c2d3923681ea0b02609a6e9dc12460319a5"
+ },
+ {
+ "name": "radio_button",
+ "unicode": "1F518",
+ "digest": "9bcdac17b3620331a32f9bb876812231a701eb5a7f696e7d875f877ab92159fc"
+ },
+ {
+ "name": "radioactive",
+ "unicode": "2622",
+ "digest": "5ad8e8594617c0153672a76421deb836e05c6098020c33af3f975f8fcfe216e4"
+ },
+ {
+ "name": "rage",
+ "unicode": "1F621",
+ "digest": "02ac70551fc51478884c133b29539cae58b463c760db38c0aeec1bdf5b282312"
+ },
+ {
+ "name": "railway_car",
+ "unicode": "1F683",
+ "digest": "8490e2ecf94c7c1d1e22fea0d80cc18a49648741009e51984f583b17bbd022e2"
+ },
+ {
+ "name": "railway_track",
+ "unicode": "1F6E4",
+ "digest": "63ee881cc775d5b2711082b6c96ab44d5204c5d390afd6d8ee97e52aeeaa5e5e"
+ },
+ {
+ "name": "rainbow",
+ "unicode": "1F308",
+ "digest": "bbd8ecc8d0737948969a3539d2d202e599404e509f1a21bdbb0a0c41c2540522"
+ },
+ {
+ "name": "raised_hand",
+ "unicode": "270B",
+ "digest": "4192881a0d613b4fcb19b1c2d8b83aadee6f0b12170721c8dd7b1ccef6540199"
+ },
+ {
+ "name": "raised_hand_tone1",
+ "unicode": "270B-1F3FB",
+ "digest": "df2e046c99dceb9184c50a777b403d72bfb25ff473d6a4e20bb9a731db64ed8d"
+ },
+ {
+ "name": "raised_hand_tone2",
+ "unicode": "270B-1F3FC",
+ "digest": "ed179299a1c397cd51cf6067d6795d71a3831d35e1ec9eacbf0286c8992c1e7a"
+ },
+ {
+ "name": "raised_hand_tone3",
+ "unicode": "270B-1F3FD",
+ "digest": "cacbd0ddef65bc01a41bd921ea159f8cd89050309b10f15780d6199f79434a54"
+ },
+ {
+ "name": "raised_hand_tone4",
+ "unicode": "270B-1F3FE",
+ "digest": "04c934c7a55b83bcfa7f3880fc1f6aa0f188090c37b9670e6775a512a1cf59e9"
+ },
+ {
+ "name": "raised_hand_tone5",
+ "unicode": "270B-1F3FF",
+ "digest": "da0c4283b7b19861237c023234c6db28045b8f5a5971acb015733e08e2940e86"
+ },
+ {
+ "name": "raised_hands",
+ "unicode": "1F64C",
+ "digest": "308e475f38558e73bd66e28693d77478caa5bca4360cffaffc2a97b5858c56ba"
+ },
+ {
+ "name": "raised_hands_tone1",
+ "unicode": "1F64C-1F3FB",
+ "digest": "e39b9bc49dccc127e44f543e98961fcf5bcd44d6e216741bcd10ec3667263c84"
+ },
+ {
+ "name": "raised_hands_tone2",
+ "unicode": "1F64C-1F3FC",
+ "digest": "f376ab13071ffdc11888ec221ef5b4de546ca0f60bd9ae30bf3da4066c220462"
+ },
+ {
+ "name": "raised_hands_tone3",
+ "unicode": "1F64C-1F3FD",
+ "digest": "67694325a43e629c00fa9bd2ff7e19f84f216b2855ae2cf097762dfa7aca25e6"
+ },
+ {
+ "name": "raised_hands_tone4",
+ "unicode": "1F64C-1F3FE",
+ "digest": "a2254fe75a0770708916a4ddd5db4420221c6ea9db9f74068d14eadfc0f3772c"
+ },
+ {
+ "name": "raised_hands_tone5",
+ "unicode": "1F64C-1F3FF",
+ "digest": "bd7c9897cefb454ccdc46027bf56d6587565bdd345d7d0f081b7b671a53f6c99"
+ },
+ {
+ "name": "raising_hand",
+ "unicode": "1F64B",
+ "digest": "d57178fc77e9fa140682634da35f9ab12a65d9b4c506b7cd8a9697f1b5910bdb"
+ },
+ {
+ "name": "raising_hand_tone1",
+ "unicode": "1F64B-1F3FB",
+ "digest": "f46b34361ef79743f3187d6860182bbe1ae411031db7fe5c0f7292fa472b9c16"
+ },
+ {
+ "name": "raising_hand_tone2",
+ "unicode": "1F64B-1F3FC",
+ "digest": "20b85a2ebca150b2020a04b41d34884c78c22f42c251e2b9d23fd3724574143b"
+ },
+ {
+ "name": "raising_hand_tone3",
+ "unicode": "1F64B-1F3FD",
+ "digest": "5e0401b528c2b8edff766d39cdcedbe9abebe4c940df7a36ace61f59c08d508a"
+ },
+ {
+ "name": "raising_hand_tone4",
+ "unicode": "1F64B-1F3FE",
+ "digest": "e4f5624264269ad09cde207cd7d4eb0fd46de816880daeec457ac8cd51cc1b7b"
+ },
+ {
+ "name": "raising_hand_tone5",
+ "unicode": "1F64B-1F3FF",
+ "digest": "eb34b6c037bee5bbc4222f6aab421aa785f527ebf1b5e971769e5102244d60e1"
+ },
+ {
+ "name": "ram",
+ "unicode": "1F40F",
+ "digest": "b71950d7a286a4c4909c5ec7c35211c2a5c20b6bad341bd863c6a85c4bcf9c80"
+ },
+ {
+ "name": "ramen",
+ "unicode": "1F35C",
+ "digest": "7dd185b24852b577913edc78647cd53b27d42e225fde29aa2f3aba25c980b5c4"
+ },
+ {
+ "name": "rat",
+ "unicode": "1F400",
+ "digest": "7a10d9ba5ee1010d421d9cf73d7966507302a69617a32fe9f1a00d57a31f7bd7"
+ },
+ {
+ "name": "record_button",
+ "unicode": "23FA",
+ "digest": "c2ba672994ad0f99d7fdc449f3fee45a2dca68a58f9fe95825b38465a30ef44e"
+ },
+ {
+ "name": "recycle",
+ "unicode": "267B",
+ "digest": "74a54ed62a40dfbdcace1f08b085658a77d45c62570273927ad270bf9a8a2f4d"
+ },
+ {
+ "name": "red_car",
+ "unicode": "1F697",
+ "digest": "558730d6418aa5d85b73af58c8041efd12cff906e26ea47c50963f66d33d6eb8"
+ },
+ {
+ "name": "red_circle",
+ "unicode": "1F534",
+ "digest": "9dcf0132f6f2cc81702f0e3b15b37984e8439796705bf98f68ba449b3dfa5307"
+ },
+ {
+ "name": "registered",
+ "unicode": "00AE",
+ "digest": "ed924107384461aabb4924c401c6c087ffa047bc2ef735823e7c2be67804707c"
+ },
+ {
+ "name": "relaxed",
+ "unicode": "263A",
+ "digest": "65072f7b9bfaaa92b8a0ed012dffe2cfd2efa3748264aaf450aa31ba6bd44045"
+ },
+ {
+ "name": "relieved",
+ "unicode": "1F60C",
+ "digest": "1f2c7ae6a9d74a112de89403be6eca3d8155d70395e7fce51032fc961f235c7d"
+ },
+ {
+ "name": "reminder_ribbon",
+ "unicode": "1F397",
+ "digest": "e4a2afc7dce40589657f7043ba8acc9638fd4117252278233ea89f84cddad387"
+ },
+ {
+ "name": "repeat",
+ "unicode": "1F501",
+ "digest": "27b6dad9215e58e24c607a39dbf398ecf66ccb692c81e08eb2f5f4912db30522"
+ },
+ {
+ "name": "repeat_one",
+ "unicode": "1F502",
+ "digest": "052d13f2b08eaf70b31252aa78f95d06fbe22c58945c19381b13cbeb1c855651"
+ },
+ {
+ "name": "restroom",
+ "unicode": "1F6BB",
+ "digest": "b77fbc4247c241362e5ef9e6eb58b1b437aa9d16b65886cec0c55ceb55c1440e"
+ },
+ {
+ "name": "revolving_hearts",
+ "unicode": "1F49E",
+ "digest": "2b8925d3e78df2dba8534252fe60bf03285346f6b3697be7668bd568e6d85931"
+ },
+ {
+ "name": "rewind",
+ "unicode": "23EA",
+ "digest": "91a95b26d12ca76111556096f4d96484c9f1d7e1b20ccff5a3291b36e529a6d1"
+ },
+ {
+ "name": "ribbon",
+ "unicode": "1F380",
+ "digest": "9c0296d8c2baa84c99347c431bf79b288d98b5f17b1ce7605ad7ce1da265d5aa"
+ },
+ {
+ "name": "rice",
+ "unicode": "1F35A",
+ "digest": "e34849496a79e71ae4700df94f2a54895bf6de758a92edeae33fe78295a3ba21"
+ },
+ {
+ "name": "rice_ball",
+ "unicode": "1F359",
+ "digest": "52df5da8b0edbdeb56d66e0f30ad4549abdd81c064f7269d920dcac66a3df2e4"
+ },
+ {
+ "name": "rice_cracker",
+ "unicode": "1F358",
+ "digest": "d55f8f9d807f4619eb243c510938067a7417a64bd9435b05dfeb2a36fdb2b6a0"
+ },
+ {
+ "name": "rice_scene",
+ "unicode": "1F391",
+ "digest": "482d854d8d30edfc1ecd48a4ce476e6498606321405bf5a0b4ff74489a092af8"
+ },
+ {
+ "name": "right_speaker",
+ "unicode": "1F568",
+ "digest": "d268bb84be863c0884620dfc6d2a764b0c7466d2f9810549b138e21ac70add4e"
+ },
+ {
+ "name": "right_speaker_one",
+ "unicode": "1F569",
+ "digest": "5b92daa87bdf6ee15e798bec382a2ee885f4e6e77a68a3f626adcfe4c782b375"
+ },
+ {
+ "name": "right_speaker_three",
+ "unicode": "1F56A",
+ "digest": "4d00b720a65bd0f4c3682b290b1976ec2388d6ae61225398f4e70556ae9e5f80"
+ },
+ {
+ "name": "ring",
+ "unicode": "1F48D",
+ "digest": "ae2a93e7895b9b89f5a39f01d356ffed988f219ef8b658a56c55285826a4533b"
+ },
+ {
+ "name": "ringing_bell",
+ "unicode": "1F56D",
+ "digest": "d71ab7fa937fc4af507b5b07ea58a4f31e875d9e8304ef2b850d7cebe0e9cd66"
+ },
+ {
+ "name": "robot",
+ "unicode": "1F916",
+ "digest": "cc0e363774b86e21a5b2cea7f7af85bca9e92c124ebcd39c6067c125048baa60"
+ },
+ {
+ "name": "rocket",
+ "unicode": "1F680",
+ "digest": "65d8bd005ceac41904237b7a8c5f55f16713a55d971522f0bbe63a1d548e515d"
+ },
+ {
+ "name": "roller_coaster",
+ "unicode": "1F3A2",
+ "digest": "907baab1f3d7becf3f8a3b1264642b395bd73b4af49e23058b3abb5c69e9106a"
+ },
+ {
+ "name": "rolling_eyes",
+ "unicode": "1F644",
+ "digest": "f596f203030b6c9bd743848512aa3fc7919447020d35ae5c2bf13ccb16fa2dbe"
+ },
+ {
+ "name": "rooster",
+ "unicode": "1F413",
+ "digest": "6cefdaa45631ed8c9480e15f578c793d95af81b42687164fd7900eee325ccf07"
+ },
+ {
+ "name": "rose",
+ "unicode": "1F339",
+ "digest": "584909a4a2ece625c688f8479a39692bb8e816b692e6eb7dfd40cb045259b1b2"
+ },
+ {
+ "name": "rosette",
+ "unicode": "1F3F5",
+ "digest": "0ce3b85ca05124ab99d57ebc9aa17bb246ee614d2fcda1ef62bf42ac7e616148"
+ },
+ {
+ "name": "rosette_black",
+ "unicode": "1F3F6",
+ "digest": "ae8675891c88f9d98463d35178445950c39b0deb0f0e8b3f341228a6e0d0e477"
+ },
+ {
+ "name": "rotating_light",
+ "unicode": "1F6A8",
+ "digest": "369e069e0bfecc7413e75f4015e9c1de527a33c7cce3f6c2b4adb60a0d9d338c"
+ },
+ {
+ "name": "round_pushpin",
+ "unicode": "1F4CD",
+ "digest": "1bc5fe5a90a6e56ea00246f1b008a0e0cce0d77c226dc0300bf9a2804b543877"
+ },
+ {
+ "name": "rowboat",
+ "unicode": "1F6A3",
+ "digest": "c10e09bf8be8b1a8ef3113edd9327126d6a4644f3bc81c7ada2922851e4d1cfb"
+ },
+ {
+ "name": "rowboat_tone1",
+ "unicode": "1F6A3-1F3FB",
+ "digest": "a84fc1b30d1a284dcd3899dc4de8f11e7b65c258528eb41c7dbf8f82425fee12"
+ },
+ {
+ "name": "rowboat_tone2",
+ "unicode": "1F6A3-1F3FC",
+ "digest": "85f001430a2ad607a15901f7c2dcf8381471f42d6cc0775e76a2ff1f457151c1"
+ },
+ {
+ "name": "rowboat_tone3",
+ "unicode": "1F6A3-1F3FD",
+ "digest": "adf8b1e45a46a13f3db40c29df0312216558e9d0c615aa46a8e913cee5003a81"
+ },
+ {
+ "name": "rowboat_tone4",
+ "unicode": "1F6A3-1F3FE",
+ "digest": "05482749ec40bdf02e53fc42d316c51f4f3ed643f21e8fc16b81930e4a884bda"
+ },
+ {
+ "name": "rowboat_tone5",
+ "unicode": "1F6A3-1F3FF",
+ "digest": "d4bb337d948996d4a23d87f99988f02fc207815b862082ffd2eef5f0c1016aa9"
+ },
+ {
+ "name": "rugby_football",
+ "unicode": "1F3C9",
+ "digest": "e14aebbded78d4a5e9b4028f79a8ca840d02798c6758cb9e926e992e2a35a4f3"
+ },
+ {
+ "name": "runner",
+ "unicode": "1F3C3",
+ "digest": "58a884f06d37b0ce78197bebcd3f0e102dd90022ebd86ec70a2ef5a5cdf9683b"
+ },
+ {
+ "name": "runner_tone1",
+ "unicode": "1F3C3-1F3FB",
+ "digest": "65f1633d1517803de23686d2dbcc75a5787874266db4981138ccdbe4badc773c"
+ },
+ {
+ "name": "runner_tone2",
+ "unicode": "1F3C3-1F3FC",
+ "digest": "2bc81f3fb77445cdc75c34806ab0ce912bacfe47f63b5d2011a4f5d370cf7064"
+ },
+ {
+ "name": "runner_tone3",
+ "unicode": "1F3C3-1F3FD",
+ "digest": "beaf5f254cba2991fdd0c38ce2ddd1b4c1110e15b2b7bc026d32f162e295c4ef"
+ },
+ {
+ "name": "runner_tone4",
+ "unicode": "1F3C3-1F3FE",
+ "digest": "21d531ba9b3d13747ad636b8f7a6f184c974bf61d9f529975a64f9629263c407"
+ },
+ {
+ "name": "runner_tone5",
+ "unicode": "1F3C3-1F3FF",
+ "digest": "b02a5bcc58cc45f8219262ec44c77764172fd8f2624d9122ded4a5a5db04c0ed"
+ },
+ {
+ "name": "running_shirt_with_sash",
+ "unicode": "1F3BD",
+ "digest": "431bed35f4a55175bf99af769e74a81e8650c6ab34af6ecddaa1417ff7e437e6"
+ },
+ {
+ "name": "sa",
+ "unicode": "1F202",
+ "digest": "a47a480631f874e8a2cd69b5d513f90a1e81a96bfa2f6025bf244a82baca3656"
+ },
+ {
+ "name": "sagittarius",
+ "unicode": "2650",
+ "digest": "14871e6681c35e4a63a0b19613f77b3674d00cb78d06975e02ca29e61b5cea8c"
+ },
+ {
+ "name": "sailboat",
+ "unicode": "26F5",
+ "digest": "6f742dde6c180a174b771aa3942b558e98a3dc1eb212dd31add86c5fa5620865"
+ },
+ {
+ "name": "sake",
+ "unicode": "1F376",
+ "digest": "aa1392790c805950779dde7778292c937f8c1aaecb522876171d5ee542ec51f8"
+ },
+ {
+ "name": "sandal",
+ "unicode": "1F461",
+ "digest": "14f1e9003a6acd90a55f23c48ed87a758fca586f2e0b0edc4dc9d1deef9eb067"
+ },
+ {
+ "name": "santa",
+ "unicode": "1F385",
+ "digest": "12feddd84eb49ce30ae68d4f93d66e2c0dd11297a4d1275c9a50d4f35bea83a9"
+ },
+ {
+ "name": "santa_tone1",
+ "unicode": "1F385-1F3FB",
+ "digest": "a75813770efe27d5b4c80ad892d0c796d88d1a0dbb1bd02d5f68882d7abad479"
+ },
+ {
+ "name": "santa_tone2",
+ "unicode": "1F385-1F3FC",
+ "digest": "90f8072fdde5f4a275cbd1902d6c94689d453b1bee0336213dc9d6f7e1d038e1"
+ },
+ {
+ "name": "santa_tone3",
+ "unicode": "1F385-1F3FD",
+ "digest": "0973053e7b77d268080126a50b95b45429630e5d49f62210e7b71840794c7dc5"
+ },
+ {
+ "name": "santa_tone4",
+ "unicode": "1F385-1F3FE",
+ "digest": "5cd49c0d199a42846b400b3c1244d448ed6fe5ce993d379817cb2a5f7c0b609b"
+ },
+ {
+ "name": "santa_tone5",
+ "unicode": "1F385-1F3FF",
+ "digest": "a54c36dfa99b39549fb1d3dd7f0021a7aee28112960172ed466dacc67961c525"
+ },
+ {
+ "name": "satellite",
+ "unicode": "1F4E1",
+ "digest": "3b9797c8161526edce0bd8e9b8563055166f9307761c367ab3e2ad7645b6dee0"
+ },
+ {
+ "name": "satellite_orbital",
+ "unicode": "1F6F0",
+ "digest": "104b135e3736a4bcfd51a42dadb53bf3e00d7f85d77a94bcb86c6704fbfacd01"
+ },
+ {
+ "name": "saxophone",
+ "unicode": "1F3B7",
+ "digest": "1090da174ce8aa4f7d35025f65d5ac235e09310abde998d2a725ef3a989a2b75"
+ },
+ {
+ "name": "scales",
+ "unicode": "2696",
+ "digest": "b2984caa182b691a33650344708f47c61d6d319fd067760d7594c2ef60c1e27b"
+ },
+ {
+ "name": "school",
+ "unicode": "1F3EB",
+ "digest": "caf35260dc465a833521e4a0034201978fed41bbf72cd770756b3340c60e8a0c"
+ },
+ {
+ "name": "school_satchel",
+ "unicode": "1F392",
+ "digest": "a89a2cc46d24d57c2d6b95ed7a56ed829ae2f97b9e6201b2d5adc78c2b78518b"
+ },
+ {
+ "name": "scissors",
+ "unicode": "2702",
+ "digest": "a4e91127ac83acf5ebc64fbeca768cbbf24f2f0a484861c9c8104bee377b97ae"
+ },
+ {
+ "name": "scorpion",
+ "unicode": "1F982",
+ "digest": "a090a96731bc1171b054b51abec4c9b36faa62708fd51ac48277ccf5e55d9d12"
+ },
+ {
+ "name": "scorpius",
+ "unicode": "264F",
+ "digest": "1ad9bc1030a8f58f3f3223bac52c954cc7a0350805a9df7a42a26972c3b74728"
+ },
+ {
+ "name": "scream",
+ "unicode": "1F631",
+ "digest": "75d613786737ee9c0a74da7394b9ae190eacc7182164627ad8205ac64e4cc09a"
+ },
+ {
+ "name": "scream_cat",
+ "unicode": "1F640",
+ "digest": "eee04ff27c2c6b57d698cb87b0af8064ba8313ffc13aa090e38cd5aa8c3d2f76"
+ },
+ {
+ "name": "scroll",
+ "unicode": "1F4DC",
+ "digest": "b8205847649e3ce6b946f1d1da972ed015adde3841c62971b8169235f4b41c1f"
+ },
+ {
+ "name": "seat",
+ "unicode": "1F4BA",
+ "digest": "054c4db0bc8939e9dd951a3f73e9ae4b3c31652784f4d304b509c2bd32f98e31"
+ },
+ {
+ "name": "secret",
+ "unicode": "3299",
+ "digest": "77daef6e5c91d55228781ddec954a7089d1851297ec81daef6e813cd22915b5e"
+ },
+ {
+ "name": "see_no_evil",
+ "unicode": "1F648",
+ "digest": "aa5883fe605aeaa172d16640b8347580f9cb7d85a596da1b13955f27b0b79297"
+ },
+ {
+ "name": "seedling",
+ "unicode": "1F331",
+ "digest": "a75ec929402de1e653fd6bc89e5be2f92fe5fe52f39e4b6c290eae3c59172b56"
+ },
+ {
+ "name": "seven",
+ "unicode": "0037-20E3",
+ "digest": "c6a34020f6bb25871164fad44302a45c5bffced87f51dfbb816c2985ad7f6a1c"
+ },
+ {
+ "name": "shamrock",
+ "unicode": "2618",
+ "digest": "530e6b987ecb9bcbf0d6e0e11bd075e7949873c784da4f9e1e1b47efd37e5058"
+ },
+ {
+ "name": "shaved_ice",
+ "unicode": "1F367",
+ "digest": "fc22c3568f6be56771e83fd0e67b7eb3750041304d5d4979d3ec417f5201230e"
+ },
+ {
+ "name": "sheep",
+ "unicode": "1F411",
+ "digest": "3e3656b82784164ca02c5d775db7245260f0119d2c1d35ba552a6dc75ef02544"
+ },
+ {
+ "name": "shell",
+ "unicode": "1F41A",
+ "digest": "ff2f4f574b61bffd85c63bc2315c80d3cbcaba37a7c15a1f00783d312bd441d4"
+ },
+ {
+ "name": "shield",
+ "unicode": "1F6E1",
+ "digest": "062aec4a325da7b637c5710846c7e7319229be49b7e59f50428442a7ef725d60"
+ },
+ {
+ "name": "shinto_shrine",
+ "unicode": "26E9",
+ "digest": "9768fe94142a7dc169703d3707b203f285a546455e29fe2bbf185d44f160d6d0"
+ },
+ {
+ "name": "ship",
+ "unicode": "1F6A2",
+ "digest": "f8d5b0c8ec66287b732d9171ac1913be02efb656de11501213a207d8a6c801e1"
+ },
+ {
+ "name": "shirt",
+ "unicode": "1F455",
+ "digest": "e2e72c323f3bfaea02e8cf52201aa144dc56ec0f25ec97d5f04ee6c2ee99104e"
+ },
+ {
+ "name": "shopping_bags",
+ "unicode": "1F6CD",
+ "digest": "0194ba540c47e4fc6403be2df68f785d56810efc2dc011dfbf700f3778cb704a"
+ },
+ {
+ "name": "shower",
+ "unicode": "1F6BF",
+ "digest": "c945120182392510348de9a957c2b77a4645d118691298a2ad660dafa62a859c"
+ },
+ {
+ "name": "signal_strength",
+ "unicode": "1F4F6",
+ "digest": "7876ed9d602e1be746ca0629f072d85668d1f9715e9135745e803bdf89819a3c"
+ },
+ {
+ "name": "six",
+ "unicode": "0036-20E3",
+ "digest": "b409f23b73e46393c7a814442816b5880c38ef12a7feb5505e71276c195e8ca9"
+ },
+ {
+ "name": "six_pointed_star",
+ "unicode": "1F52F",
+ "digest": "4bc294dcbf4185250873b52b2fb5453fb7d80df912db929add6e4b7efc066363"
+ },
+ {
+ "name": "ski",
+ "unicode": "1F3BF",
+ "digest": "7ee81a2e2f7ff4e32dbf3d64b034e7542ec0c86d32e25eb125052e674943d75f"
+ },
+ {
+ "name": "skier",
+ "unicode": "26F7",
+ "digest": "49df9a4206ae0c7c2dbfc8a8b13fd3e14e6f7e750bd5a8581ab6a1626d4c165e"
+ },
+ {
+ "name": "skull",
+ "unicode": "1F480",
+ "digest": "dfd169764b192ac7c6e5101277dd9f1e010e86bdd32ad37e00ed4499fc0a5dd6"
+ },
+ {
+ "name": "skull_crossbones",
+ "unicode": "2620",
+ "digest": "e2acf0f36b6a6800c1829a1c6551b5d0eb6dcdef4b7f02070cf69570aeab608c"
+ },
+ {
+ "name": "sleeping",
+ "unicode": "1F634",
+ "digest": "4ead95079b1a542eedd0e5a0e93fddb318a002bdaffaa2fe5d8d7f20bf8143ed"
+ },
+ {
+ "name": "sleeping_accommodation",
+ "unicode": "1F6CC",
+ "digest": "10ee8cd925a75d7977b7cf004e08b5a8147b509ee4281e879a8b57c4a7c2cb04"
+ },
+ {
+ "name": "sleepy",
+ "unicode": "1F62A",
+ "digest": "dea3b246bb8af1b28e200358e3d5d59c8bba1813f35a7f4a57ec568ef43591db"
+ },
+ {
+ "name": "slight_frown",
+ "unicode": "1F641",
+ "digest": "3ae82b38b58ffa50eddebd87153428d880ca181f4f4178a9ca3bd813ea15ccbc"
+ },
+ {
+ "name": "slight_smile",
+ "unicode": "1F642",
+ "digest": "5eee09f634a4e2031927d008a6530a258a00e611ead0c386dd5b7ebb5e75a306"
+ },
+ {
+ "name": "slot_machine",
+ "unicode": "1F3B0",
+ "digest": "9d516b389299431b608c89d3f02ac68d28cb8df2a780f2048923bbcfbb49f416"
+ },
+ {
+ "name": "small_blue_diamond",
+ "unicode": "1F539",
+ "digest": "97389e82755dc43015089dee635072357ec347f0117b2d3e9b006c46514948ee"
+ },
+ {
+ "name": "small_orange_diamond",
+ "unicode": "1F538",
+ "digest": "67442d3b707501b7768f606115688373d13617ecf0b3b03ace0f1a6d38f66ddf"
+ },
+ {
+ "name": "small_red_triangle",
+ "unicode": "1F53A",
+ "digest": "e0a556a3dd5bbf0290ed7c00eb6f6307dc2ea98d1fb3111fd85a7f46242a3638"
+ },
+ {
+ "name": "small_red_triangle_down",
+ "unicode": "1F53B",
+ "digest": "7a11dcb8a517df220493d471759e4f4bca0db3769e2d942bbf596a88a3e57f72"
+ },
+ {
+ "name": "smile",
+ "unicode": "1F604",
+ "digest": "46a7c3545b0038dfce6825d97544f6665f28512ad05c404d668e32ac599c7ecb"
+ },
+ {
+ "name": "smile_cat",
+ "unicode": "1F638",
+ "digest": "c1db961f0fa261532b842816aca7ea7f6d8b461c7e930a1a1c91f96efd9db515"
+ },
+ {
+ "name": "smiley",
+ "unicode": "1F603",
+ "digest": "deeaaee64ebdd9fc0bcb719db75c3f7e0c33ddbcc97f6cd51f9f84377a4368ce"
+ },
+ {
+ "name": "smiley_cat",
+ "unicode": "1F63A",
+ "digest": "85ad852cb3881c4b754af172fdfc6231af42578033ea9f2981ceae944c41e72f"
+ },
+ {
+ "name": "smiling_imp",
+ "unicode": "1F608",
+ "digest": "e777bdf186d89921df106d23bf002967b69afffd7e981b3cbb19f89630a06e87"
+ },
+ {
+ "name": "smirk",
+ "unicode": "1F60F",
+ "digest": "2e7fddd8bed33ef4b7d8c13320302b87a28203e576ef87bd43716952cf0b5ace"
+ },
+ {
+ "name": "smirk_cat",
+ "unicode": "1F63C",
+ "digest": "9ca0721f4c18592b4b809ade8f716b95fa30cd31dd87d1e41db29a319becd705"
+ },
+ {
+ "name": "smoking",
+ "unicode": "1F6AC",
+ "digest": "3d14b3f0c57eb7a6a31ff371b0a454986533b79dbbeac78a76e4063478911b8d"
+ },
+ {
+ "name": "snail",
+ "unicode": "1F40C",
+ "digest": "57d946c7ec84dfad71bc4f7a042927ec5712aef50c66d21af892b6c8a7faf5e1"
+ },
+ {
+ "name": "snake",
+ "unicode": "1F40D",
+ "digest": "d084da540162288721364992f3b8059cbf2efd9f5b48f49a196ddbe23a073870"
+ },
+ {
+ "name": "snowboarder",
+ "unicode": "1F3C2",
+ "digest": "de9e1767526de606f4908743af94cc17e89fdb0a2a44167d3d021ef09d033ab9"
+ },
+ {
+ "name": "snowflake",
+ "unicode": "2744",
+ "digest": "e476863ccd7d7b549c6191fb25c121c6a467b4baef4683b7dc3e0a793c2e5d76"
+ },
+ {
+ "name": "snowman",
+ "unicode": "26C4",
+ "digest": "792946b8446f2243d11b89d07c73a774be3abd36573f3918640b1ba8714270b5"
+ },
+ {
+ "name": "snowman2",
+ "unicode": "2603",
+ "digest": "571acabaa4d55782c4529b762423a7e34cb1fb6bb7852cbd013e2e846d8311d1"
+ },
+ {
+ "name": "sob",
+ "unicode": "1F62D",
+ "digest": "562f02ab584bcbcf9ba73cf7fa7d7129965266abd28db2c73913b8c42f2f5aca"
+ },
+ {
+ "name": "soccer",
+ "unicode": "26BD",
+ "digest": "5fd0d534659b63dc862c65a80561b255bece0b76708fe8ecbae8e01b08d8cad0"
+ },
+ {
+ "name": "soon",
+ "unicode": "1F51C",
+ "digest": "d2a1ab16a4056d80c827ea23f9332bb73235fc841b857cbf545062ff8aeed81d"
+ },
+ {
+ "name": "sos",
+ "unicode": "1F198",
+ "digest": "fadfe8337e133a6f05d205d0807f288e5c230db04cb09f3547ce0cb73cfcf48a"
+ },
+ {
+ "name": "sound",
+ "unicode": "1F509",
+ "digest": "c0074b338fd461f1f9d1143b7f9b3781ddb3fd501ea79b2410630433a8e87b83"
+ },
+ {
+ "name": "space_invader",
+ "unicode": "1F47E",
+ "digest": "d264390004bd28d664dfda0069104be6db32ce477e23a95ac595bac2e29fd4e7"
+ },
+ {
+ "name": "spades",
+ "unicode": "2660",
+ "digest": "d1ad99a4fc20dfea881a9062a9f2109e483dbb5dea3b29e9653cb27ec57b4800"
+ },
+ {
+ "name": "spaghetti",
+ "unicode": "1F35D",
+ "digest": "ac63f9ad143e236ce6068098e5330a333ade9cddfb3dd6b1457ea47ce9dcf7e9"
+ },
+ {
+ "name": "sparkle",
+ "unicode": "2747",
+ "digest": "95b8f4f1bb6080cd1d7bd333c4724dbba43ed196dce72a2bbaab46c4a1bc0e48"
+ },
+ {
+ "name": "sparkler",
+ "unicode": "1F387",
+ "digest": "3a296e4d0081ad1a566e111d218e352e1439bba9fd04e8a1eb9a8e36bd438cb7"
+ },
+ {
+ "name": "sparkles",
+ "unicode": "2728",
+ "digest": "5ab280ea10c30e0e0b5a26ef52b8f47ad44a983330f7ef62ac0c0888752bbdb6"
+ },
+ {
+ "name": "sparkling_heart",
+ "unicode": "1F496",
+ "digest": "f145dab6b597c07e5a851176fabaf56dd857209645483d1acc1490d12c969113"
+ },
+ {
+ "name": "speak_no_evil",
+ "unicode": "1F64A",
+ "digest": "6eae2d066d39c4ba81e58a8327ed875c68bc9b1297c18dc0f5243e477a81040f"
+ },
+ {
+ "name": "speaker",
+ "unicode": "1F508",
+ "digest": "ea59c5a9d994808ff7937c300303e644b5f1ad41097e82f9e73ea6e1c718936c"
+ },
+ {
+ "name": "speaking_head",
+ "unicode": "1F5E3",
+ "digest": "d92cfe1200887300b2f05f9576448a2f2a79d0accd51f323a65ce3db0aa5639b"
+ },
+ {
+ "name": "speech_balloon",
+ "unicode": "1F4AC",
+ "digest": "5dccfda46fc984583bc9eaece66e7e884f2a9eb12a69dbd3493035e3c862edd0"
+ },
+ {
+ "name": "speech_left",
+ "unicode": "1F5E8",
+ "digest": "478b0b07460a9f54b7d0050f886da59fde5e428daa11e899fc31477fda1707ed"
+ },
+ {
+ "name": "speech_right",
+ "unicode": "1F5E9",
+ "digest": "8439b13779163c15e678a78b08ebeeb7d131632df21d2a7868de7fed38ca9d8a"
+ },
+ {
+ "name": "speech_three",
+ "unicode": "1F5EB",
+ "digest": "55a934f3659b6e75fdce0d0c4e2ea56dd34a43892c85a6666bd1882a0bfb92a9"
+ },
+ {
+ "name": "speech_two",
+ "unicode": "1F5EA",
+ "digest": "0563ef0591da243673cf877462acc5d8e1d980a56e81668ac627de74d0c33983"
+ },
+ {
+ "name": "speedboat",
+ "unicode": "1F6A4",
+ "digest": "553a288ab8eeb3dee7b9d1c92eba38016caef7658beaa828136ba1d6ba8ed08a"
+ },
+ {
+ "name": "spider",
+ "unicode": "1F577",
+ "digest": "519f7243b5574102ce3f8953e5480812830a1feb32ae51e8573724c864338481"
+ },
+ {
+ "name": "spider_web",
+ "unicode": "1F578",
+ "digest": "42959fae08a2162d6ee8c8706f823c5932f3801bc90da30d2ca9a48c3ff25572"
+ },
+ {
+ "name": "spy",
+ "unicode": "1F575",
+ "digest": "eaa570a36d83119d0a596228e74affe84d7355714ff6901d88a89410d26dec2a"
+ },
+ {
+ "name": "spy_tone1",
+ "unicode": "1F575-1F3FB",
+ "digest": "abdc066d4cad6a17047faf7806c45feb43ae1e2056cf500536f08f4173dbfa94"
+ },
+ {
+ "name": "spy_tone2",
+ "unicode": "1F575-1F3FC",
+ "digest": "72a3313ef12364105e764cc3deabd47eb6bd086f261c435682ae1cd29dc8230b"
+ },
+ {
+ "name": "spy_tone3",
+ "unicode": "1F575-1F3FD",
+ "digest": "2a1108d3d2e778f88aa5b3ae36705c877b84d0bf6b421409582ba748aeb2aee7"
+ },
+ {
+ "name": "spy_tone4",
+ "unicode": "1F575-1F3FE",
+ "digest": "1d4fe62912384bc0d687bcf4565752caf0ed6146c903a156d1c6ba6ea239b154"
+ },
+ {
+ "name": "spy_tone5",
+ "unicode": "1F575-1F3FF",
+ "digest": "69c1baac73783edb9e2d0c951f922dc7dddac34d0a9c818fee8d1021bc17db0d"
+ },
+ {
+ "name": "stadium",
+ "unicode": "1F3DF",
+ "digest": "4356db5d2cdef8c40830638debaf1f50831130c12ae8d8dc3d9a6bd28fdaa1f7"
+ },
+ {
+ "name": "star",
+ "unicode": "2B50",
+ "digest": "13240b8fada84e7555892996e9f9652503bf9b9a002056c2bae428d543abe2da"
+ },
+ {
+ "name": "star2",
+ "unicode": "1F31F",
+ "digest": "9b56c7548f6a222499d4e848576ea25eab837db72b207ebf8a62a451b35f758f"
+ },
+ {
+ "name": "star_and_crescent",
+ "unicode": "262A",
+ "digest": "10b8a0771e415aa6610fa62185137aa1836c2bb3e82f1a3f601470e94f784923"
+ },
+ {
+ "name": "star_of_david",
+ "unicode": "2721",
+ "digest": "5bc4d1038b8316281e01a9c575ded7ede0fc24c7593db5b5d36ca2e188aa5614"
+ },
+ {
+ "name": "stars",
+ "unicode": "1F320",
+ "digest": "23605eafc949feead3eca145a7ff5ee3b211a8bfd95621bd35dd05df532b97c6"
+ },
+ {
+ "name": "station",
+ "unicode": "1F689",
+ "digest": "c346f12fff64161041af8492550c3541a6304e53f30288224ddd0c6fe08c4d6b"
+ },
+ {
+ "name": "statue_of_liberty",
+ "unicode": "1F5FD",
+ "digest": "56fa27ab059a9fd1f53aec47d9108277a3bf04a73186f36297cd1207c832ee31"
+ },
+ {
+ "name": "steam_locomotive",
+ "unicode": "1F682",
+ "digest": "d0ec2eb3d761ab6157e17eab1b8b4dec3a69f9becc4251592cbb67d71825e661"
+ },
+ {
+ "name": "stereo",
+ "unicode": "1F4FE",
+ "digest": "1ce1f9a83867514b8351ad4fd80c46bba04ad67dfb9874e63d7296e1a21161a5"
+ },
+ {
+ "name": "stew",
+ "unicode": "1F372",
+ "digest": "12e6e4bf48a7296700e07a053d831dd67b70c308ca9522ca96e933a4d1ef6c5e"
+ },
+ {
+ "name": "stock_chart",
+ "unicode": "1F5E0",
+ "digest": "4a0fbf54d19b0b5626f91c932a24e6ac12a65b4fc276d852ff4356c8c579d28a"
+ },
+ {
+ "name": "stop_button",
+ "unicode": "23F9",
+ "digest": "57310962c7738a7da4f2a62cbd5e0b26d7aec357978267a0d8ca8e6cbd7ffb02"
+ },
+ {
+ "name": "stopwatch",
+ "unicode": "23F1",
+ "digest": "c8e69c24f9da98dcb41c9c6355922d08a702f12a35667fbc5beb3f659430333d"
+ },
+ {
+ "name": "straight_ruler",
+ "unicode": "1F4CF",
+ "digest": "55ff7182a3696461df52e3000708083f803bc8bf0f3c25dacb34175cc104b51d"
+ },
+ {
+ "name": "strawberry",
+ "unicode": "1F353",
+ "digest": "fd501e1fefb70242ac7c4dc30ad3d8c3ae200b263a832daedaa984906114afaf"
+ },
+ {
+ "name": "stuck_out_tongue",
+ "unicode": "1F61B",
+ "digest": "1b49956cec511ee382177d95da77c8b6a9214a02c86bf7c6c6fd6cc9df3e9331"
+ },
+ {
+ "name": "stuck_out_tongue_closed_eyes",
+ "unicode": "1F61D",
+ "digest": "60a4d5d92550c6ad4db901d42c9f6434fe94fa3ddb353b6019a93d374d9485e9"
+ },
+ {
+ "name": "stuck_out_tongue_winking_eye",
+ "unicode": "1F61C",
+ "digest": "d9c15ad1c4782a0391a79aeda2745127527385b0b5fc01c8d96c3f3b637a74ae"
+ },
+ {
+ "name": "sun_with_face",
+ "unicode": "1F31E",
+ "digest": "56b14e92f68f8701fdc42763e1f4695ed352845f22bd5d412f827e5cf98dd83b"
+ },
+ {
+ "name": "sunflower",
+ "unicode": "1F33B",
+ "digest": "817dea222a75bb6492c32b4b144d07f48295d7dd113e21760f90b18277612ebb"
+ },
+ {
+ "name": "sunglasses",
+ "unicode": "1F60E",
+ "digest": "16003cc5256397389889f52e0a5e14daea8d8c72f2ea660b8174529868cba9cd"
+ },
+ {
+ "name": "sunny",
+ "unicode": "2600",
+ "digest": "f68a774b7d574fc711111e17368b57c40d973d263c7e857544a09051d4592ab9"
+ },
+ {
+ "name": "sunrise",
+ "unicode": "1F305",
+ "digest": "ce06a9321bc04605538a59f9fca8536d6209d7ded03120e5d2a0be955bb17ddf"
+ },
+ {
+ "name": "sunrise_over_mountains",
+ "unicode": "1F304",
+ "digest": "286244ac2bec8c5c41cf8c7c439702fa525c57fab623f7f9bd7687db0adf75b2"
+ },
+ {
+ "name": "surfer",
+ "unicode": "1F3C4",
+ "digest": "d17c7ea185ca5ef5a2950ef126ee14103bf7769acb419a20d08cc023f619e459"
+ },
+ {
+ "name": "surfer_tone1",
+ "unicode": "1F3C4-1F3FB",
+ "digest": "af66f2f26071b3ba8d7c795139055a58a857212f8cb1f51a507242ad7d2c49c7"
+ },
+ {
+ "name": "surfer_tone2",
+ "unicode": "1F3C4-1F3FC",
+ "digest": "7a34e8b1fdad0a89bbb10333d241583ef018517fdd90f171ad7121de53776a3f"
+ },
+ {
+ "name": "surfer_tone3",
+ "unicode": "1F3C4-1F3FD",
+ "digest": "b2f4cbd59a0aa93c7ee2bbb14ce55c8306dc25884377982a5f132ce6c074fa1d"
+ },
+ {
+ "name": "surfer_tone4",
+ "unicode": "1F3C4-1F3FE",
+ "digest": "b16a02cfcc3606524cca9408e69c654fb83a162eaec8faae8dfd8ec67fe391c5"
+ },
+ {
+ "name": "surfer_tone5",
+ "unicode": "1F3C4-1F3FF",
+ "digest": "b9a156e1aa57544b703db4e4a7773e244a3139e82c2c808c2e5a804fb524f512"
+ },
+ {
+ "name": "sushi",
+ "unicode": "1F363",
+ "digest": "d2709b51ee92997c7fafa1b1517259cb896819c8dc9ba98ae26e1d44ec810d4f"
+ },
+ {
+ "name": "suspension_railway",
+ "unicode": "1F69F",
+ "digest": "48903e103ef00a068b0100b28319b1e41c6a4485cb564f0ca59422ec9d3b259c"
+ },
+ {
+ "name": "sweat",
+ "unicode": "1F613",
+ "digest": "8d684fa882bcbf07f4e91ea02a48cd61f22e7aa206162b8352c26fc19361ed4e"
+ },
+ {
+ "name": "sweat_drops",
+ "unicode": "1F4A6",
+ "digest": "fca48e255dff08dab97ef98b75c67f7504a13be8b90afac88b69a7b7e887e445"
+ },
+ {
+ "name": "sweat_smile",
+ "unicode": "1F605",
+ "digest": "0c8156554eec2396b5fee908da46484945db980d2ebc6dee57b4069a86826182"
+ },
+ {
+ "name": "sweet_potato",
+ "unicode": "1F360",
+ "digest": "3ce74ea9bc14906a3d29a9592c0657aee8f7961d406992752f7580b16ca6bdd0"
+ },
+ {
+ "name": "swimmer",
+ "unicode": "1F3CA",
+ "digest": "05f3aa8544e3b15837bb06ae47344633b3e60d64c572dc6638c4cee19d6e5506"
+ },
+ {
+ "name": "swimmer_tone1",
+ "unicode": "1F3CA-1F3FB",
+ "digest": "85a266a9131f6a1b37e758305ca43ffb46e3e07b0a465c5faefbdb5e5adeb7a4"
+ },
+ {
+ "name": "swimmer_tone2",
+ "unicode": "1F3CA-1F3FC",
+ "digest": "f2afdc4d05a2694e663a420d5ad82bd48c92aedc4137d0fd3725bf08c41bd12a"
+ },
+ {
+ "name": "swimmer_tone3",
+ "unicode": "1F3CA-1F3FD",
+ "digest": "b87ecc38fb9e8eeeef8b120164d758d3f6a68a407053b03261354fd7f90f43b6"
+ },
+ {
+ "name": "swimmer_tone4",
+ "unicode": "1F3CA-1F3FE",
+ "digest": "a08629cf3484953b851b357c6a04891fb97ac15e70c376bbb82af47479835e1c"
+ },
+ {
+ "name": "swimmer_tone5",
+ "unicode": "1F3CA-1F3FF",
+ "digest": "21d83f66b2ef3e348f9e14ec108b9a90262d9934039ebd573471d2bdcde68974"
+ },
+ {
+ "name": "symbols",
+ "unicode": "1F523",
+ "digest": "f33c3ce58374e23b8957c759016fdb5c56ef7fe812bd4e693ae8ff7574cf6bbf"
+ },
+ {
+ "name": "synagogue",
+ "unicode": "1F54D",
+ "digest": "b13402c3c5793ebf924335a87a9f69befb7a6c152fc2a288261b2c2d49842eb6"
+ },
+ {
+ "name": "syringe",
+ "unicode": "1F489",
+ "digest": "39e5e7530255ccf2ff35ec5c653568c8645a4711170c573117f796ea3438c44a"
+ },
+ {
+ "name": "taco",
+ "unicode": "1F32E",
+ "digest": "6b004ce7129e00abcc10278bba1b9c3d5ac71888b99bf353f9878d8e494e3e0d"
+ },
+ {
+ "name": "tada",
+ "unicode": "1F389",
+ "digest": "956a180a1f18e3a1252761e5b3713324f63975ee1fe32168b59b60aa4dd8b72b"
+ },
+ {
+ "name": "tanabata_tree",
+ "unicode": "1F38B",
+ "digest": "d074457ba347687bfc8397ec62edee6325c411356216e7d43acd3f60628a0bb8"
+ },
+ {
+ "name": "tangerine",
+ "unicode": "1F34A",
+ "digest": "1b46bb690458914220cba18c43d7ae0f6914adfee6dba7cf2bb58ed4e1854ad8"
+ },
+ {
+ "name": "taurus",
+ "unicode": "2649",
+ "digest": "ea87fb3baa32605107d63b60847e4873ad9e21b7e7b652e3721cde777168670d"
+ },
+ {
+ "name": "taxi",
+ "unicode": "1F695",
+ "digest": "f44249c643a96d924e1eb35f67a133f3ca61128e610a880afaa09a73c7bcaf9d"
+ },
+ {
+ "name": "tea",
+ "unicode": "1F375",
+ "digest": "56ab8c291de8320c5b339e1cfbe972696e4ea31c592cefa240eda9a3abdf4fa3"
+ },
+ {
+ "name": "telephone",
+ "unicode": "260E",
+ "digest": "609104588e00039199a2fef3190ee6a7be5fca7cb09b36ffe5a7d800aac69d8d"
+ },
+ {
+ "name": "telephone_black",
+ "unicode": "1F57F",
+ "digest": "c3a42a653a91d90c6b668f678419d5438f2e546050914b841623e57107e805db"
+ },
+ {
+ "name": "telephone_receiver",
+ "unicode": "1F4DE",
+ "digest": "e3bf6034de6cf2160893ba4990eba198185a6a3f9cd5767a63b048e41c297640"
+ },
+ {
+ "name": "telephone_white",
+ "unicode": "1F57E",
+ "digest": "62a7e0e50c53e9f85eba51a92882e6064be05997910d3f7700e1e957dbaf0581"
+ },
+ {
+ "name": "telescope",
+ "unicode": "1F52D",
+ "digest": "abe0aca5f2c78105b0e9e4c8ee7a40adcd9bb013e7c49d568076459bade73556"
+ },
+ {
+ "name": "ten",
+ "unicode": "1F51F",
+ "digest": "7593aa7ffe7192a2e35c6ccec76522f6243777783c9152c7c03419835ea58c03"
+ },
+ {
+ "name": "tennis",
+ "unicode": "1F3BE",
+ "digest": "0a5fad3f7f35da0f37761e2279c148dbe154fa14c0e2a0749209b8b2b213a388"
+ },
+ {
+ "name": "tent",
+ "unicode": "26FA",
+ "digest": "7ddf437d8d186e4e3c3e818d137518d590fa06098813c7fe20e1f2a9704feab2"
+ },
+ {
+ "name": "thermometer",
+ "unicode": "1F321",
+ "digest": "597d1714442698a22187fee4d57a2580322f7206c7d51e4519023824598ec08f"
+ },
+ {
+ "name": "thermometer_face",
+ "unicode": "1F912",
+ "digest": "f19c489d89dd2d39770a6c8725a20f3e98f9e5216774af60c0665fd6a03a7687"
+ },
+ {
+ "name": "thinking",
+ "unicode": "1F914",
+ "digest": "f64a9a18dca4c502b46f933838753a818b604a9d0268aa32eda26cbd31abc58c"
+ },
+ {
+ "name": "thought_balloon",
+ "unicode": "1F4AD",
+ "digest": "76c8513191641f0a79e878ccc0d83c4576984609810633f596db2f64cc684b7d"
+ },
+ {
+ "name": "thought_left",
+ "unicode": "1F5EC",
+ "digest": "4fd591bf4318df73d1b17f434a449d8e95f49cca53a3d8f4d1ca983f3809ef46"
+ },
+ {
+ "name": "thought_right",
+ "unicode": "1F5ED",
+ "digest": "0e8c0ce26e2d0e30894f5394b0736456e8268f775e0e7eda4c7dc3c2ff9231ae"
+ },
+ {
+ "name": "three",
+ "unicode": "0033-20E3",
+ "digest": "ca0147a8f67cea3bc2516fa8deef4325188359559786c94ff0b27f90eef04b88"
+ },
+ {
+ "name": "thumbs_down_reverse",
+ "unicode": "1F593",
+ "digest": "a8b561e389bc4e4b07fba70994f6445e5ddc6afe68922fcb6e9e7282d19ad958"
+ },
+ {
+ "name": "thumbs_up_reverse",
+ "unicode": "1F592",
+ "digest": "b6e52715c5ce590bfd08f6e05058ec3765ea2da341b11f9825d100608b173837"
+ },
+ {
+ "name": "thumbsdown",
+ "unicode": "1F44E",
+ "digest": "a98f742c9773e0d95c0de5e1c10d1ab373fa761378a205f27d095e85debe69a3"
+ },
+ {
+ "name": "thumbsdown_tone1",
+ "unicode": "1F44E-1F3FB",
+ "digest": "5d0a7c63d52eafe6267c552168c5557a66622009d565c3cf7b5378c1f6e84bce"
+ },
+ {
+ "name": "thumbsdown_tone2",
+ "unicode": "1F44E-1F3FC",
+ "digest": "ca5c15dc516660b2989a1c717bf3745fdfb6964c7acf3b938285ff6c7caf2ca2"
+ },
+ {
+ "name": "thumbsdown_tone3",
+ "unicode": "1F44E-1F3FD",
+ "digest": "05740e3568795270674dac9134198bf75b1b778c11daa71649c88c231859ec16"
+ },
+ {
+ "name": "thumbsdown_tone4",
+ "unicode": "1F44E-1F3FE",
+ "digest": "5ee93bcc2f515806462a7b303064beade2b22a3f43a8162e39fd65d15d772e27"
+ },
+ {
+ "name": "thumbsdown_tone5",
+ "unicode": "1F44E-1F3FF",
+ "digest": "5c9ef8d53cf6f755668ab6dabfbfcdfd4b95fd59db3b3dd60290efefe9c33994"
+ },
+ {
+ "name": "thumbsup",
+ "unicode": "1F44D",
+ "digest": "28b31df963773ba42a1a089f43cd89d0ce1ab0981e5410f41242e9a125fc1aee"
+ },
+ {
+ "name": "thumbsup_tone1",
+ "unicode": "1F44D-1F3FB",
+ "digest": "f6365942738d2128b6959d6672b3d295757dc8240703cb84a2b014ad78d67de3"
+ },
+ {
+ "name": "thumbsup_tone2",
+ "unicode": "1F44D-1F3FC",
+ "digest": "771d30146e4dc947a69057b05d32c765c8457ab02b5342889c5489acf27ef356"
+ },
+ {
+ "name": "thumbsup_tone3",
+ "unicode": "1F44D-1F3FD",
+ "digest": "0bb7bbfb654c6139260e1786e7ffa5a33f31e19410c1d4d15737fdf5dd4c721d"
+ },
+ {
+ "name": "thumbsup_tone4",
+ "unicode": "1F44D-1F3FE",
+ "digest": "df0927c5342f0075fbf4ea83b724e6f70c0466c54769c9ce4a5c2deb602b28aa"
+ },
+ {
+ "name": "thumbsup_tone5",
+ "unicode": "1F44D-1F3FF",
+ "digest": "0683ae08c50aaf186c6406680a60617679c7b4bccd0817f24b15911dbb06866f"
+ },
+ {
+ "name": "thunder_cloud_rain",
+ "unicode": "26C8",
+ "digest": "dd836f06b41a10d6ed9bcbdae291d2886847ff66dc3ede2427382e469f60674c"
+ },
+ {
+ "name": "ticket",
+ "unicode": "1F3AB",
+ "digest": "a7654a5529535120da3c377e72cd1f7997bdc2dabf1d44b584f7df7852b158f9"
+ },
+ {
+ "name": "tickets",
+ "unicode": "1F39F",
+ "digest": "ccafcc9583a84e847ff1eaa3d53187c5ab150a7d27c6a19363e59b9bc046b567"
+ },
+ {
+ "name": "tiger",
+ "unicode": "1F42F",
+ "digest": "9ebe3117f5f1b589ff8164f8d87dcc275923e0db87121d2cee0fdb9b56dfc4ac"
+ },
+ {
+ "name": "tiger2",
+ "unicode": "1F405",
+ "digest": "212c95dc60d52420a6320917fe3fdd0683b4edc1a2a2c4a1c60920d1f90f4bc3"
+ },
+ {
+ "name": "timer",
+ "unicode": "23F2",
+ "digest": "c48199312ed42ff53a33bb2791db19e2e2521223cd49d8f758ea95b9b379c5ff"
+ },
+ {
+ "name": "tired_face",
+ "unicode": "1F62B",
+ "digest": "ad687a956388ec53ca1e301a0abe2f1e2cfb9f73cd543dd61a21c7335a42e332"
+ },
+ {
+ "name": "tm",
+ "unicode": "2122",
+ "digest": "1156c8b0af40b336bbb6534b3302ac63eab009c4cd0476adcf1fc4669f04b647"
+ },
+ {
+ "name": "toilet",
+ "unicode": "1F6BD",
+ "digest": "a4a24529c21e00e0861f4160c771f0e90aae8f6aee7550ad30d3dbb3fabbd4be"
+ },
+ {
+ "name": "tokyo_tower",
+ "unicode": "1F5FC",
+ "digest": "6324f154f5f5c722044129e5bca03484aca1439911585e42c1c181ffa30b480c"
+ },
+ {
+ "name": "tomato",
+ "unicode": "1F345",
+ "digest": "41bb6de095b27815eacb74a70aea8f7d4fe1ff947182b112001dd47ae7e45fbb"
+ },
+ {
+ "name": "tone1",
+ "unicode": "1F3FB",
+ "digest": "5c62003a098b774c068be45d658db3c0dd38483c0871f7c8ae293bc1222c4f0c"
+ },
+ {
+ "name": "tone2",
+ "unicode": "1F3FC",
+ "digest": "3c636ecbc4e58c7a360f2338daaf44e7da598fd07e0ba1514bb5c0f83fc8819f"
+ },
+ {
+ "name": "tone3",
+ "unicode": "1F3FD",
+ "digest": "398a1e5441b64c9c2d033bbc01d7a8d90b4db30ea9f30e28f0a9120c72a48df8"
+ },
+ {
+ "name": "tone4",
+ "unicode": "1F3FE",
+ "digest": "ff4a12195aeb7494c785b81266efad8cd60c8022c407a0fc032a02e8b83216b3"
+ },
+ {
+ "name": "tone5",
+ "unicode": "1F3FF",
+ "digest": "9e9f0125b5d57011b7456c84719e6be6cf71d06c1b198081d0937c0979164a81"
+ },
+ {
+ "name": "tongue",
+ "unicode": "1F445",
+ "digest": "bf9dd7c65a8dc5d77eb013658a0a12a13f7b224a784e65e203d9584bb6b41427"
+ },
+ {
+ "name": "tools",
+ "unicode": "1F6E0",
+ "digest": "9b0a36dfdb475621d326359662b22cbdb80563c4f476aa5e7d7c00cdba605bd9"
+ },
+ {
+ "name": "top",
+ "unicode": "1F51D",
+ "digest": "d645030099aeb433307569e8e1c4342c1c411a8fefe50fdca7a3207a1a0db671"
+ },
+ {
+ "name": "tophat",
+ "unicode": "1F3A9",
+ "digest": "1082fb2ee2e98fe65d21081b74ca59b07adef85043e2d36f25cac69db2d31fd3"
+ },
+ {
+ "name": "track_next",
+ "unicode": "23ED",
+ "digest": "d5415ed140933f345fea8023a3d8fca30dcfcf7d19d9dc9771fa2cae9df62a3b"
+ },
+ {
+ "name": "track_previous",
+ "unicode": "23EE",
+ "digest": "97ff4a59a236e5cf506fa3577b20715b3b0197e0f343a50615b36185d5b835f1"
+ },
+ {
+ "name": "trackball",
+ "unicode": "1F5B2",
+ "digest": "8332503454ce42059d720c285fe2b15eb0562a0a4b234dccb0f3159bb30a91aa"
+ },
+ {
+ "name": "tractor",
+ "unicode": "1F69C",
+ "digest": "a41d304c41a85d966f6a7c301735fdbe2ae41f4471dd7dcd72023046ca2546d0"
+ },
+ {
+ "name": "traffic_light",
+ "unicode": "1F6A5",
+ "digest": "005f68d028fec8d9ae389cc2b23e1343a82c028eb32820d5e56f5c84eba315d1"
+ },
+ {
+ "name": "train",
+ "unicode": "1F68B",
+ "digest": "bf32893b7b9ecd248e8afe840624061746ac6ceb741e3e861ebfa46014f4bed4"
+ },
+ {
+ "name": "train2",
+ "unicode": "1F686",
+ "digest": "08a9732453a0b4f68dd2d3d3879f04ee538f65897913b5a5157c0585132a374a"
+ },
+ {
+ "name": "train_diesel",
+ "unicode": "1F6F2",
+ "digest": "621bb967cd93fa9f8fd4b155965cc7572d3f91f88d94938ba10c8626718b623c"
+ },
+ {
+ "name": "tram",
+ "unicode": "1F68A",
+ "digest": "5a86d31f7ab677d967fecd75babc900b5169766d0228961912314c4c4d1d64ee"
+ },
+ {
+ "name": "triangle_round",
+ "unicode": "1F6C6",
+ "digest": "e24bb39ecfaaa746b03dc8418697d09ef327d5b077db39014f39d5fb87e23bd5"
+ },
+ {
+ "name": "triangular_flag_on_post",
+ "unicode": "1F6A9",
+ "digest": "d824c973d84cd62c845d64e546de87b094fda8f9972b6a33acd75e1a5ac19f75"
+ },
+ {
+ "name": "triangular_ruler",
+ "unicode": "1F4D0",
+ "digest": "5576802d8bcb8836f473d9c7641ff666250c23c8476c676b253e577695025959"
+ },
+ {
+ "name": "trident",
+ "unicode": "1F531",
+ "digest": "70c1e8254da5b0e4552673b487503a20feeb249484d4596836b75de70220be82"
+ },
+ {
+ "name": "triumph",
+ "unicode": "1F624",
+ "digest": "b09262121b0d3d9d017ded22d0fbb1acaa6ee8c9d38e9ac34292b390d97408fe"
+ },
+ {
+ "name": "trolleybus",
+ "unicode": "1F68E",
+ "digest": "5af943836cc30c3b79160c70b6488c984fa63c104dce08c436597a93d30ff6f4"
+ },
+ {
+ "name": "trophy",
+ "unicode": "1F3C6",
+ "digest": "c249938815042716db2b39cdece6715fabf9e56ed583270c451925e6c91f9191"
+ },
+ {
+ "name": "tropical_drink",
+ "unicode": "1F379",
+ "digest": "352d903e813a27d2a74803322539b50a50aec0ca2ed7ab4a92ec480b1c226cb6"
+ },
+ {
+ "name": "tropical_fish",
+ "unicode": "1F420",
+ "digest": "13a104ca9c326238ab8d85b60759629b4efaa836946fbe58d78d779443475f7b"
+ },
+ {
+ "name": "truck",
+ "unicode": "1F69A",
+ "digest": "13d381d6b43b42350a1e24c02296904b8fdc38c1bf0939fc7037850127e91f21"
+ },
+ {
+ "name": "trumpet",
+ "unicode": "1F3BA",
+ "digest": "df7fb48920ac0919ee2d7b30102016479f747a5d4dd25b3e18d9f17121d232d1"
+ },
+ {
+ "name": "tulip",
+ "unicode": "1F337",
+ "digest": "519a84336464b5dc8db57eecef3e5b8ed82ccfdaa0ed0fa9ef7bcf0e8acea1f8"
+ },
+ {
+ "name": "turkey",
+ "unicode": "1F983",
+ "digest": "e87bff52ad3e301dc62f6832b8a6fcaf99db260a96263e4203a55ce3abda8cf8"
+ },
+ {
+ "name": "turned_ok_hand",
+ "unicode": "1F58F",
+ "digest": "8a6c5b7d4c737866e7e32c6d9f7f447a48a0ac57a8909d43f87367d4a9b59246"
+ },
+ {
+ "name": "turtle",
+ "unicode": "1F422",
+ "digest": "388b3e75b931638a09f65b842d26e2cc87b200ba782dec871f84cddd71aaeaf3"
+ },
+ {
+ "name": "tv",
+ "unicode": "1F4FA",
+ "digest": "dba03be6482d6291599c7393b0f749c0de5c873d45c96a20ccc53b3e104a6a24"
+ },
+ {
+ "name": "twisted_rightwards_arrows",
+ "unicode": "1F500",
+ "digest": "5fcad0247576e10e683f353008749975e9371a4f66c0901a73c3a0c7803c63c7"
+ },
+ {
+ "name": "two",
+ "unicode": "0032-20E3",
+ "digest": "20ad722532a5073fff8aef0a5e890421da0ae97f0723a8a2cc503c13d24ba597"
+ },
+ {
+ "name": "two_hearts",
+ "unicode": "1F495",
+ "digest": "160cb11e3ed2ae1b20957d445c6c4b4bd604d067294818dfeeefba4562425eb9"
+ },
+ {
+ "name": "two_men_holding_hands",
+ "unicode": "1F46C",
+ "digest": "923734704e544f7484fdb424bfe26f51ee07754db712cd151f8fbe955023a1ab"
+ },
+ {
+ "name": "two_women_holding_hands",
+ "unicode": "1F46D",
+ "digest": "58a40e7819cab3589ac81bb4fdc485b7196ee355544b54c6b00169028c260130"
+ },
+ {
+ "name": "u5272",
+ "unicode": "1F239",
+ "digest": "b7e8ad52629a1f1fca77a5c9a51da87ce2b9a81f6af9bcbe9bec9552d398e9bf"
+ },
+ {
+ "name": "u5408",
+ "unicode": "1F234",
+ "digest": "f359799d206cff6aae3af26eb8ad153abd38e817d4c70b2e5e5e8cf2f46e645e"
+ },
+ {
+ "name": "u55b6",
+ "unicode": "1F23A",
+ "digest": "c40293bea0f148e76ca5152e830b1b474380fe259180fbf74fece1ccc9afd8a3"
+ },
+ {
+ "name": "u6307",
+ "unicode": "1F22F",
+ "digest": "45449f7ae29da9e507c19d0f2b22f17f7cbd763f2ec87eb893be5bae49c7f78e"
+ },
+ {
+ "name": "u6708",
+ "unicode": "1F237",
+ "digest": "b897ead8c952013975ce6f381cdb8c584ebe4015311ef87f2a332c8a9e155d75"
+ },
+ {
+ "name": "u6709",
+ "unicode": "1F236",
+ "digest": "8b2f792abc1313a1a58f2fb8b37ad68a964004c962535f7739131257b1331a05"
+ },
+ {
+ "name": "u6e80",
+ "unicode": "1F235",
+ "digest": "fd982a56d4c492e63526b427bb948d7f155b0d5c414a68c7177698a71e72269b"
+ },
+ {
+ "name": "u7121",
+ "unicode": "1F21A",
+ "digest": "334f87a5254b58503d9f7a8ecc3d971a99839ec9c22c443469d72caca1750a48"
+ },
+ {
+ "name": "u7533",
+ "unicode": "1F238",
+ "digest": "3c8e743ae9960e43b9fa0cc698018fcb2a52ae34d143f0561298191f9def019c"
+ },
+ {
+ "name": "u7981",
+ "unicode": "1F232",
+ "digest": "a08bf39be3a54c076de79478c09b79c5c4d221853722870dd6e81abb78a4b64a"
+ },
+ {
+ "name": "u7a7a",
+ "unicode": "1F233",
+ "digest": "5dfb74a534a6490df989f84eac271c79d52f29313b6d43662dd0ff029794367c"
+ },
+ {
+ "name": "umbrella",
+ "unicode": "2614",
+ "digest": "ff1191f6c11b82f5337f78aadb58af50c69abaf676a384b0473bf49004e4018f"
+ },
+ {
+ "name": "umbrella2",
+ "unicode": "2602",
+ "digest": "aa7db9d6ed42dff847a8e5ee48a8eeff7a6e7f30de155a28951407f5aaa3dae2"
+ },
+ {
+ "name": "unamused",
+ "unicode": "1F612",
+ "digest": "efbbcaee6f3178afe509d74d13243ec6befe3112620a01e5079171eac4b32417"
+ },
+ {
+ "name": "underage",
+ "unicode": "1F51E",
+ "digest": "ae9a300fa400a57b7216a0a040fb8a5f02236fbceeeceed58bfd953c87ad51fe"
+ },
+ {
+ "name": "unicorn",
+ "unicode": "1F984",
+ "digest": "1b1e9c209dabe619db76fd346c3fb51b28ace0e4102697fe0973fe2d46aa9f08"
+ },
+ {
+ "name": "unlock",
+ "unicode": "1F513",
+ "digest": "63dbef0855399254ae01cf4ef0676adebc1432ae1ee260b569c23ae8152deaf8"
+ },
+ {
+ "name": "up",
+ "unicode": "1F199",
+ "digest": "902a3ecbcd73099a28476b49bc9e7b06da6cc002ee584e0501e5b625fb515088"
+ },
+ {
+ "name": "upside_down",
+ "unicode": "1F643",
+ "digest": "763fe2baf07a9b04f96958adf38a43c7dd2bc70d57398f49604307bd835cbb53"
+ },
+ {
+ "name": "urn",
+ "unicode": "26B1",
+ "digest": "dbfd5b90709d1b812d2fff71a5cfa10f84a4579866c2d7cd0e80759a22b2ba0e"
+ },
+ {
+ "name": "v",
+ "unicode": "270C",
+ "digest": "df85ad1a3ff365c3232a010701c9b25cd824d19fa2511422dee60ac231f457e3"
+ },
+ {
+ "name": "v_tone1",
+ "unicode": "270C-1F3FB",
+ "digest": "ce45db8de862b6f37d9208920d7c7c19335fac2cbff59b52be1ccbc01e3249da"
+ },
+ {
+ "name": "v_tone2",
+ "unicode": "270C-1F3FC",
+ "digest": "9036c8d793b02b4d2e6a4752b8ec319ec50efd6fcd6feef7b0671a63e5659acc"
+ },
+ {
+ "name": "v_tone3",
+ "unicode": "270C-1F3FD",
+ "digest": "a94b95f7656d62b442c99f2643b96b0c6114683401a94cdda68405c37efecc4c"
+ },
+ {
+ "name": "v_tone4",
+ "unicode": "270C-1F3FE",
+ "digest": "5c75f74993856f2faeeaee68df7689056e60d30e8c573039db8303167f7d0a80"
+ },
+ {
+ "name": "v_tone5",
+ "unicode": "270C-1F3FF",
+ "digest": "bb899672adb3c11f65983fbf9581de7f0a1bbac86fde146e799cea1126fe241e"
+ },
+ {
+ "name": "vertical_traffic_light",
+ "unicode": "1F6A6",
+ "digest": "36296e03620f16d35e5cec195cd97f5b358dfdedcd43bc1b3f7988ff7e85ab47"
+ },
+ {
+ "name": "vhs",
+ "unicode": "1F4FC",
+ "digest": "f4be55f4c23a85e0caacbf569742c117c8fd52c189465a6560cbd2f8873ad74f"
+ },
+ {
+ "name": "vibration_mode",
+ "unicode": "1F4F3",
+ "digest": "b9b8dfa3160c22f78b7d627cb52636d81ca6230a196cee5e94028e32e06b9a98"
+ },
+ {
+ "name": "video_camera",
+ "unicode": "1F4F9",
+ "digest": "3bfaa24e5fb00145e3e4dd07ecf569dabbb3f211551e46085ef23cf23002cfc3"
+ },
+ {
+ "name": "video_game",
+ "unicode": "1F3AE",
+ "digest": "4dcbd76030e37d0f7429852991a5f3f126cbdedfc124ecad0ba29d227375f6e2"
+ },
+ {
+ "name": "violin",
+ "unicode": "1F3BB",
+ "digest": "8ab7adc6e1e934f9e05009cd0a6d4da3136092c8f11c0606b91914be182206f5"
+ },
+ {
+ "name": "virgo",
+ "unicode": "264D",
+ "digest": "aaa19752756d0cac949445de1d2b8bf1f75a071368ae0acf5002f4acdc34826f"
+ },
+ {
+ "name": "volcano",
+ "unicode": "1F30B",
+ "digest": "86c17d61d66bfa868c02f1d31daca22f077c096368ef53cd9bfb9914a2f0b273"
+ },
+ {
+ "name": "volleyball",
+ "unicode": "1F3D0",
+ "digest": "b505684b13f814fbc08dc8ff652849328f46068276e0a24ae1961e2aff15868f"
+ },
+ {
+ "name": "vs",
+ "unicode": "1F19A",
+ "digest": "e31bd8b48b88c21d717964d1360a7751684dd1e0b63fdd655f1a9ec10a952dfb"
+ },
+ {
+ "name": "vulcan",
+ "unicode": "1F596",
+ "digest": "ca800fce797e652c5f47bf44992e8fbe19554688a36423fdf7c29ca6defae1e0"
+ },
+ {
+ "name": "vulcan_tone1",
+ "unicode": "1F596-1F3FB",
+ "digest": "84bafdaca43426b053f5caa4e868ca109d99113a28ea9799db09d3c5d5f645c8"
+ },
+ {
+ "name": "vulcan_tone2",
+ "unicode": "1F596-1F3FC",
+ "digest": "e7cedf63ead957ee5c287e4cb0828ba70673e17b604f92b529875c32d094e7e3"
+ },
+ {
+ "name": "vulcan_tone3",
+ "unicode": "1F596-1F3FD",
+ "digest": "e124fef20f289921553274cf834f6dcc1a012889d30d9874dc5ad01afb8235b8"
+ },
+ {
+ "name": "vulcan_tone4",
+ "unicode": "1F596-1F3FE",
+ "digest": "ea2115f549e4680467521bbf362b229f4a8f0fdadbfaf231378d801f9b369f08"
+ },
+ {
+ "name": "vulcan_tone5",
+ "unicode": "1F596-1F3FF",
+ "digest": "1b322e1252491f35ae02f0b279b6529dad867f2a6b3c2c3e77f981bed07e447d"
+ },
+ {
+ "name": "walking",
+ "unicode": "1F6B6",
+ "digest": "8ec0b2207d4368422261bc58944c17dff2554b2356becfb18f21dd87425cd67b"
+ },
+ {
+ "name": "walking_tone1",
+ "unicode": "1F6B6-1F3FB",
+ "digest": "9ee2224226326833fb0c9598c737fbd2f6bca1c81f082537e9f22ea1de4ff48e"
+ },
+ {
+ "name": "walking_tone2",
+ "unicode": "1F6B6-1F3FC",
+ "digest": "4855d521e937d10d58eeb2bbada493699e31e1098128f81a9e3303bcf3edeb49"
+ },
+ {
+ "name": "walking_tone3",
+ "unicode": "1F6B6-1F3FD",
+ "digest": "82669cf7167054a3615add01059f87dbb809edac3889ee171d5994de90448000"
+ },
+ {
+ "name": "walking_tone4",
+ "unicode": "1F6B6-1F3FE",
+ "digest": "c11f03aa96248272f831f68b93c5b21b2ecbffeb1b4c1c13373bf539ee7db8f8"
+ },
+ {
+ "name": "walking_tone5",
+ "unicode": "1F6B6-1F3FF",
+ "digest": "18238ee121a64211f6bcdbd475cee4ad6debe2bf421daba53d125aa005c26d10"
+ },
+ {
+ "name": "waning_crescent_moon",
+ "unicode": "1F318",
+ "digest": "96ef03ff85247877255a5ca3e8a8bb63f7d41f66531e8db61cbcd863e3ad7355"
+ },
+ {
+ "name": "waning_gibbous_moon",
+ "unicode": "1F316",
+ "digest": "994223113ad151e6b42ee317a10dad18f86759a308e61ab88eeb10ab780aae67"
+ },
+ {
+ "name": "warning",
+ "unicode": "26A0",
+ "digest": "a702e51efd1a3ab425eada008ccf694f38a71db14bb710edacc2e206d61f5ca3"
+ },
+ {
+ "name": "wastebasket",
+ "unicode": "1F5D1",
+ "digest": "afecb31aaf5078298ab9f7c5da29a49ce0cdefe477ee50889be9c0e43ccf1799"
+ },
+ {
+ "name": "watch",
+ "unicode": "231A",
+ "digest": "410334c87b8552f601f4ea1b7e36582a8b22f11b804d5ab1008d4af2b5a0cbe6"
+ },
+ {
+ "name": "water_buffalo",
+ "unicode": "1F403",
+ "digest": "d1becfaea464372c46e5442c6030ea355806ce5864c2435c123a9bb3a2c3c5eb"
+ },
+ {
+ "name": "watermelon",
+ "unicode": "1F349",
+ "digest": "88dd78812520c44080c79fe8cb1825bc713e5155da2ce8c73286333749e7035e"
+ },
+ {
+ "name": "wave",
+ "unicode": "1F44B",
+ "digest": "5103c49914ff1a2d76a1ab6db2530ddd9f48b98b708ab15292ceadf28873c939"
+ },
+ {
+ "name": "wave_tone1",
+ "unicode": "1F44B-1F3FB",
+ "digest": "ef2d79f377d09dedd1e900b2f4e4a2412bf562cd88484f71c52d465053f8aae9"
+ },
+ {
+ "name": "wave_tone2",
+ "unicode": "1F44B-1F3FC",
+ "digest": "d323e6e2e9ce035bc11b98226d46ab393dfdf3909d99e7a828b51950e6574656"
+ },
+ {
+ "name": "wave_tone3",
+ "unicode": "1F44B-1F3FD",
+ "digest": "8a8a386d53252455c20d6b235c462fd9cb3b20c9c19c67e67b3dece4621b5cf6"
+ },
+ {
+ "name": "wave_tone4",
+ "unicode": "1F44B-1F3FE",
+ "digest": "a8281c2ab9cf6e2b3d3cad24707fe412ec2398195530b716a2617477416c0432"
+ },
+ {
+ "name": "wave_tone5",
+ "unicode": "1F44B-1F3FF",
+ "digest": "5ccbee95bfc180580c8a02b88146110c4d132b8ea618dd6a58f03c1db921d58d"
+ },
+ {
+ "name": "wavy_dash",
+ "unicode": "3030",
+ "digest": "b5b67fc12938801a98ff22b6f7b566c603f58c183737fa740a500724879f0e99"
+ },
+ {
+ "name": "waxing_crescent_moon",
+ "unicode": "1F312",
+ "digest": "20446122d170b18f88ea71524f6747d42b97f9d765c52e676e5163fee58ec379"
+ },
+ {
+ "name": "waxing_gibbous_moon",
+ "unicode": "1F314",
+ "digest": "4324e43d4d45e6333f7379c9feb8efd3093d76f3920d7dc5ad3c615e76104998"
+ },
+ {
+ "name": "wc",
+ "unicode": "1F6BE",
+ "digest": "cb7c5d35bf11149d12cda2c0897cb6038e043127055bbe2e8e33c9b422d6d8fc"
+ },
+ {
+ "name": "weary",
+ "unicode": "1F629",
+ "digest": "29a291033a1b67eda3710dffae42d63fcfa663e37dab728c236172f3e877fe8f"
+ },
+ {
+ "name": "wedding",
+ "unicode": "1F492",
+ "digest": "6c7d874f464c9c76b0d767135aa40ced94089b5f71d373098b47488d7f3ef7c4"
+ },
+ {
+ "name": "whale",
+ "unicode": "1F433",
+ "digest": "94168acda6ba502b64ea50ff4aaafb7e6258d7c6806e91f090c8a3c46edc5b6d"
+ },
+ {
+ "name": "whale2",
+ "unicode": "1F40B",
+ "digest": "e1cde2308bd510b2449c96e88ffec796856f98b19ceedc1cd7e9ea009dae1417"
+ },
+ {
+ "name": "wheel_of_dharma",
+ "unicode": "2638",
+ "digest": "bbd6927697c22a1c3e56fd0c9933d9e00dbf120505fe48d02cb486bcd67a8b2c"
+ },
+ {
+ "name": "wheelchair",
+ "unicode": "267F",
+ "digest": "513f759acf528f6a7e39d9de1d171c3faebe645c9cf3bd86b185123016beef95"
+ },
+ {
+ "name": "white_check_mark",
+ "unicode": "2705",
+ "digest": "a0b3bf7c4fb131e7a9fab5169ea4094e2665e02cedaa091f0d6e78609b2f17ed"
+ },
+ {
+ "name": "white_circle",
+ "unicode": "26AA",
+ "digest": "2e7323fa4d1e3929e529d49210a0b82a043eae4f7c95128ec86b98c46fdb0e7c"
+ },
+ {
+ "name": "white_flower",
+ "unicode": "1F4AE",
+ "digest": "a3efea4950e09994f5e9d3d16f0728969238302304a6cce90b293c56e9a3e20c"
+ },
+ {
+ "name": "white_large_square",
+ "unicode": "2B1C",
+ "digest": "99c4442a65f2e3c568f45aed9e74590206c517a716557f4d741d967c9f42ed40"
+ },
+ {
+ "name": "white_medium_small_square",
+ "unicode": "25FD",
+ "digest": "a1edfeb4e540dcc020ba5dde19f7a18d90966788baa5382a22a0f9038d593f01"
+ },
+ {
+ "name": "white_medium_square",
+ "unicode": "25FB",
+ "digest": "794c2339ca71bb6d65ac488fb7b5dc4f0a2412f30890d2c4ece53cdbf52ba78b"
+ },
+ {
+ "name": "white_small_square",
+ "unicode": "25AB",
+ "digest": "9c4c308070a0c4524993cc36feaa778aad8f0df9f209b82d28b1f3811c441bc4"
+ },
+ {
+ "name": "white_square_button",
+ "unicode": "1F533",
+ "digest": "f46e18c7250c874d1b4d6117eda741d86a081352e76f3d019dd64af2669fa4bb"
+ },
+ {
+ "name": "white_sun_cloud",
+ "unicode": "1F325",
+ "digest": "d8ce416e6bdb0e59e06e2fceac3177dbe59fefc248fd8c6d76b80d1418141070"
+ },
+ {
+ "name": "white_sun_rain_cloud",
+ "unicode": "1F326",
+ "digest": "d2b132518261864ac4a95707eaeea335dd8351ed2b8ef4e2272ced456e309bf1"
+ },
+ {
+ "name": "white_sun_small_cloud",
+ "unicode": "1F324",
+ "digest": "b86a72f1cdb4d24fd3ab180aae9db012ca51fc01f3786aab596c2e330066b185"
+ },
+ {
+ "name": "wind_blowing_face",
+ "unicode": "1F32C",
+ "digest": "20bdeb8e39dc637792ac9fbee031c5791889f3126e83556ba51f98809c19763c"
+ },
+ {
+ "name": "wind_chime",
+ "unicode": "1F390",
+ "digest": "1fc26f33ce13b6a969bb76e914de054ec5d1c7c4cd1dc5ee8fea5f3149f794d8"
+ },
+ {
+ "name": "wine_glass",
+ "unicode": "1F377",
+ "digest": "7dfcf9c5195a20fd2745b19e102910392b0fc8f1650b98ab81957807841935e0"
+ },
+ {
+ "name": "wink",
+ "unicode": "1F609",
+ "digest": "404ac6c920414ca35894da1d97b3b2fabe92bd09569274eb5798fbb297129036"
+ },
+ {
+ "name": "wolf",
+ "unicode": "1F43A",
+ "digest": "ebadd7766c4a314b4027c32435a2f5727a6283123dfb8834e10251cbfc07ca2f"
+ },
+ {
+ "name": "woman",
+ "unicode": "1F469",
+ "digest": "9f0dbb5d1e0db4f008141582dcb6413f5aebaa13e191349c976a435b2bee0956"
+ },
+ {
+ "name": "woman_tone1",
+ "unicode": "1F469-1F3FB",
+ "digest": "c1f2a503481fdd96cfbfa7d556500f8e0da0cea1c72ed1078ecbb6962221c22a"
+ },
+ {
+ "name": "woman_tone2",
+ "unicode": "1F469-1F3FC",
+ "digest": "bf78b3a8f7424037069f8ac337e154ef185f55026c71a6cf6dbe15eb42ef9813"
+ },
+ {
+ "name": "woman_tone3",
+ "unicode": "1F469-1F3FD",
+ "digest": "4ccd70a2052b932b3395ac0a957c05815327dc8082fd461abcd797411db8ce05"
+ },
+ {
+ "name": "woman_tone4",
+ "unicode": "1F469-1F3FE",
+ "digest": "71b5efc4a410102e60048ca05f87587384a6db309f3be94109a4f92ea97072dc"
+ },
+ {
+ "name": "woman_tone5",
+ "unicode": "1F469-1F3FF",
+ "digest": "91a1cd015731f4db501c276a8236eb0665e4dc7aa1891e2a67b8d3e543fbea9c"
+ },
+ {
+ "name": "womans_clothes",
+ "unicode": "1F45A",
+ "digest": "599332c0b863a40fd0c319e4e0f52ae847326a96d180c288e0466b3ac308a27e"
+ },
+ {
+ "name": "womans_hat",
+ "unicode": "1F452",
+ "digest": "231ff55c3fa56d8fb5731fe41f547e67ffacfdde82286f45d4ca65a2d2821239"
+ },
+ {
+ "name": "womens",
+ "unicode": "1F6BA",
+ "digest": "f971429456b543804412490af2e27e0b14d0d536a156db898bce67b136e1b563"
+ },
+ {
+ "name": "worried",
+ "unicode": "1F61F",
+ "digest": "e017f636e79b9301f3a06471a5f3513ba7dbb9b97938de1140c1df4c32fd8844"
+ },
+ {
+ "name": "wrench",
+ "unicode": "1F527",
+ "digest": "c9ded4f7f496bad8691677226310bbd31bb485722ea479bc7a68a2b4ef9d55d9"
+ },
+ {
+ "name": "writing_hand",
+ "unicode": "1F58E",
+ "digest": "c4fc18ece6778339ebe14438aaf570e22385c3010c2d341824fa72ac6068cfeb"
+ },
+ {
+ "name": "writing_hand_tone1",
+ "unicode": "270D-1F3FB",
+ "digest": "38e64e6dca4847a12aef8a117c113b2025d841501c4bc8188c57d0c8a4f1e34d"
+ },
+ {
+ "name": "writing_hand_tone2",
+ "unicode": "270D-1F3FC",
+ "digest": "2b2d0ac2701ae707c31d9c85feb2e3700e11398701e2b0519338897817d53baf"
+ },
+ {
+ "name": "writing_hand_tone3",
+ "unicode": "270D-1F3FD",
+ "digest": "85d67f90ff8bd2e7157f28fd857e6730b660a7eb82eb5350f57671f728ce725b"
+ },
+ {
+ "name": "writing_hand_tone4",
+ "unicode": "270D-1F3FE",
+ "digest": "056c05c201b3d0972433f00910967ad7334e37726e2956fee053ec2e1a9153c7"
+ },
+ {
+ "name": "writing_hand_tone5",
+ "unicode": "270D-1F3FF",
+ "digest": "95c59157d301ee08990e4302fd9bdd7953e1d1abed09636d0837d84e44f53ba6"
+ },
+ {
+ "name": "x",
+ "unicode": "274C",
+ "digest": "1d256b0015b9cbdeaa4558f9241782c89d86c79a42e507621f7949c56a90b6c0"
+ },
+ {
+ "name": "yellow_heart",
+ "unicode": "1F49B",
+ "digest": "e869a80266b4379a8d82988fef25e187632bfb076ae619f576e416906cd688a7"
+ },
+ {
+ "name": "yen",
+ "unicode": "1F4B4",
+ "digest": "8f3d801c687e585e4497123c5c91a8b0c558578deec6a8c1591b25e64a3a8992"
+ },
+ {
+ "name": "yin_yang",
+ "unicode": "262F",
+ "digest": "e8ea4c686518ad6165e15ed67b529f2f1e20d648aa2ecb7e9bff5a6067dd3fea"
+ },
+ {
+ "name": "yum",
+ "unicode": "1F60B",
+ "digest": "d9c97bbf6bdb6e39977437680f0b37c9335306c51e01114056ae1d4c9c85b0e0"
+ },
+ {
+ "name": "zap",
+ "unicode": "26A1",
+ "digest": "37588734c7fe330ae35e6ee99e7cf4183e8fe1bc01f6bbbc6293b21076a338cb"
+ },
+ {
+ "name": "zero",
+ "unicode": "0030-20E3",
+ "digest": "519c927db8264d5379ab2c6a18656ea6dd1ceb2afc92eb48563bf86af4697571"
+ },
+ {
+ "name": "zipper_mouth",
+ "unicode": "1F910",
+ "digest": "8396249161b6d865861b56aabd17cae2c821b0d814f4249bf8cab0bb21fa8ee9"
+ },
+ {
+ "name": "zzz",
+ "unicode": "1F4A4",
+ "digest": "f07c56d2d55c0a886c26a8e3d49a9adeab54cc1a0c0354ea8d3bf23aaed3176d"
+ }
+] \ No newline at end of file
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index 592100a7045..231840148d9 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -64,7 +64,7 @@ module API
authorize_admin_project
@branch = user_project.repository.find_branch(params[:branch])
- not_found!("Branch does not exist") unless @branch
+ not_found!("Branch") unless @branch
protected_branch = user_project.protected_branches.find_by(name: @branch.name)
protected_branch.destroy if protected_branch
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index f414c1f9885..785593461fd 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -8,14 +8,14 @@ module API
expose :id, :state, :avatar_url
expose :web_url do |user, options|
- Gitlab::Application.routes.url_helpers.user_url(user)
+ Gitlab::Routing.url_helpers.user_url(user)
end
end
class User < UserBasic
expose :created_at
expose :is_admin?, as: :is_admin
- expose :bio, :skype, :linkedin, :twitter, :website_url
+ expose :bio, :location, :skype, :linkedin, :twitter, :website_url
end
class Identity < Grape::Entity
@@ -89,7 +89,7 @@ module API
expose :avatar_url
expose :web_url do |group, options|
- Gitlab::Application.routes.url_helpers.group_url(group)
+ Gitlab::Routing.url_helpers.group_url(group)
end
end
@@ -170,6 +170,10 @@ module API
expose :label_names, as: :labels
expose :milestone, using: Entities::Milestone
expose :assignee, :author, using: Entities::UserBasic
+
+ expose :subscribed do |issue, options|
+ issue.subscribed?(options[:current_user])
+ end
end
class MergeRequest < ProjectEntity
@@ -183,6 +187,10 @@ module API
expose :milestone, using: Entities::Milestone
expose :merge_when_build_succeeds
expose :merge_status
+
+ expose :subscribed do |merge_request, options|
+ merge_request.subscribed?(options[:current_user])
+ end
end
class MergeRequestChanges < MergeRequest
@@ -339,7 +347,6 @@ module API
expose :updated_at
expose :home_page_url
expose :default_branch_protection
- expose :twitter_sharing_enabled
expose :restricted_visibility_levels
expose :max_attachment_size
expose :session_expire_delay
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 1fee1dee1a6..c4ea05ee6cf 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -55,7 +55,7 @@ module API
issues = filter_issues_state(issues, params[:state]) unless params[:state].nil?
issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil?
issues.reorder(issuable_order_by => issuable_sort)
- present paginate(issues), with: Entities::Issue
+ present paginate(issues), with: Entities::Issue, current_user: current_user
end
end
@@ -92,7 +92,7 @@ module API
end
issues.reorder(issuable_order_by => issuable_sort)
- present paginate(issues), with: Entities::Issue
+ present paginate(issues), with: Entities::Issue, current_user: current_user
end
# Get a single project issue
@@ -105,7 +105,7 @@ module API
get ":id/issues/:issue_id" do
@issue = user_project.issues.find(params[:issue_id])
not_found! unless can?(current_user, :read_issue, @issue)
- present @issue, with: Entities::Issue
+ present @issue, with: Entities::Issue, current_user: current_user
end
# Create a new project issue
@@ -149,7 +149,7 @@ module API
issue.add_labels_by_names(params[:labels].split(','))
end
- present issue, with: Entities::Issue
+ present issue, with: Entities::Issue, current_user: current_user
else
render_validation_error!(issue)
end
@@ -189,7 +189,7 @@ module API
issue.add_labels_by_names(params[:labels].split(','))
end
- present issue, with: Entities::Issue
+ present issue, with: Entities::Issue, current_user: current_user
else
render_validation_error!(issue)
end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 93052fba06b..4e7de8867b4 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -56,7 +56,7 @@ module API
end
merge_requests = merge_requests.reorder(issuable_order_by => issuable_sort)
- present paginate(merge_requests), with: Entities::MergeRequest
+ present paginate(merge_requests), with: Entities::MergeRequest, current_user: current_user
end
# Create MR
@@ -94,7 +94,7 @@ module API
merge_request.add_labels_by_names(params[:labels].split(","))
end
- present merge_request, with: Entities::MergeRequest
+ present merge_request, with: Entities::MergeRequest, current_user: current_user
else
handle_merge_request_errors! merge_request.errors
end
@@ -130,7 +130,7 @@ module API
authorize! :read_merge_request, merge_request
- present merge_request, with: Entities::MergeRequest
+ present merge_request, with: Entities::MergeRequest, current_user: current_user
end
# Show MR commits
@@ -162,7 +162,7 @@ module API
merge_request = user_project.merge_requests.
find(params[:merge_request_id])
authorize! :read_merge_request, merge_request
- present merge_request, with: Entities::MergeRequestChanges
+ present merge_request, with: Entities::MergeRequestChanges, current_user: current_user
end
# Update MR
@@ -204,7 +204,7 @@ module API
merge_request.add_labels_by_names(params[:labels].split(","))
end
- present merge_request, with: Entities::MergeRequest
+ present merge_request, with: Entities::MergeRequest, current_user: current_user
else
handle_merge_request_errors! merge_request.errors
end
@@ -246,7 +246,7 @@ module API
execute(merge_request)
end
- present merge_request, with: Entities::MergeRequest
+ present merge_request, with: Entities::MergeRequest, current_user: current_user
end
# Cancel Merge if Merge When build succeeds is enabled
@@ -325,7 +325,7 @@ module API
get "#{path}/closes_issues" do
merge_request = user_project.merge_requests.find(params[:merge_request_id])
issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
- present paginate(issues), with: Entities::Issue
+ present paginate(issues), with: Entities::Issue, current_user: current_user
end
end
end
diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb
index c5cd73943fb..0f3f505fa05 100644
--- a/lib/api/milestones.rb
+++ b/lib/api/milestones.rb
@@ -3,17 +3,33 @@ module API
class Milestones < Grape::API
before { authenticate! }
+ helpers do
+ def filter_milestones_state(milestones, state)
+ case state
+ when 'active' then milestones.active
+ when 'closed' then milestones.closed
+ else milestones
+ end
+ end
+ end
+
resource :projects do
# Get a list of project milestones
#
# Parameters:
- # id (required) - The ID of a project
+ # id (required) - The ID of a project
+ # state (optional) - Return "active" or "closed" milestones
# Example Request:
# GET /projects/:id/milestones
+ # GET /projects/:id/milestones?state=active
+ # GET /projects/:id/milestones?state=closed
get ":id/milestones" do
authorize! :read_milestone, user_project
- present paginate(user_project.milestones), with: Entities::Milestone
+ milestones = user_project.milestones
+ milestones = filter_milestones_state(milestones, params[:state])
+
+ present paginate(milestones), with: Entities::Milestone
end
# Get a single project milestone
@@ -87,7 +103,7 @@ module API
authorize! :read_milestone, user_project
@milestone = user_project.milestones.find(params[:milestone_id])
- present paginate(@milestone.issues), with: Entities::Issue
+ present paginate(@milestone.issues), with: Entities::Issue, current_user: current_user
end
end
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 6fcb5261e40..24b31005475 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -244,6 +244,34 @@ module API
end
end
+ # Archive project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # Example Request:
+ # PUT /projects/:id/archive
+ post ':id/archive' do
+ authorize!(:archive_project, user_project)
+
+ user_project.archive!
+
+ present user_project, with: Entities::Project
+ end
+
+ # Unarchive project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # Example Request:
+ # PUT /projects/:id/unarchive
+ post ':id/unarchive' do
+ authorize!(:archive_project, user_project)
+
+ user_project.unarchive!
+
+ present user_project, with: Entities::Project
+ end
+
# Remove project
#
# Parameters:
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 13ab17c6904..0a14bac07c0 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -58,6 +58,7 @@ module API
# extern_uid - External authentication provider UID
# provider - External provider
# bio - Bio
+ # location - Location of the user
# admin - User is admin - true or false (default)
# can_create_group - User can create groups - true or false
# confirm - Require user confirmation - true (default) or false
@@ -67,7 +68,7 @@ module API
post do
authenticated_as_admin!
required_attributes! [:email, :password, :name, :username]
- attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :can_create_group, :admin, :confirm, :external]
+ attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :confirm, :external]
admin = attrs.delete(:admin)
confirm = !(attrs.delete(:confirm) =~ (/(false|f|no|0)$/i))
user = User.build_user(attrs)
@@ -106,6 +107,7 @@ module API
# website_url - Website url
# projects_limit - Limit projects each user can create
# bio - Bio
+ # location - Location of the user
# admin - User is admin - true or false (default)
# can_create_group - User can create groups - true or false
# external - Flags the user as external - true or false(default)
@@ -114,7 +116,7 @@ module API
put ":id" do
authenticated_as_admin!
- attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :bio, :can_create_group, :admin, :external]
+ attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :external]
user = User.find(params[:id])
not_found!('User') unless user
diff --git a/lib/award_emoji.rb b/lib/award_emoji.rb
index 783fcfb61ad..4fc3443ac68 100644
--- a/lib/award_emoji.rb
+++ b/lib/award_emoji.rb
@@ -48,4 +48,23 @@ class AwardEmoji
JSON.parse(File.read(json_path))
end
end
+
+ # Returns an Array of Emoji names and their asset URLs.
+ def self.urls
+ @urls ||= begin
+ path = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json')
+ prefix = Gitlab::Application.config.assets.prefix
+ digest = Gitlab::Application.config.assets.digest
+
+ JSON.parse(File.read(path)).map do |hash|
+ if digest
+ fname = "#{hash['unicode']}-#{hash['digest']}"
+ else
+ fname = hash['unicode']
+ end
+
+ { name: hash['name'], path: "#{prefix}/#{fname}.png" }
+ end
+ end
+ end
end
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index 34c38913474..b8962379cb5 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -11,15 +11,19 @@ module Banzai
end
def self.object_name
- object_class.name.underscore
+ @object_name ||= object_class.name.underscore
end
def self.object_sym
- object_name.to_sym
+ @object_sym ||= object_name.to_sym
end
def self.data_reference
- "data-#{object_name.dasherize}"
+ @data_reference ||= "data-#{object_name.dasherize}"
+ end
+
+ def self.object_class_title
+ @object_title ||= object_class.name.titleize
end
# Public: Find references in text (like `!123` for merge requests)
@@ -53,6 +57,10 @@ module Banzai
self.class.object_sym
end
+ def object_class_title
+ self.class.object_class_title
+ end
+
def references_in(*args, &block)
self.class.references_in(*args, &block)
end
@@ -62,36 +70,81 @@ module Banzai
# Example: project.merge_requests.find
end
+ def find_object_cached(project, id)
+ if RequestStore.active?
+ cache = find_objects_cache[object_class][project.id]
+
+ get_or_set_cache(cache, id) { find_object(project, id) }
+ else
+ find_object(project, id)
+ end
+ end
+
+ def project_from_ref_cache(ref)
+ if RequestStore.active?
+ cache = project_refs_cache
+
+ get_or_set_cache(cache, ref) { project_from_ref(ref) }
+ else
+ project_from_ref(ref)
+ end
+ end
+
def url_for_object(object, project)
# Implement in child class
# Example: project_merge_request_url
end
- def call
- if object_class.reference_pattern
- # `#123`
- replace_text_nodes_matching(object_class.reference_pattern) do |content|
- object_link_filter(content, object_class.reference_pattern)
- end
+ def url_for_object_cached(object, project)
+ if RequestStore.active?
+ cache = url_for_object_cache[object_class][project.id]
- # `[Issue](#123)`, which is turned into
- # `<a href="#123">Issue</a>`
- replace_link_nodes_with_href(object_class.reference_pattern) do |link, text|
- object_link_filter(link, object_class.reference_pattern, link_text: text)
- end
+ get_or_set_cache(cache, object) { url_for_object(object, project) }
+ else
+ url_for_object(object, project)
end
+ end
- if object_class.link_reference_pattern
- # `http://gitlab.example.com/namespace/project/issues/123`, which is turned into
- # `<a href="http://gitlab.example.com/namespace/project/issues/123">http://gitlab.example.com/namespace/project/issues/123</a>`
- replace_link_nodes_with_text(object_class.link_reference_pattern) do |text|
- object_link_filter(text, object_class.link_reference_pattern)
- end
+ def call
+ return doc if project.nil?
+
+ ref_pattern = object_class.reference_pattern
+ link_pattern = object_class.link_reference_pattern
- # `[Issue](http://gitlab.example.com/namespace/project/issues/123)`, which is turned into
- # `<a href="http://gitlab.example.com/namespace/project/issues/123">Issue</a>`
- replace_link_nodes_with_href(object_class.link_reference_pattern) do |link, text|
- object_link_filter(link, object_class.link_reference_pattern, link_text: text)
+ each_node do |node|
+ if text_node?(node) && ref_pattern
+ replace_text_when_pattern_matches(node, ref_pattern) do |content|
+ object_link_filter(content, ref_pattern)
+ end
+
+ elsif element_node?(node)
+ yield_valid_link(node) do |link, text|
+ if ref_pattern && link =~ /\A#{ref_pattern}\z/
+ replace_link_node_with_href(node, link) do
+ object_link_filter(link, ref_pattern, link_text: text)
+ end
+
+ next
+ end
+
+ next unless link_pattern
+
+ if link == text && text =~ /\A#{link_pattern}/
+ replace_link_node_with_text(node, link) do
+ object_link_filter(text, link_pattern)
+ end
+
+ next
+ end
+
+ if link =~ /\A#{link_pattern}\z/
+ replace_link_node_with_href(node, link) do
+ object_link_filter(link, link_pattern, link_text: text)
+ end
+
+ next
+ end
+ end
end
end
@@ -109,9 +162,9 @@ module Banzai
# have `gfm` and `gfm-OBJECT_NAME` class names attached for styling.
def object_link_filter(text, pattern, link_text: nil)
references_in(text, pattern) do |match, id, project_ref, matches|
- project = project_from_ref(project_ref)
+ project = project_from_ref_cache(project_ref)
- if project && object = find_object(project, id)
+ if project && object = find_object_cached(project, id)
title = object_link_title(object)
klass = reference_class(object_sym)
@@ -121,8 +174,11 @@ module Banzai
object_sym => object.id
)
- url = matches[:url] if matches.names.include?("url")
- url ||= url_for_object(object, project)
+ if matches.names.include?("url") && matches[:url]
+ url = matches[:url]
+ else
+ url = url_for_object_cached(object, project)
+ end
text = link_text || object_link_text(object, matches)
@@ -146,7 +202,7 @@ module Banzai
end
def object_link_title(object)
- "#{object_class.name.titleize}: #{object.title}"
+ "#{object_class_title}: #{object.title}"
end
def object_link_text(object, matches)
@@ -157,6 +213,32 @@ module Banzai
text
end
+
+ private
+
+ def project_refs_cache
+ RequestStore[:banzai_project_refs] ||= {}
+ end
+
+ def find_objects_cache
+ RequestStore[:banzai_find_objects_cache] ||= Hash.new do |hash, key|
+ hash[key] = Hash.new { |h, k| h[k] = {} }
+ end
+ end
+
+ def url_for_object_cache
+ RequestStore[:banzai_url_for_object] ||= Hash.new do |hash, key|
+ hash[key] = Hash.new { |h, k| h[k] = {} }
+ end
+ end
+
+ def get_or_set_cache(cache, key)
+ if cache.key?(key)
+ cache[key]
+ else
+ cache[key] = yield
+ end
+ end
end
end
end
diff --git a/lib/banzai/filter/commit_range_reference_filter.rb b/lib/banzai/filter/commit_range_reference_filter.rb
index 470727ee312..b469ea0f626 100644
--- a/lib/banzai/filter/commit_range_reference_filter.rb
+++ b/lib/banzai/filter/commit_range_reference_filter.rb
@@ -43,7 +43,7 @@ module Banzai
end
def url_for_object(range, project)
- h = Gitlab::Application.routes.url_helpers
+ h = Gitlab::Routing.url_helpers
h.namespace_project_compare_url(project.namespace, project,
range.to_param.merge(only_path: context[:only_path]))
end
diff --git a/lib/banzai/filter/commit_reference_filter.rb b/lib/banzai/filter/commit_reference_filter.rb
index 713a56ba949..bd88207326c 100644
--- a/lib/banzai/filter/commit_reference_filter.rb
+++ b/lib/banzai/filter/commit_reference_filter.rb
@@ -37,7 +37,7 @@ module Banzai
end
def url_for_object(commit, project)
- h = Gitlab::Application.routes.url_helpers
+ h = Gitlab::Routing.url_helpers
h.namespace_project_commit_url(project.namespace, project, commit,
only_path: context[:only_path])
end
diff --git a/lib/banzai/filter/external_issue_reference_filter.rb b/lib/banzai/filter/external_issue_reference_filter.rb
index edc26386903..37344b90576 100644
--- a/lib/banzai/filter/external_issue_reference_filter.rb
+++ b/lib/banzai/filter/external_issue_reference_filter.rb
@@ -35,15 +35,29 @@ module Banzai
def call
# Early return if the project isn't using an external tracker
- return doc if project.nil? || project.default_issues_tracker?
+ return doc if project.nil? || default_issues_tracker?
- replace_text_nodes_matching(ExternalIssue.reference_pattern) do |content|
- issue_link_filter(content)
- end
+ ref_pattern = ExternalIssue.reference_pattern
+ ref_start_pattern = /\A#{ref_pattern}\z/
+
+ each_node do |node|
+ if text_node?(node)
+ replace_text_when_pattern_matches(node, ref_pattern) do |content|
+ issue_link_filter(content)
+ end
- replace_link_nodes_with_href(ExternalIssue.reference_pattern) do |link, text|
- issue_link_filter(link, link_text: text)
+ elsif element_node?(node)
+ yield_valid_link(node) do |link, text|
+ if link =~ ref_start_pattern
+ replace_link_node_with_href(node, link) do
+ issue_link_filter(link, link_text: text)
+ end
+ end
+ end
+ end
end
+
+ doc
end
# Replace `JIRA-123` issue references in text with links to the referenced
@@ -76,6 +90,21 @@ module Banzai
def url_for_issue(*args)
IssuesHelper.url_for_issue(*args)
end
+
+ def default_issues_tracker?
+ if RequestStore.active?
+ default_issues_tracker_cache[project.id] ||=
+ project.default_issues_tracker?
+ else
+ project.default_issues_tracker?
+ end
+ end
+
+ private
+
+ def default_issues_tracker_cache
+ RequestStore[:banzai_default_issues_tracker_cache] ||= {}
+ end
end
end
end
diff --git a/lib/banzai/filter/gollum_tags_filter.rb b/lib/banzai/filter/gollum_tags_filter.rb
index 7ce26db1b90..d08267a9d6c 100644
--- a/lib/banzai/filter/gollum_tags_filter.rb
+++ b/lib/banzai/filter/gollum_tags_filter.rb
@@ -118,7 +118,7 @@ module Banzai
end
if path
- content_tag(:img, nil, src: path)
+ content_tag(:img, nil, src: path, class: 'gfm')
end
end
@@ -144,12 +144,18 @@ module Banzai
# if it is not.
def process_page_link_tag(parts)
if parts.size == 1
- url = parts[0].strip
+ reference = parts[0].strip
else
- name, url = *parts.compact.map(&:strip)
+ name, reference = *parts.compact.map(&:strip)
end
- content_tag(:a, name || url, href: url)
+ if url?(reference)
+ href = reference
+ else
+ href = ::File.join(project_wiki_base_path, reference)
+ end
+
+ content_tag(:a, name || reference, href: href, class: 'gfm')
end
def project_wiki
diff --git a/lib/banzai/filter/image_link_filter.rb b/lib/banzai/filter/image_link_filter.rb
new file mode 100644
index 00000000000..ccd106860bd
--- /dev/null
+++ b/lib/banzai/filter/image_link_filter.rb
@@ -0,0 +1,27 @@
+module Banzai
+ module Filter
+ # HTML filter that wraps links around inline images.
+ class ImageLinkFilter < HTML::Pipeline::Filter
+
+ # Find every image that isn't already wrapped in an `a` tag, create
+ # a new node (a link to the image source), copy the image as a child
+ # of the anchor, and then replace the img with the link-wrapped version.
+ def call
+ doc.xpath('descendant-or-self::img[not(ancestor::a)]').each do |img|
+
+ link = doc.document.create_element(
+ 'a',
+ class: 'no-attachment-icon',
+ href: img['src'],
+ target: '_blank'
+ )
+
+ link.children = img.clone
+ img.replace(link)
+ end
+
+ doc
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb
index 8147e5ed3c7..a2987850d03 100644
--- a/lib/banzai/filter/label_reference_filter.rb
+++ b/lib/banzai/filter/label_reference_filter.rb
@@ -31,7 +31,7 @@ module Banzai
end
def url_for_object(label, project)
- h = Gitlab::Application.routes.url_helpers
+ h = Gitlab::Routing.url_helpers
h.namespace_project_issues_url(project.namespace, project, label_name: label.name,
only_path: context[:only_path])
end
diff --git a/lib/banzai/filter/merge_request_reference_filter.rb b/lib/banzai/filter/merge_request_reference_filter.rb
index 57c71708992..cad38a51851 100644
--- a/lib/banzai/filter/merge_request_reference_filter.rb
+++ b/lib/banzai/filter/merge_request_reference_filter.rb
@@ -14,7 +14,7 @@ module Banzai
end
def url_for_object(mr, project)
- h = Gitlab::Application.routes.url_helpers
+ h = Gitlab::Routing.url_helpers
h.namespace_project_merge_request_url(project.namespace, project, mr,
only_path: context[:only_path])
end
diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb
index 8f710a92bdc..4cb82178024 100644
--- a/lib/banzai/filter/milestone_reference_filter.rb
+++ b/lib/banzai/filter/milestone_reference_filter.rb
@@ -11,7 +11,7 @@ module Banzai
end
def url_for_object(issue, project)
- h = Gitlab::Application.routes.url_helpers
+ h = Gitlab::Routing.url_helpers
h.namespace_project_milestone_url(project.namespace, project, milestone,
only_path: context[:only_path])
end
diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb
index a3326ae042c..31386cf851c 100644
--- a/lib/banzai/filter/reference_filter.rb
+++ b/lib/banzai/filter/reference_filter.rb
@@ -52,18 +52,13 @@ module Banzai
html.html_safe? ? html : ERB::Util.html_escape_once(html)
end
- def ignore_parents
- @ignore_parents ||= begin
- # Don't look for references in text nodes that are children of these
- # elements.
+ def ignore_ancestor_query
+ @ignore_ancestor_query ||= begin
parents = %w(pre code a style)
parents << 'blockquote' if context[:ignore_blockquotes]
- parents.to_set
- end
- end
- def ignored_ancestry?(node)
- has_ancestor?(node, ignore_parents)
+ parents.map { |n| "ancestor::#{n}" }.join(' or ')
+ end
end
def project
@@ -74,119 +69,66 @@ module Banzai
"gfm gfm-#{type}"
end
- # Iterate through the document's text nodes, yielding the current node's
- # content if:
- #
- # * The `project` context value is present AND
- # * The node's content matches `pattern` AND
- # * The node is not an ancestor of an ignored node type
- #
- # pattern - Regex pattern against which to match the node's content
- #
- # Yields the current node's String contents. The result of the block will
- # replace the node's existing content and update the current document.
+ # Ensure that a :project key exists in context
#
- # Returns the updated Nokogiri::HTML::DocumentFragment object.
- def replace_text_nodes_matching(pattern)
- return doc if project.nil?
-
- search_text_nodes(doc).each do |node|
- next if ignored_ancestry?(node)
- next unless node.text =~ pattern
-
- content = node.to_html
-
- html = yield content
-
- next if html == content
-
- node.replace(html)
- end
-
- doc
+ # Note that while the key might exist, its value could be nil!
+ def validate
+ needs :project
end
- # Iterate through the document's link nodes, yielding the current node's
- # content if:
- #
- # * The `project` context value is present AND
- # * The node's content matches `pattern`
- #
- # pattern - Regex pattern against which to match the node's content
- #
- # Yields the current node's String contents. The result of the block will
- # replace the node and update the current document.
+ # Iterates over all <a> and text() nodes in a document.
#
- # Returns the updated Nokogiri::HTML::DocumentFragment object.
- def replace_link_nodes_with_text(pattern)
- return doc if project.nil?
+ # Nodes are skipped whenever their ancestor is one of the nodes returned
+ # by `ignore_ancestor_query`. Link tags are not processed if they have a
+ # "gfm" class or the "href" attribute is empty.
+ def each_node
+ query = %Q{descendant-or-self::text()[not(#{ignore_ancestor_query})]
+ | descendant-or-self::a[
+ not(contains(concat(" ", @class, " "), " gfm ")) and not(@href = "")
+ ]}
- doc.xpath('descendant-or-self::a').each do |node|
- klass = node.attr('class')
- next if klass && klass.include?('gfm')
-
- link = node.attr('href')
- text = node.text
-
- next unless link && text
-
- link = CGI.unescape(link)
- next unless link.force_encoding('UTF-8').valid_encoding?
- # Ignore ending punctionation like periods or commas
- next unless link == text && text =~ /\A#{pattern}/
-
- html = yield text
+ doc.xpath(query).each do |node|
+ yield node
+ end
+ end
- next if html == text
+ # Yields the link's URL and text whenever the node is a valid <a> tag.
+ def yield_valid_link(node)
+ link = CGI.unescape(node.attr('href').to_s)
+ text = node.text
- node.replace(html)
- end
+ return unless link.force_encoding('UTF-8').valid_encoding?
- doc
+ yield link, text
end
- # Iterate through the document's link nodes, yielding the current node's
- # content if:
- #
- # * The `project` context value is present AND
- # * The node's HREF matches `pattern`
- #
- # pattern - Regex pattern against which to match the node's HREF
- #
- # Yields the current node's String HREF and String content.
- # The result of the block will replace the node and update the current document.
- #
- # Returns the updated Nokogiri::HTML::DocumentFragment object.
- def replace_link_nodes_with_href(pattern)
- return doc if project.nil?
+ def replace_text_when_pattern_matches(node, pattern)
+ return unless node.text =~ pattern
- doc.xpath('descendant-or-self::a').each do |node|
- klass = node.attr('class')
- next if klass && klass.include?('gfm')
+ content = node.to_html
+ html = yield content
- link = node.attr('href')
- text = node.text
+ node.replace(html) unless content == html
+ end
- next unless link && text
- link = CGI.unescape(link)
- next unless link.force_encoding('UTF-8').valid_encoding?
- next unless link && link =~ /\A#{pattern}\z/
+ def replace_link_node_with_text(node, link)
+ html = yield
- html = yield link, text
+ node.replace(html) unless html == node.text
+ end
- next if html == link
+ def replace_link_node_with_href(node, link)
+ html = yield
- node.replace(html)
- end
+ node.replace(html) unless html == link
+ end
- doc
+ def text_node?(node)
+ node.is_a?(Nokogiri::XML::Text)
end
- # Ensure that a :project key exists in context
- #
- # Note that while the key might exist, its value could be nil!
- def validate
- needs :project
+ def element_node?(node)
+ node.is_a?(Nokogiri::XML::Element)
end
end
end
diff --git a/lib/banzai/filter/snippet_reference_filter.rb b/lib/banzai/filter/snippet_reference_filter.rb
index c870a42f741..d507eb5ebe1 100644
--- a/lib/banzai/filter/snippet_reference_filter.rb
+++ b/lib/banzai/filter/snippet_reference_filter.rb
@@ -14,7 +14,7 @@ module Banzai
end
def url_for_object(snippet, project)
- h = Gitlab::Application.routes.url_helpers
+ h = Gitlab::Routing.url_helpers
h.namespace_project_snippet_url(project.namespace, project, snippet,
only_path: context[:only_path])
end
diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb
index 24f16f8b547..eea3af842b6 100644
--- a/lib/banzai/filter/user_reference_filter.rb
+++ b/lib/banzai/filter/user_reference_filter.rb
@@ -59,13 +59,28 @@ module Banzai
end
def call
- replace_text_nodes_matching(User.reference_pattern) do |content|
- user_link_filter(content)
+ return doc if project.nil?
+
+ ref_pattern = User.reference_pattern
+ ref_pattern_start = /\A#{ref_pattern}\z/
+
+ each_node do |node|
+ if text_node?(node)
+ replace_text_when_pattern_matches(node, ref_pattern) do |content|
+ user_link_filter(content)
+ end
+ elsif element_node?(node)
+ yield_valid_link(node) do |link, text|
+ if link =~ ref_pattern_start
+ replace_link_node_with_href(node, link) do
+ user_link_filter(link, link_text: text)
+ end
+ end
+ end
+ end
end
- replace_link_nodes_with_href(User.reference_pattern) do |link, text|
- user_link_filter(link, link_text: text)
- end
+ doc
end
# Replace `@user` user references in text with links to the referenced
@@ -90,7 +105,7 @@ module Banzai
private
def urls
- Gitlab::Application.routes.url_helpers
+ Gitlab::Routing.url_helpers
end
def link_class
diff --git a/lib/banzai/filter/wiki_link_filter.rb b/lib/banzai/filter/wiki_link_filter.rb
new file mode 100644
index 00000000000..06d10c98501
--- /dev/null
+++ b/lib/banzai/filter/wiki_link_filter.rb
@@ -0,0 +1,56 @@
+require 'uri'
+
+module Banzai
+ module Filter
+ # HTML filter that "fixes" relative links to files in a repository.
+ #
+ # Context options:
+ # :project_wiki
+ class WikiLinkFilter < HTML::Pipeline::Filter
+
+ def call
+ return doc unless project_wiki?
+
+ doc.search('a:not(.gfm)').each do |el|
+ process_link_attr el.attribute('href')
+ end
+
+ doc
+ end
+
+ protected
+
+ def project_wiki?
+ !context[:project_wiki].nil?
+ end
+
+ def process_link_attr(html_attr)
+ return if html_attr.blank? || file_reference?(html_attr)
+
+ uri = URI(html_attr.value)
+ if uri.relative? && uri.path.present?
+ html_attr.value = rebuild_wiki_uri(uri).to_s
+ end
+ rescue URI::Error
+ # noop
+ end
+
+ def rebuild_wiki_uri(uri)
+ uri.path = ::File.join(project_wiki_base_path, uri.path)
+ uri
+ end
+
+ def file_reference?(html_attr)
+ !File.extname(html_attr.value).blank?
+ end
+
+ def project_wiki
+ context[:project_wiki]
+ end
+
+ def project_wiki_base_path
+ project_wiki && project_wiki.wiki_base_path
+ end
+ end
+ end
+end
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index 8cd4b50e65a..ed3cfd6b023 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -7,6 +7,7 @@ module Banzai
Filter::SanitizationFilter,
Filter::UploadLinkFilter,
+ Filter::ImageLinkFilter,
Filter::EmojiFilter,
Filter::TableOfContentsFilter,
Filter::AutolinkFilter,
diff --git a/lib/banzai/pipeline/wiki_pipeline.rb b/lib/banzai/pipeline/wiki_pipeline.rb
index 0b5a9e0b2b8..c37b8e71cb0 100644
--- a/lib/banzai/pipeline/wiki_pipeline.rb
+++ b/lib/banzai/pipeline/wiki_pipeline.rb
@@ -2,8 +2,10 @@ module Banzai
module Pipeline
class WikiPipeline < FullPipeline
def self.filters
- @filters ||= super.insert_after(Filter::TableOfContentsFilter,
- Filter::GollumTagsFilter)
+ @filters ||= begin
+ super.insert_after(Filter::TableOfContentsFilter, Filter::GollumTagsFilter)
+ .insert_before(Filter::TaskListFilter, Filter::WikiLinkFilter)
+ end
end
end
end
diff --git a/lib/gitlab/badge/build.rb b/lib/gitlab/badge/build.rb
new file mode 100644
index 00000000000..e5e9fab3f5c
--- /dev/null
+++ b/lib/gitlab/badge/build.rb
@@ -0,0 +1,46 @@
+module Gitlab
+ module Badge
+ ##
+ # Build badge
+ #
+ class Build
+ include Gitlab::Application.routes.url_helpers
+ include ActionView::Helpers::AssetTagHelper
+ include ActionView::Helpers::UrlHelper
+
+ def initialize(project, ref)
+ @project, @ref = project, ref
+ @image = ::Ci::ImageForBuildService.new.execute(project, ref: ref)
+ end
+
+ def type
+ 'image/svg+xml'
+ end
+
+ def data
+ File.read(@image[:path])
+ end
+
+ def to_s
+ @image[:name].sub(/\.svg$/, '')
+ end
+
+ def to_html
+ link_to(image_tag(image_url, alt: 'build status'), link_url)
+ end
+
+ def to_markdown
+ "[![build status](#{image_url})](#{link_url})"
+ end
+
+ def image_url
+ build_namespace_project_badges_url(@project.namespace,
+ @project, @ref, format: :svg)
+ end
+
+ def link_url
+ namespace_project_commits_url(@project.namespace, @project, id: @ref)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index 761b63e98f6..1acc22fe5bf 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -21,7 +21,6 @@ module Gitlab
default_branch_protection: Settings.gitlab['default_branch_protection'],
signup_enabled: Settings.gitlab['signup_enabled'],
signin_enabled: Settings.gitlab['signin_enabled'],
- twitter_sharing_enabled: Settings.gitlab['twitter_sharing_enabled'],
gravatar_enabled: Settings.gravatar['enabled'],
sign_in_text: Settings.extra['sign_in_text'],
restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb
index 41f0edcaf7e..8f9be6cd9a3 100644
--- a/lib/gitlab/email/message/repository_push.rb
+++ b/lib/gitlab/email/message/repository_push.rb
@@ -5,7 +5,7 @@ module Gitlab
attr_accessor :recipient
attr_reader :author_id, :ref, :action
- include Gitlab::Application.routes.url_helpers
+ include Gitlab::Routing.url_helpers
delegate :namespace, :name_with_namespace, to: :project, prefix: :project
delegate :name, to: :author, prefix: :author
diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb
index d4b6f6d120d..97ef9851d71 100644
--- a/lib/gitlab/email/receiver.rb
+++ b/lib/gitlab/email/receiver.rb
@@ -63,6 +63,10 @@ module Gitlab
end
def reply_key
+ key_from_to_header || key_from_additional_headers
+ end
+
+ def key_from_to_header
key = nil
message.to.each do |address|
key = Gitlab::IncomingEmail.key_from_address(address)
@@ -72,6 +76,17 @@ module Gitlab
key
end
+ def key_from_additional_headers
+ reply_key = nil
+
+ Array(message.references).each do |message_id|
+ reply_key = Gitlab::IncomingEmail.key_from_fallback_reply_message_id(message_id)
+ break if reply_key
+ end
+
+ reply_key
+ end
+
def sent_notification
return nil unless reply_key
diff --git a/lib/gitlab/fogbugz_import/client.rb b/lib/gitlab/fogbugz_import/client.rb
index 431d50882fd..2152182b37f 100644
--- a/lib/gitlab/fogbugz_import/client.rb
+++ b/lib/gitlab/fogbugz_import/client.rb
@@ -26,7 +26,7 @@ module Gitlab
def user_map
users = {}
res = @api.command(:listPeople)
- res['people']['person'].each do |user|
+ [res['people']['person']].flatten.each do |user|
users[user['ixPerson']] = { name: user['sFullName'], email: user['sEmail'] }
end
users
diff --git a/lib/gitlab/gfm/reference_rewriter.rb b/lib/gitlab/gfm/reference_rewriter.rb
index a1c6ee7bd69..78d7a4f27cf 100644
--- a/lib/gitlab/gfm/reference_rewriter.rb
+++ b/lib/gitlab/gfm/reference_rewriter.rb
@@ -34,16 +34,21 @@ module Gitlab
@source_project = source_project
@current_user = current_user
@original_html = markdown(text)
+ @pattern = Gitlab::ReferenceExtractor.references_pattern
end
def rewrite(target_project)
- pattern = Gitlab::ReferenceExtractor.references_pattern
+ return @text unless needs_rewrite?
- @text.gsub(pattern) do |reference|
+ @text.gsub(@pattern) do |reference|
unfold_reference(reference, Regexp.last_match, target_project)
end
end
+ def needs_rewrite?
+ @text =~ @pattern
+ end
+
private
def unfold_reference(reference, match, target_project)
diff --git a/lib/gitlab/gfm/uploads_rewriter.rb b/lib/gitlab/gfm/uploads_rewriter.rb
new file mode 100644
index 00000000000..abc8c8c55e6
--- /dev/null
+++ b/lib/gitlab/gfm/uploads_rewriter.rb
@@ -0,0 +1,51 @@
+module Gitlab
+ module Gfm
+ ##
+ # Class that rewrites markdown links for uploads
+ #
+ # Using a pattern defined in `FileUploader` it copies files to a new
+ # project and rewrites all links to uploads in in a given text.
+ #
+ #
+ class UploadsRewriter
+ def initialize(text, source_project, _current_user)
+ @text = text
+ @source_project = source_project
+ @pattern = FileUploader::MARKDOWN_PATTERN
+ end
+
+ def rewrite(target_project)
+ return @text unless needs_rewrite?
+
+ @text.gsub(@pattern) do |markdown|
+ file = find_file(@source_project, $~[:secret], $~[:file])
+ return markdown unless file.try(:exists?)
+
+ new_uploader = FileUploader.new(target_project)
+ new_uploader.store!(file)
+ new_uploader.to_markdown
+ end
+ end
+
+ def needs_rewrite?
+ files.any?
+ end
+
+ def files
+ referenced_files = @text.scan(@pattern).map do
+ find_file(@source_project, $~[:secret], $~[:file])
+ end
+
+ referenced_files.compact.select(&:exists?)
+ end
+
+ private
+
+ def find_file(project, secret, file)
+ uploader = FileUploader.new(project, secret)
+ uploader.retrieve_from_store!(file)
+ uploader.file
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/incoming_email.rb b/lib/gitlab/incoming_email.rb
index 9068d79c95e..8ce9d32abe0 100644
--- a/lib/gitlab/incoming_email.rb
+++ b/lib/gitlab/incoming_email.rb
@@ -1,13 +1,10 @@
module Gitlab
module IncomingEmail
class << self
- def enabled?
- config.enabled && address_formatted_correctly?
- end
+ FALLBACK_REPLY_MESSAGE_ID_REGEX = /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\Z/.freeze
- def address_formatted_correctly?
- config.address &&
- config.address.include?("%{key}")
+ def enabled?
+ config.enabled && config.address
end
def reply_address(key)
@@ -24,6 +21,13 @@ module Gitlab
match[1]
end
+ def key_from_fallback_reply_message_id(message_id)
+ match = message_id.match(FALLBACK_REPLY_MESSAGE_ID_REGEX)
+ return unless match
+
+ match[1]
+ end
+
def config
Gitlab.config.incoming_email
end
diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb
index da4435c7308..f2b649e50a2 100644
--- a/lib/gitlab/ldap/access.rb
+++ b/lib/gitlab/ldap/access.rb
@@ -33,7 +33,10 @@ module Gitlab
def allowed?
if ldap_user
- return true unless ldap_config.active_directory
+ unless ldap_config.active_directory
+ user.activate if user.ldap_blocked?
+ return true
+ end
# Block user in GitLab if he/she was blocked in AD
if Gitlab::LDAP::Person.disabled_via_active_directory?(user.ldap_identity.extern_uid, adapter)
diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb
index 88a265c6af2..4a3f47b5a95 100644
--- a/lib/gitlab/metrics.rb
+++ b/lib/gitlab/metrics.rb
@@ -70,6 +70,32 @@ module Gitlab
value.to_s.gsub('=', '\\=')
end
+ # Measures the execution time of a block.
+ #
+ # Example:
+ #
+ # Gitlab::Metrics.measure(:find_by_username_timings) do
+ # User.find_by_username(some_username)
+ # end
+ #
+ # series - The name of the series to store the data in.
+ # values - A Hash containing extra values to add to the metric.
+ # tags - A Hash containing extra tags to add to the metric.
+ #
+ # Returns the value yielded by the supplied block.
+ def self.measure(series, values = {}, tags = {})
+ return yield unless Transaction.current
+
+ start = Time.now.to_f
+ retval = yield
+ duration = (Time.now.to_f - start) * 1000.0
+ values = values.merge(duration: duration)
+
+ Transaction.current.add_metric(series, values, tags)
+
+ retval
+ end
+
# When enabled this should be set before being used as the usual pattern
# "@foo ||= bar" is _not_ thread-safe.
if enabled?
diff --git a/lib/gitlab/metrics/metric.rb b/lib/gitlab/metrics/metric.rb
index 7ea9555cc8c..1cd1ca30f70 100644
--- a/lib/gitlab/metrics/metric.rb
+++ b/lib/gitlab/metrics/metric.rb
@@ -2,6 +2,8 @@ module Gitlab
module Metrics
# Class for storing details of a single metric (label, value, etc).
class Metric
+ JITTER_RANGE = 0.000001..0.001
+
attr_reader :series, :values, :tags, :created_at
# series - The name of the series (as a String) to store the metric in.
@@ -16,11 +18,29 @@ module Gitlab
# Returns a Hash in a format that can be directly written to InfluxDB.
def to_hash
+ # InfluxDB overwrites an existing point if a new point has the same
+ # series, tag set, and timestamp. In a highly concurrent environment
+ # this means that using the number of seconds since the Unix epoch is
+ # inevitably going to collide with another timestamp. For example, two
+ # Rails requests processed by different processes may end up generating
+ # metrics using the _exact_ same timestamp (in seconds).
+ #
+ # Due to the way InfluxDB is set up there's no solution to this problem,
+ # all we can do is lower the amount of collisions. We do this by using
+ # Time#to_f which returns the seconds as a Float providing greater
+ # accuracy. We then add a small random value that is large enough to
+ # distinguish most timestamps but small enough to not alter the amount
+ # of seconds.
+ #
+ # See https://gitlab.com/gitlab-com/operations/issues/175 for more
+ # information.
+ time = @created_at.to_f + rand(JITTER_RANGE)
+
{
series: @series,
tags: @tags,
values: @values,
- timestamp: @created_at.to_i * 1_000_000_000
+ timestamp: (time * 1_000_000_000).to_i
}
end
end
diff --git a/lib/gitlab/note_data_builder.rb b/lib/gitlab/note_data_builder.rb
index 71cf6a0d886..18523e0aefe 100644
--- a/lib/gitlab/note_data_builder.rb
+++ b/lib/gitlab/note_data_builder.rb
@@ -41,7 +41,7 @@ module Gitlab
data[:issue] = note.noteable.hook_attrs
elsif note.for_merge_request?
data[:merge_request] = note.noteable.hook_attrs
- elsif note.for_project_snippet?
+ elsif note.for_snippet?
data[:snippet] = note.noteable.hook_attrs
end
diff --git a/lib/gitlab/routing.rb b/lib/gitlab/routing.rb
new file mode 100644
index 00000000000..5132177de51
--- /dev/null
+++ b/lib/gitlab/routing.rb
@@ -0,0 +1,13 @@
+module Gitlab
+ module Routing
+ # Returns the URL helpers Module.
+ #
+ # This method caches the output as Rails' "url_helpers" method creates an
+ # anonymous module every time it's called.
+ #
+ # Returns a Module.
+ def self.url_helpers
+ @url_helpers ||= Gitlab::Application.routes.url_helpers
+ end
+ end
+end
diff --git a/lib/gitlab/saml/auth_hash.rb b/lib/gitlab/saml/auth_hash.rb
new file mode 100644
index 00000000000..32c1c9ec5bb
--- /dev/null
+++ b/lib/gitlab/saml/auth_hash.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module Saml
+ class AuthHash < Gitlab::OAuth::AuthHash
+
+ def groups
+ get_raw(Gitlab::Saml::Config.groups)
+ end
+
+ private
+
+ def get_raw(key)
+ # Needs to call `all` because of https://git.io/vVo4u
+ # otherwise just the first value is returned
+ auth_hash.extra[:raw_info].all[key]
+ end
+
+ end
+ end
+end
diff --git a/lib/gitlab/saml/config.rb b/lib/gitlab/saml/config.rb
new file mode 100644
index 00000000000..0f40c00f547
--- /dev/null
+++ b/lib/gitlab/saml/config.rb
@@ -0,0 +1,21 @@
+module Gitlab
+ module Saml
+ class Config
+
+ class << self
+ def options
+ Gitlab.config.omniauth.providers.find { |provider| provider.name == 'saml' }
+ end
+
+ def groups
+ options[:groups_attribute]
+ end
+
+ def external_groups
+ options[:external_groups]
+ end
+ end
+
+ end
+ end
+end
diff --git a/lib/gitlab/saml/user.rb b/lib/gitlab/saml/user.rb
index b1e30110ef5..c1072452abe 100644
--- a/lib/gitlab/saml/user.rb
+++ b/lib/gitlab/saml/user.rb
@@ -18,7 +18,7 @@ module Gitlab
@user ||= find_or_create_ldap_user
end
- if auto_link_saml_enabled?
+ if auto_link_saml_user?
@user ||= find_by_email
end
@@ -26,6 +26,16 @@ module Gitlab
@user ||= build_new_user
end
+ if external_users_enabled?
+ # Check if there is overlap between the user's groups and the external groups
+ # setting then set user as external or internal.
+ if (auth_hash.groups & Gitlab::Saml::Config.external_groups).empty?
+ @user.external = false
+ else
+ @user.external = true
+ end
+ end
+
@user
end
@@ -37,11 +47,23 @@ module Gitlab
end
end
+ def changed?
+ gl_user.changed? || gl_user.identities.any?(&:changed?)
+ end
+
protected
- def auto_link_saml_enabled?
+ def auto_link_saml_user?
Gitlab.config.omniauth.auto_link_saml_user
end
+
+ def external_users_enabled?
+ !Gitlab::Saml::Config.external_groups.nil?
+ end
+
+ def auth_hash=(auth_hash)
+ @auth_hash = Gitlab::Saml::AuthHash.new(auth_hash)
+ end
end
end
end
diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb
index 6f0d02cafd1..f301d42939d 100644
--- a/lib/gitlab/url_builder.rb
+++ b/lib/gitlab/url_builder.rb
@@ -1,7 +1,8 @@
module Gitlab
class UrlBuilder
- include Gitlab::Application.routes.url_helpers
+ include Gitlab::Routing.url_helpers
include GitlabRoutingHelper
+ include ActionView::RecordIdentifier
def initialize(type)
@type = type
@@ -37,19 +38,16 @@ module Gitlab
namespace_project_commit_url(namespace_id: note.project.namespace,
id: note.commit_id,
project_id: note.project,
- anchor: "note_#{note.id}")
+ anchor: dom_id(note))
elsif note.for_issue?
issue = Issue.find(note.noteable_id)
- issue_url(issue,
- anchor: "note_#{note.id}")
+ issue_url(issue, anchor: dom_id(note))
elsif note.for_merge_request?
merge_request = MergeRequest.find(note.noteable_id)
- merge_request_url(merge_request,
- anchor: "note_#{note.id}")
- elsif note.for_project_snippet?
+ merge_request_url(merge_request, anchor: dom_id(note))
+ elsif note.for_snippet?
snippet = Snippet.find(note.noteable_id)
- project_snippet_url(snippet,
- anchor: "note_#{note.id}")
+ project_snippet_url(snippet, anchor: dom_id(note))
end
end
end
diff --git a/lib/tasks/gemojione.rake b/lib/tasks/gemojione.rake
index cfaf4a129b1..7ec00a898fd 100644
--- a/lib/tasks/gemojione.rake
+++ b/lib/tasks/gemojione.rake
@@ -1,19 +1,39 @@
-# This task will generate a standard and Retina sprite of all of the current
-# Gemojione Emojis, with the accompanying SCSS map.
-#
-# It will not appear in `rake -T` output, and the dependent gems are not
-# included in the Gemfile by default, because this task will only be needed
-# occasionally, such as when new Emojis are added to Gemojione.
-
-begin
- require 'sprite_factory'
- require 'rmagick'
-rescue LoadError
- # noop
-end
-
namespace :gemojione do
+ desc 'Generates Emoji SHA256 digests'
+ task digests: :environment do
+ require 'digest/sha2'
+ require 'json'
+
+ dir = Gemojione.index.images_path
+
+ digests = AwardEmoji.emojis.map do |name, emoji_hash|
+ fpath = File.join(dir, "#{emoji_hash['unicode']}.png")
+ digest = Digest::SHA256.file(fpath).hexdigest
+
+ { name: name, unicode: emoji_hash['unicode'], digest: digest }
+ end
+
+ out = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json')
+
+ File.open(out, 'w') do |handle|
+ handle.write(JSON.pretty_generate(digests))
+ end
+ end
+
+ # This task will generate a standard and Retina sprite of all of the current
+ # Gemojione Emojis, with the accompanying SCSS map.
+ #
+ # It will not appear in `rake -T` output, and the dependent gems are not
+ # included in the Gemfile by default, because this task will only be needed
+ # occasionally, such as when new Emojis are added to Gemojione.
task sprite: :environment do
+ begin
+ require 'sprite_factory'
+ require 'rmagick'
+ rescue LoadError
+ # noop
+ end
+
check_requirements!
SIZE = 20
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index 27ed57efe55..effb8eb6001 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -623,7 +623,6 @@ namespace :gitlab do
start_checking "Reply by email"
if Gitlab.config.incoming_email.enabled
- check_address_formatted_correctly
check_imap_authentication
if Rails.env.production?
@@ -643,20 +642,6 @@ namespace :gitlab do
# Checks
########################
- def check_address_formatted_correctly
- print "Address formatted correctly? ... "
-
- if Gitlab::IncomingEmail.address_formatted_correctly?
- puts "yes".green
- else
- puts "no".red
- try_fixing_it(
- "Make sure that the address in config/gitlab.yml includes the '%{key}' placeholder."
- )
- fix_and_rerun
- end
- end
-
def check_initd_configured_correctly
print "Init.d configured correctly? ... "
diff --git a/spec/controllers/admin/projects_controller_spec.rb b/spec/controllers/admin/projects_controller_spec.rb
new file mode 100644
index 00000000000..2ba0d489197
--- /dev/null
+++ b/spec/controllers/admin/projects_controller_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe Admin::ProjectsController do
+ let!(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
+
+ before do
+ sign_in(create(:admin))
+ end
+
+ describe 'GET /projects' do
+ render_views
+
+ it 'retrieves the project for the given visibility level' do
+ get :index, visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]
+ expect(response.body).to match(project.name)
+ end
+
+ it 'does not retrieve the project' do
+ get :index, visibility_levels: [Gitlab::VisibilityLevel::INTERNAL]
+ expect(response.body).to_not match(project.name)
+ end
+ end
+end
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index 5b1f65d7aff..9ef8ba1b097 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -1,15 +1,14 @@
require 'spec_helper'
describe Admin::UsersController do
- let(:admin) { create(:admin) }
+ let(:user) { create(:user) }
before do
- sign_in(admin)
+ sign_in(create(:admin))
end
describe 'DELETE #user with projects' do
- let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
+ let(:project) { create(:empty_project, namespace: user.namespace) }
before do
project.team << [user, :developer]
@@ -23,8 +22,6 @@ describe Admin::UsersController do
end
describe 'PUT block/:id' do
- let(:user) { create(:user) }
-
it 'blocks user' do
put :block, id: user.username
user.reload
@@ -50,8 +47,6 @@ describe Admin::UsersController do
end
context 'manually blocked users' do
- let(:user) { create(:user) }
-
before do
user.block
end
@@ -66,8 +61,6 @@ describe Admin::UsersController do
end
describe 'PUT unlock/:id' do
- let(:user) { create(:user) }
-
before do
request.env["HTTP_REFERER"] = "/"
user.lock_access!
@@ -95,8 +88,6 @@ describe Admin::UsersController do
end
describe 'PATCH disable_two_factor' do
- let(:user) { create(:user) }
-
it 'disables 2FA for the user' do
expect(user).to receive(:disable_two_factor!)
allow(subject).to receive(:user).and_return(user)
diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb
index eb0c6ac6d80..b0793cb1655 100644
--- a/spec/controllers/groups/milestones_controller_spec.rb
+++ b/spec/controllers/groups/milestones_controller_spec.rb
@@ -23,5 +23,11 @@ describe Groups::MilestonesController do
expect(response).to redirect_to(group_milestone_path(group, title.to_slug.to_s, title: title))
expect(Milestone.where(title: title).count).to eq(2)
end
+
+ it "redirects to new when there are no project ids" do
+ post :create, group_id: group.id, milestone: { title: title, project_ids: [""] }
+ expect(response).to render_template :new
+ expect(assigns(:milestone).errors).not_to be_nil
+ end
end
end
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index 98ae424ed7c..8ad73472117 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -93,6 +93,20 @@ describe Projects::BranchesController do
end
end
+ describe "POST destroy with HTML format" do
+ render_views
+
+ it 'returns 303' do
+ post :destroy,
+ format: :html,
+ id: 'foo/bar/baz',
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param
+
+ expect(response.status).to eq(303)
+ end
+ end
+
describe "POST destroy" do
render_views
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index c5b034dc064..75e6b6f45a7 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -63,7 +63,7 @@ describe Projects::MergeRequestsController do
id: merge_request.iid,
format: format)
- expect(response.body).to eq((merge_request.send(:"to_#{format}",user)).to_s)
+ expect(response.body).to eq((merge_request.send(:"to_#{format}")).to_s)
end
it "should not escape Html" do
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
new file mode 100644
index 00000000000..d47e4ab9a4f
--- /dev/null
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -0,0 +1,49 @@
+require('spec_helper')
+
+describe Projects::ProjectMembersController do
+ let(:project) { create(:project) }
+ let(:another_project) { create(:project, :private) }
+ let(:user) { create(:user) }
+ let(:member) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ another_project.team << [member, :guest]
+ sign_in(user)
+ end
+
+ describe '#apply_import' do
+ shared_context 'import applied' do
+ before do
+ post(:apply_import, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ source_project_id: another_project.id)
+ end
+ end
+
+ context 'when user can access source project members' do
+ before { another_project.team << [user, :guest] }
+ include_context 'import applied'
+
+ it 'imports source project members' do
+ expect(project.team_members).to include member
+ expect(response).to set_flash.to 'Successfully imported'
+ expect(response).to redirect_to(
+ namespace_project_project_members_path(project.namespace, project)
+ )
+ end
+ end
+
+ context 'when user is not member of a source project' do
+ include_context 'import applied'
+
+ it 'does not import team members' do
+ expect(project.team_members).to_not include member
+ end
+
+ it 'responds with not found' do
+ expect(response.status).to eq 404
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 1893e946f5c..069cd917e5a 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -83,6 +83,28 @@ describe ProjectsController do
end
end
+ describe "#update" do
+ render_views
+
+ let(:admin) { create(:admin) }
+
+ it "sets the repository to the right path after a rename" do
+ new_path = 'renamed_path'
+ project_params = { path: new_path }
+ controller.instance_variable_set(:@project, project)
+ sign_in(admin)
+
+ put :update,
+ namespace_id: project.namespace.to_param,
+ id: project.id,
+ project: project_params
+
+ expect(project.repository.path).to include(new_path)
+ expect(assigns(:repository).path).to eq(project.repository.path)
+ expect(response.status).to eq(200)
+ end
+ end
+
describe "#destroy" do
let(:admin) { create(:admin) }
diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb
new file mode 100644
index 00000000000..83cc8ec6d26
--- /dev/null
+++ b/spec/controllers/sessions_controller_spec.rb
@@ -0,0 +1,101 @@
+require 'spec_helper'
+
+describe SessionsController do
+ describe '#create' do
+ before do
+ @request.env['devise.mapping'] = Devise.mappings[:user]
+ end
+
+ context 'when using standard authentications' do
+ context 'invalid password' do
+ it 'does not authenticate user' do
+ post(:create, user: { login: 'invalid', password: 'invalid' })
+
+ expect(response)
+ .to set_flash.now[:alert].to /Invalid login or password/
+ end
+ end
+
+ context 'when using valid password' do
+ let(:user) { create(:user) }
+
+ it 'authenticates user correctly' do
+ post(:create, user: { login: user.username, password: user.password })
+
+ expect(response).to set_flash.to /Signed in successfully/
+ expect(subject.current_user). to eq user
+ end
+ end
+ end
+
+ context 'when using two-factor authentication' do
+ let(:user) { create(:user, :two_factor) }
+
+ def authenticate_2fa(user_params)
+ post(:create, { user: user_params }, { otp_user_id: user.id })
+ end
+
+ ##
+ # See #14900 issue
+ #
+ context 'when authenticating with login and OTP of another user' do
+ context 'when another user has 2FA enabled' do
+ let(:another_user) { create(:user, :two_factor) }
+
+ context 'when OTP is valid for another user' do
+ it 'does not authenticate' do
+ authenticate_2fa(login: another_user.username,
+ otp_attempt: another_user.current_otp)
+
+ expect(subject.current_user).to_not eq another_user
+ end
+ end
+
+ context 'when OTP is invalid for another user' do
+ it 'does not authenticate' do
+ authenticate_2fa(login: another_user.username,
+ otp_attempt: 'invalid')
+
+ expect(subject.current_user).to_not eq another_user
+ end
+ end
+
+ context 'when authenticating with OTP' do
+ context 'when OTP is valid' do
+ it 'authenticates correctly' do
+ authenticate_2fa(otp_attempt: user.current_otp)
+
+ expect(subject.current_user).to eq user
+ end
+ end
+
+ context 'when OTP is invalid' do
+ before { authenticate_2fa(otp_attempt: 'invalid') }
+
+ it 'does not authenticate' do
+ expect(subject.current_user).to_not eq user
+ end
+
+ it 'warns about invalid OTP code' do
+ expect(response).to set_flash.now[:alert]
+ .to /Invalid two-factor code/
+ end
+ end
+ end
+
+ context 'when another user does not have 2FA enabled' do
+ let(:another_user) { create(:user) }
+
+ it 'does not leak that 2FA is disabled for another user' do
+ authenticate_2fa(login: another_user.username,
+ otp_attempt: 'invalid')
+
+ expect(response).to set_flash.now[:alert]
+ .to /Invalid two-factor code/
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/factories/file_uploader.rb b/spec/factories/file_uploader.rb
new file mode 100644
index 00000000000..1b36e21f2b0
--- /dev/null
+++ b/spec/factories/file_uploader.rb
@@ -0,0 +1,20 @@
+FactoryGirl.define do
+ factory :file_uploader do
+ project
+ secret nil
+
+ transient do
+ fixture { 'rails_sample.jpg' }
+ path { File.join(Rails.root, 'spec/fixtures', fixture) }
+ file { Rack::Test::UploadedFile.new(path) }
+ end
+
+ after(:build) do |uploader, evaluator|
+ uploader.store!(evaluator.file)
+ end
+
+ initialize_with do
+ new(project, secret)
+ end
+ end
+end
diff --git a/spec/factories/forked_project_links.rb b/spec/factories/forked_project_links.rb
index 252bf2747e1..19a54946fe0 100644
--- a/spec/factories/forked_project_links.rb
+++ b/spec/factories/forked_project_links.rb
@@ -13,5 +13,10 @@ FactoryGirl.define do
factory :forked_project_link do
association :forked_to_project, factory: :project
association :forked_from_project, factory: :project
+
+ after(:create) do |link|
+ link.forked_from_project.reload
+ link.forked_to_project.reload
+ end
end
end
diff --git a/spec/factories_spec.rb b/spec/factories_spec.rb
index 457859dedaf..62de081661d 100644
--- a/spec/factories_spec.rb
+++ b/spec/factories_spec.rb
@@ -1,9 +1,17 @@
require 'spec_helper'
-FactoryGirl.factories.map(&:name).each do |factory_name|
- describe "#{factory_name} factory" do
- it 'should be valid' do
- expect(build(factory_name)).to be_valid
+describe 'factories' do
+ FactoryGirl.factories.each do |factory|
+ describe "#{factory.name} factory" do
+ let(:entity) { build(factory.name) }
+
+ it 'does not raise error when created 'do
+ expect { entity }.to_not raise_error
+ end
+
+ it 'should be valid', if: factory.build_class < ActiveRecord::Base do
+ expect(entity).to be_valid
+ end
end
end
end
diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb
index dc41be8246f..de6aed74fb4 100644
--- a/spec/features/atom/users_spec.rb
+++ b/spec/features/atom/users_spec.rb
@@ -61,7 +61,7 @@ describe "User Feed", feature: true do
end
it 'should have XHTML summaries in merge request descriptions' do
- expect(body).to match /Here is the fix: <img[^>]*\/>/
+ expect(body).to match /Here is the fix: <a[^>]*><img[^>]*\/><\/a>/
end
end
end
diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb
new file mode 100644
index 00000000000..90822a8c123
--- /dev/null
+++ b/spec/features/issues/filter_issues_spec.rb
@@ -0,0 +1,119 @@
+require 'rails_helper'
+
+describe 'Filter issues', feature: true do
+
+ let!(:project) { create(:project) }
+ let!(:user) { create(:user)}
+ let!(:milestone) { create(:milestone, project: project) }
+ let!(:label) { create(:label, project: project) }
+
+ before do
+ project.team << [user, :master]
+ login_as(user)
+ end
+
+ describe 'Filter issues for assignee from issues#index' do
+
+ before do
+ visit namespace_project_issues_path(project.namespace, project)
+
+ find('.js-assignee-search').click
+
+ find('.dropdown-menu-user-link', text: user.username).click
+
+ sleep 2
+ end
+
+ context 'assignee', js: true do
+ it 'should update to current user' do
+ expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
+ end
+
+ it 'should not change when closed link is clicked' do
+ find('.issues-state-filters a', text: "Closed").click
+
+ expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
+ end
+
+
+ it 'should not change when all link is clicked' do
+ find('.issues-state-filters a', text: "All").click
+
+ expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
+ end
+ end
+ end
+
+ describe 'Filter issues for milestone from issues#index' do
+
+ before do
+ visit namespace_project_issues_path(project.namespace, project)
+
+ find('.js-milestone-select').click
+
+ find('.milestone-filter .dropdown-content a', text: milestone.title).click
+
+ sleep 2
+ end
+
+ context 'milestone', js: true do
+ it 'should update to current milestone' do
+ expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title)
+ end
+
+ it 'should not change when closed link is clicked' do
+ find('.issues-state-filters a', text: "Closed").click
+
+ expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title)
+ end
+
+
+ it 'should not change when all link is clicked' do
+ find('.issues-state-filters a', text: "All").click
+
+ expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title)
+ end
+ end
+ end
+
+ describe 'Filter issues for assignee and label from issues#index' do
+
+ before do
+ visit namespace_project_issues_path(project.namespace, project)
+
+ find('.js-assignee-search').click
+
+ find('.dropdown-menu-user-link', text: user.username).click
+
+ sleep 2
+
+ find('.js-label-select').click
+
+ find('.dropdown-menu-labels .dropdown-content a', text: label.title).click
+
+ sleep 2
+ end
+
+ context 'assignee and label', js: true do
+ it 'should update to current assignee and label' do
+ expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
+ expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title)
+ end
+
+ it 'should not change when closed link is clicked' do
+ find('.issues-state-filters a', text: "Closed").click
+
+ expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
+ expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title)
+ end
+
+
+ it 'should not change when all link is clicked' do
+ find('.issues-state-filters a', text: "All").click
+
+ expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
+ expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title)
+ end
+ end
+ end
+end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index db46657c36a..79000666ccc 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -22,7 +22,7 @@ describe 'Issues', feature: true do
before do
visit edit_namespace_project_issue_path(project.namespace, project, issue)
- click_link "Edit"
+ click_button "Go full screen"
end
it 'should open new issue popup' do
diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb
index 12fd8d37210..3d0d0e59fd7 100644
--- a/spec/features/markdown_spec.rb
+++ b/spec/features/markdown_spec.rb
@@ -39,7 +39,7 @@ describe 'GitLab Markdown', feature: true do
end
def doc(html = @html)
- Nokogiri::HTML::DocumentFragment.parse(html)
+ @doc ||= Nokogiri::HTML::DocumentFragment.parse(html)
end
# Shared behavior that all pipelines should exhibit
@@ -230,6 +230,7 @@ describe 'GitLab Markdown', feature: true do
file = Gollum::File.new(@project_wiki.wiki)
expect(file).to receive(:path).and_return('images/example.jpg')
expect(@project_wiki).to receive(:find_file).with('images/example.jpg').and_return(file)
+ allow(@project_wiki).to receive(:wiki_base_path) { '/namespace1/gitlabhq/wikis' }
@html = markdown(@feat.raw_markdown, { pipeline: :wiki, project_wiki: @project_wiki })
end
diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb
new file mode 100644
index 00000000000..fd02d584848
--- /dev/null
+++ b/spec/features/merge_requests/create_new_mr_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+feature 'Create New Merge Request', feature: true, js: false do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+
+ before do
+ project.team << [user, :master]
+
+ login_as user
+ visit namespace_project_merge_requests_path(project.namespace, project)
+ end
+
+ it 'generates a diff for an orphaned branch' do
+ click_link 'New Merge Request'
+ select "orphaned-branch", from: "merge_request_source_branch"
+ select "master", from: "merge_request_target_branch"
+ click_button "Compare branches"
+
+ expect(page).to have_content "README.md"
+ expect(page).to have_content "wm.png"
+
+ fill_in "merge_request_title", with: "Orphaned MR test"
+ click_button "Submit merge request"
+
+ expect(page).to have_content 'git checkout -b orphaned-branch origin/orphaned-branch'
+ end
+end
diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb
index d9a8058efd9..70d0864783d 100644
--- a/spec/features/notes_on_merge_requests_spec.rb
+++ b/spec/features/notes_on_merge_requests_spec.rb
@@ -152,7 +152,7 @@ describe 'Comments', feature: true do
it 'has .new_note css class' do
page.within('.js-temp-notes-holder') do
- expect(subject).to have_css('.new_note')
+ expect(subject).to have_css('.new-note')
end
end
end
@@ -225,6 +225,6 @@ describe 'Comments', feature: true do
end
def click_diff_line(data = line_code)
- page.find(%Q{button[data-line-code="#{data}"]}, visible: false).click
+ execute_script("$('button[data-line-code=\"#{data}\"]').click()")
end
end
diff --git a/spec/features/projects/badges/list_spec.rb b/spec/features/projects/badges/list_spec.rb
new file mode 100644
index 00000000000..13c9b95b316
--- /dev/null
+++ b/spec/features/projects/badges/list_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+feature 'list of badges' do
+ include Select2Helper
+
+ background do
+ user = create(:user)
+ project = create(:project)
+ project.team << [user, :master]
+ login_as(user)
+ visit edit_namespace_project_path(project.namespace, project)
+ end
+
+ scenario 'user displays list of badges' do
+ click_link 'Badges'
+
+ expect(page).to have_content 'build status'
+ expect(page).to have_content 'Markdown'
+ expect(page).to have_content 'HTML'
+ expect(page).to have_css('.highlight', count: 2)
+ expect(page).to have_xpath("//img[@alt='build status']")
+
+ page.within('.highlight', match: :first) do
+ expect(page).to have_content 'badges/master/build.svg'
+ end
+ end
+
+ scenario 'user changes current ref on badges list page', js: true do
+ click_link 'Badges'
+ select2('improve/awesome', from: '#ref')
+
+ expect(page).to have_content 'badges/improve/awesome/build.svg'
+ end
+end
diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb
index 84c036e59c0..3e6289a46b1 100644
--- a/spec/features/search_spec.rb
+++ b/spec/features/search_spec.rb
@@ -1,19 +1,46 @@
require 'spec_helper'
describe "Search", feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, namespace: user.namespace) }
+
before do
- login_as :user
- @project = create(:project, namespace: @user.namespace)
- @project.team << [@user, :reporter]
+ login_with(user)
+ project.team << [user, :reporter]
visit search_path
+ end
- page.within '.search-holder' do
- fill_in "search", with: @project.name[0..3]
- click_button "Search"
+ describe 'searching for Projects' do
+ it 'finds a project' do
+ page.within '.search-holder' do
+ fill_in "search", with: project.name[0..3]
+ click_button "Search"
+ end
+
+ expect(page).to have_content project.name
end
end
- it "should show project in search results" do
- expect(page).to have_content @project.name
+ context 'search for comments' do
+ it 'finds a snippet' do
+ snippet = create(:project_snippet, :private, project: project, author: user, title: 'Some title')
+ note = create(:note,
+ noteable: snippet,
+ author: user,
+ note: 'Supercalifragilisticexpialidocious',
+ project: project)
+ # Must visit project dashboard since global search won't search
+ # everything (e.g. comments, snippets, etc.)
+ visit namespace_project_path(project.namespace, project)
+
+ page.within '.search' do
+ fill_in 'search', with: note.note
+ click_button 'Go'
+ end
+
+ click_link 'Comments'
+
+ expect(page).to have_link(snippet.title)
+ end
end
end
diff --git a/spec/fixtures/emails/reply_without_subaddressing_and_key_inside_references.eml b/spec/fixtures/emails/reply_without_subaddressing_and_key_inside_references.eml
new file mode 100644
index 00000000000..39d5cefbc2a
--- /dev/null
+++ b/spec/fixtures/emails/reply_without_subaddressing_and_key_inside_references.eml
@@ -0,0 +1,42 @@
+Return-Path: <jake@adventuretime.ooo>
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <reply@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog <jake@adventuretime.ooo>
+To: reply@appmail.adventuretime.ooo
+Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+In-Reply-To: <issue_1@localhost>
+References: <issue_1@localhost> <reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost>
+Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux'
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
+
+I could not disagree more. I am obviously biased but adventure time is the
+greatest show ever created. Everyone should watch it.
+
+- Jake out
+
+
+On Sun, Jun 9, 2013 at 1:39 PM, eviltrout via Discourse Meta
+<reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo> wrote:
+>
+>
+>
+> eviltrout posted in 'Adventure Time Sux' on Discourse Meta:
+>
+> ---
+> hey guys everyone knows adventure time sucks!
+>
+> ---
+> Please visit this link to respond: http://localhost:3000/t/adventure-time-sux/1234/3
+>
+> To unsubscribe from these emails, visit your [user preferences](http://localhost:3000/user_preferences).
+>
diff --git a/spec/fixtures/emails/valid_reply.eml b/spec/fixtures/emails/valid_reply.eml
index 1e696389954..980e10a8812 100644
--- a/spec/fixtures/emails/valid_reply.eml
+++ b/spec/fixtures/emails/valid_reply.eml
@@ -7,6 +7,8 @@ Date: Thu, 13 Jun 2013 17:03:48 -0400
From: Jake the Dog <jake@adventuretime.ooo>
To: reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo
Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+In-Reply-To: <issue_1@localhost>
+References: <issue_1@localhost> <reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost>
Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux'
Mime-Version: 1.0
Content-Type: text/plain;
@@ -37,4 +39,4 @@ On Sun, Jun 9, 2013 at 1:39 PM, eviltrout via Discourse Meta
> Please visit this link to respond: http://localhost:3000/t/adventure-time-sux/1234/3
>
> To unsubscribe from these emails, visit your [user preferences](http://localhost:3000/user_preferences).
-> \ No newline at end of file
+>
diff --git a/spec/javascripts/fixtures/zen_mode.html.haml b/spec/javascripts/fixtures/zen_mode.html.haml
index 1701652c61e..cb906a7feaa 100644
--- a/spec/javascripts/fixtures/zen_mode.html.haml
+++ b/spec/javascripts/fixtures/zen_mode.html.haml
@@ -1,4 +1,4 @@
-.zennable
+.md-area
.zen-backdrop
%textarea#note_note.js-gfm-input.markdown-area
%a.js-zen-enter(tabindex="-1" href="#")
diff --git a/spec/javascripts/issue_spec.js.coffee b/spec/javascripts/issue_spec.js.coffee
index 86ba9dd8e96..ea27f36e9b5 100644
--- a/spec/javascripts/issue_spec.js.coffee
+++ b/spec/javascripts/issue_spec.js.coffee
@@ -29,8 +29,8 @@ describe 'reopen/close issue', ->
spyOn(jQuery, 'ajax').and.callFake (req) ->
expect(req.type).toBe('PUT')
expect(req.url).toBe('http://gitlab.com/issues/6/close')
- req.success saved: true
-
+ req.success id: 34
+
$btnClose = $('a.btn-close')
$btnReopen = $('a.btn-reopen')
expect($btnReopen).toBeHidden()
@@ -94,7 +94,7 @@ describe 'reopen/close issue', ->
spyOn(jQuery, 'ajax').and.callFake (req) ->
expect(req.type).toBe('PUT')
expect(req.url).toBe('http://gitlab.com/issues/6/reopen')
- req.success saved: true
+ req.success id: 34
$btnClose = $('a.btn-close')
$btnReopen = $('a.btn-reopen')
diff --git a/spec/lib/award_emoji_spec.rb b/spec/lib/award_emoji_spec.rb
new file mode 100644
index 00000000000..330678f7f16
--- /dev/null
+++ b/spec/lib/award_emoji_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe AwardEmoji do
+ describe '.urls' do
+ subject { AwardEmoji.urls }
+
+ it { is_expected.to be_an_instance_of(Array) }
+ it { is_expected.to_not be_empty }
+
+ context 'every Hash in the Array' do
+ it 'has the correct keys and values' do
+ subject.each do |hash|
+ expect(hash[:name]).to be_an_instance_of(String)
+ expect(hash[:path]).to be_an_instance_of(String)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb
index 5e23c5c319a..fe2ce092e6b 100644
--- a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb
+++ b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb
@@ -70,20 +70,22 @@ describe Banzai::Filter::GollumTagsFilter, lib: true do
end
context 'linking internal resources' do
- it "the created link's text will be equal to the resource's text" do
+ it "the created link's text includes the resource's text and wiki base path" do
tag = '[[wiki-slug]]'
doc = filter("See #{tag}", project_wiki: project_wiki)
+ expected_path = ::File.join(project_wiki.wiki_base_path, 'wiki-slug')
expect(doc.at_css('a').text).to eq 'wiki-slug'
- expect(doc.at_css('a')['href']).to eq 'wiki-slug'
+ expect(doc.at_css('a')['href']).to eq expected_path
end
it "the created link's text will be link-text" do
tag = '[[link-text|wiki-slug]]'
doc = filter("See #{tag}", project_wiki: project_wiki)
+ expected_path = ::File.join(project_wiki.wiki_base_path, 'wiki-slug')
expect(doc.at_css('a').text).to eq 'link-text'
- expect(doc.at_css('a')['href']).to eq 'wiki-slug'
+ expect(doc.at_css('a')['href']).to eq expected_path
end
end
diff --git a/spec/lib/banzai/filter/image_link_filter_spec.rb b/spec/lib/banzai/filter/image_link_filter_spec.rb
new file mode 100644
index 00000000000..dd5594750c8
--- /dev/null
+++ b/spec/lib/banzai/filter/image_link_filter_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe Banzai::Filter::ImageLinkFilter, lib: true do
+ include FilterSpecHelper
+
+ def image(path)
+ %(<img src="#{path}" />)
+ end
+
+ it 'wraps the image with a link to the image src' do
+ doc = filter(image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
+ expect(doc.at_css('img')['src']).to eq doc.at_css('a')['href']
+ end
+
+ it 'does not wrap a duplicate link' do
+ exp = act = %q(<a href="/whatever">#{image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')}</a>)
+ expect(filter(act).to_html).to eq exp
+ end
+
+ it 'works with external images' do
+ doc = filter(image('https://i.imgur.com/DfssX9C.jpg'))
+ expect(doc.at_css('img')['src']).to eq doc.at_css('a')['href']
+ end
+end
diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
index 5a0d3d577a8..266ebef33d6 100644
--- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
@@ -95,6 +95,14 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
result = reference_pipeline_result("Fixed #{reference}")
expect(result[:references][:issue]).to eq [issue]
end
+
+ it 'does not process links containing issue numbers followed by text' do
+ href = "#{reference}st"
+ doc = reference_filter("<a href='#{href}'></a>")
+ link = doc.css('a').first.attr('href')
+
+ expect(link).to eq(href)
+ end
end
context 'cross-project reference' do
diff --git a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
index 3e25406e498..7aa1b4a3bf6 100644
--- a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
@@ -11,7 +11,7 @@ describe Banzai::Pipeline::WikiPipeline do
Foo
MD
- result = described_class.call(markdown, project: spy, project_wiki: double)
+ result = described_class.call(markdown, project: spy, project_wiki: spy)
aggregate_failures do
expect(result[:output].text).not_to include '[['
@@ -29,7 +29,7 @@ describe Banzai::Pipeline::WikiPipeline do
Foo
MD
- output = described_class.to_html(markdown, project: spy, project_wiki: double)
+ output = described_class.to_html(markdown, project: spy, project_wiki: spy)
expect(output).to include('[[<em>toc</em>]]')
end
@@ -42,7 +42,7 @@ describe Banzai::Pipeline::WikiPipeline do
Foo
MD
- output = described_class.to_html(markdown, project: spy, project_wiki: double)
+ output = described_class.to_html(markdown, project: spy, project_wiki: spy)
aggregate_failures do
expect(output).not_to include('<ul>')
diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb
index f38fadda9ba..566035c60d0 100644
--- a/spec/lib/extracts_path_spec.rb
+++ b/spec/lib/extracts_path_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe ExtractsPath, lib: true do
include ExtractsPath
include RepoHelpers
- include Gitlab::Application.routes.url_helpers
+ include Gitlab::Routing.url_helpers
let(:project) { double('project') }
diff --git a/spec/lib/gitlab/badge/build_spec.rb b/spec/lib/gitlab/badge/build_spec.rb
new file mode 100644
index 00000000000..329792bb685
--- /dev/null
+++ b/spec/lib/gitlab/badge/build_spec.rb
@@ -0,0 +1,103 @@
+require 'spec_helper'
+
+describe Gitlab::Badge::Build do
+ let(:project) { create(:project) }
+ let(:sha) { project.commit.sha }
+ let(:branch) { 'master' }
+ let(:badge) { described_class.new(project, branch) }
+
+ describe '#type' do
+ subject { badge.type }
+ it { is_expected.to eq 'image/svg+xml' }
+ end
+
+ describe '#to_html' do
+ let(:html) { Nokogiri::HTML.parse(badge.to_html) }
+ let(:a_href) { html.at('a') }
+
+ it 'points to link' do
+ expect(a_href[:href]).to eq badge.link_url
+ end
+
+ it 'contains clickable image' do
+ expect(a_href.children.first.name).to eq 'img'
+ end
+ end
+
+ describe '#to_markdown' do
+ subject { badge.to_markdown }
+
+ it { is_expected.to include badge.image_url }
+ it { is_expected.to include badge.link_url }
+ end
+
+ describe '#image_url' do
+ subject { badge.image_url }
+ it { is_expected.to include "badges/#{branch}/build.svg" }
+ end
+
+ describe '#link_url' do
+ subject { badge.link_url }
+ it { is_expected.to include "commits/#{branch}" }
+ end
+
+ context 'build exists' do
+ let(:ci_commit) { create(:ci_commit, project: project, sha: sha) }
+ let!(:build) { create(:ci_build, commit: ci_commit) }
+
+
+ context 'build success' do
+ before { build.success! }
+
+ describe '#to_s' do
+ subject { badge.to_s }
+ it { is_expected.to eq 'build-success' }
+ end
+
+ describe '#data' do
+ let(:data) { badge.data }
+
+ it 'contains infromation about success' do
+ expect(status_node(data, 'success')).to be_truthy
+ end
+ end
+ end
+
+ context 'build failed' do
+ before { build.drop! }
+
+ describe '#to_s' do
+ subject { badge.to_s }
+ it { is_expected.to eq 'build-failed' }
+ end
+
+ describe '#data' do
+ let(:data) { badge.data }
+
+ it 'contains infromation about failure' do
+ expect(status_node(data, 'failed')).to be_truthy
+ end
+ end
+ end
+ end
+
+ context 'build does not exist' do
+ describe '#to_s' do
+ subject { badge.to_s }
+ it { is_expected.to eq 'build-unknown' }
+ end
+
+ describe '#data' do
+ let(:data) { badge.data }
+
+ it 'contains infromation about unknown build' do
+ expect(status_node(data, 'unknown')).to be_truthy
+ end
+ end
+ end
+
+ def status_node(data, status)
+ xml = Nokogiri::XML.parse(data)
+ xml.at(%Q{text:contains("#{status}")})
+ end
+end
diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb
index 844fd79c991..e9b8ce6b5bb 100644
--- a/spec/lib/gitlab/closing_issue_extractor_spec.rb
+++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb
@@ -23,11 +23,21 @@ describe Gitlab::ClosingIssueExtractor, lib: true do
end
it do
+ message = "Awesome commit (Closes: #{reference})"
+ expect(subject.closed_by_message(message)).to eq([issue])
+ end
+
+ it do
message = "Awesome commit (closes #{reference})"
expect(subject.closed_by_message(message)).to eq([issue])
end
it do
+ message = "Awesome commit (closes: #{reference})"
+ expect(subject.closed_by_message(message)).to eq([issue])
+ end
+
+ it do
message = "Closed #{reference}"
expect(subject.closed_by_message(message)).to eq([issue])
end
@@ -38,105 +48,210 @@ describe Gitlab::ClosingIssueExtractor, lib: true do
end
it do
+ message = "closed: #{reference}"
+ expect(subject.closed_by_message(message)).to eq([issue])
+ end
+
+ it do
message = "Closing #{reference}"
expect(subject.closed_by_message(message)).to eq([issue])
end
it do
+ message = "Closing: #{reference}"
+ expect(subject.closed_by_message(message)).to eq([issue])
+ end
+
+ it do
message = "closing #{reference}"
expect(subject.closed_by_message(message)).to eq([issue])
end
it do
+ message = "closing: #{reference}"
+ expect(subject.closed_by_message(message)).to eq([issue])
+ end
+
+ it do
message = "Close #{reference}"
expect(subject.closed_by_message(message)).to eq([issue])
end
it do
+ message = "Close: #{reference}"
+ expect(subject.closed_by_message(message)).to eq([issue])
+ end
+
+ it do
message = "close #{reference}"
expect(subject.closed_by_message(message)).to eq([issue])
end
it do
+ message = "close: #{reference}"
+ expect(subject.closed_by_message(message)).to eq([issue])
+ end
+
+ it do
message = "Awesome commit (Fixes #{reference})"
expect(subject.closed_by_message(message)).to eq([issue])
end
it do
+ message = "Awesome commit (Fixes: #{reference})"
+ expect(subject.closed_by_message(message)).to eq([issue])
+ end
+
+ it do
message = "Awesome commit (fixes #{reference})"
expect(subject.closed_by_message(message)).to eq([issue])
end
it do
+ message = "Awesome commit (Fixes: #{reference})"
+ expect(subject.closed_by_message(message)).to eq([issue])
+ end
+
+ it do
message = "Fixed #{reference}"
expect(subject.closed_by_message(message)).to eq([issue])
end
it do
+ message = "Fixed: #{reference}"
+ expect(subject.closed_by_message(message)).to eq([issue])
+ end
+
+ it do
message = "fixed #{reference}"
expect(subject.closed_by_message(message)).to eq([issue])
end
it do
+ message = "fixed: #{reference}"
+ expect(subject.closed_by_message(message)).to eq([issue])
+ end
+
+ it do
message = "Fixing #{reference}"
expect(subject.closed_by_message(message)).to eq([issue])
end
it do
+ message = "Fixing: #{reference}"
+ expect(subject.closed_by_message(message)).to eq([issue])
+ end
+
+ it do
message = "fixing #{reference}"
expect(subject.closed_by_message(message)).to eq([issue])
end
it do
+ message = "fixing: #{reference}"
+ expect(subject.closed_by_message(message)).to eq([issue])
+ end
+
+ it do
message = "Fix #{reference}"
expect(subject.closed_by_message(message)).to eq([issue])
end
it do
+ message = "Fix: #{reference}"
+ expect(subject.closed_by_message(message)).to eq([issue])
+ end
+
+ it do
message = "fix #{reference}"
expect(subject.closed_by_message(message)).to eq([issue])
end
it do
+ message = "fix: #{reference}"
+ expect(subject.closed_by_message(message)).to eq([issue])
+ end
+
+ it do
message = "Awesome commit (Resolves #{reference})"
expect(subject.closed_by_message(message)).to eq([issue])
end
it do
+ message = "Awesome commit (Resolves: #{reference})"
+ expect(subject.closed_by_message(message)).to eq([issue])
+ end
+
+ it do
message = "Awesome commit (resolves #{reference})"
expect(subject.closed_by_message(message)).to eq([issue])
end
it do
+ message = "Awesome commit (resolves: #{reference})"
+ expect(subject.closed_by_message(message)).to eq([issue])
+ end
+
+ it do
message = "Resolved #{reference}"
expect(subject.closed_by_message(message)).to eq([issue])
end
it do
+ message = "Resolved: #{reference}"
+ expect(subject.closed_by_message(message)).to eq([issue])
+ end
+
+ it do
message = "resolved #{reference}"
expect(subject.closed_by_message(message)).to eq([issue])
end
it do
+ message = "resolved: #{reference}"
+ expect(subject.closed_by_message(message)).to eq([issue])
+ end
+
+ it do
message = "Resolving #{reference}"
expect(subject.closed_by_message(message)).to eq([issue])
end
it do
+ message = "Resolving: #{reference}"
+ expect(subject.closed_by_message(message)).to eq([issue])
+ end
+
+ it do
message = "resolving #{reference}"
expect(subject.closed_by_message(message)).to eq([issue])
end
it do
+ message = "resolving: #{reference}"
+ expect(subject.closed_by_message(message)).to eq([issue])
+ end
+
+ it do
message = "Resolve #{reference}"
expect(subject.closed_by_message(message)).to eq([issue])
end
it do
+ message = "Resolve: #{reference}"
+ expect(subject.closed_by_message(message)).to eq([issue])
+ end
+
+ it do
message = "resolve #{reference}"
expect(subject.closed_by_message(message)).to eq([issue])
end
+ it do
+ message = "resolve: #{reference}"
+ expect(subject.closed_by_message(message)).to eq([issue])
+ end
+
context 'with an external issue tracker reference' do
it 'extracts the referenced issue' do
jira_project = create(:jira_project, name: 'JIRA_EXT1')
@@ -236,6 +351,6 @@ describe Gitlab::ClosingIssueExtractor, lib: true do
end
def urls
- Gitlab::Application.routes.url_helpers
+ Gitlab::Routing.url_helpers
end
end
diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb
index abe179cd4af..36267faeb93 100644
--- a/spec/lib/gitlab/email/receiver_spec.rb
+++ b/spec/lib/gitlab/email/receiver_spec.rb
@@ -3,6 +3,7 @@ require "spec_helper"
describe Gitlab::Email::Receiver, lib: true do
before do
stub_incoming_email_setting(enabled: true, address: "reply+%{key}@appmail.adventuretime.ooo")
+ stub_config_setting(host: 'localhost')
end
let(:reply_key) { "59d8df8370b7e95c5a49fbf86aeb2c93" }
@@ -137,5 +138,27 @@ describe Gitlab::Email::Receiver, lib: true do
expect(note.note).to include(markdown)
end
+
+ context 'when sub-addressing is not supported' do
+ before do
+ stub_incoming_email_setting(enabled: true, address: nil)
+ end
+
+ shared_examples 'an email that contains a reply key' do |header|
+ it "fetches the reply key from the #{header} header and creates a comment" do
+ expect { receiver.execute }.to change { noteable.notes.count }.by(1)
+ note = noteable.notes.last
+
+ expect(note.author).to eq(sent_notification.recipient)
+ expect(note.note).to include('I could not disagree more.')
+ end
+ end
+
+ context 'reply key is in the References header' do
+ let(:email_raw) { fixture_file('emails/reply_without_subaddressing_and_key_inside_references.eml') }
+
+ it_behaves_like 'an email that contains a reply key', 'References'
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/fogbugz_import/client_spec.rb b/spec/lib/gitlab/fogbugz_import/client_spec.rb
new file mode 100644
index 00000000000..2dc71be0254
--- /dev/null
+++ b/spec/lib/gitlab/fogbugz_import/client_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe Gitlab::FogbugzImport::Client, lib: true do
+
+ let(:client) { described_class.new(uri: '', token: '') }
+ let(:one_user) { { 'people' => { 'person' => { "ixPerson" => "2", "sFullName" => "James" } } } }
+ let(:two_users) { { 'people' => { 'person' => [one_user, { "ixPerson" => "3" }] } } }
+
+ it 'retrieves user_map with one user' do
+ stub_api(one_user)
+
+ expect(client.user_map.count).to eq(1)
+ end
+
+ it 'retrieves user_map with two users' do
+ stub_api(two_users)
+
+ expect(client.user_map.count).to eq(2)
+ end
+
+ def stub_api(users)
+ allow_any_instance_of(::Fogbugz::Interface).to receive(:command).with(:listPeople).and_return(users)
+ end
+end
diff --git a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb
new file mode 100644
index 00000000000..eda956e6f0a
--- /dev/null
+++ b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb
@@ -0,0 +1,66 @@
+require 'spec_helper'
+
+describe Gitlab::Gfm::UploadsRewriter do
+ let(:user) { create(:user) }
+ let(:old_project) { create(:project) }
+ let(:new_project) { create(:project) }
+ let(:rewriter) { described_class.new(text, old_project, user) }
+
+ context 'text contains links to uploads' do
+ let(:image_uploader) do
+ build(:file_uploader, project: old_project)
+ end
+
+ let(:zip_uploader) do
+ build(:file_uploader, project: old_project,
+ fixture: 'ci_build_artifacts.zip')
+ end
+
+ let(:text) do
+ "Text and #{image_uploader.to_markdown} and #{zip_uploader.to_markdown}"
+ end
+
+ describe '#rewrite' do
+ let!(:new_text) { rewriter.rewrite(new_project) }
+
+ let(:old_files) { [image_uploader, zip_uploader].map(&:file) }
+ let(:new_files) do
+ described_class.new(new_text, new_project, user).files
+ end
+
+ let(:old_paths) { old_files.map(&:path) }
+ let(:new_paths) { new_files.map(&:path) }
+
+ it 'rewrites content' do
+ expect(new_text).to_not eq text
+ expect(new_text.length).to eq text.length
+ end
+
+ it 'copies files' do
+ expect(new_files).to all(exist)
+ expect(old_paths).to_not match_array new_paths
+ expect(old_paths).to all(include(old_project.path_with_namespace))
+ expect(new_paths).to all(include(new_project.path_with_namespace))
+ end
+
+ it 'does not remove old files' do
+ expect(old_files).to all(exist)
+ end
+
+ it 'generates a new secret for each file' do
+ expect(new_paths).to_not include image_uploader.secret
+ expect(new_paths).to_not include zip_uploader.secret
+ end
+ end
+
+ describe '#needs_rewrite?' do
+ subject { rewriter.needs_rewrite? }
+ it { is_expected.to eq true }
+ end
+
+ describe '#files' do
+ subject { rewriter.files }
+ it { is_expected.to be_an(Array) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/incoming_email_spec.rb b/spec/lib/gitlab/incoming_email_spec.rb
index bcdba8d4c12..afb3e26f8fb 100644
--- a/spec/lib/gitlab/incoming_email_spec.rb
+++ b/spec/lib/gitlab/incoming_email_spec.rb
@@ -7,24 +7,8 @@ describe Gitlab::IncomingEmail, lib: true do
stub_incoming_email_setting(enabled: true)
end
- context "when the address is valid" do
- before do
- stub_incoming_email_setting(address: "replies+%{key}@example.com")
- end
-
- it "returns true" do
- expect(described_class.enabled?).to be_truthy
- end
- end
-
- context "when the address is invalid" do
- before do
- stub_incoming_email_setting(address: "replies@example.com")
- end
-
- it "returns false" do
- expect(described_class.enabled?).to be_falsey
- end
+ it 'returns true' do
+ expect(described_class.enabled?).to be_truthy
end
end
@@ -58,4 +42,10 @@ describe Gitlab::IncomingEmail, lib: true do
expect(described_class.key_from_address("replies+key@example.com")).to eq("key")
end
end
+
+ context 'self.key_from_fallback_reply_message_id' do
+ it 'returns reply key' do
+ expect(described_class.key_from_fallback_reply_message_id('reply-key@localhost')).to eq('key')
+ end
+ end
end
diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb
index 32a19bf344b..f5b66b8156f 100644
--- a/spec/lib/gitlab/ldap/access_spec.rb
+++ b/spec/lib/gitlab/ldap/access_spec.rb
@@ -33,7 +33,7 @@ describe Gitlab::LDAP::Access, lib: true do
it { is_expected.to be_falsey }
- it 'should block user in GitLab' do
+ it 'blocks user in GitLab' do
access.allowed?
expect(user).to be_blocked
expect(user).to be_ldap_blocked
@@ -78,6 +78,31 @@ describe Gitlab::LDAP::Access, lib: true do
end
it { is_expected.to be_truthy }
+
+ context 'when user cannot be found' do
+ before do
+ allow(Gitlab::LDAP::Person).to receive(:find_by_dn).and_return(nil)
+ end
+
+ it { is_expected.to be_falsey }
+
+ it 'blocks user in GitLab' do
+ access.allowed?
+ expect(user).to be_blocked
+ expect(user).to be_ldap_blocked
+ end
+ end
+
+ context 'when user was previously ldap_blocked' do
+ before do
+ user.ldap_block
+ end
+
+ it 'unblocks the user if it exists' do
+ access.allowed?
+ expect(user).not_to be_blocked
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb
index 0ec8a6dc5cb..8f63a5f2043 100644
--- a/spec/lib/gitlab/metrics_spec.rb
+++ b/spec/lib/gitlab/metrics_spec.rb
@@ -13,7 +13,7 @@ describe Gitlab::Metrics do
end
end
- describe '#submit_metrics' do
+ describe '.submit_metrics' do
it 'prepares and writes the metrics to InfluxDB' do
connection = double(:connection)
pool = double(:pool)
@@ -26,7 +26,7 @@ describe Gitlab::Metrics do
end
end
- describe '#prepare_metrics' do
+ describe '.prepare_metrics' do
it 'returns a Hash with the keys as Symbols' do
metrics = described_class.
prepare_metrics([{ 'values' => {}, 'tags' => {} }])
@@ -51,7 +51,7 @@ describe Gitlab::Metrics do
end
end
- describe '#escape_value' do
+ describe '.escape_value' do
it 'escapes an equals sign' do
expect(described_class.escape_value('foo=')).to eq('foo\\=')
end
@@ -60,4 +60,45 @@ describe Gitlab::Metrics do
expect(described_class.escape_value(10)).to eq('10')
end
end
+
+ describe '.measure' do
+ context 'without a transaction' do
+ it 'returns the return value of the block' do
+ val = Gitlab::Metrics.measure(:foo) { 10 }
+
+ expect(val).to eq(10)
+ end
+ end
+
+ context 'with a transaction' do
+ let(:transaction) { Gitlab::Metrics::Transaction.new }
+
+ before do
+ allow(Gitlab::Metrics::Transaction).to receive(:current).
+ and_return(transaction)
+ end
+
+ it 'adds a metric to the current transaction' do
+ expect(transaction).to receive(:add_metric).
+ with(:foo, { duration: a_kind_of(Numeric) }, { tag: 'value' })
+
+ Gitlab::Metrics.measure(:foo, {}, tag: 'value') { 10 }
+ end
+
+ it 'supports adding of custom values' do
+ values = { duration: a_kind_of(Numeric), number: 10 }
+
+ expect(transaction).to receive(:add_metric).
+ with(:foo, values, { tag: 'value' })
+
+ Gitlab::Metrics.measure(:foo, { number: 10 }, tag: 'value') { 10 }
+ end
+
+ it 'returns the return value of the block' do
+ val = Gitlab::Metrics.measure(:foo) { 10 }
+
+ expect(val).to eq(10)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/saml/user_spec.rb b/spec/lib/gitlab/saml/user_spec.rb
index de7cd99d49d..c2a51d9249c 100644
--- a/spec/lib/gitlab/saml/user_spec.rb
+++ b/spec/lib/gitlab/saml/user_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::Saml::User, lib: true do
let(:gl_user) { saml_user.gl_user }
let(:uid) { 'my-uid' }
let(:provider) { 'saml' }
- let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: provider, info: info_hash) }
+ let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: provider, info: info_hash, extra: { raw_info: OneLogin::RubySaml::Attributes.new({ 'groups' => %w(Developers Freelancers Designers) }) }) }
let(:info_hash) do
{
name: 'John',
@@ -23,10 +23,20 @@ describe Gitlab::Saml::User, lib: true do
allow(Gitlab::LDAP::Config).to receive_messages(messages)
end
+ def stub_basic_saml_config
+ allow(Gitlab::Saml::Config).to receive_messages({ options: { name: 'saml', args: {} } })
+ end
+
+ def stub_saml_group_config(groups)
+ allow(Gitlab::Saml::Config).to receive_messages({ options: { name: 'saml', groups_attribute: 'groups', external_groups: groups, args: {} } })
+ end
+
+ before { stub_basic_saml_config }
+
describe 'account exists on server' do
before { stub_omniauth_config({ allow_single_sign_on: ['saml'], auto_link_saml_user: true }) }
+ let!(:existing_user) { create(:user, email: 'john@mail.com', username: 'john') }
context 'and should bind with SAML' do
- let!(:existing_user) { create(:user, email: 'john@mail.com', username: 'john') }
it 'adds the SAML identity to the existing user' do
saml_user.save
expect(gl_user).to be_valid
@@ -36,6 +46,35 @@ describe Gitlab::Saml::User, lib: true do
expect(identity.provider).to eql 'saml'
end
end
+
+ context 'external groups' do
+ context 'are defined' do
+ it 'marks the user as external' do
+ stub_saml_group_config(%w(Freelancers))
+ saml_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user.external).to be_truthy
+ end
+ end
+
+ before { stub_saml_group_config(%w(Interns)) }
+ context 'are defined but the user does not belong there' do
+ it 'does not mark the user as external' do
+ saml_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user.external).to be_falsey
+ end
+ end
+
+ context 'user was external, now should not be' do
+ it 'should make user internal' do
+ existing_user.update_attribute('external', true)
+ saml_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user.external).to be_falsey
+ end
+ end
+ end
end
describe 'no account exists on server' do
@@ -68,6 +107,26 @@ describe Gitlab::Saml::User, lib: true do
end
end
+ context 'external groups' do
+ context 'are defined' do
+ it 'marks the user as external' do
+ stub_saml_group_config(%w(Freelancers))
+ saml_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user.external).to be_truthy
+ end
+ end
+
+ context 'are defined but the user does not belong there' do
+ it 'does not mark the user as external' do
+ stub_saml_group_config(%w(Interns))
+ saml_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user.external).to be_falsey
+ end
+ end
+ end
+
context 'with auto_link_ldap_user disabled (default)' do
before { stub_omniauth_config({ auto_link_ldap_user: false, auto_link_saml_user: false, allow_single_sign_on: ['saml'] }) }
include_examples 'to verify compliance with allow_single_sign_on'
@@ -76,12 +135,6 @@ describe Gitlab::Saml::User, lib: true do
context 'with auto_link_ldap_user enabled' do
before { stub_omniauth_config({ auto_link_ldap_user: true, auto_link_saml_user: false }) }
- context 'and no LDAP provider defined' do
- before { stub_ldap_config(providers: []) }
-
- include_examples 'to verify compliance with allow_single_sign_on'
- end
-
context 'and at least one LDAP provider is defined' do
before { stub_ldap_config(providers: %w(ldapmain)) }
@@ -89,19 +142,18 @@ describe Gitlab::Saml::User, lib: true do
before do
allow(ldap_user).to receive(:uid) { uid }
allow(ldap_user).to receive(:username) { uid }
- allow(ldap_user).to receive(:email) { ['johndoe@example.com','john2@example.com'] }
+ allow(ldap_user).to receive(:email) { %w(john@mail.com john2@example.com) }
allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' }
allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user)
end
context 'and no account for the LDAP user' do
-
it 'creates a user with dual LDAP and SAML identities' do
saml_user.save
expect(gl_user).to be_valid
expect(gl_user.username).to eql uid
- expect(gl_user.email).to eql 'johndoe@example.com'
+ expect(gl_user.email).to eql 'john@mail.com'
expect(gl_user.identities.length).to eql 2
identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
expect(identities_as_hash).to match_array([ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
@@ -111,13 +163,13 @@ describe Gitlab::Saml::User, lib: true do
end
context 'and LDAP user has an account already' do
- let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') }
- it "adds the omniauth identity to the LDAP account" do
+ let!(:existing_user) { create(:omniauth_user, email: 'john@mail.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') }
+ it 'adds the omniauth identity to the LDAP account' do
saml_user.save
expect(gl_user).to be_valid
expect(gl_user.username).to eql 'john'
- expect(gl_user.email).to eql 'john@example.com'
+ expect(gl_user.email).to eql 'john@mail.com'
expect(gl_user.identities.length).to eql 2
identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
expect(identities_as_hash).to match_array([ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
@@ -126,19 +178,13 @@ describe Gitlab::Saml::User, lib: true do
end
end
end
-
- context 'and no corresponding LDAP person' do
- before { allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(nil) }
-
- include_examples 'to verify compliance with allow_single_sign_on'
- end
end
end
end
describe 'blocking' do
- before { stub_omniauth_config({ allow_saml_sign_up: true, auto_link_saml_user: true }) }
+ before { stub_omniauth_config({ allow_single_sign_on: ['saml'], auto_link_saml_user: true }) }
context 'signup with SAML only' do
context 'dont block on create' do
@@ -162,64 +208,6 @@ describe Gitlab::Saml::User, lib: true do
end
end
- context 'signup with linked omniauth and LDAP account' do
- before do
- stub_omniauth_config(auto_link_ldap_user: true)
- allow(ldap_user).to receive(:uid) { uid }
- allow(ldap_user).to receive(:username) { uid }
- allow(ldap_user).to receive(:email) { ['johndoe@example.com','john2@example.com'] }
- allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' }
- allow(saml_user).to receive(:ldap_person).and_return(ldap_user)
- end
-
- context "and no account for the LDAP user" do
- context 'dont block on create (LDAP)' do
- before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) }
-
- it do
- saml_user.save
- expect(gl_user).to be_valid
- expect(gl_user).not_to be_blocked
- end
- end
-
- context 'block on create (LDAP)' do
- before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) }
-
- it do
- saml_user.save
- expect(gl_user).to be_valid
- expect(gl_user).to be_blocked
- end
- end
- end
-
- context 'and LDAP user has an account already' do
- let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') }
-
- context 'dont block on create (LDAP)' do
- before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) }
-
- it do
- saml_user.save
- expect(gl_user).to be_valid
- expect(gl_user).not_to be_blocked
- end
- end
-
- context 'block on create (LDAP)' do
- before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) }
-
- it do
- saml_user.save
- expect(gl_user).to be_valid
- expect(gl_user).not_to be_blocked
- end
- end
- end
- end
-
-
context 'sign-in' do
before do
saml_user.save
@@ -245,26 +233,6 @@ describe Gitlab::Saml::User, lib: true do
expect(gl_user).not_to be_blocked
end
end
-
- context 'dont block on create (LDAP)' do
- before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) }
-
- it do
- saml_user.save
- expect(gl_user).to be_valid
- expect(gl_user).not_to be_blocked
- end
- end
-
- context 'block on create (LDAP)' do
- before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) }
-
- it do
- saml_user.save
- expect(gl_user).to be_valid
- expect(gl_user).not_to be_blocked
- end
- end
end
end
end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 9b47acfe0cd..631b5094f42 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -35,7 +35,9 @@ describe Notify do
subject { Notify.new_issue_email(issue.assignee_id, issue.id) }
it_behaves_like 'an assignee email'
- it_behaves_like 'an email starting a new thread', 'issue'
+ it_behaves_like 'an email starting a new thread with reply-by-email enabled' do
+ let(:model) { issue }
+ end
it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'an unsubscribeable thread'
@@ -73,9 +75,11 @@ describe Notify do
subject { Notify.reassigned_issue_email(recipient.id, issue.id, previous_assignee.id, current_user.id) }
it_behaves_like 'a multiple recipients email'
- it_behaves_like 'an answer to an existing thread', 'issue'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { issue }
+ end
it_behaves_like 'it should show Gmail Actions View Issue link'
- it_behaves_like "an unsubscribeable thread"
+ it_behaves_like 'an unsubscribeable thread'
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -104,7 +108,9 @@ describe Notify do
subject { Notify.relabeled_issue_email(recipient.id, issue.id, %w[foo bar baz], current_user.id) }
it_behaves_like 'a multiple recipients email'
- it_behaves_like 'an answer to an existing thread', 'issue'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { issue }
+ end
it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'a user cannot unsubscribe through footer link'
it_behaves_like 'an email with a labels subscriptions link in its footer'
@@ -132,7 +138,9 @@ describe Notify do
let(:status) { 'closed' }
subject { Notify.issue_status_changed_email(recipient.id, issue.id, status, current_user.id) }
- it_behaves_like 'an answer to an existing thread', 'issue'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { issue }
+ end
it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'an unsubscribeable thread'
@@ -163,7 +171,9 @@ describe Notify do
let(:new_issue) { create(:issue) }
subject { Notify.issue_moved_email(recipient, issue, new_issue, current_user) }
- it_behaves_like 'an answer to an existing thread', 'issue'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { issue }
+ end
it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'an unsubscribeable thread'
@@ -196,9 +206,11 @@ describe Notify do
subject { Notify.new_merge_request_email(merge_request.assignee_id, merge_request.id) }
it_behaves_like 'an assignee email'
- it_behaves_like 'an email starting a new thread', 'merge_request'
+ it_behaves_like 'an email starting a new thread with reply-by-email enabled' do
+ let(:model) { merge_request }
+ end
it_behaves_like 'it should show Gmail Actions View Merge request link'
- it_behaves_like "an unsubscribeable thread"
+ it_behaves_like 'an unsubscribeable thread'
it 'has the correct subject' do
is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/
@@ -216,10 +228,6 @@ describe Notify do
is_expected.to have_body_text /#{merge_request.target_branch}/
end
- it 'has the correct message-id set' do
- is_expected.to have_header 'Message-ID', "<merge_request_#{merge_request.id}@#{Gitlab.config.gitlab.host}>"
- end
-
context 'when enabled email_author_in_body' do
before do
allow(current_application_settings).to receive(:email_author_in_body).and_return(true)
@@ -247,7 +255,9 @@ describe Notify do
subject { Notify.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id) }
it_behaves_like 'a multiple recipients email'
- it_behaves_like 'an answer to an existing thread', 'merge_request'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { merge_request }
+ end
it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like "an unsubscribeable thread"
@@ -278,7 +288,9 @@ describe Notify do
subject { Notify.relabeled_merge_request_email(recipient.id, merge_request.id, %w[foo bar baz], current_user.id) }
it_behaves_like 'a multiple recipients email'
- it_behaves_like 'an answer to an existing thread', 'merge_request'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { merge_request }
+ end
it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like 'a user cannot unsubscribe through footer link'
it_behaves_like 'an email with a labels subscriptions link in its footer'
@@ -306,9 +318,11 @@ describe Notify do
let(:status) { 'reopened' }
subject { Notify.merge_request_status_email(recipient.id, merge_request.id, status, current_user.id) }
- it_behaves_like 'an answer to an existing thread', 'merge_request'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { merge_request }
+ end
it_behaves_like 'it should show Gmail Actions View Merge request link'
- it_behaves_like "an unsubscribeable thread"
+ it_behaves_like 'an unsubscribeable thread'
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -337,9 +351,11 @@ describe Notify do
subject { Notify.merged_merge_request_email(recipient.id, merge_request.id, merge_author.id) }
it_behaves_like 'a multiple recipients email'
- it_behaves_like 'an answer to an existing thread', 'merge_request'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { merge_request }
+ end
it_behaves_like 'it should show Gmail Actions View Merge request link'
- it_behaves_like "an unsubscribeable thread"
+ it_behaves_like 'an unsubscribeable thread'
it 'is sent as the merge author' do
sender = subject.header[:from].addrs[0]
@@ -456,9 +472,11 @@ describe Notify do
subject { Notify.note_commit_email(recipient.id, note.id) }
it_behaves_like 'a note email'
- it_behaves_like 'an answer to an existing thread', 'commit'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { commit }
+ end
it_behaves_like 'it should show Gmail Actions View Commit link'
- it_behaves_like "a user cannot unsubscribe through footer link"
+ it_behaves_like 'a user cannot unsubscribe through footer link'
it 'has the correct subject' do
is_expected.to have_subject /#{commit.title} \(#{commit.short_id}\)/
@@ -477,7 +495,9 @@ describe Notify do
subject { Notify.note_merge_request_email(recipient.id, note.id) }
it_behaves_like 'a note email'
- it_behaves_like 'an answer to an existing thread', 'merge_request'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { merge_request }
+ end
it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like 'an unsubscribeable thread'
@@ -498,7 +518,9 @@ describe Notify do
subject { Notify.note_issue_email(recipient.id, note.id) }
it_behaves_like 'a note email'
- it_behaves_like 'an answer to an existing thread', 'issue'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { issue }
+ end
it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'an unsubscribeable thread'
diff --git a/spec/mailers/shared/notify.rb b/spec/mailers/shared/notify.rb
index 6019af544d3..5a85cb501dd 100644
--- a/spec/mailers/shared/notify.rb
+++ b/spec/mailers/shared/notify.rb
@@ -10,6 +10,13 @@ shared_context 'gitlab email notification' do
ActionMailer::Base.deliveries.clear
email = recipient.emails.create(email: "notifications@example.com")
recipient.update_attribute(:notification_email, email.email)
+ stub_incoming_email_setting(enabled: true, address: "reply+%{key}@#{Gitlab.config.gitlab.host}")
+ end
+end
+
+shared_context 'reply-by-email is enabled with incoming address without %{key}' do
+ before do
+ stub_incoming_email_setting(enabled: true, address: "reply@#{Gitlab.config.gitlab.host}")
end
end
@@ -46,25 +53,76 @@ shared_examples 'an email with X-GitLab headers containing project details' do
end
end
-shared_examples 'an email starting a new thread' do |message_id_prefix|
- include_examples 'an email with X-GitLab headers containing project details'
+shared_examples 'a new thread email with reply-by-email enabled' do
+ let(:regex) { /\A<reply\-(.*)@#{Gitlab.config.gitlab.host}>\Z/ }
+
+ it 'has a Message-ID header' do
+ is_expected.to have_header 'Message-ID', "<#{model.class.model_name.singular_route_key}_#{model.id}@#{Gitlab.config.gitlab.host}>"
+ end
- it 'has a discussion identifier' do
- is_expected.to have_header 'Message-ID', /<#{message_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/
+ it 'has a References header' do
+ is_expected.to have_header 'References', regex
end
end
-shared_examples 'an answer to an existing thread' do |thread_id_prefix|
+shared_examples 'a thread answer email with reply-by-email enabled' do
include_examples 'an email with X-GitLab headers containing project details'
+ let(:regex) { /\A<#{model.class.model_name.singular_route_key}_#{model.id}@#{Gitlab.config.gitlab.host}> <reply\-(.*)@#{Gitlab.config.gitlab.host}>\Z/ }
+
+ it 'has a Message-ID header' do
+ is_expected.to have_header 'Message-ID', /\A<(.*)@#{Gitlab.config.gitlab.host}>\Z/
+ end
+
+ it 'has a In-Reply-To header' do
+ is_expected.to have_header 'In-Reply-To', "<#{model.class.model_name.singular_route_key}_#{model.id}@#{Gitlab.config.gitlab.host}>"
+ end
+
+ it 'has a References header' do
+ is_expected.to have_header 'References', regex
+ end
it 'has a subject that begins with Re: ' do
is_expected.to have_subject /^Re: /
end
+end
+
+shared_examples 'an email starting a new thread with reply-by-email enabled' do
+ include_examples 'an email with X-GitLab headers containing project details'
+ include_examples 'a new thread email with reply-by-email enabled'
+
+ context 'when reply-by-email is enabled with incoming address with %{key}' do
+ it 'has a Reply-To header' do
+ is_expected.to have_header 'Reply-To', /<reply+(.*)@#{Gitlab.config.gitlab.host}>\Z/
+ end
+ end
+
+ context 'when reply-by-email is enabled with incoming address without %{key}' do
+ include_context 'reply-by-email is enabled with incoming address without %{key}'
+ include_examples 'a new thread email with reply-by-email enabled'
+
+ it 'has a Reply-To header' do
+ is_expected.to have_header 'Reply-To', /<reply@#{Gitlab.config.gitlab.host}>\Z/
+ end
+ end
+end
+
+shared_examples 'an answer to an existing thread with reply-by-email enabled' do
+ include_examples 'an email with X-GitLab headers containing project details'
+ include_examples 'a thread answer email with reply-by-email enabled'
+
+ context 'when reply-by-email is enabled with incoming address with %{key}' do
+ it 'has a Reply-To header' do
+ is_expected.to have_header 'Reply-To', /<reply+(.*)@#{Gitlab.config.gitlab.host}>\Z/
+ end
+ end
+
+ context 'when reply-by-email is enabled with incoming address without %{key}' do
+ include_context 'reply-by-email is enabled with incoming address without %{key}'
+ include_examples 'a thread answer email with reply-by-email enabled'
- it 'has headers that reference an existing thread' do
- is_expected.to have_header 'Message-ID', /<(.*)@#{Gitlab.config.gitlab.host}>/
- is_expected.to have_header 'References', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/
- is_expected.to have_header 'In-Reply-To', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/
+ it 'has a Reply-To header' do
+ is_expected.to have_header 'Reply-To', /<reply@#{Gitlab.config.gitlab.host}>\Z/
+ end
end
end
@@ -83,10 +141,12 @@ shared_examples 'a new user email' do
end
shared_examples 'it should have Gmail Actions links' do
+ it { is_expected.to have_body_text '<script type="application/ld+json">' }
it { is_expected.to have_body_text /ViewAction/ }
end
shared_examples 'it should not have Gmail Actions links' do
+ it { is_expected.to_not have_body_text '<script type="application/ld+json">' }
it { is_expected.to_not have_body_text /ViewAction/ }
end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index b1764d7ac09..520cf1b75de 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -12,7 +12,6 @@
# updated_at :datetime
# home_page_url :string(255)
# default_branch_protection :integer default(2)
-# twitter_sharing_enabled :boolean default(TRUE)
# restricted_visibility_levels :text
# version_check_enabled :boolean default(TRUE)
# max_attachment_size :integer default(10), not null
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index be29b6d66ff..b16ccc6e305 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -9,6 +9,7 @@ describe Issue, "Issuable" do
it { is_expected.to belong_to(:author) }
it { is_expected.to belong_to(:assignee) }
it { is_expected.to have_many(:notes).dependent(:destroy) }
+ it { is_expected.to have_many(:todos).dependent(:destroy) }
end
describe "Validation" do
diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb
index fd1513cab1b..56a9fbe9720 100644
--- a/spec/models/hooks/system_hook_spec.rb
+++ b/spec/models/hooks/system_hook_spec.rb
@@ -20,24 +20,27 @@ require "spec_helper"
describe SystemHook, models: true do
describe "execute" do
- before(:each) do
- @system_hook = create(:system_hook)
- WebMock.stub_request(:post, @system_hook.url)
+ let(:system_hook) { create(:system_hook) }
+ let(:user) { create(:user) }
+ let(:project) { create(:project, namespace: user.namespace) }
+ let(:group) { create(:group) }
+
+ before do
+ WebMock.stub_request(:post, system_hook.url)
end
it "project_create hook" do
- Projects::CreateService.new(create(:user), name: 'empty').execute
- expect(WebMock).to have_requested(:post, @system_hook.url).with(
+ Projects::CreateService.new(user, name: 'empty').execute
+ expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /project_create/,
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
).once
end
it "project_destroy hook" do
- user = create(:user)
- project = create(:empty_project, namespace: user.namespace)
Projects::DestroyService.new(project, user, {}).pending_delete!
- expect(WebMock).to have_requested(:post, @system_hook.url).with(
+
+ expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /project_destroy/,
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
).once
@@ -45,37 +48,36 @@ describe SystemHook, models: true do
it "user_create hook" do
create(:user)
- expect(WebMock).to have_requested(:post, @system_hook.url).with(
+
+ expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /user_create/,
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
).once
end
it "user_destroy hook" do
- user = create(:user)
user.destroy
- expect(WebMock).to have_requested(:post, @system_hook.url).with(
+
+ expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /user_destroy/,
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
).once
end
it "project_create hook" do
- user = create(:user)
- project = create(:project)
project.team << [user, :master]
- expect(WebMock).to have_requested(:post, @system_hook.url).with(
+
+ expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /user_add_to_team/,
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
).once
end
it "project_destroy hook" do
- user = create(:user)
- project = create(:project)
project.team << [user, :master]
project.project_members.destroy_all
- expect(WebMock).to have_requested(:post, @system_hook.url).with(
+
+ expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /user_remove_from_team/,
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
).once
@@ -83,41 +85,39 @@ describe SystemHook, models: true do
it 'group create hook' do
create(:group)
- expect(WebMock).to have_requested(:post, @system_hook.url).with(
+
+ expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /group_create/,
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
).once
end
it 'group destroy hook' do
- group = create(:group)
group.destroy
- expect(WebMock).to have_requested(:post, @system_hook.url).with(
+
+ expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /group_destroy/,
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
).once
end
it 'group member create hook' do
- group = create(:group)
- user = create(:user)
group.add_master(user)
- expect(WebMock).to have_requested(:post, @system_hook.url).with(
+
+ expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /user_add_to_group/,
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
).once
end
it 'group member destroy hook' do
- group = create(:group)
- user = create(:user)
group.add_master(user)
group.group_members.destroy_all
- expect(WebMock).to have_requested(:post, @system_hook.url).with(
+
+ expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /user_remove_from_group/,
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
).once
end
-
end
end
diff --git a/spec/models/project_services/builds_email_service_spec.rb b/spec/models/project_services/builds_email_service_spec.rb
index 905379a64e3..2ccbff553f0 100644
--- a/spec/models/project_services/builds_email_service_spec.rb
+++ b/spec/models/project_services/builds_email_service_spec.rb
@@ -6,18 +6,38 @@ describe BuildsEmailService do
let(:service) { BuildsEmailService.new }
describe :execute do
- it "sends email" do
+ it 'sends email' do
service.recipients = 'test@gitlab.com'
data[:build_status] = 'failed'
expect(BuildEmailWorker).to receive(:perform_async)
service.execute(data)
end
- it "does not sends email with failed build and allowed_failure on" do
+ it 'does not send email with succeeded build and notify_only_broken_builds on' do
+ expect(service).to receive(:notify_only_broken_builds).and_return(true)
+ data[:build_status] = 'success'
+ expect(BuildEmailWorker).not_to receive(:perform_async)
+ service.execute(data)
+ end
+
+ it 'does not send email with failed build and build_allow_failure is true' do
data[:build_status] = 'failed'
data[:build_allow_failure] = true
expect(BuildEmailWorker).not_to receive(:perform_async)
service.execute(data)
end
+
+ it 'does not send email with unknown build status' do
+ data[:build_status] = 'foo'
+ expect(BuildEmailWorker).not_to receive(:perform_async)
+ service.execute(data)
+ end
+
+ it 'does not send email when recipients list is empty' do
+ service.recipients = ' ,, '
+ data[:build_status] = 'failed'
+ expect(BuildEmailWorker).not_to receive(:perform_async)
+ service.execute(data)
+ end
end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 55f1c665b86..f29c389e094 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -104,6 +104,15 @@ describe Project, models: true do
end
end
+ describe 'default_scope' do
+ it 'excludes projects pending deletion from the results' do
+ project = create(:empty_project)
+ create(:empty_project, pending_delete: true)
+
+ expect(Project.all).to eq [project]
+ end
+ end
+
describe 'project token' do
it 'should set an random token if none provided' do
project = FactoryGirl.create :empty_project, runners_token: ''
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index f10d671104c..4e49c413f23 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -303,7 +303,7 @@ describe Repository, models: true do
describe 'when there are no branches' do
before do
- allow(repository.raw_repository).to receive(:branch_count).and_return(0)
+ allow(repository).to receive(:branch_count).and_return(0)
end
it { is_expected.to eq(false) }
@@ -311,13 +311,13 @@ describe Repository, models: true do
describe 'when there are branches' do
it 'returns true' do
- expect(repository.raw_repository).to receive(:branch_count).and_return(3)
+ expect(repository).to receive(:branch_count).and_return(3)
expect(subject).to eq(true)
end
it 'caches the output' do
- expect(repository.raw_repository).to receive(:branch_count).
+ expect(repository).to receive(:branch_count).
once.
and_return(3)
@@ -436,7 +436,7 @@ describe Repository, models: true do
it 'expires the visible content cache' do
repository.has_visible_content?
- expect(repository.raw_repository).to receive(:branch_count).
+ expect(repository).to receive(:branch_count).
once.
and_return(0)
@@ -558,7 +558,7 @@ describe Repository, models: true do
end
it 'flushes the exists cache' do
- expect(repository).to receive(:expire_exists_cache)
+ expect(repository).to receive(:expire_exists_cache).twice
repository.before_delete
end
@@ -612,6 +612,20 @@ describe Repository, models: true do
end
end
+ describe '#before_import' do
+ it 'flushes the emptiness cachess' do
+ expect(repository).to receive(:expire_emptiness_caches)
+
+ repository.before_import
+ end
+
+ it 'flushes the exists cache' do
+ expect(repository).to receive(:expire_exists_cache)
+
+ repository.before_import
+ end
+ end
+
describe '#after_import' do
it 'flushes the emptiness cachess' do
expect(repository).to receive(:expire_emptiness_caches)
@@ -656,6 +670,19 @@ describe Repository, models: true do
repository.after_create
end
+
+ it 'flushes the root ref cache' do
+ expect(repository).to receive(:expire_root_ref_cache)
+
+ repository.after_create
+ end
+
+ it 'flushes the emptiness caches' do
+ expect(repository).to receive(:expire_emptiness_caches)
+
+ repository.after_create
+ end
+
end
describe "#main_language" do
@@ -865,4 +892,21 @@ describe Repository, models: true do
repository.build_cache
end
end
+
+ describe '#local_branches' do
+ it 'returns the local branches' do
+ masterrev = repository.find_branch('master').target
+ create_remote_branch('joe', 'remote_branch', masterrev)
+ repository.add_branch(user, 'local_branch', masterrev)
+
+ expect(repository.local_branches.any? { |branch| branch.name == 'remote_branch' }).to eq(false)
+ expect(repository.local_branches.any? { |branch| branch.name == 'local_branch' }).to eq(true)
+ end
+ end
+
+ def create_remote_branch(remote_name, branch_name, target)
+ rugged = repository.rugged
+ rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", target)
+ end
+
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 0ab7fd88ce6..8b2fb77e28e 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -173,6 +173,13 @@ describe User, models: true do
expect(user).to be_invalid
end
end
+
+ context 'owns_notification_email' do
+ it 'accepts temp_oauth_email emails' do
+ user = build(:user, email: "temp-email-for-oauth@example.com")
+ expect(user).to be_valid
+ end
+ end
end
end
diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb
index db0f6e3c0f5..d97bf6d38ff 100644
--- a/spec/requests/api/milestones_spec.rb
+++ b/spec/requests/api/milestones_spec.rb
@@ -4,6 +4,7 @@ describe API::API, api: true do
include ApiHelpers
let(:user) { create(:user) }
let!(:project) { create(:project, namespace: user.namespace ) }
+ let!(:closed_milestone) { create(:closed_milestone, project: project) }
let!(:milestone) { create(:milestone, project: project) }
before { project.team << [user, :developer] }
@@ -20,6 +21,24 @@ describe API::API, api: true do
get api("/projects/#{project.id}/milestones")
expect(response.status).to eq(401)
end
+
+ it 'returns an array of active milestones' do
+ get api("/projects/#{project.id}/milestones?state=active", user)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(milestone.id)
+ end
+
+ it 'returns an array of closed milestones' do
+ get api("/projects/#{project.id}/milestones?state=closed", user)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(closed_milestone.id)
+ end
end
describe 'GET /projects/:id/milestones/:milestone_id' do
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index a5d4985dc78..be2034e0f39 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -948,6 +948,78 @@ describe API::API, api: true do
end
end
+ describe 'POST /projects/:id/archive' do
+ context 'on an unarchived project' do
+ it 'archives the project' do
+ post api("/projects/#{project.id}/archive", user)
+
+ expect(response.status).to eq(201)
+ expect(json_response['archived']).to be_truthy
+ end
+ end
+
+ context 'on an archived project' do
+ before do
+ project.archive!
+ end
+
+ it 'remains archived' do
+ post api("/projects/#{project.id}/archive", user)
+
+ expect(response.status).to eq(201)
+ expect(json_response['archived']).to be_truthy
+ end
+ end
+
+ context 'user without archiving rights to the project' do
+ before do
+ project.team << [user3, :developer]
+ end
+
+ it 'rejects the action' do
+ post api("/projects/#{project.id}/archive", user3)
+
+ expect(response.status).to eq(403)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/unarchive' do
+ context 'on an unarchived project' do
+ it 'remains unarchived' do
+ post api("/projects/#{project.id}/unarchive", user)
+
+ expect(response.status).to eq(201)
+ expect(json_response['archived']).to be_falsey
+ end
+ end
+
+ context 'on an archived project' do
+ before do
+ project.archive!
+ end
+
+ it 'unarchives the project' do
+ post api("/projects/#{project.id}/unarchive", user)
+
+ expect(response.status).to eq(201)
+ expect(json_response['archived']).to be_falsey
+ end
+ end
+
+ context 'user without archiving rights to the project' do
+ before do
+ project.team << [user3, :developer]
+ end
+
+ it 'rejects the action' do
+ post api("/projects/#{project.id}/unarchive", user3)
+
+ expect(response.status).to eq(403)
+ end
+ end
+ end
+
describe 'DELETE /projects/:id' do
context 'when authenticated as user' do
it 'should remove project' do
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index 8490a729e51..b40a5c1c818 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -159,18 +159,44 @@ describe GitPushService, services: true do
end
describe "Updates main language" do
-
context "before push" do
it { expect(project.main_language).to eq(nil) }
end
context "after push" do
- before do
- @service = execute_service(project, user, @oldrev, @newrev, @ref)
+ def execute
+ execute_service(project, user, @oldrev, @newrev, ref)
end
- it { expect(@service.update_main_language).to eq(true) }
- it { expect(project.main_language).to eq("Ruby") }
+ context "to master" do
+ let(:ref) { @ref }
+
+ context 'when main_language is nil' do
+ it 'obtains the language from the repository' do
+ expect(project.repository).to receive(:main_language)
+ execute
+ end
+
+ it 'sets the project main language' do
+ execute
+ expect(project.main_language).to eq("Ruby")
+ end
+ end
+
+ context 'when main_language is already set' do
+ it 'does not check the repository' do
+ execute # do an initial run to simulate lang being preset
+ expect(project.repository).not_to receive(:main_language)
+ execute
+ end
+ end
+ end
+
+ context "to other branch" do
+ let(:ref) { 'refs/heads/feature/branch' }
+
+ it { expect(project.main_language).to eq(nil) }
+ end
end
end
diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb
index 9b0c73aaf37..2a5e4ac3ec4 100644
--- a/spec/services/issues/move_service_spec.rb
+++ b/spec/services/issues/move_service_spec.rb
@@ -160,6 +160,20 @@ describe Issues::MoveService, services: true do
.to eq "Note with reference to merge request #{old_project.to_reference}!1"
end
end
+
+ context 'issue description with uploads' do
+ let(:uploader) { build(:file_uploader, project: old_project) }
+ let(:description) { "Text and #{uploader.to_markdown}" }
+
+ include_context 'issue move executed'
+
+ it 'rewrites uploads in description' do
+ expect(new_issue.description).to_not eq description
+ expect(new_issue.description)
+ .to match(/Text and #{FileUploader::MARKDOWN_PATTERN}/)
+ expect(new_issue.description).to_not include uploader.secret
+ end
+ end
end
describe 'rewritting references' do
diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb
index 04f474c736c..32bf3acf483 100644
--- a/spec/services/projects/import_service_spec.rb
+++ b/spec/services/projects/import_service_spec.rb
@@ -72,6 +72,23 @@ describe Projects::ImportService, services: true do
expect(result[:status]).to eq :success
end
+ it 'flushes various caches' do
+ expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).
+ with(project.path_with_namespace, project.import_url).
+ and_return(true)
+
+ expect_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).
+ and_return(true)
+
+ expect_any_instance_of(Repository).to receive(:expire_emptiness_caches).
+ and_call_original
+
+ expect_any_instance_of(Repository).to receive(:expire_exists_cache).
+ and_call_original
+
+ subject.execute
+ end
+
it 'fails if importer fails' do
expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.path_with_namespace, project.import_url).and_return(true)
expect_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).and_return(false)
diff --git a/spec/services/projects/unlink_fork_service_spec.rb b/spec/services/projects/unlink_fork_service_spec.rb
new file mode 100644
index 00000000000..23f5555d3e0
--- /dev/null
+++ b/spec/services/projects/unlink_fork_service_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe Projects::UnlinkForkService, services: true do
+ subject { Projects::UnlinkForkService.new(fork_project, user) }
+
+ let(:fork_link) { create(:forked_project_link) }
+ let(:fork_project) { fork_link.forked_to_project }
+ let(:user) { create(:user) }
+
+ context 'with opened merge request on the source project' do
+ let(:merge_request) { create(:merge_request, source_project: fork_project, target_project: fork_link.forked_from_project) }
+ let(:mr_close_service) { MergeRequests::CloseService.new(fork_project, user) }
+
+ before do
+ allow(MergeRequests::CloseService).to receive(:new).
+ with(fork_project, user).
+ and_return(mr_close_service)
+ end
+
+ it 'close all pending merge requests' do
+ expect(mr_close_service).to receive(:execute).with(merge_request)
+
+ subject.execute
+ end
+ end
+
+ it 'remove fork relation' do
+ expect(fork_project.forked_project_link).to receive(:destroy)
+
+ subject.execute
+ end
+end
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index b4728807b8b..82b7fbfa816 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -2,22 +2,25 @@ require 'spec_helper'
describe TodoService, services: true do
let(:author) { create(:user) }
- let(:john_doe) { create(:user, username: 'john_doe') }
- let(:michael) { create(:user, username: 'michael') }
- let(:stranger) { create(:user, username: 'stranger') }
+ let(:assignee) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:member) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:john_doe) { create(:user) }
let(:project) { create(:project) }
- let(:mentions) { [author.to_reference, john_doe.to_reference, michael.to_reference, stranger.to_reference].join(' ') }
+ let(:mentions) { [author, assignee, john_doe, member, non_member, admin].map(&:to_reference).join(' ') }
let(:service) { described_class.new }
before do
project.team << [author, :developer]
+ project.team << [member, :developer]
project.team << [john_doe, :developer]
- project.team << [michael, :developer]
end
describe 'Issues' do
let(:issue) { create(:issue, project: project, assignee: john_doe, author: author, description: mentions) }
let(:unassigned_issue) { create(:issue, project: project, assignee: nil) }
+ let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee, description: mentions) }
describe '#new_issue' do
it 'creates a todo if assigned' do
@@ -37,10 +40,20 @@ describe TodoService, services: true do
it 'creates a todo for each valid mentioned user' do
service.new_issue(issue, author)
- should_create_todo(user: michael, target: issue, action: Todo::MENTIONED)
+ should_create_todo(user: member, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: author, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: john_doe, target: issue, action: Todo::MENTIONED)
- should_not_create_todo(user: stranger, target: issue, action: Todo::MENTIONED)
+ should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED)
+ end
+
+ it 'does not create todo for non project members when issue is confidential' do
+ service.new_issue(confidential_issue, john_doe)
+
+ should_create_todo(user: assignee, target: confidential_issue, author: john_doe, action: Todo::ASSIGNED)
+ should_create_todo(user: author, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
+ should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
+ should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
+ should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
end
end
@@ -48,16 +61,26 @@ describe TodoService, services: true do
it 'creates a todo for each valid mentioned user' do
service.update_issue(issue, author)
- should_create_todo(user: michael, target: issue, action: Todo::MENTIONED)
+ should_create_todo(user: member, target: issue, action: Todo::MENTIONED)
should_create_todo(user: john_doe, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: author, target: issue, action: Todo::MENTIONED)
- should_not_create_todo(user: stranger, target: issue, action: Todo::MENTIONED)
+ should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED)
end
it 'does not create a todo if user was already mentioned' do
- create(:todo, :mentioned, user: michael, project: project, target: issue, author: author)
+ create(:todo, :mentioned, user: member, project: project, target: issue, author: author)
- expect { service.update_issue(issue, author) }.not_to change(michael.todos, :count)
+ expect { service.update_issue(issue, author) }.not_to change(member.todos, :count)
+ end
+
+ it 'does not create todo for non project members when issue is confidential' do
+ service.update_issue(confidential_issue, john_doe)
+
+ should_create_todo(user: author, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
+ should_create_todo(user: assignee, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
+ should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
+ should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
+ should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
end
end
@@ -109,8 +132,10 @@ describe TodoService, services: true do
describe '#new_note' do
let!(:first_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
+ let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) }
let(:note) { create(:note, project: project, noteable: issue, author: john_doe, note: mentions) }
let(:note_on_commit) { create(:note_on_commit, project: project, author: john_doe, note: mentions) }
+ let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project, note: mentions) }
let(:note_on_project_snippet) { create(:note_on_project_snippet, project: project, author: john_doe, note: mentions) }
let(:award_note) { create(:note, :award, project: project, noteable: issue, author: john_doe, note: 'thumbsup') }
let(:system_note) { create(:system_note, project: project, noteable: issue) }
@@ -142,19 +167,29 @@ describe TodoService, services: true do
it 'creates a todo for each valid mentioned user' do
service.new_note(note, john_doe)
- should_create_todo(user: michael, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
+ should_create_todo(user: member, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
should_create_todo(user: author, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
should_not_create_todo(user: john_doe, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
- should_not_create_todo(user: stranger, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
+ should_not_create_todo(user: non_member, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
+ end
+
+ it 'does not create todo for non project members when leaving a note on a confidential issue' do
+ service.new_note(note_on_confidential_issue, john_doe)
+
+ should_create_todo(user: author, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
+ should_create_todo(user: assignee, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
+ should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
+ should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
+ should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
end
it 'creates a todo for each valid mentioned user when leaving a note on commit' do
service.new_note(note_on_commit, john_doe)
- should_create_todo(user: michael, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
+ should_create_todo(user: member, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
should_create_todo(user: author, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
should_not_create_todo(user: john_doe, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
- should_not_create_todo(user: stranger, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
+ should_not_create_todo(user: non_member, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
end
it 'does not create todo when leaving a note on snippet' do
@@ -185,10 +220,10 @@ describe TodoService, services: true do
it 'creates a todo for each valid mentioned user' do
service.new_merge_request(mr_assigned, author)
- should_create_todo(user: michael, target: mr_assigned, action: Todo::MENTIONED)
+ should_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED)
- should_not_create_todo(user: stranger, target: mr_assigned, action: Todo::MENTIONED)
+ should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED)
end
end
@@ -196,16 +231,16 @@ describe TodoService, services: true do
it 'creates a todo for each valid mentioned user' do
service.update_merge_request(mr_assigned, author)
- should_create_todo(user: michael, target: mr_assigned, action: Todo::MENTIONED)
+ should_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED)
should_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED)
- should_not_create_todo(user: stranger, target: mr_assigned, action: Todo::MENTIONED)
+ should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED)
end
it 'does not create a todo if user was already mentioned' do
- create(:todo, :mentioned, user: michael, project: project, target: mr_assigned, author: author)
+ create(:todo, :mentioned, user: member, project: project, target: mr_assigned, author: author)
- expect { service.update_merge_request(mr_assigned, author) }.not_to change(michael.todos, :count)
+ expect { service.update_merge_request(mr_assigned, author) }.not_to change(member.todos, :count)
end
end
diff --git a/spec/support/carrierwave.rb b/spec/support/carrierwave.rb
new file mode 100644
index 00000000000..aa89afd8fb3
--- /dev/null
+++ b/spec/support/carrierwave.rb
@@ -0,0 +1,7 @@
+CarrierWave.root = 'tmp/tests/uploads'
+
+RSpec.configure do |config|
+ config.after(:suite) do
+ FileUtils.rm_rf('tmp/tests/uploads')
+ end
+end
diff --git a/spec/support/filter_spec_helper.rb b/spec/support/filter_spec_helper.rb
index ef5ea7d626e..e849a9633b9 100644
--- a/spec/support/filter_spec_helper.rb
+++ b/spec/support/filter_spec_helper.rb
@@ -78,6 +78,6 @@ module FilterSpecHelper
# Shortcut to Rails' auto-generated routes helpers, to avoid including the
# module
def urls
- Gitlab::Application.routes.url_helpers
+ Gitlab::Routing.url_helpers
end
end
diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb
index 73c6792b65f..b87cd6bbca2 100644
--- a/spec/support/markdown_feature.rb
+++ b/spec/support/markdown_feature.rb
@@ -106,7 +106,7 @@ class MarkdownFeature
end
def urls
- Gitlab::Application.routes.url_helpers
+ Gitlab::Routing.url_helpers
end
def raw_markdown
diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb
index 1d52489e804..43cb6ef43f2 100644
--- a/spec/support/matchers/markdown_matchers.rb
+++ b/spec/support/matchers/markdown_matchers.rb
@@ -13,7 +13,7 @@ module MarkdownMatchers
set_default_markdown_messages
match do |actual|
- link = actual.at_css('a:contains("Relative Link")')
+ link = actual.at_css('a:contains("Relative Link")')
image = actual.at_css('img[alt="Relative Image"]')
expect(link['href']).to end_with('master/doc/README.md')
@@ -72,14 +72,15 @@ module MarkdownMatchers
have_css("img[src$='#{src}']")
end
+ prefix = '/namespace1/gitlabhq/wikis'
set_default_markdown_messages
match do |actual|
- expect(actual).to have_link('linked-resource', href: 'linked-resource')
- expect(actual).to have_link('link-text', href: 'linked-resource')
+ expect(actual).to have_link('linked-resource', href: "#{prefix}/linked-resource")
+ expect(actual).to have_link('link-text', href: "#{prefix}/linked-resource")
expect(actual).to have_link('http://example.com', href: 'http://example.com')
expect(actual).to have_link('link-text', href: 'http://example.com/pdfs/gollum.pdf')
- expect(actual).to have_image('/gitlabhq/wikis/images/example.jpg')
+ expect(actual).to have_image("#{prefix}/images/example.jpg")
expect(actual).to have_image('http://example.com/images/example.jpg')
end
end
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index 0d1bd030f3c..71664bb192e 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -15,6 +15,7 @@ module TestEnv
'lfs' => 'be93687',
'master' => '5937ac0',
"'test'" => 'e56497b',
+ 'orphaned-branch' => '45127a9',
}
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index 320be9a0b61..05fc4c4554f 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -7,8 +7,12 @@ describe 'gitlab:app namespace rake task' do
Rake.application.rake_require 'tasks/gitlab/backup'
Rake.application.rake_require 'tasks/gitlab/shell'
Rake.application.rake_require 'tasks/gitlab/db'
+
# empty task as env is already loaded
Rake::Task.define_task :environment
+
+ # We need this directory to run `gitlab:backup:create` task
+ FileUtils.mkdir_p('public/uploads')
end
def run_rake_task(task_name)
diff --git a/spec/workers/merge_worker_spec.rb b/spec/workers/merge_worker_spec.rb
index b11c5de94e3..1abd87d7d33 100644
--- a/spec/workers/merge_worker_spec.rb
+++ b/spec/workers/merge_worker_spec.rb
@@ -22,6 +22,8 @@ describe MergeWorker do
merge_request.reload
expect(merge_request).to be_merged
+
+ source_project.repository.expire_branches_cache
expect(source_project.repository.branch_names).not_to include('markdown')
end
end
diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb
new file mode 100644
index 00000000000..7e59bd2fced
--- /dev/null
+++ b/spec/workers/project_cache_worker_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe ProjectCacheWorker do
+ let(:project) { create(:project) }
+
+ subject { described_class.new }
+
+ describe '#perform' do
+ it 'updates project cache data' do
+
+ expect_any_instance_of(Repository).to receive(:size)
+ expect_any_instance_of(Repository).to receive(:commit_count)
+
+ expect_any_instance_of(Project).to receive(:update_repository_size)
+ expect_any_instance_of(Project).to receive(:update_commit_count)
+
+ subject.perform(project.id)
+ end
+
+ it 'handles missing repository data' do
+ expect_any_instance_of(Repository).to receive(:exists?).and_return(false)
+ expect_any_instance_of(Repository).not_to receive(:size)
+
+ subject.perform(project.id)
+ end
+ end
+end