diff options
author | Phil Hughes <me@iamphill.com> | 2017-05-03 17:04:47 +0100 |
---|---|---|
committer | Phil Hughes <me@iamphill.com> | 2017-05-03 17:04:47 +0100 |
commit | f29e5d4d93123ea73e5f58be46020781af2ef7cb (patch) | |
tree | d08a8f8e5a9cf30dd4f238c187673ffe30ac39b3 | |
parent | 4134d700623404948f163349882caf4a6d940cf3 (diff) | |
parent | 8f29bf96b9306fd95b9b98019e80173b47b3a6b8 (diff) | |
download | gitlab-ce-f29e5d4d93123ea73e5f58be46020781af2ef7cb.tar.gz |
Merge branch 'master' into deploy-keys-load-async
472 files changed, 8995 insertions, 2184 deletions
diff --git a/.gitignore b/.gitignore index dfc99a4ee48..bb818213de1 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ eslint-report.html /public/uploads.* /public/uploads/ /shared/artifacts/ +/spec/javascripts/fixtures/blob/pdf/ /rails_best_practices_output.html /tags /tmp/* diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e911d7e5b89..aa62a86d31d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -19,8 +19,8 @@ variables: before_script: - bundle --version - - . scripts/utils.sh - - ./scripts/prepare_build.sh + - source scripts/utils.sh + - source scripts/prepare_build.sh stages: - prepare @@ -253,38 +253,46 @@ spinach mysql 9 10: *spinach-knapsack-mysql SETUP_DB: "false" USE_BUNDLE_INSTALL: "true" -.exec: &exec +.rake-exec: &rake-exec <<: *ruby-static-analysis <<: *dedicated-runner <<: *except-docs stage: test script: - - bundle exec $CI_JOB_NAME + - bundle exec rake $CI_JOB_NAME -rubocop: +static-analysis: <<: *ruby-static-analysis <<: *dedicated-runner - <<: *except-docs stage: test script: - - bundle exec "rubocop --require rubocop-rspec" - -rake haml_lint: *exec -rake scss_lint: *exec -rake config_lint: *exec -rake brakeman: *exec -rake flay: *exec -license_finder: *exec -rake downtime_check: - <<: *exec + - scripts/static-analysis + +docs:check:links: + image: "registry.gitlab.com/gitlab-org/gitlab-build-images:nanoc-bootstrap-ruby-2.4-alpine" + stage: test + <<: *dedicated-runner + cache: {} + dependencies: [] + before_script: [] + script: + - mv doc/ /nanoc/content/ + - cd /nanoc + # Build HTML from Markdown + - bundle exec nanoc + # Check the internal links + - bundle exec nanoc check internal_links + +downtime_check: + <<: *rake-exec except: - master - tags - /^[\d-]+-stable(-ee)?$/ - /^docs\/*/ -rake ee_compat_check: - <<: *exec +ee_compat_check: + <<: *rake-exec only: - branches@gitlab-org/gitlab-ce except: @@ -306,22 +314,22 @@ rake ee_compat_check: .db-migrate-reset: &db-migrate-reset stage: test <<: *dedicated-runner + <<: *except-docs script: - bundle exec rake db:migrate:reset rake pg db:migrate:reset: <<: *db-migrate-reset <<: *use-pg - <<: *except-docs rake mysql db:migrate:reset: <<: *db-migrate-reset <<: *use-mysql - <<: *except-docs .db-rollback: &db-rollback stage: test <<: *dedicated-runner + <<: *except-docs script: - bundle exec rake db:rollback STEP=120 - bundle exec rake db:migrate @@ -329,16 +337,15 @@ rake mysql db:migrate:reset: rake pg db:rollback: <<: *db-rollback <<: *use-pg - <<: *except-docs rake mysql db:rollback: <<: *db-rollback <<: *use-mysql - <<: *except-docs .db-seed_fu: &db-seed_fu stage: test <<: *dedicated-runner + <<: *except-docs variables: SIZE: "1" SETUP_DB: "false" @@ -356,12 +363,10 @@ rake mysql db:rollback: rake pg db:seed_fu: <<: *db-seed_fu <<: *use-pg - <<: *except-docs rake mysql db:seed_fu: <<: *db-seed_fu <<: *use-mysql - <<: *except-docs rake gitlab:assets:compile: stage: test @@ -402,31 +407,6 @@ rake karma: paths: - coverage-javascript/ -docs:check:apilint: - image: "phusion/baseimage" - stage: test - <<: *dedicated-runner - cache: {} - dependencies: [] - before_script: [] - script: - - scripts/lint-doc.sh - -docs:check:links: - image: "registry.gitlab.com/gitlab-org/gitlab-build-images:nanoc-bootstrap-ruby-2.4-alpine" - stage: test - <<: *dedicated-runner - cache: {} - dependencies: [] - before_script: [] - script: - - mv doc/ /nanoc/content/ - - cd /nanoc - # Build HTML from Markdown - - bundle exec nanoc - # Check the internal links - - bundle exec nanoc check internal_links - bundler:audit: stage: test <<: *ruby-static-analysis @@ -485,14 +465,6 @@ coverage: - coverage/index.html - coverage/assets/ -lint:javascript: - <<: *dedicated-runner - <<: *except-docs - stage: test - before_script: [] - script: - - yarn run eslint - lint:javascript:report: <<: *dedicated-runner <<: *except-docs @@ -526,22 +498,6 @@ trigger_docs: - master@gitlab-org/gitlab-ce - master@gitlab-org/gitlab-ee -# Notify slack in the end -notify:slack: - stage: post-test - <<: *dedicated-runner - variables: - SETUP_DB: "false" - USE_BUNDLE_INSTALL: "false" - script: - - ./scripts/notify_slack.sh "#development" "Build on \`$CI_COMMIT_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_COMMIT_SHA"/pipelines>" - when: on_failure - only: - - master@gitlab-org/gitlab-ce - - tags@gitlab-org/gitlab-ce - - master@gitlab-org/gitlab-ee - - tags@gitlab-org/gitlab-ee - pages: before_script: [] stage: pages diff --git a/.rubocop.yml b/.rubocop.yml index e73500be2a9..e53af97a92c 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -961,7 +961,7 @@ RSpec/DescribeSymbol: # Checks that the second argument to top level describe is the tested method # name. RSpec/DescribedClass: - Enabled: false + Enabled: true # Checks for long example. RSpec/ExampleLength: @@ -983,10 +983,12 @@ RSpec/ExpectActual: # Checks the file and folder naming of the spec file. RSpec/FilePath: - Enabled: false - CustomTransform: - RuboCop: rubocop - RSpec: rspec + Enabled: true + IgnoreMethods: true + Exclude: + - 'qa/**/*' + - 'spec/javascripts/fixtures/*' + - 'spec/requests/api/v3/*' # Checks if there are focused specs. RSpec/Focus: diff --git a/CHANGELOG.md b/CHANGELOG.md index a47c43dd5d6..2686d778b09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 9.1.2 (2017-05-01) + +- Add index on ci_runners.contacted_at. !10876 (blackst0ne) +- Fix pipeline events description for Slack and Mattermost integration. !10908 +- Fixed milestone sidebar showing incorrect number of MRs when collapsed. !10933 +- Fix ordering of commits in the network graph. !10936 +- Ensure the chat notifications service properly saves the "Notify only default branch" setting. !10959 +- Lazily sets UUID in ApplicationSetting for new installations. +- Skip validation when creating internal (ghost, service desk) users. +- Use GitLab Pages v0.4.1. + ## 9.1.1 (2017-04-26) - Add a transaction around move_issues_to_ghost_user. !10465 diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index a918a2aa18d..a3df0a6959e 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.6.0 +0.8.0 diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION index 1d0ba9ea182..267577d47e4 100644 --- a/GITLAB_PAGES_VERSION +++ b/GITLAB_PAGES_VERSION @@ -1 +1 @@ -0.4.0 +0.4.1 diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 428b770e3e2..227cea21564 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -1.4.3 +2.0.0 @@ -85,14 +85,14 @@ gem 'kaminari', '~> 0.17.0' gem 'hamlit', '~> 2.6.1' # Files attachments -gem 'carrierwave', '~> 0.11.0' +gem 'carrierwave', '~> 1.0' # Drag and Drop UI gem 'dropzonejs-rails', '~> 0.7.1' # for backups gem 'fog-aws', '~> 0.9' -gem 'fog-core', '~> 1.40' +gem 'fog-core', '~> 1.44' gem 'fog-google', '~> 0.5' gem 'fog-local', '~> 0.3' gem 'fog-openstack', '~> 0.1' diff --git a/Gemfile.lock b/Gemfile.lock index 52707628748..b822a325861 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -105,12 +105,10 @@ GEM capybara-screenshot (1.0.14) capybara (>= 1.0, < 3) launchy - carrierwave (0.11.2) - activemodel (>= 3.2.0) - activesupport (>= 3.2.0) - json (>= 1.7) + carrierwave (1.0.0) + activemodel (>= 4.0.0) + activesupport (>= 4.0.0) mime-types (>= 1.16) - mimemagic (>= 0.3.0) cause (0.1) charlock_holmes (0.7.3) chronic (0.10.2) @@ -184,7 +182,7 @@ GEM erubis (2.7.0) escape_utils (1.1.1) eventmachine (1.0.8) - excon (0.52.0) + excon (0.55.0) execjs (2.6.0) expression_parser (0.9.0) extlib (0.9.16) @@ -210,12 +208,12 @@ GEM flowdock (0.7.1) httparty (~> 0.7) multi_json - fog-aws (0.11.0) + fog-aws (0.13.0) fog-core (~> 1.38) fog-json (~> 1.0) fog-xml (~> 0.1) ipaddress (~> 0.8) - fog-core (1.42.0) + fog-core (1.44.1) builder excon (~> 0.49) formatador (~> 0.2) @@ -237,9 +235,9 @@ GEM fog-json (>= 1.0) fog-xml (>= 0.1) ipaddress (>= 0.8) - fog-xml (0.1.2) + fog-xml (0.1.3) fog-core - nokogiri (~> 1.5, >= 1.5.11) + nokogiri (>= 1.5.11, < 2.0.0) font-awesome-rails (4.7.0.1) railties (>= 3.2, < 5.1) foreman (0.78.0) @@ -330,7 +328,7 @@ GEM grape-entity (0.6.0) activesupport multi_json (>= 1.3.2) - grpc (1.1.2) + grpc (1.2.5) google-protobuf (~> 3.1) googleauth (~> 0.5.1) haml (4.0.7) @@ -871,7 +869,7 @@ DEPENDENCIES bundler-audit (~> 0.5.0) capybara (~> 2.6.2) capybara-screenshot (~> 1.0.0) - carrierwave (~> 0.11.0) + carrierwave (~> 1.0) charlock_holmes (~> 0.7.3) chronic (~> 0.10.2) chronic_duration (~> 0.10.6) @@ -896,7 +894,7 @@ DEPENDENCIES ffaker (~> 2.4) flay (~> 2.8.0) fog-aws (~> 0.9) - fog-core (~> 1.40) + fog-core (~> 1.44) fog-google (~> 0.5) fog-local (~> 0.3) fog-openstack (~> 0.1) diff --git a/app/assets/images/ci_favicons/dev/favicon_status_canceled.ico b/app/assets/images/ci_favicons/dev/favicon_status_canceled.ico Binary files differnew file mode 100644 index 00000000000..4af3582b60d --- /dev/null +++ b/app/assets/images/ci_favicons/dev/favicon_status_canceled.ico diff --git a/app/assets/images/ci_favicons/dev/favicon_status_created.ico b/app/assets/images/ci_favicons/dev/favicon_status_created.ico Binary files differnew file mode 100644 index 00000000000..13639da2e8a --- /dev/null +++ b/app/assets/images/ci_favicons/dev/favicon_status_created.ico diff --git a/app/assets/images/ci_favicons/dev/favicon_status_failed.ico b/app/assets/images/ci_favicons/dev/favicon_status_failed.ico Binary files differnew file mode 100644 index 00000000000..5f0e711b104 --- /dev/null +++ b/app/assets/images/ci_favicons/dev/favicon_status_failed.ico diff --git a/app/assets/images/ci_favicons/dev/favicon_status_manual.ico b/app/assets/images/ci_favicons/dev/favicon_status_manual.ico Binary files differnew file mode 100644 index 00000000000..8b1168a1267 --- /dev/null +++ b/app/assets/images/ci_favicons/dev/favicon_status_manual.ico diff --git a/app/assets/images/ci_favicons/dev/favicon_status_not_found.ico b/app/assets/images/ci_favicons/dev/favicon_status_not_found.ico Binary files differnew file mode 100644 index 00000000000..ed19b69e1c5 --- /dev/null +++ b/app/assets/images/ci_favicons/dev/favicon_status_not_found.ico diff --git a/app/assets/images/ci_favicons/dev/favicon_status_pending.ico b/app/assets/images/ci_favicons/dev/favicon_status_pending.ico Binary files differnew file mode 100644 index 00000000000..5dfefd4cc5a --- /dev/null +++ b/app/assets/images/ci_favicons/dev/favicon_status_pending.ico diff --git a/app/assets/images/ci_favicons/dev/favicon_status_running.ico b/app/assets/images/ci_favicons/dev/favicon_status_running.ico Binary files differnew file mode 100644 index 00000000000..a41539c0e3e --- /dev/null +++ b/app/assets/images/ci_favicons/dev/favicon_status_running.ico diff --git a/app/assets/images/ci_favicons/dev/favicon_status_skipped.ico b/app/assets/images/ci_favicons/dev/favicon_status_skipped.ico Binary files differnew file mode 100644 index 00000000000..2c1ae552b93 --- /dev/null +++ b/app/assets/images/ci_favicons/dev/favicon_status_skipped.ico diff --git a/app/assets/images/ci_favicons/dev/favicon_status_success.ico b/app/assets/images/ci_favicons/dev/favicon_status_success.ico Binary files differnew file mode 100644 index 00000000000..70f0ca61eca --- /dev/null +++ b/app/assets/images/ci_favicons/dev/favicon_status_success.ico diff --git a/app/assets/images/ci_favicons/dev/favicon_status_warning.ico b/app/assets/images/ci_favicons/dev/favicon_status_warning.ico Binary files differnew file mode 100644 index 00000000000..db289e03eb1 --- /dev/null +++ b/app/assets/images/ci_favicons/dev/favicon_status_warning.ico diff --git a/app/assets/images/ci_favicons/favicon_status_canceled.ico b/app/assets/images/ci_favicons/favicon_status_canceled.ico Binary files differindex 5a19458f2a2..23adcffff50 100755..100644 --- a/app/assets/images/ci_favicons/favicon_status_canceled.ico +++ b/app/assets/images/ci_favicons/favicon_status_canceled.ico diff --git a/app/assets/images/ci_favicons/favicon_status_created.ico b/app/assets/images/ci_favicons/favicon_status_created.ico Binary files differindex 4dca9640cb3..f9d93b390d8 100755..100644 --- a/app/assets/images/ci_favicons/favicon_status_created.ico +++ b/app/assets/images/ci_favicons/favicon_status_created.ico diff --git a/app/assets/images/ci_favicons/favicon_status_failed.ico b/app/assets/images/ci_favicons/favicon_status_failed.ico Binary files differindex c961ff9a69b..28a22ebf724 100755..100644 --- a/app/assets/images/ci_favicons/favicon_status_failed.ico +++ b/app/assets/images/ci_favicons/favicon_status_failed.ico diff --git a/app/assets/images/ci_favicons/favicon_status_manual.ico b/app/assets/images/ci_favicons/favicon_status_manual.ico Binary files differindex 5fbbc99ea7c..dbbf1abf30c 100755..100644 --- a/app/assets/images/ci_favicons/favicon_status_manual.ico +++ b/app/assets/images/ci_favicons/favicon_status_manual.ico diff --git a/app/assets/images/ci_favicons/favicon_status_not_found.ico b/app/assets/images/ci_favicons/favicon_status_not_found.ico Binary files differindex 21afa9c72e6..49b9b232dd1 100755..100644 --- a/app/assets/images/ci_favicons/favicon_status_not_found.ico +++ b/app/assets/images/ci_favicons/favicon_status_not_found.ico diff --git a/app/assets/images/ci_favicons/favicon_status_pending.ico b/app/assets/images/ci_favicons/favicon_status_pending.ico Binary files differindex 8be32dab85a..05962f3f148 100755..100644 --- a/app/assets/images/ci_favicons/favicon_status_pending.ico +++ b/app/assets/images/ci_favicons/favicon_status_pending.ico diff --git a/app/assets/images/ci_favicons/favicon_status_running.ico b/app/assets/images/ci_favicons/favicon_status_running.ico Binary files differindex f328ff1a5ed..7fa3d4d48d4 100755..100644 --- a/app/assets/images/ci_favicons/favicon_status_running.ico +++ b/app/assets/images/ci_favicons/favicon_status_running.ico diff --git a/app/assets/images/ci_favicons/favicon_status_skipped.ico b/app/assets/images/ci_favicons/favicon_status_skipped.ico Binary files differindex b4394e1b4af..b0c26b62068 100755..100644 --- a/app/assets/images/ci_favicons/favicon_status_skipped.ico +++ b/app/assets/images/ci_favicons/favicon_status_skipped.ico diff --git a/app/assets/images/ci_favicons/favicon_status_success.ico b/app/assets/images/ci_favicons/favicon_status_success.ico Binary files differindex 4f436c95242..b150960b5be 100755..100644 --- a/app/assets/images/ci_favicons/favicon_status_success.ico +++ b/app/assets/images/ci_favicons/favicon_status_success.ico diff --git a/app/assets/images/ci_favicons/favicon_status_warning.ico b/app/assets/images/ci_favicons/favicon_status_warning.ico Binary files differindex 805cc20cdec..7e71d71684d 100755..100644 --- a/app/assets/images/ci_favicons/favicon_status_warning.ico +++ b/app/assets/images/ci_favicons/favicon_status_warning.ico diff --git a/app/assets/javascripts/behaviors/gl_emoji.js b/app/assets/javascripts/behaviors/gl_emoji.js index 19a607309e4..23d91fdb259 100644 --- a/app/assets/javascripts/behaviors/gl_emoji.js +++ b/app/assets/javascripts/behaviors/gl_emoji.js @@ -62,6 +62,7 @@ function glEmojiTag(inputName, options) { data-fallback-src="${fallbackImageSrc}" ${fallbackSpriteAttribute} data-unicode-version="${emojiInfo.unicodeVersion}" + title="${emojiInfo.description}" > ${contents} </gl-emoji> diff --git a/app/assets/javascripts/blob/pdf/index.js b/app/assets/javascripts/blob/pdf/index.js index 9161be98853..0ed915c1ac9 100644 --- a/app/assets/javascripts/blob/pdf/index.js +++ b/app/assets/javascripts/blob/pdf/index.js @@ -1,11 +1,6 @@ /* eslint-disable no-new */ import Vue from 'vue'; -import PDFLab from 'vendor/pdflab'; -import workerSrc from 'vendor/pdf.worker'; - -Vue.use(PDFLab, { - workerSrc, -}); +import pdfLab from '../../pdf/index.vue'; export default () => { const el = document.getElementById('js-pdf-viewer'); @@ -20,6 +15,9 @@ export default () => { pdf: el.dataset.endpoint, }; }, + components: { + pdfLab, + }, methods: { onLoad() { this.loading = false; diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js index 7efa8537298..07d67d49aa5 100644 --- a/app/assets/javascripts/blob/viewer/index.js +++ b/app/assets/javascripts/blob/viewer/index.js @@ -6,7 +6,7 @@ export default class BlobViewer { this.copySourceBtn = document.querySelector('.js-copy-blob-source-btn'); this.simpleViewer = document.querySelector('.blob-viewer[data-type="simple"]'); this.richViewer = document.querySelector('.blob-viewer[data-type="rich"]'); - this.$blobContentHolder = $('#blob-content-holder'); + this.$fileHolder = $('.file-holder'); let initialViewerName = document.querySelector('.blob-viewer:not(.hidden)').getAttribute('data-type'); @@ -82,7 +82,7 @@ export default class BlobViewer { viewer.setAttribute('data-loaded', 'true'); - this.$blobContentHolder.trigger('highlight:line'); + this.$fileHolder.trigger('highlight:line'); this.toggleCopyButtonState(); }); diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index 004bac09f59..f0066d4ec5d 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -27,6 +27,9 @@ gl.issueBoards.BoardSidebar = Vue.extend({ computed: { showSidebar () { return Object.keys(this.issue).length; + }, + assigneeId() { + return this.issue.assignee ? this.issue.assignee.id : 0; } }, watch: { diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index d3d75c4bf4a..0bdce52cc89 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -44,6 +44,7 @@ import GroupsList from './groups_list'; import ProjectsList from './projects_list'; import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater'; +import Landing from './landing'; import BlobForkSuggestion from './blob/blob_fork_suggestion'; import UserCallout from './user_callout'; import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags'; @@ -148,8 +149,19 @@ const ShortcutsBlob = require('./shortcuts_blob'); new ProjectsList(); break; case 'dashboard:groups:index': + new GroupsList(); + break; case 'explore:groups:index': new GroupsList(); + + const landingElement = document.querySelector('.js-explore-groups-landing'); + if (!landingElement) break; + const exploreGroupsLanding = new Landing( + landingElement, + landingElement.querySelector('.dismiss-button'), + 'explore_groups_landing_dismissed', + ); + exploreGroupsLanding.toggle(); break; case 'projects:milestones:new': case 'projects:milestones:edit': @@ -356,6 +368,10 @@ const ShortcutsBlob = require('./shortcuts_blob'); case 'users:show': new UserCallout(); break; + case 'snippets:show': + new LineHighlighter(); + new BlobViewer(); + break; } switch (path.first()) { case 'sessions': @@ -434,6 +450,8 @@ const ShortcutsBlob = require('./shortcuts_blob'); shortcut_handler = new ShortcutsNavigation(); if (path[2] === 'show') { new ZenMode(); + new LineHighlighter(); + new BlobViewer(); } break; case 'labels': diff --git a/app/assets/javascripts/landing.js b/app/assets/javascripts/landing.js new file mode 100644 index 00000000000..8c0950ad5d5 --- /dev/null +++ b/app/assets/javascripts/landing.js @@ -0,0 +1,37 @@ +import Cookies from 'js-cookie'; + +class Landing { + constructor(landingElement, dismissButton, cookieName) { + this.landingElement = landingElement; + this.cookieName = cookieName; + this.dismissButton = dismissButton; + this.eventWrapper = {}; + } + + toggle() { + const isDismissed = this.isDismissed(); + + this.landingElement.classList.toggle('hidden', isDismissed); + if (!isDismissed) this.addEvents(); + } + + addEvents() { + this.eventWrapper.dismissLanding = this.dismissLanding.bind(this); + this.dismissButton.addEventListener('click', this.eventWrapper.dismissLanding); + } + + removeEvents() { + this.dismissButton.removeEventListener('click', this.eventWrapper.dismissLanding); + } + + dismissLanding() { + this.landingElement.classList.add('hidden'); + Cookies.set(this.cookieName, 'true', { expires: 365 }); + } + + isDismissed() { + return Cookies.get(this.cookieName) === 'true'; + } +} + +export default Landing; diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js index a6f7bea99f5..3ac6dedf131 100644 --- a/app/assets/javascripts/line_highlighter.js +++ b/app/assets/javascripts/line_highlighter.js @@ -57,9 +57,9 @@ require('vendor/jquery.scrollTo'); } LineHighlighter.prototype.bindEvents = function() { - const $blobContentHolder = $('#blob-content-holder'); - $blobContentHolder.on('click', 'a[data-line-number]', this.clickHandler); - $blobContentHolder.on('highlight:line', this.highlightHash); + const $fileHolder = $('.file-holder'); + $fileHolder.on('click', 'a[data-line-number]', this.clickHandler); + $fileHolder.on('highlight:line', this.highlightHash); }; LineHighlighter.prototype.highlightHash = function() { diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js index 38c673e8907..841b24a60a3 100644 --- a/app/assets/javascripts/milestone.js +++ b/app/assets/javascripts/milestone.js @@ -19,12 +19,10 @@ }); }; - Milestone.sortIssues = function(data) { - var sort_issues_url; - sort_issues_url = location.href + "/sort_issues"; + Milestone.sortIssues = function(url, data) { return $.ajax({ type: "PUT", - url: sort_issues_url, + url, data: data, success: function(_data) { return Milestone.successCallback(_data); @@ -36,12 +34,10 @@ }); }; - Milestone.sortMergeRequests = function(data) { - var sort_mr_url; - sort_mr_url = location.href + "/sort_merge_requests"; + Milestone.sortMergeRequests = function(url, data) { return $.ajax({ type: "PUT", - url: sort_mr_url, + url, data: data, success: function(_data) { return Milestone.successCallback(_data); @@ -81,42 +77,55 @@ }; function Milestone() { - var oldMouseStart; + this.issuesSortEndpoint = $('#tab-issues').data('sort-endpoint'); + this.mergeRequestsSortEndpoint = $('#tab-merge-requests').data('sort-endpoint'); + this.bindIssuesSorting(); - this.bindMergeRequestSorting(); this.bindTabsSwitching(); + + // Load merge request tab if it is active + // merge request tab is active based on different conditions in the backend + this.loadTab($('.js-milestone-tabs .active a')); + + this.loadInitialTab(); } Milestone.prototype.bindIssuesSorting = function() { + if (!this.issuesSortEndpoint) return; + $('#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed').each(function (i, el) { this.createSortable(el, { group: 'issue-list', listEls: $('.issues-sortable-list'), fieldName: 'issue', - sortCallback: Milestone.sortIssues, + sortCallback: (data) => { + Milestone.sortIssues(this.issuesSortEndpoint, data); + }, updateCallback: Milestone.updateIssue, }); }.bind(this)); }; Milestone.prototype.bindTabsSwitching = function() { - return $('a[data-toggle="tab"]').on('show.bs.tab', function(e) { - var currentTabClass, previousTabClass; - currentTabClass = $(e.target).data('show'); - previousTabClass = $(e.relatedTarget).data('show'); - $(previousTabClass).hide(); - $(currentTabClass).removeClass('hidden'); - return $(currentTabClass).show(); + return $('a[data-toggle="tab"]').on('show.bs.tab', (e) => { + const $target = $(e.target); + + location.hash = $target.attr('href'); + this.loadTab($target); }); }; Milestone.prototype.bindMergeRequestSorting = function() { + if (!this.mergeRequestsSortEndpoint) return; + $("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").each(function (i, el) { this.createSortable(el, { group: 'merge-request-list', listEls: $(".merge_requests-sortable-list:not(#merge_requests-list-merged)"), fieldName: 'merge_request', - sortCallback: Milestone.sortMergeRequests, + sortCallback: (data) => { + Milestone.sortMergeRequests(this.mergeRequestsSortEndpoint, data); + }, updateCallback: Milestone.updateMergeRequest, }); }.bind(this)); @@ -169,6 +178,35 @@ }); }; + Milestone.prototype.loadInitialTab = function() { + const $target = $(`.js-milestone-tabs a[href="${location.hash}"]`); + + if ($target.length) { + $target.tab('show'); + } + }; + + Milestone.prototype.loadTab = function($target) { + const endpoint = $target.data('endpoint'); + const tabElId = $target.attr('href'); + + if (endpoint && !$target.hasClass('is-loaded')) { + $.ajax({ + url: endpoint, + dataType: 'JSON', + }) + .fail(() => new Flash('Error loading milestone tab')) + .done((data) => { + $(tabElId).html(data.html); + $target.addClass('is-loaded'); + + if (tabElId === '#tab-merge-requests') { + this.bindMergeRequestSorting(); + } + }); + } + }; + return Milestone; })(); }).call(window); diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js new file mode 100644 index 00000000000..c3a8da52404 --- /dev/null +++ b/app/assets/javascripts/monitoring/constants.js @@ -0,0 +1,4 @@ +import d3 from 'd3'; + +export const dateFormat = d3.time.format('%b %d, %Y'); +export const timeFormat = d3.time.format('%H:%M%p'); diff --git a/app/assets/javascripts/monitoring/deployments.js b/app/assets/javascripts/monitoring/deployments.js new file mode 100644 index 00000000000..fc92ab61b31 --- /dev/null +++ b/app/assets/javascripts/monitoring/deployments.js @@ -0,0 +1,211 @@ +/* global Flash */ +import d3 from 'd3'; +import { + dateFormat, + timeFormat, +} from './constants'; + +export default class Deployments { + constructor(width, height) { + this.width = width; + this.height = height; + + this.endpoint = document.getElementById('js-metrics').dataset.deploymentEndpoint; + + this.createGradientDef(); + } + + init(chartData) { + this.chartData = chartData; + + this.x = d3.time.scale().range([0, this.width]); + this.x.domain(d3.extent(this.chartData, d => d.time)); + + this.charts = d3.selectAll('.prometheus-graph'); + + this.getData(); + } + + getData() { + $.ajax({ + url: this.endpoint, + dataType: 'JSON', + }) + .fail(() => new Flash('Error getting deployment information.')) + .done((data) => { + this.data = data.deployments.reduce((deploymentDataArray, deployment) => { + const time = new Date(deployment.created_at); + const xPos = Math.floor(this.x(time)); + + time.setSeconds(this.chartData[0].time.getSeconds()); + + if (xPos >= 0) { + deploymentDataArray.push({ + id: deployment.id, + time, + sha: deployment.sha, + tag: deployment.tag, + ref: deployment.ref.name, + xPos, + }); + } + + return deploymentDataArray; + }, []); + + this.plotData(); + }); + } + + plotData() { + this.charts.each((d, i) => { + const svg = d3.select(this.charts[0][i]); + const chart = svg.select('.graph-container'); + const key = svg.node().getAttribute('graph-type'); + + this.createLine(chart, key); + this.createDeployInfoBox(chart, key); + }); + } + + createGradientDef() { + const defs = d3.select('body') + .append('svg') + .attr({ + height: 0, + width: 0, + }) + .append('defs'); + + defs.append('linearGradient') + .attr({ + id: 'shadow-gradient', + }) + .append('stop') + .attr({ + offset: '0%', + 'stop-color': '#000', + 'stop-opacity': 0.4, + }) + .select(this.selectParentNode) + .append('stop') + .attr({ + offset: '100%', + 'stop-color': '#000', + 'stop-opacity': 0, + }); + } + + createLine(chart, key) { + chart.append('g') + .attr({ + class: 'deploy-info', + }) + .selectAll('.deploy-info') + .data(this.data) + .enter() + .append('g') + .attr({ + class: d => `deploy-info-${d.id}-${key}`, + transform: d => `translate(${Math.floor(d.xPos) + 1}, 0)`, + }) + .append('rect') + .attr({ + x: 1, + y: 0, + height: this.height + 1, + width: 3, + fill: 'url(#shadow-gradient)', + }) + .select(this.selectParentNode) + .append('line') + .attr({ + class: 'deployment-line', + x1: 0, + x2: 0, + y1: 0, + y2: this.height + 1, + }); + } + + createDeployInfoBox(chart, key) { + chart.selectAll('.deploy-info') + .selectAll('.js-deploy-info-box') + .data(this.data) + .enter() + .select(d => document.querySelector(`.deploy-info-${d.id}-${key}`)) + .append('svg') + .attr({ + class: 'js-deploy-info-box hidden', + x: 3, + y: 0, + width: 92, + height: 60, + }) + .append('rect') + .attr({ + class: 'rect-text-metric deploy-info-rect rect-metric', + x: 1, + y: 1, + rx: 2, + width: 90, + height: 58, + }) + .select(this.selectParentNode) + .append('g') + .attr({ + transform: 'translate(5, 2)', + }) + .append('text') + .attr({ + class: 'deploy-info-text text-metric-bold', + }) + .text(Deployments.refText) + .select(this.selectParentNode) + .append('text') + .attr({ + class: 'deploy-info-text', + y: 18, + }) + .text(d => dateFormat(d.time)) + .select(this.selectParentNode) + .append('text') + .attr({ + class: 'deploy-info-text text-metric-bold', + y: 38, + }) + .text(d => timeFormat(d.time)); + } + + static toggleDeployTextbox(deploy, key, showInfoBox) { + d3.selectAll(`.deploy-info-${deploy.id}-${key} .js-deploy-info-box`) + .classed('hidden', !showInfoBox); + } + + mouseOverDeployInfo(mouseXPos, key) { + if (!this.data) return false; + + let dataFound = false; + + this.data.forEach((d) => { + if (d.xPos >= mouseXPos - 10 && d.xPos <= mouseXPos + 10 && !dataFound) { + dataFound = d.xPos + 1; + + Deployments.toggleDeployTextbox(d, key, true); + } else { + Deployments.toggleDeployTextbox(d, key, false); + } + }); + + return dataFound; + } + + /* `this` is bound to the D3 node */ + selectParentNode() { + return this.parentNode; + } + + static refText(d) { + return d.tag ? d.ref : d.sha.slice(0, 6); + } +} diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js index 78bb0e6fb47..6af88769129 100644 --- a/app/assets/javascripts/monitoring/prometheus_graph.js +++ b/app/assets/javascripts/monitoring/prometheus_graph.js @@ -3,16 +3,20 @@ import d3 from 'd3'; import statusCodes from '~/lib/utils/http_status'; -import { formatRelevantDigits } from '~/lib/utils/number_utils'; +import Deployments from './deployments'; +import '../lib/utils/common_utils'; +import { formatRelevantDigits } from '../lib/utils/number_utils'; import '../flash'; +import { + dateFormat, + timeFormat, +} from './constants'; const prometheusContainer = '.prometheus-container'; const prometheusParentGraphContainer = '.prometheus-graphs'; const prometheusGraphsContainer = '.prometheus-graph'; const prometheusStatesContainer = '.prometheus-state'; const metricsEndpoint = 'metrics.json'; -const timeFormat = d3.time.format('%H:%M'); -const dayFormat = d3.time.format('%b %e, %a'); const bisectDate = d3.bisector(d => d.time).left; const extraAddedWidthParent = 100; @@ -36,6 +40,7 @@ class PrometheusGraph { this.width = parentContainerWidth - this.margin.left - this.margin.right; this.height = this.originalHeight - this.margin.top - this.margin.bottom; this.backOffRequestCounter = 0; + this.deployments = new Deployments(this.width, this.height); this.configureGraph(); this.init(); } else { @@ -74,6 +79,12 @@ class PrometheusGraph { $(prometheusParentGraphContainer).show(); this.transformData(metricsResponse); this.createGraph(); + + const firstMetricData = this.graphSpecificProperties[ + Object.keys(this.graphSpecificProperties)[0] + ].data; + + this.deployments.init(firstMetricData); } }); } @@ -96,6 +107,7 @@ class PrometheusGraph { .attr('width', this.width + this.margin.left + this.margin.right) .attr('height', this.height + this.margin.bottom + this.margin.top) .append('g') + .attr('class', 'graph-container') .attr('transform', `translate(${this.margin.left},${this.margin.top})`); const axisLabelContainer = d3.select(prometheusGraphContainer) @@ -116,6 +128,7 @@ class PrometheusGraph { .scale(y) .ticks(this.commonGraphProperties.axis_no_ticks) .tickSize(-this.width) + .outerTickSize(0) .orient('left'); this.createAxisLabelContainers(axisLabelContainer, key); @@ -248,7 +261,8 @@ class PrometheusGraph { const d1 = currentGraphProps.data[overlayIndex]; const evalTime = timeValueOverlay - d0.time > d1.time - timeValueOverlay; const currentData = evalTime ? d1 : d0; - const currentTimeCoordinate = currentGraphProps.xScale(currentData.time); + const currentTimeCoordinate = Math.floor(currentGraphProps.xScale(currentData.time)); + const currentDeployXPos = this.deployments.mouseOverDeployInfo(currentXCoordinate, key); const currentPrometheusGraphContainer = `${prometheusGraphsContainer}[graph-type=${key}]`; const maxValueFromData = d3.max(currentGraphProps.data.map(metricValue => metricValue.value)); const maxMetricValue = currentGraphProps.yScale(maxValueFromData); @@ -256,13 +270,12 @@ class PrometheusGraph { // Clear up all the pieces of the flag d3.selectAll(`${currentPrometheusGraphContainer} .selected-metric-line`).remove(); d3.selectAll(`${currentPrometheusGraphContainer} .circle-metric`).remove(); - d3.selectAll(`${currentPrometheusGraphContainer} .rect-text-metric`).remove(); - d3.selectAll(`${currentPrometheusGraphContainer} .text-metric`).remove(); + d3.selectAll(`${currentPrometheusGraphContainer} .rect-text-metric:not(.deploy-info-rect)`).remove(); const currentChart = d3.select(currentPrometheusGraphContainer).select('g'); currentChart.append('line') - .attr('class', 'selected-metric-line') .attr({ + class: `${currentDeployXPos ? 'hidden' : ''} selected-metric-line`, x1: currentTimeCoordinate, y1: currentGraphProps.yScale(0), x2: currentTimeCoordinate, @@ -272,33 +285,45 @@ class PrometheusGraph { currentChart.append('circle') .attr('class', 'circle-metric') .attr('fill', currentGraphProps.line_color) - .attr('cx', currentTimeCoordinate) + .attr('cx', currentDeployXPos || currentTimeCoordinate) .attr('cy', currentGraphProps.yScale(currentData.value)) .attr('r', this.commonGraphProperties.circle_radius_metric); + if (currentDeployXPos) return; + // The little box with text - const rectTextMetric = currentChart.append('g') - .attr('class', 'rect-text-metric') - .attr('translate', `(${currentTimeCoordinate}, ${currentGraphProps.yScale(currentData.value)})`); + const rectTextMetric = currentChart.append('svg') + .attr({ + class: 'rect-text-metric', + x: currentTimeCoordinate, + y: 0, + }); rectTextMetric.append('rect') - .attr('class', 'rect-metric') - .attr('x', currentTimeCoordinate + 10) - .attr('y', maxMetricValue) - .attr('width', this.commonGraphProperties.rect_text_width) - .attr('height', this.commonGraphProperties.rect_text_height); + .attr({ + class: 'rect-metric', + x: 4, + y: 1, + rx: 2, + width: this.commonGraphProperties.rect_text_width, + height: this.commonGraphProperties.rect_text_height, + }); rectTextMetric.append('text') - .attr('class', 'text-metric') - .attr('x', currentTimeCoordinate + 35) - .attr('y', maxMetricValue + 35) + .attr({ + class: 'text-metric text-metric-bold', + x: 8, + y: 35, + }) .text(timeFormat(currentData.time)); rectTextMetric.append('text') - .attr('class', 'text-metric-date') - .attr('x', currentTimeCoordinate + 15) - .attr('y', maxMetricValue + 15) - .text(dayFormat(currentData.time)); + .attr({ + class: 'text-metric-date', + x: 8, + y: 15, + }) + .text(dateFormat(currentData.time)); let currentMetricValue = formatRelevantDigits(currentData.value); if (key === 'cpu_values') { diff --git a/app/assets/javascripts/pdf/assets/img/bg.gif b/app/assets/javascripts/pdf/assets/img/bg.gif Binary files differnew file mode 100644 index 00000000000..c7e98e044f5 --- /dev/null +++ b/app/assets/javascripts/pdf/assets/img/bg.gif diff --git a/app/assets/javascripts/pdf/index.vue b/app/assets/javascripts/pdf/index.vue new file mode 100644 index 00000000000..4603859d7b0 --- /dev/null +++ b/app/assets/javascripts/pdf/index.vue @@ -0,0 +1,73 @@ +<template> + <div class="pdf-viewer" v-if="hasPDF"> + <page v-for="(page, index) in pages" + :key="index" + :v-if="!loading" + :page="page" + :number="index + 1" /> + </div> +</template> + +<script> + import pdfjsLib from 'pdfjs-dist'; + import workerSrc from 'vendor/pdf.worker'; + + import page from './page/index.vue'; + + export default { + props: { + pdf: { + type: [String, Uint8Array], + required: true, + }, + }, + data() { + return { + loading: false, + pages: [], + }; + }, + components: { page }, + watch: { pdf: 'load' }, + computed: { + document() { + return typeof this.pdf === 'string' ? this.pdf : { data: this.pdf }; + }, + hasPDF() { + return this.pdf && this.pdf.length > 0; + }, + }, + methods: { + load() { + this.pages = []; + return pdfjsLib.getDocument(this.document) + .then(this.renderPages) + .then(() => this.$emit('pdflabload')) + .catch(error => this.$emit('pdflaberror', error)) + .then(() => { this.loading = false; }); + }, + renderPages(pdf) { + const pagePromises = []; + this.loading = true; + for (let num = 1; num <= pdf.numPages; num += 1) { + pagePromises.push( + pdf.getPage(num).then(p => this.pages.push(p)), + ); + } + return Promise.all(pagePromises); + }, + }, + mounted() { + pdfjsLib.PDFJS.workerSrc = workerSrc; + if (this.hasPDF) this.load(); + }, + }; +</script> + +<style> + .pdf-viewer { + background: url('./assets/img/bg.gif'); + display: flex; + flex-flow: column nowrap; + } +</style> diff --git a/app/assets/javascripts/pdf/page/index.vue b/app/assets/javascripts/pdf/page/index.vue new file mode 100644 index 00000000000..7b74ee4eb2e --- /dev/null +++ b/app/assets/javascripts/pdf/page/index.vue @@ -0,0 +1,68 @@ +<template> + <canvas + class="pdf-page" + ref="canvas" + :data-page="number" /> +</template> + +<script> + export default { + props: { + page: { + type: Object, + required: true, + }, + number: { + type: Number, + required: true, + }, + }, + data() { + return { + scale: 4, + rendering: false, + }; + }, + computed: { + viewport() { + return this.page.getViewport(this.scale); + }, + context() { + return this.$refs.canvas.getContext('2d'); + }, + renderContext() { + return { + canvasContext: this.context, + viewport: this.viewport, + }; + }, + }, + mounted() { + this.$refs.canvas.height = this.viewport.height; + this.$refs.canvas.width = this.viewport.width; + this.rendering = true; + this.page.render(this.renderContext) + .then(() => { this.rendering = false; }) + .catch(error => this.$emit('pdflaberror', error)); + }, + }; +</script> + +<style> +.pdf-page { + margin: 8px auto 0 auto; + border-top: 1px #ddd solid; + border-bottom: 1px #ddd solid; + width: 100%; +} + +.pdf-page:first-child { + margin-top: 0px; + border-top: 0px; +} + +.pdf-page:last-child { + margin-bottom: 0px; + border-bottom: 0px; +} +</style> diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index 0344ce9ffb4..68cf9ced3ef 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -30,7 +30,7 @@ $els.each((function(_this) { return function(i, dropdown) { var options = {}; - var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser, showMenuAbove; + var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, defaultNullUser, firstUser, issueURL, selectedId, selectedIdDefault, showAnyUser, showNullUser, showMenuAbove; $dropdown = $(dropdown); options.projectId = $dropdown.data('project-id'); options.groupId = $dropdown.data('group-id'); @@ -38,11 +38,11 @@ options.todoFilter = $dropdown.data('todo-filter'); options.todoStateFilter = $dropdown.data('todo-state-filter'); showNullUser = $dropdown.data('null-user'); + defaultNullUser = $dropdown.data('null-user-default'); showMenuAbove = $dropdown.data('showMenuAbove'); showAnyUser = $dropdown.data('any-user'); firstUser = $dropdown.data('first-user'); options.authorId = $dropdown.data('author-id'); - selectedId = $dropdown.data('selected'); defaultLabel = $dropdown.data('default-label'); issueURL = $dropdown.data('issueUpdate'); $selectbox = $dropdown.closest('.selectbox'); @@ -51,6 +51,8 @@ $value = $block.find('.value'); $collapsedSidebar = $block.find('.sidebar-collapsed-user'); $loading = $block.find('.block-loading').fadeOut(); + selectedIdDefault = (defaultNullUser && showNullUser) ? 0 : null; + selectedId = $dropdown.data('selected') || selectedIdDefault; var updateIssueBoardsIssue = function () { $loading.removeClass('hidden').fadeIn(); @@ -186,12 +188,14 @@ fieldName: $dropdown.data('field-name'), toggleLabel: function(selected, el) { if (selected && 'id' in selected && $(el).hasClass('is-active')) { + $dropdown.find('.dropdown-toggle-text').removeClass('is-default'); if (selected.text) { return selected.text; } else { return selected.name; } } else { + $dropdown.find('.dropdown-toggle-text').addClass('is-default'); return defaultLabel; } }, @@ -204,13 +208,14 @@ }, vue: $dropdown.hasClass('js-issue-board-sidebar'), clicked: function(user, $el, e) { - var isIssueIndex, isMRIndex, page, selected; + var isIssueIndex, isMRIndex, page, selected, isSelecting; page = $('body').data('page'); isIssueIndex = page === 'projects:issues:index'; isMRIndex = (page === page && page === 'projects:merge_requests:index'); + isSelecting = (user.id !== selectedId); + selectedId = isSelecting ? user.id : selectedIdDefault; if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) { e.preventDefault(); - selectedId = user.id; if (selectedId === gon.current_user_id) { $('.assign-to-me-link').hide(); } else { @@ -221,12 +226,11 @@ if ($el.closest('.add-issues-modal').length) { gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id; } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { - selectedId = user.id; return Issuable.filterResults($dropdown.closest('form')); } else if ($dropdown.hasClass('js-filter-submit')) { return $dropdown.closest('form').submit(); } else if ($dropdown.hasClass('js-issue-board-sidebar')) { - if (user.id) { + if (user.id && isSelecting) { gl.issueBoards.boardStoreIssueSet('assignee', new ListUser({ id: user.id, username: user.username, @@ -248,6 +252,9 @@ }, opened: function(e) { const $el = $(e.currentTarget); + if ($dropdown.hasClass('js-issue-board-sidebar')) { + selectedId = parseInt($dropdown[0].dataset.selected, 10) || selectedIdDefault; + } $el.find('.is-active').removeClass('is-active'); $el.find(`li[data-user-id="${selectedId}"] .dropdown-menu-user-link`).addClass('is-active'); }, diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss index b2102d2fbc5..9159927ed8b 100644 --- a/app/assets/stylesheets/framework/awards.scss +++ b/app/assets/stylesheets/framework/awards.scss @@ -227,8 +227,8 @@ .award-control-icon-positive, .award-control-icon-super-positive { position: absolute; - left: 7px; - bottom: 9px; + left: 11px; + bottom: 7px; opacity: 0; @include transition(opacity, transform); } diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index f3e2a5db0a6..ac1fc0eb8ae 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -254,6 +254,63 @@ padding: 10px 0; } +.landing { + margin-bottom: $gl-padding; + overflow: hidden; + display: flex; + position: relative; + border: 1px solid $blue-300; + border-radius: $border-radius-default; + background-color: $blue-25; + justify-content: center; + + .dismiss-button { + position: absolute; + right: 6px; + top: 6px; + cursor: pointer; + color: $blue-300; + z-index: 1; + border: none; + background-color: transparent; + + &:hover, + &:focus { + border: none; + color: $blue-400; + } + } + + .svg-container { + align-self: center; + } + + .inner-content { + text-align: left; + white-space: nowrap; + + h4 { + color: $gl-text-color; + font-size: 17px; + } + + p { + color: $gl-text-color; + margin-bottom: $gl-padding; + } + } + + @media (max-width: $screen-sm-min) { + flex-direction: column; + + .inner-content { + white-space: normal; + padding: 0 28px; + text-align: center; + } + } +} + .empty-state { margin: 100px 0 0; diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 638c1403b25..1a6f36d032d 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -424,6 +424,11 @@ table { } } +.bordered-box { + border: 1px solid $border-color; + border-radius: $border-radius-default; +} + .str-truncated { &-60 { @include str-truncated(60%); diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 1313ea25c2a..73ded9f30d4 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -390,7 +390,8 @@ &::before { position: absolute; left: 6px; - top: 6px; + top: 50%; + transform: translateY(-50%); font: normal normal normal 14px/1 FontAwesome; font-size: inherit; text-rendering: auto; diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index df819ffe4bc..c197bf6b9f5 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -61,11 +61,13 @@ .file-content { background: $white-light; - &.image_file { + &.image_file, + &.video { background: $file-image-bg; text-align: center; - img { + img, + video { padding: 20px; max-width: 80%; } diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index ad3dbc7ac48..403724cd68a 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -93,11 +93,6 @@ top: $gl-padding-top; } - .bordered-box { - border: 1px solid $border-color; - border-radius: $border-radius-default; - } - .content-list { li { padding: 18px $gl-padding $gl-padding; @@ -139,42 +134,9 @@ } } - .landing { - margin-bottom: $gl-padding; - overflow: hidden; - - .dismiss-icon { - position: absolute; - right: $cycle-analytics-box-padding; - cursor: pointer; - color: $cycle-analytics-dismiss-icon-color; - } - - .svg-container { - text-align: center; - - svg { - width: 136px; - height: 136px; - } - } - - .inner-content { - @media (max-width: $screen-xs-max) { - padding: 0 28px; - text-align: center; - } - - h4 { - color: $gl-text-color; - font-size: 17px; - } - - p { - color: $cycle-analytics-box-text-color; - margin-bottom: $gl-padding; - } - } + .landing svg { + width: 136px; + height: 136px; } .fa-spinner { diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 72e7d42858d..026d35295d7 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -157,7 +157,8 @@ .prometheus-graph { text { - fill: $stat-graph-axis-fill; + fill: $gl-text-color; + stroke-width: 0; } .label-axis-text, @@ -210,27 +211,33 @@ .rect-text-metric { fill: $white-light; stroke-width: 1; - stroke: $black; + stroke: $gray-darkest; } .rect-axis-text { fill: $white-light; } -.text-metric, -.text-median-metric, -.text-metric-usage, -.text-metric-date { - fill: $black; +.text-metric { + font-weight: 600; } -.text-metric-date { - font-weight: 200; +.selected-metric-line { + stroke: $gl-gray-dark; + stroke-width: 1; } -.selected-metric-line { +.deployment-line { stroke: $black; - stroke-width: 1; + stroke-width: 2; +} + +.deploy-info-text { + dominant-baseline: text-before-edge; +} + +.text-metric-bold { + font-weight: 600; } .prometheus-state { diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 73a5889867a..72d73b89a2a 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -88,3 +88,26 @@ color: $gl-text-color-secondary; margin-top: 10px; } + +.explore-groups.landing { + margin-top: 10px; + + .inner-content { + padding: 0; + + p { + margin: 7px 0 0; + max-width: 480px; + padding: 0 $gl-padding; + + @media (max-width: $screen-sm-min) { + margin: 0 auto; + } + } + } + + svg { + width: 62px; + height: 50px; + } +} diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 97fab513b01..ad6eb9f6fe0 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -6,7 +6,13 @@ } .limit-container-width { - .detail-page-header { + .detail-page-header, + .page-content-header, + .commit-box, + .info-well, + .notes, + .commit-ci-menu, + .files-changed { @extend .fixed-width-container; } @@ -36,8 +42,7 @@ } .diffs { - .mr-version-controls, - .files-changed { + .mr-version-controls { @extend .fixed-width-container; } } diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index 04ff2d52b91..b64b89485f7 100644 --- a/app/assets/stylesheets/pages/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss @@ -71,7 +71,6 @@ .nav-controls { width: auto; min-width: 50%; - white-space: nowrap; } } diff --git a/app/controllers/admin/hooks_controller.rb b/app/controllers/admin/hooks_controller.rb index cbfc4581411..a119934febc 100644 --- a/app/controllers/admin/hooks_controller.rb +++ b/app/controllers/admin/hooks_controller.rb @@ -1,4 +1,6 @@ class Admin::HooksController < Admin::ApplicationController + before_action :hook, only: :edit + def index @hooks = SystemHook.all @hook = SystemHook.new @@ -15,15 +17,25 @@ class Admin::HooksController < Admin::ApplicationController end end + def edit + end + + def update + if hook.update_attributes(hook_params) + flash[:notice] = 'System hook was successfully updated.' + redirect_to admin_hooks_path + else + render 'edit' + end + end + def destroy - @hook = SystemHook.find(params[:id]) - @hook.destroy + hook.destroy redirect_to admin_hooks_path end def test - @hook = SystemHook.find(params[:hook_id]) data = { event_name: "project_create", name: "Ruby", @@ -32,11 +44,17 @@ class Admin::HooksController < Admin::ApplicationController owner_name: "Someone", owner_email: "example@gitlabhq.com" } - @hook.execute(data, 'system_hooks') + hook.execute(data, 'system_hooks') redirect_back_or_default end + private + + def hook + @hook ||= SystemHook.find(params[:id]) + end + def hook_params params.require(:hook).permit( :enable_ssl_verification, diff --git a/app/controllers/concerns/milestone_actions.rb b/app/controllers/concerns/milestone_actions.rb new file mode 100644 index 00000000000..3e2a0fe4f8b --- /dev/null +++ b/app/controllers/concerns/milestone_actions.rb @@ -0,0 +1,53 @@ +module MilestoneActions + extend ActiveSupport::Concern + + def merge_requests + respond_to do |format| + format.html { redirect_to milestone_redirect_path } + format.json do + render json: tabs_json("shared/milestones/_merge_requests_tab", { + merge_requests: @milestone.merge_requests, + show_project_name: true + }) + end + end + end + + def participants + respond_to do |format| + format.html { redirect_to milestone_redirect_path } + format.json do + render json: tabs_json("shared/milestones/_participants_tab", { + users: @milestone.participants + }) + end + end + end + + def labels + respond_to do |format| + format.html { redirect_to milestone_redirect_path } + format.json do + render json: tabs_json("shared/milestones/_labels_tab", { + labels: @milestone.labels + }) + end + end + end + + private + + def tabs_json(partial, data = {}) + { + html: view_to_html_string(partial, data) + } + end + + def milestone_redirect_path + if @project + namespace_project_milestone_path(@project.namespace, @project, @milestone) + else + group_milestone_path(@group, @milestone.safe_title, title: @milestone.title) + end + end +end diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb new file mode 100644 index 00000000000..c32038d07bf --- /dev/null +++ b/app/controllers/concerns/notes_actions.rb @@ -0,0 +1,136 @@ +module NotesActions + include RendersNotes + extend ActiveSupport::Concern + + included do + before_action :authorize_admin_note!, only: [:update, :destroy] + end + + def index + current_fetched_at = Time.now.to_i + + notes_json = { notes: [], last_fetched_at: current_fetched_at } + + @notes = notes_finder.execute.inc_relations_for_view + @notes = prepare_notes_for_rendering(@notes) + + @notes.each do |note| + next if note.cross_reference_not_visible_for?(current_user) + + notes_json[:notes] << note_json(note) + end + + render json: notes_json + end + + def create + create_params = note_params.merge( + merge_request_diff_head_sha: params[:merge_request_diff_head_sha], + in_reply_to_discussion_id: params[:in_reply_to_discussion_id] + ) + @note = Notes::CreateService.new(project, current_user, create_params).execute + + if @note.is_a?(Note) + Banzai::NoteRenderer.render([@note], @project, current_user) + end + + respond_to do |format| + format.json { render json: note_json(@note) } + format.html { redirect_back_or_default } + end + end + + def update + @note = Notes::UpdateService.new(project, current_user, note_params).execute(note) + + if @note.is_a?(Note) + Banzai::NoteRenderer.render([@note], @project, current_user) + end + + respond_to do |format| + format.json { render json: note_json(@note) } + format.html { redirect_back_or_default } + end + end + + def destroy + if note.editable? + Notes::DestroyService.new(project, current_user).execute(note) + end + + respond_to do |format| + format.js { head :ok } + end + end + + private + + def note_json(note) + attrs = { + commands_changes: note.commands_changes + } + + if note.persisted? + attrs.merge!( + valid: true, + id: note.id, + discussion_id: note.discussion_id(noteable), + html: note_html(note), + note: note.note + ) + + discussion = note.to_discussion(noteable) + unless discussion.individual_note? + attrs.merge!( + discussion_resolvable: discussion.resolvable?, + + diff_discussion_html: diff_discussion_html(discussion), + discussion_html: discussion_html(discussion) + ) + end + else + attrs.merge!( + valid: false, + errors: note.errors + ) + end + + attrs + end + + def authorize_admin_note! + return access_denied! unless can?(current_user, :admin_note, note) + end + + def note_params + params.require(:note).permit( + :project_id, + :noteable_type, + :noteable_id, + :commit_id, + :noteable, + :type, + + :note, + :attachment, + + # LegacyDiffNote + :line_code, + + # DiffNote + :position + ) + end + + def noteable + @noteable ||= notes_finder.target + end + + def last_fetched_at + request.headers['X-Last-Fetched-At'] + end + + def notes_finder + @notes_finder ||= NotesFinder.new(project, current_user, finder_params) + end +end diff --git a/app/controllers/concerns/renders_blob.rb b/app/controllers/concerns/renders_blob.rb index d478c3bb6ca..9faf68e6d97 100644 --- a/app/controllers/concerns/renders_blob.rb +++ b/app/controllers/concerns/renders_blob.rb @@ -14,4 +14,8 @@ module RendersBlob html: view_to_html_string("projects/blob/_viewer", viewer: viewer, load_asynchronously: false) } end + + def override_max_blob_size(blob) + blob.override_max_size! if params[:override_max_size] == 'true' + end end diff --git a/app/controllers/concerns/renders_notes.rb b/app/controllers/concerns/renders_notes.rb index dd21066ac13..41c3114ad1e 100644 --- a/app/controllers/concerns/renders_notes.rb +++ b/app/controllers/concerns/renders_notes.rb @@ -10,6 +10,8 @@ module RendersNotes private def preload_max_access_for_authors(notes, project) + return nil unless project + user_ids = notes.map(&:author_id) project.team.max_member_access_for_user_ids(user_ids) end diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb index a8c0937569c..be2e6c7f193 100644 --- a/app/controllers/concerns/service_params.rb +++ b/app/controllers/concerns/service_params.rb @@ -38,6 +38,7 @@ module ServiceParams :new_issue_url, :notify, :notify_only_broken_pipelines, + :notify_only_default_branch, :password, :priority, :project_key, diff --git a/app/controllers/concerns/snippets_actions.rb b/app/controllers/concerns/snippets_actions.rb index ca6dffe1cc5..ffea712a833 100644 --- a/app/controllers/concerns/snippets_actions.rb +++ b/app/controllers/concerns/snippets_actions.rb @@ -5,10 +5,12 @@ module SnippetsActions end def raw + disposition = params[:inline] == 'false' ? 'attachment' : 'inline' + send_data( convert_line_endings(@snippet.content), type: 'text/plain; charset=utf-8', - disposition: 'inline', + disposition: disposition, filename: @snippet.sanitized_file_name ) end diff --git a/app/controllers/concerns/toggle_award_emoji.rb b/app/controllers/concerns/toggle_award_emoji.rb index fbf9a026b10..ba5b7d33f87 100644 --- a/app/controllers/concerns/toggle_award_emoji.rb +++ b/app/controllers/concerns/toggle_award_emoji.rb @@ -22,7 +22,8 @@ module ToggleAwardEmoji def to_todoable(awardable) case awardable when Note - awardable.noteable + # we don't create todos for personal snippet comments for now + awardable.for_personal_snippet? ? nil : awardable.noteable when MergeRequest, Issue awardable when Snippet diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index 43102596201..e52fa766044 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -1,6 +1,8 @@ class Groups::MilestonesController < Groups::ApplicationController + include MilestoneActions + before_action :group_projects - before_action :milestone, only: [:show, :update] + before_action :milestone, only: [:show, :update, :merge_requests, :participants, :labels] before_action :authorize_admin_milestones!, only: [:new, :create, :update] def index diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index be5822b2cd4..9489bbddfc4 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -35,7 +35,7 @@ class Projects::BlobController < Projects::ApplicationController end def show - @blob.override_max_size! if params[:override_max_size] == 'true' + override_max_blob_size(@blob) respond_to do |format| format.html do diff --git a/app/controllers/projects/deployments_controller.rb b/app/controllers/projects/deployments_controller.rb new file mode 100644 index 00000000000..c319671456d --- /dev/null +++ b/app/controllers/projects/deployments_controller.rb @@ -0,0 +1,18 @@ +class Projects::DeploymentsController < Projects::ApplicationController + before_action :authorize_read_environment! + before_action :authorize_read_deployment! + + def index + deployments = environment.deployments.reorder(created_at: :desc) + deployments = deployments.where('created_at > ?', params[:after].to_time) if params[:after]&.to_time + + render json: { deployments: DeploymentSerializer.new(user: @current_user, project: project) + .represent_concise(deployments) } + end + + private + + def environment + @environment ||= project.environments.find(params[:environment_id]) + end +end diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index 1e41f980f31..86d13a0d222 100644 --- a/app/controllers/projects/hooks_controller.rb +++ b/app/controllers/projects/hooks_controller.rb @@ -1,6 +1,7 @@ class Projects::HooksController < Projects::ApplicationController # Authorize before_action :authorize_admin_project! + before_action :hook, only: :edit respond_to :html @@ -17,6 +18,18 @@ class Projects::HooksController < Projects::ApplicationController redirect_to namespace_project_settings_integrations_path(@project.namespace, @project) end + def edit + end + + def update + if hook.update_attributes(hook_params) + flash[:notice] = 'Hook was successfully updated.' + redirect_to namespace_project_settings_integrations_path(@project.namespace, @project) + else + render 'edit' + end + end + def test if !@project.empty_repo? status, message = TestHookService.new.execute(hook, current_user) diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index d0dd524c484..c56bce19eee 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -1,12 +1,14 @@ class Projects::MilestonesController < Projects::ApplicationController + include MilestoneActions + before_action :module_enabled - before_action :milestone, only: [:edit, :update, :destroy, :show, :sort_issues, :sort_merge_requests] + before_action :milestone, only: [:edit, :update, :destroy, :show, :sort_issues, :sort_merge_requests, :merge_requests, :participants, :labels] # Allow read any milestone before_action :authorize_read_milestone! # Allow admin milestone - before_action :authorize_admin_milestone!, except: [:index, :show] + before_action :authorize_admin_milestone!, except: [:index, :show, :merge_requests, :participants, :labels] respond_to :html diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 405ea3c0a4f..37f51b2ebe3 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -1,68 +1,22 @@ class Projects::NotesController < Projects::ApplicationController - include RendersNotes + include NotesActions include ToggleAwardEmoji - # Authorize before_action :authorize_read_note! before_action :authorize_create_note!, only: [:create] - before_action :authorize_admin_note!, only: [:update, :destroy] before_action :authorize_resolve_note!, only: [:resolve, :unresolve] - def index - current_fetched_at = Time.now.to_i - - notes_json = { notes: [], last_fetched_at: current_fetched_at } - - @notes = notes_finder.execute.inc_relations_for_view - @notes = prepare_notes_for_rendering(@notes) - - @notes.each do |note| - next if note.cross_reference_not_visible_for?(current_user) - - notes_json[:notes] << note_json(note) - end - - render json: notes_json - end - + # + # This is a fix to make spinach feature tests passing: + # Controller actions are returned from AbstractController::Base and methods of parent classes are + # excluded in order to return only specific controller related methods. + # That is ok for the app (no :create method in ancestors) + # but fails for tests because there is a :create method on FactoryGirl (one of the ancestors) + # + # see https://github.com/rails/rails/blob/v4.2.7/actionpack/lib/abstract_controller/base.rb#L78 + # def create - create_params = note_params.merge( - merge_request_diff_head_sha: params[:merge_request_diff_head_sha], - in_reply_to_discussion_id: params[:in_reply_to_discussion_id] - ) - @note = Notes::CreateService.new(project, current_user, create_params).execute - - if @note.is_a?(Note) - Banzai::NoteRenderer.render([@note], @project, current_user) - end - - respond_to do |format| - format.json { render json: note_json(@note) } - format.html { redirect_back_or_default } - end - end - - def update - @note = Notes::UpdateService.new(project, current_user, note_params).execute(note) - - if @note.is_a?(Note) - Banzai::NoteRenderer.render([@note], @project, current_user) - end - - respond_to do |format| - format.json { render json: note_json(@note) } - format.html { redirect_back_or_default } - end - end - - def destroy - if note.editable? - Notes::DestroyService.new(project, current_user).execute(note) - end - - respond_to do |format| - format.js { head :ok } - end + super end def delete_attachment @@ -110,7 +64,7 @@ class Projects::NotesController < Projects::ApplicationController def note_html(note) render_to_string( - "projects/notes/_note", + "shared/notes/_note", layout: false, formats: [:html], locals: { note: note } @@ -152,76 +106,11 @@ class Projects::NotesController < Projects::ApplicationController ) end - def note_json(note) - attrs = { - commands_changes: note.commands_changes - } - - if note.persisted? - attrs.merge!( - valid: true, - id: note.id, - discussion_id: note.discussion_id(noteable), - html: note_html(note), - note: note.note - ) - - discussion = note.to_discussion(noteable) - unless discussion.individual_note? - attrs.merge!( - discussion_resolvable: discussion.resolvable?, - - diff_discussion_html: diff_discussion_html(discussion), - discussion_html: discussion_html(discussion) - ) - end - else - attrs.merge!( - valid: false, - errors: note.errors - ) - end - - attrs - end - - def authorize_admin_note! - return access_denied! unless can?(current_user, :admin_note, note) + def finder_params + params.merge(last_fetched_at: last_fetched_at) end def authorize_resolve_note! return access_denied! unless can?(current_user, :resolve_note, note) end - - def note_params - params.require(:note).permit( - :project_id, - :noteable_type, - :noteable_id, - :commit_id, - :noteable, - :type, - - :note, - :attachment, - - # LegacyDiffNote - :line_code, - - # DiffNote - :position - ) - end - - def notes_finder - @notes_finder ||= NotesFinder.new(project, current_user, params.merge(last_fetched_at: last_fetched_at)) - end - - def noteable - @noteable ||= notes_finder.target - end - - def last_fetched_at - request.headers['X-Last-Fetched-At'] - end end diff --git a/app/controllers/projects/settings/integrations_controller.rb b/app/controllers/projects/settings/integrations_controller.rb index fb2a4837735..1ff08cce8cb 100644 --- a/app/controllers/projects/settings/integrations_controller.rb +++ b/app/controllers/projects/settings/integrations_controller.rb @@ -5,7 +5,7 @@ module Projects before_action :authorize_admin_project! layout "project_settings" - + def show @hooks = @project.hooks @hook = ProjectHook.new diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index 5c9e0d4d1a1..66f913f8f9d 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -3,6 +3,7 @@ class Projects::SnippetsController < Projects::ApplicationController include ToggleAwardEmoji include SpammableActions include SnippetsActions + include RendersBlob before_action :module_enabled before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam] @@ -55,11 +56,23 @@ class Projects::SnippetsController < Projects::ApplicationController end def show - @note = @project.notes.new(noteable: @snippet) - @noteable = @snippet - - @discussions = @snippet.discussions - @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes)) + blob = @snippet.blob + override_max_blob_size(blob) + + respond_to do |format| + format.html do + @note = @project.notes.new(noteable: @snippet) + @noteable = @snippet + + @discussions = @snippet.discussions + @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes)) + render 'show' + end + + format.json do + render_blob_json(blob) + end + end end def destroy diff --git a/app/controllers/snippets/notes_controller.rb b/app/controllers/snippets/notes_controller.rb new file mode 100644 index 00000000000..3c4ddc1680d --- /dev/null +++ b/app/controllers/snippets/notes_controller.rb @@ -0,0 +1,44 @@ +class Snippets::NotesController < ApplicationController + include NotesActions + include ToggleAwardEmoji + + skip_before_action :authenticate_user!, only: [:index] + before_action :snippet + before_action :authorize_read_snippet!, only: [:show, :index, :create] + + private + + def note + @note ||= snippet.notes.find(params[:id]) + end + alias_method :awardable, :note + + def note_html(note) + render_to_string( + "shared/notes/_note", + layout: false, + formats: [:html], + locals: { note: note } + ) + end + + def project + nil + end + + def snippet + PersonalSnippet.find_by(id: params[:snippet_id]) + end + + def note_params + super.merge(noteable_id: params[:snippet_id]) + end + + def finder_params + params.merge(last_fetched_at: last_fetched_at, target_id: snippet.id, target_type: 'personal_snippet') + end + + def authorize_read_snippet! + return render_404 unless can?(current_user, :read_personal_snippet, snippet) + end +end diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index 056910fad67..da1ae9a34d9 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -1,13 +1,15 @@ class SnippetsController < ApplicationController + include RendersNotes include ToggleAwardEmoji include SpammableActions include SnippetsActions include MarkdownPreview + include RendersBlob - before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download] + before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] # Allow read snippet - before_action :authorize_read_snippet!, only: [:show, :raw, :download] + before_action :authorize_read_snippet!, only: [:show, :raw] # Allow modify snippet before_action :authorize_update_snippet!, only: [:edit, :update] @@ -15,7 +17,7 @@ class SnippetsController < ApplicationController # Allow destroy snippet before_action :authorize_admin_snippet!, only: [:destroy] - skip_before_action :authenticate_user!, only: [:index, :show, :raw, :download] + skip_before_action :authenticate_user!, only: [:index, :show, :raw] layout 'snippets' respond_to :html @@ -60,6 +62,23 @@ class SnippetsController < ApplicationController end def show + blob = @snippet.blob + override_max_blob_size(blob) + + @noteable = @snippet + + @discussions = @snippet.discussions + @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes)) + + respond_to do |format| + format.html do + render 'show' + end + + format.json do + render_blob_json(blob) + end + end end def destroy @@ -70,14 +89,6 @@ class SnippetsController < ApplicationController redirect_to snippets_path end - def download - send_data( - convert_line_endings(@snippet.content), - type: 'text/plain; charset=utf-8', - filename: @snippet.sanitized_file_name - ) - end - def preview_markdown render_markdown_preview(params[:text], skip_project_check: true) end diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb index 3c499184b41..dc6a8ad1f66 100644 --- a/app/finders/notes_finder.rb +++ b/app/finders/notes_finder.rb @@ -68,6 +68,8 @@ class NotesFinder MergeRequestsFinder.new(@current_user, project_id: @project.id).execute when "snippet", "project_snippet" SnippetsFinder.new.execute(@current_user, filter: :by_project, project: @project) + when "personal_snippet" + PersonalSnippet.all else raise 'invalid target_type' end diff --git a/app/helpers/award_emoji_helper.rb b/app/helpers/award_emoji_helper.rb index 167b09e678f..024cf38469e 100644 --- a/app/helpers/award_emoji_helper.rb +++ b/app/helpers/award_emoji_helper.rb @@ -1,10 +1,14 @@ module AwardEmojiHelper def toggle_award_url(awardable) - return url_for([:toggle_award_emoji, awardable]) unless @project + return url_for([:toggle_award_emoji, awardable]) unless @project || awardable.is_a?(Note) if awardable.is_a?(Note) # We render a list of notes very frequently and calling the specific method is a lot faster than the generic one (4.5x) - toggle_award_emoji_namespace_project_note_url(@project.namespace, @project, awardable.id) + if awardable.for_personal_snippet? + toggle_award_emoji_snippet_note_path(awardable.noteable, awardable) + else + toggle_award_emoji_namespace_project_note_path(@project.namespace, @project, awardable.id) + end else url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index cc47654dc06..377b080b3c6 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -119,7 +119,15 @@ module BlobHelper end def blob_raw_url - namespace_project_raw_path(@project.namespace, @project, @id) + if @snippet + if @snippet.project_id + raw_namespace_project_snippet_path(@project.namespace, @project, @snippet) + else + raw_snippet_path(@snippet) + end + elsif @blob + namespace_project_raw_path(@project.namespace, @project, @id) + end end # SVGs can contain malicious JavaScript; only include whitelisted @@ -209,11 +217,21 @@ module BlobHelper end def copy_blob_source_button(blob) + return unless blob.rendered_as_text?(ignore_errors: false) + clipboard_button(target: ".blob-content[data-blob-id='#{blob.id}']", class: "btn btn-sm js-copy-blob-source-btn", title: "Copy source to clipboard") end - def open_raw_file_button(path) - link_to icon('file-code-o'), path, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: 'Open raw', data: { container: 'body' } + def open_raw_blob_button(blob) + if blob.raw_binary? + icon = icon('download') + title = 'Download' + else + icon = icon('file-code-o') + title = 'Open raw' + end + + link_to icon, blob_raw_url, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: title, data: { container: 'body' } end def blob_render_error_reason(viewer) diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 5f5c76d3722..960111ca045 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -10,11 +10,12 @@ module EventsHelper 'deleted' => 'icon_trash_o' }.freeze - def link_to_author(event) + def link_to_author(event, self_added: false) author = event.author if author - link_to author.name, user_path(author.username), title: author.name + name = self_added ? 'You' : author.name + link_to name, user_path(author.username), title: name else event.author_name end diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb index 0781874d7fc..b241a14740b 100644 --- a/app/helpers/markup_helper.rb +++ b/app/helpers/markup_helper.rb @@ -74,7 +74,7 @@ module MarkupHelper context[:project] ||= @project html = markdown_unsafe(text, context) - banzai_postprocess(html, context) + prepare_for_rendering(html, context) end def markdown_field(object, field) @@ -82,13 +82,13 @@ module MarkupHelper return '' unless object.present? html = Banzai.render_field(object, field) - banzai_postprocess(html, object.banzai_render_context(field)) + prepare_for_rendering(html, object.banzai_render_context(field)) end def markup(file_name, text, context = {}) context[:project] ||= @project html = context.delete(:rendered) || markup_unsafe(file_name, text, context) - banzai_postprocess(html, context) + prepare_for_rendering(html, context) end def render_wiki_content(wiki_page) @@ -107,14 +107,14 @@ module MarkupHelper wiki_page.formatted_content.html_safe end - banzai_postprocess(html, context) + prepare_for_rendering(html, context) end def markup_unsafe(file_name, text, context = {}) return '' unless text.present? if gitlab_markdown?(file_name) - Hamlit::RailsHelpers.preserve(markdown_unsafe(text, context)) + markdown_unsafe(text, context) elsif asciidoc?(file_name) asciidoc_unsafe(text) elsif plain?(file_name) @@ -225,8 +225,7 @@ module MarkupHelper Gitlab::OtherMarkup.render(file_name, text) end - # Calls Banzai.post_process with some common context options - def banzai_postprocess(html, context = {}) + def prepare_for_rendering(html, context = {}) return '' unless html.present? context.merge!( @@ -239,7 +238,9 @@ module MarkupHelper requested_path: @path ) - Banzai.post_process(html, context) + html = Banzai.post_process(html, context) + + Hamlit::RailsHelpers.preserve(html) end extend self diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index e347f61fb8d..2614cdfe90e 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -1,6 +1,6 @@ module MergeRequestsHelper def new_mr_path_from_push_event(event) - target_project = event.project.forked_from_project || event.project + target_project = event.project.default_merge_request_target new_namespace_project_merge_request_path( event.project.namespace, event.project, @@ -127,6 +127,10 @@ module MergeRequestsHelper end end + def target_projects(project) + [project, project.default_merge_request_target].uniq + end + def merge_request_button_visibility(merge_request, closed) return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) || merge_request.closed_without_fork? end diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index c9e70faa52e..c515774140c 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -115,4 +115,28 @@ module MilestonesHelper end end end + + def milestone_merge_request_tab_path(milestone) + if @project + merge_requests_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json) + elsif @group + merge_requests_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json) + end + end + + def milestone_participants_tab_path(milestone) + if @project + participants_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json) + elsif @group + participants_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json) + end + end + + def milestone_labels_tab_path(milestone) + if @project + labels_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json) + elsif @group + labels_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json) + end + end end diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb index 979264c9421..2fd64b3441e 100644 --- a/app/helpers/snippets_helper.rb +++ b/app/helpers/snippets_helper.rb @@ -8,6 +8,14 @@ module SnippetsHelper end end + def download_snippet_path(snippet) + if snippet.project_id + raw_namespace_project_snippet_path(@project.namespace, @project, snippet, inline: false) + else + raw_snippet_path(snippet, inline: false) + end + end + # Return the path of a snippets index for a user or for a project # # @returns String, path to snippet index diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index 4f5adf623f2..f19e2f9db9c 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -13,13 +13,13 @@ module TodosHelper def todo_action_name(todo) case todo.action - when Todo::ASSIGNED then 'assigned you' - when Todo::MENTIONED then 'mentioned you on' + when Todo::ASSIGNED then todo.self_added? ? 'assigned' : 'assigned you' + when Todo::MENTIONED then "mentioned #{todo_action_subject(todo)} on" when Todo::BUILD_FAILED then 'The build failed for' when Todo::MARKED then 'added a todo for' - when Todo::APPROVAL_REQUIRED then 'set you as an approver for' + when Todo::APPROVAL_REQUIRED then "set #{todo_action_subject(todo)} as an approver for" when Todo::UNMERGEABLE then 'Could not merge' - when Todo::DIRECTLY_ADDRESSED then 'directly addressed you on' + when Todo::DIRECTLY_ADDRESSED then "directly addressed #{todo_action_subject(todo)} on" end end @@ -148,6 +148,10 @@ module TodosHelper private + def todo_action_subject(todo) + todo.self_added? ? 'yourself' : 'you' + end + def show_todo_state?(todo) (todo.target.is_a?(MergeRequest) || todo.target.is_a?(Issue)) && %w(closed merged).include?(todo.target.state) end diff --git a/app/models/blob.rb b/app/models/blob.rb index 290df5d5520..1cdb8811cff 100644 --- a/app/models/blob.rb +++ b/app/models/blob.rb @@ -27,6 +27,8 @@ class Blob < SimpleDelegator BlobViewer::Image, BlobViewer::Sketch, + BlobViewer::Video, + BlobViewer::PDF, BlobViewer::BinarySTL, diff --git a/app/models/blob_viewer/video.rb b/app/models/blob_viewer/video.rb new file mode 100644 index 00000000000..057f9fe516f --- /dev/null +++ b/app/models/blob_viewer/video.rb @@ -0,0 +1,12 @@ +module BlobViewer + class Video < Base + include Rich + include ClientSide + + self.partial_name = 'video' + self.extensions = UploaderHelper::VIDEO_EXT + self.binary = true + self.switcher_icon = 'film' + self.switcher_title = 'video' + end +end diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb index f033028c4e5..eb32bf3d32a 100644 --- a/app/models/concerns/cache_markdown_field.rb +++ b/app/models/concerns/cache_markdown_field.rb @@ -78,6 +78,9 @@ module CacheMarkdownField def cached_html_up_to_date?(markdown_field) html_field = cached_markdown_fields.html_field(markdown_field) + cached = !cached_html_for(markdown_field).nil? && !__send__(markdown_field).nil? + return false unless cached + markdown_changed = attribute_changed?(markdown_field) || false html_changed = attribute_changed?(html_field) || false diff --git a/app/models/member.rb b/app/models/member.rb index 97fba501759..7228e82e978 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -154,6 +154,11 @@ class Member < ActiveRecord::Base def add_users(source, users, access_level, current_user: nil, expires_at: nil) return [] unless users.present? + # Collect all user ids into separate array + # so we can use single sql query to get user objects + user_ids = users.select { |user| user =~ /\A\d+\Z/ } + users = users - user_ids + User.where(id: user_ids) + self.transaction do users.map do |user| add_user( diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 9d2288c311e..365fa4f1e70 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -100,6 +100,7 @@ class MergeRequest < ActiveRecord::Base validates :merge_user, presence: true, if: :merge_when_pipeline_succeeds?, unless: :importing? validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?] validate :validate_fork, unless: :closed_without_fork? + validate :validate_target_project, on: :create scope :by_source_or_target_branch, ->(branch_name) do where("source_branch = :branch OR target_branch = :branch", branch: branch_name) @@ -330,6 +331,12 @@ class MergeRequest < ActiveRecord::Base end end + def validate_target_project + return true if target_project.merge_requests_enabled? + + errors.add :base, 'Target project has disabled merge requests' + end + def validate_fork return true unless target_project && source_project return true if target_project == source_project diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 9bfa731785f..397dc7a25ab 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -33,7 +33,7 @@ class Namespace < ActiveRecord::Base validates :path, presence: true, length: { maximum: 255 }, - namespace: true + dynamic_path: true validate :nesting_level_allowed @@ -220,6 +220,10 @@ class Namespace < ActiveRecord::Base Project.inside_path(full_path) end + def has_parent? + parent.present? + end + private def repository_storage_paths diff --git a/app/models/project.rb b/app/models/project.rb index c7dc562c238..025db89ebfd 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -196,13 +196,14 @@ class Project < ActiveRecord::Base message: Gitlab::Regex.project_name_regex_message } validates :path, presence: true, - project_path: true, + dynamic_path: true, length: { maximum: 255 }, format: { with: Gitlab::Regex.project_path_regex, - message: Gitlab::Regex.project_path_regex_message } + message: Gitlab::Regex.project_path_regex_message }, + uniqueness: { scope: :namespace_id } + validates :namespace, presence: true validates :name, uniqueness: { scope: :namespace_id } - validates :path, uniqueness: { scope: :namespace_id } validates :import_url, addressable_url: true, if: :external_import? validates :import_url, importable_url: true, if: [:external_import?, :import_url_changed?] validates :star_count, numericality: { greater_than_or_equal_to: 0 } @@ -1270,6 +1271,9 @@ class Project < ActiveRecord::Base else update_attribute(name, value) end + + rescue ActiveRecord::RecordNotSaved => e + handle_update_attribute_error(e, value) end def pushes_since_gc @@ -1314,6 +1318,14 @@ class Project < ActiveRecord::Base namespace_id_changed? end + def default_merge_request_target + if forked_from_project&.merge_requests_enabled? + forked_from_project + else + self + end + end + alias_method :name_with_namespace, :full_name alias_method :human_name, :full_name alias_method :path_with_namespace, :full_path @@ -1383,4 +1395,16 @@ class Project < ActiveRecord::Base ContainerRepository.build_root_repository(self).has_tags? end + + def handle_update_attribute_error(ex, value) + if ex.message.start_with?('Failed to replace') + if value.respond_to?(:each) + invalid = value.detect(&:invalid?) + + raise ex, ([ex.message] + invalid.errors.full_messages).join(' ') if invalid + end + end + + raise ex + end end diff --git a/app/models/repository.rb b/app/models/repository.rb index d02aea49689..ba34d570dbd 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -505,14 +505,8 @@ class Repository delegate :tag_names, to: :raw_repository cache_method :tag_names, fallback: [] - def branch_count - branches.size - end + delegate :branch_count, :tag_count, to: :raw_repository cache_method :branch_count, fallback: 0 - - def tag_count - raw_repository.rugged.tags.count - end cache_method :tag_count, fallback: 0 def avatar @@ -961,15 +955,13 @@ class Repository end def is_ancestor?(ancestor_id, descendant_id) - # NOTE: This feature is intentionally disabled until - # https://gitlab.com/gitlab-org/gitlab-ce/issues/30586 is resolved - # Gitlab::GitalyClient.migrate(:is_ancestor) do |is_enabled| - # if is_enabled - # raw_repository.is_ancestor?(ancestor_id, descendant_id) - # else - merge_base_commit(ancestor_id, descendant_id) == ancestor_id - # end - # end + Gitlab::GitalyClient.migrate(:is_ancestor) do |is_enabled| + if is_enabled + raw_repository.is_ancestor?(ancestor_id, descendant_id) + else + merge_base_commit(ancestor_id, descendant_id) == ancestor_id + end + end end def empty_repo? diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 380835707e8..d8860718cb5 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -1,6 +1,5 @@ class Snippet < ActiveRecord::Base include Gitlab::VisibilityLevel - include Linguist::BlobHelper include CacheMarkdownField include Noteable include Participable @@ -87,47 +86,26 @@ class Snippet < ActiveRecord::Base ] end - def data - content + def blob + @blob ||= Blob.decorate(SnippetBlob.new(self), nil) end def hook_attrs attributes end - def size - 0 - end - def file_name super.to_s end - # alias for compatibility with blobs and highlighting - def path - file_name - end - - def name - file_name - end - def sanitized_file_name file_name.gsub(/[^a-zA-Z0-9_\-\.]+/, '') end - def mode - nil - end - def visibility_level_field :visibility_level end - def no_highlighting? - content.lines.count > 1000 - end - def notes_with_associations notes.includes(:author) end diff --git a/app/models/snippet_blob.rb b/app/models/snippet_blob.rb new file mode 100644 index 00000000000..d6cab74eb1a --- /dev/null +++ b/app/models/snippet_blob.rb @@ -0,0 +1,59 @@ +class SnippetBlob + include Linguist::BlobHelper + + attr_reader :snippet + + def initialize(snippet) + @snippet = snippet + end + + delegate :id, to: :snippet + + def name + snippet.file_name + end + + alias_method :path, :name + + def size + data.bytesize + end + + def data + snippet.content + end + + def rendered_markup + return unless Gitlab::MarkupHelper.gitlab_markdown?(name) + + Banzai.render_field(snippet, :content) + end + + def mode + nil + end + + def binary? + false + end + + def load_all_data!(repository) + # No-op + end + + def lfs_pointer? + false + end + + def lfs_oid + nil + end + + def lfs_size + nil + end + + def truncated? + false + end +end diff --git a/app/models/todo.rb b/app/models/todo.rb index da3fa7277c2..b011001b235 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -84,6 +84,10 @@ class Todo < ActiveRecord::Base action == BUILD_FAILED end + def assigned? + action == ASSIGNED + end + def action_name ACTION_NAMES[action] end @@ -117,6 +121,14 @@ class Todo < ActiveRecord::Base end end + def self_added? + author == user + end + + def self_assigned? + assigned? && self_added? + end + private def keep_around_commit diff --git a/app/models/user.rb b/app/models/user.rb index bd9c9f99663..2b7ebe6c1a7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -118,7 +118,7 @@ class User < ActiveRecord::Base presence: true, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE } validates :username, - namespace: true, + dynamic_path: true, presence: true, uniqueness: { case_sensitive: false } diff --git a/app/serializers/deployment_entity.rb b/app/serializers/deployment_entity.rb index d610fbe0c8a..8b3de1bed0f 100644 --- a/app/serializers/deployment_entity.rb +++ b/app/serializers/deployment_entity.rb @@ -18,8 +18,10 @@ class DeploymentEntity < Grape::Entity end end + expose :created_at expose :tag expose :last? + expose :user, using: UserEntity expose :commit, using: CommitEntity expose :deployable, using: BuildEntity diff --git a/app/serializers/deployment_serializer.rb b/app/serializers/deployment_serializer.rb new file mode 100644 index 00000000000..cba5c3f311f --- /dev/null +++ b/app/serializers/deployment_serializer.rb @@ -0,0 +1,8 @@ +class DeploymentSerializer < BaseSerializer + entity DeploymentEntity + + def represent_concise(resource, opts = {}) + opts[:only] = [:iid, :id, :sha, :created_at, :tag, :last?, :id, ref: [:name]] + represent(resource, opts) + end +end diff --git a/app/serializers/status_entity.rb b/app/serializers/status_entity.rb index 944472f3e51..188c3747f18 100644 --- a/app/serializers/status_entity.rb +++ b/app/serializers/status_entity.rb @@ -7,6 +7,9 @@ class StatusEntity < Grape::Entity expose :details_path expose :favicon do |status| - ActionController::Base.helpers.image_path(File.join('ci_favicons', "#{status.favicon}.ico")) + dir = 'ci_favicons' + dir = File.join(dir, 'dev') if Rails.env.development? + + ActionController::Base.helpers.image_path(File.join(dir, "#{status.favicon}.ico")) end end diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index d45da5180e1..bc0e7ad4e39 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -28,7 +28,7 @@ module MergeRequests def find_target_project return target_project if target_project.present? && can?(current_user, :read_project, target_project) - project.forked_from_project || project + project.default_merge_request_target end def find_target_branch diff --git a/app/services/projects/enable_deploy_key_service.rb b/app/services/projects/enable_deploy_key_service.rb index 3cf4264ce9b..121385afca3 100644 --- a/app/services/projects/enable_deploy_key_service.rb +++ b/app/services/projects/enable_deploy_key_service.rb @@ -4,7 +4,10 @@ module Projects key = accessible_keys.find_by(id: params[:key_id] || params[:id]) return unless key - project.deploy_keys << key + unless project.deploy_keys.include?(key) + project.deploy_keys << key + end + key end diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index b6e88b0280f..8ae61694b50 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -281,7 +281,7 @@ class TodoService def attributes_for_target(target) attributes = { - project_id: target.project.id, + project_id: target&.project&.id, target_id: target.id, target_type: target.class.name, commit_id: nil diff --git a/app/validators/dynamic_path_validator.rb b/app/validators/dynamic_path_validator.rb new file mode 100644 index 00000000000..226eb6b313c --- /dev/null +++ b/app/validators/dynamic_path_validator.rb @@ -0,0 +1,208 @@ +# DynamicPathValidator +# +# Custom validator for GitLab path values. +# These paths are assigned to `Namespace` (& `Group` as a subclass) & `Project` +# +# Values are checked for formatting and exclusion from a list of reserved path +# names. +class DynamicPathValidator < ActiveModel::EachValidator + # All routes that appear on the top level must be listed here. + # This will make sure that groups cannot be created with these names + # as these routes would be masked by the paths already in place. + # + # Example: + # /api/api-project + # + # the path `api` shouldn't be allowed because it would be masked by `api/*` + # + TOP_LEVEL_ROUTES = %w[ + - + .well-known + abuse_reports + admin + all + api + assets + autocomplete + ci + dashboard + explore + files + groups + health_check + help + hooks + import + invites + issues + jwt + koding + member + merge_requests + new + notes + notification_settings + oauth + profile + projects + public + repository + robots.txt + s + search + sent_notifications + services + snippets + teams + u + unicorn_test + unsubscribes + uploads + users + ].freeze + + # This list should contain all words following `/*namespace_id/:project_id` in + # routes that contain a second wildcard. + # + # Example: + # /*namespace_id/:project_id/badges/*ref/build + # + # If `badges` was allowed as a project/group name, we would not be able to access the + # `badges` route for those projects: + # + # Consider a namespace with path `foo/bar` and a project called `badges`. + # The route to the build badge would then be `/foo/bar/badges/badges/master/build.svg` + # + # When accessing this path the route would be matched to the `badges` path + # with the following params: + # - namespace_id: `foo` + # - project_id: `bar` + # - ref: `badges/master` + # + # Failing to find the project, this would result in a 404. + # + # By rejecting `badges` the router can _count_ on the fact that `badges` will + # be preceded by the `namespace/project`. + WILDCARD_ROUTES = %w[ + badges + blame + blob + builds + commits + create + create_dir + edit + environments/folders + files + find_file + gitlab-lfs/objects + info/lfs/objects + new + preview + raw + refs + tree + update + wikis + ].freeze + + # These are all the paths that follow `/groups/*id/ or `/groups/*group_id` + # We need to reject these because we have a `/groups/*id` page that is the same + # as the `/*id`. + # + # If we would allow a subgroup to be created with the name `activity` then + # this group would not be accessible through `/groups/parent/activity` since + # this would map to the activity-page of it's parent. + GROUP_ROUTES = %w[ + activity + avatar + edit + group_members + issues + labels + merge_requests + milestones + projects + subgroups + ].freeze + + CHILD_ROUTES = (WILDCARD_ROUTES | GROUP_ROUTES).freeze + + def self.without_reserved_wildcard_paths_regex + @without_reserved_wildcard_paths_regex ||= regex_excluding_child_paths(WILDCARD_ROUTES) + end + + def self.without_reserved_child_paths_regex + @without_reserved_child_paths_regex ||= regex_excluding_child_paths(CHILD_ROUTES) + end + + # This is used to validate a full path. + # It doesn't match paths + # - Starting with one of the top level words + # - Containing one of the child level words in the middle of a path + def self.regex_excluding_child_paths(child_routes) + reserved_top_level_words = Regexp.union(TOP_LEVEL_ROUTES) + not_starting_in_reserved_word = %r{\A/?(?!(#{reserved_top_level_words})(/|\z))} + + reserved_child_level_words = Regexp.union(child_routes) + not_containing_reserved_child = %r{(?!\S+/(#{reserved_child_level_words})(/|\z))} + + %r{#{not_starting_in_reserved_word} + #{not_containing_reserved_child} + #{Gitlab::Regex.full_namespace_regex}}x + end + + def self.valid?(path) + path =~ Gitlab::Regex.full_namespace_regex && !full_path_reserved?(path) + end + + def self.full_path_reserved?(path) + path = path.to_s.downcase + _project_part, namespace_parts = path.reverse.split('/', 2).map(&:reverse) + + wildcard_reserved?(path) || child_reserved?(namespace_parts) + end + + def self.child_reserved?(path) + return false unless path + + path !~ without_reserved_child_paths_regex + end + + def self.wildcard_reserved?(path) + return false unless path + + path !~ without_reserved_wildcard_paths_regex + end + + delegate :full_path_reserved?, + :child_reserved?, + to: :class + + def path_reserved_for_record?(record, value) + full_path = record.respond_to?(:full_path) ? record.full_path : value + + # For group paths the entire path cannot contain a reserved child word + # The path doesn't contain the last `_project_part` so we need to validate + # if the entire path. + # Example: + # A *group* with full path `parent/activity` is reserved. + # A *project* with full path `parent/activity` is allowed. + if record.is_a? Group + child_reserved?(full_path) + else + full_path_reserved?(full_path) + end + end + + def validate_each(record, attribute, value) + unless value =~ Gitlab::Regex.namespace_regex + record.errors.add(attribute, Gitlab::Regex.namespace_regex_message) + return + end + + if path_reserved_for_record?(record, value) + record.errors.add(attribute, "#{value} is a reserved name") + end + end +end diff --git a/app/validators/namespace_validator.rb b/app/validators/namespace_validator.rb deleted file mode 100644 index 77ca033e97f..00000000000 --- a/app/validators/namespace_validator.rb +++ /dev/null @@ -1,73 +0,0 @@ -# NamespaceValidator -# -# Custom validator for GitLab namespace values. -# -# Values are checked for formatting and exclusion from a list of reserved path -# names. -class NamespaceValidator < ActiveModel::EachValidator - RESERVED = %w[ - .well-known - admin - all - assets - ci - dashboard - files - groups - help - hooks - issues - merge_requests - new - notes - profile - projects - public - repository - robots.txt - s - search - services - snippets - teams - u - unsubscribes - users - ].freeze - - WILDCARD_ROUTES = %w[tree commits wikis new edit create update logs_tree - preview blob blame raw files create_dir find_file - artifacts graphs refs badges].freeze - - STRICT_RESERVED = (RESERVED + WILDCARD_ROUTES).freeze - - def self.valid?(value) - !reserved?(value) && follow_format?(value) - end - - def self.reserved?(value, strict: false) - if strict - STRICT_RESERVED.include?(value) - else - RESERVED.include?(value) - end - end - - def self.follow_format?(value) - value =~ Gitlab::Regex.namespace_regex - end - - delegate :reserved?, :follow_format?, to: :class - - def validate_each(record, attribute, value) - unless follow_format?(value) - record.errors.add(attribute, Gitlab::Regex.namespace_regex_message) - end - - strict = record.is_a?(Group) && record.parent_id - - if reserved?(value, strict: strict) - record.errors.add(attribute, "#{value} is a reserved name") - end - end -end diff --git a/app/validators/project_path_validator.rb b/app/validators/project_path_validator.rb deleted file mode 100644 index ee2ae65be7b..00000000000 --- a/app/validators/project_path_validator.rb +++ /dev/null @@ -1,35 +0,0 @@ -# ProjectPathValidator -# -# Custom validator for GitLab project path values. -# -# Values are checked for formatting and exclusion from a list of reserved path -# names. -class ProjectPathValidator < ActiveModel::EachValidator - # All project routes with wildcard argument must be listed here. - # Otherwise it can lead to routing issues when route considered as project name. - # - # Example: - # /group/project/tree/deploy_keys - # - # without tree as reserved name routing can match 'group/project' as group name, - # 'tree' as project name and 'deploy_keys' as route. - # - RESERVED = (NamespaceValidator::STRICT_RESERVED - - %w[dashboard help ci admin search notes services assets profile public]).freeze - - def self.valid?(value) - !reserved?(value) - end - - def self.reserved?(value) - RESERVED.include?(value) - end - - delegate :reserved?, to: :class - - def validate_each(record, attribute, value) - if reserved?(value) - record.errors.add(attribute, "#{value} is a reserved name") - end - end -end diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 8c9fdc9ae42..53f0a1e7fde 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -73,6 +73,12 @@ = container_reg %span.light.pull-right = boolean_to_icon Gitlab.config.registry.enabled + - gitlab_pages = 'GitLab Pages' + - gitlab_pages_enabled = Gitlab.config.pages.enabled + %p{ "aria-label" => "#{gitlab_pages}: status " + (gitlab_pages_enabled ? "on" : "off") } + = gitlab_pages + %span.light.pull-right + = boolean_to_icon gitlab_pages_enabled .col-md-4 %h4 diff --git a/app/views/admin/hooks/_form.html.haml b/app/views/admin/hooks/_form.html.haml new file mode 100644 index 00000000000..6217d5fb135 --- /dev/null +++ b/app/views/admin/hooks/_form.html.haml @@ -0,0 +1,40 @@ += form_errors(hook) + +.form-group + = form.label :url, 'URL', class: 'control-label' + .col-sm-10 + = form.text_field :url, class: 'form-control' +.form-group + = form.label :token, 'Secret Token', class: 'control-label' + .col-sm-10 + = form.text_field :token, class: 'form-control' + %p.help-block + Use this token to validate received payloads +.form-group + = form.label :url, 'Trigger', class: 'control-label' + .col-sm-10.prepend-top-10 + %div + System hook will be triggered on set of events like creating project + or adding ssh key. But you can also enable extra triggers like Push events. + + .prepend-top-default + = form.check_box :push_events, class: 'pull-left' + .prepend-left-20 + = form.label :push_events, class: 'list-label' do + %strong Push events + %p.light + This url will be triggered by a push to the repository + %div + = form.check_box :tag_push_events, class: 'pull-left' + .prepend-left-20 + = form.label :tag_push_events, class: 'list-label' do + %strong Tag push events + %p.light + This url will be triggered when a new tag is pushed to the repository +.form-group + = form.label :enable_ssl_verification, 'SSL verification', class: 'control-label checkbox' + .col-sm-10 + .checkbox + = form.label :enable_ssl_verification do + = form.check_box :enable_ssl_verification + %strong Enable SSL verification diff --git a/app/views/admin/hooks/edit.html.haml b/app/views/admin/hooks/edit.html.haml new file mode 100644 index 00000000000..0777f5e2629 --- /dev/null +++ b/app/views/admin/hooks/edit.html.haml @@ -0,0 +1,14 @@ +- page_title 'Edit System Hook' +%h3.page-title + Edit System Hook + +%p.light + #{link_to 'System hooks ', help_page_path('system_hooks/system_hooks'), class: 'vlink'} can be + used for binding events when GitLab creates a User or Project. + +%hr + += form_for @hook, as: :hook, url: admin_hook_path, html: { class: 'form-horizontal' } do |f| + = render partial: 'form', locals: { form: f, hook: @hook } + .form-actions + = f.submit 'Save changes', class: 'btn btn-create' diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml index d9c7948763a..71117758921 100644 --- a/app/views/admin/hooks/index.html.haml +++ b/app/views/admin/hooks/index.html.haml @@ -1,57 +1,17 @@ -- page_title "System Hooks" +- page_title 'System Hooks' %h3.page-title System hooks %p.light - #{link_to "System hooks ", help_page_path("system_hooks/system_hooks"), class: "vlink"} can be + #{link_to 'System hooks ', help_page_path('system_hooks/system_hooks'), class: 'vlink'} can be used for binding events when GitLab creates a User or Project. %hr - = form_for @hook, as: :hook, url: admin_hooks_path, html: { class: 'form-horizontal' } do |f| - = form_errors(@hook) - - .form-group - = f.label :url, 'URL', class: 'control-label' - .col-sm-10 - = f.text_field :url, class: 'form-control' - .form-group - = f.label :token, 'Secret Token', class: 'control-label' - .col-sm-10 - = f.text_field :token, class: 'form-control' - %p.help-block - Use this token to validate received payloads - .form-group - = f.label :url, "Trigger", class: 'control-label' - .col-sm-10.prepend-top-10 - %div - System hook will be triggered on set of events like creating project - or adding ssh key. But you can also enable extra triggers like Push events. - - .prepend-top-default - = f.check_box :push_events, class: 'pull-left' - .prepend-left-20 - = f.label :push_events, class: 'list-label' do - %strong Push events - %p.light - This url will be triggered by a push to the repository - %div - = f.check_box :tag_push_events, class: 'pull-left' - .prepend-left-20 - = f.label :tag_push_events, class: 'list-label' do - %strong Tag push events - %p.light - This url will be triggered when a new tag is pushed to the repository - .form-group - = f.label :enable_ssl_verification, "SSL verification", class: 'control-label checkbox' - .col-sm-10 - .checkbox - = f.label :enable_ssl_verification do - = f.check_box :enable_ssl_verification - %strong Enable SSL verification + = render partial: 'form', locals: { form: f, hook: @hook } .form-actions - = f.submit "Add system hook", class: "btn btn-create" + = f.submit 'Add system hook', class: 'btn btn-create' %hr - if @hooks.any? @@ -62,11 +22,12 @@ - @hooks.each do |hook| %li .controls - = link_to 'Test hook', admin_hook_test_path(hook), class: "btn btn-sm" - = link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-remove btn-sm" + = link_to 'Test hook', test_admin_hook_path(hook), class: 'btn btn-sm' + = link_to 'Edit', edit_admin_hook_path(hook), class: 'btn btn-sm' + = link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: 'btn btn-remove btn-sm' .monospace= hook.url %div - %w(push_events tag_push_events issues_events note_events merge_requests_events build_events).each do |trigger| - if hook.send(trigger) %span.label.label-gray= trigger.titleize - %span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"} + %span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'} diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index 840d843f069..89d0bbb7126 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -175,11 +175,7 @@ .panel-body - if @user.can_be_removed? && can?(current_user, :destroy_user, @user) %p Deleting a user has the following effects: - %ul - %li All user content like authored issues, snippets, comments will be removed - - rp = @user.personal_projects.count - - unless rp.zero? - %li #{pluralize rp, 'personal project'} will be removed and cannot be restored + = render 'users/deletion_guidance', user: @user %br = link_to 'Remove user', [:admin, @user], data: { confirm: "USER #{@user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-remove" - else diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml index 0e848386ebb..4594c52b34b 100644 --- a/app/views/dashboard/_groups_head.html.haml +++ b/app/views/dashboard/_groups_head.html.haml @@ -2,10 +2,10 @@ %ul.nav-links = nav_link(page: dashboard_groups_path) do = link_to dashboard_groups_path, title: 'Your groups' do - Your Groups + Your groups = nav_link(page: explore_groups_path) do - = link_to explore_groups_path, title: 'Explore groups' do - Explore Groups + = link_to explore_groups_path, title: 'Explore public groups' do + Explore public groups .nav-controls = render 'shared/groups/search_form' = render 'shared/groups/dropdown' diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml index d0c12aa57ae..38fd053ae65 100644 --- a/app/views/dashboard/todos/_todo.html.haml +++ b/app/views/dashboard/todos/_todo.html.haml @@ -9,7 +9,7 @@ .title-item.author-name - if todo.author - = link_to_author(todo) + = link_to_author(todo, self_added: todo.self_added?) - else (removed) @@ -22,6 +22,10 @@ - else (removed) + - if todo.self_assigned? + .title-item.action-name + to yourself + .title-item · diff --git a/app/views/discussions/_notes.html.haml b/app/views/discussions/_notes.html.haml index 34789808f10..964473ee3e0 100644 --- a/app/views/discussions/_notes.html.haml +++ b/app/views/discussions/_notes.html.haml @@ -1,6 +1,6 @@ .discussion-notes %ul.notes{ data: { discussion_id: discussion.id } } - = render partial: "projects/notes/note", collection: discussion.notes, as: :note + = render partial: "shared/notes/note", collection: discussion.notes, as: :note - if current_user .discussion-reply-holder diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml index bb2cd0d44c8..ffe07b217a7 100644 --- a/app/views/explore/groups/index.html.haml +++ b/app/views/explore/groups/index.html.haml @@ -7,6 +7,15 @@ = render 'explore/head' = render 'nav' +- if cookies[:explore_groups_landing_dismissed] != 'true' + .explore-groups.landing.content-block.js-explore-groups-landing.hidden + %button.dismiss-button{ type: 'button', 'aria-label' => 'Dismiss' }= icon('times') + .svg-container + = custom_icon('icon_explore_groups_splash') + .inner-content + %p Below you will find all the groups that are public. + %p You can easily contribute to them by requesting to join these groups. + - if @groups.present? = render 'groups' - else diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml index f93b6b63426..b20e3a22133 100644 --- a/app/views/help/index.html.haml +++ b/app/views/help/index.html.haml @@ -27,8 +27,7 @@ .row .col-md-8 .documentation-index - = preserve do - = markdown(@help_index) + = markdown(@help_index) .col-md-4 .panel.panel-default .panel-heading diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index d843cacd52d..73f33e69d68 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -118,11 +118,7 @@ - if @user.can_be_removed? && can?(current_user, :destroy_user, @user) %p Deleting an account has the following effects: - %ul - %li All user content like authored issues, snippets, comments will be removed - - rp = current_user.personal_projects.count - - unless rp.zero? - %li #{pluralize rp, 'personal project'} will be removed and cannot be restored + = render 'users/deletion_guidance', user: current_user = link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove" - else - if @user.solo_owned_groups.present? diff --git a/app/views/projects/_wiki.html.haml b/app/views/projects/_wiki.html.haml index 41d42740f61..2bab22e125d 100644 --- a/app/views/projects/_wiki.html.haml +++ b/app/views/projects/_wiki.html.haml @@ -2,8 +2,7 @@ %div{ class: container_class } .wiki-holder.prepend-top-default.append-bottom-default .wiki - = preserve do - = render_wiki_content(@wiki_home) + = render_wiki_content(@wiki_home) - else - can_create_wiki = can?(current_user, :create_wiki, @project) .project-home-empty{ class: [('row-content-block' if can_create_wiki), ('content-block' unless can_create_wiki)] } diff --git a/app/views/projects/blob/_header.html.haml b/app/views/projects/blob/_header.html.haml index b89cd460455..cd098acda81 100644 --- a/app/views/projects/blob/_header.html.haml +++ b/app/views/projects/blob/_header.html.haml @@ -1,22 +1,13 @@ - blame = local_assigns.fetch(:blame, false) .js-file-title.file-title-flex-parent - .file-header-content - = blob_icon blob.mode, blob.name - - %strong.file-title-name - = blob.name - - = copy_file_path_button(blob.path) - - %small - = number_to_human_size(blob.raw_size) + = render 'projects/blob/header_content', blob: blob .file-actions.hidden-xs = render 'projects/blob/viewer_switcher', blob: blob unless blame .btn-group{ role: "group" }< - = copy_blob_source_button(blob) if !blame && blob.rendered_as_text?(ignore_errors: false) - = open_raw_file_button(namespace_project_raw_path(@project.namespace, @project, @id)) + = copy_blob_source_button(blob) unless blame + = open_raw_blob_button(blob) = view_on_environment_button(@commit.sha, @path, @environment) if @environment .btn-group{ role: "group" }< diff --git a/app/views/projects/blob/_header_content.html.haml b/app/views/projects/blob/_header_content.html.haml new file mode 100644 index 00000000000..98bedae650a --- /dev/null +++ b/app/views/projects/blob/_header_content.html.haml @@ -0,0 +1,10 @@ +.file-header-content + = blob_icon blob.mode, blob.name + + %strong.file-title-name + = blob.name + + = copy_file_path_button(blob.path) + + %small + = number_to_human_size(blob.raw_size) diff --git a/app/views/projects/blob/viewers/_markup.html.haml b/app/views/projects/blob/viewers/_markup.html.haml index b9a998d96ff..230305b488d 100644 --- a/app/views/projects/blob/viewers/_markup.html.haml +++ b/app/views/projects/blob/viewers/_markup.html.haml @@ -1,3 +1,4 @@ - blob = viewer.blob +- rendered_markup = blob.rendered_markup if blob.respond_to?(:rendered_markup) .file-content.wiki - = markup(blob.name, blob.data) + = markup(blob.name, blob.data, rendered: rendered_markup) diff --git a/app/views/projects/blob/viewers/_video.html.haml b/app/views/projects/blob/viewers/_video.html.haml new file mode 100644 index 00000000000..595a890a27d --- /dev/null +++ b/app/views/projects/blob/viewers/_video.html.haml @@ -0,0 +1,2 @@ +.file-content.video + %video{ src: blob_raw_url, controls: true, data: { setup: '{}' } } diff --git a/app/views/projects/boards/components/sidebar/_assignee.html.haml b/app/views/projects/boards/components/sidebar/_assignee.html.haml index e75ce305440..0f424334521 100644 --- a/app/views/projects/boards/components/sidebar/_assignee.html.haml +++ b/app/views/projects/boards/components/sidebar/_assignee.html.haml @@ -28,8 +28,9 @@ ":value" => "issue.assignee.id", "v-if" => "issue.assignee" } .dropdown - %button.dropdown-menu-toggle.js-user-search.js-author-search.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", field_name: "issue[assignee_id]", first_user: (current_user.username if current_user), current_user: "true", project_id: @project.id, null_user: "true" }, + %button.dropdown-menu-toggle.js-user-search.js-author-search.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", field_name: "issue[assignee_id]", first_user: (current_user.username if current_user), current_user: "true", project_id: @project.id, null_user: "true", null_user_default: "true" }, ":data-issuable-id" => "issue.id", + ":data-selected" => "assigneeId", ":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" } Select assignee = icon("chevron-down") diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml index c4159ce1a36..43191fae9e6 100644 --- a/app/views/projects/builds/_sidebar.html.haml +++ b/app/views/projects/builds/_sidebar.html.haml @@ -48,7 +48,7 @@ - if @build.merge_request %p.build-detail-row %span.build-light-text Merge Request: - = link_to "#{@build.merge_request.to_reference}", merge_request_path(@build.merge_request) + = link_to "#{@build.merge_request.to_reference}", merge_request_path(@build.merge_request), class: 'bold' - if @build.duration %p.build-detail-row %span.build-light-text Duration: diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index 0d11da2451a..16d2646cb4e 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -1,9 +1,11 @@ - @no_container = true +- container_class = !fluid_layout && diff_view == :inline ? 'container-limited' : '' +- limited_container_width = fluid_layout || diff_view == :inline ? '' : 'limit-container-width' - page_title "#{@commit.title} (#{@commit.short_id})", "Commits" - page_description @commit.description = render "projects/commits/head" -%div{ class: container_class } +.container-fluid{ class: [limited_container_width, container_class] } = render "commit_box" - if @commit.status = render "ci_menu" diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 85e442e115c..50e0bad3ccf 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -60,7 +60,7 @@ git init git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} git add . - git commit + git commit -m "Initial commit" git push -u origin master %fieldset diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml index 766f119116f..e8f8fbbcf09 100644 --- a/app/views/projects/environments/metrics.html.haml +++ b/app/views/projects/environments/metrics.html.haml @@ -5,7 +5,7 @@ = page_specific_javascript_bundle_tag('monitoring') = render "projects/pipelines/head" -.prometheus-container{ class: container_class, 'data-has-metrics': "#{@environment.has_metrics?}" } +#js-metrics.prometheus-container{ class: container_class, data: { has_metrics: "#{@environment.has_metrics?}", deployment_endpoint: namespace_project_environment_deployments_path(@project.namespace, @project, @environment, format: :json) } } .top-area .row .col-sm-6 diff --git a/app/views/projects/hooks/_index.html.haml b/app/views/projects/hooks/_index.html.haml index 8faad351463..676b7c345bc 100644 --- a/app/views/projects/hooks/_index.html.haml +++ b/app/views/projects/hooks/_index.html.haml @@ -1 +1,23 @@ -= render 'shared/web_hooks/form', hook: @hook, hooks: @hooks, url_components: [@project.namespace.becomes(Namespace), @project] +.row.prepend-top-default + .col-lg-3 + %h4.prepend-top-0 + = page_title + %p + #{link_to 'Webhooks', help_page_path('user/project/integrations/webhooks')} can be + used for binding events when something is happening within the project. + + .col-lg-9.append-bottom-default + = form_for @hook, as: :hook, url: polymorphic_path([@project.namespace.becomes(Namespace), @project, :hooks]) do |f| + = render partial: 'shared/web_hooks/form', locals: { form: f, hook: @hook } + = f.submit 'Add webhook', class: 'btn btn-create' + + %hr + %h5.prepend-top-default + Webhooks (#{@hooks.count}) + - if @hooks.any? + %ul.well-list + - @hooks.each do |hook| + = render 'project_hook', hook: hook + - else + %p.settings-message.text-center.append-bottom-0 + No webhooks found, add one in the form above. diff --git a/app/views/projects/hooks/edit.html.haml b/app/views/projects/hooks/edit.html.haml new file mode 100644 index 00000000000..7998713be1f --- /dev/null +++ b/app/views/projects/hooks/edit.html.haml @@ -0,0 +1,14 @@ += render 'projects/settings/head' + +.row.prepend-top-default + .col-lg-3 + %h4.prepend-top-0 + = page_title + %p + #{link_to 'Webhooks', help_page_path('user/project/integrations/webhooks')} can be + used for binding events when something is happening within the project. + .col-lg-9.append-bottom-default + = form_for [@project.namespace.becomes(Namespace), @project, @hook], as: :hook, url: namespace_project_hook_path do |f| + = render partial: 'shared/web_hooks/form', locals: { form: f, hook: @hook } + = f.submit 'Save changes', class: 'btn btn-create' + diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 4d56aa214e2..2a871966aa8 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -58,8 +58,7 @@ - if @issue.description.present? .description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' } .wiki - = preserve do - = markdown_field(@issue, :description) + = markdown_field(@issue, :description) %textarea.hidden.js-task-list-field = @issue.description = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago') diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml index 8d134aaac67..9cf24e10842 100644 --- a/app/views/projects/merge_requests/_new_compare.html.haml +++ b/app/views/projects/merge_requests/_new_compare.html.haml @@ -38,7 +38,7 @@ .panel-heading Target branch .panel-body.clearfix - - projects = @project.forked_from_project.nil? ? [@project] : [@project, @project.forked_from_project] + - projects = target_projects(@project) .merge-request-select.dropdown = f.hidden_field :target_project_id = dropdown_toggle f.object.target_project.path_with_namespace, { toggle: "dropdown", field_name: "#{f.object_name}[target_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-target-project" } diff --git a/app/views/projects/merge_requests/show/_mr_box.html.haml b/app/views/projects/merge_requests/show/_mr_box.html.haml index 683cb8a5a27..8a390cf8700 100644 --- a/app/views/projects/merge_requests/show/_mr_box.html.haml +++ b/app/views/projects/merge_requests/show/_mr_box.html.haml @@ -6,8 +6,7 @@ - if @merge_request.description.present? .description{ class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : '' } .wiki - = preserve do - = markdown_field(@merge_request, :description) + = markdown_field(@merge_request, :description) %textarea.hidden.js-task-list-field = @merge_request.description diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 33bbbd9a3f8..4b692aba11c 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -43,8 +43,7 @@ - if @milestone.description.present? .description .wiki - = preserve do - = markdown_field(@milestone, :description) + = markdown_field(@milestone, :description) - if can?(current_user, :read_issue, @project) && @milestone.total_items_count(current_user).zero? .alert.alert-success.prepend-top-default diff --git a/app/views/projects/notes/_actions.html.haml b/app/views/projects/notes/_actions.html.haml new file mode 100644 index 00000000000..718b52dd82e --- /dev/null +++ b/app/views/projects/notes/_actions.html.haml @@ -0,0 +1,44 @@ +- access = note_max_access_for_user(note) +- if access + %span.note-role= access + +- if note.resolvable? + - can_resolve = can?(current_user, :resolve_note, note) + %resolve-btn{ "project-path" => project_path(note.project), + "discussion-id" => note.discussion_id(@noteable), + ":note-id" => note.id, + ":resolved" => note.resolved?, + ":can-resolve" => can_resolve, + ":author-name" => "'#{j(note.author.name)}'", + "author-avatar" => note.author.avatar_url, + ":note-truncated" => "'#{j(truncate(note.note, length: 17))}'", + ":resolved-by" => "'#{j(note.resolved_by.try(:name))}'", + "v-show" => "#{can_resolve || note.resolved?}", + "inline-template" => true, + "ref" => "note_#{note.id}" } + + %button.note-action-button.line-resolve-btn{ type: "button", + class: ("is-disabled" unless can_resolve), + ":class" => "{ 'is-active': isResolved }", + ":aria-label" => "buttonText", + "@click" => "resolve", + ":title" => "buttonText", + ":ref" => "'button'" } + + = icon('spin spinner', 'v-show' => 'loading', class: 'loading', 'aria-hidden' => 'true', 'aria-label' => 'Loading') + %div{ 'v-show' => '!loading' }= render 'shared/icons/icon_status_success.svg' + +- if current_user + - if note.emoji_awardable? + - user_authored = note.user_authored?(current_user) + = link_to '#', title: 'Award Emoji', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored}", data: { position: 'right' } do + = icon('spinner spin') + %span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face') + %span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley') + %span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile') + + - if note_editable + = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do + = icon('pencil', class: 'link-highlight') + = 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 danger' do + = icon('trash-o', class: 'danger-highlight') diff --git a/app/views/projects/notes/_edit.html.haml b/app/views/projects/notes/_edit.html.haml new file mode 100644 index 00000000000..f1e251d65b7 --- /dev/null +++ b/app/views/projects/notes/_edit.html.haml @@ -0,0 +1,3 @@ +.original-note-content.hidden{ data: { post_url: namespace_project_note_path(@project.namespace, @project, note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } } + #{note.note} +%textarea.hidden.js-task-list-field.original-task-list{ data: {update_url: namespace_project_note_path(@project.namespace, @project, note) } }= note.note diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml deleted file mode 100644 index 7cf604bb772..00000000000 --- a/app/views/projects/notes/_note.html.haml +++ /dev/null @@ -1,102 +0,0 @@ -- return unless note.author -- return if note.cross_reference_not_visible_for?(current_user) - -- note_editable = note_editable?(note) -%li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable, note_id: note.id} } - .timeline-entry-inner - .timeline-icon - - if note.system - = icon_for_system_note(note) - - else - %a{ href: user_path(note.author) } - = image_tag avatar_icon(note.author), alt: '', class: 'avatar s40' - .timeline-content - .note-header - .note-header-info - %a{ href: user_path(note.author) } - %span.hidden-xs - = sanitize(note.author.name) - %span.note-headline-light - = note.author.to_reference - %span.note-headline-light - %span.note-headline-meta - - unless note.system - commented - - if note.system - %span.system-note-message - = note.redacted_note_html - %a{ href: "##{dom_id(note)}" } - = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago') - - unless note.system? - .note-actions - - access = note_max_access_for_user(note) - - if access - %span.note-role= access - - - if note.resolvable? - - can_resolve = can?(current_user, :resolve_note, note) - %resolve-btn{ "project-path" => project_path(note.project), - "discussion-id" => note.discussion_id(@noteable), - ":note-id" => note.id, - ":resolved" => note.resolved?, - ":can-resolve" => can_resolve, - ":author-name" => "'#{j(note.author.name)}'", - "author-avatar" => note.author.avatar_url, - ":note-truncated" => "'#{j(truncate(note.note, length: 17))}'", - ":resolved-by" => "'#{j(note.resolved_by.try(:name))}'", - "v-show" => "#{can_resolve || note.resolved?}", - "inline-template" => true, - "ref" => "note_#{note.id}" } - - %button.note-action-button.line-resolve-btn{ type: "button", - class: ("is-disabled" unless can_resolve), - ":class" => "{ 'is-active': isResolved }", - ":aria-label" => "buttonText", - "@click" => "resolve", - ":title" => "buttonText", - ":ref" => "'button'" } - - = icon("spin spinner", "v-show" => "loading", class: 'loading') - %div{ 'v-show' => '!loading' }= render "shared/icons/icon_status_success.svg" - - - if current_user - - if note.emoji_awardable? - - user_authored = note.user_authored?(current_user) - = link_to '#', title: 'Award Emoji', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored}", data: { position: 'right' } do - = icon('spinner spin') - %span{ class: "link-highlight award-control-icon-neutral" }= custom_icon('emoji_slightly_smiling_face') - %span{ class: "link-highlight award-control-icon-positive" }= custom_icon('emoji_smiley') - %span{ class: "link-highlight award-control-icon-super-positive" }= custom_icon('emoji_smile') - - - if note_editable - = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do - = icon('pencil', class: 'link-highlight') - = 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 danger' do - = icon('trash-o', class: 'danger-highlight') - .note-body{ class: note_editable ? 'js-task-list-container' : '' } - .note-text.md - = preserve do - = note.redacted_note_html - = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) - - if note_editable - .original-note-content.hidden{ data: { post_url: namespace_project_note_path(@project.namespace, @project, note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } } - #{note.note} - %textarea.hidden.js-task-list-field.original-task-list{ data: {update_url: namespace_project_note_path(@project.namespace, @project, note) } }= note.note - .note-awards - = render 'award_emoji/awards_block', awardable: note, inline: false - - if note.system - .system-note-commit-list-toggler - Toggle commit list - %i.fa.fa-angle-down - - if note.attachment.url - .note-attachment - - if note.attachment.image? - = link_to note.attachment.url, target: '_blank' do - = image_tag note.attachment.url, class: 'note-image-attach' - .attachment - = link_to note.attachment.url, target: '_blank' do - = icon('paperclip') - = note.attachment_identifier - = link_to delete_attachment_namespace_project_note_path(note.project.namespace, note.project, note), - title: 'Delete this attachment', method: :delete, remote: true, data: { confirm: 'Are you sure you want to remove the attachment?' }, class: 'danger js-note-attachment-delete' do - = icon('trash-o', class: 'cred') diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index 90a150aa74c..555228623cc 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -1,5 +1,5 @@ %ul#notes-list.notes.main-notes-list.timeline - = render "projects/notes/notes" + = render "shared/notes/notes" = render 'projects/notes/edit_form' diff --git a/app/views/projects/settings/_head.html.haml b/app/views/projects/settings/_head.html.haml index e50a543ffa8..5a5ade03624 100644 --- a/app/views/projects/settings/_head.html.haml +++ b/app/views/projects/settings/_head.html.haml @@ -14,7 +14,7 @@ %span Members - if can_edit - = nav_link(controller: [:integrations, :services]) do + = nav_link(controller: [:integrations, :services, :hooks]) do = link_to project_settings_integrations_path(@project), title: 'Integrations' do %span Integrations diff --git a/app/views/projects/settings/integrations/_project_hook.html.haml b/app/views/projects/settings/integrations/_project_hook.html.haml index ceabe2eab3d..8dc276a3bec 100644 --- a/app/views/projects/settings/integrations/_project_hook.html.haml +++ b/app/views/projects/settings/integrations/_project_hook.html.haml @@ -9,6 +9,7 @@ .col-md-4.col-lg-5.text-right-lg.prepend-top-5 %span.append-right-10.inline SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"} + = link_to "Edit", edit_namespace_project_hook_path(@project.namespace, @project, hook), class: "btn btn-sm" = link_to "Test", test_namespace_project_hook_path(@project.namespace, @project, hook), class: "btn btn-sm" = link_to namespace_project_hook_path(@project.namespace, @project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-transparent" do %span.sr-only Remove diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml index 7c6be003d4c..7a175f63eeb 100644 --- a/app/views/projects/snippets/show.html.haml +++ b/app/views/projects/snippets/show.html.haml @@ -4,7 +4,7 @@ .project-snippets %article.file-holder.snippet-file-content - = render 'shared/snippets/blob', raw_path: raw_namespace_project_snippet_path(@project.namespace, @project, @snippet) + = render 'shared/snippets/blob' .row-content-block.top-block.content-component-block = render 'award_emoji/awards_block', awardable: @snippet, inline: true diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index 451e011a4b8..4c4f3655b97 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -24,8 +24,7 @@ - if release && release.description.present? .description.prepend-top-default .wiki - = preserve do - = markdown_field(release, :description) + = markdown_field(release, :description) .row-fixed-content.controls = render 'projects/buttons/download', project: @project, ref: tag.name, pipeline: @tags_pipelines[tag.name] diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml index 1c4135c8a54..e996ae3e4fc 100644 --- a/app/views/projects/tags/show.html.haml +++ b/app/views/projects/tags/show.html.haml @@ -38,7 +38,6 @@ - if @release.description.present? .description .wiki - = preserve do - = markdown_field(@release, :description) + = markdown_field(@release, :description) - else This tag has no release notes. diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index 3609461b721..c00967546aa 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -27,7 +27,6 @@ .wiki-holder.prepend-top-default.append-bottom-default .wiki - = preserve do - = render_wiki_content(@page) + = render_wiki_content(@page) = render 'sidebar' diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml index fc4385865a4..b4bc8982c05 100644 --- a/app/views/search/results/_issue.html.haml +++ b/app/views/search/results/_issue.html.haml @@ -8,7 +8,6 @@ .pull-right ##{issue.iid} - if issue.description.present? .description.term - = preserve do - = search_md_sanitize(issue, :description) + = search_md_sanitize(issue, :description) %span.light #{issue.project.name_with_namespace} diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml index 9b583285d02..1a5499e4d58 100644 --- a/app/views/search/results/_merge_request.html.haml +++ b/app/views/search/results/_merge_request.html.haml @@ -9,7 +9,6 @@ .pull-right= merge_request.to_reference - if merge_request.description.present? .description.term - = preserve do - = search_md_sanitize(merge_request, :description) + = search_md_sanitize(merge_request, :description) %span.light #{merge_request.project.name_with_namespace} diff --git a/app/views/search/results/_milestone.html.haml b/app/views/search/results/_milestone.html.haml index 9664f65a36e..2daa96e34d1 100644 --- a/app/views/search/results/_milestone.html.haml +++ b/app/views/search/results/_milestone.html.haml @@ -5,5 +5,4 @@ - if milestone.description.present? .description.term - = preserve do - = search_md_sanitize(milestone, :description) + = search_md_sanitize(milestone, :description) diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml index f3701b89bb4..a7e178dfa71 100644 --- a/app/views/search/results/_note.html.haml +++ b/app/views/search/results/_note.html.haml @@ -22,5 +22,4 @@ .note-search-result .term - = preserve do - = search_md_sanitize(note, :note) + = search_md_sanitize(note, :note) diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml index f2fe5742c12..c4a5131c1a7 100644 --- a/app/views/search/results/_snippet_blob.html.haml +++ b/app/views/search/results/_snippet_blob.html.haml @@ -39,7 +39,7 @@ .blob-content - snippet_chunks.each do |chunk| - unless chunk[:data].empty? - = highlight(snippet.file_name, chunk[:data], repository: nil, plain: snippet.no_highlighting?) + = highlight(snippet.file_name, chunk[:data], repository: nil, plain: snippet.blob.no_highlighting?) - else .file-content.code .nothing-here-block Empty file diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml index 9c5053dace5..b200e5fc528 100644 --- a/app/views/shared/_service_settings.html.haml +++ b/app/views/shared/_service_settings.html.haml @@ -4,8 +4,7 @@ = render "projects/services/#{@service.to_param}/help", subject: subject - elsif @service.help.present? .well - = preserve do - = markdown @service.help + = markdown @service.help .service-settings .form-group diff --git a/app/views/shared/icons/_icon_explore_groups_splash.svg b/app/views/shared/icons/_icon_explore_groups_splash.svg new file mode 100644 index 00000000000..79f17872739 --- /dev/null +++ b/app/views/shared/icons/_icon_explore_groups_splash.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="62" height="50" viewBox="260 141 62 50" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M24.6 7.7H56c3.3 0 6 2.7 6 6V44c0 3.3-2.7 6-6 6H6c-3.3 0-6-2.7-6-6V4.8C0 2 2.2 0 4.8 0h12c1.5 0 3 1 4 2l3.8 5.7z"/><mask id="e" width="62" height="50" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><path id="b" d="M4.2 13c3.7 0 4-1.7 4-4.5S7 4.8 4.2 4.8 0 5.8 0 8.5C0 11.3.5 13 4.2 13z"/><mask id="f" width="10.7" height="10.7" x="-1.2" y="-1.2"><path fill="#fff" d="M-1.2 3.6H9.5v10.7H-1.2z"/><use xlink:href="#b"/></mask><path id="c" d="M4.2 13c3.7 0 4-1.7 4-4.5S7 4.8 4.2 4.8 0 5.8 0 8.5C0 11.3.5 13 4.2 13z"/><mask id="g" width="10.7" height="10.7" x="-1.2" y="-1.2"><path fill="#fff" d="M-1.2 3.6H9.5v10.7H-1.2z"/><use xlink:href="#c"/></mask><path id="d" d="M5.4 16c4.7 0 5.3-2.3 5.3-6 0-3.5-1.7-4.6-5.3-4.6C1.7 5.4 0 6.4 0 10s.6 6 5.4 6z"/><mask id="h" width="13.1" height="13.1" x="-1.2" y="-1.2"><path fill="#fff" d="M-1.2 4.2h13v13H-1z"/><use xlink:href="#d"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(260 141)"><use fill="#FFF" stroke="#EEE" stroke-width="4.8" mask="url(#e)" xlink:href="#a"/><g transform="translate(33.98 22.62)"><use fill="#B5A7DD" xlink:href="#b"/><use stroke="#FFF" stroke-width="2.4" mask="url(#f)" xlink:href="#b"/><ellipse cx="4.2" cy="3" fill="#B5A7DD" stroke="#FFF" stroke-width="1.2" rx="3" ry="3"/></g><g transform="translate(19.673 22.62)"><use fill="#B5A7DD" xlink:href="#c"/><use stroke="#FFF" stroke-width="2.4" mask="url(#g)" xlink:href="#c"/><ellipse cx="4.2" cy="3" fill="#B5A7DD" stroke="#FFF" stroke-width="1.2" rx="3" ry="3"/></g><g transform="translate(25.635 21.43)"><use fill="#B5A7DD" xlink:href="#d"/><use stroke="#FFF" stroke-width="2.4" mask="url(#h)" xlink:href="#d"/><ellipse cx="5.4" cy="3.6" fill="#B5A7DD" stroke="#FFF" stroke-width="1.2" rx="3.6" ry="3.6"/></g></g></svg> diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index f1350169bbe..b6fce5e3cd4 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -117,7 +117,7 @@ .issues_bulk_update.hide = form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: 'bulk-update' do .filter-item.inline - = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do + = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]", default_label: "Status" } } ) do %ul %li %a{ href: "#", data: { id: "reopen" } } Open @@ -125,13 +125,13 @@ %a{ href: "#", data: { id: "close" } } Closed .filter-item.inline = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", - placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } }) + placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]", default_label: "Assignee" } }) .filter-item.inline - = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) + = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true, default_label: "Milestone" } }) .filter-item.inline.labels-filter - = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], dropdown_title: 'Apply a label', show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true } + = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], dropdown_title: 'Apply a label', show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true, default_label: "Labels" } .filter-item.inline - = dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do + = dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]", default_label: "Subscription" } } ) do %ul %li %a{ href: "#", data: { id: "subscribe" } } Subscribe diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 2e0d6a129fb..bc638e994f3 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -48,7 +48,7 @@ .selectbox.hide-collapsed = f.hidden_field 'assignee_id', value: issuable.assignee_id, id: 'issue_assignee_id' - = dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_id]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } }) + = dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_id]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true, null_user_default: true, selected: issuable.assignee_id } }) .block.milestone .sidebar-collapsed-icon @@ -136,7 +136,7 @@ - selected_labels.each do |label| = hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil .dropdown - %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project) } } + %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project) } } %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) } = multi_label_name(selected_labels, "Labels") = icon('chevron-down', 'aria-hidden': 'true') diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml index 774d20fb5ba..5e8a2a0f5d8 100644 --- a/app/views/shared/milestones/_sidebar.html.haml +++ b/app/views/shared/milestones/_sidebar.html.haml @@ -64,7 +64,7 @@ %span.remaining-days= remaining_days - if !project || can?(current_user, :read_issue, project) - .block + .block.issues .sidebar-collapsed-icon %strong = icon('hashtag', 'aria-hidden': 'true') @@ -85,7 +85,7 @@ Closed: = milestone.issues_visible_to_user(current_user).closed.count - .block + .block.merge-requests .sidebar-collapsed-icon %strong = icon('exclamation', 'aria-hidden': 'true') diff --git a/app/views/shared/milestones/_tab_loading.html.haml b/app/views/shared/milestones/_tab_loading.html.haml new file mode 100644 index 00000000000..68458c2d0aa --- /dev/null +++ b/app/views/shared/milestones/_tab_loading.html.haml @@ -0,0 +1,2 @@ +.text-center.prepend-top-default + = icon('spin spinner 2x', 'aria-hidden': 'true', 'aria-label': 'Loading tab content') diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml index 9a4502873ef..6a6d817b344 100644 --- a/app/views/shared/milestones/_tabs.html.haml +++ b/app/views/shared/milestones/_tabs.html.haml @@ -1,27 +1,27 @@ .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller .fade-left= icon('angle-left') .fade-right= icon('angle-right') - %ul.nav-links.scrolling-tabs + %ul.nav-links.scrolling-tabs.js-milestone-tabs - if milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project) %li.active = link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do Issues %span.badge= milestone.issues_visible_to_user(current_user).size %li - = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do + = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-endpoint': milestone_merge_request_tab_path(milestone) do Merge Requests %span.badge= milestone.merge_requests.size - else %li.active - = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do + = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-endpoint': milestone_merge_request_tab_path(milestone) do Merge Requests %span.badge= milestone.merge_requests.size %li - = link_to '#tab-participants', 'data-toggle' => 'tab' do + = link_to '#tab-participants', 'data-toggle' => 'tab', 'data-endpoint': milestone_participants_tab_path(milestone) do Participants %span.badge= milestone.participants.count %li - = link_to '#tab-labels', 'data-toggle' => 'tab' do + = link_to '#tab-labels', 'data-toggle' => 'tab', 'data-endpoint': milestone_labels_tab_path(milestone) do Labels %span.badge= milestone.labels.count @@ -30,14 +30,18 @@ .tab-content.milestone-content - if milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project) - .tab-pane.active#tab-issues + .tab-pane.active#tab-issues{ data: { sort_endpoint: (sort_issues_namespace_project_milestone_path(@project.namespace, @project, @milestone) if @project && current_user) } } = render 'shared/milestones/issues_tab', issues: milestone.issues_visible_to_user(current_user).include_associations, show_project_name: show_project_name, show_full_project_name: show_full_project_name - .tab-pane#tab-merge-requests - = render 'shared/milestones/merge_requests_tab', merge_requests: milestone.merge_requests, show_project_name: show_project_name, show_full_project_name: show_full_project_name + .tab-pane#tab-merge-requests{ data: { sort_endpoint: (sort_merge_requests_namespace_project_milestone_path(@project.namespace, @project, @milestone) if @project && current_user) } } + -# loaded async + = render "shared/milestones/tab_loading" - else - .tab-pane.active#tab-merge-requests - = render 'shared/milestones/merge_requests_tab', merge_requests: milestone.merge_requests, show_project_name: show_project_name, show_full_project_name: show_full_project_name + .tab-pane.active#tab-merge-requests{ data: { sort_endpoint: (sort_merge_requests_namespace_project_milestone_path(@project.namespace, @project, @milestone) if @project && current_user) } } + -# loaded async + = render "shared/milestones/tab_loading" .tab-pane#tab-participants - = render 'shared/milestones/participants_tab', users: milestone.participants + -# loaded async + = render "shared/milestones/tab_loading" .tab-pane#tab-labels - = render 'shared/milestones/labels_tab', labels: milestone.labels + -# loaded async + = render "shared/milestones/tab_loading" diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml new file mode 100644 index 00000000000..731270d4127 --- /dev/null +++ b/app/views/shared/notes/_note.html.haml @@ -0,0 +1,62 @@ +- return unless note.author +- return if note.cross_reference_not_visible_for?(current_user) + +- note_editable = note_editable?(note) +%li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable, note_id: note.id} } + .timeline-entry-inner + .timeline-icon + - if note.system + = icon_for_system_note(note) + - else + %a{ href: user_path(note.author) } + = image_tag avatar_icon(note.author), alt: '', class: 'avatar s40' + .timeline-content + .note-header + .note-header-info + %a{ href: user_path(note.author) } + %span.hidden-xs + = sanitize(note.author.name) + %span.note-headline-light + = note.author.to_reference + %span.note-headline-light + %span.note-headline-meta + - unless note.system + commented + - if note.system + %span.system-note-message + = note.redacted_note_html + %a{ href: "##{dom_id(note)}" } + = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago') + - unless note.system? + .note-actions + - if note.for_personal_snippet? + = render 'snippets/notes/actions', note: note, note_editable: note_editable + - else + = render 'projects/notes/actions', note: note, note_editable: note_editable + .note-body{ class: note_editable ? 'js-task-list-container' : '' } + .note-text.md + = note.redacted_note_html + = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) + - if note_editable + - if note.for_personal_snippet? + = render 'snippets/notes/edit', note: note + - else + = render 'projects/notes/edit', note: note + .note-awards + = render 'award_emoji/awards_block', awardable: note, inline: false + - if note.system + .system-note-commit-list-toggler + Toggle commit list + %i.fa.fa-angle-down + - if note.attachment.url + .note-attachment + - if note.attachment.image? + = link_to note.attachment.url, target: '_blank' do + = image_tag note.attachment.url, class: 'note-image-attach' + .attachment + = link_to note.attachment.url, target: '_blank' do + = icon('paperclip') + = note.attachment_identifier + = link_to delete_attachment_namespace_project_note_path(note.project.namespace, note.project, note), + title: 'Delete this attachment', method: :delete, remote: true, data: { confirm: 'Are you sure you want to remove the attachment?' }, class: 'danger js-note-attachment-delete' do + = icon('trash-o', class: 'cred') diff --git a/app/views/projects/notes/_notes.html.haml b/app/views/shared/notes/_notes.html.haml index 2b2bab09c74..cfdfeeb9e97 100644 --- a/app/views/projects/notes/_notes.html.haml +++ b/app/views/shared/notes/_notes.html.haml @@ -1,8 +1,8 @@ - if defined?(@discussions) - @discussions.each do |discussion| - if discussion.individual_note? - = render partial: "projects/notes/note", collection: discussion.notes, as: :note + = render partial: "shared/notes/note", collection: discussion.notes, as: :note - else = render 'discussions/discussion', discussion: discussion - else - = render partial: "projects/notes/note", collection: @notes, as: :note + = render partial: "shared/notes/note", collection: @notes, as: :note diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml index 37c66ff2595..11f0fa7c49f 100644 --- a/app/views/shared/snippets/_blob.html.haml +++ b/app/views/shared/snippets/_blob.html.haml @@ -1,29 +1,14 @@ +- blob = @snippet.blob .js-file-title.file-title-flex-parent - .file-header-content - = blob_icon @snippet.mode, @snippet.path - - %strong.file-title-name - = @snippet.path - - = copy_file_path_button(@snippet.path) + = render 'projects/blob/header_content', blob: blob .file-actions.hidden-xs + = render 'projects/blob/viewer_switcher', blob: blob + .btn-group{ role: "group" }< - = copy_blob_source_button(@snippet) - = open_raw_file_button(raw_path) + = copy_blob_source_button(blob) + = open_raw_blob_button(blob) - - if defined?(download_path) && download_path - = link_to icon('download'), download_path, class: "btn btn-sm has-tooltip", title: 'Download', data: { container: 'body' } + = link_to icon('download'), download_snippet_path(@snippet), target: '_blank', class: "btn btn-sm has-tooltip", title: 'Download', data: { container: 'body' } -- if @snippet.content.empty? - .file-content.code - .nothing-here-block Empty file -- else - - if markup?(@snippet.file_name) - .file-content.wiki - - if gitlab_markdown?(@snippet.file_name) - = preserve(markdown_field(@snippet, :content)) - - else - = markup(@snippet.file_name, @snippet.content) - - else - = render 'shared/file_highlight', blob: @snippet += render 'projects/blob/content', blob: blob diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml index ee3be3c789a..37c3e61912c 100644 --- a/app/views/shared/web_hooks/_form.html.haml +++ b/app/views/shared/web_hooks/_form.html.haml @@ -1,102 +1,82 @@ -.row.prepend-top-default - .col-lg-3 - %h4.prepend-top-0 - = page_title - %p - #{link_to "Webhooks", help_page_path("user/project/integrations/webhooks")} can be - used for binding events when something is happening within the project. - .col-lg-9.append-bottom-default - = form_for hook, as: :hook, url: polymorphic_path(url_components + [:hooks]) do |f| - = form_errors(hook) += form_errors(hook) - .form-group - = f.label :url, "URL", class: 'label-light' - = f.text_field :url, class: "form-control", placeholder: 'http://example.com/trigger-ci.json' - .form-group - = f.label :token, "Secret Token", class: 'label-light' - = f.text_field :token, class: "form-control", placeholder: '' - %p.help-block - Use this token to validate received payloads. It will be sent with the request in the X-Gitlab-Token HTTP header. - .form-group - = f.label :url, "Trigger", class: 'label-light' - %ul.list-unstyled - %li - = f.check_box :push_events, class: 'pull-left' - .prepend-left-20 - = f.label :push_events, class: 'list-label' do - %strong Push events - %p.light - This URL will be triggered by a push to the repository - %li - = f.check_box :tag_push_events, class: 'pull-left' - .prepend-left-20 - = f.label :tag_push_events, class: 'list-label' do - %strong Tag push events - %p.light - This URL will be triggered when a new tag is pushed to the repository - %li - = f.check_box :note_events, class: 'pull-left' - .prepend-left-20 - = f.label :note_events, class: 'list-label' do - %strong Comments - %p.light - This URL will be triggered when someone adds a comment - %li - = f.check_box :issues_events, class: 'pull-left' - .prepend-left-20 - = f.label :issues_events, class: 'list-label' do - %strong Issues events - %p.light - This URL will be triggered when an issue is created/updated/merged - %li - = f.check_box :confidential_issues_events, class: 'pull-left' - .prepend-left-20 - = f.label :confidential_issues_events, class: 'list-label' do - %strong Confidential Issues events - %p.light - This URL will be triggered when a confidential issue is created/updated/merged - %li - = f.check_box :merge_requests_events, class: 'pull-left' - .prepend-left-20 - = f.label :merge_requests_events, class: 'list-label' do - %strong Merge Request events - %p.light - This URL will be triggered when a merge request is created/updated/merged - %li - = f.check_box :build_events, class: 'pull-left' - .prepend-left-20 - = f.label :build_events, class: 'list-label' do - %strong Jobs events - %p.light - This URL will be triggered when the job status changes - %li - = f.check_box :pipeline_events, class: 'pull-left' - .prepend-left-20 - = f.label :pipeline_events, class: 'list-label' do - %strong Pipeline events - %p.light - This URL will be triggered when the pipeline status changes - %li - = f.check_box :wiki_page_events, class: 'pull-left' - .prepend-left-20 - = f.label :wiki_page_events, class: 'list-label' do - %strong Wiki Page events - %p.light - This URL will be triggered when a wiki page is created/updated - .form-group - = f.label :enable_ssl_verification, "SSL verification", class: 'label-light checkbox' - .checkbox - = f.label :enable_ssl_verification do - = f.check_box :enable_ssl_verification - %strong Enable SSL verification - = f.submit "Add webhook", class: "btn btn-create" - %hr - %h5.prepend-top-default - Webhooks (#{hooks.count}) - - if hooks.any? - %ul.well-list - - hooks.each do |hook| - = render "project_hook", hook: hook - - else - %p.settings-message.text-center.append-bottom-0 - No webhooks found, add one in the form above. +.form-group + = form.label :url, 'URL', class: 'label-light' + = form.text_field :url, class: 'form-control', placeholder: 'http://example.com/trigger-ci.json' +.form-group + = form.label :token, 'Secret Token', class: 'label-light' + = form.text_field :token, class: 'form-control', placeholder: '' + %p.help-block + Use this token to validate received payloads. It will be sent with the request in the X-Gitlab-Token HTTP header. +.form-group + = form.label :url, 'Trigger', class: 'label-light' + %ul.list-unstyled + %li + = form.check_box :push_events, class: 'pull-left' + .prepend-left-20 + = form.label :push_events, class: 'list-label' do + %strong Push events + %p.light + This URL will be triggered by a push to the repository + %li + = form.check_box :tag_push_events, class: 'pull-left' + .prepend-left-20 + = form.label :tag_push_events, class: 'list-label' do + %strong Tag push events + %p.light + This URL will be triggered when a new tag is pushed to the repository + %li + = form.check_box :note_events, class: 'pull-left' + .prepend-left-20 + = form.label :note_events, class: 'list-label' do + %strong Comments + %p.light + This URL will be triggered when someone adds a comment + %li + = form.check_box :issues_events, class: 'pull-left' + .prepend-left-20 + = form.label :issues_events, class: 'list-label' do + %strong Issues events + %p.light + This URL will be triggered when an issue is created/updated/merged + %li + = form.check_box :confidential_issues_events, class: 'pull-left' + .prepend-left-20 + = form.label :confidential_issues_events, class: 'list-label' do + %strong Confidential Issues events + %p.light + This URL will be triggered when a confidential issue is created/updated/merged + %li + = form.check_box :merge_requests_events, class: 'pull-left' + .prepend-left-20 + = form.label :merge_requests_events, class: 'list-label' do + %strong Merge Request events + %p.light + This URL will be triggered when a merge request is created/updated/merged + %li + = form.check_box :build_events, class: 'pull-left' + .prepend-left-20 + = form.label :build_events, class: 'list-label' do + %strong Jobs events + %p.light + This URL will be triggered when the job status changes + %li + = form.check_box :pipeline_events, class: 'pull-left' + .prepend-left-20 + = form.label :pipeline_events, class: 'list-label' do + %strong Pipeline events + %p.light + This URL will be triggered when the pipeline status changes + %li + = form.check_box :wiki_page_events, class: 'pull-left' + .prepend-left-20 + = form.label :wiki_page_events, class: 'list-label' do + %strong Wiki Page events + %p.light + This URL will be triggered when a wiki page is created/updated +.form-group + = form.label :enable_ssl_verification, 'SSL verification', class: 'label-light checkbox' + .checkbox + = form.label :enable_ssl_verification do + = form.check_box :enable_ssl_verification + %strong Enable SSL verification diff --git a/app/views/snippets/notes/_actions.html.haml b/app/views/snippets/notes/_actions.html.haml new file mode 100644 index 00000000000..dace11e5474 --- /dev/null +++ b/app/views/snippets/notes/_actions.html.haml @@ -0,0 +1,13 @@ +- if current_user + - if note.emoji_awardable? + - user_authored = note.user_authored?(current_user) + = link_to '#', title: 'Award Emoji', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored}", data: { position: 'right' } do + = icon('spinner spin') + %span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face') + %span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley') + %span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile') + - if note_editable + = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do + = icon('pencil', class: 'link-highlight') + = link_to snippet_note_path(note.noteable, 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 danger' do + = icon('trash-o', class: 'danger-highlight') diff --git a/app/views/snippets/notes/_edit.html.haml b/app/views/snippets/notes/_edit.html.haml new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/app/views/snippets/notes/_edit.html.haml diff --git a/app/views/snippets/notes/_notes.html.haml b/app/views/snippets/notes/_notes.html.haml new file mode 100644 index 00000000000..f07d6b8c126 --- /dev/null +++ b/app/views/snippets/notes/_notes.html.haml @@ -0,0 +1,2 @@ +%ul#notes-list.notes.main-notes-list.timeline + = render "projects/notes/notes" diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index e5711ca79c7..98287cba5b4 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -3,7 +3,10 @@ = render 'shared/snippets/header' %article.file-holder.snippet-file-content - = render 'shared/snippets/blob', raw_path: raw_snippet_path(@snippet), download_path: download_snippet_path(@snippet) + = render 'shared/snippets/blob' .row-content-block.top-block.content-component-block = render 'award_emoji/awards_block', awardable: @snippet, inline: true + +%ul#notes-list.notes.main-notes-list.timeline + #notes= render 'shared/notes/notes' diff --git a/app/views/users/_deletion_guidance.html.haml b/app/views/users/_deletion_guidance.html.haml new file mode 100644 index 00000000000..0545cab538c --- /dev/null +++ b/app/views/users/_deletion_guidance.html.haml @@ -0,0 +1,10 @@ +- user = local_assigns.fetch(:user) + +%ul + %li + %p + Certain user content will be moved to a system-wide "Ghost User" in order to maintain content for posterity. For further information, please refer to the + = link_to 'user account deletion documentation.', help_page_path("user/profile/account/delete_account", anchor: "associated-records") + - personal_projects_count = user.personal_projects.count + - unless personal_projects_count.zero? + %li #{pluralize(personal_projects_count, 'personal project')} will be removed and cannot be restored diff --git a/app/workers/expire_build_instance_artifacts_worker.rb b/app/workers/expire_build_instance_artifacts_worker.rb index eb403c134d1..7b59e976492 100644 --- a/app/workers/expire_build_instance_artifacts_worker.rb +++ b/app/workers/expire_build_instance_artifacts_worker.rb @@ -8,7 +8,7 @@ class ExpireBuildInstanceArtifactsWorker .reorder(nil) .find_by(id: build_id) - return unless build.try(:project) + return unless build&.project && !build.project.pending_delete Rails.logger.info "Removing artifacts for build #{build.id}..." build.erase_artifacts! diff --git a/changelogs/unreleased/12910-personal-snippets-notes-show.yml b/changelogs/unreleased/12910-personal-snippets-notes-show.yml new file mode 100644 index 00000000000..15c6f3c5e6a --- /dev/null +++ b/changelogs/unreleased/12910-personal-snippets-notes-show.yml @@ -0,0 +1,4 @@ +--- +title: Display comments for personal snippets +merge_request: +author: diff --git a/changelogs/unreleased/19364-webhook-edit.yml b/changelogs/unreleased/19364-webhook-edit.yml new file mode 100644 index 00000000000..60e154b8b83 --- /dev/null +++ b/changelogs/unreleased/19364-webhook-edit.yml @@ -0,0 +1,4 @@ +--- +title: Implement ability to edit hooks +merge_request: 10816 +author: Alexander Randa diff --git a/changelogs/unreleased/2246-uuid-is-nil-for-new-installation.yml b/changelogs/unreleased/2246-uuid-is-nil-for-new-installation.yml deleted file mode 100644 index 70d35f06af4..00000000000 --- a/changelogs/unreleased/2246-uuid-is-nil-for-new-installation.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Lazily sets UUID in ApplicationSetting for new installations -merge_request: -author: diff --git a/changelogs/unreleased/26488-target-disabled-mr.yml b/changelogs/unreleased/26488-target-disabled-mr.yml new file mode 100644 index 00000000000..02058481ccf --- /dev/null +++ b/changelogs/unreleased/26488-target-disabled-mr.yml @@ -0,0 +1,4 @@ +--- +title: Disallow merge requests from fork when source project have disabled merge requests +merge_request: +author: mhasbini diff --git a/changelogs/unreleased/28020-improve-todo-list-when-comes-from-yourself.yml b/changelogs/unreleased/28020-improve-todo-list-when-comes-from-yourself.yml new file mode 100644 index 00000000000..14aecc35bd2 --- /dev/null +++ b/changelogs/unreleased/28020-improve-todo-list-when-comes-from-yourself.yml @@ -0,0 +1,4 @@ +--- +title: Improve text on todo list when the todo action comes from yourself +merge_request: 10594 +author: Jacopo Beschi @jacopo-beschi diff --git a/changelogs/unreleased/28968-prevent-people-from-creating-branches-if-they-don-have-permission-to-push.yml b/changelogs/unreleased/28968-prevent-people-from-creating-branches-if-they-don-have-permission-to-push.yml new file mode 100644 index 00000000000..6612cfd8866 --- /dev/null +++ b/changelogs/unreleased/28968-prevent-people-from-creating-branches-if-they-don-have-permission-to-push.yml @@ -0,0 +1,4 @@ +--- +title: Prevent people from creating branches if they don't have persmission to push +merge_request: +author: diff --git a/changelogs/unreleased/29673-500-internal-server-error-when-enabling-a-deploy-key-more-than-once-through-api.yml b/changelogs/unreleased/29673-500-internal-server-error-when-enabling-a-deploy-key-more-than-once-through-api.yml new file mode 100644 index 00000000000..3e62ede1521 --- /dev/null +++ b/changelogs/unreleased/29673-500-internal-server-error-when-enabling-a-deploy-key-more-than-once-through-api.yml @@ -0,0 +1,4 @@ +--- +title: Detect already enabled DeployKeys in EnableDeployKeyService +merge_request: +author: diff --git a/changelogs/unreleased/30272-bvl-reject-more-namespaces.yml b/changelogs/unreleased/30272-bvl-reject-more-namespaces.yml new file mode 100644 index 00000000000..56bce084546 --- /dev/null +++ b/changelogs/unreleased/30272-bvl-reject-more-namespaces.yml @@ -0,0 +1,4 @@ +--- +title: Improve validation of namespace & project paths +merge_request: 10413 +author: diff --git a/changelogs/unreleased/30535-display-whether-pages-is-enabled-in-the-admin-dashboard.yml b/changelogs/unreleased/30535-display-whether-pages-is-enabled-in-the-admin-dashboard.yml new file mode 100644 index 00000000000..4452b13037b --- /dev/null +++ b/changelogs/unreleased/30535-display-whether-pages-is-enabled-in-the-admin-dashboard.yml @@ -0,0 +1,4 @@ +--- +title: Display GitLab Pages status in Admin Dashboard +merge_request: +author: diff --git a/changelogs/unreleased/30645-show-pipeline-events-description.yml b/changelogs/unreleased/30645-show-pipeline-events-description.yml deleted file mode 100644 index fb75dde1d86..00000000000 --- a/changelogs/unreleased/30645-show-pipeline-events-description.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix pipeline events description for Slack and Mattermost integration -merge_request: 10908 -author: diff --git a/changelogs/unreleased/30667-creating-new-label-on-new-issue-causing-bug.yml b/changelogs/unreleased/30667-creating-new-label-on-new-issue-causing-bug.yml new file mode 100644 index 00000000000..ce0ea69211e --- /dev/null +++ b/changelogs/unreleased/30667-creating-new-label-on-new-issue-causing-bug.yml @@ -0,0 +1,4 @@ +--- +title: Fix label creation from issuable for subgroup projects +merge_request: +author: diff --git a/changelogs/unreleased/30973-fix-network-graph-ordering.yml b/changelogs/unreleased/30973-fix-network-graph-ordering.yml deleted file mode 100644 index 420ec107842..00000000000 --- a/changelogs/unreleased/30973-fix-network-graph-ordering.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix ordering of commits in the network graph -merge_request: 10936 -author: diff --git a/changelogs/unreleased/31057-unnecessary-padding-along-left-side-of-assignees-dropdown.yml b/changelogs/unreleased/31057-unnecessary-padding-along-left-side-of-assignees-dropdown.yml new file mode 100644 index 00000000000..0d82bf878c7 --- /dev/null +++ b/changelogs/unreleased/31057-unnecessary-padding-along-left-side-of-assignees-dropdown.yml @@ -0,0 +1,4 @@ +--- +title: Show checkmark on current assignee in assignee dropdown +merge_request: 10767 +author: diff --git a/changelogs/unreleased/31254-change-git-commit-command-in-existing-folder.yml b/changelogs/unreleased/31254-change-git-commit-command-in-existing-folder.yml new file mode 100644 index 00000000000..950336ea932 --- /dev/null +++ b/changelogs/unreleased/31254-change-git-commit-command-in-existing-folder.yml @@ -0,0 +1,4 @@ +--- +title: Change Git commit command in Existing folder to git commit -m +merge_request: 10900 +author: TM Lee diff --git a/changelogs/unreleased/31274-creating-schedule-trigger--causes-http-500-when-accessing-settings-ci_cd.yml b/changelogs/unreleased/31274-creating-schedule-trigger--causes-http-500-when-accessing-settings-ci_cd.yml new file mode 100644 index 00000000000..b0c33ab3fa4 --- /dev/null +++ b/changelogs/unreleased/31274-creating-schedule-trigger--causes-http-500-when-accessing-settings-ci_cd.yml @@ -0,0 +1,4 @@ +--- +title: Fix error on CI/CD Settings page related to invalid pipeline trigger +merge_request: 10948 +author: dosuken123 diff --git a/changelogs/unreleased/31292-milestone-sidebar-display-incorect-number-of-mr-when-minimized.yml b/changelogs/unreleased/31292-milestone-sidebar-display-incorect-number-of-mr-when-minimized.yml deleted file mode 100644 index dee831c668b..00000000000 --- a/changelogs/unreleased/31292-milestone-sidebar-display-incorect-number-of-mr-when-minimized.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed milestone sidebar showing incorrect number of MRs when collapsed -merge_request: 10933 -author: diff --git a/changelogs/unreleased/31383-admin-remove-user-text-incorrect.yml b/changelogs/unreleased/31383-admin-remove-user-text-incorrect.yml new file mode 100644 index 00000000000..a2a2c0c42bd --- /dev/null +++ b/changelogs/unreleased/31383-admin-remove-user-text-incorrect.yml @@ -0,0 +1,4 @@ +--- +title: Note Ghost user and refer to user deletion documentation +merge_request: +author: diff --git a/changelogs/unreleased/31560-workhose-gitaly-from-mirror.yml b/changelogs/unreleased/31560-workhose-gitaly-from-mirror.yml new file mode 100644 index 00000000000..02c048cb3b4 --- /dev/null +++ b/changelogs/unreleased/31560-workhose-gitaly-from-mirror.yml @@ -0,0 +1,4 @@ +--- +title: rickettm Add repo parameter to gitaly:install and workhorse:install rake tasks +merge_request: 10979 +author: M. Ricketts diff --git a/changelogs/unreleased/31704-misaligned-buttons-in-wiki-pages.yml b/changelogs/unreleased/31704-misaligned-buttons-in-wiki-pages.yml new file mode 100644 index 00000000000..46368b4510e --- /dev/null +++ b/changelogs/unreleased/31704-misaligned-buttons-in-wiki-pages.yml @@ -0,0 +1,4 @@ +--- +title: Fix misaligned buttons in wiki pages +merge_request: 11043 +author: diff --git a/changelogs/unreleased/add-tanuki-ci-status-favicons.yml b/changelogs/unreleased/add-tanuki-ci-status-favicons.yml new file mode 100644 index 00000000000..b60ad81947a --- /dev/null +++ b/changelogs/unreleased/add-tanuki-ci-status-favicons.yml @@ -0,0 +1,4 @@ +--- +title: Updated CI status favicons to include the tanuki +merge_request: 10923 +author: diff --git a/changelogs/unreleased/add_index_on_ci_runners_contacted_at.yml b/changelogs/unreleased/add_index_on_ci_runners_contacted_at.yml deleted file mode 100644 index 10c3206c2ff..00000000000 --- a/changelogs/unreleased/add_index_on_ci_runners_contacted_at.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add index on ci_runners.contacted_at -merge_request: 10876 -author: blackst0ne diff --git a/changelogs/unreleased/async-milestone-tabs.yml b/changelogs/unreleased/async-milestone-tabs.yml new file mode 100644 index 00000000000..c199a95610c --- /dev/null +++ b/changelogs/unreleased/async-milestone-tabs.yml @@ -0,0 +1,4 @@ +--- +title: Load milestone tabs asynchronously to increase initial load performance +merge_request: +author: diff --git a/changelogs/unreleased/commit-limited-container-width.yml b/changelogs/unreleased/commit-limited-container-width.yml new file mode 100644 index 00000000000..253646b13da --- /dev/null +++ b/changelogs/unreleased/commit-limited-container-width.yml @@ -0,0 +1,4 @@ +--- +title: Side-by-side view in commits correcly expands full window width +merge_request: +author: diff --git a/changelogs/unreleased/dm-blob-download-button.yml b/changelogs/unreleased/dm-blob-download-button.yml new file mode 100644 index 00000000000..bd31137b670 --- /dev/null +++ b/changelogs/unreleased/dm-blob-download-button.yml @@ -0,0 +1,4 @@ +--- +title: Show Raw button as Download for binary files +merge_request: +author: diff --git a/changelogs/unreleased/dm-fix-ghost-user-validation.yml b/changelogs/unreleased/dm-fix-ghost-user-validation.yml deleted file mode 100644 index 4214786cb5a..00000000000 --- a/changelogs/unreleased/dm-fix-ghost-user-validation.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Skip validation when creating internal (ghost, service desk) users -merge_request: -author: diff --git a/changelogs/unreleased/dm-snippet-blob-viewers.yml b/changelogs/unreleased/dm-snippet-blob-viewers.yml new file mode 100644 index 00000000000..f218095f401 --- /dev/null +++ b/changelogs/unreleased/dm-snippet-blob-viewers.yml @@ -0,0 +1,4 @@ +--- +title: Use blob viewers for snippets +merge_request: +author: diff --git a/changelogs/unreleased/dm-snippet-download-button.yml b/changelogs/unreleased/dm-snippet-download-button.yml new file mode 100644 index 00000000000..09ece1e7f98 --- /dev/null +++ b/changelogs/unreleased/dm-snippet-download-button.yml @@ -0,0 +1,4 @@ +--- +title: Add download button to project snippets +merge_request: +author: diff --git a/changelogs/unreleased/dm-video-viewer.yml b/changelogs/unreleased/dm-video-viewer.yml new file mode 100644 index 00000000000..1c42b16e967 --- /dev/null +++ b/changelogs/unreleased/dm-video-viewer.yml @@ -0,0 +1,4 @@ +--- +title: Display video blobs in-line like images +merge_request: +author: diff --git a/changelogs/unreleased/dont-blow-up-when-email-has-no-references-header.yml b/changelogs/unreleased/dont-blow-up-when-email-has-no-references-header.yml new file mode 100644 index 00000000000..a4345b70744 --- /dev/null +++ b/changelogs/unreleased/dont-blow-up-when-email-has-no-references-header.yml @@ -0,0 +1,5 @@ +--- +title: Gracefully handle failures for incoming emails which do not match on the To + header, and have no References header +merge_request: +author: diff --git a/changelogs/unreleased/emoji-button-titles.yml b/changelogs/unreleased/emoji-button-titles.yml new file mode 100644 index 00000000000..c8e1b2c6c6b --- /dev/null +++ b/changelogs/unreleased/emoji-button-titles.yml @@ -0,0 +1,4 @@ +--- +title: Added title to award emoji buttons +merge_request: +author: diff --git a/changelogs/unreleased/fix-import-export-missing-attributes.yml b/changelogs/unreleased/fix-import-export-missing-attributes.yml new file mode 100644 index 00000000000..a1338b4eb48 --- /dev/null +++ b/changelogs/unreleased/fix-import-export-missing-attributes.yml @@ -0,0 +1,4 @@ +--- +title: Add missing project attributes to Import/Export +merge_request: +author: diff --git a/changelogs/unreleased/fix-n-plus-one-project-features.yml b/changelogs/unreleased/fix-n-plus-one-project-features.yml new file mode 100644 index 00000000000..1b19bd65224 --- /dev/null +++ b/changelogs/unreleased/fix-n-plus-one-project-features.yml @@ -0,0 +1,4 @@ +--- +title: Remove N+1 queries in processing MR references +merge_request: +author: diff --git a/changelogs/unreleased/tc-job-page-mr-bold.yml b/changelogs/unreleased/tc-job-page-mr-bold.yml new file mode 100644 index 00000000000..0243a259119 --- /dev/null +++ b/changelogs/unreleased/tc-job-page-mr-bold.yml @@ -0,0 +1,4 @@ +--- +title: Make MR link in build sidebar bold +merge_request: +author: diff --git a/config/initializers/active_record_query_trace.rb b/config/initializers/active_record_query_trace.rb deleted file mode 100644 index 4b3c2803b3b..00000000000 --- a/config/initializers/active_record_query_trace.rb +++ /dev/null @@ -1,5 +0,0 @@ -if ENV['ENABLE_QUERY_TRACE'] - require 'active_record_query_trace' - - ActiveRecordQueryTrace.enabled = 'true' -end diff --git a/config/initializers/carrierwave.rb b/config/initializers/carrierwave.rb index 1933afcbfb1..cd7df44351a 100644 --- a/config/initializers/carrierwave.rb +++ b/config/initializers/carrierwave.rb @@ -6,6 +6,8 @@ if File.exist?(aws_file) AWS_CONFIG = YAML.load(File.read(aws_file))[Rails.env] CarrierWave.configure do |config| + config.fog_provider = 'fog/aws' + config.fog_credentials = { provider: 'AWS', # required aws_access_key_id: AWS_CONFIG['access_key_id'], # required diff --git a/config/initializers/rspec_profiling.rb b/config/initializers/rspec_profiling.rb index b909cc5b9a4..a7efd74f09e 100644 --- a/config/initializers/rspec_profiling.rb +++ b/config/initializers/rspec_profiling.rb @@ -36,10 +36,10 @@ if Rails.env.test? RspecProfiling::Collectors::PSQL.prepend(RspecProfilingExt::PSQL) config.collector = RspecProfiling::Collectors::PSQL end - end - if ENV.has_key?('CI') && ENV['GITLAB_DATABASE'] == 'postgresql' - RspecProfiling::VCS::Git.prepend(RspecProfilingExt::Git) - RspecProfiling::Run.prepend(RspecProfilingExt::Run) + if ENV.key?('CI') + RspecProfiling::VCS::Git.prepend(RspecProfilingExt::Git) + RspecProfiling::Run.prepend(RspecProfilingExt::Run) + end end end diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 52ba10604d4..48993420ed9 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -50,8 +50,10 @@ namespace :admin do resources :deploy_keys, only: [:index, :new, :create, :destroy] - resources :hooks, only: [:index, :create, :destroy] do - get :test + resources :hooks, only: [:index, :create, :edit, :update, :destroy] do + member do + get :test + end end resources :broadcast_messages, only: [:index, :edit, :create, :update, :destroy] do diff --git a/config/routes/group.rb b/config/routes/group.rb index 73f69d76995..7b29e0e807c 100644 --- a/config/routes/group.rb +++ b/config/routes/group.rb @@ -10,7 +10,13 @@ scope(path: 'groups/*group_id', end resource :avatar, only: [:destroy] - resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create] + resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create] do + member do + get :merge_requests + get :participants + get :labels + end + end resources :labels, except: [:show] do post :toggle_subscription, on: :member diff --git a/config/routes/project.rb b/config/routes/project.rb index 115ae2324b3..a15e365cc2f 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -44,7 +44,7 @@ constraints(ProjectUrlConstrainer.new) do resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do member do - get 'raw' + get :raw post :mark_as_spam end end @@ -138,6 +138,8 @@ constraints(ProjectUrlConstrainer.new) do collection do get :folder, path: 'folders/*id', constraints: { format: /(html|json)/ } end + + resources :deployments, only: [:index] end resource :cycle_analytics, only: [:show] @@ -185,7 +187,7 @@ constraints(ProjectUrlConstrainer.new) do end end - resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do + resources :hooks, only: [:index, :create, :edit, :update, :destroy], constraints: { id: /\d+/ } do member do get :test end @@ -205,6 +207,9 @@ constraints(ProjectUrlConstrainer.new) do member do put :sort_issues put :sort_merge_requests + get :merge_requests + get :participants + get :labels end end diff --git a/config/routes/snippets.rb b/config/routes/snippets.rb index 56534f677be..dae83734fe6 100644 --- a/config/routes/snippets.rb +++ b/config/routes/snippets.rb @@ -1,10 +1,17 @@ resources :snippets, concerns: :awardable do member do - get 'raw' - get 'download' + get :raw post :mark_as_spam post :preview_markdown end + + scope module: :snippets do + resources :notes, only: [:index, :create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do + member do + delete :delete_attachment + end + end + end end get '/s/:username', to: redirect('/u/%{username}/snippets'), diff --git a/config/webpack.config.js b/config/webpack.config.js index 1721d275685..742d22d0c1f 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -79,6 +79,11 @@ var config = { loader: 'raw-loader', }, { + test: /\.gif$/, + loader: 'url-loader', + query: { mimetype: 'image/gif' }, + }, + { test: /\.(worker\.js|pdf)$/, exclude: /node_modules/, loader: 'file-loader', diff --git a/db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb b/db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb index 69d64ccd006..22bac46e25c 100644 --- a/db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb +++ b/db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/AddColumnWithDefaultToLargeTable class AddOnlyAllowMergeIfBuildSucceedsToProjects < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers disable_ddl_transaction! diff --git a/db/migrate/20160608195742_add_repository_storage_to_projects.rb b/db/migrate/20160608195742_add_repository_storage_to_projects.rb index c700d2b569d..0f3664c13ef 100644 --- a/db/migrate/20160608195742_add_repository_storage_to_projects.rb +++ b/db/migrate/20160608195742_add_repository_storage_to_projects.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/AddColumnWithDefaultToLargeTable class AddRepositoryStorageToProjects < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers disable_ddl_transaction! diff --git a/db/migrate/20160715154212_add_request_access_enabled_to_projects.rb b/db/migrate/20160715154212_add_request_access_enabled_to_projects.rb index bf0131c6d76..5dc26f8982a 100644 --- a/db/migrate/20160715154212_add_request_access_enabled_to_projects.rb +++ b/db/migrate/20160715154212_add_request_access_enabled_to_projects.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/AddColumnWithDefaultToLargeTable class AddRequestAccessEnabledToProjects < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers disable_ddl_transaction! diff --git a/db/migrate/20160715204316_add_request_access_enabled_to_groups.rb b/db/migrate/20160715204316_add_request_access_enabled_to_groups.rb index e7b14cd3ee2..4a317646788 100644 --- a/db/migrate/20160715204316_add_request_access_enabled_to_groups.rb +++ b/db/migrate/20160715204316_add_request_access_enabled_to_groups.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/AddColumnWithDefaultToLargeTable class AddRequestAccessEnabledToGroups < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers disable_ddl_transaction! diff --git a/db/migrate/20160831223750_remove_features_enabled_from_projects.rb b/db/migrate/20160831223750_remove_features_enabled_from_projects.rb index a2c207b49ea..7414a28ac97 100644 --- a/db/migrate/20160831223750_remove_features_enabled_from_projects.rb +++ b/db/migrate/20160831223750_remove_features_enabled_from_projects.rb @@ -1,6 +1,7 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. +# rubocop:disable Migration/AddColumnWithDefaultToLargeTable class RemoveFeaturesEnabledFromProjects < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers disable_ddl_transaction! diff --git a/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb b/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb index 18ea9d43a43..0100e30a733 100644 --- a/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb +++ b/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb @@ -1,6 +1,7 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. +# rubocop:disable Migration/AddColumnWithDefaultToLargeTable class RemoveProjectsPushesSinceGc < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170124193147_add_two_factor_columns_to_namespaces.rb b/db/migrate/20170124193147_add_two_factor_columns_to_namespaces.rb index df5cddeb205..ae37da275fd 100644 --- a/db/migrate/20170124193147_add_two_factor_columns_to_namespaces.rb +++ b/db/migrate/20170124193147_add_two_factor_columns_to_namespaces.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/AddColumnWithDefaultToLargeTable class AddTwoFactorColumnsToNamespaces < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170124193205_add_two_factor_columns_to_users.rb b/db/migrate/20170124193205_add_two_factor_columns_to_users.rb index 1d1021fcbb3..8d4aefa4365 100644 --- a/db/migrate/20170124193205_add_two_factor_columns_to_users.rb +++ b/db/migrate/20170124193205_add_two_factor_columns_to_users.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/AddColumnWithDefaultToLargeTable class AddTwoFactorColumnsToUsers < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb b/db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb index f54608ecceb..7ad01a04815 100644 --- a/db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb +++ b/db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb @@ -1,6 +1,7 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. +# rubocop:disable Migration/AddColumnWithDefaultToLargeTable class AddPrintingMergeRequestLinkEnabledToProject < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers disable_ddl_transaction! diff --git a/db/migrate/20170305180853_add_auto_cancel_pending_pipelines_to_project.rb b/db/migrate/20170305180853_add_auto_cancel_pending_pipelines_to_project.rb index aa64f2dddca..f335e77fb5e 100644 --- a/db/migrate/20170305180853_add_auto_cancel_pending_pipelines_to_project.rb +++ b/db/migrate/20170305180853_add_auto_cancel_pending_pipelines_to_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/AddColumnWithDefaultToLargeTable class AddAutoCancelPendingPipelinesToProject < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb b/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb index b39c0a3be0f..6c9fe19ca34 100644 --- a/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb +++ b/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/AddColumnWithDefaultToLargeTable class RevertAddNotifiedOfOwnActivityToUsers < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers disable_ddl_transaction! diff --git a/db/migrate/20170327091750_add_created_at_index_to_deployments.rb b/db/migrate/20170327091750_add_created_at_index_to_deployments.rb new file mode 100644 index 00000000000..fd6ed499b80 --- /dev/null +++ b/db/migrate/20170327091750_add_created_at_index_to_deployments.rb @@ -0,0 +1,15 @@ +class AddCreatedAtIndexToDeployments < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index :deployments, :created_at + end + + def down + remove_concurrent_index :deployments, :created_at + end +end diff --git a/db/post_migrate/20170412174900_rename_reserved_dynamic_paths.rb b/db/post_migrate/20170412174900_rename_reserved_dynamic_paths.rb new file mode 100644 index 00000000000..a23f83205f1 --- /dev/null +++ b/db/post_migrate/20170412174900_rename_reserved_dynamic_paths.rb @@ -0,0 +1,55 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RenameReservedDynamicPaths < ActiveRecord::Migration + include Gitlab::Database::RenameReservedPathsMigration::V1 + + DOWNTIME = false + + disable_ddl_transaction! + + DISALLOWED_ROOT_PATHS = %w[ + - + abuse_reports + api + autocomplete + explore + health_check + import + invites + jwt + koding + member + notification_settings + oauth + sent_notifications + unicorn_test + uploads + users + ] + + DISALLOWED_WILDCARD_PATHS = %w[ + environments/folders + gitlab-lfs/objects + info/lfs/objects + ] + + DISSALLOWED_GROUP_PATHS = %w[ + activity + avatar + group_members + labels + milestones + subgroups + ] + + def up + rename_root_paths(DISALLOWED_ROOT_PATHS) + rename_wildcard_paths(DISALLOWED_WILDCARD_PATHS) + rename_child_paths(DISSALLOWED_GROUP_PATHS) + end + + def down + # nothing to do + end +end diff --git a/db/schema.rb b/db/schema.rb index b938657a186..be6684f3a6b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -386,6 +386,7 @@ ActiveRecord::Schema.define(version: 20170426181740) do t.string "on_stop" end + add_index "deployments", ["created_at"], name: "index_deployments_on_created_at", using: :btree add_index "deployments", ["project_id", "environment_id", "iid"], name: "index_deployments_on_project_id_and_environment_id_and_iid", using: :btree add_index "deployments", ["project_id", "iid"], name: "index_deployments_on_project_id_and_iid", unique: true, using: :btree diff --git a/doc/README.md b/doc/README.md index 6406040da4b..4397465bd3d 100644 --- a/doc/README.md +++ b/doc/README.md @@ -92,7 +92,7 @@ Take a step ahead and dive into GitLab's advanced features. - [GitLab Pages](user/project/pages/index.md): Build, test, and deploy your static website with GitLab Pages. - [Snippets](user/snippets.md): Snippets allow you to create little bits of code. -- [Wikis](workflow/project_features.md#wiki): Enhance your repository documentation with built-in wikis. +- [Wikis](user/project/wiki/index.md): Enhance your repository documentation with built-in wikis. ### Continuous Integration, Delivery, and Deployment diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md index f6027b2f99e..725fc1f6076 100644 --- a/doc/administration/auth/ldap.md +++ b/doc/administration/auth/ldap.md @@ -65,14 +65,14 @@ main: # 'main' is the GitLab 'provider ID' of this LDAP server # # Example: 'Paris' or 'Acme, Ltd.' label: 'LDAP' - + # Example: 'ldap.mydomain.com' host: '_your_ldap_server' # This port is an example, it is sometimes different but it is always an integer and not a string port: 389 - uid: 'sAMAccountName' + uid: 'sAMAccountName' # This should be the attribute, not the value that maps to uid. method: 'plain' # "tls" or "ssl" or "plain" - + # Examples: 'america\\momo' or 'CN=Gitlab Git,CN=Users,DC=mydomain,DC=com' bind_dn: '_the_full_dn_of_the_user_you_will_bind_with' password: '_the_password_of_the_bind_user' diff --git a/doc/administration/high_availability/load_balancer.md b/doc/administration/high_availability/load_balancer.md index d9ca74ca1a3..359de0efadb 100644 --- a/doc/administration/high_availability/load_balancer.md +++ b/doc/administration/high_availability/load_balancer.md @@ -13,7 +13,7 @@ you need to use with GitLab. | LB Port | Backend Port | Protocol | | ------- | ------------ | --------------- | | 80 | 80 | HTTP [^1] | -| 443 | 443 | HTTPS [^1] [^2] | +| 443 | 443 | TCP or HTTPS [^1] [^2] | | 22 | 22 | TCP | ## GitLab Pages Ports diff --git a/doc/administration/integration/terminal.md b/doc/administration/integration/terminal.md index 3b5ee86b68b..91e844c7b42 100644 --- a/doc/administration/integration/terminal.md +++ b/doc/administration/integration/terminal.md @@ -32,7 +32,7 @@ In brief: As web terminals use WebSockets, every HTTP/HTTPS reverse proxy in front of Workhorse needs to be configured to pass the `Connection` and `Upgrade` headers -through to the next one in the chain. If you installed Gitlab using Omnibus, or +through to the next one in the chain. If you installed GitLab using Omnibus, or from source, starting with GitLab 8.15, this should be done by the default configuration, so there's no need for you to do anything. @@ -58,7 +58,7 @@ document for more details. If you'd like to disable web terminal support in GitLab, just stop passing the `Connection` and `Upgrade` hop-by-hop headers in the *first* HTTP reverse proxy in the chain. For most users, this will be the NGINX server bundled with -Omnibus Gitlab, in which case, you need to: +Omnibus GitLab, in which case, you need to: * Find the `nginx['proxy_set_headers']` section of your `gitlab.rb` file * Ensure the whole block is uncommented, and then comment out or remove the diff --git a/doc/ci/img/pipelines_grouped.png b/doc/ci/img/pipelines_grouped.png Binary files differnew file mode 100644 index 00000000000..06f52e03320 --- /dev/null +++ b/doc/ci/img/pipelines_grouped.png diff --git a/doc/ci/img/pipelines_index.png b/doc/ci/img/pipelines_index.png Binary files differnew file mode 100644 index 00000000000..3b522a9c5e4 --- /dev/null +++ b/doc/ci/img/pipelines_index.png diff --git a/doc/ci/img/pipelines_mini_graph.png b/doc/ci/img/pipelines_mini_graph.png Binary files differnew file mode 100644 index 00000000000..042c8ffeef5 --- /dev/null +++ b/doc/ci/img/pipelines_mini_graph.png diff --git a/doc/ci/img/pipelines_mini_graph_simple.png b/doc/ci/img/pipelines_mini_graph_simple.png Binary files differnew file mode 100644 index 00000000000..eb36c09b2d4 --- /dev/null +++ b/doc/ci/img/pipelines_mini_graph_simple.png diff --git a/doc/ci/img/pipelines_mini_graph_sorting.png b/doc/ci/img/pipelines_mini_graph_sorting.png Binary files differnew file mode 100644 index 00000000000..3a4e5453360 --- /dev/null +++ b/doc/ci/img/pipelines_mini_graph_sorting.png diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md index db92a4b0d80..5a2b61fb0cb 100644 --- a/doc/ci/pipelines.md +++ b/doc/ci/pipelines.md @@ -1,7 +1,6 @@ # Introduction to pipelines and jobs ->**Note:** -Introduced in GitLab 8.8. +> Introduced in GitLab 8.8. ## Pipelines @@ -9,11 +8,17 @@ A pipeline is a group of [jobs][] that get executed in [stages][](batches). All of the jobs in a stage are executed in parallel (if there are enough concurrent [Runners]), and if they all succeed, the pipeline moves on to the next stage. If one of the jobs fails, the next stage is not (usually) -executed. +executed. You can access the pipelines page in your project's **Pipelines** tab. + +In the following image you can see that the pipeline consists of four stages +(`build`, `test`, `staging`, `production`) each one having one or more jobs. + +>**Note:** +GitLab capitalizes the stages' names when shown in the [pipeline graphs](#pipeline-graphs). ![Pipelines example](img/pipelines.png) -## Types of Pipelines +## Types of pipelines There are three types of pipelines that often use the single shorthand of "pipeline". People often talk about them as if each one is "the" pipeline, but really, they're just pieces of a single, comprehensive pipeline. @@ -23,7 +28,7 @@ There are three types of pipelines that often use the single shorthand of "pipel 2. **Deploy Pipeline**: Deploy stage(s) defined in `.gitlab-ci.yml` The flow of deploying code to servers through various stages: e.g. development to staging to production 3. **Project Pipeline**: Cross-project CI dependencies [triggered via API][triggers], particularly for micro-services, but also for complicated build dependencies: e.g. api -> front-end, ce/ee -> omnibus. -## Development Workflows +## Development workflows Pipelines accommodate several development workflows: @@ -45,18 +50,141 @@ confused with a `build` job or `build` stage. Pipelines are defined in `.gitlab-ci.yml` by specifying [jobs] that run in [stages]. -See full [documentation](yaml/README.md#jobs). +See the reference [documentation for jobs](yaml/README.md#jobs). ## Seeing pipeline status -You can find the current and historical pipeline runs under **Pipelines** for -your project. +You can find the current and historical pipeline runs under your project's +**Pipelines** tab. Clicking on a pipeline will show the jobs that were run for +that pipeline. + +![Pipelines index page](img/pipelines_index.png) ## Seeing job status -Clicking on a pipeline will show the jobs that were run for that pipeline. +When you visit a single pipeline you can see the related jobs for that pipeline. Clicking on an individual job will show you its job trace, and allow you to -cancel the job, retry it, or erase the job trace. +cancel the job, retry it, or erase the job trace. + +![Pipelines example](img/pipelines.png) + +## Pipeline graphs + +> [Introduced][ce-5742] in GitLab 8.11. + +Pipelines can be complex structures with many sequential and parallel jobs. +To make it a little easier to see what is going on, you can view a graph +of a single pipeline and its status. + +A pipeline graph can be shown in two different ways depending on what page you +are on. + +--- + +The regular pipeline graph that shows the names of the jobs of each stage can +be found when you are on a [single pipeline page](#seeing-pipeline-status). + +![Pipelines example](img/pipelines.png) + +Then, there is the pipeline mini graph which takes less space and can give you a +quick glance if all jobs pass or something failed. The pipeline mini graph can +be found when you visit: + +- the pipelines index page +- a single commit page +- a merge request page + +That way, you can see all related jobs for a single commit and the net result +of each stage of your pipeline. This allows you to quickly see what failed and +fix it. Stages in pipeline mini graphs are collapsible. Hover your mouse over +them and click to expand their jobs. + +| **Mini graph** | **Mini graph expanded** | +| :------------: | :---------------------: | +| ![Pipelines mini graph](img/pipelines_mini_graph_simple.png) | ![Pipelines mini graph extended](img/pipelines_mini_graph.png) | + +### Grouping similar jobs in the pipeline graph + +> [Introduced][ce-6242] in GitLab 8.12. + +If you have many similar jobs, your pipeline graph becomes very long and hard +to read. For that reason, similar jobs can automatically be grouped together. +If the job names are formatted in certain ways, they will be collapsed into +a single group in regular pipeline graphs (not the mini graphs). +You'll know when a pipeline has grouped jobs if you don't see the retry or +cancel button inside them. Hovering over them will show the number of grouped +jobs. Click to expand them. + +![Grouped pipelines](img/pipelines_grouped.png) + +The basic requirements is that there are two numbers separated with one of +the following (you can even use them interchangeably): + +- a space +- a backslash (`/`) +- a colon (`:`) + +>**Note:** +More specifically, [it uses][regexp] this regular expression: `\d+[\s:\/\\]+\d+\s*`. + +The jobs will be ordered by comparing those two numbers from left to right. You +usually want the first to be the index and the second the total. + +For example, the following jobs will be grouped under a job named `test`: + +- `test 0 3` => `test` +- `test 1 3` => `test` +- `test 2 3` => `test` + +The following jobs will be grouped under a job named `test ruby`: + +- `test 1:2 ruby` => `test ruby` +- `test 2:2 ruby` => `test ruby` + +The following jobs will be grouped under a job named `test ruby` as well: + +- `1/3 test ruby` => `test ruby` +- `2/3 test ruby` => `test ruby` +- `3/3 test ruby` => `test ruby` + +### Manual actions from the pipeline graph + +> [Introduced][ce-7931] in GitLab 8.15. + +[Manual actions][manual] allow you to require manual interaction before moving +forward with a particular job in CI. Your entire pipeline can run automatically, +but the actual [deploy to production][env-manual] will require a click. + +You can do this straight from the pipeline graph. Just click on the play button +to execute that particular job. For example, in the image below, the `production` +stage has a job with a manual action. + +![Pipelines example](img/pipelines.png) + +### Ordering of jobs in pipeline graphs + +**Regular pipeline graph** + +In the single pipeline page, jobs are sorted by name. + +**Mini pipeline graph** + +> [Introduced][ce-9760] in GitLab 9.0. + +In the pipeline mini graphs, the jobs are sorted first by severity and then +by name. The order of severity is: + +- failed +- warning +- pending +- running +- manual +- canceled +- success +- skipped +- created + +![Pipeline mini graph sorting](img/pipelines_mini_graph_sorting.png) ## How the pipeline duration is calculated @@ -96,7 +224,14 @@ respective link in the [Pipelines settings] page. [jobs]: #jobs [jobs-yaml]: yaml/README.md#jobs +[manual]: yaml/README.md#manual +[env-manual]: environments.md#manually-deploying-to-environments [stages]: yaml/README.md#stages [runners]: runners/README.html [pipelines settings]: ../user/project/pipelines/settings.md [triggers]: triggers/README.md +[ce-5742]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5742 +[ce-6242]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6242 +[ce-7931]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7931 +[ce-9760]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9760 +[regexp]: https://gitlab.com/gitlab-org/gitlab-ce/blob/2f3dc314f42dbd79813e6251792853bc231e69dd/app/models/commit_status.rb#L99 diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md index 3e8b709c18f..77ba2a5fd87 100644 --- a/doc/development/migration_style_guide.md +++ b/doc/development/migration_style_guide.md @@ -270,3 +270,28 @@ end When doing so be sure to explicitly set the model's table name so it's not derived from the class name or namespace. + +### Renaming reserved paths + +When a new route for projects is introduced that could conflict with any +existing records. The path for this records should be renamed, and the +related data should be moved on disk. + +Since we had to do this a few times already, there are now some helpers to help +with this. + +To use this you can include `Gitlab::Database::RenameReservedPathsMigration::V1` +in your migration. This will provide 3 methods which you can pass one or more +paths that need to be rejected. + +**`rename_root_paths`**: This will rename the path of all _namespaces_ with the +given name that don't have a `parent_id`. + +**`rename_child_paths`**: This will rename the path of all _namespaces_ with the +given name that have a `parent_id`. + +**`rename_wildcard_paths`**: This will rename the path of all _projects_, and all +_namespaces_ that have a `project_id`. + +The `path` column for these rows will be renamed to their previous value followed +by an integer. For example: `users` would turn into `users0` diff --git a/doc/development/testing.md b/doc/development/testing.md index 9b0b9808827..6d8b846d27f 100644 --- a/doc/development/testing.md +++ b/doc/development/testing.md @@ -188,7 +188,8 @@ Please consult the [dedicated "Frontend testing" guide](./fe_guide/testing.md). ### General Guidelines - Use a single, top-level `describe ClassName` block. -- Use `described_class` instead of repeating the class name being described. +- Use `described_class` instead of repeating the class name being described + (_this is enforced by RuboCop_). - Use `.method` to describe class methods and `#method` to describe instance methods. - Use `context` to test branching logic. @@ -197,7 +198,7 @@ Please consult the [dedicated "Frontend testing" guide](./fe_guide/testing.md). - Don't `describe` symbols (see [Gotchas](gotchas.md#dont-describe-symbols)). - Don't assert against the absolute value of a sequence-generated attribute (see [Gotchas](gotchas.md#dont-assert-against-the-absolute-value-of-a-sequence-generated-attribute)). - Don't supply the `:each` argument to hooks since it's the default. -- Prefer `not_to` to `to_not` (_this is enforced by Rubocop_). +- Prefer `not_to` to `to_not` (_this is enforced by RuboCop_). - 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. diff --git a/doc/gitlab-basics/create-project.md b/doc/gitlab-basics/create-project.md index 1c549844ee1..2513f4b420a 100644 --- a/doc/gitlab-basics/create-project.md +++ b/doc/gitlab-basics/create-project.md @@ -1,24 +1,28 @@ # How to create a project in GitLab -There are two ways to create a new project in GitLab. - -1. While in your dashboard, you can create a new project using the **New project** - green button or you can use the cross icon in the upper right corner next to - your avatar which is always visible. +1. In your dashboard, click the green **New project** button or use the plus + icon in the upper right corner of the navigation bar. ![Create a project](img/create_new_project_button.png) -1. From there you can see several options. +1. This opens the **New project** page. ![Project information](img/create_new_project_info.png) -1. Fill out the information: - - 1. "Project name" is the name of your project (you can't use special characters, - but you can use spaces, hyphens, underscores or even emojis). - 1. The "Project description" is optional and will be shown in your project's - dashboard so others can briefly understand what your project is about. - 1. Select a [visibility level](../public_access/public_access.md). - 1. You can also [import your existing projects](../workflow/importing/README.md). - -1. Finally, click **Create project**. +1. Provide the following information: + - Enter the name of your project in the **Project name** field. You can't use + special characters, but you can use spaces, hyphens, underscores or even + emoji. + - If you have a project in a different repository, you can [import it] by + clicking an **Import project from** button provided this is enabled in + your GitLab instance. Ask your administrator if not. + - The **Project description (optional)** field enables you to enter a + description for your project's dashboard, which will help others + understand what your project is about. Though it's not required, it's a good + idea to fill this in. + - Changing the **Visibility Level** modifies the project's + [viewing and access rights](../public_access/public_access.md) for users. + +1. Click **Create project**. + +[import it]: ../workflow/importing/README.md diff --git a/doc/gitlab-basics/img/create_new_project_button.png b/doc/gitlab-basics/img/create_new_project_button.png Binary files differindex 8d7a69e55ed..567f104880f 100644 --- a/doc/gitlab-basics/img/create_new_project_button.png +++ b/doc/gitlab-basics/img/create_new_project_button.png diff --git a/doc/install/installation.md b/doc/install/installation.md index b6bbc2a0af6..dc807d93bbb 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -423,6 +423,11 @@ which is the recommended location. sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production +You can specify a different Git repository by providing it as an extra paramter: + + sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse,https://example.com/gitlab-workhorse.git]" RAILS_ENV=production + + ### Initialize Database and Activate Advanced Features sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production @@ -466,6 +471,12 @@ with setting up Gitaly until you upgrade to GitLab 9.2 or later. # Fetch Gitaly source with Git and compile with Go sudo -u git -H bundle exec rake "gitlab:gitaly:install[/home/git/gitaly]" RAILS_ENV=production +You can specify a different Git repository by providing it as an extra paramter: + + sudo -u git -H bundle exec rake "gitlab:gitaly:install[/home/git/gitaly,https://example.com/gitaly.git]" RAILS_ENV=production + +Next, make sure gitaly configured: + # Restrict Gitaly socket access sudo chmod 0700 /home/git/gitlab/tmp/sockets/private sudo chown git /home/git/gitlab/tmp/sockets/private diff --git a/doc/update/8.10-to-8.11.md b/doc/update/8.10-to-8.11.md index e5e3cd395df..e538983e603 100644 --- a/doc/update/8.10-to-8.11.md +++ b/doc/update/8.10-to-8.11.md @@ -49,6 +49,8 @@ sudo gem install bundler --no-ri --no-rdoc ### 4. Get latest code ```bash +cd /home/git/gitlab + sudo -u git -H git fetch --all sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically ``` diff --git a/doc/update/8.11-to-8.12.md b/doc/update/8.11-to-8.12.md index d6b3b0ffa5a..604166beb56 100644 --- a/doc/update/8.11-to-8.12.md +++ b/doc/update/8.11-to-8.12.md @@ -49,6 +49,8 @@ sudo gem install bundler --no-ri --no-rdoc ### 4. Get latest code ```bash +cd /home/git/gitlab + sudo -u git -H git fetch --all sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically ``` diff --git a/doc/update/8.12-to-8.13.md b/doc/update/8.12-to-8.13.md index ed0e668d854..d83965131f5 100644 --- a/doc/update/8.12-to-8.13.md +++ b/doc/update/8.12-to-8.13.md @@ -49,6 +49,8 @@ sudo gem install bundler --no-ri --no-rdoc ### 4. Get latest code ```bash +cd /home/git/gitlab + sudo -u git -H git fetch --all sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically ``` diff --git a/doc/update/8.13-to-8.14.md b/doc/update/8.13-to-8.14.md index aa1c659717e..aaadcec8ac0 100644 --- a/doc/update/8.13-to-8.14.md +++ b/doc/update/8.13-to-8.14.md @@ -49,6 +49,8 @@ sudo gem install bundler --no-ri --no-rdoc ### 4. Get latest code ```bash +cd /home/git/gitlab + sudo -u git -H git fetch --all sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically ``` diff --git a/doc/user/admin_area/monitoring/health_check.md b/doc/user/admin_area/monitoring/health_check.md index a4935f66cbd..a954840b8a6 100644 --- a/doc/user/admin_area/monitoring/health_check.md +++ b/doc/user/admin_area/monitoring/health_check.md @@ -100,7 +100,7 @@ On failure, the endpoint will return a `500` HTTP status code. On success, the e will return a valid successful HTTP status code, and a `success` message. Ideally your uptime monitoring should look for the success message. -[ce-10416]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3888 +[ce-10416]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10416 [ce-3888]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3888 [pingdom]: https://www.pingdom.com [nagios-health]: https://nagios-plugins.org/doc/man/check_http.html diff --git a/doc/user/profile/account/delete_account.md b/doc/user/profile/account/delete_account.md index 505248536c8..b5d3b009044 100644 --- a/doc/user/profile/account/delete_account.md +++ b/doc/user/profile/account/delete_account.md @@ -1,7 +1,7 @@ # Deleting a User Account - As a user, you can delete your own account by navigating to **Settings** > **Account** and selecting **Delete account** -- As an admin, you can delete a user account by navigating to the **Admin Area**, selecting the **Users** tab, selecting a user, and clicking on **Remvoe user** +- As an admin, you can delete a user account by navigating to the **Admin Area**, selecting the **Users** tab, selecting a user, and clicking on **Remove user** ## Associated Records diff --git a/doc/user/project/wiki/img/wiki_create_home_page.png b/doc/user/project/wiki/img/wiki_create_home_page.png Binary files differnew file mode 100644 index 00000000000..f50f564034c --- /dev/null +++ b/doc/user/project/wiki/img/wiki_create_home_page.png diff --git a/doc/user/project/wiki/img/wiki_create_new_page.png b/doc/user/project/wiki/img/wiki_create_new_page.png Binary files differnew file mode 100644 index 00000000000..c19124a8923 --- /dev/null +++ b/doc/user/project/wiki/img/wiki_create_new_page.png diff --git a/doc/user/project/wiki/img/wiki_create_new_page_modal.png b/doc/user/project/wiki/img/wiki_create_new_page_modal.png Binary files differnew file mode 100644 index 00000000000..ece437967dc --- /dev/null +++ b/doc/user/project/wiki/img/wiki_create_new_page_modal.png diff --git a/doc/user/project/wiki/img/wiki_page_history.png b/doc/user/project/wiki/img/wiki_page_history.png Binary files differnew file mode 100644 index 00000000000..0e6af1b468d --- /dev/null +++ b/doc/user/project/wiki/img/wiki_page_history.png diff --git a/doc/user/project/wiki/img/wiki_sidebar.png b/doc/user/project/wiki/img/wiki_sidebar.png Binary files differnew file mode 100644 index 00000000000..59814e2a06e --- /dev/null +++ b/doc/user/project/wiki/img/wiki_sidebar.png diff --git a/doc/user/project/wiki/index.md b/doc/user/project/wiki/index.md new file mode 100644 index 00000000000..e9ee1abc6c1 --- /dev/null +++ b/doc/user/project/wiki/index.md @@ -0,0 +1,97 @@ +# Wiki + +A separate system for documentation called Wiki, is built right into each +GitLab project. It is enabled by default on all new projects and you can find +it under **Wiki** in your project. + +Wikis are very convenient if you don't want to keep you documentation in your +repository, but you do want to keep it in the same project where your code +resides. + +You can create Wiki pages in the web interface or +[locally using Git](#adding-and-editing-wiki-pages-locally) since every Wiki is +a separate Git repository. + +>**Note:** +A [permission level][permissions] of **Guest** is needed to view a Wiki and +**Developer** is needed to create and edit Wiki pages. + +## First time creating the Home page + +The first time you visit a Wiki, you will be directed to create the Home page. +The Home page is necessary to be created since it serves as the landing page +when viewing a Wiki. You only have to fill in the **Content** section and click +**Create page**. You can always edit it later, so go ahead and write a welcome +message. + +![New home page](img/wiki_create_home_page.png) + +## Creating a new wiki page + +Create a new page by clicking the **New page** button that can be found +in all wiki pages. You will be asked to fill in the page name from which GitLab +will create the path to the page. You can specify a full path for the new file +and any missing directories will be created automatically. + +![New page modal](img/wiki_create_new_page_modal.png) + +Once you enter the page name, it's time to fill in its content. GitLab wikis +support Markdown, RDoc and AsciiDoc. For Markdown based pages, all the +[Markdown features](../../markdown.md) are supported and for links there is +some [wiki specific](../../markdown.md#wiki-specific-markdown) behavior. + +>**Note:** +The wiki is based on a Git repository and contains only text files. Uploading +files via the web interface will upload them in GitLab itself, and they will +not be available if you clone the wiki repo locally. + +In the web interface the commit message is optional, but the GitLab Wiki is +based on Git and needs a commit message, so one will be created for you if you +do not enter one. + +When you're ready, click the **Create page** and the new page will be created. + +![New page](img/wiki_create_new_page.png) + +## Editing a wiki page + +To edit a page, simply click on the **Edit** button. From there on, you can +change its content. When done, click **Save changes** for the changes to take +effect. + +## Deleting a wiki page + +You can find the **Delete** button only when editing a page. Click on it and +confirm you want the page to be deleted. + +## Viewing a list of all created wiki pages + +Every wiki has a sidebar from which a short list of the created pages can be +found. The list is ordered alphabetically. + +![Wiki sidebar](img/wiki_sidebar.png) + +If you have many pages, not all will be listed in the sidebar. Click on +**More pages** to see all of them. + +## Viewing the history of a wiki page + +The changes of a wiki page over time are recorded in the wiki's Git repository, +and you can view them by clicking the **Page history** button. + +From the history page you can see the revision of the page (Git commit SHA), its +author, the commit message, when it was last updated and the page markup format. +To see how a previous version of the page looked like, click on a revision +number. + +![Wiki page history](img/wiki_page_history.png) + +## Adding and editing wiki pages locally + +Since wikis are based on Git repositories, you can clone them locally and edit +them like you would do with every other Git repository. + +On the right sidebar, click on **Clone repository** and follow the on-screen +instructions. + +[permissions]: ../../permissions.md diff --git a/doc/workflow/project_features.md b/doc/workflow/project_features.md index f19e7df8c9a..3f5de2bd4b1 100644 --- a/doc/workflow/project_features.md +++ b/doc/workflow/project_features.md @@ -26,6 +26,8 @@ This is a separate system for documentation, built right into GitLab. It is source controlled and is very convenient if you don't want to keep you documentation in your source code, but you do want to keep it in your GitLab project. +[Read more about Wikis.](../user/project/wiki/index.md) + ## Snippets Snippets are little bits of code or text. diff --git a/features/group/milestones.feature b/features/group/milestones.feature index d6c05df9840..1c1539b3e12 100644 --- a/features/group/milestones.feature +++ b/features/group/milestones.feature @@ -38,6 +38,7 @@ Feature: Group Milestones And I should see the "feature" label And I should see the project name in the Issue row + @javascript Scenario: I should see the Labels tab Given Group has projects with milestones When I visit group "Owned" page diff --git a/features/project/milestone.feature b/features/project/milestone.feature index 713f0f3b979..5e7b211fa27 100644 --- a/features/project/milestone.feature +++ b/features/project/milestone.feature @@ -7,14 +7,6 @@ Feature: Project Milestone And milestone has issue "Bugfix1" with labels: "bug", "feature" And milestone has issue "Bugfix2" with labels: "bug", "enhancement" - - @javascript - Scenario: Listing issues from issues tab - Given I visit project "Shop" milestones page - And I click link "v2.2" - Then I should see the labels "bug", "enhancement" and "feature" - And I should see the "bug" label listed only once - @javascript Scenario: Listing labels from labels tab Given I visit project "Shop" milestones page diff --git a/features/project/snippets.feature b/features/project/snippets.feature index 3c51ea56585..50bc4c93df3 100644 --- a/features/project/snippets.feature +++ b/features/project/snippets.feature @@ -11,6 +11,7 @@ Feature: Project Snippets Then I should see "Snippet one" in snippets And I should not see "Snippet two" in snippets + @javascript Scenario: I create new project snippet Given I click link "New snippet" And I submit new snippet "Snippet three" diff --git a/features/snippets/snippets.feature b/features/snippets/snippets.feature index e15d7c79342..1ad02780229 100644 --- a/features/snippets/snippets.feature +++ b/features/snippets/snippets.feature @@ -5,6 +5,7 @@ Feature: Snippets And I have public "Personal snippet one" snippet And I have private "Personal snippet private" snippet + @javascript Scenario: I create new snippet Given I visit new snippet page And I submit new snippet "Personal snippet three" diff --git a/features/steps/group/milestones.rb b/features/steps/group/milestones.rb index f8f5e3f2382..49fcd6f1201 100644 --- a/features/steps/group/milestones.rb +++ b/features/steps/group/milestones.rb @@ -1,4 +1,5 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps + include WaitForAjax include SharedAuthentication include SharedPaths include SharedGroup @@ -90,6 +91,8 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps end step 'I should see the list of labels' do + wait_for_ajax + page.within('#tab-labels') do expect(page).to have_content 'bug' expect(page).to have_content 'feature' diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index de737cdc823..f19fa1c7600 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -21,7 +21,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps expect(response_headers['Content-Type']).to have_content("application/atom+xml") expect(body).to have_selector("title", text: "#{@project.name}:master commits") expect(body).to have_selector("author email", text: commit.author_email) - expect(body).to have_selector("entry summary", text: commit.description[0..10].delete("\r")) + expect(body).to have_selector("entry summary", text: commit.description[0..10].delete("\r\n")) end step 'I click on tag link' do diff --git a/features/steps/project/project_milestone.rb b/features/steps/project/project_milestone.rb index 1864b3a2b52..dc1190b7eea 100644 --- a/features/steps/project/project_milestone.rb +++ b/features/steps/project/project_milestone.rb @@ -2,6 +2,7 @@ class Spinach::Features::ProjectMilestone < Spinach::FeatureSteps include SharedAuthentication include SharedProject include SharedPaths + include WaitForAjax step 'milestone has issue "Bugfix1" with labels: "bug", "feature"' do project = Project.find_by(name: "Shop") @@ -34,6 +35,8 @@ class Spinach::Features::ProjectMilestone < Spinach::FeatureSteps end step 'I should see the labels "bug", "enhancement" and "feature"' do + wait_for_ajax + page.within('#tab-issues') do expect(page).to have_content 'bug' expect(page).to have_content 'enhancement' diff --git a/features/steps/project/snippets.rb b/features/steps/project/snippets.rb index a3bebfa4b71..60febd20104 100644 --- a/features/steps/project/snippets.rb +++ b/features/steps/project/snippets.rb @@ -3,6 +3,7 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps include SharedProject include SharedNote include SharedPaths + include WaitForAjax step 'project "Shop" have "Snippet one" snippet' do create(:project_snippet, @@ -55,9 +56,10 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps fill_in "project_snippet_title", with: "Snippet three" fill_in "project_snippet_file_name", with: "my_snippet.rb" page.within('.file-editor') do - find(:xpath, "//input[@id='project_snippet_content']").set 'Content of snippet three' + find('.ace_editor').native.send_keys 'Content of snippet three' end click_button "Create snippet" + wait_for_ajax end step 'I should see snippet "Snippet three"' do @@ -79,6 +81,7 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps fill_in "note_note", with: "Good snippet!" click_button "Comment" end + wait_for_ajax end step 'I should see comment "Good snippet!"' do diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index 36fe21a047c..ef09bddddd8 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -367,7 +367,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps step 'I should see buttons for allowed commands' do page.within '.content' do - expect(page).to have_link 'Open raw' + expect(page).to have_link 'Download' expect(page).to have_content 'History' expect(page).to have_content 'Permalink' expect(page).not_to have_content 'Edit' diff --git a/features/steps/snippets/snippets.rb b/features/steps/snippets/snippets.rb index 19366b11071..0b3e942a4fd 100644 --- a/features/steps/snippets/snippets.rb +++ b/features/steps/snippets/snippets.rb @@ -3,6 +3,7 @@ class Spinach::Features::Snippets < Spinach::FeatureSteps include SharedPaths include SharedProject include SharedSnippet + include WaitForAjax step 'I click link "Personal snippet one"' do click_link "Personal snippet one" @@ -26,9 +27,10 @@ class Spinach::Features::Snippets < Spinach::FeatureSteps fill_in "personal_snippet_title", with: "Personal snippet three" fill_in "personal_snippet_file_name", with: "my_snippet.rb" page.within('.file-editor') do - find(:xpath, "//input[@id='personal_snippet_content']").set 'Content of snippet three' + find('.ace_editor').native.send_keys 'Content of snippet three' end click_button "Create snippet" + wait_for_ajax end step 'I submit new internal snippet' do diff --git a/features/support/env.rb b/features/support/env.rb index 06c804b1db7..92d13bea4b6 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -10,7 +10,7 @@ if ENV['CI'] Knapsack::Adapters::SpinachAdapter.bind end -%w(select2_helper test_env repo_helpers wait_for_ajax sidekiq).each do |f| +%w(select2_helper test_env repo_helpers wait_for_ajax wait_for_requests sidekiq).each do |f| require Rails.root.join('spec', 'support', f) end @@ -30,6 +30,13 @@ Spinach.hooks.before_run do include FactoryGirl::Syntax::Methods end +Spinach.hooks.after_feature do |feature_data| + if feature_data.scenarios.flat_map(&:tags).include?('javascript') + include WaitForRequests + wait_for_requests_complete + end +end + module StdoutReporterWithScenarioLocation # Override the standard reporter to show filename and line number next to each # scenario for easy, focused re-runs diff --git a/fixtures/emojis/digests.json b/fixtures/emojis/digests.json index 3cbc4702dac..589cff165f3 100644 --- a/fixtures/emojis/digests.json +++ b/fixtures/emojis/digests.json @@ -2,10746 +2,12537 @@ "100": { "category": "symbols", "moji": "💯", + "description": "hundred points symbol", "unicodeVersion": "6.0", "digest": "add3bd7d06b6dd445788b277f8c9e5dcf42a54d3ec8b7fb9e7a39695dd95d094" }, "1234": { "category": "symbols", "moji": "🔢", + "description": "input symbol for numbers", "unicodeVersion": "6.0", "digest": "c5ac5c8147f5bfd644fad6b470432bba86ffc7bcee04a0e0d277cd1ca485207f" }, "8ball": { "category": "activity", "moji": "🎱", + "description": "billiards", "unicodeVersion": "6.0", "digest": "a6e6855775b66c505adee65926a264103ebddf2e2d963db7c009b4fec3a24178" }, "a": { "category": "symbols", "moji": "🅰", + "description": "negative squared latin capital letter a", "unicodeVersion": "6.0", "digest": "bddbb39e8a1d35d42b7c08e7d47f63988cb4d8614b79f74e70b9c67c221896cc" }, "ab": { "category": "symbols", "moji": "🆎", + "description": "negative squared ab", "unicodeVersion": "6.0", "digest": "67430fe5fce981160e2ea9052962e49f264322d3abfc2828fbc311b6cdf67ae8" }, "abc": { "category": "symbols", "moji": "🔤", + "description": "input symbol for latin letters", "unicodeVersion": "6.0", "digest": "282c817ee3414d77a74b815962c33dd9fe71fabaea8c7a9cec466100fbe32187" }, "abcd": { "category": "symbols", "moji": "🔡", + "description": "input symbol for latin small letters", "unicodeVersion": "6.0", "digest": "686728c759f4683c64762ee4eda0a91bf2041f0ae4f358aacf6c09bf51892eff" }, "accept": { "category": "symbols", "moji": "🉑", + "description": "circled ideograph accept", "unicodeVersion": "6.0", "digest": "7208d34c761f10a7fd28f98e25535eba13ff91a64442fc282a98bb77722614f1" }, "aerial_tramway": { "category": "travel", "moji": "🚡", + "description": "aerial tramway", "unicodeVersion": "6.0", "digest": "98df666f34370fc34ce280d84bba5a7e617f733fbbfe66caa424b2afa6ab6777" }, "airplane": { "category": "travel", "moji": "✈", + "description": "airplane", "unicodeVersion": "1.1", "digest": "cc12cf259ef88e57717620cd2bd5aa6a02a8631ee532a3bde24bee78edc5de33" }, "airplane_arriving": { "category": "travel", "moji": "🛬", + "description": "airplane arriving", "unicodeVersion": "7.0", "digest": "80d5b4675f91c4cff06d146d795a065b0ce2a74557df4d9e3314e3d3b5c4ae82" }, "airplane_departure": { "category": "travel", "moji": "🛫", + "description": "airplane departure", "unicodeVersion": "7.0", "digest": "5544eace06b8e1b6ea91940e893e013d33d6b166e14e6d128a87f2cd2de88332" }, "airplane_small": { "category": "travel", "moji": "🛩", + "description": "small airplane", "unicodeVersion": "7.0", "digest": "1a2e07abbbe90d05cee7ff8dd52f443d595ccb38959f3089fe016b77e5d6de7d" }, "alarm_clock": { "category": "objects", "moji": "⏰", + "description": "alarm clock", "unicodeVersion": "6.0", "digest": "fef05a3cd1cddbeca4de8091b94bddb93790b03fa213da86c0eec420f8c49599" }, "alembic": { "category": "objects", "moji": "⚗", + "description": "alembic", "unicodeVersion": "4.1", "digest": "c94b2a4bf24ccf4db27a22c9725cfe900f4a99ec49ef2411d67952bcb2ca1bfb" }, "alien": { "category": "people", "moji": "👽", + "description": "extraterrestrial alien", "unicodeVersion": "6.0", "digest": "856ba98202b244c13a5ee3014a6f7ad592d8c119a30d79e4fc790b74b0e321f7" }, "ambulance": { "category": "travel", "moji": "🚑", + "description": "ambulance", "unicodeVersion": "6.0", "digest": "d9b3c1873de496a4554e715342c72290fb69a9c6766d7885f38bfe9491d052da" }, "amphora": { "category": "objects", "moji": "🏺", + "description": "amphora", "unicodeVersion": "8.0", "digest": "4015f907b649b5e348502cc0e3685ed184e180dca5cc81c43ec516e14df127bf" }, "anchor": { "category": "travel", "moji": "⚓", + "description": "anchor", "unicodeVersion": "4.1", "digest": "2b29b34ef896ebab70016301e3d1880209bbc3c5a5b8d832e43afff9b17ad792" }, "angel": { "category": "people", "moji": "👼", + "description": "baby angel", "unicodeVersion": "6.0", "digest": "db75c2460aaf9cd07cb41fe22c8a6079f3667ffe612a71611358720e2b5512a4" }, "angel_tone1": { "category": "people", "moji": "👼🏻", + "description": "baby angel tone 1", "unicodeVersion": "8.0", "digest": "5871a622469b96296365adaf77d83167759692124c20e5a6e062a525af33472a" }, "angel_tone2": { "category": "people", "moji": "👼🏼", + "description": "baby angel tone 2", "unicodeVersion": "8.0", "digest": "f5993198a5d9daf39e761c783461f07bca237f4e9b739ac300bb8ca001a69a1a" }, "angel_tone3": { "category": "people", "moji": "👼🏽", + "description": "baby angel tone 3", "unicodeVersion": "8.0", "digest": "f0c97a7c4354626267d6ab0f388e4297ad255ab9b061f9c68fbcaa0abfc52783" }, "angel_tone4": { "category": "people", "moji": "👼🏾", + "description": "baby angel tone 4", "unicodeVersion": "8.0", "digest": "6e5dc724c1939d1b0d1a91343662b5bd61ced7709c97802977145ffab6a1f7ac" }, "angel_tone5": { "category": "people", "moji": "👼🏿", + "description": "baby angel tone 5", "unicodeVersion": "8.0", "digest": "52186e1de350c27d25d6010edf44f64a30338b65912ca178429fbcfbd88113c2" }, "anger": { "category": "symbols", "moji": "💢", + "description": "anger symbol", "unicodeVersion": "6.0", "digest": "332493913891aa0eda2743b4bb16c4682400f249998bf34eb292246c9009e17f" }, "anger_right": { "category": "symbols", "moji": "🗯", + "description": "right anger bubble", "unicodeVersion": "7.0", "digest": "8b049511ef3b1b28325841e2f87c60773eaf2f65cabba58d8b0ec3de9b10c0ae" }, "angry": { "category": "people", "moji": "😠", + "description": "angry face", "unicodeVersion": "6.0", "digest": "7e09e7e821f511606341fb5ce4011a8ed9809766ab86b7983ffa6ea352b39ec1" }, "ant": { "category": "nature", "moji": "🐜", + "description": "ant", "unicodeVersion": "6.0", "digest": "929abeaff7ba21ab71cd1ab798af7a6b611e3b3ce1af80cede09a116b223e442" }, "apple": { "category": "food", "moji": "🍎", + "description": "red apple", "unicodeVersion": "6.0", "digest": "2a1b85ce57e3d236ae7777dcf332ec37d03bfd7b19806521a353bc532083224d" }, "aquarius": { "category": "symbols", "moji": "♒", + "description": "aquarius", "unicodeVersion": "1.1", "digest": "fdc42cd41b0dace5eae6baba3143f1e40295d48a29e7103a5bba1d84a056c39d" }, "aries": { "category": "symbols", "moji": "♈", + "description": "aries", "unicodeVersion": "1.1", "digest": "deb135debcde0a98f40361a84ab64d57c18b5b445cd2f4199e8936f052899737" }, "arrow_backward": { "category": "symbols", "moji": "◀", + "description": "black left-pointing triangle", "unicodeVersion": "1.1", "digest": "e162ac82e90d1e925d479fa5c45b9340e0a53287be04e43cbbb2a89c7e7e45e4" }, "arrow_double_down": { "category": "symbols", "moji": "⏬", + "description": "black down-pointing double triangle", "unicodeVersion": "6.0", "digest": "03ca890b05338d40972c7a056d672df620a203c6ca52ff3ff530f1a710905507" }, "arrow_double_up": { "category": "symbols", "moji": "⏫", + "description": "black up-pointing double triangle", "unicodeVersion": "6.0", "digest": "e753f05bce993d62d5dc79e33c441ced059381b6ce21fa3ea4200f1b3236e59d" }, "arrow_down": { "category": "symbols", "moji": "⬇", + "description": "downwards black arrow", "unicodeVersion": "4.0", "digest": "9bf1bd2ea652ca9321087de58c7a112ea04c35676a6ee0766154183f8b95af6c" }, "arrow_down_small": { "category": "symbols", "moji": "🔽", + "description": "down-pointing small red triangle", "unicodeVersion": "6.0", "digest": "7766198bc60cf59d6cdaeeaa700c2282bfff2f0fdeb22cf4581ca284b87a3bb7" }, "arrow_forward": { "category": "symbols", "moji": "▶", + "description": "black right-pointing triangle", "unicodeVersion": "1.1", "digest": "db77d9accd1e02224f5d612f79cd691e6befdf22063475204836be6572510fb7" }, "arrow_heading_down": { "category": "symbols", "moji": "⤵", + "description": "arrow pointing rightwards then curving downwards", "unicodeVersion": "3.2", "digest": "f5396069c8f63c13e6c3e0ecd34267c932451309ade9c1171d410563153bf909" }, "arrow_heading_up": { "category": "symbols", "moji": "⤴", + "description": "arrow pointing rightwards then curving upwards", "unicodeVersion": "3.2", "digest": "1cad71923fa3df24cf543cae4ce775b0f74936f2edd685fd86a7525c41a14568" }, "arrow_left": { "category": "symbols", "moji": "⬅", + "description": "leftwards black arrow", "unicodeVersion": "4.0", "digest": "b629bb3dbe161ef89cfcfced0c7968a68e44a019ad509132987e4973bdc874e7" }, "arrow_lower_left": { "category": "symbols", "moji": "↙", + "description": "south west arrow", "unicodeVersion": "1.1", "digest": "879136ba0e24e6bf3be70118abcb716d71bd74f7b62347bc052b6533c0ea534d" }, "arrow_lower_right": { "category": "symbols", "moji": "↘", + "description": "south east arrow", "unicodeVersion": "1.1", "digest": "86d52ac9b961991e3aaa6a9f9b5ace4db6ffd1b5c171c09c23b516473b55066d" }, "arrow_right": { "category": "symbols", "moji": "➡", + "description": "black rightwards arrow", "unicodeVersion": "1.1", "digest": "45f26a1cbb0f00ed3609b39da52e9d9e896a77e361c4c8036b1bf8038171bd49" }, "arrow_right_hook": { "category": "symbols", "moji": "↪", + "description": "rightwards arrow with hook", "unicodeVersion": "1.1", "digest": "4f452679c71bcea4fc4a701c55156fef3ddc1ebbc70570bedfc9d3a029637ab1" }, "arrow_up": { "category": "symbols", "moji": "⬆", + "description": "upwards black arrow", "unicodeVersion": "4.0", "digest": "982b988ef6651d8a71867ba7c87f640f62dd0eeb0b7c358f5a5c37e8fe507b8b" }, "arrow_up_down": { "category": "symbols", "moji": "↕", + "description": "up down arrow", "unicodeVersion": "1.1", "digest": "645ed8fb6646f49bfd95af1752336deacdadbe5cba13904023a704288f3b0e2c" }, "arrow_up_small": { "category": "symbols", "moji": "🔼", + "description": "up-pointing small red triangle", "unicodeVersion": "6.0", "digest": "4a8c5789c13a852517e639e7a62c2d331464e6fb0358985aa97c1515e97b5e8b" }, "arrow_upper_left": { "category": "symbols", "moji": "↖", + "description": "north west arrow", "unicodeVersion": "1.1", "digest": "79026f828d6ceb7c55a9542770962ba6dcd08203995f6ceeb70333a12307d376" }, "arrow_upper_right": { "category": "symbols", "moji": "↗", + "description": "north east arrow", "unicodeVersion": "1.1", "digest": "7e0f33dfbe65628991c170130d366a3e2cedaf8862ddfcaf3960f395d3da1926" }, "arrows_clockwise": { "category": "symbols", "moji": "🔃", + "description": "clockwise downwards and upwards open circle arrows", "unicodeVersion": "6.0", "digest": "88669679977f7157f0acaa9d6a1b77ccf84d25eb78c5bc8afcde38d3635e7144" }, "arrows_counterclockwise": { "category": "symbols", "moji": "🔄", + "description": "anticlockwise downwards and upwards open circle ar", "unicodeVersion": "6.0", "digest": "a2c6a6d3643c128aee3304cd03bb3d7cfe4d35d3ba825bc9c1142d7832b4426e" }, "art": { "category": "activity", "moji": "🎨", + "description": "artist palette", "unicodeVersion": "6.0", "digest": "b6bc6c4bfb594aadcbb641d006031867678504764bbe0ab84e7b08567a9498da" }, "articulated_lorry": { "category": "travel", "moji": "🚛", + "description": "articulated lorry", "unicodeVersion": "6.0", "digest": "c115e6613ebd718268aa31d265e017138b9fb58bbb8201eb3f40de2380e460aa" }, "asterisk": { "category": "symbols", "moji": "*⃣", + "description": "keycap asterisk", "unicodeVersion": "3.0", "digest": "33d92093f2914448d5a939cf62e8ee3e32931923abdef5f0210e8a8150fa312d" }, "astonished": { "category": "people", "moji": "😲", + "description": "astonished face", "unicodeVersion": "6.0", "digest": "f8531bdda5070d10492709085f4ff652b8be9be6458758940358b9fc594a1f14" }, "athletic_shoe": { "category": "people", "moji": "👟", + "description": "athletic shoe", "unicodeVersion": "6.0", "digest": "1f90dc390e0dea679085465b7f9e786dfd7dd56a3b219987144ed37ab1e9bf95" }, "atm": { "category": "symbols", "moji": "🏧", + "description": "automated teller machine", "unicodeVersion": "6.0", "digest": "7d3ce6a6afb4951546883404b8e36904179f88f1aa533706cf7bf0bbe0d6fd3c" }, "atom": { "category": "symbols", "moji": "⚛", + "description": "atom symbol", "unicodeVersion": "4.1", "digest": "6b6bb83b00707a314e46ff8eefbda40978a291ec7881caba1b1ee273f49c1368" }, "avocado": { "category": "food", "moji": "🥑", + "description": "avocado", "unicodeVersion": "9.0", "digest": "bc1fb203d63b18985598400925de24050bb192afda1cbf0813f85cb139869eff" }, "b": { "category": "symbols", "moji": "🅱", + "description": "negative squared latin capital letter b", "unicodeVersion": "6.0", "digest": "722f9db9442e7c0fc0d0ac0f5291fbf47c6a0ac4d8abd42e97957da705fb82bf" }, "baby": { "category": "people", "moji": "👶", + "description": "baby", "unicodeVersion": "6.0", "digest": "219ae5a571aaf90c060956cd1c56dcc27708c827cecdca3ba1122058a3c4847b" }, "baby_bottle": { "category": "food", "moji": "🍼", + "description": "baby bottle", "unicodeVersion": "6.0", "digest": "4fb71689e9d634e8d1699cf454a71e43f2b5b1a5dbab0bf186626934fdf5b782" }, "baby_chick": { "category": "nature", "moji": "🐤", + "description": "baby chick", "unicodeVersion": "6.0", "digest": "14119874e9b5548028dfb9cc593a541efc1d075ac839a565b92e0c3253cffe7e" }, "baby_symbol": { "category": "symbols", "moji": "🚼", + "description": "baby symbol", "unicodeVersion": "6.0", "digest": "fb4db66868cda45ea3879ffc2ff4f763c56d2d889ae0ab17fe171129ede02f98" }, "baby_tone1": { "category": "people", "moji": "👶🏻", + "description": "baby tone 1", "unicodeVersion": "8.0", "digest": "cd3faf223a298c34e05d469d9d0db08438d97df7fd82c0973f8a9e07d553f5b1" }, "baby_tone2": { "category": "people", "moji": "👶🏼", + "description": "baby tone 2", "unicodeVersion": "8.0", "digest": "5b4539e22e0dd726c27eb8af2357f9240a52aed3f710f3234571cff029cc6198" }, "baby_tone3": { "category": "people", "moji": "👶🏽", + "description": "baby tone 3", "unicodeVersion": "8.0", "digest": "720e740e1ac63c6372269132b1fb6e07a6b91f5c808cc3adef59f0b4500e5e72" }, "baby_tone4": { "category": "people", "moji": "👶🏾", + "description": "baby tone 4", "unicodeVersion": "8.0", "digest": "5e43b69c509bd526ad6f081764578c30b6f3285fb7442222e05ccf62e53bfb64" }, "baby_tone5": { "category": "people", "moji": "👶🏿", + "description": "baby tone 5", "unicodeVersion": "8.0", "digest": "85bba6e0940ccfb99999fe124e815f9dd340d00a5568e13967b02245a62dbf54" }, "back": { "category": "symbols", "moji": "🔙", + "description": "back with leftwards arrow above", "unicodeVersion": "6.0", "digest": "083e4e48b51092c28efb4532e840e1091b5d4b685c6e0f221aa0228f061cd91e" }, "bacon": { "category": "food", "moji": "🥓", + "description": "bacon", "unicodeVersion": "9.0", "digest": "18ad3817f1f88a69706db5727a58e763dde6c21a2d4f184c3d728c32dc5fa05a" }, "badminton": { "category": "activity", "moji": "🏸", + "description": "badminton racquet", "unicodeVersion": "8.0", "digest": "353eb7ee93decd9fe0072e4d78a5618d5e2d9e77a6e4de9fe171870d75e02a66" }, "baggage_claim": { "category": "symbols", "moji": "🛄", + "description": "baggage claim", "unicodeVersion": "6.0", "digest": "7d6bceca92c266da6d2b91dfcf244546fc11022e039e7da8e6888c1696bb2186" }, "balloon": { "category": "objects", "moji": "🎈", + "description": "balloon", "unicodeVersion": "6.0", "digest": "65760aedc1503b426927cff78c24449d563843a274961d962718fa9638375d54" }, "ballot_box": { "category": "objects", "moji": "🗳", + "description": "ballot box with ballot", "unicodeVersion": "7.0", "digest": "4175a56eca5c6458574a681e109b1403fbb143cf27f69ae6c1917650f3e08892" }, "ballot_box_with_check": { "category": "symbols", "moji": "☑", + "description": "ballot box with check", "unicodeVersion": "1.1", "digest": "c98d6f3588dd87e2f318bbfe6c646399a905450edfd814edae4e5b1bddef2134" }, "bamboo": { "category": "nature", "moji": "🎍", + "description": "pine decoration", "unicodeVersion": "6.0", "digest": "e4ee65088df43d7081b1ce6fd996f66f3e0accd88840855c47a98a22997823dd" }, "banana": { "category": "food", "moji": "🍌", + "description": "banana", "unicodeVersion": "6.0", "digest": "f9e8ff910c282c20a8907ff64926b5de4ee250529a1ed718fb33302e6fff8dd9" }, "bangbang": { "category": "symbols", "moji": "‼", + "description": "double exclamation mark", "unicodeVersion": "1.1", "digest": "76536fee63fe964a3f3839d309b1f45028fb0c43f4d1eeee495f17e1532b4def" }, "bank": { "category": "travel", "moji": "🏦", + "description": "bank", "unicodeVersion": "6.0", "digest": "f5d2976bf6d521638ccacc74be06bd4abfeab06c5d898a9d245edad45a5b6306" }, "bar_chart": { "category": "objects", "moji": "📊", + "description": "bar chart", "unicodeVersion": "6.0", "digest": "65a328a1b2d7a5332dd4d93f4dbca13d976f0a505b00835c3fc458e394804240" }, "barber": { "category": "objects", "moji": "💈", + "description": "barber pole", "unicodeVersion": "6.0", "digest": "5e8053d3bb3765a8632fd1cbfe21163f74ed79f6be377eb9603eaaf883d8dc46" }, "baseball": { "category": "activity", "moji": "⚾", + "description": "baseball", "unicodeVersion": "5.2", "digest": "46ac16f8b5455b942f6dbff9483a6fd277721e6719d2731573baabd21c44b34f" }, "basketball": { "category": "activity", "moji": "🏀", + "description": "basketball and hoop", "unicodeVersion": "6.0", "digest": "cc83e2aea8fcd2e9a5789e1932ee3766c40843c142fd3565c4e77dafb21ec7d7" }, "basketball_player": { "category": "activity", "moji": "⛹", + "description": "person with ball", "unicodeVersion": "5.2", "digest": "793ba53c95e8def769383b612037bc9b9bceecaf1e0430c50a4cc128ad18d9b9" }, "basketball_player_tone1": { "category": "activity", "moji": "⛹🏻", + "description": "person with ball tone 1", "unicodeVersion": "8.0", "digest": "2a06522b971e68ee5b8777a58253009b548f4da2fb723c638acb3d7b04edba8f" }, "basketball_player_tone2": { "category": "activity", "moji": "⛹🏼", + "description": "person with ball tone 2", "unicodeVersion": "8.0", "digest": "ecc0e44ab9bc478ba45a055fd69a3a38377b917aac5047963fe80ff8ae5fd8e3" }, "basketball_player_tone3": { "category": "activity", "moji": "⛹🏽", + "description": "person with ball tone 3", "unicodeVersion": "8.0", "digest": "2d38f1851c685d29532c042461d7b5b996e5f04f0ed54857c66073c62a99ceac" }, "basketball_player_tone4": { "category": "activity", "moji": "⛹🏾", + "description": "person with ball tone 4", "unicodeVersion": "8.0", "digest": "09e957c6e9ffc196415f28073aa261feba8efba0bdc694dc08f8f7cd1f88f720" }, "basketball_player_tone5": { "category": "activity", "moji": "⛹🏿", + "description": "person with ball tone 5", "unicodeVersion": "8.0", "digest": "c631cefc5d2a0a31bdb9f0a0d97ea68b1c6928e565468998403034644572a0b0" }, "bat": { "category": "nature", "moji": "🦇", + "description": "bat", "unicodeVersion": "9.0", "digest": "8fc19e0d7d6f80906bdbc06d616a810de66180d96cf28070a53fa61b88904535" }, "bath": { "category": "activity", "moji": "🛀", + "description": "bath", "unicodeVersion": "6.0", "digest": "33b371832f90aad50baf5296f3ad4cc081c319b279f989c74409903d8568e917" }, "bath_tone1": { "category": "activity", "moji": "🛀🏻", + "description": "bath tone 1", "unicodeVersion": "8.0", "digest": "7ae2989e47788ba71359d52da68feec95aaff68a77d5a6556957df1617af8536" }, "bath_tone2": { "category": "activity", "moji": "🛀🏼", + "description": "bath tone 2", "unicodeVersion": "8.0", "digest": "2e86f8edad54d15a7094cd52160cbe51d10aa1750cfb0b3b58e93533f070e327" }, "bath_tone3": { "category": "activity", "moji": "🛀🏽", + "description": "bath tone 3", "unicodeVersion": "8.0", "digest": "654c0cd083a67ff330a38d07352876d265390e5399e5352598d64a6c7e5eeba7" }, "bath_tone4": { "category": "activity", "moji": "🛀🏾", + "description": "bath tone 4", "unicodeVersion": "8.0", "digest": "adad88c6830f31c4b5be194d1987d6aadf4adf45e4cb7f2e4657f0d20c0d663a" }, "bath_tone5": { "category": "activity", "moji": "🛀🏿", + "description": "bath tone 5", "unicodeVersion": "8.0", "digest": "952c4c9bf24e001e23a33ebf97bd92969cd9143e28ce93f9aafc708a8f966903" }, "bathtub": { "category": "objects", "moji": "🛁", + "description": "bathtub", "unicodeVersion": "6.0", "digest": "844dffb87ef872594195069b0d0df27c3fe51f3967ccbc8b2df811a086dd483a" }, "battery": { "category": "objects", "moji": "🔋", + "description": "battery", "unicodeVersion": "6.0", "digest": "949ae06648667fb13d9121a6dfdd03bf8692794b28c36e9a8e8ac4515664449a" }, "beach": { "category": "travel", "moji": "🏖", + "description": "beach with umbrella", "unicodeVersion": "7.0", "digest": "37fa2158977d470186caaa1aa06669b6dc5026ba49a0c44c5255541f8e974e26" }, "beach_umbrella": { "category": "objects", "moji": "⛱", + "description": "umbrella on ground", "unicodeVersion": "5.2", "digest": "d045f1de10038b9fb1eaa2529b2f80b7e3be1cff503efcc2d680663d1fbbc18f" }, "bear": { "category": "nature", "moji": "🐻", + "description": "bear face", "unicodeVersion": "6.0", "digest": "a4b9066eaa5681e6af06e596a96a5217037460ffc3b013e8db4d34d762413246" }, "bed": { "category": "objects", "moji": "🛏", + "description": "bed", "unicodeVersion": "7.0", "digest": "08f6e20db51b1fb650b390a0a3074938646772f3fcee8c295d47742e44fe1e30" }, "bee": { "category": "nature", "moji": "🐝", + "description": "honeybee", "unicodeVersion": "6.0", "digest": "5beb9a1650681b4adf69999d4808231c38f41a3ec693480b807cda86f964c570" }, "beer": { "category": "food", "moji": "🍺", + "description": "beer mug", "unicodeVersion": "6.0", "digest": "69e227104976548ee0f37375fe1526fd65ef0a328d2d92db2feb1edfd7032bd4" }, "beers": { "category": "food", "moji": "🍻", + "description": "clinking beer mugs", "unicodeVersion": "6.0", "digest": "db8b32d93bf6d161a3b027e55651d8f51231b13928b3610987ef62bb634d7501" }, "beetle": { "category": "nature", "moji": "🐞", + "description": "lady beetle", "unicodeVersion": "6.0", "digest": "5aaa428e3f63f7cd1696839ab05be03fa0cd0cbed30a05c36cb270da330c3849" }, "beginner": { "category": "symbols", "moji": "🔰", + "description": "japanese symbol for beginner", "unicodeVersion": "6.0", "digest": "2de4fdf92f182c42b12b7527034eaf767d996848b61f31ee69167728411ca0b1" }, "bell": { "category": "symbols", "moji": "🔔", + "description": "bell", "unicodeVersion": "6.0", "digest": "18d419417746ead408072b78fe2edb6314cdb49492873966fa9f9f06be09899b" }, "bellhop": { "category": "objects", "moji": "🛎", + "description": "bellhop bell", "unicodeVersion": "7.0", "digest": "b8187bc4059f6a0924a47fe3f6c07f656bed0334bbcbfa1e89f800fe6594ff08" }, "bento": { "category": "food", "moji": "🍱", + "description": "bento box", "unicodeVersion": "6.0", "digest": "d46d4f681c5da7f7678b51be3445454a8ed18d917e132ae79077f05310e485f1" }, "bicyclist": { "category": "activity", "moji": "🚴", + "description": "bicyclist", "unicodeVersion": "6.0", "digest": "3302147b6b47c16adb97d78b7b761a1ca80e6d0b41d0b60f4da338d2f55f968b" }, "bicyclist_tone1": { "category": "activity", "moji": "🚴🏻", + "description": "bicyclist tone 1", "unicodeVersion": "8.0", "digest": "27eaae0eb61f5e7b3cd9faf02c042d6643a368051a7c9d7da4e0fb9802d39242" }, "bicyclist_tone2": { "category": "activity", "moji": "🚴🏼", + "description": "bicyclist tone 2", "unicodeVersion": "8.0", "digest": "39ee9e1071700da7079ad0146bf5711c3a222991eeca8b29b72a65677604444d" }, "bicyclist_tone3": { "category": "activity", "moji": "🚴🏽", + "description": "bicyclist tone 3", "unicodeVersion": "8.0", "digest": "03e1d2c4232c896147a9d4bf43becd61edbb5c84fc7193ecea474c0f9fb36817" }, "bicyclist_tone4": { "category": "activity", "moji": "🚴🏾", + "description": "bicyclist tone 4", "unicodeVersion": "8.0", "digest": "61393d9c4805be0379d86dd5bec9a1b02314433ab36cfd85bb48dfd073746617" }, "bicyclist_tone5": { "category": "activity", "moji": "🚴🏿", + "description": "bicyclist tone 5", "unicodeVersion": "8.0", "digest": "2b46d5f8303e5710dbf5db3a4edc9d88a032fe123fe79158024c9f51df5458c6" }, "bike": { "category": "travel", "moji": "🚲", + "description": "bicycle", "unicodeVersion": "6.0", "digest": "b41daa7c549d483e2336186a28baaa8ecb11986f490c0c54c793c44900c8f652" }, "bikini": { "category": "people", "moji": "👙", + "description": "bikini", "unicodeVersion": "6.0", "digest": "07fe156f64673818d69ce3bf03950ca59e3b5d346e45ca541da4078ab791f5ae" }, "biohazard": { "category": "symbols", "moji": "☣", + "description": "biohazard sign", "unicodeVersion": "1.1", "digest": "96163e31f0b8dc5a59772133ede9cc2f40f94330d0b15e3d044b28747e2be788" }, "bird": { "category": "nature", "moji": "🐦", + "description": "bird", "unicodeVersion": "6.0", "digest": "f916eaf8f271b3767ade9eabb69594c0479f45472d471cabaf59f6e965c161e0" }, "birthday": { "category": "food", "moji": "🎂", + "description": "birthday cake", "unicodeVersion": "6.0", "digest": "89e7c4c598ebee8ec8ab11ebe4ccc6defb7c4d2987ee2379a19b3b59827dd98a" }, "black_circle": { "category": "symbols", "moji": "⚫", + "description": "medium black circle", "unicodeVersion": "4.1", "digest": "c2ba672994ad0f99d7fdc449f3fee45a2dca68a58f9fe95825b38465a30ef44e" }, "black_heart": { "category": "symbols", "moji": "🖤", + "description": "black heart", "unicodeVersion": "9.0", "digest": "f334679168d6dd7328c28e9ae3cb2b1fca0e9c2777938d586bfe623db2a688b9" }, "black_joker": { "category": "symbols", "moji": "🃏", + "description": "playing card black joker", "unicodeVersion": "6.0", "digest": "d004b25f186494d5b2c65204caa9daecd749c840a0bea5718735e18109e5394d" }, "black_large_square": { "category": "symbols", "moji": "⬛", + "description": "black large square", "unicodeVersion": "5.1", "digest": "cbd90dcbc2f674eafa53820548b5263c18c9845ab39937f085e85aca0aebb479" }, "black_medium_small_square": { "category": "symbols", "moji": "◾", + "description": "black medium small square", "unicodeVersion": "3.2", "digest": "ab38363c2e862b8f67c719397a09a18e1ef996eec190691fdf769f5cfb209660" }, "black_medium_square": { "category": "symbols", "moji": "◼", + "description": "black medium square", "unicodeVersion": "3.2", "digest": "c9ffa87c37e8ee65fadcf755176949901aec7367e02abb85e63cad60cd922116" }, "black_nib": { "category": "objects", "moji": "✒", + "description": "black nib", "unicodeVersion": "1.1", "digest": "58fb23b1155102970eaa23765e7d529a21e8e545e076ec1158bf11b4de5f51a8" }, "black_small_square": { "category": "symbols", "moji": "▪", + "description": "black small square", "unicodeVersion": "1.1", "digest": "f69be6de578fffce5a3e60eda690104b2ef6a855c630040104fb760a02ff1aef" }, "black_square_button": { "category": "symbols", "moji": "🔲", + "description": "black square button", "unicodeVersion": "6.0", "digest": "9d818fcd08ed38cd0bbbcfd83e665aa29b3761c0d8b9806d8954d36785e267a8" }, "blossom": { "category": "nature", "moji": "🌼", + "description": "blossom", "unicodeVersion": "6.0", "digest": "e8cf369d4e4cdb4eccc2ebcbb35439b0344221115701daae642e58dff8544922" }, "blowfish": { "category": "nature", "moji": "🐡", + "description": "blowfish", "unicodeVersion": "6.0", "digest": "e706849ed00f08a82312381c76f6f9ba6cc261fbf87a839c85e7dd54138f9dc3" }, "blue_book": { "category": "objects", "moji": "📘", + "description": "blue book", "unicodeVersion": "6.0", "digest": "4c845748fe890516b32981b0b62bf3e8e9d906840c2060179f4f844100780615" }, "blue_car": { "category": "travel", "moji": "🚙", + "description": "recreational vehicle", "unicodeVersion": "6.0", "digest": "eca91934eb5481726cfd897b1ed5eac306e14d02499fbe49316aaec6c72b6707" }, "blue_heart": { "category": "symbols", "moji": "💙", + "description": "blue heart", "unicodeVersion": "6.0", "digest": "2caa0c8d18538cc871c6fe328a52f71e1df8aabf4d1cc2f5324b261d1b8cb99a" }, "blush": { "category": "people", "moji": "😊", + "description": "smiling face with smiling eyes", "unicodeVersion": "6.0", "digest": "3bfe8d603cfa39999c164779f666d39bbc507f124ba80233ee72da7b3b0c0457" }, "boar": { "category": "nature", "moji": "🐗", + "description": "boar", "unicodeVersion": "6.0", "digest": "c9d67479cace427ac3c30460fcffa1bf9a8e5262c0390962405dbbe6bf830fa6" }, "bomb": { "category": "objects", "moji": "💣", + "description": "bomb", "unicodeVersion": "6.0", "digest": "0155559abc4084f80e9b0b2a2091b8710ddd6369993b7fdd0685f4f8c2fd7e6c" }, "book": { "category": "objects", "moji": "📖", + "description": "open book", "unicodeVersion": "6.0", "digest": "9d912a9d1bb10dc7f2645b345ed09e90461e83df0de275acb806f1f75cef1fcf" }, "bookmark": { "category": "objects", "moji": "🔖", + "description": "bookmark", "unicodeVersion": "6.0", "digest": "5705e3108259d6900649157843c50e22d0086c3630b291d3f942da1a736e3e3d" }, "bookmark_tabs": { "category": "objects", "moji": "📑", + "description": "bookmark tabs", "unicodeVersion": "6.0", "digest": "c8fc7c9f3f82e1ccc97fc591345fdd88b09eec0fca428d8d4632a121cf1bc39a" }, "books": { "category": "objects", "moji": "📚", + "description": "books", "unicodeVersion": "6.0", "digest": "cbcf55d39dd05d26ef7350bc51e0e2f064f78bb8f59d407b516d63f68558f8e4" }, "boom": { "category": "nature", "moji": "💥", + "description": "collision symbol", "unicodeVersion": "6.0", "digest": "f5400e9583f7f997cd2385f21379f6229424a9b221445bc8f36c0bb64bdb3168" }, "boot": { "category": "people", "moji": "👢", + "description": "womans boots", "unicodeVersion": "6.0", "digest": "b4706ff35909a6fb759a3b8a797e90cb67ffc60e4853386a7d89ace9693a9364" }, "bouquet": { "category": "nature", "moji": "💐", + "description": "bouquet", "unicodeVersion": "6.0", "digest": "b93751a27b40f6185a22b3e8b413f0fe09b6010d1057c672e1a23088e0b8286f" }, "bow": { "category": "people", "moji": "🙇", + "description": "person bowing deeply", "unicodeVersion": "6.0", "digest": "33cd6da4d408f18d98bebc6a277dea8b914150e32ee472586ce3f1eb814462bd" }, "bow_and_arrow": { "category": "activity", "moji": "🏹", + "description": "bow and arrow", "unicodeVersion": "8.0", "digest": "051b4d50ab21a68b8583a6313ec183e3e1e96f493b0f4541fbb888f0b95fdd4d" }, "bow_tone1": { "category": "people", "moji": "🙇🏻", + "description": "person bowing deeply tone 1", "unicodeVersion": "8.0", "digest": "995c8400ad60d5adc66c9ae5e3c0ecf56c48b478ad79418d45b6289933d25bdd" }, "bow_tone2": { "category": "people", "moji": "🙇🏼", + "description": "person bowing deeply tone 2", "unicodeVersion": "8.0", "digest": "af89eec2fccda99d9bdd373b2345595882fee1c0a15d29af9028089e20255325" }, "bow_tone3": { "category": "people", "moji": "🙇🏽", + "description": "person bowing deeply tone 3", "unicodeVersion": "8.0", "digest": "015d8122abdf2d0caa03815545f50fb7a71e05dacd46aaa133cc9ace5192f266" }, "bow_tone4": { "category": "people", "moji": "🙇🏾", + "description": "person bowing deeply tone 4", "unicodeVersion": "8.0", "digest": "e8409096a795b775def654d36aeccb8eb91e83d7d1b32145cd73fd0b7b9e885c" }, "bow_tone5": { "category": "people", "moji": "🙇🏿", + "description": "person bowing deeply tone 5", "unicodeVersion": "8.0", "digest": "d87042cde8dbad9fb1a91a2ec60116e27b4a76388b5779d771a0bbae12a2814d" }, "bowling": { "category": "activity", "moji": "🎳", + "description": "bowling", "unicodeVersion": "6.0", "digest": "737f2cdfa4ac964baade585a39771b18080bd5e9b55c8661d3518f468f344662" }, "boxing_glove": { "category": "activity", "moji": "🥊", + "description": "boxing glove", "unicodeVersion": "9.0", "digest": "c914b2ce45f20afad66ad6f0d1b0750c4469e4f48b686dfc4aad1ec8d289c563" }, "boy": { "category": "people", "moji": "👦", + "description": "boy", "unicodeVersion": "6.0", "digest": "7bc0173d8c88f3f12d41f213f7a3a9f5ebf65efad610fd5a2a31935128a6a6c1" }, "boy_tone1": { "category": "people", "moji": "👦🏻", + "description": "boy tone 1", "unicodeVersion": "8.0", "digest": "c0e2f0483715b239fe145b0056566f7a3a722319d9a87c1e66733dff1916a19f" }, "boy_tone2": { "category": "people", "moji": "👦🏼", + "description": "boy tone 2", "unicodeVersion": "8.0", "digest": "0001d0bd1ff4dbd898604ba965b4039d09667d955bc0349301b992f9ab6dd7fd" }, "boy_tone3": { "category": "people", "moji": "👦🏽", + "description": "boy tone 3", "unicodeVersion": "8.0", "digest": "e0f08755955fd2e0bd1c5d5e84429b2a234b24a744bb50bb9f1148495b2b29f9" }, "boy_tone4": { "category": "people", "moji": "👦🏾", + "description": "boy tone 4", "unicodeVersion": "8.0", "digest": "04b6bfee58a26b1ce2e5b403504a7033aaf395f03f5cd23e824f32c90c395fe6" }, "boy_tone5": { "category": "people", "moji": "👦🏿", + "description": "boy tone 5", "unicodeVersion": "8.0", "digest": "0f76e97237203950da36c737dcc6f56dcd6c123401a8c817a0636376c7f38ef5" }, "bread": { "category": "food", "moji": "🍞", + "description": "bread", "unicodeVersion": "6.0", "digest": "81739830f16f33e6a1dd7cc17c25df207846062bb5167bb8abed7fdd49268b86" }, "bride_with_veil": { "category": "people", "moji": "👰", + "description": "bride with veil", "unicodeVersion": "6.0", "digest": "8e24bd91c3f564cf6148f2b3b4a7d692c11dd059e76a13331fdfb04ae060ea70" }, "bride_with_veil_tone1": { "category": "people", "moji": "👰🏻", + "description": "bride with veil tone 1", "unicodeVersion": "8.0", "digest": "0bd2f16f72586f50e768b14b9b353f2e98ccbb2581a568c33b06be56e70ca063" }, "bride_with_veil_tone2": { "category": "people", "moji": "👰🏼", + "description": "bride with veil tone 2", "unicodeVersion": "8.0", "digest": "e5463f811b2075754f0718b891757cd2e81071edf7af2215581227e1aad1d068" }, "bride_with_veil_tone3": { "category": "people", "moji": "👰🏽", + "description": "bride with veil tone 3", "unicodeVersion": "8.0", "digest": "e5a053a26f7ccebae7eb12f638be5ed80f77b744708d783eab2eb8aa091cf516" }, "bride_with_veil_tone4": { "category": "people", "moji": "👰🏾", + "description": "bride with veil tone 4", "unicodeVersion": "8.0", "digest": "410e23825e4401460946dc67a618bd3ace6e1a7c07dd88580a2349423685261f" }, "bride_with_veil_tone5": { "category": "people", "moji": "👰🏿", + "description": "bride with veil tone 5", "unicodeVersion": "8.0", "digest": "454e87e5a74e13e5b4993541231516fbbe6dbe9f990e1a6f3f4a744d7d4c1615" }, "bridge_at_night": { "category": "travel", "moji": "🌉", + "description": "bridge at night", "unicodeVersion": "6.0", "digest": "9d3cda5a59e27e3c90939f1ddbe7e998b3ea4fcacfa1467dea0edf39613c2d7f" }, "briefcase": { "category": "people", "moji": "💼", + "description": "briefcase", "unicodeVersion": "6.0", "digest": "9d00d6a92632aaadc71b017f448c883b27eb31a7554ebb51f7e3a9841f0f7f2b" }, "broken_heart": { "category": "symbols", "moji": "💔", + "description": "broken heart", "unicodeVersion": "6.0", "digest": "c7ca53f444d72e596af46b61ffbc9e7c18a645020c22691e44f967db98dbf853" }, "bug": { "category": "nature", "moji": "🐛", + "description": "bug", "unicodeVersion": "6.0", "digest": "0dccb1d5eb91769377b4c5b310f007b60f54a5c48ba9e467b3a06898a4831b90" }, "bulb": { "category": "objects", "moji": "💡", + "description": "electric light bulb", "unicodeVersion": "6.0", "digest": "ccdaa2dfde5a88a347035a94b9d4d86cfc335ce0a73292423f5788a4bd21a5a8" }, "bullettrain_front": { "category": "travel", "moji": "🚅", + "description": "high-speed train with bullet nose", "unicodeVersion": "6.0", "digest": "5195a6a6d23f28e1aa5ebac6ede0f6c6a8b7ff33a9edf034814f227fe976177a" }, "bullettrain_side": { "category": "travel", "moji": "🚄", + "description": "high-speed train", "unicodeVersion": "6.0", "digest": "96e74842e919716b7bbbab57339bfd70f099a9bcb4710dffd7c80cf38a7bbff7" }, "burrito": { "category": "food", "moji": "🌯", + "description": "burrito", "unicodeVersion": "8.0", "digest": "b2cf81f1efdf87e674461f73f67cd4b58a5f695e65598d0dd3899f2597da43cf" }, "bus": { "category": "travel", "moji": "🚌", + "description": "bus", "unicodeVersion": "6.0", "digest": "192850b762edad21ac8770df38b9cae6d2bc1697a838462f3e36066bfb4eee50" }, "busstop": { "category": "travel", "moji": "🚏", + "description": "bus stop", "unicodeVersion": "6.0", "digest": "adabb1ec36402b33feb636eae3656e5a8b51ff1071bcb14125d8ab80d6d12d2a" }, "bust_in_silhouette": { "category": "people", "moji": "👤", + "description": "bust in silhouette", "unicodeVersion": "6.0", "digest": "277ae43301f1e49e0be03c8e52f0dc7b70c67f9d146bca0a14172e0098f115e6" }, "busts_in_silhouette": { "category": "people", "moji": "👥", + "description": "busts in silhouette", "unicodeVersion": "6.0", "digest": "7fee96f1b68bb2c6002e47f2ed13c06baa6a3168441b9aca572db7ec45612f7b" }, "butterfly": { "category": "nature", "moji": "🦋", + "description": "butterfly", "unicodeVersion": "9.0", "digest": "a91b6598c17b44a8dc8935a1d99e25f4483ea41470cdd2da343039a9eec29ef1" }, "cactus": { "category": "nature", "moji": "🌵", + "description": "cactus", "unicodeVersion": "6.0", "digest": "2c5c4c35f26c7046fdc002b337e0d939729b33a26980e675950f9934c91e40fd" }, "cake": { "category": "food", "moji": "🍰", + "description": "shortcake", "unicodeVersion": "6.0", "digest": "b928902df8084210d51c1da36f9119164a325393c391b28cd8ea914e0b95c17b" }, "calendar": { "category": "objects", "moji": "📆", + "description": "tear-off calendar", "unicodeVersion": "6.0", "digest": "9d990be27778daab041a3583edbd8f83fc8957e42a3aec729c0e2e224a8d05e3" }, "calendar_spiral": { "category": "objects", "moji": "🗓", + "description": "spiral calendar pad", "unicodeVersion": "7.0", "digest": "441a0750eade7ce33e28e58bec76958990c412b68409fcdde59ebad1f25361bb" }, "call_me": { "category": "people", "moji": "🤙", + "description": "call me hand", "unicodeVersion": "9.0", "digest": "83d2ed96dcb8b4adf4f4d030ffd07e25ca16351e1a4fbefdf9f46f5ca496a55f" }, "call_me_tone1": { "category": "people", "moji": "🤙🏻", + "description": "call me hand tone 1", "unicodeVersion": "9.0", "digest": "4a5748efa83e7294e8338b8795d4d315ff1cd31ead6759004d0eb330e50de8cd" }, "call_me_tone2": { "category": "people", "moji": "🤙🏼", + "description": "call me hand tone 2", "unicodeVersion": "9.0", "digest": "54feaa6e3c5789ae6e15622127f0e0213234b4b886e1588ce95814348b1f1519" }, "call_me_tone3": { "category": "people", "moji": "🤙🏽", + "description": "call me hand tone 3", "unicodeVersion": "9.0", "digest": "57e949b951e14843b712dab5a828f915ee255f5bb973db33946aab4057427419" }, "call_me_tone4": { "category": "people", "moji": "🤙🏾", + "description": "call me hand tone 4", "unicodeVersion": "9.0", "digest": "f7787e933978a09c7b8ab8d3b1e1ab395aaae998c455e93bb3db24a4c8a60fe0" }, "call_me_tone5": { "category": "people", "moji": "🤙🏿", + "description": "call me hand tone 5", "unicodeVersion": "9.0", "digest": "1fdb7d833d000b117d20d48142d3026a61cc9c8b712ebb498fa66bf75c74d7a5" }, "calling": { "category": "objects", "moji": "📲", + "description": "mobile phone with rightwards arrow at left", "unicodeVersion": "6.0", "digest": "acf668c75c11c36686005788266524a972fa1c5bcf666ff3403d909edc5cee91" }, "camel": { "category": "nature", "moji": "🐫", + "description": "bactrian camel", "unicodeVersion": "6.0", "digest": "5f927927a7ab1277d0dc8b8211436957968b1e11365a8bf535e9bb94f92c5631" }, "camera": { "category": "objects", "moji": "📷", + "description": "camera", "unicodeVersion": "6.0", "digest": "fde03e396822a36cd6ae756ede885b945a074395264162731ca5db47a3b39d80" }, "camera_with_flash": { "category": "objects", "moji": "📸", + "description": "camera with flash", "unicodeVersion": "7.0", "digest": "9afd380208187780f00244c45d4db6c5ea1ea088d4a1bd8fc92a8f3877149750" }, "camping": { "category": "travel", "moji": "🏕", + "description": "camping", "unicodeVersion": "7.0", "digest": "a42a4ff9521affa72db7b0f01da169b4cb6afb9db1c5dfad47dd4c507bfc30d9" }, "cancer": { "category": "symbols", "moji": "♋", + "description": "cancer", "unicodeVersion": "1.1", "digest": "528c6f21df99a756b553d93a7f395b0f662b30a323affd05f0cedee8ff7b41d6" }, "candle": { "category": "objects", "moji": "🕯", + "description": "candle", "unicodeVersion": "7.0", "digest": "211c04dc3a91b071c284d4180ed09f9d3320e3fd6ba8a9fddd0677bc97fd12cb" }, "candy": { "category": "food", "moji": "🍬", + "description": "candy", "unicodeVersion": "6.0", "digest": "9cff4538918f60f770fceb96e964f5dc3ce31fd08ddd2ab3bfdf2981bfa74100" }, "canoe": { "category": "travel", "moji": "🛶", + "description": "canoe", "unicodeVersion": "9.0", "digest": "56ca308cc2ad4827468cf58c4ccf6ef6b3382835a91e935540a2b973e01d2572" }, "capital_abcd": { "category": "symbols", "moji": "🔠", + "description": "input symbol for latin capital letters", "unicodeVersion": "6.0", "digest": "a416d0b3f564037b680f801fb773b6eaf67225e2cbbfd2cb8a5db0de044321fa" }, "capricorn": { "category": "symbols", "moji": "♑", + "description": "capricorn", "unicodeVersion": "1.1", "digest": "f11abad102603737b55486fe2ea4d01f28b203394bcd84f19a7948156e6c4b96" }, "card_box": { "category": "objects", "moji": "🗃", + "description": "card file box", "unicodeVersion": "7.0", "digest": "7a6199d562f30e02ed31094de6aebeb99eae8ac156f6910463dfed73256f4c9a" }, "card_index": { "category": "objects", "moji": "📇", + "description": "card index", "unicodeVersion": "6.0", "digest": "86e187e0a72ca5d00207d6ef34d66ce15046848a831c2b5184fb840c5332a2a8" }, "carousel_horse": { "category": "travel", "moji": "🎠", + "description": "carousel horse", "unicodeVersion": "6.0", "digest": "c0e7059efc39a64233f774c02ddb1ab51888fff180f906ce13a6e4f9509672fe" }, "carrot": { "category": "food", "moji": "🥕", + "description": "carrot", "unicodeVersion": "9.0", "digest": "3a6fd98b63ee73d982a9cdacb08cf7b4014368cde8ffce6056b7df25a5a472b1" }, "cartwheel": { "category": "activity", "moji": "🤸", + "description": "person doing cartwheel", "unicodeVersion": "9.0", "digest": "d78de3435e0b04a9b1a1048ae12e63e3248f9ace3a0db4d3bda584f22af18863" }, "cartwheel_tone1": { "category": "activity", "moji": "🤸🏻", + "description": "person doing cartwheel tone 1", "unicodeVersion": "9.0", "digest": "39a49781a269bb40d8efc8fd73c973b00fb2e192850ea6073062b5dea0cd5b74" }, "cartwheel_tone2": { "category": "activity", "moji": "🤸🏼", + "description": "person doing cartwheel tone 2", "unicodeVersion": "9.0", "digest": "6231eb35be45457fd648f8f4b79983f03705c9d983a18067f7e6d9ae47bc1958" }, "cartwheel_tone3": { "category": "activity", "moji": "🤸🏽", + "description": "person doing cartwheel tone 3", "unicodeVersion": "9.0", "digest": "ca483c78cc823811a8c279c501d9b283e4c990dafc5995ad40e68ecb0af554df" }, "cartwheel_tone4": { "category": "activity", "moji": "🤸🏾,", + "description": "person doing cartwheel tone 4", "unicodeVersion": "9.0", "digest": "8253afb672431c84e498014c30babb00b9284bec773009e79f7f06aa7108643e" }, "cartwheel_tone5": { "category": "activity", "moji": "🤸🏿", + "description": "person doing cartwheel tone 5", "unicodeVersion": "9.0", "digest": "6fd92baff57c38b3adb6753d9e7e547e762971a8872fd3f1e71c6aaf0b1d3ab9" }, "cat": { "category": "nature", "moji": "🐱", + "description": "cat face", "unicodeVersion": "6.0", "digest": "e52d0d3a205a0ba99094717e171a7f572b713a0e21b276ffa4a826596fe5cafc" }, "cat2": { "category": "nature", "moji": "🐈", + "description": "cat", "unicodeVersion": "6.0", "digest": "46aa67a99f782935932c77b8de93287142297abe52928c173191cf55bb8f4339" }, "cd": { "category": "objects", "moji": "💿", + "description": "optical disc", "unicodeVersion": "6.0", "digest": "16363d8a34b873c12df6354b99f575cae3d80e0d27100ed7eea70f0310953c7b" }, "chains": { "category": "objects", "moji": "⛓", + "description": "chains", "unicodeVersion": "5.2", "digest": "3884cdbc6f2b433062af06f942552e563231c24727a2f10fa280b3bb7aa614e2" }, "champagne": { "category": "food", "moji": "🍾", + "description": "bottle with popping cork", "unicodeVersion": "8.0", "digest": "9e6e8987f30a37ae0f3d7dab2f5eeb50aa32b4f31402b29315eb2994afc72457" }, "champagne_glass": { "category": "food", "moji": "🥂", + "description": "clinking glasses", "unicodeVersion": "9.0", "digest": "5a2e4773f7eb126a00122cbfa4dc535da51ce00e0bf0d8d6ff8bab8b3365f8d2" }, "chart": { "category": "symbols", "moji": "💹", + "description": "chart with upwards trend and yen sign", "unicodeVersion": "6.0", "digest": "a092dbc08f925b028286b2b495a5f59033b8537a586a694f46f4c1e7c3a1e27f" }, "chart_with_downwards_trend": { "category": "objects", "moji": "📉", + "description": "chart with downwards trend", "unicodeVersion": "6.0", "digest": "5db7ccbc37665736a9c0b2f50247dcc09e404ec37f39db45b7b8b9464172a18c" }, "chart_with_upwards_trend": { "category": "objects", "moji": "📈", + "description": "chart with upwards trend", "unicodeVersion": "6.0", "digest": "bc4ea250b102fe5c09847e471478aff065ad3df755d9717896d38d887d9c6733" }, "checkered_flag": { "category": "travel", "moji": "🏁", + "description": "chequered flag", "unicodeVersion": "6.0", "digest": "0e77180e0cf9fc87e755a5a42cf23aec6bf30931db41331311e97ba0be178b78" }, "cheese": { "category": "food", "moji": "🧀", + "description": "cheese wedge", "unicodeVersion": "8.0", "digest": "50a6cb906c2120e2bbc0e22105924262007cfe1554d7b02b8cc84b6adedc6a0b" }, "cherries": { "category": "food", "moji": "🍒", + "description": "cherries", "unicodeVersion": "6.0", "digest": "13b8db9e7e6eec8509aa80c762966e1bf3538fcb1ac3d6eab18ee4da1528cf84" }, "cherry_blossom": { "category": "nature", "moji": "🌸", + "description": "cherry blossom", "unicodeVersion": "6.0", "digest": "af3083f5f8dd94936113f2e16caba5aec7a774d5589aa08bf5de82a2d278cc66" }, "chestnut": { "category": "nature", "moji": "🌰", + "description": "chestnut", "unicodeVersion": "6.0", "digest": "9f85b79b207a69ab81ab88dcef04954000965b039b4cf57de5f1b381745ab98b" }, "chicken": { "category": "nature", "moji": "🐔", + "description": "chicken", "unicodeVersion": "6.0", "digest": "57ceb4459d183740009caac6ebed089d2f1e12f67c138e1be1d0f992313c0ac4" }, "children_crossing": { "category": "symbols", "moji": "🚸", + "description": "children crossing", "unicodeVersion": "6.0", "digest": "0ded7d9aca0161e8ef8e2858c3c198e70e4badc7105ac3a6886e06975de19106" }, "chipmunk": { "category": "nature", "moji": "🐿", + "description": "chipmunk", "unicodeVersion": "7.0", "digest": "5b0dc1a859163097727ba2ba5ffca38b0a54d925eebb089977d28d0b4d917a3f" }, "chocolate_bar": { "category": "food", "moji": "🍫", + "description": "chocolate bar", "unicodeVersion": "6.0", "digest": "dd273e5050488acaf885f8a18b6e2b3901f69c5b39fa6465fb60621783d4109a" }, "christmas_tree": { "category": "nature", "moji": "🎄", + "description": "christmas tree", "unicodeVersion": "6.0", "digest": "ce60cbe2ebbe8057be8edea2392455fedd2bcda64a0a831f6a1942028af7e747" }, "church": { "category": "travel", "moji": "⛪", + "description": "church", "unicodeVersion": "5.2", "digest": "2c328456528f7336e59443e20ec3ab22fe71f1fccb1dd50d0ad68eb206937557" }, "cinema": { "category": "symbols", "moji": "🎦", + "description": "cinema", "unicodeVersion": "6.0", "digest": "4c26dcdc76f93dbc2a1dc49ed4e132b8e8f2b7cdc1acf5e09b3dfd99430d97cd" }, "circus_tent": { "category": "activity", "moji": "🎪", + "description": "circus tent", "unicodeVersion": "6.0", "digest": "fec5f2a06222be8be549178b29720343cc00145177ec387ca4e6f3432481fe77" }, "city_dusk": { "category": "travel", "moji": "🌆", + "description": "cityscape at dusk", "unicodeVersion": "6.0", "digest": "bba345e949dcc51f5f018220f000223797970c82ead2ab9c822f9dc0847aa155" }, "city_sunset": { "category": "travel", "moji": "🌇", + "description": "sunset over buildings", "unicodeVersion": "6.0", "digest": "a846df1a4c7c778f8e1729804aece86eb29d2fcb95dc39eaaf2aae1897f3dcc7" }, "cityscape": { "category": "travel", "moji": "🏙", + "description": "cityscape", "unicodeVersion": "7.0", "digest": "ee360be7514c4bfb0d539dd28f3b2031ebcef04e850723ec0685fb54bd8e6d5f" }, "cl": { "category": "symbols", "moji": "🆑", + "description": "squared cl", "unicodeVersion": "6.0", "digest": "fcec2855dbad9fda11d6e2802bc0dcaabab0b5be233508f5e439f156f07602c1" }, "clap": { "category": "people", "moji": "👏", + "description": "clapping hands sign", "unicodeVersion": "6.0", "digest": "a1860ce7812a9f6fb55e45761e1b79a2f8f0620eb04f80748a38420889d58a2a" }, "clap_tone1": { "category": "people", "moji": "👏🏻", + "description": "clapping hands sign tone 1", "unicodeVersion": "8.0", "digest": "18a7022e08223fb2109af5a9b9a5b4f47dc870ce4453f4987d2d0b729ef54586" }, "clap_tone2": { "category": "people", "moji": "👏🏼", + "description": "clapping hands sign tone 2", "unicodeVersion": "8.0", "digest": "5954c8658b15e755d2018d8674df84d38e22ffededc4d726c6a33b709f71426a" }, "clap_tone3": { "category": "people", "moji": "👏🏽", + "description": "clapping hands sign tone 3", "unicodeVersion": "8.0", "digest": "22639b6bd3c53784a2f855d6db7bdf31621519f19dfc29a6bc310eee6421f742" }, "clap_tone4": { "category": "people", "moji": "👏🏾", + "description": "clapping hands sign tone 4", "unicodeVersion": "8.0", "digest": "e55248dc163d1bbd118b50cd8767750ead86d082151febbc0a75b32d63abceec" }, "clap_tone5": { "category": "people", "moji": "👏🏿", + "description": "clapping hands sign tone 5", "unicodeVersion": "8.0", "digest": "76046b8157dabbe048a07fc318122456020c9c980fc1b8ab76802330e07b3b53" }, "clapper": { "category": "activity", "moji": "🎬", + "description": "clapper board", "unicodeVersion": "6.0", "digest": "8149752a0e3e8abede2d433d1afab6d217877d0c76adb1e2845a0142c0cdcbaa" }, "classical_building": { "category": "travel", "moji": "🏛", + "description": "classical building", "unicodeVersion": "7.0", "digest": "9ee0d00c43d6e22b6a3ddea67619737270cc7e9294797a19c7c60d5f92aa44fa" }, "clipboard": { "category": "objects", "moji": "📋", + "description": "clipboard", "unicodeVersion": "6.0", "digest": "bdd7f7d973c714e59d2903d401a876e6018794c7987c9ca57108c137c5edc25f" }, "clock": { "category": "objects", "moji": "🕰", + "description": "mantlepiece clock", "unicodeVersion": "7.0", "digest": "302835eab2637db799acf69b3d795571ef3432251267050db0704f2954e8b190" }, "clock1": { "category": "symbols", "moji": "🕐", + "description": "clock face one oclock", "unicodeVersion": "6.0", "digest": "1778eec07ce061c9393e5abee5ca83b24e1ce61d8a75fa2e39efcb31aa160395" }, "clock10": { "category": "symbols", "moji": "🕙", + "description": "clock face ten oclock", "unicodeVersion": "6.0", "digest": "601fc12ea5280a54c2e69dbb685f454e4165fe771756ed6f89016e29e683a24f" }, "clock1030": { "category": "symbols", "moji": "🕥", + "description": "clock face ten-thirty", "unicodeVersion": "6.0", "digest": "4fd155f08f797542d52cff4b0aa3ca9f080f37a41c301b82f90ff6d4693c890e" }, "clock11": { "category": "symbols", "moji": "🕚", + "description": "clock face eleven oclock", "unicodeVersion": "6.0", "digest": "5c79dc812e812e8a01993ea633b323d654ce3a7ea258692781a4896e4ad2017e" }, "clock1130": { "category": "symbols", "moji": "🕦", + "description": "clock face eleven-thirty", "unicodeVersion": "6.0", "digest": "41497ee2020ee5ac9aa5f9b07560f7afca7c422b04214449cfc5cea9f020f52e" }, "clock12": { "category": "symbols", "moji": "🕛", + "description": "clock face twelve oclock", "unicodeVersion": "6.0", "digest": "046bb7ffa5f5d27c2e3411ba543484d9dabb8ebf6d6e7a7e9bfb088c1813500c" }, "clock1230": { "category": "symbols", "moji": "🕧", + "description": "clock face twelve-thirty", "unicodeVersion": "6.0", "digest": "bbfe9db5a2043aaba19a7a2a0185c7efcebf1e8c9263b8233f75b53c4825f0f4" }, "clock130": { "category": "symbols", "moji": "🕜", + "description": "clock face one-thirty", "unicodeVersion": "6.0", "digest": "8662cb395ee680c2781123305c4c8ce8c0df9565c2c942668940be540cc0c094" }, "clock2": { "category": "symbols", "moji": "🕑", + "description": "clock face two oclock", "unicodeVersion": "6.0", "digest": "42f7429748b612dce7de77221cbbc710655811f7bb23e2a986c36e6d662f0ec4" }, "clock230": { "category": "symbols", "moji": "🕝", + "description": "clock face two-thirty", "unicodeVersion": "6.0", "digest": "e710b6ef14227cd240ea3e2a867c8ef45b5c060adf3cb30ba9077c2351fe6677" }, "clock3": { "category": "symbols", "moji": "🕒", + "description": "clock face three oclock", "unicodeVersion": "6.0", "digest": "7340d465b398a378211dff9ec806db579d061206fd6fc238623d070cfe0a55ce" }, "clock330": { "category": "symbols", "moji": "🕞", + "description": "clock face three-thirty", "unicodeVersion": "6.0", "digest": "7aa4a15cc8de04ed3bdeb0f8a54a7915065f2809a07054e002d89926c9766831" }, "clock4": { "category": "symbols", "moji": "🕓", + "description": "clock face four oclock", "unicodeVersion": "6.0", "digest": "36fd88e81ad488b0ec49a911a838693281573fa14736ae4a6dd1c40a4ff69bb1" }, "clock430": { "category": "symbols", "moji": "🕟", + "description": "clock face four-thirty", "unicodeVersion": "6.0", "digest": "7bd5dd71e89d95dcf18b9e8c1fe2a353a7da3b69aadb8dda80ee9bafb05da58d" }, "clock5": { "category": "symbols", "moji": "🕔", + "description": "clock face five oclock", "unicodeVersion": "6.0", "digest": "aa406409e56a0bfd8c850e44efe45fd190ffd7bf7061e934ed7928dfbdfc9eba" }, "clock530": { "category": "symbols", "moji": "🕠", + "description": "clock face five-thirty", "unicodeVersion": "6.0", "digest": "25dd3bcc53ddd98eeea498d7dbd4c306ef39dd033f15909063388a0800febf41" }, "clock6": { "category": "symbols", "moji": "🕕", + "description": "clock face six oclock", "unicodeVersion": "6.0", "digest": "0a321eaf1bc5db8436bbadac66c45ba257fc98ad4c7569ce3fc6602c824b6d7c" }, "clock630": { "category": "symbols", "moji": "🕡", + "description": "clock face six-thirty", "unicodeVersion": "6.0", "digest": "55a4c5a665fdd38a724e9357a93c55401fcd5f1b13078c25754bd70c3fc4ccec" }, "clock7": { "category": "symbols", "moji": "🕖", + "description": "clock face seven oclock", "unicodeVersion": "6.0", "digest": "6154306545716e865da0ec537ee4f22bfe6c7294502a64a2dcf425c587d0e2a2" }, "clock730": { "category": "symbols", "moji": "🕢", + "description": "clock face seven-thirty", "unicodeVersion": "6.0", "digest": "6925654de642e50f84661f94364a96c87757d73fffe766aacbf4bbd70130547b" }, "clock8": { "category": "symbols", "moji": "🕗", + "description": "clock face eight oclock", "unicodeVersion": "6.0", "digest": "9be2d189c7ea56d39fd259f84853d753c1cf33e64f8ed57f86f822d9ae23a1ee" }, "clock830": { "category": "symbols", "moji": "🕣", + "description": "clock face eight-thirty", "unicodeVersion": "6.0", "digest": "16878613c0000d2f558c88d080551f424a8bd9df1358e0f931dd25c3da68f2d9" }, "clock9": { "category": "symbols", "moji": "🕘", + "description": "clock face nine oclock", "unicodeVersion": "6.0", "digest": "1d1e7e3c9d085ffa5b7c0f3d9fd394b734f16ae3b60df09af50fe6c8d4f3c8bb" }, "clock930": { "category": "symbols", "moji": "🕤", + "description": "clock face nine-thirty", "unicodeVersion": "6.0", "digest": "9fdef6a4939315c017b165e1dbac7710fb335df8c309be3fe2a011ef7fc28d74" }, "closed_book": { "category": "objects", "moji": "📕", + "description": "closed book", "unicodeVersion": "6.0", "digest": "b18288629d201bfdfc5d66ec47df89809d00642b15732757e6a04789f36a7d9f" }, "closed_lock_with_key": { "category": "objects", "moji": "🔐", + "description": "closed lock with key", "unicodeVersion": "6.0", "digest": "e39adfe9b30973bca16472c2b7e6462b064a93b9d452aa48edd74c727641a83d" }, "closed_umbrella": { "category": "people", "moji": "🌂", + "description": "closed umbrella", "unicodeVersion": "6.0", "digest": "2cc0592c74601f7439e88c3c1ec4f05e3459608ef1ea6558c5824ed7c3889727" }, "cloud": { "category": "nature", "moji": "☁", + "description": "cloud", "unicodeVersion": "1.1", "digest": "5b3a19718dfa8a381929665afdc2284464d24020c8dd0caff4dad465a1f536ba" }, "cloud_lightning": { "category": "nature", "moji": "🌩", + "description": "cloud with lightning", "unicodeVersion": "7.0", "digest": "2b32f6d87726df2935ad81870879ccec30ce9b4fd5861d1a6317f9eca2f013d9" }, "cloud_rain": { "category": "nature", "moji": "🌧", + "description": "cloud with rain", "unicodeVersion": "7.0", "digest": "1e1e8bc59e168e1d2e72bf11f2d43cb578cbf0a5f1daf383bba5c56fb750ee71" }, "cloud_snow": { "category": "nature", "moji": "🌨", + "description": "cloud with snow", "unicodeVersion": "7.0", "digest": "2d364f859b83e684213e8eece1640208d80a8de0a49d0fc8e0e24c5a8493a3b1" }, "cloud_tornado": { "category": "nature", "moji": "🌪", + "description": "cloud with tornado", "unicodeVersion": "7.0", "digest": "7cbed2343c280ba3996082b3d0fb9d8cd57d6e62fe6c9ecb159f46b4a2e49151" }, "clown": { "category": "people", "moji": "🤡", + "description": "clown face", "unicodeVersion": "9.0", "digest": "eea95687caabc9e808514c2450ba599e5e24ef47923dbec86f5297a64438e2e5" }, "clubs": { "category": "symbols", "moji": "♣", + "description": "black club suit", "unicodeVersion": "1.1", "digest": "b8cf72ecd8568ced077b475d94788fb282bdb06d25031b5d54dd63e25effb138" }, "cocktail": { "category": "food", "moji": "🍸", + "description": "cocktail glass", "unicodeVersion": "6.0", "digest": "3792def2cde885cf32167f04904d3b0b788388e8af410c63e4cd31550feba775" }, "coffee": { "category": "food", "moji": "☕", + "description": "hot beverage", "unicodeVersion": "4.0", "digest": "0d29615a7a67d3aafa257b909bb915dc74fa8f854acb0d9a2c29e94eedf80326" }, "coffin": { "category": "objects", "moji": "⚰", + "description": "coffin", "unicodeVersion": "4.1", "digest": "78eccc1aad2a822649fba8503d4d30354bef367c4271193c40ddb692308f9db8" }, "cold_sweat": { "category": "people", "moji": "😰", + "description": "face with open mouth and cold sweat", "unicodeVersion": "6.0", "digest": "f53aab523ed3fa2224a16881d263fb5e039f163380f92feb2c63c20f9b14dcd2" }, "comet": { "category": "nature", "moji": "☄", + "description": "comet", "unicodeVersion": "1.1", "digest": "40ce93e55c6e57a88d80670b37171190bd5ffc87b7078891d8de5b15795385c5" }, "compression": { "category": "objects", "moji": "🗜", + "description": "compression", "unicodeVersion": "7.0", "digest": "c8841f7afb5345f1c31da116a7fb41d07232ea58d3f7f1a75c5890aa1a80bfd6" }, "computer": { "category": "objects", "moji": "💻", + "description": "personal computer", "unicodeVersion": "6.0", "digest": "c970ce76b5607434895b0407bdaa93140f887930781a17dd7dcf16f711451d93" }, "confetti_ball": { "category": "objects", "moji": "🎊", + "description": "confetti ball", "unicodeVersion": "6.0", "digest": "a638b16f1acdbcf69edf760161b1bd7ff1fd5426c5b1203ad9d294dcc0701f10" }, "confounded": { "category": "people", "moji": "😖", + "description": "confounded face", "unicodeVersion": "6.0", "digest": "e2ff3b4df65d00c1ca9ae0cb379f959ea2cecefb3d676d4f8c2c5f2c103da4f6" }, "confused": { "category": "people", "moji": "😕", + "description": "confused face", "unicodeVersion": "6.1", "digest": "118d7f830ec08a3ac4b798eebb77a989b8c142f2588727181be4a2548e3c4f06" }, "congratulations": { "category": "symbols", "moji": "㊗", + "description": "circled ideograph congratulation", "unicodeVersion": "1.1", "digest": "02fd1338c54fe5f9a0fd861f23c56edc1d39bcd3140b68f0f626f9e2494d2d1c" }, "construction": { "category": "travel", "moji": "🚧", + "description": "construction sign", "unicodeVersion": "6.0", "digest": "c3a0401331111b9eda1206bee5f322db80b0870547d307b10dcac1314e4078c8" }, "construction_site": { "category": "travel", "moji": "🏗", + "description": "building construction", "unicodeVersion": "7.0", "digest": "c611f0a5de10f000a0756935f226845c7292f19ff5581d1f7a7554316338bbcb" }, "construction_worker": { "category": "people", "moji": "👷", + "description": "construction worker", "unicodeVersion": "6.0", "digest": "8c094733987e7c4da8d3aa4588b530ae07042bd70cf337b1fd412a70ee8f0ed6" }, "construction_worker_tone1": { "category": "people", "moji": "👷🏻", + "description": "construction worker tone 1", "unicodeVersion": "8.0", "digest": "fcd927405fef4486105cd3aff62155467d21cebbc013924d4b52b717b566602b" }, "construction_worker_tone2": { "category": "people", "moji": "👷🏼", + "description": "construction worker tone 2", "unicodeVersion": "8.0", "digest": "d1ec773828936c703dd6e334e696dc3cf7c34c0a8ec691564a384b735cdeaaba" }, "construction_worker_tone3": { "category": "people", "moji": "👷🏽", + "description": "construction worker tone 3", "unicodeVersion": "8.0", "digest": "37c114d6879b9b32b800b0d4cf770dcbe04d1455698130ecd709a0cb9dea880b" }, "construction_worker_tone4": { "category": "people", "moji": "👷🏾", + "description": "construction worker tone 4", "unicodeVersion": "8.0", "digest": "5264996c1bedb6061a0dfdddce233d863bf308d27127ad152b63bfd983162cf7" }, "construction_worker_tone5": { "category": "people", "moji": "👷🏿", + "description": "construction worker tone 5", "unicodeVersion": "8.0", "digest": "87051aec81fd5dfd4dc44ff0411a528ee08253e9494d37efa550694e28dde6d3" }, "control_knobs": { "category": "objects", "moji": "🎛", + "description": "control knobs", "unicodeVersion": "7.0", "digest": "0d7f33ff7acc1cc3a81e6a786ff007df20da145e3070f338505dfed5100e9fcb" }, "convenience_store": { "category": "travel", "moji": "🏪", + "description": "convenience store", "unicodeVersion": "6.0", "digest": "975dcf9b8e9e3fb1e29574b41300b9d96fd64703b3c18ff52f9f1875d1cf1b52" }, "cookie": { "category": "food", "moji": "🍪", + "description": "cookie", "unicodeVersion": "6.0", "digest": "4bed3522bd50091ac5b68ca760661eb484d7f1b9c9d564d2097bd812b7f28ae4" }, "cooking": { "category": "food", "moji": "🍳", + "description": "cooking", "unicodeVersion": "6.0", "digest": "563ffd6cae381ce1e318cdacc54e70040d6a01a50d0db8aeb50edbbe413eac58" }, "cool": { "category": "symbols", "moji": "🆒", + "description": "squared cool", "unicodeVersion": "6.0", "digest": "5739a37341c782a4736adfce804e12776ae33081098a3d052d8ae9a64b4d22d1" }, "cop": { "category": "people", "moji": "👮", + "description": "police officer", "unicodeVersion": "6.0", "digest": "78996521bbe231d03ebea355226d8a1515f47cde7b2fbeca1037e7b7e5133466" }, "cop_tone1": { "category": "people", "moji": "👮🏻", + "description": "police officer tone 1", "unicodeVersion": "8.0", "digest": "8a38cd107f5f4c0b821ac43f32df5dc57facaf39fbafb98483ec00fd7df41baf" }, "cop_tone2": { "category": "people", "moji": "👮🏼", + "description": "police officer tone 2", "unicodeVersion": "8.0", "digest": "8ab8ab086f3ff82aa4bf4760c3c822846ec2696c41d21dffdac12d5afbe398b7" }, "cop_tone3": { "category": "people", "moji": "👮🏽", + "description": "police officer tone 3", "unicodeVersion": "8.0", "digest": "fce710a99fd44a7c8af3ea01b2007e46d3ff38d7a0dff1ef26d6f893ede7e6d2" }, "cop_tone4": { "category": "people", "moji": "👮🏾", + "description": "police officer tone 4", "unicodeVersion": "8.0", "digest": "3017dd73ef475379911c5e6c79bd0f9f533dbbc5057bce6a11244faa12996ba0" }, "cop_tone5": { "category": "people", "moji": "👮🏿", + "description": "police officer tone 5", "unicodeVersion": "8.0", "digest": "a3b8807b3f2a8d6ee9bcec0339355bda486e8c930f727139f5447a4b046a6307" }, "copyright": { "category": "symbols", "moji": "©", + "description": "copyright sign", "unicodeVersion": "1.1", "digest": "cc28663cdd3f8333d9bb57b511348cde4e51bda19cf0629dccb05c8fc425e079" }, "corn": { "category": "food", "moji": "🌽", + "description": "ear of maize", "unicodeVersion": "6.0", "digest": "a099a0b291fa758690e6ee6c762b9ade9a0e3350a707c52d968dfffbcc467de5" }, "couch": { "category": "objects", "moji": "🛋", + "description": "couch and lamp", "unicodeVersion": "7.0", "digest": "84cd734dbaa7f9f519438036d687e7a53217130779bc3de30258f163521b9474" }, "couple": { "category": "people", "moji": "👫", + "description": "man and woman holding hands", "unicodeVersion": "6.0", "digest": "c897ba76e24e2f43a4aa261c2754800a8473f43c7ce53f9909a6af2c4897732a" }, "couple_mm": { "category": "people", "moji": "👨❤️👨", + "description": "couple (man,man)", "unicodeVersion": "6.0", "digest": "c812471d35d46e12270653039a907d1dfa2dea0defd65596283e5b8e03cea803" }, "couple_with_heart": { "category": "people", "moji": "💑", + "description": "couple with heart", "unicodeVersion": "6.0", "digest": "420bfa81bad10365550c77a98e1c07eb00d03663fe7b610fab1aca8a0a9d201b" }, "couple_ww": { "category": "people", "moji": "👩❤️👩", + "description": "couple (woman,woman)", "unicodeVersion": "6.0", "digest": "7ac49153a612d63302299eee996308b7dcafa0a152473dab679215036fe6567e" }, "couplekiss": { "category": "people", "moji": "💏", + "description": "kiss", "unicodeVersion": "6.0", "digest": "1acfef9d375c4c1deb235babd856b0f90ad4f3194751694cb6abb44f00f29e42" }, "cow": { "category": "nature", "moji": "🐮", + "description": "cow face", "unicodeVersion": "6.0", "digest": "d71c854ff8b343ee24b8c2b9d56c7cb3fc6fa1a6dc0d7a137841b9f646e6d71b" }, "cow2": { "category": "nature", "moji": "🐄", + "description": "cow", "unicodeVersion": "6.0", "digest": "e7a5131d7dee0f3356814b0ac1ea8ff280b12a7b580181e20ddb0b7eeb7e7339" }, "cowboy": { "category": "people", "moji": "🤠", + "description": "face with cowboy hat", "unicodeVersion": "9.0", "digest": "1aabf23f6b95a9b772fdb8eb45b8ec93584a5357f9131c6eabc9d1b83fe67e89" }, "crab": { "category": "nature", "moji": "🦀", + "description": "crab", "unicodeVersion": "8.0", "digest": "e6be16699fdb5d87f42f28f6cc141a44b7ffd834ecdd536813c4b5b86d3fc4a5" }, "crayon": { "category": "objects", "moji": "🖍", + "description": "lower left crayon", "unicodeVersion": "7.0", "digest": "b180d6afa4777861222a4228164ce284230fe90c589f52ffa9351bac777e901a" }, "credit_card": { "category": "objects", "moji": "💳", + "description": "credit card", "unicodeVersion": "6.0", "digest": "808cd120fd3738eb2be1f6c6c029d98387b0e03fca7d1451e8fbf9c5ab3f643f" }, "crescent_moon": { "category": "nature", "moji": "🌙", + "description": "crescent moon", "unicodeVersion": "6.0", "digest": "042e7e01e6e88b97a763b7cc41e2a2b3fe68a649bacf4a090cd28fc653baf640" }, "cricket": { "category": "activity", "moji": "🏏", + "description": "cricket bat and ball", "unicodeVersion": "8.0", "digest": "4c4559d0b4efe24cc248fa57f413541307992e519d0cb9fb8828637ac2f4cc16" }, "crocodile": { "category": "nature", "moji": "🐊", + "description": "crocodile", "unicodeVersion": "6.0", "digest": "59cb4164c50b6bc9ae311ce6f7610467c1aaafa848b5fff7614f064715f91992" }, "croissant": { "category": "food", "moji": "🥐", + "description": "croissant", "unicodeVersion": "9.0", "digest": "b751e287157a1e276617a841a5b5f7f1208ca226cfd8fa947f144390b65a5e16" }, "cross": { "category": "symbols", "moji": "✝", + "description": "latin cross", "unicodeVersion": "1.1", "digest": "a6b07c838fb75ef2ebefa2df6005e8d784753239ec03c37695a13e3b1954d653" }, "crossed_flags": { "category": "objects", "moji": "🎌", + "description": "crossed flags", "unicodeVersion": "6.0", "digest": "2841c671075e6f1a79c61c2d716423159fb0bc0786e3fb0049697766533bf262" }, "crossed_swords": { "category": "objects", "moji": "⚔", + "description": "crossed swords", "unicodeVersion": "4.1", "digest": "3771a5b26b514236521ce44e15f7730fa9148c6a782b9b600ab870a1f7de6f9f" }, "crown": { "category": "people", "moji": "👑", + "description": "crown", "unicodeVersion": "6.0", "digest": "6741e58d8f823194e0a3484ac1563e20d9e0b44c1bc46d82444dfffa092cdfc7" }, "cruise_ship": { "category": "travel", "moji": "🛳", + "description": "passenger ship", "unicodeVersion": "7.0", "digest": "2b7b62db5d118a632673564099e3405ea6d61ea9b8e123b5a2aaf011bb2a54a4" }, "cry": { "category": "people", "moji": "😢", + "description": "crying face", "unicodeVersion": "6.0", "digest": "fc3307ec4fe75539770c1123a0e8e721d9e021009a502655132f68d7cc453816" }, "crying_cat_face": { "category": "people", "moji": "😿", + "description": "crying cat face", "unicodeVersion": "6.0", "digest": "4942c24935c22babdcb8af41d2c0a7588356b6b674bc238902e2f10ad03e2c5b" }, "crystal_ball": { "category": "objects", "moji": "🔮", + "description": "crystal ball", "unicodeVersion": "6.0", "digest": "05f73b30b1e5b0fc66fb5dc6caddd2d547ee7b9d2f97513dc908ba1a2e352e30" }, "cucumber": { "category": "food", "moji": "🥒", + "description": "cucumber", "unicodeVersion": "9.0", "digest": "d1196e23f2f155ef5c1330f8497f40957a7357cb177127f457c5c471f0a23727" }, "cupid": { "category": "symbols", "moji": "💘", + "description": "heart with arrow", "unicodeVersion": "6.0", "digest": "246e71f44c6ebc2e4f887e25438e4f894e8cc92e06069e711b893ff391abb658" }, "curly_loop": { "category": "symbols", "moji": "➰", + "description": "curly loop", "unicodeVersion": "6.0", "digest": "9e4eb98d6597888f91208080c6a79824adb432ea34f46c85da26cb630bd1cc73" }, "currency_exchange": { "category": "symbols", "moji": "💱", + "description": "currency exchange", "unicodeVersion": "6.0", "digest": "b85377265b9876888969aa42b65bba0be523a370175baf226f20131e535af554" }, "curry": { "category": "food", "moji": "🍛", + "description": "curry and rice", "unicodeVersion": "6.0", "digest": "a01c0a713662817720b485f7739f57e61afc025f5c43792f4de961c94f92f31e" }, "custard": { "category": "food", "moji": "🍮", + "description": "custard", "unicodeVersion": "6.0", "digest": "85c2b9ac904134a6c3587eb0a0806f2ab4282c5ed5c79d41734f3203998f757e" }, "customs": { "category": "symbols", "moji": "🛃", + "description": "customs", "unicodeVersion": "6.0", "digest": "eb2546e1e617d4c1a1f614318af5e5dacf3e8d9479ffa08108977defa83ded32" }, "cyclone": { "category": "symbols", "moji": "🌀", + "description": "cyclone", "unicodeVersion": "6.0", "digest": "7a0f8564d76adf2d0ed272f56dc0d01fb7b557852e0ca797e73f5472b8630bf3" }, "dagger": { "category": "objects", "moji": "🗡", + "description": "dagger knife", "unicodeVersion": "7.0", "digest": "35a179168198d03295e626cc27d3b92d30a732c55a2ca75d7a11a0fbed414772" }, "dancer": { "category": "people", "moji": "💃", + "description": "dancer", "unicodeVersion": "6.0", "digest": "66ffa86827e85acae4aa870c0859fe3a9dad03d21ff4bc800b61c95c902a8a90" }, "dancer_tone1": { "category": "people", "moji": "💃🏻", + "description": "dancer tone 1", "unicodeVersion": "8.0", "digest": "bdbee740addc890e369d3469a3585eb0d1e4fbc7e04dd6f6aca762d8aeee6a8c" }, "dancer_tone2": { "category": "people", "moji": "💃🏼", + "description": "dancer tone 2", "unicodeVersion": "8.0", "digest": "9f7b4c627241eaa2def9717a5286a423f0b9c1b044dd9ea4442a76f1858d14a4" }, "dancer_tone3": { "category": "people", "moji": "💃🏽", + "description": "dancer tone 3", "unicodeVersion": "8.0", "digest": "a6bd49a377ce6c2004bf126b6f66d0b21d8c14103c2add7b10f12ed9e1c2d302" }, "dancer_tone4": { "category": "people", "moji": "💃🏾", + "description": "dancer tone 4", "unicodeVersion": "8.0", "digest": "4ec2a7629c01b0e9006b5cda4deae3bf297ce3b71d18063f93eeb5c14be19a1a" }, "dancer_tone5": { "category": "people", "moji": "💃🏿", + "description": "dancer tone 5", "unicodeVersion": "8.0", "digest": "2b48e3a6b366c6f55f73b816e6fb03c39e9890f586f7e9c9043cf0c013d9cdd5" }, "dancers": { "category": "people", "moji": "👯", + "description": "woman with bunny ears", "unicodeVersion": "6.0", "digest": "12be66ed19d232bb387270f40bece68bd0cb2342b318f6c9bb8b49c64ff7d0ad" }, "dango": { "category": "food", "moji": "🍡", + "description": "dango", "unicodeVersion": "6.0", "digest": "34e8cd153c50f2d725abe8934c35c96a3ab533f0cc5fbb1e1474eafad1dc1fc2" }, "dark_sunglasses": { "category": "people", "moji": "🕶", + "description": "dark sunglasses", "unicodeVersion": "7.0", "digest": "d0a735ad5bf0ece00af2a21abf950b89292ebd8ca6e28b1dbb1368252fb44afe" }, "dart": { "category": "activity", "moji": "🎯", + "description": "direct hit", "unicodeVersion": "6.0", "digest": "998642f06a875905e0a6bf30963c025baff1cf55b8e76884b9119f2d71188b0c" }, "dash": { "category": "nature", "moji": "💨", + "description": "dash symbol", "unicodeVersion": "6.0", "digest": "f7aae7d3887c67d76f3329c2dc9e6807dc580a4b07ab35599c7805e41823a345" }, "date": { "category": "objects", "moji": "📅", + "description": "calendar", "unicodeVersion": "6.0", "digest": "d0b695e4a7cfbbe71b4fbebf345b66ca98f0cf1c751362928e54c23ca78d4c7b" }, "deciduous_tree": { "category": "nature", "moji": "🌳", + "description": "deciduous tree", "unicodeVersion": "6.0", "digest": "3c70f1a77f2754f41c830e88d43b7d53c14311d64626ded164aa9ac7d2695790" }, "deer": { "category": "nature", "moji": "🦌", + "description": "deer", "unicodeVersion": "9.0", "digest": "7f4302ca68fd121ee73be48d0a0a0fb9e7e2741071a491ad2b7b0eab9f11ad25" }, "department_store": { "category": "travel", "moji": "🏬", + "description": "department store", "unicodeVersion": "6.0", "digest": "4be910d2efe74d8ce2c1f41d7753c8873579faca83fcf779a4887d8ab9e5923b" }, "desert": { "category": "travel", "moji": "🏜", + "description": "desert", "unicodeVersion": "7.0", "digest": "d4b1a11c5130debe042df6cc2b3389f15c68a5cb32dc1b3a82b78f733d0c9e4e" }, "desktop": { "category": "objects", "moji": "🖥", + "description": "desktop computer", "unicodeVersion": "7.0", "digest": "cde5bfb6c71bb7d663808a3561b24cb5b5560f95f510b40f81250cac1b21933e" }, "diamond_shape_with_a_dot_inside": { "category": "symbols", "moji": "💠", + "description": "diamond shape with a dot inside", "unicodeVersion": "6.0", "digest": "e91323577ab89e95b0fa0b9272ea0c797b76908f24d36992630e9325273a4ce3" }, "diamonds": { "category": "symbols", "moji": "♦", + "description": "black diamond suit", "unicodeVersion": "1.1", "digest": "bf3d9a020afe8aa226db73590bc193a9c2c3e6e642edd2445c5960c3e67cf153" }, "disappointed": { "category": "people", "moji": "😞", + "description": "disappointed face", "unicodeVersion": "6.0", "digest": "c0f406c6beea0fd1328adefc097d04aa16b72f7a5afa0867967d8ea25d72db17" }, "disappointed_relieved": { "category": "people", "moji": "😥", + "description": "disappointed but relieved face", "unicodeVersion": "6.0", "digest": "c826f5dd4f2f7e5289d720851d4826ab8284d915606c1b152ab229b7fadbba14" }, "dividers": { "category": "objects", "moji": "🗂", + "description": "card index dividers", "unicodeVersion": "7.0", "digest": "4b2c653b18cf0fa31f1f0ac94a6fbd214ea0d1b0a90a450ab6e169906fc5764f" }, "dizzy": { "category": "nature", "moji": "💫", + "description": "dizzy symbol", "unicodeVersion": "6.0", "digest": "d577545c2de42389695447c6ebbfef895f30f0fda84eef45684f9bf4a9c27ff1" }, "dizzy_face": { "category": "people", "moji": "😵", + "description": "dizzy face", "unicodeVersion": "6.0", "digest": "7b3aeaffb4e15ccf633b91dda4a44847a1eb28d78ce58b4d171b20a771bde414" }, "do_not_litter": { "category": "symbols", "moji": "🚯", + "description": "do not litter symbol", "unicodeVersion": "6.0", "digest": "98b07fbbcdb438d1b8a755869fa2de8e180a77fce359ec830eb46d38ec3e67cb" }, "dog": { "category": "nature", "moji": "🐶", + "description": "dog face", "unicodeVersion": "6.0", "digest": "3b31ce067b13e463284ce85536512cb1f8cd8b52fe73659f69971d0d6c1dfc11" }, "dog2": { "category": "nature", "moji": "🐕", + "description": "dog", "unicodeVersion": "6.0", "digest": "0a8901bce5ed994533ff84299b2a1364de28d872c9f9510d3426a83e8a9d2e34" }, "dollar": { "category": "objects", "moji": "💵", + "description": "banknote with dollar sign", "unicodeVersion": "6.0", "digest": "52438e38867aedc021740bb41f9ba336e75a50faa148419412a01d75d8c93155" }, "dolls": { "category": "objects", "moji": "🎎", + "description": "japanese dolls", "unicodeVersion": "6.0", "digest": "a687184e9a0915deef44bb3cacfb19d3f3f19cf2c110f1da90191dd567333c57" }, "dolphin": { "category": "nature", "moji": "🐬", + "description": "dolphin", "unicodeVersion": "6.0", "digest": "0b7ee08f4236232ca533ed3a3023d28020d36f178efaec5ce8b0e13a84778512" }, "door": { "category": "objects", "moji": "🚪", + "description": "door", "unicodeVersion": "6.0", "digest": "984a9ca88852ebdb539e0c385d9c6ffe5010e9189bc372a3d00f5c8d44c8e6f5" }, "doughnut": { "category": "food", "moji": "🍩", + "description": "doughnut", "unicodeVersion": "6.0", "digest": "27634587e6a53807baa32157bb06b0e115c8ad8aefebba7ebb0b65a084170e3a" }, "dove": { "category": "nature", "moji": "🕊", + "description": "dove of peace", "unicodeVersion": "7.0", "digest": "7c665f8594ffa53e72b01647e9d27360fb87d52d02fe9f20fc5fda08f9797dc3" }, "dragon": { "category": "nature", "moji": "🐉", + "description": "dragon", "unicodeVersion": "6.0", "digest": "2abcb3d945d848e34ffc76203b29ef26df7458856166fffd155611f7bbe72652" }, "dragon_face": { "category": "nature", "moji": "🐲", + "description": "dragon face", "unicodeVersion": "6.0", "digest": "0030548931b931e3b51f26cf660394aee36499e688ba83ce9cfccb635dcd4d54" }, "dress": { "category": "people", "moji": "👗", + "description": "dress", "unicodeVersion": "6.0", "digest": "96ceba928fb356f7c0ae99bf22552321f08a65d5f1c0340ab89641219ad366ad" }, "dromedary_camel": { "category": "nature", "moji": "🐪", + "description": "dromedary camel", "unicodeVersion": "6.0", "digest": "e06ef69c29f0fb12481727c0b4124e700572d3d7955e173279320f43f286518d" }, "drooling_face": { "category": "people", "moji": "🤤", + "description": "drooling face", "unicodeVersion": "9.0", "digest": "5203cb05cd266d7a7c929ab40364ad68571d380d9c7ff93a8d6d55261abaa1ba" }, "droplet": { "category": "nature", "moji": "💧", + "description": "droplet", "unicodeVersion": "6.0", "digest": "6475b4a4460a672c436a68f282ac97fb31e2934db4b80620063ee816159aa7c3" }, "drum": { "category": "activity", "moji": "🥁", + "description": "drum with drumsticks", "unicodeVersion": "9.0", "digest": "0d0639980b1a5dcbf1c3e7ef47263fb6543b871242c58452a8c2f642525d9dd8" }, "duck": { "category": "nature", "moji": "🦆", + "description": "duck", "unicodeVersion": "9.0", "digest": "8f8373798a7727368b32328e7a9a349727a949e7391ddd243b6456141a4f7e94" }, "dvd": { "category": "objects", "moji": "📀", + "description": "dvd", "unicodeVersion": "6.0", "digest": "3b7903285d91277181c26fdc9df857761bbac509d352e320c2519ea3b132704f" }, "e-mail": { "category": "objects", "moji": "📧", + "description": "e-mail symbol", "unicodeVersion": "6.0", "digest": "39b5a57a2376e4a1137e381be02a1775bd580e0371438f5297a401ea634f1830" }, "eagle": { "category": "nature", "moji": "🦅", + "description": "eagle", "unicodeVersion": "9.0", "digest": "b44fd4f61b83c5114358a272343ac9b0eabbc70847f739bbdbf8aae3ade5bc1d" }, "ear": { "category": "people", "moji": "👂", + "description": "ear", "unicodeVersion": "6.0", "digest": "4fdeb5a46e69311ecfd09c5b45c9018c24b625e28475cca8fa516b086ef952f8" }, "ear_of_rice": { "category": "nature", "moji": "🌾", + "description": "ear of rice", "unicodeVersion": "6.0", "digest": "2997c340c2b333d6ba9b73f94ff1a1881735fe0cc4f0c72d7719b305499fc425" }, "ear_tone1": { "category": "people", "moji": "👂🏻", + "description": "ear tone 1", "unicodeVersion": "8.0", "digest": "5ca759b8569a377a4e63e30d94b585b9f76d15348a8a0c1ba19fdc522790615e" }, "ear_tone2": { "category": "people", "moji": "👂🏼", + "description": "ear tone 2", "unicodeVersion": "8.0", "digest": "12aafb3ef2cfcdc892b2877c2e24920620f0f77f850e12afbfe55eadce9e37df" }, "ear_tone3": { "category": "people", "moji": "👂🏽", + "description": "ear tone 3", "unicodeVersion": "8.0", "digest": "f4d28d9f72cf116ac92d80061eb84c918d6523bf53b2ad526f5457aba487d527" }, "ear_tone4": { "category": "people", "moji": "👂🏾", + "description": "ear tone 4", "unicodeVersion": "8.0", "digest": "eaa9453670f7e3adc6ec6934ee70efc9bf60fe6c99c5804b7ba9e3804aec65de" }, "ear_tone5": { "category": "people", "moji": "👂🏿", + "description": "ear tone 5", "unicodeVersion": "8.0", "digest": "54bd0782419489556b80e9e0d15b05df74757aa4e04ba565f45c20d3dd60e3f1" }, "earth_africa": { "category": "nature", "moji": "🌍", + "description": "earth globe europe-africa", "unicodeVersion": "6.0", "digest": "c691a6f591f5a07b268fd64efe113e81cec8d5963ad83ced2537422343ff7ecf" }, "earth_americas": { "category": "nature", "moji": "🌎", + "description": "earth globe americas", "unicodeVersion": "6.0", "digest": "a9c60cf8341ff59a9cc1a715b7144af734fcd28915a8e003a31ebf2abf9aedb1" }, "earth_asia": { "category": "nature", "moji": "🌏", + "description": "earth globe asia-australia", "unicodeVersion": "6.0", "digest": "ee2beb61fb8c87279161c5a8c4ad17bb71ce790123f8fa33522941d027e060a5" }, "egg": { "category": "food", "moji": "🥚", + "description": "egg", "unicodeVersion": "9.0", "digest": "72b9c841af784e7cbccbbe48ba833df5cecdd284397c199cab079872e879d92f" }, "eggplant": { "category": "food", "moji": "🍆", + "description": "aubergine", "unicodeVersion": "6.0", "digest": "ec0a460e0cf0e615f51279677594a899672e1b4ecd9396e17a8cfa2a3efe5238" }, "eight": { "category": "symbols", "moji": "8️⃣", + "description": "keycap digit eight", "unicodeVersion": "3.0", "digest": "57ff905033a32747690adba6486d12b09eb4d45de556f4e1ab6fb04e1fb861a8" }, "eight_pointed_black_star": { "category": "symbols", "moji": "✴", + "description": "eight pointed black star", "unicodeVersion": "1.1", "digest": "7bf11f6e28591e3d0625296aaabf4ecb75c982e425abf3049339e93494acc17e" }, "eight_spoked_asterisk": { "category": "symbols", "moji": "✳", + "description": "eight spoked asterisk", "unicodeVersion": "1.1", "digest": "bb0758e7cc0e357285937671a91489bd32ce9d248eecdcc9c275a53a66325b26" }, "eject": { "category": "symbols", "moji": "⏏", + "description": "eject symbol", "unicodeVersion": "4.0", "digest": "eeb0cd23ead0c965e307de517a6805265f0c780c3e454e64bc4c1425dfe7548e" }, "electric_plug": { "category": "objects", "moji": "🔌", + "description": "electric plug", "unicodeVersion": "6.0", "digest": "b10ce87af86fa4f4022572ceb5ecd73bea867347a86832a7ea248364b0aad8d0" }, "elephant": { "category": "nature", "moji": "🐘", + "description": "elephant", "unicodeVersion": "6.0", "digest": "b7750f4b013fbd28ac5330e1694ef4d3b4a9c6fc7b807879db0c24b035a16c29" }, "end": { "category": "symbols", "moji": "🔚", + "description": "end with leftwards arrow above", "unicodeVersion": "6.0", "digest": "dd93aee6986eb637a8b58f234da47568b88525599f73246e322af030351997a2" }, "envelope": { "category": "objects", "moji": "✉", + "description": "envelope", "unicodeVersion": "1.1", "digest": "f5a512022a2f5280f372ff39c22cbda815f698710ca66f8f8c4d08418f98ca78" }, "envelope_with_arrow": { "category": "objects", "moji": "📩", + "description": "envelope with downwards arrow above", "unicodeVersion": "6.0", "digest": "f8643212e6a94f58ccf2bcedc54c5fda8ebeab274f4a8803f253de5f50ddb1d6" }, "euro": { "category": "objects", "moji": "💶", + "description": "banknote with euro sign", "unicodeVersion": "6.0", "digest": "3af3e223e8f26468a94f6f5c17198432656e8d20b3bab31566c2b5a86e717df4" }, "european_castle": { "category": "travel", "moji": "🏰", + "description": "european castle", "unicodeVersion": "6.0", "digest": "21082d0be7e3b2794e59ff0170da0cfe42a9b734cf02704603e3b52ff48202ba" }, "european_post_office": { "category": "travel", "moji": "🏤", + "description": "european post office", "unicodeVersion": "6.0", "digest": "02b4c7602939f0cb9cb2b4e05996bcdb6bd93cf8025c2ea02db8cbe13ca397d0" }, "evergreen_tree": { "category": "nature", "moji": "🌲", + "description": "evergreen tree", "unicodeVersion": "6.0", "digest": "74b226098e66c0a94a92e0f22b9d631736e12dca72c34182c9d0ba56aa593172" }, "exclamation": { "category": "symbols", "moji": "❗", + "description": "heavy exclamation mark symbol", "unicodeVersion": "5.2", "digest": "45b87ae4593656d7da49ff5645fb6a2a18d582553295358da9f09f1ae8272445" }, "expressionless": { "category": "people", "moji": "😑", + "description": "expressionless face", "unicodeVersion": "6.1", "digest": "34e2a1c8121f4f0bc4ce33d226d8cc1a4ebf5260746df2b23e29eef24ee9372e" }, "eye": { "category": "people", "moji": "👁", + "description": "eye", "unicodeVersion": "7.0", "digest": "79ecff79c2edee630e72725b54e67ee2e96d24ca03fef2954a56a09c0a2227f8" }, "eye_in_speech_bubble": { "category": "symbols", "moji": "👁🗨", + "description": "eye in speech bubble", "unicodeVersion": "7.0", "digest": "c0050c026c2a3060723cab2df2603c1c7da7ed81faedb9ebe16cd89721928a55" }, "eyeglasses": { "category": "people", "moji": "👓", + "description": "eyeglasses", "unicodeVersion": "6.0", "digest": "d4a9585d6c43ef514a97c45c64607162e775a45544821f1470c6f8f25b93ab81" }, "eyes": { "category": "people", "moji": "👀", + "description": "eyes", "unicodeVersion": "6.0", "digest": "1d5cae0b9b2e51e1de54295685d7f0c72ee794e2e6335a95b1d056c7e77260e8" }, "face_palm": { "category": "people", "moji": "🤦", + "description": "face palm", "unicodeVersion": "9.0", "digest": "4ec873048b34b1bb34430724cf28e4bee6c0a9eee88ce39b9d1565047dc92420" }, "face_palm_tone1": { "category": "people", "moji": "🤦🏻", + "description": "face palm tone 1", "unicodeVersion": "9.0", "digest": "e93ef92b4c01dbea6c400e708e23dd36da92ccfbf5eb4f177b3b20c3a46bdc19" }, "face_palm_tone2": { "category": "people", "moji": "🤦🏼", + "description": "face palm tone 2", "unicodeVersion": "9.0", "digest": "22c8bf9fd9fa2ed9dca7a6397ed00ba6cfe9aeef2b0fb7b516ee4dda0df050ea" }, "face_palm_tone3": { "category": "people", "moji": "🤦🏽", + "description": "face palm tone 3", "unicodeVersion": "9.0", "digest": "c0b8bb9d2423e6787b6bdf1ca5a13f52853e4f48a9a1af0f2d4af1364fff022e" }, "face_palm_tone4": { "category": "people", "moji": "🤦🏾", + "description": "face palm tone 4", "unicodeVersion": "9.0", "digest": "f522ab186adcbb4549ea2c03500cdd7a86add548e43ebf7a54d58cc24deea072" }, "face_palm_tone5": { "category": "people", "moji": "🤦🏿", + "description": "face palm tone 5", "unicodeVersion": "9.0", "digest": "363507ae7178b5ec583635f47bcab10c897346f48b85d8759b1004c32cd8ad65" }, "factory": { "category": "travel", "moji": "🏭", + "description": "factory", "unicodeVersion": "6.0", "digest": "c7aeb61ed8b0ac5c91d5197c73f1e2bb801921c22a76bb82c7659d990680dcb0" }, "fallen_leaf": { "category": "nature", "moji": "🍂", + "description": "fallen leaf", "unicodeVersion": "6.0", "digest": "81fce04231d48db0e55f3697f930e9a7e3306bed5e35f1234e98c40a24ac5626" }, "family": { "category": "people", "moji": "👪", + "description": "family", "unicodeVersion": "6.0", "digest": "06f2ce63768ffe43b3d9b2a9660b34d043f37b3c91610dd62343ba21df8ecbe5" }, "family_mmb": { "category": "people", "moji": "👨👨👦", + "description": "family (man,man,boy)", "unicodeVersion": "6.0", "digest": "41a18405be796699a7eb7c36ab6f7d898e322749997f45387377acf5bb16a50f" }, "family_mmbb": { "category": "people", "moji": "👨👨👦👦", + "description": "family (man,man,boy,boy)", "unicodeVersion": "6.0", "digest": "87255d1d18c6971c8c083c818e598424c1bd717eed892478b7e9516639dbfb45" }, "family_mmg": { "category": "people", "moji": "👨👨👧", + "description": "family (man,man,girl)", "unicodeVersion": "6.0", "digest": "a132b1b8f10b318d8e23aee15dab4caa14528aeb3c89966d4bcc25fb54af72ad" }, "family_mmgb": { "category": "people", "moji": "👨👨👧👦", + "description": "family (man,man,girl,boy)", "unicodeVersion": "6.0", "digest": "eb2bc1966df406aaf38ce5a58db9324162799cdacf31f74f40e6384807a8efc2" }, "family_mmgg": { "category": "people", "moji": "👨👨👧👧", + "description": "family (man,man,girl,girl)", "unicodeVersion": "6.0", "digest": "24f3d60f98fbd6b687f7cacfb629390b90509a754036e5439ae5294759c0606b" }, "family_mwbb": { "category": "people", "moji": "👨👩👦👦", + "description": "family (man,woman,boy,boy)", "unicodeVersion": "6.0", "digest": "2f77692bcb9275c4df501b64a18401dcaf8c68b21f26fbdad59b1feab0c98fd1" }, "family_mwg": { "category": "people", "moji": "👨👩👧", + "description": "family (man,woman,girl)", "unicodeVersion": "6.0", "digest": "1a976d13127665d9386cebfdb24e5572dc499bda484c0ee05585886edc616130" }, "family_mwgb": { "category": "people", "moji": "👨👩👧👦", + "description": "family (man,woman,girl,boy)", "unicodeVersion": "6.0", "digest": "960ec2cbac13ef208e73644cd36711b83e6c070c36950f834f3669812839b7f8" }, "family_mwgg": { "category": "people", "moji": "👨👩👧👧", + "description": "family (man,woman,girl,girl)", "unicodeVersion": "6.0", "digest": "8353b03dfa5c24aba75a0abdfdac01603f593819d54b4c7f2f88aafb31da0c6a" }, "family_wwb": { "category": "people", "moji": "👩👩👦", + "description": "family (woman,woman,boy)", "unicodeVersion": "6.0", "digest": "07a5dd397718c553573689f6512f386729c13a12d5dc78be47c06405769cd98a" }, "family_wwbb": { "category": "people", "moji": "👩👩👦👦", + "description": "family (woman,woman,boy,boy)", "unicodeVersion": "6.0", "digest": "b627f460f1da0d47b0b662402940b2b77c9538d380d05436dfca4b456c50c939" }, "family_wwg": { "category": "people", "moji": "👩👩👧", + "description": "family (woman,woman,girl)", "unicodeVersion": "6.0", "digest": "2d6f373bed53f1028f0fbe9caf036465a351f37b9e00fca7d722cc5a1984f251" }, "family_wwgb": { "category": "people", "moji": "👩👩👧👦", + "description": "family (woman,woman,girl,boy)", "unicodeVersion": "6.0", "digest": "72be5c85e1621f73d6794edd6e428febdb366b9e4c816f7829897fd1ab34642b" }, "family_wwgg": { "category": "people", "moji": "👩👩👧👧", + "description": "family (woman,woman,girl,girl)", "unicodeVersion": "6.0", "digest": "c39e0916069460d2d9741bddf58e76f5d6a09254cba0eeb262345adf8630bc32" }, "fast_forward": { "category": "symbols", "moji": "⏩", + "description": "black right-pointing double triangle", "unicodeVersion": "6.0", "digest": "e7d2d8085cfd406c2b096e8dd147dd3722290a5727b1f7df185989526a2335ec" }, "fax": { "category": "objects", "moji": "📠", + "description": "fax machine", "unicodeVersion": "6.0", "digest": "ff85ffa440c5379c9b138ebe2d7912d6098da3b37a051b80442d5557b7f993b0" }, "fearful": { "category": "people", "moji": "😨", + "description": "fearful face", "unicodeVersion": "6.0", "digest": "b72bdf7d075d5c4e38bbd8512fb45fda2e85c9c8732a47e67575ae9f2ed4c5df" }, "feet": { "category": "nature", "moji": "🐾", + "description": "paw prints", "unicodeVersion": "6.0", "digest": "45aca538d3a9831a0c7de491e5656c17705c07b8f4ac8e85254656b608976016" }, "fencer": { "category": "activity", "moji": "🤺", + "description": "fencer", "unicodeVersion": "9.0", "digest": "5db00fa456af9f6c7cb88d300579dd63e426bcb97ad25486b664aff25c688e21" }, "ferris_wheel": { "category": "travel", "moji": "🎡", + "description": "ferris wheel", "unicodeVersion": "6.0", "digest": "24b4551b7b79a2a5fd73de61542f2b444f896a52030c5f29791c8fcfcc28b95c" }, "ferry": { "category": "travel", "moji": "⛴", + "description": "ferry", "unicodeVersion": "5.2", "digest": "5002a72af2e3c4cef9a36ad5987aeed7d99f96bfd13e56f78957315ec7e749a3" }, "field_hockey": { "category": "activity", "moji": "🏑", + "description": "field hockey stick and ball", "unicodeVersion": "8.0", "digest": "4ee091d96161ba719ab8fd6f2b03f96d902a6f22cffe0563b930618bb8ac2b67" }, "file_cabinet": { "category": "objects", "moji": "🗄", + "description": "file cabinet", "unicodeVersion": "7.0", "digest": "92914147bf93e6d64271ff99d217a18a9850a367d08a5f9f458ecf9311a5bbe9" }, "file_folder": { "category": "objects", "moji": "📁", + "description": "file folder", "unicodeVersion": "6.0", "digest": "62a42a929267cfbfdb795ead381c9657c343458bc5fca95ea8a0ab892c61d4f6" }, "film_frames": { "category": "objects", "moji": "🎞", + "description": "film frames", "unicodeVersion": "7.0", "digest": "4da212148cadb9c4ea91e60d2d8316e38cea99ef4f14afc023711dd7c54ade5a" }, "fingers_crossed": { "category": "people", "moji": "🤞", + "description": "hand with first and index finger crossed", "unicodeVersion": "9.0", "digest": "a5c797ead191b9712e185083266b455cdf09f6a34c10f8c51aa145e6073427e1" }, "fingers_crossed_tone1": { "category": "people", "moji": "🤞🏻", + "description": "hand with index and middle fingers crossed tone 1", "unicodeVersion": "9.0", "digest": "db56d47bf887f2d8459a3aaba23f15c0087234ae5a54125052e7046e034a4988" }, "fingers_crossed_tone2": { "category": "people", "moji": "🤞🏼", + "description": "hand with index and middle fingers crossed tone 2", "unicodeVersion": "9.0", "digest": "19f1bcca3991db7ed2037278c0baab6cd7f12aeaf2e0074de402c4d9e45c1899" }, "fingers_crossed_tone3": { "category": "people", "moji": "🤞🏽", + "description": "hand with index and middle fingers crossed tone 3", "unicodeVersion": "9.0", "digest": "895a3314f6a310f31f7e728bcca20ff834fbfac62ce00e27e3ea5ad0dfc1ba35" }, "fingers_crossed_tone4": { "category": "people", "moji": "🤞🏾", + "description": "hand with index and middle fingers crossed tone 4", "unicodeVersion": "9.0", "digest": "fcb5c4de2001d23a5df1b8702624d134b7f94e93e2dcc8adf6c1033c77722b0e" }, "fingers_crossed_tone5": { "category": "people", "moji": "🤞🏿", + "description": "hand with index and middle fingers crossed tone 5", "unicodeVersion": "9.0", "digest": "50132c78d530b048c21be4e788b446872a79b3b3a91009db12f4021c44c8469d" }, "fire": { "category": "nature", "moji": "🔥", + "description": "fire", "unicodeVersion": "6.0", "digest": "b3e67c913903d900f5e50e7e7e4d7e9370bb6ceedfbee548be39e4c9e4b69416" }, "fire_engine": { "category": "travel", "moji": "🚒", + "description": "fire engine", "unicodeVersion": "6.0", "digest": "c3a518f27d625e3b62dffa227eb82764bf0a147f10ec0e7f4f43f3f96751af20" }, "fireworks": { "category": "travel", "moji": "🎆", + "description": "fireworks", "unicodeVersion": "6.0", "digest": "b62ae08a00c0cc6eba8f9666c8fd9946ce57c3cfc01fe99542a8690a4a566a65" }, "first_place": { "category": "activity", "moji": "🥇", + "description": "first place medal", "unicodeVersion": "9.0", "digest": "e3de5d9f14f05544dbee5965cc2baa20e7b417a488c8a18598979038860fd901" }, "first_quarter_moon": { "category": "nature", "moji": "🌓", + "description": "first quarter moon symbol", "unicodeVersion": "6.0", "digest": "a207ce93084448622a4a5c49c85c566a9fda6be7337c86a013eeb713fe47fd29" }, "first_quarter_moon_with_face": { "category": "nature", "moji": "🌛", + "description": "first quarter moon with face", "unicodeVersion": "6.0", "digest": "1d1f54a5075f2311bcc017c44898b9d8c58edc13b298d58c238fff9ab8ee2ef3" }, "fish": { "category": "nature", "moji": "🐟", + "description": "fish", "unicodeVersion": "6.0", "digest": "8f62f08fbeaf39694c19816b5c7d4f292017fe5bf9f8dd7e40f1630f5f83b28b" }, "fish_cake": { "category": "food", "moji": "🍥", + "description": "fish cake with swirl design", "unicodeVersion": "6.0", "digest": "5a6ca2100c8830927b22afa6f1d2fc821f5692cd23507fe5a776f6e085cbbfb2" }, "fishing_pole_and_fish": { "category": "activity", "moji": "🎣", + "description": "fishing pole and fish", "unicodeVersion": "6.0", "digest": "f8fb84eccceec88321b0a2a46f732ecfc378f787c19c27ac1327735f1ca9a48b" }, "fist": { "category": "people", "moji": "✊", + "description": "raised fist", "unicodeVersion": "6.0", "digest": "557f96d85615b8d78436bc67266115bfc8556c97c14f7909dfda1cf134e8344f" }, "fist_tone1": { "category": "people", "moji": "✊🏻", + "description": "raised fist tone 1", "unicodeVersion": "8.0", "digest": "6c1b946f9e01abc39b5085e24e8b6077fc0e34188e8daa30c6a3adddd387413e" }, "fist_tone2": { "category": "people", "moji": "✊🏼", + "description": "raised fist tone 2", "unicodeVersion": "8.0", "digest": "e9b9e1ec638dca4d5e1519bca7338f58cce2f2a282ee4c3581e8643166fc415f" }, "fist_tone3": { "category": "people", "moji": "✊🏽", + "description": "raised fist tone 3", "unicodeVersion": "8.0", "digest": "8c14d24055c143960b3d2a27fe23c55d2d3ac5f84f87e4e876616235e8698c7f" }, "fist_tone4": { "category": "people", "moji": "✊🏾", + "description": "raised fist tone 4", "unicodeVersion": "8.0", "digest": "923f034f481e952e6e5d1664588f99f79bd5416d4197b0ade6621f2669ce5765" }, "fist_tone5": { "category": "people", "moji": "✊🏿", + "description": "raised fist tone 5", "unicodeVersion": "8.0", "digest": "d691d2902216080916a29047e07d7a5bf2aed07e062067ca9d01cbf6fdf48c8d" }, "five": { "category": "symbols", "moji": "5️⃣", + "description": "keycap digit five", "unicodeVersion": "3.0", "digest": "8f03f62fdbf744ae49c8a60fbf715ebfccbd6b62d91148e0923907006f3c2726" }, "flag_ac": { "category": "flags", "moji": "🇦🇨", + "description": "ascension", "unicodeVersion": "6.0", "digest": "2e5c08535dc8ea96422d56a36b4fffc0b3bd2a13f2ab0d8dbd0e3a29bf3fc40c" }, "flag_ad": { "category": "flags", "moji": "🇦🇩", + "description": "andorra", "unicodeVersion": "6.0", "digest": "184fdcf790b8e2fd851b2b2b32f8636c595dd289734d12dc01ae4aa177e2043a" }, "flag_ae": { "category": "flags", "moji": "🇦🇪", + "description": "the united arab emirates", "unicodeVersion": "6.0", "digest": "4a3257a9ce118e97567e76280f24d60fb555f1bada2eb26a2442a47f9398d21e" }, "flag_af": { "category": "flags", "moji": "🇦🇫", + "description": "afghanistan", "unicodeVersion": "6.0", "digest": "0f6c719cac7ab3140694f6b580787ecdbf503e38f16de7ec5803f7d06a088ec3" }, "flag_ag": { "category": "flags", "moji": "🇦🇬", + "description": "antigua and barbuda", "unicodeVersion": "6.0", "digest": "92bf5a0e74564739862e9ba79331ffa656b7bae2ace0fc8dfd288984e4d510d4" }, "flag_ai": { "category": "flags", "moji": "🇦🇮", + "description": "anguilla", "unicodeVersion": "6.0", "digest": "aeaadc7ffafd8a1e01fdabc69d35f725d5f737b4c284a36191d96729f4e66e8f" }, "flag_al": { "category": "flags", "moji": "🇦🇱", + "description": "albania", "unicodeVersion": "6.0", "digest": "5ce7866d214d18c5f3438d480d14e77d104c4de679f0fdfca8cf0a44ce48eeea" }, "flag_am": { "category": "flags", "moji": "🇦🇲", + "description": "armenia", "unicodeVersion": "6.0", "digest": "b40f5705f0cf9ef0fa7ffff0b371c4099319001ce79f894c317912f4dc5de4c8" }, "flag_ao": { "category": "flags", "moji": "🇦🇴", + "description": "angola", "unicodeVersion": "6.0", "digest": "eab6fbc1824d6e3cd152e8ec1d82e1beaebe02b53b35c6f7a883b8548af02f3a" }, "flag_aq": { "category": "flags", "moji": "🇦🇶", + "description": "antarctica", "unicodeVersion": "6.0", "digest": "367f6677a683a5f0e7248ab3a8f46d06ba146a0fd75004c70bac0e913147cdaa" }, "flag_ar": { "category": "flags", "moji": "🇦🇷", + "description": "argentina", "unicodeVersion": "6.0", "digest": "f0dc466b3216957f2679d7208c2d7cf288448b0739b9270a7c5fa717577bdf25" }, "flag_as": { "category": "flags", "moji": "🇦🇸", + "description": "american samoa", "unicodeVersion": "6.0", "digest": "fcb7a865c7763c63b23485cc27207b99a3a8492e83d5b5ee2df259a9f68f77d6" }, "flag_at": { "category": "flags", "moji": "🇦🇹", + "description": "austria", "unicodeVersion": "6.0", "digest": "1d3d58e9abc034f9a093a94716eddf9811d54dfaf27969fd322b3809fac70217" }, "flag_au": { "category": "flags", "moji": "🇦🇺", + "description": "australia", "unicodeVersion": "6.0", "digest": "789563b64c71a5ad49078d335dc166ef614edb56d1e401885d32fb191c198fbd" }, "flag_aw": { "category": "flags", "moji": "🇦🇼", + "description": "aruba", "unicodeVersion": "6.0", "digest": "1504dc3fd8457b44fdf75c15e136dc46a13e8342d1f98949728cdc1238843e0c" }, "flag_ax": { "category": "flags", "moji": "🇦🇽", + "description": "åland islands", "unicodeVersion": "6.0", "digest": "e96fa3525f3be25016a4cf8428261735f3ed5fc9fe5b827b461746a3f08877bf" }, "flag_az": { "category": "flags", "moji": "🇦🇿", + "description": "azerbaijan", "unicodeVersion": "6.0", "digest": "12c366ac2c38b91314fb29056e09fa6e7417766cebde3045859cdb127549f4a2" }, "flag_ba": { "category": "flags", "moji": "🇧🇦", + "description": "bosnia and herzegovina", "unicodeVersion": "6.0", "digest": "0819ea3901510ac20c7f10e67e5f6c818210f17a362c1d12e299c41feb07f828" }, "flag_bb": { "category": "flags", "moji": "🇧🇧", + "description": "barbados", "unicodeVersion": "6.0", "digest": "cf32778a272ed6cbc8e783b59befd9b204009c69c61a425e148d867808b7fab9" }, "flag_bd": { "category": "flags", "moji": "🇧🇩", + "description": "bangladesh", "unicodeVersion": "6.0", "digest": "e6ed186644a874588e879513aec92f8107220dcdd14c766dee61f266ce045665" }, "flag_be": { "category": "flags", "moji": "🇧🇪", + "description": "belgium", "unicodeVersion": "6.0", "digest": "4d941011d15d9f6e755d6f7694884758baf17ac0691bf5d63700f8d6dbcdb948" }, "flag_bf": { "category": "flags", "moji": "🇧🇫", + "description": "burkina faso", "unicodeVersion": "6.0", "digest": "fcc57dbda9a86f725f558b6c6309484c97e65f1644aae4f9fb5e642681f6c2e0" }, "flag_bg": { "category": "flags", "moji": "🇧🇬", + "description": "bulgaria", "unicodeVersion": "6.0", "digest": "816c47ed96c36c90723da150645902ea8ba18b44757fdd776c7b3542cfecfb18" }, "flag_bh": { "category": "flags", "moji": "🇧🇭", + "description": "bahrain", "unicodeVersion": "6.0", "digest": "2cd5c21775a6e73f59d08c9ee0cedf4e8241e562eab939573501d47681987737" }, "flag_bi": { "category": "flags", "moji": "🇧🇮", + "description": "burundi", "unicodeVersion": "6.0", "digest": "2da82acbec5518360633c1b0b56d55a79b67237f67d92af5e5cd75a2f3bd550e" }, "flag_bj": { "category": "flags", "moji": "🇧🇯", + "description": "benin", "unicodeVersion": "6.0", "digest": "8fe8c34651eb4e28ab395261a5b72b6f37579535ed676d15de131914e19c0436" }, "flag_bl": { "category": "flags", "moji": "🇧🇱", + "description": "saint barthélemy", "unicodeVersion": "6.0", "digest": "d37f2a215ee7ef5b5ab62d2a0c87e90553b17c6ee310f803a71e9fd72db880e7" }, "flag_black": { "category": "objects", "moji": "🏴", + "description": "waving black flag", "unicodeVersion": "6.0", "digest": "3740bfc9bcb3b46b697b8b7c47ab2c3e95eca9dbcba12f2bf98a01302704f203" }, "flag_bm": { "category": "flags", "moji": "🇧🇲", + "description": "bermuda", "unicodeVersion": "6.0", "digest": "ccd21655573f3c955d616c5c7b1eac2be1d4772ff611648d6713ba55d9e4aa9b" }, "flag_bn": { "category": "flags", "moji": "🇧🇳", + "description": "brunei", "unicodeVersion": "6.0", "digest": "54330c3d7a37392e69098c213fd8c78f3faab4e7e5909c039188110422514228" }, "flag_bo": { "category": "flags", "moji": "🇧🇴", + "description": "bolivia", "unicodeVersion": "6.0", "digest": "32aff973b26f4f91ca19dddd7861b564da43cfbee87603d8c004f1111342366c" }, "flag_bq": { "category": "flags", "moji": "🇧🇶", + "description": "caribbean netherlands", "unicodeVersion": "6.0", "digest": "b1ebc959c43f706ca430d8633d9efaa9c60133871506b5f030b730cfb4c19e6f" }, "flag_br": { "category": "flags", "moji": "🇧🇷", + "description": "brazil", "unicodeVersion": "6.0", "digest": "64fb154d71fa34ff4838bc405f3e58a4102cf0cb49ca4b06fc3c7a6bf39671f0" }, "flag_bs": { "category": "flags", "moji": "🇧🇸", + "description": "the bahamas", "unicodeVersion": "6.0", "digest": "c4b07e5f652ab06ece95d3774ce8b1399a935f8a28d440cb13cc8bd0b9728ed5" }, "flag_bt": { "category": "flags", "moji": "🇧🇹", + "description": "bhutan", "unicodeVersion": "6.0", "digest": "901ddbd999dd89a87c1e1208b1470cb4e604a9bc023d0cbcdee64e1bc54079ba" }, "flag_bv": { "category": "flags", "moji": "🇧🇻", + "description": "bouvet island", "unicodeVersion": "6.0", "digest": "bbf6daa6174c6fbbbf541c8274f31b6757c3a16007c2687015ea041fd1e2c6b6" }, "flag_bw": { "category": "flags", "moji": "🇧🇼", + "description": "botswana", "unicodeVersion": "6.0", "digest": "05aa351bc04dc0fe2669441ab500e000d48b1f0d7ad9e885c7abfb898aa0eb3f" }, "flag_by": { "category": "flags", "moji": "🇧🇾", + "description": "belarus", "unicodeVersion": "6.0", "digest": "6eda3b87336ecf0aae4963986d86b916a055d8268c70520303288f235a93b0d9" }, "flag_bz": { "category": "flags", "moji": "🇧🇿", + "description": "belize", "unicodeVersion": "6.0", "digest": "d76ed945b1408558a30a99b8eed6712de968fc49fba1721b5660b8f48087e45a" }, "flag_ca": { "category": "flags", "moji": "🇨🇦", + "description": "canada", "unicodeVersion": "6.0", "digest": "2fd036047d89751c05de5577909b58347883bc89c3b7d90bec28ad4770a98ecd" }, "flag_cc": { "category": "flags", "moji": "🇨🇨", + "description": "cocos (keeling) islands", "unicodeVersion": "6.0", "digest": "837ba181a01c71f05d438d205efaaee99f93b2370c97b13e6132f99860323e36" }, "flag_cd": { "category": "flags", "moji": "🇨🇩", + "description": "the democratic republic of the congo", "unicodeVersion": "6.0", "digest": "318689274b4b3b58aed7fc1654127499a9da69bff1b83e592e86e69d167ce16f" }, "flag_cf": { "category": "flags", "moji": "🇨🇫", + "description": "central african republic", "unicodeVersion": "6.0", "digest": "06d6042849d3b7b217c2b18ba787aae449e8c7d2537e2e5974744ec196062228" }, "flag_cg": { "category": "flags", "moji": "🇨🇬", + "description": "the republic of the congo", "unicodeVersion": "6.0", "digest": "09f45d2dcb5a24d8349ef86e7405cc29ef3d65a908c0bff3221c3b4546547813" }, "flag_ch": { "category": "flags", "moji": "🇨🇭", + "description": "switzerland", "unicodeVersion": "6.0", "digest": "53d6d35aeeebb0b4b1ad858dc3691e649ac73d30b3be76f96d5fe9605fa99386" }, "flag_ci": { "category": "flags", "moji": "🇨🇮", + "description": "cote d'ivoire", "unicodeVersion": "6.0", "digest": "7d85a0c314b7397c9397a54ce2f3a4dc5f40d0234e586dbd8a541a8666f0f51e" }, "flag_ck": { "category": "flags", "moji": "🇨🇰", + "description": "cook islands", "unicodeVersion": "6.0", "digest": "c1aa105fe106ed09ed59a596859a0ce4e65a415c59f63df51961491cb947b136" }, "flag_cl": { "category": "flags", "moji": "🇨🇱", + "description": "chile", "unicodeVersion": "6.0", "digest": "0fffdad0d892f5c08aaa332af1ed2c228583d89a43190e979a3c3cb020d5a723" }, "flag_cm": { "category": "flags", "moji": "🇨🇲", + "description": "cameroon", "unicodeVersion": "6.0", "digest": "e9f55e41a1fd2735a82ad7a7ac39326a944cb20423ffba3608ac53a46036caad" }, "flag_cn": { "category": "flags", "moji": "🇨🇳", + "description": "china", "unicodeVersion": "6.0", "digest": "e2c8fee7e3bd51b13d6083d5bf344abe6b9b642e3cbb099d38b4ce341c99d890" }, "flag_co": { "category": "flags", "moji": "🇨🇴", + "description": "colombia", "unicodeVersion": "6.0", "digest": "51c60d0979bf8342eaff7cda9faf4b0dfab38efaf5ddf3717eb8f0e2a595b15f" }, "flag_cp": { "category": "flags", "moji": "🇨🇵", + "description": "clipperton island", "unicodeVersion": "6.0", "digest": "a0124683aa88cd7da886da70c65796c5ad84eb3751e356e9b2aa8ac249cf0bf9" }, "flag_cr": { "category": "flags", "moji": "🇨🇷", + "description": "costa rica", "unicodeVersion": "6.0", "digest": "907905971b219e617a34eef4839b0bd08d98f3480e2631bce523120dcef95196" }, "flag_cu": { "category": "flags", "moji": "🇨🇺", + "description": "cuba", "unicodeVersion": "6.0", "digest": "d88cea729dc9dbbbcadac0409ec561995f061b2280577c01c6c6b37de347f150" }, "flag_cv": { "category": "flags", "moji": "🇨🇻", + "description": "cape verde", "unicodeVersion": "6.0", "digest": "5ce97944adfce09e96387e6f872256482ac99ccbc60017c4d58ddd15b6fb67a7" }, "flag_cw": { "category": "flags", "moji": "🇨🇼", + "description": "curaçao", "unicodeVersion": "6.0", "digest": "a6fc31bd66ddc2ee8e7bde3aeabfe1c4ad00c9688abae234a541cc1236d68c1b" }, "flag_cx": { "category": "flags", "moji": "🇨🇽", + "description": "christmas island", "unicodeVersion": "6.0", "digest": "1261b32bfa22fa1441f5390ff499ac6b921d7ac59cc8acda3deb3a2beb4fb345" }, "flag_cy": { "category": "flags", "moji": "🇨🇾", + "description": "cyprus", "unicodeVersion": "6.0", "digest": "82b1baa05ecffa0ea1f9a83b518163cbd7910985a21955740520bb16b7bb624f" }, "flag_cz": { "category": "flags", "moji": "🇨🇿", + "description": "the czech republic", "unicodeVersion": "6.0", "digest": "a169b18968992a52299b67c24fba495e84de28dec2ebb947a08e0d615ac54a5a" }, "flag_de": { "category": "flags", "moji": "🇩🇪", + "description": "germany", "unicodeVersion": "6.0", "digest": "99d1906944966a188c72ae592362ed907e2a0bfe95263955c34a0941507b30c1" }, "flag_dg": { "category": "flags", "moji": "🇩🇬", + "description": "diego garcia", "unicodeVersion": "6.0", "digest": "dd45e1afe792fca57d4161434bf611bcb7170072d63e4a27fb9dcd6e8912621e" }, "flag_dj": { "category": "flags", "moji": "🇩🇯", + "description": "djibouti", "unicodeVersion": "6.0", "digest": "e90ba4e98fca71ff0ca5e65c28b911cc52f043428f375d8f954ecbd3b0c8f4dd" }, "flag_dk": { "category": "flags", "moji": "🇩🇰", + "description": "denmark", "unicodeVersion": "6.0", "digest": "65b3b5f31935a4969d81fedbb8279c7ad32da454d15c5eafcceba5d140927c77" }, "flag_dm": { "category": "flags", "moji": "🇩🇲", + "description": "dominica", "unicodeVersion": "6.0", "digest": "f6225ded6d2cfd6c182ab1a53b8c49dc9df195df11eb7ff27b15f5d3721ba0eb" }, "flag_do": { "category": "flags", "moji": "🇩🇴", + "description": "the dominican republic", "unicodeVersion": "6.0", "digest": "dc2ad6856cebbe47c5bd7f5dcf087e4f680d396b2d49440a9b71f0ad49fb8102" }, "flag_dz": { "category": "flags", "moji": "🇩🇿", + "description": "algeria", "unicodeVersion": "6.0", "digest": "ea69fffc4d545f9c0fcef6768257501952955ba4d274c9b81843229a1265c5ed" }, "flag_ea": { "category": "flags", "moji": "🇪🇦", + "description": "ceuta, melilla", "unicodeVersion": "6.0", "digest": "e63bfe15428c481dd23b569e7aaf0a76106e58a946995b4415a81097ecd53b7d" }, "flag_ec": { "category": "flags", "moji": "🇪🇨", + "description": "ecuador", "unicodeVersion": "6.0", "digest": "0cdabf85cd567047fda1d9a4508220cab829943a7c542c315078db0aac33edac" }, "flag_ee": { "category": "flags", "moji": "🇪🇪", + "description": "estonia", "unicodeVersion": "6.0", "digest": "6dc4e3377e8e2af3ff40cf940a914bc7840980b4a14e7da86954343f2b1025fe" }, "flag_eg": { "category": "flags", "moji": "🇪🇬", + "description": "egypt", "unicodeVersion": "6.0", "digest": "2ed6bc056015694d75993eb5ee3c1850921d5630681207b04dfbdb982ab346a2" }, "flag_eh": { "category": "flags", "moji": "🇪🇭", + "description": "western sahara", "unicodeVersion": "6.0", "digest": "72adb55943e4df99c00843c65463718609d937480f73dcf4a4451d46b9967a5e" }, "flag_er": { "category": "flags", "moji": "🇪🇷", + "description": "eritrea", "unicodeVersion": "6.0", "digest": "3fa59331eb5300c8c1f7b1f1bc15cfcfe688da6fa4a79341854598086a44eebc" }, "flag_es": { "category": "flags", "moji": "🇪🇸", + "description": "spain", "unicodeVersion": "6.0", "digest": "1fa1d5cb0a7e8b14aaec758b2e7bf49cdf8f3d09bbcc7dfd589053a432eeae25" }, "flag_et": { "category": "flags", "moji": "🇪🇹", + "description": "ethiopia", "unicodeVersion": "6.0", "digest": "72771decfb214394e4beb594e848ea590c3615800adbba24b5df4c5db6ee9617" }, "flag_eu": { "category": "flags", "moji": "🇪🇺", + "description": "european union", "unicodeVersion": "6.0", "digest": "4bfa1b2ef23764ead5ef7899806f93e13fd29a09c75e61431579a4116c836aa4" }, "flag_fi": { "category": "flags", "moji": "🇫🇮", + "description": "finland", "unicodeVersion": "6.0", "digest": "d0208cdd5b153a2865f9f674179c62871d4675abb0fb639fba88fcd62553f54e" }, "flag_fj": { "category": "flags", "moji": "🇫🇯", + "description": "fiji", "unicodeVersion": "6.0", "digest": "6c5ec41114af3846b093a418f6e2b5ff7a83cb72cecde75a7dc62e8cb6dcfe45" }, "flag_fk": { "category": "flags", "moji": "🇫🇰", + "description": "falkland islands", "unicodeVersion": "6.0", "digest": "c69ad641d53785deff5c3934b7dcfcd3dc32ffc31b6d3e799d0555b03c23fc15" }, "flag_fm": { "category": "flags", "moji": "🇫🇲", + "description": "micronesia", "unicodeVersion": "6.0", "digest": "1e29fb06b273f253c23a9e4aa8ff84bfe22cffb5fa158a0c6f4cdeabe0216990" }, "flag_fo": { "category": "flags", "moji": "🇫🇴", + "description": "faroe islands", "unicodeVersion": "6.0", "digest": "f4907d2f606f4f9d3bef06c6d38e8e88f2a148197b1573668866431a007afc2e" }, "flag_fr": { "category": "flags", "moji": "🇫🇷", + "description": "france", "unicodeVersion": "6.0", "digest": "5a1308ab3cbf6bffcab12588cf3325151a6c72990db7408c2b8605d89f94ed6e" }, "flag_ga": { "category": "flags", "moji": "🇬🇦", + "description": "gabon", "unicodeVersion": "6.0", "digest": "ddc32dee2976507be878ec3d3d2408632ca21bc434cd9f58db4f6ac9774a2db5" }, "flag_gb": { "category": "flags", "moji": "🇬🇧", + "description": "great britain", "unicodeVersion": "6.0", "digest": "6b3bb254d134870b02cb066b06e206f652638a915c84b8649ceb30ec67fbebde" }, "flag_gd": { "category": "flags", "moji": "🇬🇩", + "description": "grenada", "unicodeVersion": "6.0", "digest": "b6a210541ca22d816405f2a7d0d5241dc4d5488c8a36e15bd1e3063f9c41327f" }, "flag_ge": { "category": "flags", "moji": "🇬🇪", + "description": "georgia", "unicodeVersion": "6.0", "digest": "e9a5035b7a46b925737e7f7b0ae2419cc4af0e980fbee5bd916edeef13823367" }, "flag_gf": { "category": "flags", "moji": "🇬🇫", + "description": "french guiana", "unicodeVersion": "6.0", "digest": "ce1bcd8c303897c1c22c5994182f21240b4aa635f0d7ce9944f76cbdbf0e4956" }, "flag_gg": { "category": "flags", "moji": "🇬🇬", + "description": "guernsey", "unicodeVersion": "6.0", "digest": "a435aab3609533ab2d68acd97deba844bfb0fc27b2adac68668223011f23ae5d" }, "flag_gh": { "category": "flags", "moji": "🇬🇭", + "description": "ghana", "unicodeVersion": "6.0", "digest": "7cad43b40f69b9b00cc1b38036789ce774fd3d597c89f0bf38433847ea69be26" }, "flag_gi": { "category": "flags", "moji": "🇬🇮", + "description": "gibraltar", "unicodeVersion": "6.0", "digest": "70e9b17d18bf3e0e4d03f4f824323a57909416e4082ca9d8a0796a6959de4f07" }, "flag_gl": { "category": "flags", "moji": "🇬🇱", + "description": "greenland", "unicodeVersion": "6.0", "digest": "1963d8cca1c1f06b7536b7fb8f5a4782ac0bb05afdf6e481101bce45c58cdd4b" }, "flag_gm": { "category": "flags", "moji": "🇬🇲", + "description": "the gambia", "unicodeVersion": "6.0", "digest": "6c776a8daa3f4daa2597b0025aec06fc0a53aed262e845d4da3897cd7a89c6a1" }, "flag_gn": { "category": "flags", "moji": "🇬🇳", + "description": "guinea", "unicodeVersion": "6.0", "digest": "134cf7c839370d171ae80a72e5d18d32ea1967df19c191d1a4ea446d649e9558" }, "flag_gp": { "category": "flags", "moji": "🇬🇵", + "description": "guadeloupe", "unicodeVersion": "6.0", "digest": "be3e906b039ba4884053c78f4f14de9aa87c5573860ccb69ec766068ae3887c2" }, "flag_gq": { "category": "flags", "moji": "🇬🇶", + "description": "equatorial guinea", "unicodeVersion": "6.0", "digest": "d476059c4ab41f5a1ef88583087362a5bc57cede930126f37041d1546564ab70" }, "flag_gr": { "category": "flags", "moji": "🇬🇷", + "description": "greece", "unicodeVersion": "6.0", "digest": "b9fa9304647aaa08167a07858bb18d778dcc399375f86f580b8d4244794678bc" }, "flag_gs": { "category": "flags", "moji": "🇬🇸", + "description": "south georgia", "unicodeVersion": "6.0", "digest": "de33fbef6e294eb7af36e5b94d8ff573b354a4ff1ebdccf50ca528b86ed601d9" }, "flag_gt": { "category": "flags", "moji": "🇬🇹", + "description": "guatemala", "unicodeVersion": "6.0", "digest": "4160843e5d642df597c8423eb8e3b74deafe304f3d141c8a4d2fc07509e44832" }, "flag_gu": { "category": "flags", "moji": "🇬🇺", + "description": "guam", "unicodeVersion": "6.0", "digest": "3b0cb257ba5b1c3e15d9102410c5f7418da03372e91ce90513de25b9f45283e3" }, "flag_gw": { "category": "flags", "moji": "🇬🇼", + "description": "guinea-bissau", "unicodeVersion": "6.0", "digest": "bdf07a8f93c0f0a573af5f5361be404a3ba65b729c1a4c05b7632c03d85efc72" }, "flag_gy": { "category": "flags", "moji": "🇬🇾", + "description": "guyana", "unicodeVersion": "6.0", "digest": "b47d8c98b747556f827ad0d1169264eb68ecaf9d2fb76595e8c31866361cbfc6" }, "flag_hk": { "category": "flags", "moji": "🇭🇰", + "description": "hong kong", "unicodeVersion": "6.0", "digest": "8e5a54b2e4bd4f5182085299b9648062463da05d535cf0e46a7d9c58eaeb171f" }, "flag_hm": { "category": "flags", "moji": "🇭🇲", + "description": "heard island and mcdonald islands", "unicodeVersion": "6.0", "digest": "63c3e080c5e82a72c6d4cf5997ac823dc02184719ec59aadea6dd41b127abf22" }, "flag_hn": { "category": "flags", "moji": "🇭🇳", + "description": "honduras", "unicodeVersion": "6.0", "digest": "87c1d160db810b5ed208fb33add54f96c17b0f08d87b81f6f09429abf6ec93ac" }, "flag_hr": { "category": "flags", "moji": "🇭🇷", + "description": "croatia", "unicodeVersion": "6.0", "digest": "8b68112f79baea38565673acf4f1cb90675a5829ff17e4cf9415c928b62aed88" }, "flag_ht": { "category": "flags", "moji": "🇭🇹", + "description": "haiti", "unicodeVersion": "6.0", "digest": "05dbd548c310ef1ebd1724aa85d821f8320106b16ddbf1f6442ea37e4407d5e1" }, "flag_hu": { "category": "flags", "moji": "🇭🇺", + "description": "hungary", "unicodeVersion": "6.0", "digest": "5079f3d6f1459e6df8dda5c19d2367ead8f5a755b8874ac999bae58e3c9f47a7" }, "flag_ic": { "category": "flags", "moji": "🇮🇨", + "description": "canary islands", "unicodeVersion": "6.0", "digest": "8dcb18c4b75a60867a68d2f6edbf81e782aafb4b9a0404c8081f872dfe71e432" }, "flag_id": { "category": "flags", "moji": "🇮🇩", + "description": "indonesia", "unicodeVersion": "6.0", "digest": "1b0eb69a158ed3afe24be448d44751f95dcc5cbc7d1393a5753293f16ef0a66c" }, "flag_ie": { "category": "flags", "moji": "🇮🇪", + "description": "ireland", "unicodeVersion": "6.0", "digest": "5fc8c101ad7296224455f72f73c335aa4f676023b68645bafaf69087f69af390" }, "flag_il": { "category": "flags", "moji": "🇮🇱", + "description": "israel", "unicodeVersion": "6.0", "digest": "5aea4207415b7615dcdd69413705aefda700aefd0d27010cd0a0a338d879d9b8" }, "flag_im": { "category": "flags", "moji": "🇮🇲", + "description": "isle of man", "unicodeVersion": "6.0", "digest": "1ee9b3a5f1a52fc6d8369bfd81995fc0567e7a61deacd013701b3ec5fd64502e" }, "flag_in": { "category": "flags", "moji": "🇮🇳", + "description": "india", "unicodeVersion": "6.0", "digest": "202ede502f34d55d180726ac2f29141c6875516f1b3e7ee99f266b16c2fe4bfd" }, "flag_io": { "category": "flags", "moji": "🇮🇴", + "description": "british indian ocean territory", "unicodeVersion": "6.0", "digest": "dd45e1afe792fca57d4161434bf611bcb7170072d63e4a27fb9dcd6e8912621e" }, "flag_iq": { "category": "flags", "moji": "🇮🇶", + "description": "iraq", "unicodeVersion": "6.0", "digest": "bef294772b5ffccd6c061c19d60af66f61b248d78705faf347ade9ebfca2b46d" }, "flag_ir": { "category": "flags", "moji": "🇮🇷", + "description": "iran", "unicodeVersion": "6.0", "digest": "d4faca93577a5546330ab6a09252307e19fb420d89912c0b48ceb90bf409d48e" }, "flag_is": { "category": "flags", "moji": "🇮🇸", + "description": "iceland", "unicodeVersion": "6.0", "digest": "b2fc04226b274009b4d99d92bcb72b255b534b6fd4b76d82dce1575ad975a456" }, "flag_it": { "category": "flags", "moji": "🇮🇹", + "description": "italy", "unicodeVersion": "6.0", "digest": "735760f193855d55460a0fb93dad55ff67253cab63176eceb90b9bde1faead1e" }, "flag_je": { "category": "flags", "moji": "🇯🇪", + "description": "jersey", "unicodeVersion": "6.0", "digest": "671a487a60571d928d2abaf306d0a9ba50239ec54ada14ea29a9a99df658d3cc" }, "flag_jm": { "category": "flags", "moji": "🇯🇲", + "description": "jamaica", "unicodeVersion": "6.0", "digest": "fb9047199d030b78fc0dcfc58d9b524fdb929238d922809da88147b7cebf4211" }, "flag_jo": { "category": "flags", "moji": "🇯🇴", + "description": "jordan", "unicodeVersion": "6.0", "digest": "19f7d536d0293ebf3db49e05a158097cbde467115ef96523a0553808fd0b4178" }, "flag_jp": { "category": "flags", "moji": "🇯🇵", + "description": "japan", "unicodeVersion": "6.0", "digest": "51e971f777fe481ca9f7e077ecb2ce252c3cc0086b76384e7b965cdc337f3f9e" }, "flag_ke": { "category": "flags", "moji": "🇰🇪", + "description": "kenya", "unicodeVersion": "6.0", "digest": "0cec8f068548cfd3e7a20c10af84f97ca415fd6f8ab8b50783bf982e77d7260e" }, "flag_kg": { "category": "flags", "moji": "🇰🇬", + "description": "kyrgyzstan", "unicodeVersion": "6.0", "digest": "5803ea6ab028261923fd7570c670a50518c6f462a2fb4d463531b12c3e382e6f" }, "flag_kh": { "category": "flags", "moji": "🇰🇭", + "description": "cambodia", "unicodeVersion": "6.0", "digest": "287d357afe47179853fd485fb102834ead145598ed892664fc62d245cac16080" }, "flag_ki": { "category": "flags", "moji": "🇰🇮", + "description": "kiribati", "unicodeVersion": "6.0", "digest": "ae4aee0d9cd7a21d4e250d45a484f5f641acdab3d79b437337b25fe34a0b49b0" }, "flag_km": { "category": "flags", "moji": "🇰🇲", + "description": "the comoros", "unicodeVersion": "6.0", "digest": "2d1730acbf5421fd02bd5483e26a86d82ec2fa99f0ff75bfd728a9df7914ad3b" }, "flag_kn": { "category": "flags", "moji": "🇰🇳", + "description": "saint kitts and nevis", "unicodeVersion": "6.0", "digest": "b9ed979db9c6d243b00f61f19a9ec0f2c2390b2e5cace5ad61d9371dc8c670ac" }, "flag_kp": { "category": "flags", "moji": "🇰🇵", + "description": "north korea", "unicodeVersion": "6.0", "digest": "1bab0b9cab8028a95ce7231ad8d88ebcd31601cfa321284bba017ead47f6c729" }, "flag_kr": { "category": "flags", "moji": "🇰🇷", + "description": "korea", "unicodeVersion": "6.0", "digest": "33be8c09ebe273e203aa703cc827d52a6d9bf1699f5445bba13a77af2df45fa6" }, "flag_kw": { "category": "flags", "moji": "🇰🇼", + "description": "kuwait", "unicodeVersion": "6.0", "digest": "04d901a92ea55b13dc4983a9e3adb52dc89c9f3decee86fd06022aa902678b6d" }, "flag_ky": { "category": "flags", "moji": "🇰🇾", + "description": "cayman islands", "unicodeVersion": "6.0", "digest": "10f4d02f33cadd34da89de71a3b763809bad480cd9ae9d2ec000db026bd94cd1" }, "flag_kz": { "category": "flags", "moji": "🇰🇿", + "description": "kazakhstan", "unicodeVersion": "6.0", "digest": "dfaff69a78cf635f7fad41bd5bdcc8003298454708a6178ba7348b1b40c360c1" }, "flag_la": { "category": "flags", "moji": "🇱🇦", + "description": "laos", "unicodeVersion": "6.0", "digest": "4fcfbdc694cf99ae3f832500cdcdedb88c444b6df88bc9b7141f4f26ba3d5bfd" }, "flag_lb": { "category": "flags", "moji": "🇱🇧", + "description": "lebanon", "unicodeVersion": "6.0", "digest": "af4b1f784bea0ec7a712495491dffbd1152cc857a99fd433f76bfeb313819a62" }, "flag_lc": { "category": "flags", "moji": "🇱🇨", + "description": "saint lucia", "unicodeVersion": "6.0", "digest": "40784aa558b75d07ae499c004e2cc5d0b2efdfc3e5be705b5a9f6b70d681c396" }, "flag_li": { "category": "flags", "moji": "🇱🇮", + "description": "liechtenstein", "unicodeVersion": "6.0", "digest": "c4eb4c43f457ce60ff9d046adb512c1d3462203403eeb595bff3ebc010ed6633" }, "flag_lk": { "category": "flags", "moji": "🇱🇰", + "description": "sri lanka", "unicodeVersion": "6.0", "digest": "a5285cdfdc3715fa3941f5f0eb03dc425969eaaf22c719c27ab4418628d09bc5" }, "flag_lr": { "category": "flags", "moji": "🇱🇷", + "description": "liberia", "unicodeVersion": "6.0", "digest": "ed04334264953b4da570db8c392b99d2fab4e0b7efc2331427016c6a08e818be" }, "flag_ls": { "category": "flags", "moji": "🇱🇸", + "description": "lesotho", "unicodeVersion": "6.0", "digest": "cd56022106d027317cc9bf4c848758cf29ffe277ce71fdb9c1cf89ac4fd6e6db" }, "flag_lt": { "category": "flags", "moji": "🇱🇹", + "description": "lithuania", "unicodeVersion": "6.0", "digest": "3c4395b068e421100fd97a102f170cb8d5c093885eef7cb40d3faff4f4e47fe9" }, "flag_lu": { "category": "flags", "moji": "🇱🇺", + "description": "luxembourg", "unicodeVersion": "6.0", "digest": "df15a2c47eecad17e0cc169bdf0d31c6a51eb22de7ca4e70d2431359a33f930d" }, "flag_lv": { "category": "flags", "moji": "🇱🇻", + "description": "latvia", "unicodeVersion": "6.0", "digest": "9b53c6ce23287935200da8ca8a8af78013a4b1572f9821e7e1724cbad248e7e2" }, "flag_ly": { "category": "flags", "moji": "🇱🇾", + "description": "libya", "unicodeVersion": "6.0", "digest": "42efa9f3526ef006d6723fa17538a98ab9556ae25f14df1b06d21361bf7e1a44" }, "flag_ma": { "category": "flags", "moji": "🇲🇦", + "description": "morocco", "unicodeVersion": "6.0", "digest": "96c07296cfd7aa1cb642faed8ace26744105b81ca880157a4ef4caee0befe26e" }, "flag_mc": { "category": "flags", "moji": "🇲🇨", + "description": "monaco", "unicodeVersion": "6.0", "digest": "6b44608842fe849ae2b4bae5eb87ccd436459a427051dfda25080196273d4b9f" }, "flag_md": { "category": "flags", "moji": "🇲🇩", + "description": "moldova", "unicodeVersion": "6.0", "digest": "78c7b01c698873a9129d52ba38b3eb4cfc683ef2ae10b7b922b17c07f1c938c8" }, "flag_me": { "category": "flags", "moji": "🇲🇪", + "description": "montenegro", "unicodeVersion": "6.0", "digest": "01aa0f9df89302edc4ae319b5dd78069ba8807c3f38cc7bfe01bff67c8efd416" }, "flag_mf": { "category": "flags", "moji": "🇲🇫", + "description": "saint martin", "unicodeVersion": "6.0", "digest": "a0124683aa88cd7da886da70c65796c5ad84eb3751e356e9b2aa8ac249cf0bf9" }, "flag_mg": { "category": "flags", "moji": "🇲🇬", + "description": "madagascar", "unicodeVersion": "6.0", "digest": "56ebcd2a2e144d656d3b38a62595138fe6e50f9c1144f70b0a120cce7a72eb5b" }, "flag_mh": { "category": "flags", "moji": "🇲🇭", + "description": "the marshall islands", "unicodeVersion": "6.0", "digest": "008660adc4c2e4d04830498988184d1ef8a372a6c085da369a94ee6b820dbbb7" }, "flag_mk": { "category": "flags", "moji": "🇲🇰", + "description": "macedonia", "unicodeVersion": "6.0", "digest": "f3c4c5106ace81c21fc0c6a7cc5c5e04e9453468fbc6ccbc851bb8dd61ff237f" }, "flag_ml": { "category": "flags", "moji": "🇲🇱", + "description": "mali", "unicodeVersion": "6.0", "digest": "e70a6b30e46adc2e19684308a848fef2c3ad76e2cac4bb493ee3270ad39f9d1b" }, "flag_mm": { "category": "flags", "moji": "🇲🇲", + "description": "myanmar", "unicodeVersion": "6.0", "digest": "720f5d38887202ba049cd5a46c183679be6a01f169d99e6e656c73b515793a7d" }, "flag_mn": { "category": "flags", "moji": "🇲🇳", + "description": "mongolia", "unicodeVersion": "6.0", "digest": "5f0fd6fcb2ed73a5a6d9396c3703612503c1f16283bbb4e9362a1c8324b762ad" }, "flag_mo": { "category": "flags", "moji": "🇲🇴", + "description": "macau", "unicodeVersion": "6.0", "digest": "fc2a9e7323867cf195f551e59afdab778c56b84c96af28c20207c9870caa2c39" }, "flag_mp": { "category": "flags", "moji": "🇲🇵", + "description": "northern mariana islands", "unicodeVersion": "6.0", "digest": "ddce3be9d72914240c42e1b97ea97af01016d0a3879999cb0e447552682c06ba" }, "flag_mq": { "category": "flags", "moji": "🇲🇶", + "description": "martinique", "unicodeVersion": "6.0", "digest": "888f455b1322d6fb83dc9f469f5505fea3dd6ece77d17d0d7345319c3ebcec0e" }, "flag_mr": { "category": "flags", "moji": "🇲🇷", + "description": "mauritania", "unicodeVersion": "6.0", "digest": "72621914c92dd9c9f3ac9973ee3589583bfe42b841cdd35f47af75e2f629726c" }, "flag_ms": { "category": "flags", "moji": "🇲🇸", + "description": "montserrat", "unicodeVersion": "6.0", "digest": "5944996295132f41ec55261ff7927518bd47aec95d274a6ff257c357b43657bc" }, "flag_mt": { "category": "flags", "moji": "🇲🇹", + "description": "malta", "unicodeVersion": "6.0", "digest": "95f0550e8823441a4e69b26c540baea94f3ddcc282100fd0239021c00df0b469" }, "flag_mu": { "category": "flags", "moji": "🇲🇺", + "description": "mauritius", "unicodeVersion": "6.0", "digest": "5fda78a6df0ea7f5cac5fb4c8fd68529c14c5e15bac4e0b167493cb6ac459253" }, "flag_mv": { "category": "flags", "moji": "🇲🇻", + "description": "maldives", "unicodeVersion": "6.0", "digest": "f75c8f6fd3a68f2944a04c833c649d4b576997f491100cf3f3160fe77117fabb" }, "flag_mw": { "category": "flags", "moji": "🇲🇼", + "description": "malawi", "unicodeVersion": "6.0", "digest": "d46b484a97e5b90b6b259f8de1712b553f93f0dfb6391209200358bb9429ebf5" }, "flag_mx": { "category": "flags", "moji": "🇲🇽", + "description": "mexico", "unicodeVersion": "6.0", "digest": "dc57c10307fc0aa09bd7fcd25ee0fca561f3b382276faa8432a927c1baea53fd" }, "flag_my": { "category": "flags", "moji": "🇲🇾", + "description": "malaysia", "unicodeVersion": "6.0", "digest": "15ca00660a1eb0096fdaa00b85a7b95fcf192bf2ee4781ba72c36d2d2cb015ef" }, "flag_mz": { "category": "flags", "moji": "🇲🇿", + "description": "mozambique", "unicodeVersion": "6.0", "digest": "0c8605a9319dcf86672a833b4c4d6acea5f6aa25a3f8e1dfac78fbf7c452ba97" }, "flag_na": { "category": "flags", "moji": "🇳🇦", + "description": "namibia", "unicodeVersion": "6.0", "digest": "e63cde5ee49d3ada1e33d2ab15dc24fbb129b90d65b6fd1d7c07455f71a53601" }, "flag_nc": { "category": "flags", "moji": "🇳🇨", + "description": "new caledonia", "unicodeVersion": "6.0", "digest": "a4a350ce7404ba7bdda9a341e7a48fcfe16312be4964b1bd6eed7115acd2e329" }, "flag_ne": { "category": "flags", "moji": "🇳🇪", + "description": "niger", "unicodeVersion": "6.0", "digest": "6b32483b4445bc52855509f618c570b9c9606de5649e4878b71b44ff2acbc9fd" }, "flag_nf": { "category": "flags", "moji": "🇳🇫", + "description": "norfolk island", "unicodeVersion": "6.0", "digest": "96b1ec33acbd2b1ffe42703c11a2a633b036e6779849b0e6fa8f399167820584" }, "flag_ng": { "category": "flags", "moji": "🇳🇬", + "description": "nigeria", "unicodeVersion": "6.0", "digest": "f97d0630cbfa5e75440251df7529a67b58c22598643390cbeea82fb04a1cd956" }, "flag_ni": { "category": "flags", "moji": "🇳🇮", + "description": "nicaragua", "unicodeVersion": "6.0", "digest": "c52fb5f9134122a91defa75425be2c6b3c909e051d546244e0e7bdf5f9ee1710" }, "flag_nl": { "category": "flags", "moji": "🇳🇱", + "description": "the netherlands", "unicodeVersion": "6.0", "digest": "b8918f9c0c92513aa0ec6ba6cee5448270168cbe6f0a970fb06e7ceb9f52ec71" }, "flag_no": { "category": "flags", "moji": "🇳🇴", + "description": "norway", "unicodeVersion": "6.0", "digest": "05ce84095f8d93407d611b39d8b6a67fd9f11df6cfab7a185bcb4eec186d85ef" }, "flag_np": { "category": "flags", "moji": "🇳🇵", + "description": "nepal", "unicodeVersion": "6.0", "digest": "cc41c2f97ec2b38fe5781d553792f6aab5d37cc3be02586f361fe89d12683bee" }, "flag_nr": { "category": "flags", "moji": "🇳🇷", + "description": "nauru", "unicodeVersion": "6.0", "digest": "7837edf59ec33a25380d76afea5f04cfcab4f17df4e33fca0dcaacb517c5cbec" }, "flag_nu": { "category": "flags", "moji": "🇳🇺", + "description": "niue", "unicodeVersion": "6.0", "digest": "fd9ab45c6f32bc4da47542392e5beba73ddac302a4a9a00e6deedc913a4c087d" }, "flag_nz": { "category": "flags", "moji": "🇳🇿", + "description": "new zealand", "unicodeVersion": "6.0", "digest": "0719830dcca400cefb30ce399bb03f49dd84c9a98f7d6a28270f9278e2a7af75" }, "flag_om": { "category": "flags", "moji": "🇴🇲", + "description": "oman", "unicodeVersion": "6.0", "digest": "3f9039becd52e3454fdf7611cdb0d7fb1196e053eea29ef87daab6c21a94f1ee" }, "flag_pa": { "category": "flags", "moji": "🇵🇦", + "description": "panama", "unicodeVersion": "6.0", "digest": "1adf0e5d4084e072aa44bd9978829e77546e0be75785e9be69f92e326bd714a7" }, "flag_pe": { "category": "flags", "moji": "🇵🇪", + "description": "peru", "unicodeVersion": "6.0", "digest": "f8a4e257676f4ab8962ffe5509b8417777a8be2f0e9dc7735d3e014ff221aab1" }, "flag_pf": { "category": "flags", "moji": "🇵🇫", + "description": "french polynesia", "unicodeVersion": "6.0", "digest": "1ace6cc71d130cdf09246297740a911f14828c322e35330cc548ca5975015c23" }, "flag_pg": { "category": "flags", "moji": "🇵🇬", + "description": "papua new guinea", "unicodeVersion": "6.0", "digest": "9c37719d9f51ef31fec0f898d38e522b4253cd00344408e3f660132514efddb7" }, "flag_ph": { "category": "flags", "moji": "🇵🇭", + "description": "the philippines", "unicodeVersion": "6.0", "digest": "f1af628cf6d1d290cedef3d564b2386e2d6f14ba4426d3fefc0312cb8772e517" }, "flag_pk": { "category": "flags", "moji": "🇵🇰", + "description": "pakistan", "unicodeVersion": "6.0", "digest": "61c77f73d2a10a5acb289fadfe0d25d1a1c343e1223bd802099ff4e0e9356521" }, "flag_pl": { "category": "flags", "moji": "🇵🇱", + "description": "poland", "unicodeVersion": "6.0", "digest": "38c2c8618446e1f72cf983ab33e736d943f0db7c4cce52a187299e8cec2ea895" }, "flag_pm": { "category": "flags", "moji": "🇵🇲", + "description": "saint pierre and miquelon", "unicodeVersion": "6.0", "digest": "656be9ea1a79c3885a759c7ce353d338345a198d7939556949affaf5490cb644" }, "flag_pn": { "category": "flags", "moji": "🇵🇳", + "description": "pitcairn", "unicodeVersion": "6.0", "digest": "2792260d8087ab0253b1214c1420f0160ab2eef9afe7315f9e7ff0b87cd15d72" }, "flag_pr": { "category": "flags", "moji": "🇵🇷", + "description": "puerto rico", "unicodeVersion": "6.0", "digest": "c4cfa1f2201dcda9de310a8247e6ce32d2798ae426a14dd70a9ebb00a2804d46" }, "flag_ps": { "category": "flags", "moji": "🇵🇸", + "description": "palestinian authority", "unicodeVersion": "6.0", "digest": "197f2ec6294bf0ee4a08cf2f2d1e237ee867c98b3085454a3f42abc955eeb289" }, "flag_pt": { "category": "flags", "moji": "🇵🇹", + "description": "portugal", "unicodeVersion": "6.0", "digest": "86a50827963756b5bf471ed9df5b3f2a2058b4c5d778a303414b6b0556e2082b" }, "flag_pw": { "category": "flags", "moji": "🇵🇼", + "description": "palau", "unicodeVersion": "6.0", "digest": "a6321c47a0cd188fbfdf3b55f17a7170c63080d28d50e4f5463eb1ee09af2412" }, "flag_py": { "category": "flags", "moji": "🇵🇾", + "description": "paraguay", "unicodeVersion": "6.0", "digest": "1a169e8d8703c510c5a2265b57dbed2f811b03ec375bcb341ab4cd0b100a9dd6" }, "flag_qa": { "category": "flags", "moji": "🇶🇦", + "description": "qatar", "unicodeVersion": "6.0", "digest": "de6283965cd98a244b7fa6288174f9ff0d8feb497f191f2e4ab3b690138a3d5d" }, "flag_re": { "category": "flags", "moji": "🇷🇪", + "description": "réunion", "unicodeVersion": "6.0", "digest": "260e1b97abc1562e5a73d7e53652ffed8059fc9b1c969741c466f48ec6ab0e80" }, "flag_ro": { "category": "flags", "moji": "🇷🇴", + "description": "romania", "unicodeVersion": "6.0", "digest": "6d648e03955fa2a9fd2bad6f60ec96d3e20ee57f5855f3721a4d4e0c8e99f95c" }, "flag_rs": { "category": "flags", "moji": "🇷🇸", + "description": "serbia", "unicodeVersion": "6.0", "digest": "95cd5e197ed364e403eeb7f1d18a83487d89166910ba8119ea994e5e19d6a7ee" }, "flag_ru": { "category": "flags", "moji": "🇷🇺", + "description": "russia", "unicodeVersion": "6.0", "digest": "a4a81617a59d9eaf3c526431ca6f90ed334a7c1f516bf70cbd3f1fdc6e6103d7" }, "flag_rw": { "category": "flags", "moji": "🇷🇼", + "description": "rwanda", "unicodeVersion": "6.0", "digest": "7a369f60db0876ffef111c319a3e8c9eaed620c875c51b98ed9ad5207b836dca" }, "flag_sa": { "category": "flags", "moji": "🇸🇦", + "description": "saudi arabia", "unicodeVersion": "6.0", "digest": "b249fbfd7ed415943f60bbd841965cf721979f960ccbe09396aebac1eca913d7" }, "flag_sb": { "category": "flags", "moji": "🇸🇧", + "description": "the solomon islands", "unicodeVersion": "6.0", "digest": "526b411260024ea7b6ea6c47f2549345c6cc6960e9a29bfa9aaec0772664d2dc" }, "flag_sc": { "category": "flags", "moji": "🇸🇨", + "description": "the seychelles", "unicodeVersion": "6.0", "digest": "d036b0d068745926120eaf746fa2e4433306e2e14c6b540d0cd6265e34471056" }, "flag_sd": { "category": "flags", "moji": "🇸🇩", + "description": "sudan", "unicodeVersion": "6.0", "digest": "889615bdb9b1f9c59c5f83ed4d22d54a0ed5dd5de263e729c58544cb06c55885" }, "flag_se": { "category": "flags", "moji": "🇸🇪", + "description": "sweden", "unicodeVersion": "6.0", "digest": "f471d80cfff340960a752c8c152ed4fb482df2a3712b0a56dfab31b9b806926a" }, "flag_sg": { "category": "flags", "moji": "🇸🇬", + "description": "singapore", "unicodeVersion": "6.0", "digest": "82f58a09f98593cc87e545f7e5c03d2aedaf82e54e73f71f58c18e994c3085ac" }, "flag_sh": { "category": "flags", "moji": "🇸🇭", + "description": "saint helena", "unicodeVersion": "6.0", "digest": "53914b1fa8c1b4f30bae6c1f6717f138fb4dbf482c3e20e33f7aea4ecfc0438d" }, "flag_si": { "category": "flags", "moji": "🇸🇮", + "description": "slovenia", "unicodeVersion": "6.0", "digest": "65d491daa69f9a11cec9ccc4df3a669f12ef95a5c312137776d4472719940ba3" }, "flag_sj": { "category": "flags", "moji": "🇸🇯", + "description": "svalbard and jan mayen", "unicodeVersion": "6.0", "digest": "bbf6daa6174c6fbbbf541c8274f31b6757c3a16007c2687015ea041fd1e2c6b6" }, "flag_sk": { "category": "flags", "moji": "🇸🇰", + "description": "slovakia", "unicodeVersion": "6.0", "digest": "d4fd03eca5bd3c9fb324ee04fae37c9a2d852bac8335369e3e720ef9b98fff36" }, "flag_sl": { "category": "flags", "moji": "🇸🇱", + "description": "sierra leone", "unicodeVersion": "6.0", "digest": "1455c98c11c248623d82be5484ab1c4dcd1dae449adc393eb1aa2d8c74aa3f02" }, "flag_sm": { "category": "flags", "moji": "🇸🇲", + "description": "san marino", "unicodeVersion": "6.0", "digest": "daec5864ac50c625d7bf49d6c1a170a094cf0d1b9a0bdf62a62406e7ec500a94" }, "flag_sn": { "category": "flags", "moji": "🇸🇳", + "description": "senegal", "unicodeVersion": "6.0", "digest": "4e4d43c467e5eb84c70f535f37f4f468319bd4b06c6ec3db3b54f69efdafd334" }, "flag_so": { "category": "flags", "moji": "🇸🇴", + "description": "somalia", "unicodeVersion": "6.0", "digest": "c1434dca361563a8e3ba88f1ad19c3f6c9cbb8f3ebc17ce128fde2351ff67d0c" }, "flag_sr": { "category": "flags", "moji": "🇸🇷", + "description": "suriname", "unicodeVersion": "6.0", "digest": "f3c6bfee2a052f03d56ba917b88595450cef111ffa9e92c7f39ef8c3c3bd12d1" }, "flag_ss": { "category": "flags", "moji": "🇸🇸", + "description": "south sudan", "unicodeVersion": "6.0", "digest": "c0ed7e4f41206f5363e8ebdc6c3f28080e2f07d99e6fb73c1f6226d83310e69d" }, "flag_st": { "category": "flags", "moji": "🇸🇹", + "description": "sao tome and principe", "unicodeVersion": "6.0", "digest": "b022ae5d6885e28c6e9c83c17dd0c24c731d4f3d5773c49051768cdd4df51330" }, "flag_sv": { "category": "flags", "moji": "🇸🇻", + "description": "el salvador", "unicodeVersion": "6.0", "digest": "5bafdd04d243ee3f3998f4ec0a3d03ff5a3975e771b1f94f89d7713193d7a242" }, "flag_sx": { "category": "flags", "moji": "🇸🇽", + "description": "sint maarten", "unicodeVersion": "6.0", "digest": "fb92e9f514bcc2f7abbd4e146edde50f030c940c833f184618cbb48e56af22bd" }, "flag_sy": { "category": "flags", "moji": "🇸🇾", + "description": "syria", "unicodeVersion": "6.0", "digest": "ee330da644d4ce1fdba98be5eaab5054aed8d91a34ab617199a4b2b77f62a10b" }, "flag_sz": { "category": "flags", "moji": "🇸🇿", + "description": "swaziland", "unicodeVersion": "6.0", "digest": "7fe0c7429efd9682cc39e57f4bba8d1491d301643ba999d57c4e1bc37517ed64" }, "flag_ta": { "category": "flags", "moji": "🇹🇦", + "description": "tristan da cunha", "unicodeVersion": "6.0", "digest": "b47e245a2708072a4dbaf190c9606baa4daf02e51627eeae6f20c3b4c95024c0" }, "flag_tc": { "category": "flags", "moji": "🇹🇨", + "description": "turks and caicos islands", "unicodeVersion": "6.0", "digest": "18cfff14c2503b9d24c91c668583d4a14efb17657d800eca86ae49b547c9da5c" }, "flag_td": { "category": "flags", "moji": "🇹🇩", + "description": "chad", "unicodeVersion": "6.0", "digest": "73d1db3365736915c4cdf9ba9343d9fd78962203b60334e8f3724d4b330b17db" }, "flag_tf": { "category": "flags", "moji": "🇹🇫", + "description": "french southern territories", "unicodeVersion": "6.0", "digest": "3bffeb4bc9ceb9cbb150de88e957b6e46509862ca7d616d5693124af084eb435" }, "flag_tg": { "category": "flags", "moji": "🇹🇬", + "description": "togo", "unicodeVersion": "6.0", "digest": "eb13a0e85baf73326f3ae3bc75e8406eca42000d7e42b0641120e64c0ab7ebaa" }, "flag_th": { "category": "flags", "moji": "🇹🇭", + "description": "thailand", "unicodeVersion": "6.0", "digest": "a4e42efa4bb94e90f3a92ae9ce14affaacd3a142c1e0da40d8cc839500e771fd" }, "flag_tj": { "category": "flags", "moji": "🇹🇯", + "description": "tajikistan", "unicodeVersion": "6.0", "digest": "ff926fa3e86e095683a61c4754355a5b4dd0ecb74393306bd791d130fd1a909d" }, "flag_tk": { "category": "flags", "moji": "🇹🇰", + "description": "tokelau", "unicodeVersion": "6.0", "digest": "3fa732d457ded6c83cd5f73d934f64c4e687eb0cde7c157d2fdcdccaf3b5fb52" }, "flag_tl": { "category": "flags", "moji": "🇹🇱", + "description": "east timor", "unicodeVersion": "6.0", "digest": "0ec2a4d22fb832060693089e518bbe370a4e13bfc28748f110fc13726409f473" }, "flag_tm": { "category": "flags", "moji": "🇹🇲", + "description": "turkmenistan", "unicodeVersion": "6.0", "digest": "b4724aa7ad13352f16a0936e61cbb85f0bd147583fc66597aff7e8ee7cf19c21" }, "flag_tn": { "category": "flags", "moji": "🇹🇳", + "description": "tunisia", "unicodeVersion": "6.0", "digest": "5ab308ffdde40f504d6ee080817bbddbe4f3f4ddb71f508c75e0144a8c8044d9" }, "flag_to": { "category": "flags", "moji": "🇹🇴", + "description": "tonga", "unicodeVersion": "6.0", "digest": "75b7e7198fa42f87986882b8ca251a229afcaa0a1188ae7b9f5ece87dc31a723" }, "flag_tr": { "category": "flags", "moji": "🇹🇷", + "description": "turkey", "unicodeVersion": "6.0", "digest": "9cc48a8f8fa9c17c1627272f68d4740da0e7ce17a2cf8c6b5c08cc9b95e1390c" }, "flag_tt": { "category": "flags", "moji": "🇹🇹", + "description": "trinidad and tobago", "unicodeVersion": "6.0", "digest": "f9e63543121bb3cd2e41bc7b0c2c4ba662bc1cc0520b79fc4e201ec6456fdf59" }, "flag_tv": { "category": "flags", "moji": "🇹🇻", + "description": "tuvalu", "unicodeVersion": "6.0", "digest": "6431e5f06cc7995ae7208c429ecf39339b545854cb6d6b7447f465fe53614dfc" }, "flag_tw": { "category": "flags", "moji": "🇹🇼", + "description": "the republic of china", "unicodeVersion": "6.0", "digest": "8395ab3c6a595023b006518a5345ac3612f2893d3a8f011b7e5802414236b03c" }, "flag_tz": { "category": "flags", "moji": "🇹🇿", + "description": "tanzania", "unicodeVersion": "6.0", "digest": "716181733cd9ac7a8f51a9a64bc5d21020e8112f6768e8c49c4d651a3ee0b8a4" }, "flag_ua": { "category": "flags", "moji": "🇺🇦", + "description": "ukraine", "unicodeVersion": "6.0", "digest": "304570736345e28734f5ff84a2b0481c2bb00bf29d9892bd749b57dec7741e30" }, "flag_ug": { "category": "flags", "moji": "🇺🇬", + "description": "uganda", "unicodeVersion": "6.0", "digest": "a1bafb74c54ee8c92cb025b55aebdb6081eec3fda6a7f86f2ee14d1b801a8e9c" }, "flag_um": { "category": "flags", "moji": "🇺🇲", + "description": "united states minor outlying islands", "unicodeVersion": "6.0", "digest": "b3c9ac72211f481f50cde09e10b92aa03b1ea90abf85418e60a35b84963273ee" }, "flag_us": { "category": "flags", "moji": "🇺🇸", + "description": "united states", "unicodeVersion": "6.0", "digest": "da79f9af0a188178a82e7dc3a62298fa416f4cfbcae432838df1abebca5c0d63" }, "flag_uy": { "category": "flags", "moji": "🇺🇾", + "description": "uruguay", "unicodeVersion": "6.0", "digest": "8348e901d775722497ee911c9c9b4bd767710760c507630a67ecb6d47cc646c7" }, "flag_uz": { "category": "flags", "moji": "🇺🇿", + "description": "uzbekistan", "unicodeVersion": "6.0", "digest": "2a1dc1e9469e01c58ea91f545ef3fe0bdfe5544a73a80407f8960d01b1e5db5c" }, "flag_va": { "category": "flags", "moji": "🇻🇦", + "description": "the vatican city", "unicodeVersion": "6.0", "digest": "0e8134ec94bff032bfc63b0b08587d5298c9b7f31edd5a5b35633ae911434e61" }, "flag_vc": { "category": "flags", "moji": "🇻🇨", + "description": "saint vincent and the grenadines", "unicodeVersion": "6.0", "digest": "e0290e1be72c8939ee6c398f00a107703b21b97d91b9bf465e553ffbf00304a7" }, "flag_ve": { "category": "flags", "moji": "🇻🇪", + "description": "venezuela", "unicodeVersion": "6.0", "digest": "76a6a6c2353def1f984d1a6980831e63f3aea5af2201b574197834e7c203d57a" }, "flag_vg": { "category": "flags", "moji": "🇻🇬", + "description": "british virgin islands", "unicodeVersion": "6.0", "digest": "56fc9317b8dd62cccc60010819f8b895dd4569a9b06368a9250f815c39177b8a" }, "flag_vi": { "category": "flags", "moji": "🇻🇮", + "description": "u.s. virgin islands", "unicodeVersion": "6.0", "digest": "2526a3e13b8ccd301f0763580430898c227bd209e3ce482c7951140b28948375" }, "flag_vn": { "category": "flags", "moji": "🇻🇳", + "description": "vietnam", "unicodeVersion": "6.0", "digest": "0cf6b9896bbe4da8ed7718d0abfd56cef1a8321e26f89d3ad1b48488eaffb7a5" }, "flag_vu": { "category": "flags", "moji": "🇻🇺", + "description": "vanuatu", "unicodeVersion": "6.0", "digest": "9dfa282ce1aafc62beacab76e1fc19a141c8bdeaa30898f69b083067b775d362" }, "flag_wf": { "category": "flags", "moji": "🇼🇫", + "description": "wallis and futuna", "unicodeVersion": "6.0", "digest": "a0124683aa88cd7da886da70c65796c5ad84eb3751e356e9b2aa8ac249cf0bf9" }, "flag_white": { "category": "objects", "moji": "🏳", + "description": "waving white flag", "unicodeVersion": "6.0", "digest": "d9be4b7ceb8309c48f88cfd07a9f7ce6758ea6e620e73293cf14baec03ca381c" }, "flag_ws": { "category": "flags", "moji": "🇼🇸", + "description": "samoa", "unicodeVersion": "6.0", "digest": "53addd0dc304a3c8893389ed227986ef2431828b8c071926aa09f9efd815b649" }, "flag_xk": { "category": "flags", "moji": "🇽🇰", + "description": "kosovo", "unicodeVersion": "6.0", "digest": "eba1a832e489e1c2734e773e685df5d128271fa5559d23c060e68be067bf6469" }, "flag_ye": { "category": "flags", "moji": "🇾🇪", + "description": "yemen", "unicodeVersion": "6.0", "digest": "edfa14266785042b6d5fe0f64fafa630b16a3ee7d010501de7cc8554c959afb0" }, "flag_yt": { "category": "flags", "moji": "🇾🇹", + "description": "mayotte", "unicodeVersion": "6.0", "digest": "472ebc676b5d31dec2ac5e02ce69014a3dd94609d30a95f39f3a752f49c85e8b" }, "flag_za": { "category": "flags", "moji": "🇿🇦", + "description": "south africa", "unicodeVersion": "6.0", "digest": "dad162942a43392b4cff6929bd5cbf58c382a03dbc0e552f03c07ad2d8ff08ce" }, "flag_zm": { "category": "flags", "moji": "🇿🇲", + "description": "zambia", "unicodeVersion": "6.0", "digest": "1521ecaf1d1fdc8c15f0c96a6b04e6d4050f26f943a826b3d3d661f6ded6d438" }, "flag_zw": { "category": "flags", "moji": "🇿🇼", + "description": "zimbabwe", "unicodeVersion": "6.0", "digest": "46d05b597c5c77c8e2dc7bd6d8dd62ebca01bc9c9dc9915dafe694ca56402825" }, "flags": { "category": "objects", "moji": "🎏", + "description": "carp streamer", "unicodeVersion": "6.0", "digest": "f860aa4df587cf140c3e9735bbd101e9fd5a1bfcea42e420d85ac0a9877fa21d" }, "flashlight": { "category": "objects", "moji": "🔦", + "description": "electric torch", "unicodeVersion": "6.0", "digest": "e929bbe76e0fd2dc5bd6476858a0bbc717fd21467710435d35d80efb38033d73" }, "fleur-de-lis": { "category": "symbols", "moji": "⚜", + "description": "fleur-de-lis", "unicodeVersion": "4.1", "digest": "ebf49007f367dc05580e9dab942e93e9dda12fa1dc2caa410ac7f8d8cd55d2a3" }, "floppy_disk": { "category": "objects", "moji": "💾", + "description": "floppy disk", "unicodeVersion": "6.0", "digest": "4ee0b5bba41b9e301ed125d3ee1c263bef171ca499e6e1b89276b09af2bc03a0" }, "flower_playing_cards": { "category": "symbols", "moji": "🎴", + "description": "flower playing cards", "unicodeVersion": "6.0", "digest": "edba47c2e3051b2c7effd98794ec977174052782edcb491daec82a2b0d853869" }, "flushed": { "category": "people", "moji": "😳", + "description": "flushed face", "unicodeVersion": "6.0", "digest": "e759d46bab92af5494d78b6c712c06568759afe397e7828ca0a0de1e3eab0165" }, "fog": { "category": "nature", "moji": "🌫", + "description": "fog", "unicodeVersion": "7.0", "digest": "0cbd4733961d30fe0f40f95dd1f37254aebbef26f82dd18ad2000e799eb2898e" }, "foggy": { "category": "travel", "moji": "🌁", + "description": "foggy", "unicodeVersion": "6.0", "digest": "bc3631a4e9e8473b92e842008937add2cd9ffad5b7d772ce759fb5ff6c0e3dca" }, "football": { "category": "activity", "moji": "🏈", + "description": "american football", "unicodeVersion": "6.0", "digest": "ebd790471c3a28d3077818e3b31d915ffe443e06e299bc5cf0dd2534d080634c" }, "footprints": { "category": "people", "moji": "👣", + "description": "footprints", "unicodeVersion": "6.0", "digest": "85bbf2bc0ae8e6259d83a06f513600095d7fcfc44372670f5b2405d380b78811" }, "fork_and_knife": { "category": "food", "moji": "🍴", + "description": "fork and knife", "unicodeVersion": "6.0", "digest": "f228accd36ddccb4ec636207c19d7185191ec79723b780a1bd5c3d00a4b1ef3b" }, "fork_knife_plate": { "category": "food", "moji": "🍽", + "description": "fork and knife with plate", "unicodeVersion": "7.0", "digest": "ec6be99dac8efd3d145807fa60d2b6d8f6d3c02cb95552b55cc0fac39a4db48e" }, "fountain": { "category": "travel", "moji": "⛲", + "description": "fountain", "unicodeVersion": "5.2", "digest": "87043f9256e1d4615159307fcfd21bf6ae2aba0bada7de2bd50d7d6f2ab82395" }, "four": { "category": "symbols", "moji": "4️⃣", + "description": "keycap digit four", "unicodeVersion": "3.0", "digest": "c2c82a966bbb599aae557d930a4fc42604f2081aa45528872f5caf4942ee79d9" }, "four_leaf_clover": { "category": "nature", "moji": "🍀", + "description": "four leaf clover", "unicodeVersion": "6.0", "digest": "ebee16e86bc9be843dfc72ab5372fb462f06be4486b5b25d7d4cac9b2c8b01c8" }, "fox": { "category": "nature", "moji": "🦊", + "description": "fox face", "unicodeVersion": "9.0", "digest": "e9903cb0396f7e49bdd2c384b38e614c13bfa576b3ecc1ec7b9819e4a40d91d1" }, "frame_photo": { "category": "objects", "moji": "🖼", + "description": "frame with picture", "unicodeVersion": "7.0", "digest": "d5074f748a15055ec1fb812c1e5e169e6e3cc73c522c54be1359b0e26c0fc75c" }, "free": { "category": "symbols", "moji": "🆓", + "description": "squared free", "unicodeVersion": "6.0", "digest": "9973522457158362fc5bdd7da858e6371e28a8403d1ef9e4b6427195c7f72cfa" }, "french_bread": { "category": "food", "moji": "🥖", + "description": "baguette bread", "unicodeVersion": "9.0", "digest": "47518a4312f57207b8e8c38188d4a2bd8b16830a885cfcf2d281cfab50c1bc6e" }, "fried_shrimp": { "category": "food", "moji": "🍤", + "description": "fried shrimp", "unicodeVersion": "6.0", "digest": "0792bdc4484852de970c8f43bc3a1a339dc0e48090ec77d6de97cbfcdd17f9e1" }, "fries": { "category": "food", "moji": "🍟", + "description": "french fries", "unicodeVersion": "6.0", "digest": "47915aea67251d358d91a0e4dc3dcc347155336007d6b931a192be72a743b4e9" }, "frog": { "category": "nature", "moji": "🐸", + "description": "frog face", "unicodeVersion": "6.0", "digest": "d024b2ce771df64040534fb0906737d18b562bc3578dee62c2f25ec03c7caffd" }, "frowning": { "category": "people", "moji": "😦", + "description": "frowning face with open mouth", "unicodeVersion": "6.1", "digest": "c01af48537b0011d313d8f65103e1401fce4f5c0269c68e0e9806926c59acc44" }, "frowning2": { "category": "people", "moji": "☹", + "description": "white frowning face", "unicodeVersion": "1.1", "digest": "6568ee393b950c852d440112e86908c456b89fb7780e27778c5fcec168373fbf" }, "fuelpump": { "category": "travel", "moji": "⛽", + "description": "fuel pump", "unicodeVersion": "5.2", "digest": "105e736469f19911b8bab4ab6d29f949ded4b061b54e3dd763726577d6453095" }, "full_moon": { "category": "nature", "moji": "🌕", + "description": "full moon symbol", "unicodeVersion": "6.0", "digest": "aaa87f4676a5aaa29c1b721a3b582e89db6c1f35a25c52e4b480bd193ef39c43" }, "full_moon_with_face": { "category": "nature", "moji": "🌝", + "description": "full moon with face", "unicodeVersion": "6.0", "digest": "05c4b9c339fcdf81ae67027641522baa99c370d87873ff4c8133b8349e627e33" }, "game_die": { "category": "activity", "moji": "🎲", + "description": "game die", "unicodeVersion": "6.0", "digest": "00d19ce8e21dba2cdfeb18709fa8741f3af9d6207f81d5657b68e05e64f105a8" }, "gear": { "category": "objects", "moji": "⚙", + "description": "gear", "unicodeVersion": "4.1", "digest": "c5ba354c0f7a36dce95477091984e352ecc59af8c9f26a94ad8e296dc042b9de" }, "gem": { "category": "objects", "moji": "💎", + "description": "gem stone", "unicodeVersion": "6.0", "digest": "180e66f19d9285e02d0a5e859722c608206826e80323942b9938fc49d44973b1" }, "gemini": { "category": "symbols", "moji": "♊", + "description": "gemini", "unicodeVersion": "1.1", "digest": "278239c598d490a110f1f3f52fc3b85259be8e76034b38228ef3f68d7ddd8cdd" }, "ghost": { "category": "people", "moji": "👻", + "description": "ghost", "unicodeVersion": "6.0", "digest": "80d528fcf8ef9198631527547e43a608a4332a799f9e5550b8318dec67c9c4d2" }, "gift": { "category": "objects", "moji": "🎁", + "description": "wrapped present", "unicodeVersion": "6.0", "digest": "4061a84a59f0300473299678c43e533341eb965db09597fffc6e221fd7b77376" }, "gift_heart": { "category": "symbols", "moji": "💝", + "description": "heart with ribbon", "unicodeVersion": "6.0", "digest": "5420199b515b9b32c964a3c19d87e07461639e3068a939dae26c6436335c0cee" }, "girl": { "category": "people", "moji": "👧", + "description": "girl", "unicodeVersion": "6.0", "digest": "8d2d0b72a91e6e44921b71030ffc4c89c0f50f1364787784afe1e7e568cf1bc6" }, "girl_tone1": { "category": "people", "moji": "👧🏻", + "description": "girl tone 1", "unicodeVersion": "8.0", "digest": "bda12a6b38994a578ee65166bbdd93ea04df4101697b52ed236de8d687df09de" }, "girl_tone2": { "category": "people", "moji": "👧🏼", + "description": "girl tone 2", "unicodeVersion": "8.0", "digest": "de7a0925c30b7181a289f71b1a849c1b7751ee8c104e8f2029bd9c2fe3f91c64" }, "girl_tone3": { "category": "people", "moji": "👧🏽", + "description": "girl tone 3", "unicodeVersion": "8.0", "digest": "e41272816db0e642d003dce7cb262e1593a592251f46729f7830f4515149e1f2" }, "girl_tone4": { "category": "people", "moji": "👧🏾", + "description": "girl tone 4", "unicodeVersion": "8.0", "digest": "8d6a4513ecbf08408c0ecc5336767777a2216f7a19437faf9e51f65101822469" }, "girl_tone5": { "category": "people", "moji": "👧🏿", + "description": "girl tone 5", "unicodeVersion": "8.0", "digest": "f55e4b16a41b6f5e3c817a301420360ba4486e4e82e1092a56a3e3cc4069087d" }, "globe_with_meridians": { "category": "symbols", "moji": "🌐", + "description": "globe with meridians", "unicodeVersion": "6.0", "digest": "725bebeb3c09a9e3701ebe49e672dcfbf2b73575e05f0821263511577b013b75" }, "goal": { "category": "activity", "moji": "🥅", + "description": "goal net", "unicodeVersion": "9.0", "digest": "7088c432f276ff6f447dc0d431b9062b394fb401de1072fe59ca56267bfd6717" }, "goat": { "category": "nature", "moji": "🐐", + "description": "goat", "unicodeVersion": "6.0", "digest": "d07e384d08529ddcaddd2710f2ad913e5665dc15d5f99c28e16dadd245a111e8" }, "golf": { "category": "activity", "moji": "⛳", + "description": "flag in hole", "unicodeVersion": "5.2", "digest": "eed79364754eec97855e3c7b584f347ae139d9ddb4eb7fb66c00867610b8f1c1" }, "golfer": { "category": "activity", "moji": "🏌", + "description": "golfer", "unicodeVersion": "7.0", "digest": "7d7ecc6e226596f646030a4109c2b0001ef0cc690e4863e450bf5d29e7a90344" }, "gorilla": { "category": "nature", "moji": "🦍", + "description": "gorilla", "unicodeVersion": "9.0", "digest": "4a564dc14f8ae5450d094f6410ec7f099a7f07dc5254b6395f44a35527bdb4b7" }, "grapes": { "category": "food", "moji": "🍇", + "description": "grapes", "unicodeVersion": "6.0", "digest": "74d1a09ab411234a84d025a2e717e7ec5791bc02aad29853896d21c0f0283c50" }, "green_apple": { "category": "food", "moji": "🍏", + "description": "green apple", "unicodeVersion": "6.0", "digest": "457490e9b2b20894f50768262d63f1021717079da104d4847076b3fa779e9a21" }, "green_book": { "category": "objects", "moji": "📗", + "description": "green book", "unicodeVersion": "6.0", "digest": "370f635b200efe5e4a9f17da58bd22500e258e61d17795cef375f19c9a45468f" }, "green_heart": { "category": "symbols", "moji": "💚", + "description": "green heart", "unicodeVersion": "6.0", "digest": "f71e30416d9019873f2ed38ef375c48386424ff60b5a07b89b15dc9e0a3970f9" }, "grey_exclamation": { "category": "symbols", "moji": "❕", + "description": "white exclamation mark ornament", "unicodeVersion": "6.0", "digest": "2fa1d356e12c17cc4025e43afb6c3070385f677102a35223302fda46c47a9b03" }, "grey_question": { "category": "symbols", "moji": "❔", + "description": "white question mark ornament", "unicodeVersion": "6.0", "digest": "e1035bcbf0f66d238ef478ba451f5cf2c51627fbf101ed03bad3b2bf38db8aa2" }, "grimacing": { "category": "people", "moji": "😬", + "description": "grimacing face", "unicodeVersion": "6.1", "digest": "2cedad13b8b2a1d4385ca6fa88a251eb7757a4c65dd6d362267864a01247846b" }, "grin": { "category": "people", "moji": "😁", + "description": "grinning face with smiling eyes", "unicodeVersion": "6.0", "digest": "634b2f37e32e57ed6edc7f371993a92e34137dd21ba393de5227cfbbe2422815" }, "grinning": { "category": "people", "moji": "😀", + "description": "grinning face", "unicodeVersion": "6.1", "digest": "cef76aa41771db9fd1d6bd9b4233c22c1fb1931494af54cab29e6347ed9b678d" }, "guardsman": { "category": "people", "moji": "💂", + "description": "guardsman", "unicodeVersion": "6.0", "digest": "17bc7fad6b8c8dbd015bb709380d129f8b8e1e971062d15e6ab0b2e63e500564" }, "guardsman_tone1": { "category": "people", "moji": "💂🏻", + "description": "guardsman tone 1", "unicodeVersion": "8.0", "digest": "c531ecb101bdf9ce1db18e1567882e6db927410237100b0a2492a1401860246e" }, "guardsman_tone2": { "category": "people", "moji": "💂🏼", + "description": "guardsman tone 2", "unicodeVersion": "8.0", "digest": "602168c5204af0f1de8b4aa5863b192ef20c19d263999377aa5eb60f98311732" }, "guardsman_tone3": { "category": "people", "moji": "💂🏽", + "description": "guardsman tone 3", "unicodeVersion": "8.0", "digest": "d0a85de46dd02c7bd6cb14bff0f22d2db9083d4b171a8806c83363b49f3dd9ef" }, "guardsman_tone4": { "category": "people", "moji": "💂🏾", + "description": "guardsman tone 4", "unicodeVersion": "8.0", "digest": "1c9d4d72b6b50bdac8271613b6d2a38340ec2067bc344e8ee2a3c863fd5c23a1" }, "guardsman_tone5": { "category": "people", "moji": "💂🏿", + "description": "guardsman tone 5", "unicodeVersion": "8.0", "digest": "9899a796d01842e495d716fbe737a16d85724f7d3e23f50807ec2bc70f057318" }, "guitar": { "category": "activity", "moji": "🎸", + "description": "guitar", "unicodeVersion": "6.0", "digest": "a1027ceae4dd3ea270740587c9d373329e5677e375c9e00af6ae3275e0b67500" }, "gun": { "category": "objects", "moji": "🔫", + "description": "pistol", "unicodeVersion": "6.0", "digest": "fc12b577df2283e7b336f23774f9cfe5b79f1d26ddd28a64a560519b28d94ca5" }, "haircut": { "category": "people", "moji": "💇", + "description": "haircut", "unicodeVersion": "6.0", "digest": "b243a04f5ca889accd45e7abe095ac5caa92274ed95103f5966a36b415fff412" }, "haircut_tone1": { "category": "people", "moji": "💇🏻", + "description": "haircut tone 1", "unicodeVersion": "8.0", "digest": "a58d0cff1427b80dfd7a9ea5267b4a181e9faaac6a51a0165db522f668b4cf91" }, "haircut_tone2": { "category": "people", "moji": "💇🏼", + "description": "haircut tone 2", "unicodeVersion": "8.0", "digest": "675083ff40001405f8de99268477d50dd8594ff6ca40ddfd442dd42ad76e8216" }, "haircut_tone3": { "category": "people", "moji": "💇🏽", + "description": "haircut tone 3", "unicodeVersion": "8.0", "digest": "70d7581e49c315a3771dd61a3713229886db32aaaeb3af078a69cc042f809150" }, "haircut_tone4": { "category": "people", "moji": "💇🏾", + "description": "haircut tone 4", "unicodeVersion": "8.0", "digest": "ec5e3e909eb3bc375ef9cc0fe0e0f90b33f44f273ada91ccf415bbc43b8ffbfc" }, "haircut_tone5": { "category": "people", "moji": "💇🏿", + "description": "haircut tone 5", "unicodeVersion": "8.0", "digest": "7c89739ee458546a808fded7f96d9354c47a76883ebb262d5f5abeafd021260e" }, "hamburger": { "category": "food", "moji": "🍔", + "description": "hamburger", "unicodeVersion": "6.0", "digest": "48204235238bd89d3a69f319f65135102f3d6b181eec241d4d86b302bbffa9bf" }, "hammer": { "category": "objects", "moji": "🔨", + "description": "hammer", "unicodeVersion": "6.0", "digest": "d0e7830539d935fcd82820c4e0c1d724f0756dfc83a51171fe0f4b36b69fac42" }, "hammer_pick": { "category": "objects", "moji": "⚒", + "description": "hammer and pick", "unicodeVersion": "4.1", "digest": "aa0445f43bca58d17afa7f3577632ca7775f5a28336385b3020b268b15b18142" }, "hamster": { "category": "nature", "moji": "🐹", + "description": "hamster face", "unicodeVersion": "6.0", "digest": "a7e7582e8b1bccd5b7df27ccb05e353a3f0e39bdeb40877732706b9d74a70de1" }, "hand_splayed": { "category": "people", "moji": "🖐", + "description": "raised hand with fingers splayed", "unicodeVersion": "7.0", "digest": "c51a30cb7e575d29ffed16780a6c95ae3f300b8ac523012f4a6e116d68c1fd15" }, "hand_splayed_tone1": { "category": "people", "moji": "🖐🏻", + "description": "raised hand with fingers splayed tone 1", "unicodeVersion": "8.0", "digest": "c31fb44a982ed8808e1c311ec1b0b9c5afcb47f16bb1fc731dc483adf8f0d049" }, "hand_splayed_tone2": { "category": "people", "moji": "🖐🏼", + "description": "raised hand with fingers splayed tone 2", "unicodeVersion": "8.0", "digest": "56a236881184e9ffad54613fa08a67368c432af738f5254fb1cd87b20368acdf" }, "hand_splayed_tone3": { "category": "people", "moji": "🖐🏽", + "description": "raised hand with fingers splayed tone 3", "unicodeVersion": "8.0", "digest": "9242ca97dfd2bbc1947228f6535029afb31f8feb72c14ff4b7f2deea30217425" }, "hand_splayed_tone4": { "category": "people", "moji": "🖐🏾", + "description": "raised hand with fingers splayed tone 4", "unicodeVersion": "8.0", "digest": "43348d9fd3d43b3c45cebaf663bf181bcad3b6df841a5aeed838180db2cdd481" }, "hand_splayed_tone5": { "category": "people", "moji": "🖐🏿", + "description": "raised hand with fingers splayed tone 5", "unicodeVersion": "8.0", "digest": "4b3a0aba7829772fec09f26d6facc19a2f822d2998015297b18b5cab85190ee2" }, "handbag": { "category": "people", "moji": "👜", + "description": "handbag", "unicodeVersion": "6.0", "digest": "45410a3eed0c2e3f68748d7649fa9e33a90f4e80d5291206bdd0b40380c6da45" }, "handball": { "category": "activity", "moji": "🤾", + "description": "handball", "unicodeVersion": "9.0", "digest": "94ceb28024eb3259d8b137cafd7438773e717fbc04f5da810f85e43ca0fa9e00" }, "handball_tone1": { "category": "activity", "moji": "🤾🏻", + "description": "handball tone 1", "unicodeVersion": "9.0", "digest": "8bec4de0d05c80e335e44d65598d186ca92696977353c9fd9c2a5efa122cb842" }, "handball_tone2": { "category": "activity", "moji": "🤾🏼", + "description": "handball tone 2", "unicodeVersion": "9.0", "digest": "2ff4131e1e2f089b315d8e176c9348877c26c2bd03706fb75d41bc61bc99bf93" }, "handball_tone3": { "category": "activity", "moji": "🤾🏽", + "description": "handball tone 3", "unicodeVersion": "9.0", "digest": "224a71f94dd37d3729325d11412334667a81422e21f6d7c008730ff350f51a80" }, "handball_tone4": { "category": "activity", "moji": "🤾🏾", + "description": "handball tone 4", "unicodeVersion": "9.0", "digest": "a5f7a9db790565981bad2d0d9e09554c8c509a8179b4705a418300d58a7894b4" }, "handball_tone5": { "category": "activity", "moji": "🤾🏿", + "description": "handball tone 5", "unicodeVersion": "9.0", "digest": "00404572d4683f2e8e8a494aa733e96fbec1723634d0a8cb8d75f2829a789d27" }, "handshake": { "category": "people", "moji": "🤝", + "description": "handshake", "unicodeVersion": "9.0", "digest": "cb4b08b70560908f96bda0aecd2f4c966bea180f9b7200e4c81d342dc8d36087" }, "handshake_tone1": { "category": "people", "moji": "🤝🏻", + "description": "handshake tone 1", "unicodeVersion": "9.0", "digest": "40470e224683ba375ed8698c0cbd560556be5a8898237ddf504377a3a7e89ff0" }, "handshake_tone2": { "category": "people", "moji": "🤝🏼", + "description": "handshake tone 2", "unicodeVersion": "9.0", "digest": "77ed378243bf682f1f4f1a8caeabcbedf772f54631cc40ea46c099e46a499b18" }, "handshake_tone3": { "category": "people", "moji": "🤝🏽", + "description": "handshake tone 3", "unicodeVersion": "9.0", "digest": "81b95050f0878b617f5d2640e34031c26a0072e46ca5a688eb4356e48bc74c92" }, "handshake_tone4": { "category": "people", "moji": "🤝🏾", + "description": "handshake tone 4", "unicodeVersion": "9.0", "digest": "74919a6f026fbbd0ccdbdbd4288d1b2ef3bda8930e9142c07736db4a7f3ef345" }, "handshake_tone5": { "category": "people", "moji": "🤝🏿", + "description": "handshake tone 5", "unicodeVersion": "9.0", "digest": "a30d662bfad0074ca7e32cf6f7229b643b636c4beaec496777eb7e1d5b6fc470" }, "hash": { "category": "symbols", "moji": "#⃣", + "description": "number sign", "unicodeVersion": "3.0", "digest": "01c8b577953010bff0c20f797c2c96ab5d98d4e6ac179c4895a78f34ea904655" }, "hatched_chick": { "category": "nature", "moji": "🐥", + "description": "front-facing baby chick", "unicodeVersion": "6.0", "digest": "006571b9e9e839ec9fcb1a911b935c8ca71eb8bcdce9775bee6a2a4c7c927277" }, "hatching_chick": { "category": "nature", "moji": "🐣", + "description": "hatching chick", "unicodeVersion": "6.0", "digest": "fd7f69fa186407f80de59dec5116e318325a5743ee0e8bba1db541f1e57e7f74" }, "head_bandage": { "category": "people", "moji": "🤕", + "description": "face with head-bandage", "unicodeVersion": "8.0", "digest": "d09019a73e203b38cc43729a96163147de88e09eab8adb073888e55366854c72" }, "headphones": { "category": "activity", "moji": "🎧", + "description": "headphone", "unicodeVersion": "6.0", "digest": "34f9d5598158d5d6f978a5ea5c5aa9948bb2990625565a3afad7710f864fbe2f" }, "hear_no_evil": { "category": "nature", "moji": "🙉", + "description": "hear-no-evil monkey", "unicodeVersion": "6.0", "digest": "53b030b6d6f4ed1a734fa7d48b46f42eb1b2b01653202c1838b742082f08c4bf" }, "heart": { "category": "symbols", "moji": "❤", + "description": "heavy black heart", "unicodeVersion": "1.1", "digest": "92be652ec3e50c6e7393440b5d52b88a367f98a28dffe12660095ed3253aa6c0" }, "heart_decoration": { "category": "symbols", "moji": "💟", + "description": "heart decoration", "unicodeVersion": "6.0", "digest": "6ec5bbf3aa75c6f43eb3dc05e9204366936e8b6b4219310bacdc2fc45f51e245" }, "heart_exclamation": { "category": "symbols", "moji": "❣", + "description": "heavy heart exclamation mark ornament", "unicodeVersion": "1.1", "digest": "5985ea4d82232a2a07052a59db268aed9ac943895d0c82f637595bb5386329a6" }, "heart_eyes": { "category": "people", "moji": "😍", + "description": "smiling face with heart-shaped eyes", "unicodeVersion": "6.0", "digest": "0eff616517a6252ec89d47d9b4ad85589bcf2bdc7f490578934350acb84b2fcc" }, "heart_eyes_cat": { "category": "people", "moji": "😻", + "description": "smiling cat face with heart-shaped eyes", "unicodeVersion": "6.0", "digest": "8a1f28b97d661ca4cff5ee13889ca61b5fa745ccb590e80832b7d7701df101d6" }, "heartbeat": { "category": "symbols", "moji": "💓", + "description": "beating heart", "unicodeVersion": "6.0", "digest": "c9ec024943439d476df6f5ec3a6b30508365a7af3427671a80de3ef2f4f95ffe" }, "heartpulse": { "category": "symbols", "moji": "💗", + "description": "growing heart", "unicodeVersion": "6.0", "digest": "281d8aebfea37db5b7fe82d9115be167006881fe29ab64a5b09ac92ac27a2309" }, "hearts": { "category": "symbols", "moji": "♥", + "description": "black heart suit", "unicodeVersion": "1.1", "digest": "271429d12c40be921897005b7bdd08f9518960af1e1e6f56bb0060f1f183651e" }, "heavy_check_mark": { "category": "symbols", "moji": "✔", + "description": "heavy check mark", "unicodeVersion": "1.1", "digest": "e347728e1290eb9e7b0742d628e2fd124fc049e0774f8a6ddf8e5286e7318718" }, "heavy_division_sign": { "category": "symbols", "moji": "➗", + "description": "heavy division sign", "unicodeVersion": "6.0", "digest": "c1e8c40f0788f140b1c5fcb81ed9b5ce1bcfa5988bb8140ed2808e9cb7e0d651" }, "heavy_dollar_sign": { "category": "symbols", "moji": "💲", + "description": "heavy dollar sign", "unicodeVersion": "6.0", "digest": "7cdeef38348654b93d566e01a48973281cb404a63d0b75b3bad51032887f3f55" }, "heavy_minus_sign": { "category": "symbols", "moji": "➖", + "description": "heavy minus sign", "unicodeVersion": "6.0", "digest": "e5335cc6b22abdce49a6127c34269b65a4a6643ddd3253d9baac425089143e7d" }, "heavy_multiplication_x": { "category": "symbols", "moji": "✖", + "description": "heavy multiplication x", "unicodeVersion": "1.1", "digest": "64bbe9e9716a922e405d2f6d3b6d803863a53fac80ff8cd775899971046cb1ca" }, "heavy_plus_sign": { "category": "symbols", "moji": "➕", + "description": "heavy plus sign", "unicodeVersion": "6.0", "digest": "d0d8ade2020ceb252205180b85c66e665856e6cb505518d395b9913b0b24b746" }, "helicopter": { "category": "travel", "moji": "🚁", + "description": "helicopter", "unicodeVersion": "6.0", "digest": "4bd6fd13650fbe3a19cfffeffe6c21b1cda74bd6af64c5dc5999185e35444bc3" }, "helmet_with_cross": { "category": "people", "moji": "⛑", + "description": "helmet with white cross", "unicodeVersion": "5.2", "digest": "8286107391d44b9cd7fce5dc83bfdebbcdcf5a8214c46a8990732ec40263ed77" }, "herb": { "category": "nature", "moji": "🌿", + "description": "herb", "unicodeVersion": "6.0", "digest": "9fe8ed65515ede59d0926dcf98f14e2498785e1965610aa0dd56eca9b4bedad9" }, "hibiscus": { "category": "nature", "moji": "🌺", + "description": "hibiscus", "unicodeVersion": "6.0", "digest": "c442e8eacbd8727bd154bd39692a9a2a03ea2f674b9670ad8361f78a038afe49" }, "high_brightness": { "category": "symbols", "moji": "🔆", + "description": "high brightness symbol", "unicodeVersion": "6.0", "digest": "35ced42426dcfd5214c2c6c577dce84bb708156433945e6b6adaff7ea530cc57" }, "high_heel": { "category": "people", "moji": "👠", + "description": "high-heeled shoe", "unicodeVersion": "6.0", "digest": "1e7c7aba50eb1d02cf1d9aa372caca741a6005cf47f68dfa75b7310c3cb18f05" }, "hockey": { "category": "activity", "moji": "🏒", + "description": "ice hockey stick and puck", "unicodeVersion": "8.0", "digest": "2d00fb17baa617e799db8e9b1771cc365bb4545c7633df0123e66e1a6e2ed25d" }, "hole": { "category": "objects", "moji": "🕳", + "description": "hole", "unicodeVersion": "7.0", "digest": "8b5539f6f24f09d5d68ffd56be5aa2a8a2f753a8dfbf64892fb02c8f2703e920" }, "homes": { "category": "travel", "moji": "🏘", + "description": "house buildings", "unicodeVersion": "7.0", "digest": "cd512f2b4ce747325607d47da48e083dbfe38a44b85b2522bc372bd105afd25f" }, "honey_pot": { "category": "food", "moji": "🍯", + "description": "honey pot", "unicodeVersion": "6.0", "digest": "f6eec8c32fbd1b461446dc6c5d5031c43e6ee9685dc9b1ea1b839114e48c4eee" }, "horse": { "category": "nature", "moji": "🐴", + "description": "horse face", "unicodeVersion": "6.0", "digest": "e377649a9549835770a2a721a92570f699255f88efa646029638eb8ec5f10e3d" }, "horse_racing": { "category": "activity", "moji": "🏇", + "description": "horse racing", "unicodeVersion": "6.0", "digest": "3b98e94e9c028ad85b9a750cc61db5ee3ac23cf5ad9243ea3e996b1f772bad54" }, "horse_racing_tone1": { "category": "activity", "moji": "🏇🏻", + "description": "horse racing tone 1", "unicodeVersion": "8.0", "digest": "382d8e4502ed34fc1bbf1779ce483bc2e22b83f89c91746c11a5d7aea656d446" }, "horse_racing_tone2": { "category": "activity", "moji": "🏇🏼", + "description": "horse racing tone 2", "unicodeVersion": "8.0", "digest": "198df9973b492ea63e5cfc210dd9591750ccce04a6380adc1dc5b4cb0462a8cd" }, "horse_racing_tone3": { "category": "activity", "moji": "🏇🏽", + "description": "horse racing tone 3", "unicodeVersion": "8.0", "digest": "a67f95fc92c366750ebad3c4db92982893d67a5ed78163c8cc809ac40d2ab9a3" }, "horse_racing_tone4": { "category": "activity", "moji": "🏇🏾", + "description": "horse racing tone 4", "unicodeVersion": "8.0", "digest": "986b1706c4a3395b58a8ae3b7609ffdd4424dfefcbf26c88c8085f4f6379734e" }, "horse_racing_tone5": { "category": "activity", "moji": "🏇🏿", + "description": "horse racing tone 5", "unicodeVersion": "8.0", "digest": "66656b5e3d0f43f16f983f9db6214b07aac73b143eeff6475782f98aa5b9ba53" }, "hospital": { "category": "travel", "moji": "🏥", + "description": "hospital", "unicodeVersion": "6.0", "digest": "034573e76df444f5b0eb7aff3a4103e4b49a1813869155ab3ae29a6fc0c6c8a2" }, "hot_pepper": { "category": "food", "moji": "🌶", + "description": "hot pepper", "unicodeVersion": "7.0", "digest": "0b05777d42698196a10db17d04030175b1dfa772d06288f71d666d5f8d3fddbc" }, "hotdog": { "category": "food", "moji": "🌭", + "description": "hot dog", "unicodeVersion": "8.0", "digest": "7a25bbd1a7531fd34a22c654c0931d9e74bea2bbe7baa9f9cbd88f43baa79fb5" }, "hotel": { "category": "travel", "moji": "🏨", + "description": "hotel", "unicodeVersion": "6.0", "digest": "2d78e0ad4cfb0caad778c7de49fefd6e8356afe902a43e3f1c40bceb6b0be422" }, "hotsprings": { "category": "symbols", "moji": "♨", + "description": "hot springs", "unicodeVersion": "1.1", "digest": "4c10c3a974b44693e8cbe91365c8b8d7f14f62db234cc516b6e54c08a6bacaed" }, "hourglass": { "category": "objects", "moji": "⌛", + "description": "hourglass", "unicodeVersion": "1.1", "digest": "f0bae8392aaf6f75a83f5d8914936b8650665b24ba1b232fa546b71545dd9acd" }, "hourglass_flowing_sand": { "category": "objects", "moji": "⏳", + "description": "hourglass with flowing sand", "unicodeVersion": "6.0", "digest": "2d077729f40fc04007a933e97356bd511cbd8be76b8c55962ca3fa0d8b828e23" }, "house": { "category": "travel", "moji": "🏠", + "description": "house building", "unicodeVersion": "6.0", "digest": "b4ac25979fbe161ada0d2a75769aa7552d2371d37d78cddba4ffdc7f076d3279" }, "house_abandoned": { "category": "travel", "moji": "🏚", + "description": "derelict house building", "unicodeVersion": "7.0", "digest": "6e1a58533fbfe88a0eb03668c9f17c5c654a6cc7734ed798d4a885400f823610" }, "house_with_garden": { "category": "travel", "moji": "🏡", + "description": "house with garden", "unicodeVersion": "6.0", "digest": "817463f23ec0a849393ba75c333e822b4d253cd4db998c127e90d1b924f35d20" }, "hugging": { "category": "people", "moji": "🤗", + "description": "hugging face", "unicodeVersion": "8.0", "digest": "69810a98b1247e1f1e496aa757e428189ef5cc086764fabd8189cf1eef82234f" }, "hushed": { "category": "people", "moji": "😯", + "description": "hushed face", "unicodeVersion": "6.1", "digest": "22586107f7399eff64538a52929dade152633aa268fc5ec4e6fe1c0e00a7bd89" }, "ice_cream": { "category": "food", "moji": "🍨", + "description": "ice cream", "unicodeVersion": "6.0", "digest": "d1a8e685f2ecf83dead28733859e369d6ce120a2669cdab97dc4423547d472ac" }, "ice_skate": { "category": "activity", "moji": "⛸", + "description": "ice skate", "unicodeVersion": "5.2", "digest": "41ef65c143bc068868fa64080ffd447d91aa3fe2a39e69ecaa97022820af4dcd" }, "icecream": { "category": "food", "moji": "🍦", + "description": "soft ice cream", "unicodeVersion": "6.0", "digest": "22cfe17b80cbd2a0377ee90da45bd40d33533c914b2639d363fbb1f00714e194" }, "id": { "category": "symbols", "moji": "🆔", + "description": "squared id", "unicodeVersion": "6.0", "digest": "bcf0922e083821d3be7951893084ea0d72a0110ef0b20d11dfec24dd70633893" }, "ideograph_advantage": { "category": "symbols", "moji": "🉐", + "description": "circled ideograph advantage", "unicodeVersion": "6.0", "digest": "0b6bf59f63fda1afa92d652814a778a056c3f4abdd9cf3f6796068bd71783051" }, "imp": { "category": "people", "moji": "👿", + "description": "imp", "unicodeVersion": "6.0", "digest": "52598cf2441988f875ccb4e479637baefc679e3ca64e9a6400e56488b0fde811" }, "inbox_tray": { "category": "objects", "moji": "📥", + "description": "inbox tray", "unicodeVersion": "6.0", "digest": "d5d9497022b5318fcfbfdfcd56df9c65dd8f4a4cb5e6283ca260836df57da301" }, "incoming_envelope": { "category": "objects", "moji": "📨", + "description": "incoming envelope", "unicodeVersion": "6.0", "digest": "310b7bdcca93452fe10c72c03d0aafa12b98e5d3408896d275d06d3693812c7a" }, "information_desk_person": { "category": "people", "moji": "💁", + "description": "information desk person", "unicodeVersion": "6.0", "digest": "9f12a4a58a650e8e1d3836ef857003c3ccd42ad4203a2479eb95100bf6559064" }, "information_desk_person_tone1": { "category": "people", "moji": "💁🏻", + "description": "information desk person tone 1", "unicodeVersion": "8.0", "digest": "6674f2e059eff7cfd7fd6abc800da37c4f1087feb4ff26c9e4e31aa29fdf9921" }, "information_desk_person_tone2": { "category": "people", "moji": "💁🏼", + "description": "information desk person tone 2", "unicodeVersion": "8.0", "digest": "9983412ecd130b7e9cfb078167016c06fd043b6f9f3c26d21733ca3f059fd109" }, "information_desk_person_tone3": { "category": "people", "moji": "💁🏽", + "description": "information desk person tone 3", "unicodeVersion": "8.0", "digest": "d8907bf47af5722127afca8fc0da587eab33044a6c60a94890983deb8d6f7a66" }, "information_desk_person_tone4": { "category": "people", "moji": "💁🏾", + "description": "information desk person tone 4", "unicodeVersion": "8.0", "digest": "3be086d4edfe9ca8e4a364b4e8d09b81b5b594b5eeb9ffdf6370179fb3118658" }, "information_desk_person_tone5": { "category": "people", "moji": "💁🏿", + "description": "information desk person tone 5", "unicodeVersion": "8.0", "digest": "2fde4e98dd11c5c29c89cad7cbb7bd2d5077dfad07913b20e01955b2d0dfad40" }, "information_source": { "category": "symbols", "moji": "ℹ", + "description": "information source", "unicodeVersion": "3.0", "digest": "b6bf3cce86d42c2e3c46470baab4af01e900b8ae337b605c3da07c3eba671269" }, "innocent": { "category": "people", "moji": "😇", + "description": "smiling face with halo", "unicodeVersion": "6.0", "digest": "20f8d856bc3e46f4b1173cea05d4577e1c61f06b2daba46e57db90f4066bb428" }, "interrobang": { "category": "symbols", "moji": "⁉", + "description": "exclamation question mark", "unicodeVersion": "3.0", "digest": "92a2d5b4c0bd6714e402f6f12fe19774cb41d081b5e9c23c415ce794224d8117" }, "iphone": { "category": "objects", "moji": "📱", + "description": "mobile phone", "unicodeVersion": "6.0", "digest": "1ebc54215713cd4bf1c1e50770999f2512bb4fea29e37d0bb3a8aa2460ff875d" }, "island": { "category": "travel", "moji": "🏝", + "description": "desert island", "unicodeVersion": "7.0", "digest": "7f9eb5c0cd865762f7a0f187e09c1be442de7010e7c2e113d56aae998597c90d" }, "izakaya_lantern": { "category": "objects", "moji": "🏮", + "description": "izakaya lantern", "unicodeVersion": "6.0", "digest": "fbdc290e666d43d0776a73b955c26df4518692b35e72742e073705fc4ca2ae88" }, "jack_o_lantern": { "category": "nature", "moji": "🎃", + "description": "jack-o-lantern", "unicodeVersion": "6.0", "digest": "78d666c2e80f64bfb6796f53e5ba4960a83ec36192110e8661031bee2b5e370a" }, "japan": { "category": "travel", "moji": "🗾", + "description": "silhouette of japan", "unicodeVersion": "6.0", "digest": "e7d9d6ebf9047fdd3c52e074ba259659c6d8e51a6abae3cdb8d6cf6dbf9a93fe" }, "japanese_castle": { "category": "travel", "moji": "🏯", + "description": "japanese castle", "unicodeVersion": "6.0", "digest": "938ae132c403330288223b88d28c19a47224d4f254fbc2366ecef73d9633112c" }, "japanese_goblin": { "category": "people", "moji": "👺", + "description": "japanese goblin", "unicodeVersion": "6.0", "digest": "63d4bcf58b9d0c29612994432aad2ae35819fdd2890674e60a2f1d51601b742e" }, "japanese_ogre": { "category": "people", "moji": "👹", + "description": "japanese ogre", "unicodeVersion": "6.0", "digest": "434ceedd102e7dcbc07e086811673dd63659ddf8c3ec4d029a3d759a0abfcbdb" }, "jeans": { "category": "people", "moji": "👖", + "description": "jeans", "unicodeVersion": "6.0", "digest": "f986ad32e419cca81c995f8371f0189d1490172a97ebbeac60054a1af08949c5" }, "joy": { "category": "people", "moji": "😂", + "description": "face with tears of joy", "unicodeVersion": "6.0", "digest": "75d7a05043523d290c46d3b313b19ed3c95271f1110bcf234cf13d4273625b08" }, "joy_cat": { "category": "people", "moji": "😹", + "description": "cat face with tears of joy", "unicodeVersion": "6.0", "digest": "a65c999604147e5e20170fcb14f80a1ff0a633f991492e1f790b2ad4caec7b7e" }, "joystick": { "category": "objects", "moji": "🕹", + "description": "joystick", "unicodeVersion": "7.0", "digest": "671ee588f397a96f27056a67e6a06d6e8d22c2109ec57b2859badb5fec9cf8dd" }, "juggling": { "category": "activity", "moji": "🤹", + "description": "juggling", "unicodeVersion": "9.0", "digest": "1f5dafa78de8b37f3df88fdf3084d2380666bd74ab2f449754d8724f6f8dbfa5" }, "juggling_tone1": { "category": "activity", "moji": "🤹🏻", + "description": "juggling tone 1", "unicodeVersion": "9.0", "digest": "b0b4d020148c896be69c28b08e3c486f6db270d138c7ccf4be362b29eb99878d" }, "juggling_tone2": { "category": "activity", "moji": "🤹🏼", + "description": "juggling tone 2", "unicodeVersion": "9.0", "digest": "cfe0c1649b2fdca03673e0e64f3a7d06d4bd49b8954c769aeb7eb88b70ec99f4" }, "juggling_tone3": { "category": "activity", "moji": "🤹🏽", + "description": "juggling tone 3", "unicodeVersion": "9.0", "digest": "7f87022722008bb265abe245e8157dc7a61944f5da62b3cf86f26ee1b3bdef63" }, "juggling_tone4": { "category": "activity", "moji": "🤹🏾", + "description": "juggling tone 4", "unicodeVersion": "9.0", "digest": "1f00da8c05582c95501cc6c3fe5ce0f9bfbc16789dcee59844a8fe7831198583" }, "juggling_tone5": { "category": "activity", "moji": "🤹🏿", + "description": "juggling tone 5", "unicodeVersion": "9.0", "digest": "a195bf734788eb7961c00dbc05255a49da8b9d5042fada29b26cc20393d3ce52" }, "kaaba": { "category": "travel", "moji": "🕋", + "description": "kaaba", "unicodeVersion": "8.0", "digest": "a4618782f9583f077bd383965f1c91b9985a949bb7b6cec7af22914e7f5e9ab6" }, "key": { "category": "objects", "moji": "🔑", + "description": "key", "unicodeVersion": "6.0", "digest": "66719fa77a50a0827c8d47237e2704c03e38186e6fef80627a765473b2294c2e" }, "key2": { "category": "objects", "moji": "🗝", + "description": "old key", "unicodeVersion": "7.0", "digest": "f57240a014a9da5da3d4d98c17d0a55e0ff2e5f2d22731d2fc867105cff54c6e" }, "keyboard": { "category": "objects", "moji": "⌨", + "description": "keyboard", "unicodeVersion": "1.1", "digest": "34da8ff62ca964142f9281b80123dbba74deaac8d77fa61758c30cfb36c31386" }, "kimono": { "category": "people", "moji": "👘", + "description": "kimono", "unicodeVersion": "6.0", "digest": "637182590e256c8fb74ce4c0565f5180c07f06e3bdebf30138ed3259b209c27f" }, "kiss": { "category": "people", "moji": "💋", + "description": "kiss mark", "unicodeVersion": "6.0", "digest": "62f9b9ffcb01558cd5bb829344a1d1d399511663ff5235405c1f786c9416a94d" }, "kiss_mm": { "category": "people", "moji": "👨❤️💋👨", + "description": "kiss (man,man)", "unicodeVersion": "6.0", "digest": "6b0ae32ecb7ec0f0f43dc7a1350711185cce114c52752395f364ddbfb4f1fff4" }, "kiss_ww": { "category": "people", "moji": "👩❤️💋👩", + "description": "kiss (woman,woman)", "unicodeVersion": "6.0", "digest": "6de420cf752e706b1b7e9522b1b9be62eda069cb028c8fd587caf39f6a142e6a" }, "kissing": { "category": "people", "moji": "😗", + "description": "kissing face", "unicodeVersion": "6.1", "digest": "b4a505f9e3d7fbd0ac60111f0e678cf425a5fd1abc65a3e9db59ae4abcfb8e85" }, "kissing_cat": { "category": "people", "moji": "😽", + "description": "kissing cat face with closed eyes", "unicodeVersion": "6.0", "digest": "a00431bf10601db4998e78433279167e52cbd36aed885399482529d5cdab8636" }, "kissing_closed_eyes": { "category": "people", "moji": "😚", + "description": "kissing face with closed eyes", "unicodeVersion": "6.0", "digest": "ae474db7daf80fe0b82ae1f2a11672cfcd9f9126e100f6e6d4b8a0d135dce39d" }, "kissing_heart": { "category": "people", "moji": "😘", + "description": "face throwing a kiss", "unicodeVersion": "6.0", "digest": "bce372573bd3b347b555c1cd22087e03e650df73c8e0284ab668bf6633251632" }, "kissing_smiling_eyes": { "category": "people", "moji": "😙", + "description": "kissing face with smiling eyes", "unicodeVersion": "6.1", "digest": "f0f8636cb1a02b93cc72ce1b194b890fca823d91e35926b889be3ecfae79207f" }, "kiwi": { "category": "food", "moji": "🥝", + "description": "kiwifruit", "unicodeVersion": "9.0", "digest": "70a3a05f333d9455d2da12eed970bc3baae416286848fed8e5dd31b5be0819be" }, "knife": { "category": "objects", "moji": "🔪", + "description": "hocho", "unicodeVersion": "6.0", "digest": "e6189e4843c6e80875b4952fcddb0c858f7c6039b9214bbec6a261a1358425df" }, "koala": { "category": "nature", "moji": "🐨", + "description": "koala", "unicodeVersion": "6.0", "digest": "c58f7e0abae42c2218a85efed0e04151df67187815bebca7f3db6f435e0dab4d" }, "koko": { "category": "symbols", "moji": "🈁", + "description": "squared katakana koko", "unicodeVersion": "6.0", "digest": "5f45eb49bbf298e1fadedfe6cccc297850fcaaa4535e4cc911d48d979af55807" }, "label": { "category": "objects", "moji": "🏷", + "description": "label", "unicodeVersion": "7.0", "digest": "9550ed50cedbc56eb1bd22a8a0809d837048a33d6e2e6e7d65c50d95fa05a85d" }, "large_blue_circle": { "category": "symbols", "moji": "🔵", + "description": "large blue circle", "unicodeVersion": "6.0", "digest": "0df3fb3b09a6269459a3d9a1fe78db572190a948680844cfe758f53b6a482ff4" }, "large_blue_diamond": { "category": "symbols", "moji": "🔷", + "description": "large blue diamond", "unicodeVersion": "6.0", "digest": "7f646b4e9de2788ed09e45f72cb512c269dda4989029b39bf9a2556659321651" }, "large_orange_diamond": { "category": "symbols", "moji": "🔶", + "description": "large orange diamond", "unicodeVersion": "6.0", "digest": "80ae005ef9d79190c777f00de0993f8b3cb783f7051d76e971640c8c0827c338" }, "last_quarter_moon": { "category": "nature", "moji": "🌗", + "description": "last quarter moon symbol", "unicodeVersion": "6.0", "digest": "3d1f276607c685d50f4b70d00a57750a57ad9ad84256dafd2dc8eef8c72300c3" }, "last_quarter_moon_with_face": { "category": "nature", "moji": "🌜", + "description": "last quarter moon with face", "unicodeVersion": "6.0", "digest": "d516825ba52dc67f5a01433fb9df2aa77742d38efde4225983ebc4882cbdfe5d" }, "laughing": { "category": "people", "moji": "😆", + "description": "smiling face with open mouth and tightly-closed ey", "unicodeVersion": "6.0", "digest": "e9ea994b39650740c4961f070ed492d86b3acf6e6a830a6dadaa3a6872e81b81" }, "leaves": { "category": "nature", "moji": "🍃", + "description": "leaf fluttering in wind", "unicodeVersion": "6.0", "digest": "56a7a0e767a6f214d340d1b5989efd99fec52c6aa306ec5c3328e32234a1631b" }, "ledger": { "category": "objects", "moji": "📒", + "description": "ledger", "unicodeVersion": "6.0", "digest": "e58cb714353e96a2891a5d97910ff79660e637af909b81c49c919d3735db55b4" }, "left_facing_fist": { "category": "people", "moji": "🤛", + "description": "left-facing fist", "unicodeVersion": "9.0", "digest": "7861be485beefae0de341df2f21576666e22f63511a033e785752f30c07291da" }, "left_facing_fist_tone1": { "category": "people", "moji": "🤛🏻", + "description": "left facing fist tone 1", "unicodeVersion": "9.0", "digest": "2e4c4dd96b0e4b46fe0f9ce5666344d266d0f17a8544cbae73d96638d1955296" }, "left_facing_fist_tone2": { "category": "people", "moji": "🤛🏼", + "description": "left facing fist tone 2", "unicodeVersion": "9.0", "digest": "b96a63a801175ce98a75f0edad7b5574251a3fbbd894d8ab3f21aeeda366cc13" }, "left_facing_fist_tone3": { "category": "people", "moji": "🤛🏽", + "description": "left facing fist tone 3", "unicodeVersion": "9.0", "digest": "99df84635513c2ebfef24df1bd3705233e02149eef788c7b82ca0548df6f6ea5" }, "left_facing_fist_tone4": { "category": "people", "moji": "🤛🏾", + "description": "left facing fist tone 4", "unicodeVersion": "9.0", "digest": "68954842ca725aec0aa39bce4aa81aad17ac30f5f298561dfa411feb07414cd3" }, "left_facing_fist_tone5": { "category": "people", "moji": "🤛🏿", + "description": "left facing fist tone 5", "unicodeVersion": "9.0", "digest": "a419b33fae82612dc860ff48950c0547a1642d4f0c94b6547324440837d3bb21" }, "left_luggage": { "category": "symbols", "moji": "🛅", + "description": "left luggage", "unicodeVersion": "6.0", "digest": "6625077767a51163ea20cbc299f3c13fd5ccf1b5ce365ee702ef1fef6be3dadf" }, "left_right_arrow": { "category": "symbols", "moji": "↔", + "description": "left right arrow", "unicodeVersion": "1.1", "digest": "560fcf1b794eb0d5269c73b3f8da57540cbb8a6f1a9af7a9d10b202252247e34" }, "leftwards_arrow_with_hook": { "category": "symbols", "moji": "↩", + "description": "leftwards arrow with hook", "unicodeVersion": "1.1", "digest": "504714c5559b1bd35aa469be83069a923d1a25f364cac08c10df0195749e7b26" }, "lemon": { "category": "food", "moji": "🍋", + "description": "lemon", "unicodeVersion": "6.0", "digest": "ccca25bb6ac47770dba3aaf75144128f9a73299061969b25a35ad1733dcde5fe" }, "leo": { "category": "symbols", "moji": "♌", + "description": "leo", "unicodeVersion": "1.1", "digest": "f2ed930e279699962f189e0cac519cc29d339b3e82debfdc90c5b0935a7543bb" }, "leopard": { "category": "nature", "moji": "🐆", + "description": "leopard", "unicodeVersion": "6.0", "digest": "d4a8964b6f2cdf6ddf074d0f1f2f65783a1a43eb4af426905fad0e60899939c7" }, "level_slider": { "category": "objects", "moji": "🎚", + "description": "level slider", "unicodeVersion": "7.0", "digest": "48842324f54d971ebf548a89a82ac7f29e235702081c91b477b1a92d427290e7" }, "levitate": { "category": "activity", "moji": "🕴", + "description": "man in business suit levitating", "unicodeVersion": "7.0", "digest": "453c24bf2544ed3ef3c710a7fabbd5fdace4dc65cddd377274d30d921523b50b" }, "libra": { "category": "symbols", "moji": "♎", + "description": "libra", "unicodeVersion": "1.1", "digest": "e330ba05bb449db074bc23d1514246ca5e249110f44ddb5804e5510eef6deac1" }, "lifter": { "category": "activity", "moji": "🏋", + "description": "weight lifter", "unicodeVersion": "7.0", "digest": "d6c94a32eb863d14a2a01add8ab95040f42a55d9e3f90641a0fe143d58127558" }, "lifter_tone1": { "category": "activity", "moji": "🏋🏻", + "description": "weight lifter tone 1", "unicodeVersion": "8.0", "digest": "870acf2f554fce360b58d3e98b4c0558d7ec7775587776c0f9d40c6fb1bdacf9" }, "lifter_tone2": { "category": "activity", "moji": "🏋🏼", + "description": "weight lifter tone 2", "unicodeVersion": "8.0", "digest": "1a7ece8512e42241cdd95c85ccc509bc0ff9c7c6ffaff2be343c77f417a27576" }, "lifter_tone3": { "category": "activity", "moji": "🏋🏽", + "description": "weight lifter tone 3", "unicodeVersion": "8.0", "digest": "4bc633ee82a0fb59feba379fb6901a489e4ac849d758f9c8e7a1a0a26eaa380c" }, "lifter_tone4": { "category": "activity", "moji": "🏋🏾", + "description": "weight lifter tone 4", "unicodeVersion": "8.0", "digest": "d086fe5577b5ba80676f2224d886f8ebe4588314f429f12a34c52c971ed71b5c" }, "lifter_tone5": { "category": "activity", "moji": "🏋🏿", + "description": "weight lifter tone 5", "unicodeVersion": "8.0", "digest": "79b0edf6ce1fd024dd7f458e322ad8588af0b789a04cc1cf38380dc8b9c76f55" }, "light_rail": { "category": "travel", "moji": "🚈", + "description": "light rail", "unicodeVersion": "6.0", "digest": "2f30b23a738371690b2f00d96ddb5ceb90a1442b5478754626a3dfa263ed2fc1" }, "link": { "category": "objects", "moji": "🔗", + "description": "link symbol", "unicodeVersion": "6.0", "digest": "7bf567aabd1fc38b3d70422f9db3a13b50950cf6207e70962c9938827c196ccb" }, "lion_face": { "category": "nature", "moji": "🦁", + "description": "lion face", "unicodeVersion": "8.0", "digest": "dd24f2668e973ec973e97dc111f59a2cc14e9b608387401191dd53368d28d4fa" }, "lips": { "category": "people", "moji": "👄", + "description": "mouth", "unicodeVersion": "6.0", "digest": "8740d8086525c7a836d64625a6915cc1c59af69ba143456dbb59e0179276895e" }, "lipstick": { "category": "people", "moji": "💄", + "description": "lipstick", "unicodeVersion": "6.0", "digest": "751dcb22706a796033b13a2ccb94304236ec13207ad4d011e02d230ae33ab5c1" }, "lizard": { "category": "nature", "moji": "🦎", + "description": "lizard", "unicodeVersion": "9.0", "digest": "fb9191f9eab58b8403d4c4626ccbb14ba05c1f6944011751a8edcc4dd03c66e6" }, "lock": { "category": "objects", "moji": "🔒", + "description": "lock", "unicodeVersion": "6.0", "digest": "043b4fc0b8c79d47a07d91308e628e1ac262aea6c1ec05e6b84bf7bcdf89dc83" }, "lock_with_ink_pen": { "category": "objects", "moji": "🔏", + "description": "lock with ink pen", "unicodeVersion": "6.0", "digest": "7b5e959b26cf7296c7b230fc2be9feb9e38391c5001951a019d16b169a71aba9" }, "lollipop": { "category": "food", "moji": "🍭", + "description": "lollipop", "unicodeVersion": "6.0", "digest": "17b6a0df47ec758a2f9c087b46a6902cee344d39407ef4c321e408505cbb72ca" }, "loop": { "category": "symbols", "moji": "➿", + "description": "double curly loop", "unicodeVersion": "6.0", "digest": "9f20ecc34b3c871789ba7d0712aa31e7a74b6c1558ac8bea385bc40590056726" }, "loud_sound": { "category": "symbols", "moji": "🔊", + "description": "speaker with three sound waves", "unicodeVersion": "6.0", "digest": "64b12db9ddd8adf74a9fc2bd83c7979ea865113347f7ce8666e9ccf5019e715f" }, "loudspeaker": { "category": "symbols", "moji": "📢", + "description": "public address loudspeaker", "unicodeVersion": "6.0", "digest": "1e1f35d16dd2898ebaa6f2b2868203df6e09c8a70df069c92d6d1b5cb2ac0976" }, "love_hotel": { "category": "travel", "moji": "🏩", + "description": "love hotel", "unicodeVersion": "6.0", "digest": "ff8966a50fd47a216855488eb09a367d231fea21f49e7e5325191d32fb494473" }, "love_letter": { "category": "objects", "moji": "💌", + "description": "love letter", "unicodeVersion": "6.0", "digest": "037261c8ca4d72f7205e51664591696da2ae7ceb19f1c1c9f6123da5a5979d29" }, "low_brightness": { "category": "symbols", "moji": "🔅", + "description": "low brightness symbol", "unicodeVersion": "6.0", "digest": "a065d00a416e297c168b0a675cafcf492fedf94865cb21801a1be5a3914593d4" }, "lying_face": { "category": "people", "moji": "🤥", + "description": "lying face", "unicodeVersion": "9.0", "digest": "ce836170165e1b70938273f289c02c2106873cd9ab5472dbcd487c2f9f53f13d" }, "m": { "category": "symbols", "moji": "Ⓜ", + "description": "circled latin capital letter m", "unicodeVersion": "1.1", "digest": "54588ac2b7fcd53a96f17124e9de69b617613fcd5af9ad2930a094cb795bb9f4" }, "mag": { "category": "objects", "moji": "🔍", + "description": "left-pointing magnifying glass", "unicodeVersion": "6.0", "digest": "a6e31a2efa7d9427aaa30b45d9f4181ee55c44be08aea2df165a86e0e6d9eaa1" }, "mag_right": { "category": "objects", "moji": "🔎", + "description": "right-pointing magnifying glass", "unicodeVersion": "6.0", "digest": "c7d8ceeb05db261e5eaab31dc4da432d0d5592a2ed71e526c5a542daa230bbaf" }, "mahjong": { "category": "symbols", "moji": "🀄", + "description": "mahjong tile red dragon", "unicodeVersion": "5.1", "digest": "755d69f988434ce1c17531a8b7ac92ead6f5607c2635a22f10e0ad70f09fc3e6" }, "mailbox": { "category": "objects", "moji": "📫", + "description": "closed mailbox with raised flag", "unicodeVersion": "6.0", "digest": "2069091be90a530a43ef29d5ec7688c351bf4d5b08d63a0d20d72b67d639ec62" }, "mailbox_closed": { "category": "objects", "moji": "📪", + "description": "closed mailbox with lowered flag", "unicodeVersion": "6.0", "digest": "d88d65bfebb8216535fd055c69f319564b2cf0b0901820f8312f581864557ed4" }, "mailbox_with_mail": { "category": "objects", "moji": "📬", + "description": "open mailbox with raised flag", "unicodeVersion": "6.0", "digest": "69e966b4659128991a70c6a2dd4d647551bedb91bdf5ce688958686bbec56381" }, "mailbox_with_no_mail": { "category": "objects", "moji": "📭", + "description": "open mailbox with lowered flag", "unicodeVersion": "6.0", "digest": "9e92d8ee88f660ce56da61077c80ec26c5d8f54ebd2306c4cfa16f6c1b981f83" }, "man": { "category": "people", "moji": "👨", + "description": "man", "unicodeVersion": "6.0", "digest": "42b882d2c6aa095f1afcf901203838d95c1908bdc725519779186b9c33c728d7" }, "man_dancing": { "category": "people", "moji": "🕺", + "description": "man dancing", "unicodeVersion": "9.0", "digest": "9f632ee0c886d5f03c61e5f3a27668262c0cc2693b857a91c23c1e5ea3785b9e" }, "man_dancing_tone1": { "category": "activity", "moji": "🕺🏻", + "description": "man dancing tone 1", "unicodeVersion": "9.0", "digest": "6c56a16cb105bcdd97472645b3a351cebdbb1132cbfd18b0118f289db5fbe741" }, "man_dancing_tone2": { "category": "activity", "moji": "🕺🏼", + "description": "man dancing tone 2", "unicodeVersion": "9.0", "digest": "ed7e78c14d205a03fdd5581e5213add69a55e13b4cbaf76a6d5a0d6c80f53327" }, "man_dancing_tone3": { "category": "activity", "moji": "🕺🏽", + "description": "man dancing tone 3", "unicodeVersion": "9.0", "digest": "13b45403e11800163406206eedeb8b579cc83eca2f60246be97e099164387bc8" }, "man_dancing_tone4": { "category": "activity", "moji": "🕺🏾", + "description": "man dancing tone 4", "unicodeVersion": "9.0", "digest": "f6feb1b0b83565fadcdd1a8737d3daa08893e919547d2a06de899160162d9c4a" }, "man_dancing_tone5": { "category": "activity", "moji": "🕺🏿", + "description": "man dancing tone 5", "unicodeVersion": "9.0", "digest": "fe20a9ed9ba991653b4d0683de347ed7c226a5d75610307584a2ddd6fcd1e3f2" }, "man_in_tuxedo": { "category": "people", "moji": "🤵", + "description": "man in tuxedo", "unicodeVersion": "9.0", "digest": "4d451a971dfefedc4830ba78e19b123f250e09ae65baddccdc56c0f8aa3a9b50" }, "man_in_tuxedo_tone1": { "category": "people", "moji": "🤵🏻", + "description": "man in tuxedo tone 1", "unicodeVersion": "9.0", "digest": "2814833334fb211ae2ecb1fb5964e9752282d0fb4d7f3477de5dd2a4f812a793" }, "man_in_tuxedo_tone2": { "category": "people", "moji": "🤵🏼", + "description": "man in tuxedo tone 2", "unicodeVersion": "9.0", "digest": "cd1bab9ee0e2335d3cd99d51216cccdc4fc3c2cf20129b8b7e11a51a77258f68" }, "man_in_tuxedo_tone3": { "category": "people", "moji": "🤵🏽", + "description": "man in tuxedo tone 3", "unicodeVersion": "9.0", "digest": "f387775f925fe60b9f3e7cad63a55d4d196ddd41658029a70440d14c17cb99f9" }, "man_in_tuxedo_tone4": { "category": "people", "moji": "🤵🏾", + "description": "man in tuxedo tone 4", "unicodeVersion": "9.0", "digest": "08debd7a573d1201aee8a2f281ef7cb638d4a2a096222150391f36963f07c622" }, "man_in_tuxedo_tone5": { "category": "people", "moji": "🤵🏿", + "description": "man in tuxedo tone 5", "unicodeVersion": "9.0", "digest": "e3b10e0619f0911cf9b665a265f4ef829b8f6ba6e9c3a021d0539a27e315f8fe" }, "man_tone1": { "category": "people", "moji": "👨🏻", + "description": "man tone 1", "unicodeVersion": "8.0", "digest": "7053e265fa7d2594de54a6c5d06c21795b9a7dfb36a1c5594ca43c4c6cc56504" }, "man_tone2": { "category": "people", "moji": "👨🏼", + "description": "man tone 2", "unicodeVersion": "8.0", "digest": "7ebc64de40d3ac60fb761be5cf94f53fa10b4f03fb66add46c90f5d98eaf71eb" }, "man_tone3": { "category": "people", "moji": "👨🏽", + "description": "man tone 3", "unicodeVersion": "8.0", "digest": "77ceef4d3740ed4751acb83dd45b6b754cf625c522c6757309cd4d61202d7149" }, "man_tone4": { "category": "people", "moji": "👨🏾", + "description": "man tone 4", "unicodeVersion": "8.0", "digest": "41e6037c393f61cca61b9a81b27ed14a95d75fe380e3a00153c33a371a836ffd" }, "man_tone5": { "category": "people", "moji": "👨🏿", + "description": "man tone 5", "unicodeVersion": "8.0", "digest": "a8cebfd39a5b9c79af7cc37f205e1135376056fee287af967c9f55d415572d99" }, "man_with_gua_pi_mao": { "category": "people", "moji": "👲", + "description": "man with gua pi mao", "unicodeVersion": "6.0", "digest": "3dae285e900c69986a48db0fa89d4f371a49f38608059cdae52be098030c5ac4" }, "man_with_gua_pi_mao_tone1": { "category": "people", "moji": "👲🏻", + "description": "man with gua pi mao tone 1", "unicodeVersion": "8.0", "digest": "35404d8e266920c78edd9e7143fb052b42f65242a5698494c4f4365e9183cc67" }, "man_with_gua_pi_mao_tone2": { "category": "people", "moji": "👲🏼", + "description": "man with gua pi mao tone 2", "unicodeVersion": "8.0", "digest": "82d4f968665a93c7543372c8a1eeb0f25d0ea6842d5e518bd91c226c6c3ab8c2" }, "man_with_gua_pi_mao_tone3": { "category": "people", "moji": "👲🏽", + "description": "man with gua pi mao tone 3", "unicodeVersion": "8.0", "digest": "f44159f0c672b9b833449382896180e799abf574f5b3c6cd9541caa992fa18ce" }, "man_with_gua_pi_mao_tone4": { "category": "people", "moji": "👲🏾", + "description": "man with gua pi mao tone 4", "unicodeVersion": "8.0", "digest": "c79060188f9461ca34eaa225b7682d8c410883609509fb731c992db69bfeeb50" }, "man_with_gua_pi_mao_tone5": { "category": "people", "moji": "👲🏿", + "description": "man with gua pi mao tone 5", "unicodeVersion": "8.0", "digest": "de9e4acdb10f7abddeeabc0b48d91139fc8b544a601c530db811f099991b0d38" }, "man_with_turban": { "category": "people", "moji": "👳", + "description": "man with turban", "unicodeVersion": "6.0", "digest": "db72c944e93983f38d00e3e936ebb5b243c6069f1f1236d46f6a9f1beb8d6634" }, "man_with_turban_tone1": { "category": "people", "moji": "👳🏻", + "description": "man with turban tone 1", "unicodeVersion": "8.0", "digest": "b6d7489c4cd151af09fff48b62c54c336303e14866e6ef38f94cd834b085d09e" }, "man_with_turban_tone2": { "category": "people", "moji": "👳🏼", + "description": "man with turban tone 2", "unicodeVersion": "8.0", "digest": "7854ef973c21847f452d7e78e5c460ea300e12b539ce92c69dabe8f1bf3a4382" }, "man_with_turban_tone3": { "category": "people", "moji": "👳🏽", + "description": "man with turban tone 3", "unicodeVersion": "8.0", "digest": "1dbd9bd78f5263cbadee7d0d5754c14cfbc914f7329e25fbd97d9f5b8ce0737e" }, "man_with_turban_tone4": { "category": "people", "moji": "👳🏾", + "description": "man with turban tone 4", "unicodeVersion": "8.0", "digest": "4f4804da4a7c98ad4f9db3ae3eaf674c8977c638e73414e33ef1f65098e413a3" }, "man_with_turban_tone5": { "category": "people", "moji": "👳🏿", + "description": "man with turban tone 5", "unicodeVersion": "8.0", "digest": "240282aa346ef9b1d0d475ea93a02597697f0f56f086305879b532b0b933210a" }, "mans_shoe": { "category": "people", "moji": "👞", + "description": "mans shoe", "unicodeVersion": "6.0", "digest": "f53fe74abd9906cd3e2dd7e7bddbe1feb9f8f7be28b807fabe452f1f60ca1b84" }, "map": { "category": "objects", "moji": "🗺", + "description": "world map", "unicodeVersion": "7.0", "digest": "84f496a062b5c3ae1e8013506175a69036038c8130891bcf780a69ce7fcbe4de" }, "maple_leaf": { "category": "nature", "moji": "🍁", + "description": "maple leaf", "unicodeVersion": "6.0", "digest": "72629a205e33f89337815ad7e51bb5c73947d1a9f98afe5072bdf4846827ae72" }, "martial_arts_uniform": { "category": "activity", "moji": "🥋", + "description": "martial arts uniform", "unicodeVersion": "9.0", "digest": "a1ae797b31081425b388ab31efc635d8eb73a40980fd0fae4708aa5313e2a964" }, "mask": { "category": "people", "moji": "😷", + "description": "face with medical mask", "unicodeVersion": "6.0", "digest": "1b58af9ae599308aabf41bbd38f599fa896bd9fe5df7a40be9f2dc7e0e230600" }, "massage": { "category": "people", "moji": "💆", + "description": "face massage", "unicodeVersion": "6.0", "digest": "6ee48b4d8cec0bf31e11d7803ad9fc1f909457c8c00cb320b5671395af3c170c" }, "massage_tone1": { "category": "people", "moji": "💆🏻", + "description": "face massage tone 1", "unicodeVersion": "8.0", "digest": "9da162c2f39628156b87db986a6ada59372a9e9a6b3f0488d21c9e65ec3309bb" }, "massage_tone2": { "category": "people", "moji": "💆🏼", + "description": "face massage tone 2", "unicodeVersion": "8.0", "digest": "ac259188549b5b429b8c4929e1da2314859e8857ee49720551467aedfcc96567" }, "massage_tone3": { "category": "people", "moji": "💆🏽", + "description": "face massage tone 3", "unicodeVersion": "8.0", "digest": "cfd9c105b6debc10448f172afcb20d4192899f7ae5aa8af54c834153a5466364" }, "massage_tone4": { "category": "people", "moji": "💆🏾", + "description": "face massage tone 4", "unicodeVersion": "8.0", "digest": "38ab715c621c58454f3cb09153a96380118cf082568554b6edc5f83fb62e9297" }, "massage_tone5": { "category": "people", "moji": "💆🏿", + "description": "face massage tone 5", "unicodeVersion": "8.0", "digest": "32480457734121b0c83e9be6d693ae379c95535f43f963c0c2f0f20434ee12c6" }, "meat_on_bone": { "category": "food", "moji": "🍖", + "description": "meat on bone", "unicodeVersion": "6.0", "digest": "d71a8e0b118d5e6ca60690793ce9649afb78e707fcbd7be890a75564c94434fd" }, "medal": { "category": "activity", "moji": "🏅", + "description": "sports medal", "unicodeVersion": "7.0", "digest": "9600cbe57e08da090c60629bcafd2821c87322e738c2454f8e883ceb756e7391" }, "mega": { "category": "symbols", "moji": "📣", + "description": "cheering megaphone", "unicodeVersion": "6.0", "digest": "4b1def6b5b051c5045514063f0ac006222ad81fbfe56d840e14bb950713e331b" }, "melon": { "category": "food", "moji": "🍈", + "description": "melon", "unicodeVersion": "6.0", "digest": "0cdd663e6f2129808856cdf0746e6571b62aac641f224adb553baf3bb63ba3bd" }, "menorah": { "category": "symbols", "moji": "🕎", + "description": "menorah with nine branches", "unicodeVersion": "8.0", "digest": "49fca8c3bc00ea69653ee2f8d4e21e561856ba39716c13e9d107db3e805a2997" }, "mens": { "category": "symbols", "moji": "🚹", + "description": "mens symbol", "unicodeVersion": "6.0", "digest": "7d92292586ee12a5d1a557c37da4d14708dc3ce701cf32d3280dcc83d91e5df8" }, "metal": { "category": "people", "moji": "🤘", + "description": "sign of the horns", "unicodeVersion": "8.0", "digest": "ffb750caf187f5d821c990108e2699ac3e216492bcff6ee543f4a7aa55b9fd29" }, "metal_tone1": { "category": "people", "moji": "🤘🏻", + "description": "sign of the horns tone 1", "unicodeVersion": "8.0", "digest": "5505f0b0340f9ba572db8897e40adf598cfa784686ad5ee360a7351bf44ddc1d" }, "metal_tone2": { "category": "people", "moji": "🤘🏼", + "description": "sign of the horns tone 2", "unicodeVersion": "8.0", "digest": "8f9eee3ad5fc7eeeb30118d16d27467b16fd87297e0ecf02656db77e701f5aeb" }, "metal_tone3": { "category": "people", "moji": "🤘🏽", + "description": "sign of the horns tone 3", "unicodeVersion": "8.0", "digest": "8270a7ecf5eb11431a07ef04cc476c2651ac8aacb0d4768e5cb69355f8a5e84e" }, "metal_tone4": { "category": "people", "moji": "🤘🏾", + "description": "sign of the horns tone 4", "unicodeVersion": "8.0", "digest": "f24f7b137dd6c7899dc0a8794204bbde7ad43ec1e63b419c90dd70a8b77871e8" }, "metal_tone5": { "category": "people", "moji": "🤘🏿", + "description": "sign of the horns tone 5", "unicodeVersion": "8.0", "digest": "07b0726a632653b980df775f460cd3fe1ea8d4a7b0b46fe29e089b66579482d2" }, "metro": { "category": "travel", "moji": "🚇", + "description": "metro", "unicodeVersion": "6.0", "digest": "b380247b61b5e2ca1b9b70fabff65907b2c3a5191a14b169ae094af94659b9b1" }, "microphone": { "category": "activity", "moji": "🎤", + "description": "microphone", "unicodeVersion": "6.0", "digest": "9ef4fc2e40d5391c4bb2d30f34f59662cff7cbb1b04341c9dac210d0e21b44ae" }, "microphone2": { "category": "objects", "moji": "🎙", + "description": "studio microphone", "unicodeVersion": "7.0", "digest": "8a30464d51f7f101335778444c43270ac0679900f49463e6556682d9db1cb4dc" }, "microscope": { "category": "objects", "moji": "🔬", + "description": "microscope", "unicodeVersion": "6.0", "digest": "4ca4322c6ba99b8c15acdb8b605f84f87398769e504b262b134c1f3868b2692f" }, "middle_finger": { "category": "people", "moji": "🖕", + "description": "reversed hand with middle finger extended", "unicodeVersion": "7.0", "digest": "0c3f1cc0ec7323f6d19508ad22fa90050845f7b5cc83f599ab2cacb89cf5dd0e" }, "middle_finger_tone1": { "category": "people", "moji": "🖕🏻", + "description": "reversed hand with middle finger extended tone 1", "unicodeVersion": "8.0", "digest": "4ebecf1058a3059aaa826eaad39c1a791120f115f65dde6d6ae32fc5561f60f7" }, "middle_finger_tone2": { "category": "people", "moji": "🖕🏼", + "description": "reversed hand with middle finger extended tone 2", "unicodeVersion": "8.0", "digest": "85ff506a08c38663c2dfa2e3a90584c02a36aa3dda33af47cdb49834bf9baf83" }, "middle_finger_tone3": { "category": "people", "moji": "🖕🏽", + "description": "reversed hand with middle finger extended tone 3", "unicodeVersion": "8.0", "digest": "cac697ff5207bf8a4e091912f3127f4e73c88ef69b5c6561d1d7b12ed60be8f1" }, "middle_finger_tone4": { "category": "people", "moji": "🖕🏾", + "description": "reversed hand with middle finger extended tone 4", "unicodeVersion": "8.0", "digest": "9324a5a4e3986b798ad8c61f31c18fb507ca7a4abfd6e9ae1408b80b185bf8c7" }, "middle_finger_tone5": { "category": "people", "moji": "🖕🏿", + "description": "reversed hand with middle finger extended tone 5", "unicodeVersion": "8.0", "digest": "078f917cd4d8be08a880724e9400449980d92740ccbee4a57f5046a9cf7f6575" }, "military_medal": { "category": "activity", "moji": "🎖", + "description": "military medal", "unicodeVersion": "7.0", "digest": "5da18351dc14b66cfc070148c83b7c8e67e6b1e3f515ae501133c38ee5c28d3d" }, "milk": { "category": "food", "moji": "🥛", + "description": "glass of milk", "unicodeVersion": "9.0", "digest": "38b28ea40399601fabc95bac5eaaf5a9e4e25548ec80325bd5069395ea884f85" }, "milky_way": { "category": "travel", "moji": "🌌", + "description": "milky way", "unicodeVersion": "6.0", "digest": "17405ff31d94b13a1fb0adcda204b8adb95ca340bc3980d9ad9f42ba1e366e7d" }, "minibus": { "category": "travel", "moji": "🚐", + "description": "minibus", "unicodeVersion": "6.0", "digest": "08ccb4b1bf397b7c9aed901e2b5dcdd6cb8ca5c5487ef26775bb3120f7b92524" }, "minidisc": { "category": "objects", "moji": "💽", + "description": "minidisc", "unicodeVersion": "6.0", "digest": "bebf82c0b91ef66321e7ae7a0abf322e59b2f7d8e6fbf9a94243210c00229c59" }, "mobile_phone_off": { "category": "symbols", "moji": "📴", + "description": "mobile phone off", "unicodeVersion": "6.0", "digest": "6f9d8d6a32fc998f5d8144a5ff7e2ad00de37ad464cd97285e7c72efb09a1feb" }, "money_mouth": { "category": "people", "moji": "🤑", + "description": "money-mouth face", "unicodeVersion": "8.0", "digest": "5a43973dadf48a89201b1816fea9972c5cfe501a26fe457b6f7eee0a6362018e" }, "money_with_wings": { "category": "objects", "moji": "💸", + "description": "money with wings", "unicodeVersion": "6.0", "digest": "15fcf0595021374ba091ca00efdb4167770da4d421eab930964108545f4edab9" }, "moneybag": { "category": "objects", "moji": "💰", + "description": "money bag", "unicodeVersion": "6.0", "digest": "02d708e2f603b0df6f6c169b5c49b3452e1c02e7d72e96f228b73d0b0a20bff4" }, "monkey": { "category": "nature", "moji": "🐒", + "description": "monkey", "unicodeVersion": "6.0", "digest": "3588a544d6d9e9995b45d60327a1a42002fa1faa4d48224b140facd249af1c67" }, "monkey_face": { "category": "nature", "moji": "🐵", + "description": "monkey face", "unicodeVersion": "6.0", "digest": "9e263ef5ca42bb76d1b1d1e3cbf020bcf05023a6e9f91301d30c9eb406363a2a" }, "monorail": { "category": "travel", "moji": "🚝", + "description": "monorail", "unicodeVersion": "6.0", "digest": "2c9f185babcb4001fcef2b8dfc4a32126729843084d0076c3e3ccdc845ab23ad" }, "mortar_board": { "category": "people", "moji": "🎓", + "description": "graduation cap", "unicodeVersion": "6.0", "digest": "d7fbe41d4b340d3564e484aec46a22c9613521414b2ba6eece2180db4d23e410" }, "mosque": { "category": "travel", "moji": "🕌", + "description": "mosque", "unicodeVersion": "8.0", "digest": "5f3d3de7feac953a70a318113531c2857d760a516c3d8d6f42d2a3b3b67ed196" }, "motor_scooter": { "category": "travel", "moji": "🛵", + "description": "motor scooter", "unicodeVersion": "9.0", "digest": "e2dc7c981744a71f46858bd0858ff91af704ac06425ed80377bc3b119e57c872" }, "motorboat": { "category": "travel", "moji": "🛥", + "description": "motorboat", "unicodeVersion": "7.0", "digest": "81c156643528c5a94a12d6d478e52a019f5a4e3eb58ee365cdd9d2361a7fdb01" }, "motorcycle": { "category": "travel", "moji": "🏍", + "description": "racing motorcycle", "unicodeVersion": "7.0", "digest": "354aa8157732184ad50eff9330f7a8915309dc9b7893cc308226adb429311a62" }, "motorway": { "category": "travel", "moji": "🛣", + "description": "motorway", "unicodeVersion": "7.0", "digest": "148c3c13c7c4565453d16e504e0d4b8d007e4f2cad1ab56b1b51fefe39162d17" }, "mount_fuji": { "category": "travel", "moji": "🗻", + "description": "mount fuji", "unicodeVersion": "6.0", "digest": "f8093b9dba62b22c6c88f137be88b2fd3971c560714db15ec053cf697a3820bc" }, "mountain": { "category": "travel", "moji": "⛰", + "description": "mountain", "unicodeVersion": "5.2", "digest": "07423804ad79da68f140948d29df193f5d5343b7b2c23758c086697c4d3a50da" }, "mountain_bicyclist": { "category": "activity", "moji": "🚵", + "description": "mountain bicyclist", "unicodeVersion": "6.0", "digest": "91084b6c887cb7e34f3d7ec30656ecb82c36cc987f53a6c83ccb4c6f7950f96a" }, "mountain_bicyclist_tone1": { "category": "activity", "moji": "🚵🏻", + "description": "mountain bicyclist tone 1", "unicodeVersion": "8.0", "digest": "5d57fcfad61bca26c3e8965eb57602a1993a3117ebdda0f24569af730310ab6e" }, "mountain_bicyclist_tone2": { "category": "activity", "moji": "🚵🏼", + "description": "mountain bicyclist tone 2", "unicodeVersion": "8.0", "digest": "c0da7fb85d99aa01a665f64063cd7e2d994f8a16d3f6fbf52df5d471e771a98a" }, "mountain_bicyclist_tone3": { "category": "activity", "moji": "🚵🏽", + "description": "mountain bicyclist tone 3", "unicodeVersion": "8.0", "digest": "b099e7ee84eae44ebc99023fa06bdf37ffa0d69767c7c0163a89f7ced2a26765" }, "mountain_bicyclist_tone4": { "category": "activity", "moji": "🚵🏾", + "description": "mountain bicyclist tone 4", "unicodeVersion": "8.0", "digest": "9d09f7b3899ea44e736f237a161ef8d5170dccfa162a872c59532ceaf65ee007" }, "mountain_bicyclist_tone5": { "category": "activity", "moji": "🚵🏿", + "description": "mountain bicyclist tone 5", "unicodeVersion": "8.0", "digest": "71e374981d955056748a60c6d1820b45e9688a156b55318b4ea54a3a67ca801c" }, "mountain_cableway": { "category": "travel", "moji": "🚠", + "description": "mountain cableway", "unicodeVersion": "6.0", "digest": "e261c3292758b1c0063c5a0d0c7f5c9803306d2265e08677027e1210506ced94" }, "mountain_railway": { "category": "travel", "moji": "🚞", + "description": "mountain railway", "unicodeVersion": "6.0", "digest": "b0987f8f391b3cbc7a56b9b8945ebfca240e01d12f8fd163877ebebe51d6b277" }, "mountain_snow": { "category": "travel", "moji": "🏔", + "description": "snow capped mountain", "unicodeVersion": "7.0", "digest": "49aac2b851aa6f2bd2ca641efa8060f93e89395357f49d211658d46f5a2b0189" }, "mouse": { "category": "nature", "moji": "🐭", + "description": "mouse face", "unicodeVersion": "6.0", "digest": "007dd108507b45224f7a1fad3c1de6ecc75f38d71fc142744611eb13555f5eff" }, "mouse2": { "category": "nature", "moji": "🐁", + "description": "mouse", "unicodeVersion": "6.0", "digest": "f3ed37b639b7c16aae49502bd423f9fdeabaf15bc6f0f74063954b189e176b5d" }, "mouse_three_button": { "category": "objects", "moji": "🖱", + "description": "three button mouse", "unicodeVersion": "7.0", "digest": "3724341ac5ad0d01027ef1575db64f1db7619f590ca6ada960d1f2c18dc7fc6a" }, "movie_camera": { "category": "objects", "moji": "🎥", + "description": "movie camera", "unicodeVersion": "6.0", "digest": "f7e285eda35b4431c07951e071643ddc34147cd76640e0d516fbfd11208346e9" }, "moyai": { "category": "objects", "moji": "🗿", + "description": "moyai", "unicodeVersion": "6.0", "digest": "2c1d0662c95928936e6b9ab5a40c6110ff1cea5339f2803c7b63aabc76115afb" }, "mrs_claus": { "category": "people", "moji": "🤶", + "description": "mother christmas", "unicodeVersion": "9.0", "digest": "1f72f586ca75bd7ebb4150cdcc8199a930c32fa4b81510cb8d200f1b3ddd4076" }, "mrs_claus_tone1": { "category": "people", "moji": "🤶🏻", + "description": "mother christmas tone 1", "unicodeVersion": "9.0", "digest": "244596919e0fed050203cf9e040899de323d7821235929f175852439927bd129" }, "mrs_claus_tone2": { "category": "people", "moji": "🤶🏼", + "description": "mother christmas tone 2", "unicodeVersion": "9.0", "digest": "8cde96e8521f3a90262a7f5f8a2989a9590d9a02cda2c37e92335dc05975c18d" }, "mrs_claus_tone3": { "category": "people", "moji": "🤶🏽", + "description": "mother christmas tone 3", "unicodeVersion": "9.0", "digest": "c39cd4346d4581799dd0e9a6447c91a954a75747bf2682c8e4d79c3b0fcf7405" }, "mrs_claus_tone4": { "category": "people", "moji": "🤶🏾", + "description": "mother christmas tone 4", "unicodeVersion": "9.0", "digest": "84c85cf54559ea2d78d196fee96149a249af4f959b78e223a0ec4fb72abdbcab" }, "mrs_claus_tone5": { "category": "people", "moji": "🤶🏿", + "description": "mother christmas tone 5", "unicodeVersion": "9.0", "digest": "ce26c0e0645713b17e7497d9f2d0484cc5477564dae99320cabf04d160d3b2ff" }, "muscle": { "category": "people", "moji": "💪", + "description": "flexed biceps", "unicodeVersion": "6.0", "digest": "e4ce52757b2b7982e2516e0e8bf2e2253617cc9f3e6178f1887c61c9039461ba" }, "muscle_tone1": { "category": "people", "moji": "💪🏻", + "description": "flexed biceps tone 1", "unicodeVersion": "8.0", "digest": "4a2fa226a05bb847b62cdd163eb6c2d514d3c2330a727991cf550c0d32b0e818" }, "muscle_tone2": { "category": "people", "moji": "💪🏼", + "description": "flexed biceps tone 2", "unicodeVersion": "8.0", "digest": "a8d5ecce335c782ca5f5e55763c06cfefa1c16c24cd6602237cf125d4ff95e47" }, "muscle_tone3": { "category": "people", "moji": "💪🏽", + "description": "flexed biceps tone 3", "unicodeVersion": "8.0", "digest": "070354b443faec3969663b770545fc4cf5ec75148557b2b9d6fc82ab22b43bd1" }, "muscle_tone4": { "category": "people", "moji": "💪🏾", + "description": "flexed biceps tone 4", "unicodeVersion": "8.0", "digest": "8eafcdb6a607aeafa673c257df0d2a1b20f00fc0868d811babcbe784490a0dd3" }, "muscle_tone5": { "category": "people", "moji": "💪🏿", + "description": "flexed biceps tone 5", "unicodeVersion": "8.0", "digest": "85a1e2b5c89907694240e9c5b9d876a741fa7ba38918c5718273e289cbc40efe" }, "mushroom": { "category": "nature", "moji": "🍄", + "description": "mushroom", "unicodeVersion": "6.0", "digest": "aaca8cf7c5cfa4487b5fef365a231f98be4bbf041197fc022161bcc8ce6f57c8" }, "musical_keyboard": { "category": "activity", "moji": "🎹", + "description": "musical keyboard", "unicodeVersion": "6.0", "digest": "fb0a726728900377d76d94aac9c94dce29107e8e3f1dcb0599d95bce7169b492" }, "musical_note": { "category": "symbols", "moji": "🎵", + "description": "musical note", "unicodeVersion": "6.0", "digest": "41288e79b4070bb980281d0e0d1c14d8b144b4aedb2eaadb9f2bebcb4ef892b4" }, "musical_score": { "category": "activity", "moji": "🎼", + "description": "musical score", "unicodeVersion": "6.0", "digest": "f0f91b9fa4a2bff7a5a1a11afa6f31cfe7e5fa8b0d6f3cce904b781a28ed0277" }, "mute": { "category": "symbols", "moji": "🔇", + "description": "speaker with cancellation stroke", "unicodeVersion": "6.0", "digest": "def277da49d744b55c7cdde269a15aa05315898f615e721ee7e9205d7b8030d6" }, "nail_care": { "category": "people", "moji": "💅", + "description": "nail polish", "unicodeVersion": "6.0", "digest": "48b33b1dbbd25b4f34ab2ca07bb99ddaaaa741990142c5623310f76b78c076f9" }, "nail_care_tone1": { "category": "people", "moji": "💅🏻", + "description": "nail polish tone 1", "unicodeVersion": "8.0", "digest": "a9ac92a34f407e7dd7c71377e6275e66657f7f42e4b911c540d1a66a02d92ac5" }, "nail_care_tone2": { "category": "people", "moji": "💅🏼", + "description": "nail polish tone 2", "unicodeVersion": "8.0", "digest": "f295ec85980aaa75818fad619c3d25042146ecbbf361db9e9bb96e7bc202bc73" }, "nail_care_tone3": { "category": "people", "moji": "💅🏽", + "description": "nail polish tone 3", "unicodeVersion": "8.0", "digest": "02ec373052a250977298bae85262177910126cc10de9480f1afa328ac2f65a95" }, "nail_care_tone4": { "category": "people", "moji": "💅🏾", + "description": "nail polish tone 4", "unicodeVersion": "8.0", "digest": "f3d95390ab59caedfda66122bbd0acf3aabedc142fc48352d68900766a7e6f5c" }, "nail_care_tone5": { "category": "people", "moji": "💅🏿", + "description": "nail polish tone 5", "unicodeVersion": "8.0", "digest": "009423c97f2aafd24fb8c7c485c58b30bbf9ae6797cc14b80d472b207327b518" }, "name_badge": { "category": "symbols", "moji": "📛", + "description": "name badge", "unicodeVersion": "6.0", "digest": "f9f6a4895ff0be8fb2ccc7ad195b94e9650f742f66ead999e90724cfb77af628" }, "nauseated_face": { "category": "people", "moji": "🤢", + "description": "nauseated face", "unicodeVersion": "9.0", "digest": "f8471cf4720948d8246ec9d30e29783e819f90e3cfe8b1ba628671a1aad1a91c" }, "necktie": { "category": "people", "moji": "👔", + "description": "necktie", "unicodeVersion": "6.0", "digest": "01bb18dc8bfe787daa9613b5d09988cd5a065449ef906099ce3cb308c8a7da68" }, "negative_squared_cross_mark": { "category": "symbols", "moji": "❎", + "description": "negative squared cross mark", "unicodeVersion": "6.0", "digest": "1cdaf4abc9adafa089c91c2e33a24e9e647aea0f857e767941a899a16ec53b74" }, "nerd": { "category": "people", "moji": "🤓", + "description": "nerd face", "unicodeVersion": "8.0", "digest": "9e5f3c93db25cf1d0f9d6e6bd2993161afec6c30573ba3fe85e13b8c84483d66" }, "neutral_face": { "category": "people", "moji": "😐", + "description": "neutral face", "unicodeVersion": "6.0", "digest": "7449430a60619956573e9dc80834045296f2b99853737b6c7794c785ff53d64e" }, "new": { "category": "symbols", "moji": "🆕", + "description": "squared new", "unicodeVersion": "6.0", "digest": "e20bc3e9f40726afd0cfb7268d02f1e1a07343364fd08b252d59f38de067bf06" }, "new_moon": { "category": "nature", "moji": "🌑", + "description": "new moon symbol", "unicodeVersion": "6.0", "digest": "dbfc5dcae34b45f15ff767e297cba3a12cb83f3b542db8cfc8dbd9669e0df46c" }, "new_moon_with_face": { "category": "nature", "moji": "🌚", + "description": "new moon with face", "unicodeVersion": "6.0", "digest": "c66d347d2222ac8d77d323a07699aff6b168328648db4f885b1ed0e2831fd59b" }, "newspaper": { "category": "objects", "moji": "📰", + "description": "newspaper", "unicodeVersion": "6.0", "digest": "c05e986d9cdac11afa30c6a21a72572ddf50fc64e87ae0c4e0ad57ffe70acc5c" }, "newspaper2": { "category": "objects", "moji": "🗞", + "description": "rolled-up newspaper", "unicodeVersion": "7.0", "digest": "63db7bcf51effc73e5124392740736383774a4bcfbc1156cf55599504760883d" }, "ng": { "category": "symbols", "moji": "🆖", + "description": "squared ng", "unicodeVersion": "6.0", "digest": "34d5a11c70f48ea719e602908534f446b192622e775d4160f0e1ec52c342a35c" }, "night_with_stars": { "category": "travel", "moji": "🌃", + "description": "night with stars", "unicodeVersion": "6.0", "digest": "39d9c079be80ee6ce1667531be528a2aa7f8bd46c7b6c2a6ee279d9a207c84a4" }, "nine": { "category": "symbols", "moji": "9️⃣", + "description": "keycap digit nine", "unicodeVersion": "3.0", "digest": "8bb40750eda8506ef877c9a3b8e2039d26f20eef345742f635740574a7e8daa6" }, "no_bell": { "category": "symbols", "moji": "🔕", + "description": "bell with cancellation stroke", "unicodeVersion": "6.0", "digest": "6542a9a5656c79c153f8c37f12d48f677c89b02ed0989ae37fa5e51ce6895422" }, "no_bicycles": { "category": "symbols", "moji": "🚳", + "description": "no bicycles", "unicodeVersion": "6.0", "digest": "af71c183545da2ff4c05609f9d572edb64b63ccba7c6a4b208d271558aa92b0a" }, "no_entry": { "category": "symbols", "moji": "⛔", + "description": "no entry", "unicodeVersion": "5.2", "digest": "dc0bac1ed9ab8e9af143f0fce5043fe68f7f46bd80856cdec95d20c3999b637d" }, "no_entry_sign": { "category": "symbols", "moji": "🚫", + "description": "no entry sign", "unicodeVersion": "6.0", "digest": "2c1fceef23b62effca68e0e087b8f020125d25b98d61492b1540055d1914fdc3" }, "no_good": { "category": "people", "moji": "🙅", + "description": "face with no good gesture", "unicodeVersion": "6.0", "digest": "6eb970b104389be5d18657d7c04be5149958c26855c52ea68574af852c5f85c4" }, "no_good_tone1": { "category": "people", "moji": "🙅🏻", + "description": "face with no good gesture tone 1", "unicodeVersion": "8.0", "digest": "c20a24a1e536240b4dcf90ecb530796de621d7ba1fb9e3fa0f849d048c509c03" }, "no_good_tone2": { "category": "people", "moji": "🙅🏼", + "description": "face with no good gesture tone 2", "unicodeVersion": "8.0", "digest": "f31a4628c1f2e6a39288fda8eb19c9ec89983e3726e17a09384d9ecc13ef0b4c" }, "no_good_tone3": { "category": "people", "moji": "🙅🏽", + "description": "face with no good gesture tone 3", "unicodeVersion": "8.0", "digest": "959dec1bfdaf37b20a86ab2bcbdbacd3179c87b163042377d966eab47564c0fb" }, "no_good_tone4": { "category": "people", "moji": "🙅🏾", + "description": "face with no good gesture tone 4", "unicodeVersion": "8.0", "digest": "efd931f0080adf2e04129c83a8b24fda0ae7a9fa7c4b463686c0b99023620db8" }, "no_good_tone5": { "category": "people", "moji": "🙅🏿", + "description": "face with no good gesture tone 5", "unicodeVersion": "8.0", "digest": "f35df2b26af9baef47c1f8cc97a1b28a58aa7fcb2a13fdac7b2d9189f1e40105" }, "no_mobile_phones": { "category": "symbols", "moji": "📵", + "description": "no mobile phones", "unicodeVersion": "6.0", "digest": "a472decd6ac7f9777961c09e00458746b2c04965585e3bee4556be3968e55bcd" }, "no_mouth": { "category": "people", "moji": "😶", + "description": "face without mouth", "unicodeVersion": "6.0", "digest": "72dda8b1e3ad4b05d9b095f9bd05e95d7ba013906c68914976a4554e8edf5866" }, "no_pedestrians": { "category": "symbols", "moji": "🚷", + "description": "no pedestrians", "unicodeVersion": "6.0", "digest": "062b4a71b338fe09775e465bfba8ac04efbb3640330e8cabe88f3af62b0f4225" }, "no_smoking": { "category": "symbols", "moji": "🚭", + "description": "no smoking symbol", "unicodeVersion": "6.0", "digest": "ae2ebb331f79f6074091c0ee9cd69fce16d5e12a131d18973fc05520097e14ee" }, "non-potable_water": { "category": "symbols", "moji": "🚱", + "description": "non-potable water symbol", "unicodeVersion": "6.0", "digest": "32eba0a99b498133c2e4450036f768d3dccaaf5b50adc9ad988757adc777a6a1" }, "nose": { "category": "people", "moji": "👃", + "description": "nose", "unicodeVersion": "6.0", "digest": "9f800e24658ea3cebe1144d5d808cf13a88261f1a7f1f81a10d03b3d9d00e541" }, "nose_tone1": { "category": "people", "moji": "👃🏻", + "description": "nose tone 1", "unicodeVersion": "8.0", "digest": "a2d0af22284b1d264eb780943b8360f463996a5c9c9584b8473edf8d442d9173" }, "nose_tone2": { "category": "people", "moji": "👃🏼", + "description": "nose tone 2", "unicodeVersion": "8.0", "digest": "244dcaa8540024cf521f29f36bd48f933bf82f4833e35e6fa0abf113022038f3" }, "nose_tone3": { "category": "people", "moji": "👃🏽", + "description": "nose tone 3", "unicodeVersion": "8.0", "digest": "c935b64866f0d49da52035aa09f36ff56d238eb7f5b92205386451056e8ea74f" }, "nose_tone4": { "category": "people", "moji": "👃🏾", + "description": "nose tone 4", "unicodeVersion": "8.0", "digest": "a87e95fd9319c49e66b6dea0e57319d0ed9921b8d94df037767bf3d5dc7c94f3" }, "nose_tone5": { "category": "people", "moji": "👃🏿", + "description": "nose tone 5", "unicodeVersion": "8.0", "digest": "1e0f9842e0f8ad5805eabd3f35a6038b7a2e49d566a1f5c17271f9cdf467ca60" }, "notebook": { "category": "objects", "moji": "📓", + "description": "notebook", "unicodeVersion": "6.0", "digest": "fc679d3728f86073d1607a926885dd8b0261132f5c4a0322f1e46ea9f95c8cb8" }, "notebook_with_decorative_cover": { "category": "objects", "moji": "📔", + "description": "notebook with decorative cover", "unicodeVersion": "6.0", "digest": "d822eda4b49cbfa399b36f134c1a0b8dcfdd27ed89f12c50bc18f6f0a9aa56ef" }, "notepad_spiral": { "category": "objects", "moji": "🗒", + "description": "spiral note pad", "unicodeVersion": "7.0", "digest": "c6a8e16aa62474cef13e5659fddb4afc57e3f79635e32e6020edbee2b5b50f18" }, "notes": { "category": "symbols", "moji": "🎶", + "description": "multiple musical notes", "unicodeVersion": "6.0", "digest": "98467e0adc134d45676ef1c6c459e5853a9db50c8a6e91b6aec7d449aa737f48" }, "nut_and_bolt": { "category": "objects", "moji": "🔩", + "description": "nut and bolt", "unicodeVersion": "6.0", "digest": "a77bd72f29a7302195dcec240174b15586de79e3204258e3fb401a6ea90563b3" }, "o": { "category": "symbols", "moji": "⭕", + "description": "heavy large circle", "unicodeVersion": "5.2", "digest": "2387e5fd9ae4c2972d40298d32319b8fa55c50dbfc1c04c5c36088213e6951dd" }, "o2": { "category": "symbols", "moji": "🅾", + "description": "negative squared latin capital letter o", "unicodeVersion": "6.0", "digest": "6a9ccb0bf394e4d05ffda19327cee18f7b9ed80367fc7f41c93da9bb7efab0bf" }, "ocean": { "category": "nature", "moji": "🌊", + "description": "water wave", "unicodeVersion": "6.0", "digest": "1a9ca9848d4fb75852addfc10bf84eccf7caa5339714b90e3de4cb6f2518465e" }, "octagonal_sign": { "category": "symbols", "moji": "🛑", + "description": "octagonal sign", "unicodeVersion": "9.0", "digest": "9f6927048e1f9da57f89d1ae1eb86fa4ab7abdbabca756a738a799e948d0b3f9" }, "octopus": { "category": "nature", "moji": "🐙", + "description": "octopus", "unicodeVersion": "6.0", "digest": "0fcc65c12f4b29ea75a8c4823d20838a7e6db6978fdcb536943072aa1460bc59" }, "oden": { "category": "food", "moji": "🍢", + "description": "oden", "unicodeVersion": "6.0", "digest": "089974cb13a0bef6a245fc73029c5ed5153fd4caae0177b835f779e32200b8aa" }, "office": { "category": "travel", "moji": "🏢", + "description": "office building", "unicodeVersion": "6.0", "digest": "3633a2e91036362e273eef4e0cfbdbbb4cb1208afe2cfa110ebef7b78109a66f" }, "oil": { "category": "objects", "moji": "🛢", + "description": "oil drum", "unicodeVersion": "7.0", "digest": "00b94d33bcc9b9e8a5d4bd6e7f7e2fced9497ce05919edd5e58eafbc011c2caa" }, "ok": { "category": "symbols", "moji": "🆗", + "description": "squared ok", "unicodeVersion": "6.0", "digest": "5f320f9b96e98a2f17ebe240daff9b9fd2ae0727cd6c8e4633b1744356e89365" }, "ok_hand": { "category": "people", "moji": "👌", + "description": "ok hand sign", "unicodeVersion": "6.0", "digest": "d63002dce3cc3655b67b8765b7c28d370edba0e3758b2329b60e0e61c4d8e78d" }, "ok_hand_tone1": { "category": "people", "moji": "👌🏻", + "description": "ok hand sign tone 1", "unicodeVersion": "8.0", "digest": "ef1508efcf483b09807554fe0e451c2948224f9deb85463e8e0dad6875b54012" }, "ok_hand_tone2": { "category": "people", "moji": "👌🏼", + "description": "ok hand sign tone 2", "unicodeVersion": "8.0", "digest": "1215a101a082fd8e04c5d2f7e3c59d0f480cb0bedd79aeab5d36676bfe760088" }, "ok_hand_tone3": { "category": "people", "moji": "👌🏽", + "description": "ok hand sign tone 3", "unicodeVersion": "8.0", "digest": "6fe0ed9fb42e86bb2bed4cb37b2acacacda1471fb1ee845ad55e54fb0897fbf4" }, "ok_hand_tone4": { "category": "people", "moji": "👌🏾", + "description": "ok hand sign tone 4", "unicodeVersion": "8.0", "digest": "bfb9041c49d95e901a667264abaf9b398f6c4aa8b52bf5191c122db20c13c020" }, "ok_hand_tone5": { "category": "people", "moji": "👌🏿", + "description": "ok hand sign tone 5", "unicodeVersion": "8.0", "digest": "1c218dc04d698da2cbdd7bea1ca3f845f9b386e967b7247c52f4b0f6ec8f5320" }, "ok_woman": { "category": "people", "moji": "🙆", + "description": "face with ok gesture", "unicodeVersion": "6.0", "digest": "3f8bd4ce2c4497155d697e5a71ebdc9339f65633d07fa9a7903e1bd76cfa4ba1" }, "ok_woman_tone1": { "category": "people", "moji": "🙆🏻", + "description": "face with ok gesture tone1", "unicodeVersion": "8.0", "digest": "1660cd904ccd2ecdc6f4ba00527f7d4ec8c33f3c6183344616f97badae4c3730" }, "ok_woman_tone2": { "category": "people", "moji": "🙆🏼", + "description": "face with ok gesture tone2", "unicodeVersion": "8.0", "digest": "7ba5fddd1e141424fac6778894dfc5af28e125839c58937c69496f99cd2c4002" }, "ok_woman_tone3": { "category": "people", "moji": "🙆🏽", + "description": "face with ok gesture tone3", "unicodeVersion": "8.0", "digest": "1d972b8377c52f598406f59ab1e5be41aaf8f027e1fefba3deda66312ccd6a9b" }, "ok_woman_tone4": { "category": "people", "moji": "🙆🏾", + "description": "face with ok gesture tone4", "unicodeVersion": "8.0", "digest": "a176328d8f53503aa743448968afd21d72ffd3510555526a3fb38d6b30ee7c15" }, "ok_woman_tone5": { "category": "people", "moji": "🙆🏿", + "description": "face with ok gesture tone5", "unicodeVersion": "8.0", "digest": "13cfc1b589c57e81f768ee07a14b737cafc71407a7eb0956728b2ec4b1df14c4" }, "older_man": { "category": "people", "moji": "👴", + "description": "older man", "unicodeVersion": "6.0", "digest": "4c0462b199bf26181c9e4d2d4cb878a32b0294566941212efc67362d0645f948" }, "older_man_tone1": { "category": "people", "moji": "👴🏻", + "description": "older man tone 1", "unicodeVersion": "8.0", "digest": "99baa083f78cb01166d0a928d0b53682be14be04c29fc17bef14aac1a73a61e6" }, "older_man_tone2": { "category": "people", "moji": "👴🏼", + "description": "older man tone 2", "unicodeVersion": "8.0", "digest": "5b4ce713e8820ba517fe92c25f3b93e6a6bf3704d1f982c461d5f31fc02b9d3d" }, "older_man_tone3": { "category": "people", "moji": "👴🏽", + "description": "older man tone 3", "unicodeVersion": "8.0", "digest": "0eff72b3226c3a703c635798ee84129a695c896fa011fe1adbc105312eecc083" }, "older_man_tone4": { "category": "people", "moji": "👴🏾", + "description": "older man tone 4", "unicodeVersion": "8.0", "digest": "ad9ba82b0c5d3b171b0639ee4265370dbddff5e0eeb70729db122659bb8c8f84" }, "older_man_tone5": { "category": "people", "moji": "👴🏿", + "description": "older man tone 5", "unicodeVersion": "8.0", "digest": "5eb0a7467cc40e75752e11fd5126b275863dc037557a0d0d3b24b681e00c2386" }, "older_woman": { "category": "people", "moji": "👵", + "description": "older woman", "unicodeVersion": "6.0", "digest": "c261fdf3b01e0c7d949e177144531add5895197fbadf1acbba8eb17d18766bf6" }, "older_woman_tone1": { "category": "people", "moji": "👵🏻", + "description": "older woman tone 1", "unicodeVersion": "8.0", "digest": "1f2bb9e42270a58194498254da27ac2b7a50edaa771b90ee194ccd6d24660c62" }, "older_woman_tone2": { "category": "people", "moji": "👵🏼", + "description": "older woman tone 2", "unicodeVersion": "8.0", "digest": "2e28198e9b7ac08c55980677ed66655fd899e157f14184958bebd87fcd714940" }, "older_woman_tone3": { "category": "people", "moji": "👵🏽", + "description": "older woman tone 3", "unicodeVersion": "8.0", "digest": "c968be0170f7e0c65d4f796337034cfb1daba897884da6fad85635ab5b6edf67" }, "older_woman_tone4": { "category": "people", "moji": "👵🏾", + "description": "older woman tone 4", "unicodeVersion": "8.0", "digest": "3596a6fa9a643bf79255afcd29657b03850df8499db9669b92ce013af908af44" }, "older_woman_tone5": { "category": "people", "moji": "👵🏿", + "description": "older woman tone 5", "unicodeVersion": "8.0", "digest": "c8998cb3dbd15e22bd1d6dad613d109ce371d9ffca3657e1a8afe5aeb30c1275" }, "om_symbol": { "category": "symbols", "moji": "🕉", + "description": "om symbol", "unicodeVersion": "7.0", "digest": "5ead73bea546ba9ba6da522f7280cc289c75ff5467742bdba31f92d0e1b3f4e6" }, "on": { "category": "symbols", "moji": "🔛", + "description": "on with exclamation mark with left right arrow abo", "unicodeVersion": "6.0", "digest": "9cc61a6b31a30c32dab594191bf23f91e341c4105384ab22158a6d43e6364631" }, "oncoming_automobile": { "category": "travel", "moji": "🚘", + "description": "oncoming automobile", "unicodeVersion": "6.0", "digest": "557c9cacdc3f95215d4f7a6f097a2baa7c007cb9c519492a6717077af4ca6b56" }, "oncoming_bus": { "category": "travel", "moji": "🚍", + "description": "oncoming bus", "unicodeVersion": "6.0", "digest": "059f28ce6bfb337e107db5982cbd2004844450ef20b4a54b9ca3cb738360ab05" }, "oncoming_police_car": { "category": "travel", "moji": "🚔", + "description": "oncoming police car", "unicodeVersion": "6.0", "digest": "aee79306a0d129cfc1980f58db80391eb46d2d7d5f814bf431414dc7680cab72" }, "oncoming_taxi": { "category": "travel", "moji": "🚖", + "description": "oncoming taxi", "unicodeVersion": "6.0", "digest": "84351489fc86d980b8d3eb9ec4e81120fe700b3ac01346daebe2b7aeb9607a55" }, "one": { "category": "symbols", "moji": "1️⃣", + "description": "keycap digit one", "unicodeVersion": "3.0", "digest": "d5d3fff04e68a114ff6464ee06fc831f3f381713045165f62a88d5e8215c195b" }, "open_file_folder": { "category": "objects", "moji": "📂", + "description": "open file folder", "unicodeVersion": "6.0", "digest": "96cfc322ee4903ae8cec07604811742245fd7d14f00bb70276d39d29c48bed28" }, "open_hands": { "category": "people", "moji": "👐", + "description": "open hands sign", "unicodeVersion": "6.0", "digest": "a6c131da2040b48103cea14f280e728675da50fa448d2b3f3438fcbb5bf5596a" }, "open_hands_tone1": { "category": "people", "moji": "👐🏻", + "description": "open hands sign tone 1", "unicodeVersion": "8.0", "digest": "867128dff2fa9b860c10c6b792f989f0c057928783696062378f834c0ef89d85" }, "open_hands_tone2": { "category": "people", "moji": "👐🏼", + "description": "open hands sign tone 2", "unicodeVersion": "8.0", "digest": "487ff2745b03d49bb3b1d0acd86ba530fd8cc3f467ca3fa504f88f0ef1cbbc01" }, "open_hands_tone3": { "category": "people", "moji": "👐🏽", + "description": "open hands sign tone 3", "unicodeVersion": "8.0", "digest": "cb8cddc8b8661f874ac9478289d16cc41406b947bb87f3363df518a588a53e16" }, "open_hands_tone4": { "category": "people", "moji": "👐🏾", + "description": "open hands sign tone 4", "unicodeVersion": "8.0", "digest": "17dcc2c07230846a769f3c79ce618a757c88b9b58c95c6c5b2d7f968814d447d" }, "open_hands_tone5": { "category": "people", "moji": "👐🏿", + "description": "open hands sign tone 5", "unicodeVersion": "8.0", "digest": "36b2493d67c84cea4f3f85a3088c6abcfd35cf99f7aeaeedfafa420ee878e3d2" }, "open_mouth": { "category": "people", "moji": "😮", + "description": "face with open mouth", "unicodeVersion": "6.1", "digest": "1906c5100ae0c8326ca5c4f9422976958a38dadd8d77724d68538a25d9623035" }, "ophiuchus": { "category": "symbols", "moji": "⛎", + "description": "ophiuchus", "unicodeVersion": "6.0", "digest": "6112e2a1656b1cb8bd9a8b0dfa6cbf66d30cae671710a9ef75c821de344aab2b" }, "orange_book": { "category": "objects", "moji": "📙", + "description": "orange book", "unicodeVersion": "6.0", "digest": "41141b08d2beceded21a94795431603c47fd7d42a3a472a2aa8b2bb25fa87ebf" }, "orthodox_cross": { "category": "symbols", "moji": "☦", + "description": "orthodox cross", "unicodeVersion": "1.1", "digest": "c16372102f0169dd6d32eb2b27a633aaee74e4e0fddcf723c15ad97f9dc6075c" }, "outbox_tray": { "category": "objects", "moji": "📤", + "description": "outbox tray", "unicodeVersion": "6.0", "digest": "e47cb481a0ffcb39996f32fd313e19b362a91d8dda15ffca48ac23a3b5bb5baf" }, "owl": { "category": "nature", "moji": "🦉", + "description": "owl", "unicodeVersion": "9.0", "digest": "f62ec1ad23ad9038966eea8d8b79660ac212f291af2e89bcdb0fdc683caf41e5" }, "ox": { "category": "nature", "moji": "🐂", + "description": "ox", "unicodeVersion": "6.0", "digest": "d13bc60552190bb9936bf32d681bdc742439b702a09cfc62137ea09a98624aed" }, "package": { "category": "objects", "moji": "📦", + "description": "package", "unicodeVersion": "6.0", "digest": "e82bf5accebb65136e897c15607eef635fb79fd7b2d8c8e19a9eb00b6786918c" }, "page_facing_up": { "category": "objects", "moji": "📄", + "description": "page facing up", "unicodeVersion": "6.0", "digest": "3884868bdcb2f29615b09a13a30385cbc5269379094a54b5a7e8a5f4e8ce905a" }, "page_with_curl": { "category": "objects", "moji": "📃", + "description": "page with curl", "unicodeVersion": "6.0", "digest": "3d6257670189f841ad1fa45415c34feb2433b2cb35bb435c4ee122ce89b39669" }, "pager": { "category": "objects", "moji": "📟", + "description": "pager", "unicodeVersion": "6.0", "digest": "e21c756cc1c58ebc1b37ebcd38e22a25b31e2e81306c6f18285d6a7671f9eb12" }, "paintbrush": { "category": "objects", "moji": "🖌", + "description": "lower left paintbrush", "unicodeVersion": "7.0", "digest": "fc0da7a25b726b8be9dd6467953e27293d2313a21eeff21424c2a19be614fff2" }, "palm_tree": { "category": "nature", "moji": "🌴", + "description": "palm tree", "unicodeVersion": "6.0", "digest": "90fedafd62fe0abf51325174d0f293ebb9a4794913b9ba93b12f2d0119056df1" }, "pancakes": { "category": "food", "moji": "🥞", + "description": "pancakes", "unicodeVersion": "9.0", "digest": "5256b4832431e8a88555796b1a9726f12d909a26fb2bdc3a0abff76412c45903" }, "panda_face": { "category": "nature", "moji": "🐼", + "description": "panda face", "unicodeVersion": "6.0", "digest": "56a4b84abe983bd6569be1b81ac5e43071015fd308389a16b92231310ae56a5b" }, "paperclip": { "category": "objects", "moji": "📎", + "description": "paperclip", "unicodeVersion": "6.0", "digest": "d1e2ce94a12b7e8b7a9bba49e47ddc7432ec0288545d3b6817c7a499e806e3f0" }, "paperclips": { "category": "objects", "moji": "🖇", + "description": "linked paperclips", "unicodeVersion": "7.0", "digest": "70cefa0d0777f070e393e9f95c24146fe2dd627f30fa3845baa19310d9291fe2" }, "park": { "category": "travel", "moji": "🏞", + "description": "national park", "unicodeVersion": "7.0", "digest": "444dce8014e0817ddd756c36a38adfbbf7ae4c6aa509e4cae291828f0716d5e7" }, "parking": { "category": "symbols", "moji": "🅿", + "description": "negative squared latin capital letter p", "unicodeVersion": "5.2", "digest": "9f1da460a7dd58b26beab8cf701be2691fb812208fbc941c71daa35be1507c2f" }, "part_alternation_mark": { "category": "symbols", "moji": "〽", + "description": "part alternation mark", "unicodeVersion": "3.2", "digest": "956da19353bb38fd4dfe0ab5360679a9035d566858fb5de62887b85c75fb8eef" }, "partly_sunny": { "category": "nature", "moji": "⛅", + "description": "sun behind cloud", "unicodeVersion": "5.2", "digest": "8fb9a6d2caf9e0cce58447762f0dfd6aa0b581b2e83fea6411348e0cbc8cf3c4" }, "passport_control": { "category": "symbols", "moji": "🛂", + "description": "passport control", "unicodeVersion": "6.0", "digest": "d9be6eed2c90e1c89171c42d70a06485fdf86a4c68833371832cc1f6897fadd0" }, "pause_button": { "category": "symbols", "moji": "⏸", + "description": "double vertical bar", "unicodeVersion": "7.0", "digest": "143221d99e82399ed7824b6c5e185700896492058b65c04e4c668291de78b203" }, "peace": { "category": "symbols", "moji": "☮", + "description": "peace symbol", "unicodeVersion": "1.1", "digest": "65181429e373c1f0507bbd98425c1bec0c042d648fb285a392460cbce60f44d4" }, "peach": { "category": "food", "moji": "🍑", + "description": "peach", "unicodeVersion": "6.0", "digest": "768d1f4f29e1e06aff5abb29043be83087ded16427ce6a2d0f682814e665e311" }, "peanuts": { "category": "food", "moji": "🥜", + "description": "peanuts", "unicodeVersion": "9.0", "digest": "e2384846b6e4a6c3a56e991ebb749cb68b330ac00a9e9d888b2c39105ff7ff5d" }, "pear": { "category": "food", "moji": "🍐", + "description": "pear", "unicodeVersion": "6.0", "digest": "b7c9cf90bb979649b863d2f4132f1b51f6f8107d42e08fb8b4033fea32844948" }, "pen_ballpoint": { "category": "objects", "moji": "🖊", + "description": "lower left ballpoint pen", "unicodeVersion": "7.0", "digest": "aacb20b220f26704e10303deeea33be0eec2d3811dcba7795902ca44b6ae9876" }, "pen_fountain": { "category": "objects", "moji": "🖋", + "description": "lower left fountain pen", "unicodeVersion": "7.0", "digest": "3619913eab2b6291f518b40481bb3eca0820d68b0a1b3c11fb6a69c62b75a626" }, "pencil": { "category": "objects", "moji": "📝", + "description": "memo", "unicodeVersion": "6.0", "digest": "accbc3f1439b7faa4411e502385f78a16c8e71851f71fc13582753291ffb507c" }, "pencil2": { "category": "objects", "moji": "✏", + "description": "pencil", "unicodeVersion": "1.1", "digest": "9ca1b56b5726f472b1f1b23050ed163e213916dac379d22e38e4c8358fe871e0" }, "penguin": { "category": "nature", "moji": "🐧", + "description": "penguin", "unicodeVersion": "6.0", "digest": "a1800ab931d6dc84a9c89bfab2c815198025c276d952509c55b18dd20bd9d316" }, "pensive": { "category": "people", "moji": "😔", + "description": "pensive face", "unicodeVersion": "6.0", "digest": "d237deff9f5ead8a0b281b7e5c6f4b82e98cc30c80c86c22c3fdc6160090b2f2" }, "performing_arts": { "category": "activity", "moji": "🎭", + "description": "performing arts", "unicodeVersion": "6.0", "digest": "d7c7bc9213e308ca26286cbbd8012e656b0f9b00293758faf1bfccc4c5ceabed" }, "persevere": { "category": "people", "moji": "😣", + "description": "persevering face", "unicodeVersion": "6.0", "digest": "c361509c9b8663af19a02a1ffff61b1b0d0b4bd75d693ce3d406b0ca1bde1ca0" }, "person_frowning": { "category": "people", "moji": "🙍", + "description": "person frowning", "unicodeVersion": "6.0", "digest": "b37be8bd95f21a6860ad3f171b8086125ab37331b382d87bcdb4cd684800546b" }, "person_frowning_tone1": { "category": "people", "moji": "🙍🏻", + "description": "person frowning tone 1", "unicodeVersion": "8.0", "digest": "3d5e78a367f9673baed2a86bc11cf04fd44394aadb65291fa51ade8dca318427" }, "person_frowning_tone2": { "category": "people", "moji": "🙍🏼", + "description": "person frowning tone 2", "unicodeVersion": "8.0", "digest": "7456c414c65ad6b6f11855f68a2eedc18113526f86862c4373202397cb1bed2c" }, "person_frowning_tone3": { "category": "people", "moji": "🙍🏽", + "description": "person frowning tone 3", "unicodeVersion": "8.0", "digest": "c86cf2d6951f1e6a7c786a74caaf68a777cf00e88023e23849d4383f864ae437" }, "person_frowning_tone4": { "category": "people", "moji": "🙍🏾", + "description": "person frowning tone 4", "unicodeVersion": "8.0", "digest": "944e96ced645ced8db6bb50120c7e37ed46b6960d595cbfe964c81803efa83aa" }, "person_frowning_tone5": { "category": "people", "moji": "🙍🏿", + "description": "person frowning tone 5", "unicodeVersion": "8.0", "digest": "4bd0ea571be6ef9f0493784ef0d12d5e47bc2d6ac610fb42c450bf3d87fb2948" }, "person_with_blond_hair": { "category": "people", "moji": "👱", + "description": "person with blond hair", "unicodeVersion": "6.0", "digest": "a7f94ede2e43308108c2260d83fc10121dda09a67f94a0a840e6d7bba7fd5616" }, "person_with_blond_hair_tone1": { "category": "people", "moji": "👱🏻", + "description": "person with blond hair tone 1", "unicodeVersion": "8.0", "digest": "00a116357a7878554c83e5bade4bddfa9cfabf76a229efa19cbb58e0d216219c" }, "person_with_blond_hair_tone2": { "category": "people", "moji": "👱🏼", + "description": "person with blond hair tone 2", "unicodeVersion": "8.0", "digest": "df509ebe92ed3138b9d5bd4645eff4b13f77f714cf62bb949c59eff1adc00019" }, "person_with_blond_hair_tone3": { "category": "people", "moji": "👱🏽", + "description": "person with blond hair tone 3", "unicodeVersion": "8.0", "digest": "6f328513f440a0c8cd1dc44596a5028fd8f306bdaf57c1e6f3aa94a3aa262b3c" }, "person_with_blond_hair_tone4": { "category": "people", "moji": "👱🏾", + "description": "person with blond hair tone 4", "unicodeVersion": "8.0", "digest": "32df1a577815b009696643ad80d063cc97b35d54add6d4e5517fc936f6da9ee8" }, "person_with_blond_hair_tone5": { "category": "people", "moji": "👱🏿", + "description": "person with blond hair tone 5", "unicodeVersion": "8.0", "digest": "2e270bb39187d8e36a33f4aa4d6045308189595fafc157cf7993e82d7ce93442" }, "person_with_pouting_face": { "category": "people", "moji": "🙎", + "description": "person with pouting face", "unicodeVersion": "6.0", "digest": "57e9a6e5f82121516dc189173f2a63b218f726cd51014e24a18c2bdfeeec3a0b" }, "person_with_pouting_face_tone1": { "category": "people", "moji": "🙎🏻", + "description": "person with pouting face tone1", "unicodeVersion": "8.0", "digest": "d10dadb1ac03fc2e221eff77b4c47935dc0b4fe897af3de30461e7226c3b4bbc" }, "person_with_pouting_face_tone2": { "category": "people", "moji": "🙎🏼", + "description": "person with pouting face tone2", "unicodeVersion": "8.0", "digest": "efface531537ab934b3b96985210a2dac88de812e82e804d6ec12174e536d1cc" }, "person_with_pouting_face_tone3": { "category": "people", "moji": "🙎🏽", + "description": "person with pouting face tone3", "unicodeVersion": "8.0", "digest": "7ff26ece237216b949bfa96d16bd12cfd248c6fd3e4ed89aa6c735c09eafaeff" }, "person_with_pouting_face_tone4": { "category": "people", "moji": "🙎🏾", + "description": "person with pouting face tone4", "unicodeVersion": "8.0", "digest": "045c04105df41d94ff4942133c7394e42ff35ef76c4ccb711497ab77ae6219f2" }, "person_with_pouting_face_tone5": { "category": "people", "moji": "🙎🏿", + "description": "person with pouting face tone5", "unicodeVersion": "8.0", "digest": "783ee37f146fcf61d38af5009f5823cf6526fe99ed891979f454016bce9dd4ba" }, "pick": { "category": "objects", "moji": "⛏", + "description": "pick", "unicodeVersion": "5.2", "digest": "7f0ec5445b4d5c66cf46e2a7332946cce34bd70e9929ac7a119251a7f57f555d" }, "pig": { "category": "nature", "moji": "🐷", + "description": "pig face", "unicodeVersion": "6.0", "digest": "51362570ab36805c8f67622ee4543e38811f8abb20f732a1af2ffbff2d63d042" }, "pig2": { "category": "nature", "moji": "🐖", + "description": "pig", "unicodeVersion": "6.0", "digest": "67010e255f28061b9d9210bcdab6edc072642ad134122a1d0c7e3a6b1795a45b" }, "pig_nose": { "category": "nature", "moji": "🐽", + "description": "pig nose", "unicodeVersion": "6.0", "digest": "0b21cac238bf4910939fbea9bed35552378c1b605a3867d7b85c1556dbda22a9" }, "pill": { "category": "objects", "moji": "💊", + "description": "pill", "unicodeVersion": "6.0", "digest": "cb00be361aaba6dbcf8da58bd20b76221dd75031362ecae99496b088ed413a7f" }, "pineapple": { "category": "food", "moji": "🍍", + "description": "pineapple", "unicodeVersion": "6.0", "digest": "621d4d4c52b59e566c2e29ed7845c8bd2d1da0946577527342097808d170dd70" }, "ping_pong": { "category": "activity", "moji": "🏓", + "description": "table tennis paddle and ball", "unicodeVersion": "8.0", "digest": "943a858bd054c81a08a08951f8351c27c8009b85a9359729c7362868298b58e1" }, "pisces": { "category": "symbols", "moji": "♓", + "description": "pisces", "unicodeVersion": "1.1", "digest": "453c3915122a4b6b32867056d2447be48675a84469145c88d52f8007fcb0861a" }, "pizza": { "category": "food", "moji": "🍕", + "description": "slice of pizza", "unicodeVersion": "6.0", "digest": "169bc6c1e1d7fdab1b8bf2eab0eeec4f9a7ae08b7b9b38f33b0b0c642e72053a" }, "place_of_worship": { "category": "symbols", "moji": "🛐", + "description": "place of worship", "unicodeVersion": "8.0", "digest": "daf271d36a38ee8c0f8b9de84c128ab8b25a5b7df8f107308d0353c961f2c644" }, "play_pause": { "category": "symbols", "moji": "⏯", + "description": "black right-pointing double triangle with double vertical bar", "unicodeVersion": "6.0", "digest": "af1498f34a3d6e0da8bbd26ebaa447e697e2df08c8eb255437cf7905c93f8c42" }, "point_down": { "category": "people", "moji": "👇", + "description": "white down pointing backhand index", "unicodeVersion": "6.0", "digest": "4ecdb3f31c16dc38113b8854ec1a7884613b688a185ebdf967eab9a81018f76d" }, "point_down_tone1": { "category": "people", "moji": "👇🏻", + "description": "white down pointing backhand index tone 1", "unicodeVersion": "8.0", "digest": "c74a7c94367cddbfa840542dc0924adeb0d108be0c7fde8c25fb95d69115d283" }, "point_down_tone2": { "category": "people", "moji": "👇🏼", + "description": "white down pointing backhand index tone 2", "unicodeVersion": "8.0", "digest": "dc4bda0726d85418b974addb42738f437fbb9cf16e5815cdbab3859c4ada6cae" }, "point_down_tone3": { "category": "people", "moji": "👇🏽", + "description": "white down pointing backhand index tone 3", "unicodeVersion": "8.0", "digest": "e460f81a501376d2f0ed1d45e358c5ed03ba049e8f466e4298afb4f3ca6d24dc" }, "point_down_tone4": { "category": "people", "moji": "👇🏾", + "description": "white down pointing backhand index tone 4", "unicodeVersion": "8.0", "digest": "4bc91cd771f24e0f897a9d8b18f323fec9a82da0fc2429c4a7e4e6a9d885a0a3" }, "point_down_tone5": { "category": "people", "moji": "👇🏿", + "description": "white down pointing backhand index tone 5", "unicodeVersion": "8.0", "digest": "7e47c6bc73250f36dc7ae1c1c09e7b41f30647b9d0ff703a53a75cc046b5057d" }, "point_left": { "category": "people", "moji": "👈", + "description": "white left pointing backhand index", "unicodeVersion": "6.0", "digest": "b5a7e864a0016afbadb3bec41f51ecf8c4af73cc20462e1a08b357f90bca6879" }, "point_left_tone1": { "category": "people", "moji": "👈🏻", + "description": "white left pointing backhand index tone 1", "unicodeVersion": "8.0", "digest": "9f1868272a10a2b738c065be5d30241643324550cfd47baf01c7a09060e66d31" }, "point_left_tone2": { "category": "people", "moji": "👈🏼", + "description": "white left pointing backhand index tone 2", "unicodeVersion": "8.0", "digest": "bf0d58c68178a2c2c01d4a6235a1a66b90073cea170f9f6fe2668b6dd68424f7" }, "point_left_tone3": { "category": "people", "moji": "👈🏽", + "description": "white left pointing backhand index tone 3", "unicodeVersion": "8.0", "digest": "34d28c97bc8f9d111d14e328153c4298fc32cf18e39e20aacaec17846645ed90" }, "point_left_tone4": { "category": "people", "moji": "👈🏾", + "description": "white left pointing backhand index tone 4", "unicodeVersion": "8.0", "digest": "c40c8436316915d516c53bb1c98a469528cefd98baa719be7e748c4608cbbcc9" }, "point_left_tone5": { "category": "people", "moji": "👈🏿", + "description": "white left pointing backhand index tone 5", "unicodeVersion": "8.0", "digest": "c410fe32e4ce0ded74845a54b86090e59e5820d457837b16e175b36cc71ecb46" }, "point_right": { "category": "people", "moji": "👉", + "description": "white right pointing backhand index", "unicodeVersion": "6.0", "digest": "44d9251ab41f2f48c2250c44a47f92b3476a71f13fbbbfb637547db837fd5a49" }, "point_right_tone1": { "category": "people", "moji": "👉🏻", + "description": "white right pointing backhand index tone 1", "unicodeVersion": "8.0", "digest": "9fcce259eb81c0b52ec7796b98a1653194e3a9021a1d338df1dbbab7522fc406" }, "point_right_tone2": { "category": "people", "moji": "👉🏼", + "description": "white right pointing backhand index tone 2", "unicodeVersion": "8.0", "digest": "9d00a0b1cfc435674dc56065b3d28d28839196977504cf20581205351d8708f2" }, "point_right_tone3": { "category": "people", "moji": "👉🏽", + "description": "white right pointing backhand index tone 3", "unicodeVersion": "8.0", "digest": "e3026a70630ba73d76892a055a80cac2f78d509faddce737f802d2abefa074ba" }, "point_right_tone4": { "category": "people", "moji": "👉🏾", + "description": "white right pointing backhand index tone 4", "unicodeVersion": "8.0", "digest": "ea508fde90561460361773b4e1b8e80874667b19ac115926206e7c592587cb76" }, "point_right_tone5": { "category": "people", "moji": "👉🏿", + "description": "white right pointing backhand index tone 5", "unicodeVersion": "8.0", "digest": "d59cdb2864eb2929941ecd233f8b8afcddc30fbd4594e5f9acf6386ae06ac12c" }, "point_up": { "category": "people", "moji": "☝", + "description": "white up pointing index", "unicodeVersion": "1.1", "digest": "b69ff4f650989709f2185822d278c7773672bd9eb4a625da80f3038a2b9ce42b" }, "point_up_2": { "category": "people", "moji": "👆", + "description": "white up pointing backhand index", "unicodeVersion": "6.0", "digest": "e83cd9eff2af5125a25f5a306c3ee3cfea240add683b5c36a86a994a8d8c805c" }, "point_up_2_tone1": { "category": "people", "moji": "👆🏻", + "description": "white up pointing backhand index tone 1", "unicodeVersion": "8.0", "digest": "b02ec3e7e04a83bfb769cffb951cbf32aa78e56fa5a51c097f9326df9e08ed33" }, "point_up_2_tone2": { "category": "people", "moji": "👆🏼", + "description": "white up pointing backhand index tone 2", "unicodeVersion": "8.0", "digest": "32994b85c8b4a1383ca985ebc3382be88866cea1ff1315adfb71fb05e992a232" }, "point_up_2_tone3": { "category": "people", "moji": "👆🏽", + "description": "white up pointing backhand index tone 3", "unicodeVersion": "8.0", "digest": "9e263bcfb82ada34ff85291f36e64e66b86760fb11a4e0c554e801644d417d6d" }, "point_up_2_tone4": { "category": "people", "moji": "👆🏾", + "description": "white up pointing backhand index tone 4", "unicodeVersion": "8.0", "digest": "3edc92130a0851ac7b5236772ce7918d088689221df287098688e1ed5b3ff181" }, "point_up_2_tone5": { "category": "people", "moji": "👆🏿", + "description": "white up pointing backhand index tone 5", "unicodeVersion": "8.0", "digest": "cabb3b7da9290840ef59d0c8b22625bdb2e94842f01b0a575ccbc348f3069d77" }, "point_up_tone1": { "category": "people", "moji": "☝🏻", + "description": "white up pointing index tone 1", "unicodeVersion": "8.0", "digest": "e496fda349072f8b321ceb7a251175f7244c3076661f5ede48ea75ba1acf8339" }, "point_up_tone2": { "category": "people", "moji": "☝🏼", + "description": "white up pointing index tone 2", "unicodeVersion": "8.0", "digest": "5a8081323f3baa67e6431e21e16a36559b339f5175d586644e34947f738dd07a" }, "point_up_tone3": { "category": "people", "moji": "☝🏽", + "description": "white up pointing index tone 3", "unicodeVersion": "8.0", "digest": "07bf0cea812eb226b443334e026e13d1ec23e013478f4af862a3919703107842" }, "point_up_tone4": { "category": "people", "moji": "☝🏾", + "description": "white up pointing index tone 4", "unicodeVersion": "8.0", "digest": "1fbbd71433108143ee157d0fdadd183f7f013bafa96f0dd93b181e1fd5fd4af2" }, "point_up_tone5": { "category": "people", "moji": "☝🏿", + "description": "white up pointing index tone 5", "unicodeVersion": "8.0", "digest": "ad068ef32df32f8297955490a9a90590a0f93ed5702a052cd0d8f6484c6cc679" }, "police_car": { "category": "travel", "moji": "🚓", + "description": "police car", "unicodeVersion": "6.0", "digest": "0909be1bd615ae331a7cce71e16dee3ca663c721d5170072c593cb7c76f9f661" }, "poodle": { "category": "nature", "moji": "🐩", + "description": "poodle", "unicodeVersion": "6.0", "digest": "f1742fdf3fd26a8a5cfeaba57026518dacaad364cbd03344c4000a35af13e47a" }, "poop": { "category": "people", "moji": "💩", + "description": "pile of poo", "unicodeVersion": "6.0", "digest": "857a61c872138d359a7fe8257bb26118afa49d75186eca2addb415d07c92b3ec" }, "popcorn": { "category": "food", "moji": "🍿", + "description": "popcorn", "unicodeVersion": "8.0", "digest": "684f1b7ef34ea7ca933aed41569bc6595a19ef0d546a1b7b9e69f8335540b323" }, "post_office": { "category": "travel", "moji": "🏣", + "description": "japanese post office", "unicodeVersion": "6.0", "digest": "54398ee396c1314a7993b1cb1cba264946b5c9d5a7dbb43fd67286854d1d1a0f" }, "postal_horn": { "category": "objects", "moji": "📯", + "description": "postal horn", "unicodeVersion": "6.0", "digest": "0ea12f44f3bae9a14bde3b37361b48bd738d2f613bb1b53a9204959b70e643f8" }, "postbox": { "category": "objects", "moji": "📮", + "description": "postbox", "unicodeVersion": "6.0", "digest": "bbc424ae8d46de380d7023a43ea064002fd614657d00330d3503275827ac87e2" }, "potable_water": { "category": "symbols", "moji": "🚰", + "description": "potable water symbol", "unicodeVersion": "6.0", "digest": "dbe80d9637837377cc2a290da2e895f81a3108cc18b049e3d87212402c1c2098" }, "potato": { "category": "food", "moji": "🥔", + "description": "potato", "unicodeVersion": "9.0", "digest": "a56a69f36f3a0793f278726d92c0cea2960554f3062ef1a0904526a04511d8e1" }, "pouch": { "category": "people", "moji": "👝", + "description": "pouch", "unicodeVersion": "6.0", "digest": "9f012b90310b4a072b6a8fa2c64def087b5f7ffffaafc36e1856ba943a170351" }, "poultry_leg": { "category": "food", "moji": "🍗", + "description": "poultry leg", "unicodeVersion": "6.0", "digest": "1445ec4f5e68a19e5a84e5537dca8190d62409070c954d112e6097f1a6b7f054" }, "pound": { "category": "objects", "moji": "💷", + "description": "banknote with pound sign", "unicodeVersion": "6.0", "digest": "eb11b83eb52adb0a15e69a3bc15788a2dc7825dedee81ac3af84963c9dd517b5" }, "pouting_cat": { "category": "people", "moji": "😾", + "description": "pouting cat face", "unicodeVersion": "6.0", "digest": "8822abedf3499cf98278d7eeea0764d1100ec25cad71b4b2e877f9346f8c8138" }, "pray": { "category": "people", "moji": "🙏", + "description": "person with folded hands", "unicodeVersion": "6.0", "digest": "735b79dab34ac2cf81fd42fdcd7eb1f13c24655e5e343816d5764896c03edeea" }, "pray_tone1": { "category": "people", "moji": "🙏🏻", + "description": "person with folded hands tone 1", "unicodeVersion": "8.0", "digest": "e8b6103450215e8566797f150978355e297deade4eb47a6371f7a7bc558fed9d" }, "pray_tone2": { "category": "people", "moji": "🙏🏼", + "description": "person with folded hands tone 2", "unicodeVersion": "8.0", "digest": "ee8baacd95d7e8dbad8a1f2d9a12e36c98f3d518db5d3b117d0a18290815e62b" }, "pray_tone3": { "category": "people", "moji": "🙏🏽", + "description": "person with folded hands tone 3", "unicodeVersion": "8.0", "digest": "ae8c0caa9aca0a6c44069e76a7535c961d0284cd701812f76bbd2bd79ce2bd53" }, "pray_tone4": { "category": "people", "moji": "🙏🏾", + "description": "person with folded hands tone 4", "unicodeVersion": "8.0", "digest": "64f7b3178b8cd6f6a877ed583539eefe068fa87a0dd658fdcd58c8bc809f7e17" }, "pray_tone5": { "category": "people", "moji": "🙏🏿", + "description": "person with folded hands tone 5", "unicodeVersion": "8.0", "digest": "5bc8cdce937ac06779c87021423efcec4f602aa4a39dba90b00de81033005332" }, "prayer_beads": { "category": "objects", "moji": "📿", + "description": "prayer beads", "unicodeVersion": "8.0", "digest": "80177091264430cbcf7c994fbe5ee17319d1a58d933636cc752a54dafcf98a05" }, "pregnant_woman": { "category": "people", "moji": "🤰", + "description": "pregnant woman", "unicodeVersion": "9.0", "digest": "49abb86409103338bdb6ae43c13a78ca2dc9cd158a26df35eadd0da3c84a4352" }, "pregnant_woman_tone1": { "category": "people", "moji": "🤰🏻", + "description": "pregnant woman tone 1", "unicodeVersion": "9.0", "digest": "5a9f8ed2b631ecf8af111803a5c11f4c156435a5293cb50329c7b98697c8da25" }, "pregnant_woman_tone2": { "category": "people", "moji": "🤰🏼", + "description": "pregnant woman tone 2", "unicodeVersion": "9.0", "digest": "279a2eafff603b11629c955b05f5bd3d7da9a271d4fb3f02e9ccd457b8d2d815" }, "pregnant_woman_tone3": { "category": "people", "moji": "🤰🏽", + "description": "pregnant woman tone 3", "unicodeVersion": "9.0", "digest": "93bb63ec2312db315e3f0065520b715cc413ac0fd65538ec9b5cd97df2a42b20" }, "pregnant_woman_tone4": { "category": "people", "moji": "🤰🏾", + "description": "pregnant woman tone 4", "unicodeVersion": "9.0", "digest": "b8dc3dcec894bfd832a249459b10850f8786b6778d8887a677d1291865623da2" }, "pregnant_woman_tone5": { "category": "people", "moji": "🤰🏿", + "description": "pregnant woman tone 5", "unicodeVersion": "9.0", "digest": "73ee432752f81980f353a7f9b9f7a5ece62512dca08e15c1876b89227face21c" }, "prince": { "category": "people", "moji": "🤴", + "description": "prince", "unicodeVersion": "9.0", "digest": "34a0e0625f0a9825d3674192d6233b6cae4d8130451293df09f91a6a4165869c" }, "prince_tone1": { "category": "people", "moji": "🤴🏻", + "description": "prince tone 1", "unicodeVersion": "9.0", "digest": "ccecdfeccb2ab1fceceae14f3fba875c8c7099785a4c40131c08a697b5b675fc" }, "prince_tone2": { "category": "people", "moji": "🤴🏼", + "description": "prince tone 2", "unicodeVersion": "9.0", "digest": "c373fd3e0c1798415e3d8d88fab6c98c1bbdedcbe6f52f3a3899f6e2124a768d" }, "prince_tone3": { "category": "people", "moji": "🤴🏽", + "description": "prince tone 3", "unicodeVersion": "9.0", "digest": "71d15695ca954d55aa69d3c753c7d31a8ba5329713a8ddbc90dafc11e524c4ef" }, "prince_tone4": { "category": "people", "moji": "🤴🏾", + "description": "prince tone 4", "unicodeVersion": "9.0", "digest": "08f6cb32424f15cc3aaf83c31a5dac7c01a6be2f37ea8f13aed579ce6fb4db19" }, "prince_tone5": { "category": "people", "moji": "🤴🏿", + "description": "prince tone 5", "unicodeVersion": "9.0", "digest": "77d521148efa33fa4d3409693d050fecfd948411e807327484f174e289834649" }, "princess": { "category": "people", "moji": "👸", + "description": "princess", "unicodeVersion": "6.0", "digest": "efabd28480a843c735f0868734da2f9ce28133933b02ab07b645498f494f3f80" }, "princess_tone1": { "category": "people", "moji": "👸🏻", + "description": "princess tone 1", "unicodeVersion": "8.0", "digest": "52b88b99ba64f82e8f36e2a1827c85145e4fcd6863478c2345fe9fa9e8901cdf" }, "princess_tone2": { "category": "people", "moji": "👸🏼", + "description": "princess tone 2", "unicodeVersion": "8.0", "digest": "7e44289404693668f20e681fcdc2e516512d54a69c627eedae958f69dfe6eea9" }, "princess_tone3": { "category": "people", "moji": "👸🏽", + "description": "princess tone 3", "unicodeVersion": "8.0", "digest": "96c9a9857348d7a1a8be899c50d55b352b9a9fd5c65e4777bfa199fe7929d41c" }, "princess_tone4": { "category": "people", "moji": "👸🏾", + "description": "princess tone 4", "unicodeVersion": "8.0", "digest": "67696f96be60f2a36598072172d2db197d007e6c1ac3acef526a5ce6d59bf3f7" }, "princess_tone5": { "category": "people", "moji": "👸🏿", + "description": "princess tone 5", "unicodeVersion": "8.0", "digest": "007f624e2fad91bb57ce32ecd35213a796d71807f3b12f3f1575bf50e6a50eeb" }, "printer": { "category": "objects", "moji": "🖨", + "description": "printer", "unicodeVersion": "7.0", "digest": "5e5307e3dc7ec4e16c9978fb00934c99c4adefca7d32732a244d1f2de71ce6f8" }, "projector": { "category": "objects", "moji": "📽", + "description": "film projector", "unicodeVersion": "7.0", "digest": "7f8e1fdb89584849a56ee34c62cab808af48b7bd4823467d090af4657a2e0420" }, "punch": { "category": "people", "moji": "👊", + "description": "fisted hand sign", "unicodeVersion": "6.0", "digest": "c7e7edf6d64f755db3f02874354f08337b3971aff329476d19ac946e0b421329" }, "punch_tone1": { "category": "people", "moji": "👊🏻", + "description": "fisted hand sign tone 1", "unicodeVersion": "8.0", "digest": "c9ba508b0c36041047473782acfedab5af40dd7946b33daf4d8d54c726e06a11" }, "punch_tone2": { "category": "people", "moji": "👊🏼", + "description": "fisted hand sign tone 2", "unicodeVersion": "8.0", "digest": "d53011cd2f3334c7b3fffdfe1e2b8cc1c832c74306e1ac6d03f954a1309d7d0b" }, "punch_tone3": { "category": "people", "moji": "👊🏽", + "description": "fisted hand sign tone 3", "unicodeVersion": "8.0", "digest": "f7522347094e0130ed8e304678106574dbd7dd2b6b3aeb4d8a7a0fef880920b2" }, "punch_tone4": { "category": "people", "moji": "👊🏾", + "description": "fisted hand sign tone 4", "unicodeVersion": "8.0", "digest": "3e62bdd426f3e6ff175ce3b8dd6f6d3998d9c1506128defa96b528b455295b47" }, "punch_tone5": { "category": "people", "moji": "👊🏿", + "description": "fisted hand sign tone 5", "unicodeVersion": "8.0", "digest": "7d9bff777dc4ec41ac132b1252fa08cf92a398c8dc146c4a5327b45d568982d8" }, "purple_heart": { "category": "symbols", "moji": "💜", + "description": "purple heart", "unicodeVersion": "6.0", "digest": "a6bf01de806525942be480e45a4b2879f91df8129b78a1b8734d4f917bcab773" }, "purse": { "category": "people", "moji": "👛", + "description": "purse", "unicodeVersion": "6.0", "digest": "2b785f36e01875d66cfda2192c8c53606e7224a7c869a4826b62cb61613d60c8" }, "pushpin": { "category": "objects", "moji": "📌", + "description": "pushpin", "unicodeVersion": "6.0", "digest": "c3f7d7008be6bab8dc02284d4d759abf7aafbb3dbbe3a53f0f5b2ff685af88f8" }, "put_litter_in_its_place": { "category": "symbols", "moji": "🚮", + "description": "put litter in its place symbol", "unicodeVersion": "6.0", "digest": "f52a57d6f1bada7b6e6b9a6458597d70cb701c01e1120d8cb1d7ff65e01d405c" }, "question": { "category": "symbols", "moji": "❓", + "description": "black question mark ornament", "unicodeVersion": "6.0", "digest": "40050a1fd29bed321fd601d13dc33de5d6084121f1d873b29bde9dc3d823a310" }, "rabbit": { "category": "nature", "moji": "🐰", + "description": "rabbit face", "unicodeVersion": "6.0", "digest": "678ad953a7ab8f618c59051449a67c965d1f04f42dd6f6669adaf3fadebd080c" }, "rabbit2": { "category": "nature", "moji": "🐇", + "description": "rabbit", "unicodeVersion": "6.0", "digest": "19b1f5108292472434cc7a49efac4ea9275779735c7aeb0f15c36021d5998ca0" }, "race_car": { "category": "travel", "moji": "🏎", + "description": "racing car", "unicodeVersion": "7.0", "digest": "46f4814259d3d17ff35c04110e73e5327aee99f4711cd459ca1ee951508da3a6" }, "racehorse": { "category": "nature", "moji": "🐎", + "description": "horse", "unicodeVersion": "6.0", "digest": "a57b7aca35347ada8225eeee06b70cfd040484104963b4df56ea8fec690576b0" }, "radio": { "category": "objects", "moji": "📻", + "description": "radio", "unicodeVersion": "6.0", "digest": "9245951dd779cdd141089891b15a90d3999a6358acf1fc296aa505100f812108" }, "radio_button": { "category": "symbols", "moji": "🔘", + "description": "radio button", "unicodeVersion": "6.0", "digest": "565bec59198df2592e96564c6e314d3cde33c47b453db1bec6c5d027b5cb4fd9" }, "radioactive": { "category": "symbols", "moji": "☢", + "description": "radioactive sign", "unicodeVersion": "1.1", "digest": "0ed6634057824e0cfd10b2533753e3632b0624341a7eac8d9835706480335581" }, "rage": { "category": "people", "moji": "😡", + "description": "pouting face", "unicodeVersion": "6.0", "digest": "d97ba6bd08eec46dbc7199f530c945b73a87a878e35397b0a3e4f2b45039e89e" }, "railway_car": { "category": "travel", "moji": "🚃", + "description": "railway car", "unicodeVersion": "6.0", "digest": "2cddc08d555e7fc24e312c3d255ed013fbf9cd2974a6918369c32554049ba2be" }, "railway_track": { "category": "travel", "moji": "🛤", + "description": "railway track", "unicodeVersion": "7.0", "digest": "0da351b6d4e75c6beeaef1225e151d9580d4b5c41dfa1cf192715bf3cec981d7" }, "rainbow": { "category": "travel", "moji": "🌈", + "description": "rainbow", "unicodeVersion": "6.0", "digest": "a93aceb54e965f35e397e8c8716b1831614933308d026012d5464ee42783ed4d" }, "raised_back_of_hand": { "category": "people", "moji": "🤚", + "description": "raised back of hand", "unicodeVersion": "9.0", "digest": "20973a697e826625deba5ee3c4f25eb5e1737f2e860ac6fe4ee4d0e0c84b5e12" }, "raised_back_of_hand_tone1": { "category": "people", "moji": "🤚🏻", + "description": "raised back of hand tone 1", "unicodeVersion": "9.0", "digest": "06af5941255ca69d10d99d0a512bbda6141a296453835dbccf259ce0afe1dd3d" }, "raised_back_of_hand_tone2": { "category": "people", "moji": "🤚🏼", + "description": "raised back of hand tone 2", "unicodeVersion": "9.0", "digest": "429ed19555c9e5197b729b3e7bd8013346551051cb0b3fbc8a4372717c9a027d" }, "raised_back_of_hand_tone3": { "category": "people", "moji": "🤚🏽", + "description": "raised back of hand tone 3", "unicodeVersion": "9.0", "digest": "487a1c3f19e77c99b520ec073de2acc4a9e585b739a84b3989f7de85d2c2045c" }, "raised_back_of_hand_tone4": { "category": "people", "moji": "🤚🏾", + "description": "raised back of hand tone 4", "unicodeVersion": "9.0", "digest": "154254d8500c55ec3de698be4a352f9bcf06e2950cabc4eabaccad0f39a1e1e9" }, "raised_back_of_hand_tone5": { "category": "people", "moji": "🤚🏿", + "description": "raised back of hand tone 5", "unicodeVersion": "9.0", "digest": "6e9c0855ecd5f14adca5e5862427c3d39ffcf86f7ddd3aaa1fefc3cefc7483c8" }, "raised_hand": { "category": "people", "moji": "✋", + "description": "raised hand", "unicodeVersion": "6.0", "digest": "5cf11be683aea985d5ba51fbd44722c2327311bfe26b61c3d441c90f5d5a195a" }, "raised_hand_tone1": { "category": "people", "moji": "✋🏻", + "description": "raised hand tone 1", "unicodeVersion": "8.0", "digest": "865afca29b57577fed8fe8c2be57b74254a008c8cf34194680be2759239b5f5d" }, "raised_hand_tone2": { "category": "people", "moji": "✋🏼", + "description": "raised hand tone 2", "unicodeVersion": "8.0", "digest": "832169a0b626a682a58a3b998f68413657b4962c1fab05f1fdc2668e82727210" }, "raised_hand_tone3": { "category": "people", "moji": "✋🏽", + "description": "raised hand tone 3", "unicodeVersion": "8.0", "digest": "3959a873ad7671de82c615c4ed840b011e67baafb2bab7dd16859608d3e83cb1" }, "raised_hand_tone4": { "category": "people", "moji": "✋🏾", + "description": "raised hand tone 4", "unicodeVersion": "8.0", "digest": "db542f65d076ccf3dbfca27cb7c2f135a8bf7a487a81a04873e70172bdfcd579" }, "raised_hand_tone5": { "category": "people", "moji": "✋🏿", + "description": "raised hand tone 5", "unicodeVersion": "8.0", "digest": "88ca884d14baaae48df21d75c22d82fb15bdc395e42026f5ca34cd65e5ae8674" }, "raised_hands": { "category": "people", "moji": "🙌", + "description": "person raising both hands in celebration", "unicodeVersion": "6.0", "digest": "2ee73466a3f5079e542857fe6f5497e9f87753a81854985ce3356a8d3da1d8b8" }, "raised_hands_tone1": { "category": "people", "moji": "🙌🏻", + "description": "person raising both hands in celebration tone 1", "unicodeVersion": "8.0", "digest": "43e73c60f040a66374b8ec98f3629a90d13ae9f472446ed7676cd5573e824f4b" }, "raised_hands_tone2": { "category": "people", "moji": "🙌🏼", + "description": "person raising both hands in celebration tone 2", "unicodeVersion": "8.0", "digest": "fcc5255bb2b06dc82d6878e74cf34e8ce118c70004a06d39a980683772b98c52" }, "raised_hands_tone3": { "category": "people", "moji": "🙌🏽", + "description": "person raising both hands in celebration tone 3", "unicodeVersion": "8.0", "digest": "3ee3e0aafef486e766a166935e8147fb75a7329cfebc96dec876cc45e83a8754" }, "raised_hands_tone4": { "category": "people", "moji": "🙌🏾", + "description": "person raising both hands in celebration tone 4", "unicodeVersion": "8.0", "digest": "78a8cbf6b2b85be4d6b18f0ff6a77f197963117955725fb7e57e0441effb928f" }, "raised_hands_tone5": { "category": "people", "moji": "🙌🏿", + "description": "person raising both hands in celebration tone 5", "unicodeVersion": "8.0", "digest": "2a5ed7334a17172db0cd820a559e7f75df40ec44de6c25d194c76e1b58c634cb" }, "raising_hand": { "category": "people", "moji": "🙋", + "description": "happy person raising one hand", "unicodeVersion": "6.0", "digest": "512750b00704f1ccefd3c757743540b785ad7670dbbe4a2c4dca8d93e6701920" }, "raising_hand_tone1": { "category": "people", "moji": "🙋🏻", + "description": "happy person raising one hand tone1", "unicodeVersion": "8.0", "digest": "2897722f091c273dd3714cff7423c2475bc3070416c28014ca03322b9ece48bc" }, "raising_hand_tone2": { "category": "people", "moji": "🙋🏼", + "description": "happy person raising one hand tone2", "unicodeVersion": "8.0", "digest": "59199b334b3845911382c1f29bd7c0d5ef9d2486417345e265b166ead7d3e1c1" }, "raising_hand_tone3": { "category": "people", "moji": "🙋🏽", + "description": "happy person raising one hand tone3", "unicodeVersion": "8.0", "digest": "f95b338d5efcf14ef12f415a2c1bba93df48628ddc94f34f70c31e1b3c2e1d28" }, "raising_hand_tone4": { "category": "people", "moji": "🙋🏾", + "description": "happy person raising one hand tone4", "unicodeVersion": "8.0", "digest": "951ddbfdb57d5a60551b59b3d0f7ca00a64912f4a101a73afaebd68445cd6cec" }, "raising_hand_tone5": { "category": "people", "moji": "🙋🏿", + "description": "happy person raising one hand tone5", "unicodeVersion": "8.0", "digest": "9370f93704d8f89ca6dc946715eab5e7dba82bf04dd68c00f5c0abb8bc16371e" }, "ram": { "category": "nature", "moji": "🐏", + "description": "ram", "unicodeVersion": "6.0", "digest": "2875ab28e1018b39062aeb0c5ce488c48a98f13e9f2364470a0a700b126604f2" }, "ramen": { "category": "food", "moji": "🍜", + "description": "steaming bowl", "unicodeVersion": "6.0", "digest": "425662a49c4c13577c0de8d45d004e5ba204aaadbaabae62a5c283ecd7a9a2c5" }, "rat": { "category": "nature", "moji": "🐀", + "description": "rat", "unicodeVersion": "6.0", "digest": "14380d65498c6ce037c02a93bca2b24f25a368d85278d6015b8c9f7cd261f8e2" }, "record_button": { "category": "symbols", "moji": "⏺", + "description": "black circle for record", "unicodeVersion": "7.0", "digest": "92be12161ba206bb2e06a39131711c7b17368d55b4aae0b48f0ac5b6b1cde76b" }, "recycle": { "category": "symbols", "moji": "♻", + "description": "black universal recycling symbol", "unicodeVersion": "3.2", "digest": "c377e8537367b05b5de9be860a0fcabd7aed2bf4ba146eefc423671a21530369" }, "red_car": { "category": "travel", "moji": "🚗", + "description": "automobile", "unicodeVersion": "6.0", "digest": "8a99832a195263c0e922af53d52dea37aa3e07032b3c2a1977f8527b4a144b9c" }, "red_circle": { "category": "symbols", "moji": "🔴", + "description": "large red circle", "unicodeVersion": "6.0", "digest": "9dcf0132f6f2cc81702f0e3b15b37984e8439796705bf98f68ba449b3dfa5307" }, "registered": { "category": "symbols", "moji": "®", + "description": "registered sign", "unicodeVersion": "1.1", "digest": "9661b1df529ecb752d130820c55c403e5de263748eb02f7fea327818bc282d94" }, "relaxed": { "category": "people", "moji": "☺", + "description": "white smiling face", "unicodeVersion": "1.1", "digest": "2d5aed4fb8504c6d6660ef8d3bfe0cc053dcd6099c2f53748c202dc970c639bc" }, "relieved": { "category": "people", "moji": "😌", + "description": "relieved face", "unicodeVersion": "6.0", "digest": "b4ce2ba6c220d887fe5e333c05ed773df9b6df0ac456879fd8f5103ff68604a5" }, "reminder_ribbon": { "category": "activity", "moji": "🎗", + "description": "reminder ribbon", "unicodeVersion": "7.0", "digest": "c3de2a7c9350b77a0b86c0dcce9dcd9953ea8a97aa1e7aed149755924742f54d" }, "repeat": { "category": "symbols", "moji": "🔁", + "description": "clockwise rightwards and leftwards open circle arr", "unicodeVersion": "6.0", "digest": "b9512d508613ed0eb3181eb1030f7f6fd6b994476ecdfa308733c6df975fb99e" }, "repeat_one": { "category": "symbols", "moji": "🔂", + "description": "clockwise rightwards and leftwards open circle arr", "unicodeVersion": "6.0", "digest": "53409cf24dd4bb0d7b50ae359f15d06b87b7f4a292ed5c3a09652fa421a90bf2" }, "restroom": { "category": "symbols", "moji": "🚻", + "description": "restroom", "unicodeVersion": "6.0", "digest": "2e7a1bfc9a9d49b0272230a91db7369e24d54bf1de8e683d36b85f1d8c037f77" }, "revolving_hearts": { "category": "symbols", "moji": "💞", + "description": "revolving hearts", "unicodeVersion": "6.0", "digest": "c43d3197cb4cf06659f643638f6c4e91a2889e0f6531b7d81ea826c2a8b784fc" }, "rewind": { "category": "symbols", "moji": "⏪", + "description": "black left-pointing double triangle", "unicodeVersion": "6.0", "digest": "d20c918c1e528ff0947312738501ca9a6fb6ff4016aad07db7a8125d00fd65cd" }, "rhino": { "category": "nature", "moji": "🦏", + "description": "rhinoceros", "unicodeVersion": "9.0", "digest": "163fa3acd78eead72c431a1f48b8465a6d45272a9169560e456d30b4df93dc6b" }, "ribbon": { "category": "objects", "moji": "🎀", + "description": "ribbon", "unicodeVersion": "6.0", "digest": "74315fe907f9f0203afe139cd4552aa442eecfa2a64fac12db3e1292fc5a8828" }, "rice": { "category": "food", "moji": "🍚", + "description": "cooked rice", "unicodeVersion": "6.0", "digest": "f544f12606de59d28739798003f14ebd8869856add8e24496ec5dda3e131daf4" }, "rice_ball": { "category": "food", "moji": "🍙", + "description": "rice ball", "unicodeVersion": "6.0", "digest": "2cba6f5364cd366859bc8948897b65fc97b225ea7973d9be3b24aba388fed8e8" }, "rice_cracker": { "category": "food", "moji": "🍘", + "description": "rice cracker", "unicodeVersion": "6.0", "digest": "ac0f805d41d4f322154c1968bd3ce3e9aabcd39d908182e52fd7d28458dbef92" }, "rice_scene": { "category": "travel", "moji": "🎑", + "description": "moon viewing ceremony", "unicodeVersion": "6.0", "digest": "b942a06d3da0570aca59bab0af57cd8c16863934f12a38f70339fd0a36f675f5" }, "right_facing_fist": { "category": "people", "moji": "🤜", + "description": "right-facing fist", "unicodeVersion": "9.0", "digest": "f815d1cc0c0345ddcc8886ae9c133582d7dc779732ac9b93dde1ab4fdd3b251d" }, "right_facing_fist_tone1": { "category": "people", "moji": "🤜🏻", + "description": "right facing fist tone 1", "unicodeVersion": "9.0", "digest": "0f9269b70cf68071d97389e059a2bdacffd73f2afd2ce6cfd7447bb1a4e9abbb" }, "right_facing_fist_tone2": { "category": "people", "moji": "🤜🏼", + "description": "right facing fist tone 2", "unicodeVersion": "9.0", "digest": "32a9833db853972e49e65aa227fb0512c57362da190aa1cc40e1d64f238e837e" }, "right_facing_fist_tone3": { "category": "people", "moji": "🤜🏽", + "description": "right facing fist tone 3", "unicodeVersion": "9.0", "digest": "be4706f8bb088411f5cbbf9065a0ae5b773c97456bd975c2b6789765657847b9" }, "right_facing_fist_tone4": { "category": "people", "moji": "🤜🏾", + "description": "right facing fist tone 4", "unicodeVersion": "9.0", "digest": "1680862891a9d85c4b6f76232a80e2ef7428bcec93087c86eae2efaba9c6a3f7" }, "right_facing_fist_tone5": { "category": "people", "moji": "🤜🏿", + "description": "right facing fist tone 5", "unicodeVersion": "9.0", "digest": "388715a4bc2178c52bbb3bc2729f57be50acab5d751784c9f3220e86c6b1fbcc" }, "ring": { "category": "people", "moji": "💍", + "description": "ring", "unicodeVersion": "6.0", "digest": "b5322907222797b5e1786209cda88513e76cd397a40f0a7da24847245c95ef9d" }, "robot": { "category": "people", "moji": "🤖", + "description": "robot face", "unicodeVersion": "8.0", "digest": "4d788e6ec89279588b036fca6b17f5a981291681df8f90306ecf5c039de40848" }, "rocket": { "category": "travel", "moji": "🚀", + "description": "rocket", "unicodeVersion": "6.0", "digest": "b82e68a95aa89a6de344d6e256fef86a848ebc91de560b043b3e1f7fd072d57d" }, "rofl": { "category": "people", "moji": "🤣", + "description": "rolling on the floor laughing", "unicodeVersion": "9.0", "digest": "f4f99ba2ac67b97338f904f9384ff03fb832a2e427bf6e74611bf5fee45f1f48" }, "roller_coaster": { "category": "travel", "moji": "🎢", + "description": "roller coaster", "unicodeVersion": "6.0", "digest": "a65e9ace1d7900499777af1225995f17af90a398bb414764c20b6e09a8c23a2c" }, "rolling_eyes": { "category": "people", "moji": "🙄", + "description": "face with rolling eyes", "unicodeVersion": "8.0", "digest": "23dea8100da488a05721a4e82823eb438393b0ea762211c9ecef011d127aa1b7" }, "rooster": { "category": "nature", "moji": "🐓", + "description": "rooster", "unicodeVersion": "6.0", "digest": "2b90c5cf6fa46da13eb77285443d600afcea0c48bd1d215d60167e7dc510da5d" }, "rose": { "category": "nature", "moji": "🌹", + "description": "rose", "unicodeVersion": "6.0", "digest": "73799e459dba188de4de704605d824242feeb65d587c5bf9109acf528d037146" }, "rosette": { "category": "activity", "moji": "🏵", + "description": "rosette", "unicodeVersion": "7.0", "digest": "2537def4deef422d4e669b28b1a0675259306ab38601019df3ec3482b14e52d5" }, "rotating_light": { "category": "travel", "moji": "🚨", + "description": "police cars revolving light", "unicodeVersion": "6.0", "digest": "91fcdb85a752ae904d335a978c7e7936aed4c75d414b35219b5a74430e51555f" }, "round_pushpin": { "category": "objects", "moji": "📍", + "description": "round pushpin", "unicodeVersion": "6.0", "digest": "8ffca77bbdc6f1f726daf3abd6eff338a5ad1aa9b09dbbd8782c1e7ef5452f30" }, "rowboat": { "category": "activity", "moji": "🚣", + "description": "rowboat", "unicodeVersion": "6.0", "digest": "83715d83a061926d4ad3bb569b21f5d337e3ebd4c9bcdfe493e661c12adc0a16" }, "rowboat_tone1": { "category": "activity", "moji": "🚣🏻", + "description": "rowboat tone 1", "unicodeVersion": "8.0", "digest": "e279ac816442c0876fba1f42c700b80f2fb6de671e1a8a9e9d11b71eed5c58e8" }, "rowboat_tone2": { "category": "activity", "moji": "🚣🏼", + "description": "rowboat tone 2", "unicodeVersion": "8.0", "digest": "6a48eba352ed4971d26498b6c622e5772389c89c5205ed02acde8e995dddcc3b" }, "rowboat_tone3": { "category": "activity", "moji": "🚣🏽", + "description": "rowboat tone 3", "unicodeVersion": "8.0", "digest": "875948f6d8354ebd95ce9a66fde30f06a8366dcd89d5ca3e660845f8801e9305" }, "rowboat_tone4": { "category": "activity", "moji": "🚣🏾", + "description": "rowboat tone 4", "unicodeVersion": "8.0", "digest": "8c7ac7346b0020d0ff5e2f4a1efb1b7785eac637f17556663ec33e2335083f0a" }, "rowboat_tone5": { "category": "activity", "moji": "🚣🏿", + "description": "rowboat tone 5", "unicodeVersion": "8.0", "digest": "a399dbb647892b22323e0bf17bc36a9b5f1708ebedf9ba525233ee7b9d48339a" }, "rugby_football": { "category": "activity", "moji": "🏉", + "description": "rugby football", "unicodeVersion": "6.0", "digest": "cc6f00ade3e0bbb7899e7bfb138b57216dd66de26d7967d5ffa501f382ed09f4" }, "runner": { "category": "people", "moji": "🏃", + "description": "runner", "unicodeVersion": "6.0", "digest": "e9af7b591be60ade2049dbada0f062ba2d3e17f02bec76cbd34ce68854a2a10c" }, "runner_tone1": { "category": "people", "moji": "🏃🏻", + "description": "runner tone 1", "unicodeVersion": "8.0", "digest": "21091cbb09c558712ecf63548bf28b7995df42bdb85235088799a517800e52f5" }, "runner_tone2": { "category": "people", "moji": "🏃🏼", + "description": "runner tone 2", "unicodeVersion": "8.0", "digest": "1fe3d194f675a46fe67799394192e66c407dd81163363692c5e7da32ddb9af2b" }, "runner_tone3": { "category": "people", "moji": "🏃🏽", + "description": "runner tone 3", "unicodeVersion": "8.0", "digest": "8cea1bf4ef3be71f42dc5bae978d5b7a197a3851543225349ef0dda29a370537" }, "runner_tone4": { "category": "people", "moji": "🏃🏾", + "description": "runner tone 4", "unicodeVersion": "8.0", "digest": "c33f0b8b5a71d295fb6ba322e79446964a8eca9e4573efd591e4273808b088a0" }, "runner_tone5": { "category": "people", "moji": "🏃🏿", + "description": "runner tone 5", "unicodeVersion": "8.0", "digest": "9f59e6dd0fdf2f17bceb41f5c355b4e6f3c8bb8cbd8af0992f0b5630ff8892e8" }, "running_shirt_with_sash": { "category": "activity", "moji": "🎽", + "description": "running shirt with sash", "unicodeVersion": "6.0", "digest": "7542307d3595aca45e8ccae66b6e58b6e92870144b738263d5379ec6dc992b76" }, "sa": { "category": "symbols", "moji": "🈂", + "description": "squared katakana sa", "unicodeVersion": "6.0", "digest": "6042bcabd1516ef3847d695aba22851c49421244432d256e24eba04e8a223dab" }, "sagittarius": { "category": "symbols", "moji": "♐", + "description": "sagittarius", "unicodeVersion": "1.1", "digest": "a02593e025023f2e82a01c587a8c0bbb1eff88cbcabf535a1558413eb32ed1d5" }, "sailboat": { "category": "travel", "moji": "⛵", + "description": "sailboat", "unicodeVersion": "5.2", "digest": "c95ef4dc939cbdcb757ef3cd90331310e8c0a426add8cc800bae2540148a3195" }, "sake": { "category": "food", "moji": "🍶", + "description": "sake bottle and cup", "unicodeVersion": "6.0", "digest": "0a786075f3d9da48ae91afccf6ae0d097888da9509d354ee1d3cb99afcc88fe4" }, "salad": { "category": "food", "moji": "🥗", + "description": "green salad", "unicodeVersion": "9.0", "digest": "fe321487ab847abe670e68a83f1d9e096129741c689c769ee7de4a65aeac29f8" }, "sandal": { "category": "people", "moji": "👡", + "description": "womans sandal", "unicodeVersion": "6.0", "digest": "03c3077cb4bd900934f9bdf921165b465e5cc9a6bee53e45a091411bceb8892d" }, "santa": { "category": "people", "moji": "🎅", + "description": "father christmas", "unicodeVersion": "6.0", "digest": "178513e3d815917e59958870f5885b3414b43a16b8056980c863a468dfe00179" }, "santa_tone1": { "category": "people", "moji": "🎅🏻", + "description": "father christmas tone 1", "unicodeVersion": "8.0", "digest": "bf900bbc19bbd329229add9326e28e8197b69d6ddceb69f42162b0200fde5d16" }, "santa_tone2": { "category": "people", "moji": "🎅🏼", + "description": "father christmas tone 2", "unicodeVersion": "8.0", "digest": "7340f2171adab97198e3eecac8b0d84c4c2a41f84606301a0d10e9fe655c93d1" }, "santa_tone3": { "category": "people", "moji": "🎅🏽", + "description": "father christmas tone 3", "unicodeVersion": "8.0", "digest": "7368ab75454ec28d8f7d6baef6ad69b5278445a9f50753f6624731bffde32054" }, "santa_tone4": { "category": "people", "moji": "🎅🏾", + "description": "father christmas tone 4", "unicodeVersion": "8.0", "digest": "0ee60188353e0ee7772079c192bebbc6d49e74e63906f840c66da4eb35f4f245" }, "santa_tone5": { "category": "people", "moji": "🎅🏿", + "description": "father christmas tone 5", "unicodeVersion": "8.0", "digest": "e4378a0cc5d21e9b9fe6e35c32d1ebc6fb8c2e1c09554cd096aeaefd3a6eb511" }, "satellite": { "category": "objects", "moji": "📡", + "description": "satellite antenna", "unicodeVersion": "6.0", "digest": "c9d63118dcb445856917bb080460ab695cc78e715dcbba30ba18dffa9e906b27" }, "satellite_orbital": { "category": "travel", "moji": "🛰", + "description": "satellite", "unicodeVersion": "7.0", "digest": "beb2f50e7f2b010e76bed9daa95d7329a93c783d3ebc4f0b797dd721c5e3d32d" }, "saxophone": { "category": "activity", "moji": "🎷", + "description": "saxophone", "unicodeVersion": "6.0", "digest": "dfd138634f6702a3b89b5a2a50016720eef3f800b0d1d8c9fe097808c9491e96" }, "scales": { "category": "objects", "moji": "⚖", + "description": "scales", "unicodeVersion": "4.1", "digest": "2280c026f16c6b92e0daa00bc14e718770f8d231c571ab439bde84d837cf31cc" }, "school": { "category": "travel", "moji": "🏫", + "description": "school", "unicodeVersion": "6.0", "digest": "af198b068a86ccad3daec4c6873e6b4735086c1ecbb3848182e70bae9aa3ee24" }, "school_satchel": { "category": "people", "moji": "🎒", + "description": "school satchel", "unicodeVersion": "6.0", "digest": "f670ae8aea67eb9d8aaa0bf2748c1cc3e503dcc1dbe999133afcdf21af046b24" }, "scissors": { "category": "objects", "moji": "✂", + "description": "black scissors", "unicodeVersion": "1.1", "digest": "95225be28f05d8b5a6b6e6bf58d973f61f183ad4fef55a558dc1b810796b85c8" }, "scooter": { "category": "travel", "moji": "🛴", + "description": "scooter", "unicodeVersion": "9.0", "digest": "4a7db148880398db75e059711cb53edefb6b8fa9d442009f52856b887ab1dde4" }, "scorpion": { "category": "nature", "moji": "🦂", + "description": "scorpion", "unicodeVersion": "8.0", "digest": "d41119d1ea5daf727c17dbea7dadec1718c72fc9f98ae88252161df5fde0938a" }, "scorpius": { "category": "symbols", "moji": "♏", + "description": "scorpius", "unicodeVersion": "1.1", "digest": "a36404b408814c2ecb8fa8b61f5c5432dfcf54cae8c09cc67b8d0fadf7cbdc03" }, "scream": { "category": "people", "moji": "😱", + "description": "face screaming in fear", "unicodeVersion": "6.0", "digest": "916e4903a4b694da4b00f190f872a4e100e7736b7a2e6171fa1636f46bf646e6" }, "scream_cat": { "category": "people", "moji": "🙀", + "description": "weary cat face", "unicodeVersion": "6.0", "digest": "f1d3a6ff538064e7d5e0321bbc33aba44e8da703dc1894ef1403c0cd6d63d781" }, "scroll": { "category": "objects", "moji": "📜", + "description": "scroll", "unicodeVersion": "6.0", "digest": "9b2cb00860bcc2d20017cafb2ed9681b6232dc07273d489d75d53ce29e4ba3ab" }, "seat": { "category": "travel", "moji": "💺", + "description": "seat", "unicodeVersion": "6.0", "digest": "ae68d86fc2a07cae332451b23bd1ceba3f6526a6c56d8c1089777fa4632850e1" }, "second_place": { "category": "activity", "moji": "🥈", + "description": "second place medal", "unicodeVersion": "9.0", "digest": "9e2336fc16e532829b55380252f94655b58817d47c909fc2570002c5b06b9c40" }, "secret": { "category": "symbols", "moji": "㊙", + "description": "circled ideograph secret", "unicodeVersion": "1.1", "digest": "1d0b9adde2657f41421b135962de20820cf4b4eb0204044f9859522ab9d211b0" }, "see_no_evil": { "category": "nature", "moji": "🙈", + "description": "see-no-evil monkey", "unicodeVersion": "6.0", "digest": "3ff66d2e84b36d071d0a34f8e41cfd620a56b83131474ea50ed7803b635551ed" }, "seedling": { "category": "nature", "moji": "🌱", + "description": "seedling", "unicodeVersion": "6.0", "digest": "c0ec5e6d20e1afdc4e78eeddb1301c8b708ad6278e7287a4e4e825417c858e75" }, "selfie": { "category": "people", "moji": "🤳", + "description": "selfie", "unicodeVersion": "9.0", "digest": "2a1bc9f18ad4d6fb893d91c88ef1b2d9bd063dc2bb1a4b08c248c30f52545d4e" }, "selfie_tone1": { "category": "people", "moji": "🤳🏻", + "description": "selfie tone 1", "unicodeVersion": "9.0", "digest": "26dc212ffed30c276bd6a66a72bc4513e68098a2205fb4ca5b51ccfa1de5b544" }, "selfie_tone2": { "category": "people", "moji": "🤳🏼", + "description": "selfie tone 2", "unicodeVersion": "9.0", "digest": "71eceaefda46e3521f374f76693e7fa8f215067498067900080e2925ca94d7de" }, "selfie_tone3": { "category": "people", "moji": "🤳🏽", + "description": "selfie tone 3", "unicodeVersion": "9.0", "digest": "53eabbd4f6b8ebbd2f7af7bf5cd64309c4039ac1c5b2180290a547deaafcebdf" }, "selfie_tone4": { "category": "people", "moji": "🤳🏾", + "description": "selfie tone 4", "unicodeVersion": "9.0", "digest": "0baad378b09652b99c5d458db2e03b4db14a1557db4ea0969806a0ca1d33d40c" }, "selfie_tone5": { "category": "people", "moji": "🤳🏿", + "description": "selfie tone 5", "unicodeVersion": "9.0", "digest": "9a07608f34ec4dad48764a855f83f3965709d7b2fd2342e6dc9ed61f23f4adfd" }, "seven": { "category": "symbols", "moji": "7️⃣", + "description": "keycap digit seven", "unicodeVersion": "3.0", "digest": "ae85172d2c76c44afb4e3b45d277d400abb2dc895244b9abfbd1dac1cd7c53c2" }, "shallow_pan_of_food": { "category": "food", "moji": "🥘", + "description": "shallow pan of food", "unicodeVersion": "9.0", "digest": "7c7ad9d5d3f7226427d310b5853e8257fad899febe58dcbc5adb4677964f5c6d" }, "shamrock": { "category": "nature", "moji": "☘", + "description": "shamrock", "unicodeVersion": "4.1", "digest": "68ed70c26e04a818439a1742d2da6bc169edd02db86b6e6f8014b651f3235488" }, "shark": { "category": "nature", "moji": "🦈", + "description": "shark", "unicodeVersion": "9.0", "digest": "23a2364b6356e7bbb84c138e9cf58e2c68cd8caabb337a0c4d365ce87bf5d2da" }, "shaved_ice": { "category": "food", "moji": "🍧", + "description": "shaved ice", "unicodeVersion": "6.0", "digest": "54048e77268b7548d03088517bf8558d11324db901ca57f9bec93f1873663a74" }, "sheep": { "category": "nature", "moji": "🐑", + "description": "sheep", "unicodeVersion": "6.0", "digest": "c867c8e6e51768f1f51f4fe5abd3fbd5c1d69b01a3cb48b5fb94b6e2338a271c" }, "shell": { "category": "nature", "moji": "🐚", + "description": "spiral shell", "unicodeVersion": "6.0", "digest": "8983652d33ad6ab91195518cecb5a268a1c0ae603d271f0ddd756ff50058ddb3" }, "shield": { "category": "objects", "moji": "🛡", + "description": "shield", "unicodeVersion": "7.0", "digest": "763d0a56a62c51c730ccb0fbea38ab597cbf41a85ab968198e6ec35630d50aa5" }, "shinto_shrine": { "category": "travel", "moji": "⛩", + "description": "shinto shrine", "unicodeVersion": "5.2", "digest": "38a6d756c5aa9703510afa5076d75192f7814bbb6632394d4b8253d9ceda7f8c" }, "ship": { "category": "travel", "moji": "🚢", + "description": "ship", "unicodeVersion": "6.0", "digest": "79c680845892a3e81ec6af2160ee07c29147155943e5daba6c76d04252014c20" }, "shirt": { "category": "people", "moji": "👕", + "description": "t-shirt", "unicodeVersion": "6.0", "digest": "46c7253e15d7cac03699ddb1550fbb7565bbe487310f7e218c0583aa69f9d3c5" }, "shopping_bags": { "category": "objects", "moji": "🛍", + "description": "shopping bags", "unicodeVersion": "7.0", "digest": "95a3f03c675207bb1354270d02a630c204455c47b3edca23c48523a40cf3ea3b" }, "shopping_cart": { "category": "objects", "moji": "🛒", + "description": "shopping trolley", "unicodeVersion": "9.0", "digest": "4599b63f6861cdb4d8272cac84435c24c1d4d6a73c66d51e04a1cd14a1d333e6" }, "shower": { "category": "objects", "moji": "🚿", + "description": "shower", "unicodeVersion": "6.0", "digest": "6b3c767c0eb472d4861c6c3cc2735a5e2c09681872ef42a11dc89f3c80b9da01" }, "shrimp": { "category": "nature", "moji": "🦐", + "description": "shrimp", "unicodeVersion": "9.0", "digest": "b3651f3be3767125076a013fe903854f5b456a8afae865cb219cf528e0f44caa" }, "shrug": { "category": "people", "moji": "🤷", + "description": "shrug", "unicodeVersion": "9.0", "digest": "6e264243cc3b6e396069dea4357a958bdcd4081cb1af0ed6aa47235bef88cf27" }, "shrug_tone1": { "category": "people", "moji": "🤷🏻", + "description": "shrug tone 1", "unicodeVersion": "9.0", "digest": "0567b9fd95c8a857914003a5465a500ca79c8111811d45b865021b1b1d92d0b1" }, "shrug_tone2": { "category": "people", "moji": "🤷🏼", + "description": "shrug tone 2", "unicodeVersion": "9.0", "digest": "1557c2f5e3d4599c806d74c0b78afcca940678787534b6862bb89a20601bac8a" }, "shrug_tone3": { "category": "people", "moji": "🤷🏽", + "description": "shrug tone 3", "unicodeVersion": "9.0", "digest": "f02754541a7bf74ba7eebe6c27daf1e3e1dac25172c35b8ba45641e278dfda3d" }, "shrug_tone4": { "category": "people", "moji": "🤷🏾", + "description": "shrug tone 4", "unicodeVersion": "9.0", "digest": "2b5121164cb5f4e253d8fb31f6445cf8afaf30dba41732edc511440cdb78d15c" }, "shrug_tone5": { "category": "people", "moji": "🤷🏿", + "description": "shrug tone 5", "unicodeVersion": "9.0", "digest": "62d99a26bbad479f574f66208c41b9960cd41fb9d79d3a13fbdaa44682077115" }, "signal_strength": { "category": "symbols", "moji": "📶", + "description": "antenna with bars", "unicodeVersion": "6.0", "digest": "2c6f04ba4ecd2d2d423e19eb52cfbfd253f4db6e0908d91c1af4ea6192597447" }, "six": { "category": "symbols", "moji": "6️⃣", + "description": "keycap digit six", "unicodeVersion": "3.0", "digest": "cede9324261208d0fd5d00fcdfc0df0331944bd9cff4f40b30a582a641526c1c" }, "six_pointed_star": { "category": "symbols", "moji": "🔯", + "description": "six pointed star with middle dot", "unicodeVersion": "6.0", "digest": "9203e3b4f08af439ae0bfb6a7b29a02dceb027b6c2dc5463b524dfd314cbff4e" }, "ski": { "category": "activity", "moji": "🎿", + "description": "ski and ski boot", "unicodeVersion": "6.0", "digest": "80f0ca8660ba373fef823af9e98e148c4ddb1e217eb6d0a0ea2bae2288b57570" }, "skier": { "category": "activity", "moji": "⛷", + "description": "skier", "unicodeVersion": "5.2", "digest": "4fff0aa155367f551a59aed9657b8afa159173882b25db9cd8434293d1eed76d" }, "skull": { "category": "people", "moji": "💀", + "description": "skull", "unicodeVersion": "6.0", "digest": "cdd2031164281bf2b0083df4479651d96bc16d11e44bac4deaf402a9c0d6f40a" }, "skull_crossbones": { "category": "objects", "moji": "☠", + "description": "skull and crossbones", "unicodeVersion": "1.1", "digest": "ae764ba21a1fcc4409f4cc9e75a261d70b87548f64158dbd3451374ad5724123" }, "sleeping": { "category": "people", "moji": "😴", + "description": "sleeping face", "unicodeVersion": "6.1", "digest": "1050a011509b56735c9f30a6fccc876256e2a4546dc6052e518151c8aca4b526" }, "sleeping_accommodation": { "category": "objects", "moji": "🛌", + "description": "sleeping accommodation", "unicodeVersion": "7.0", "digest": "2ce42c027d1d0947abc403c359fd668a7bc44f5ead2582e97f3db7dd4e22e5d5" }, "sleepy": { "category": "people", "moji": "😪", + "description": "sleepy face", "unicodeVersion": "6.0", "digest": "2ee9bb1f72ef99e0e33095ec2bbf7a58ffea0ff7d40b840f4cdba57be9de74b0" }, "slight_frown": { "category": "people", "moji": "🙁", + "description": "slightly frowning face", "unicodeVersion": "7.0", "digest": "d71d564a6c2d366a8e28a78ef4e07d387a77037fe8c99aa0ea1571299dc490c9" }, "slight_smile": { "category": "people", "moji": "🙂", + "description": "slightly smiling face", "unicodeVersion": "7.0", "digest": "10f4b66a755f5c78762a330f20d1866e4a22f3f1d495161d758d3bab8d2f36fe" }, "slot_machine": { "category": "activity", "moji": "🎰", + "description": "slot machine", "unicodeVersion": "6.0", "digest": "914184788f8cd865cd074dca25c22acee31f5498117bd9a6e78cae67e6601652" }, "small_blue_diamond": { "category": "symbols", "moji": "🔹", + "description": "small blue diamond", "unicodeVersion": "6.0", "digest": "0b56d8e6b5ddf1f49fcc76e45e5fb2ee9f99ae6ffe682c26eaea4d9b7faac36c" }, "small_orange_diamond": { "category": "symbols", "moji": "🔸", + "description": "small orange diamond", "unicodeVersion": "6.0", "digest": "a2235830550e289c1608f2dcf5ede48f5c1a0eff45570699c39708c9677ab950" }, "small_red_triangle": { "category": "symbols", "moji": "🔺", + "description": "up-pointing red triangle", "unicodeVersion": "6.0", "digest": "8c2985c4e9ce42d2f3b35539b879bc36206c5ef749f39fbd1eac51bd2676e1e5" }, "small_red_triangle_down": { "category": "symbols", "moji": "🔻", + "description": "down-pointing red triangle", "unicodeVersion": "6.0", "digest": "46bd328df2fbf5d0597596bbf00d2d5f6e0c65bcb8f3fb325df8ba0c25e445b5" }, "smile": { "category": "people", "moji": "😄", + "description": "smiling face with open mouth and smiling eyes", "unicodeVersion": "6.0", "digest": "14905c372d5bf7719bd727c9efae31a03291acec79801652a23710c6848c5d14" }, "smile_cat": { "category": "people", "moji": "😸", + "description": "grinning cat face with smiling eyes", "unicodeVersion": "6.0", "digest": "c35b76d6df100edb4022d762f47abfeb9f5e70886960c1d25908bd5d57ccb47e" }, "smiley": { "category": "people", "moji": "😃", + "description": "smiling face with open mouth", "unicodeVersion": "6.0", "digest": "a89f31eb9d814636852517a7f4eadec59195e2ac2cc9f8d124f1a1cc0f775b4a" }, "smiley_cat": { "category": "people", "moji": "😺", + "description": "smiling cat face with open mouth", "unicodeVersion": "6.0", "digest": "3e66a113c5e3e73fb94be29084cb27986b6bdb0e78ab44785bf2a35a550e71bf" }, "smiling_imp": { "category": "people", "moji": "😈", + "description": "smiling face with horns", "unicodeVersion": "6.0", "digest": "3e02131d16525938f6facc7e097365dec7e13c8a0049a3be35fc29c80cc291b3" }, "smirk": { "category": "people", "moji": "😏", + "description": "smirking face", "unicodeVersion": "6.0", "digest": "3c180d46f5574d6fca3bb68eb02517da60b7008843cb3e90f2f9620d0c8ee943" }, "smirk_cat": { "category": "people", "moji": "😼", + "description": "cat face with wry smile", "unicodeVersion": "6.0", "digest": "0683c7f73e1f65984e91313607d7cca21d99acd4b2e9932f00e0fffd0ce90742" }, "smoking": { "category": "objects", "moji": "🚬", + "description": "smoking symbol", "unicodeVersion": "6.0", "digest": "baa9cb444bf0fe5c74358f981b19bc9e5c0415ced7f042baf93642282476ea61" }, "snail": { "category": "nature", "moji": "🐌", + "description": "snail", "unicodeVersion": "6.0", "digest": "5733bf3672ae4b2b3e090fa670aeac70dcbcc04ca5b13abc8c8e53b8b3d4ff33" }, "snake": { "category": "nature", "moji": "🐍", + "description": "snake", "unicodeVersion": "6.0", "digest": "18da2d97c771149ef5454dd23470e900903a62ab93f9e2ce301aad5a8181d773" }, "sneezing_face": { "category": "people", "moji": "🤧", + "description": "sneezing face", "unicodeVersion": "9.0", "digest": "c20ef571dc7e35572fe3c18b7845aefc89af083ea925c48a29de3b7387af6e17" }, "snowboarder": { "category": "activity", "moji": "🏂", + "description": "snowboarder", "unicodeVersion": "6.0", "digest": "c6e074139b851aa53b1ba6464d84da14b3da7412fc44c6c196a8469d76915c19" }, "snowflake": { "category": "nature", "moji": "❄", + "description": "snowflake", "unicodeVersion": "1.1", "digest": "6556c918e181df01ba849e76c43972d5310439971e5d8fc2409d112c05bf0028" }, "snowman": { "category": "nature", "moji": "⛄", + "description": "snowman without snow", "unicodeVersion": "5.2", "digest": "6137456b2335e88e09c1859615eb22bb636355ef438f7a3949ad2f3d54478dd3" }, "snowman2": { "category": "nature", "moji": "☃", + "description": "snowman", "unicodeVersion": "1.1", "digest": "33ec75c22a13c81fa3c6b24a77ac1a08dc0dbe70b3716cf17b6702014d8a63fe" }, "sob": { "category": "people", "moji": "😭", + "description": "loudly crying face", "unicodeVersion": "6.0", "digest": "d1ed4b31861f9f9fd4e9c95a9c17530e2320a1b4cad6ececb1545ce25d65e4ce" }, "soccer": { "category": "activity", "moji": "⚽", + "description": "soccer ball", "unicodeVersion": "5.2", "digest": "6a3f2e6a9a0b64c3fbf8705995792091daf386a4112dba75507a1f556f662f84" }, "soon": { "category": "symbols", "moji": "🔜", + "description": "soon with rightwards arrow above", "unicodeVersion": "6.0", "digest": "a49d1bcfbac3e6ccc05b9a9863eff74b0eb8b4d4b22b8b0f7b2787fcba1c73cc" }, "sos": { "category": "symbols", "moji": "🆘", + "description": "squared sos", "unicodeVersion": "6.0", "digest": "2fa7e0274383aeed6019eb9177e778d7aab8b88575b078b0ffeb77cd18df14b3" }, "sound": { "category": "symbols", "moji": "🔉", + "description": "speaker with one sound wave", "unicodeVersion": "6.0", "digest": "faaca7b315b2495cbc381468580d25f1d11362441c35bb43d8a914f2ec8202d2" }, "space_invader": { "category": "activity", "moji": "👾", + "description": "alien monster", "unicodeVersion": "6.0", "digest": "e75379cb5063f9a8861d762ad1886097c1697fbb61f2e4e8f531047955a4a2dd" }, "spades": { "category": "symbols", "moji": "♠", + "description": "black spade suit", "unicodeVersion": "1.1", "digest": "2c4d20f6a4893cfc62498d3f1f8f67577f39ed09f3e6682d8cb9cd8f365d30da" }, "spaghetti": { "category": "food", "moji": "🍝", + "description": "spaghetti", "unicodeVersion": "6.0", "digest": "6d3451dc0faa1913539edb99261448f51735f269b61193c53dfe63466c0191e8" }, "sparkle": { "category": "symbols", "moji": "❇", + "description": "sparkle", "unicodeVersion": "1.1", "digest": "7131163cd6c2f879110c86e9f068c33cf580f7c4b619449c41851fe6083402ee" }, "sparkler": { "category": "travel", "moji": "🎇", + "description": "firework sparkler", "unicodeVersion": "6.0", "digest": "88539ed8a13bd66e0c265c0913bd3ec2ddc4d95484323595713beb102221a1f6" }, "sparkles": { "category": "nature", "moji": "✨", + "description": "sparkles", "unicodeVersion": "6.0", "digest": "cf84d16b1c0a381d5a7ae79031872747c9a6887eab6e92cc4a10a4b8600ef506" }, "sparkling_heart": { "category": "symbols", "moji": "💖", + "description": "sparkling heart", "unicodeVersion": "6.0", "digest": "b80b1ddef83b6528b309a194f6f2faf5acab603daeb9254523efc2b941bcb6d2" }, "speak_no_evil": { "category": "nature", "moji": "🙊", + "description": "speak-no-evil monkey", "unicodeVersion": "6.0", "digest": "d2d7cfb4d471928a496bdc146890adc8422a68500b68115630b24c125d18e81f" }, "speaker": { "category": "symbols", "moji": "🔈", + "description": "speaker", "unicodeVersion": "6.0", "digest": "dbca5f7181728d2ad67ff76fd566ffbdf53e333e7eeed341f54668bd47969413" }, "speaking_head": { "category": "people", "moji": "🗣", + "description": "speaking head in silhouette", "unicodeVersion": "7.0", "digest": "4be1af79b4506c00af4df64663413bcbae195dab0bc63c5011feb8f9663ed544" }, "speech_balloon": { "category": "symbols", "moji": "💬", + "description": "speech balloon", "unicodeVersion": "6.0", "digest": "817100d9979456e7d2f253ac22e13b7a2302dc1590566214915b003e403c53ca" }, "speedboat": { "category": "travel", "moji": "🚤", + "description": "speedboat", "unicodeVersion": "6.0", "digest": "a523b2320f0b24be1e9fdbc1ff828e28d8fd9a64d51e5888ab453ef0bc9f0576" }, "spider": { "category": "nature", "moji": "🕷", + "description": "spider", "unicodeVersion": "7.0", "digest": "8411eac0c1b80926fd93cc1d6423e00b05d04c485b79ee232da8f1714e899a37" }, "spider_web": { "category": "nature", "moji": "🕸", + "description": "spider web", "unicodeVersion": "7.0", "digest": "2434bdfbe56dcc4a43699dd59b638af431486b52fb1d6d685451f3b231b2be23" }, "spoon": { "category": "food", "moji": "🥄", + "description": "spoon", "unicodeVersion": "9.0", "digest": "4fa31d59e5bffd2c45a8e01fcd5652e78a5691cbfa744e69882bc67173ddea05" }, "spy": { "category": "people", "moji": "🕵", + "description": "sleuth or spy", "unicodeVersion": "7.0", "digest": "99fe3cdeff934726ee5855b0e401bf32570084aaad4eb10df837fd410ca742aa" }, "spy_tone1": { "category": "people", "moji": "🕵🏻", + "description": "sleuth or spy tone 1", "unicodeVersion": "8.0", "digest": "1720a99064061c43c7647b6bd517efa2ee2621b355a644adfb347d62849366a2" }, "spy_tone2": { "category": "people", "moji": "🕵🏼", + "description": "sleuth or spy tone 2", "unicodeVersion": "8.0", "digest": "23ff0026723f2b5a46fbfb55e24c4a4a33af2bd96808b3ea3af76aae99965d68" }, "spy_tone3": { "category": "people", "moji": "🕵🏽", + "description": "sleuth or spy tone 3", "unicodeVersion": "8.0", "digest": "1d0cb3d54fb61e4763a4f0642ef32094bdd40832be0d42799ce9ba69773616df" }, "spy_tone4": { "category": "people", "moji": "🕵🏾", + "description": "sleuth or spy tone 4", "unicodeVersion": "8.0", "digest": "e36a4b52df6cb954fab9d9128111f1301c6d46bdeacf51993ffb5bb354cd0ad3" }, "spy_tone5": { "category": "people", "moji": "🕵🏿", + "description": "sleuth or spy tone 5", "unicodeVersion": "8.0", "digest": "ffc6fefd9a537124ebf0a9ddf387414dce1291335026064644f6cf9315591129" }, "squid": { "category": "nature", "moji": "🦑", + "description": "squid", "unicodeVersion": "9.0", "digest": "65a1b318c2c506b9d26cfd8282a5cf9922109595c8d12e92c3f7481ac7c08c49" }, "stadium": { "category": "travel", "moji": "🏟", + "description": "stadium", "unicodeVersion": "7.0", "digest": "73bf955e767ba1518c9c92b2ba59a2aa1ec4b018652dffd97bcd74832a33789f" }, "star": { "category": "nature", "moji": "⭐", + "description": "white medium star", "unicodeVersion": "5.1", "digest": "d78e5c1b78caed103e100150c10b08a9ca3ee30c243943d6fc3cc08f422122e9" }, "star2": { "category": "nature", "moji": "🌟", + "description": "glowing star", "unicodeVersion": "6.0", "digest": "f91ac4afe3f5d4a52847ae8b4a9704b591e00399aebba553d150d7e34ee939fa" }, "star_and_crescent": { "category": "symbols", "moji": "☪", + "description": "star and crescent", "unicodeVersion": "1.1", "digest": "1bf3d29e50034f5e7c0dccff0a3a533b74bfa9b489e357b2739a473311f1332a" }, "star_of_david": { "category": "symbols", "moji": "✡", + "description": "star of david", "unicodeVersion": "1.1", "digest": "28a0bd0eeac9d0835ceb8425d72c2472464e863dd09b76a0ddc1c08cf1986402" }, "stars": { "category": "travel", "moji": "🌠", + "description": "shooting star", "unicodeVersion": "6.0", "digest": "837d9045316b8fb5e533457eac61241534f641eb78d8cb75f688f80fb8e8a7f0" }, "station": { "category": "travel", "moji": "🚉", + "description": "station", "unicodeVersion": "6.0", "digest": "27a163ac0aea4ed247a121cae826eafc475977c68b0d888e9405bea14326ff56" }, "statue_of_liberty": { "category": "travel", "moji": "🗽", + "description": "statue of liberty", "unicodeVersion": "6.0", "digest": "f5a43599ab3f24ed3a78a745e06e2ac3e33107a292386ad81c67935ee5b22493" }, "steam_locomotive": { "category": "travel", "moji": "🚂", + "description": "steam locomotive", "unicodeVersion": "6.0", "digest": "52ad0073f37b978faf3884fb193046f2b0614e1557bbcc9de1b020e42aff2dba" }, "stew": { "category": "food", "moji": "🍲", + "description": "pot of food", "unicodeVersion": "6.0", "digest": "c16f61236db314ad8d9f2dd241ec1e15c8d64e5872cce93ec4d0996490dd39df" }, "stop_button": { "category": "symbols", "moji": "⏹", + "description": "black square for stop", "unicodeVersion": "7.0", "digest": "83f9d0da3ad845fef41b4e8336815d30e9c8f042ab2a8340894ade2f428fc98a" }, "stopwatch": { "category": "objects", "moji": "⏱", + "description": "stopwatch", "unicodeVersion": "6.0", "digest": "9b6b9491a24d8ab4f896eb876da7973f028bd5e7c51a3767ba7e61bb6fbb2be0" }, "straight_ruler": { "category": "objects", "moji": "📏", + "description": "straight ruler", "unicodeVersion": "6.0", "digest": "cee31101767bd3f961363599924dc3790675d05a1285a8396428d2f91771c111" }, "strawberry": { "category": "food", "moji": "🍓", + "description": "strawberry", "unicodeVersion": "6.0", "digest": "5750a15e12f21259286ddbc3a8222a385b3b97a9f368897f42dd000060343174" }, "stuck_out_tongue": { "category": "people", "moji": "😛", + "description": "face with stuck-out tongue", "unicodeVersion": "6.1", "digest": "92dc42980a6dfdd7204fc874a762d6a0bbf0fdbfb5a7c0698fca04782e99fde6" }, "stuck_out_tongue_closed_eyes": { "category": "people", "moji": "😝", + "description": "face with stuck-out tongue and tightly-closed eyes", "unicodeVersion": "6.0", "digest": "434d25ac24cad7ba699eae876a25d9a99b584449cca50b124bf6aa7f20a83d51" }, "stuck_out_tongue_winking_eye": { "category": "people", "moji": "😜", + "description": "face with stuck-out tongue and winking eye", "unicodeVersion": "6.0", "digest": "dbacd6428a2a2933212e6a4dc0c7f302177fb23b963626ccb26f27f91737f03d" }, "stuffed_flatbread": { "category": "food", "moji": "🥙", + "description": "stuffed flatbread", "unicodeVersion": "9.0", "digest": "9f841f2520640d69be4f20a3199023d5811842b28556b5e1152e5ec11f0fda07" }, "sun_with_face": { "category": "nature", "moji": "🌞", + "description": "sun with face", "unicodeVersion": "6.0", "digest": "7256ff5263006c64c03f1eb66e3ddb56d67d785d65dacc37aa886d0cd4be63be" }, "sunflower": { "category": "nature", "moji": "🌻", + "description": "sunflower", "unicodeVersion": "6.0", "digest": "27d1161f50f932a6b26c404cf2e8f7083683ed0f2382d62b7472acccaa6eb695" }, "sunglasses": { "category": "people", "moji": "😎", + "description": "smiling face with sunglasses", "unicodeVersion": "6.0", "digest": "966684382e5c59e98319e4c0ea7c304c61c2638ad5408faa49ce2c83c4416757" }, "sunny": { "category": "nature", "moji": "☀", + "description": "black sun with rays", "unicodeVersion": "1.1", "digest": "460fea4cbbdd1595450c1033a2ee5de7fea2e2f147822efa49f7e204812415aa" }, "sunrise": { "category": "travel", "moji": "🌅", + "description": "sunrise", "unicodeVersion": "6.0", "digest": "7718a49636b0cdd1862ed67c7a9d6e72f471c2591ff0d912485b1be55d1ea115" }, "sunrise_over_mountains": { "category": "travel", "moji": "🌄", + "description": "sunrise over mountains", "unicodeVersion": "6.0", "digest": "743d0701cdbe2a814962363813c3153d3c5e62c3e410349f56d49dbb9581f356" }, "surfer": { "category": "activity", "moji": "🏄", + "description": "surfer", "unicodeVersion": "6.0", "digest": "bb440775e9213430942015c37db8de58b5a561ee971b2a0f3993fc3f1d2554d4" }, "surfer_tone1": { "category": "activity", "moji": "🏄🏻", + "description": "surfer tone 1", "unicodeVersion": "8.0", "digest": "a4937b030aca30b68bb644f37cf63c38aebce3c00b57d1c8a0ffe596b57d2f1e" }, "surfer_tone2": { "category": "activity", "moji": "🏄🏼", + "description": "surfer tone 2", "unicodeVersion": "8.0", "digest": "1c2a954a9c5284dedf0327d6f3c954c9fdd3953b848076d298874775ad8bf0a3" }, "surfer_tone3": { "category": "activity", "moji": "🏄🏽", + "description": "surfer tone 3", "unicodeVersion": "8.0", "digest": "418a3408b9ab026124f067c8597b500217e56bc28d9844a29eea5eee6f604ff8" }, "surfer_tone4": { "category": "activity", "moji": "🏄🏾", + "description": "surfer tone 4", "unicodeVersion": "8.0", "digest": "530870b9ac9f4d45ff750e264feb90b44fb93ca2852f323987b06f5f12fb5a4d" }, "surfer_tone5": { "category": "activity", "moji": "🏄🏿", + "description": "surfer tone 5", "unicodeVersion": "8.0", "digest": "40e11b1ae652cfd085d083377f1da24160065ed1b67403c6fa4655e6e44169ec" }, "sushi": { "category": "food", "moji": "🍣", + "description": "sushi", "unicodeVersion": "6.0", "digest": "b924c621236ca3284b349b0509ae1043f2fc2c7f6d67615716f9717ada78c992" }, "suspension_railway": { "category": "travel", "moji": "🚟", + "description": "suspension railway", "unicodeVersion": "6.0", "digest": "cd3d21da79864f0c018b863e82fb0561fff3c5e3c065303cfcb89c3663d638ba" }, "sweat": { "category": "people", "moji": "😓", + "description": "face with cold sweat", "unicodeVersion": "6.0", "digest": "1aa771479aa1ac5eeea4bafbe93ebd85a0f692f6d869034f31e25b689c2e264d" }, "sweat_drops": { "category": "nature", "moji": "💦", + "description": "splashing sweat symbol", "unicodeVersion": "6.0", "digest": "b575b85415bc9852cf6415d417ebf799167fde03c6819ebcaa24ae1b3dde8dab" }, "sweat_smile": { "category": "people", "moji": "😅", + "description": "smiling face with open mouth and cold sweat", "unicodeVersion": "6.0", "digest": "171b0d0845d46c33bedb6d3b39fb1ff366e22ba90685eedabebd91bb2b0680de" }, "sweet_potato": { "category": "food", "moji": "🍠", + "description": "roasted sweet potato", "unicodeVersion": "6.0", "digest": "4b91920f0b87d42763313bc476f4c821a74e4c12dc1c92165a859dddeaaf8844" }, "swimmer": { "category": "activity", "moji": "🏊", + "description": "swimmer", "unicodeVersion": "6.0", "digest": "2c4ed4a51aad99d9957ae11a219d5164db9748fc3a65002c6085a9f15adfa9e2" }, "swimmer_tone1": { "category": "activity", "moji": "🏊🏻", + "description": "swimmer tone 1", "unicodeVersion": "8.0", "digest": "48588f129ee4af52ca2e0f4594213391978601087cd607896b2f979ca077284b" }, "swimmer_tone2": { "category": "activity", "moji": "🏊🏼", + "description": "swimmer tone 2", "unicodeVersion": "8.0", "digest": "fff209448524bd1ef4d6decabf6c1ead94c8d3d5b1bfb5e54f20cc8e139232fc" }, "swimmer_tone3": { "category": "activity", "moji": "🏊🏽", + "description": "swimmer tone 3", "unicodeVersion": "8.0", "digest": "2003932cb2cf4ae9a10b23338bf375a9293fb18c0ecf91bdfae73be6eebb3800" }, "swimmer_tone4": { "category": "activity", "moji": "🏊🏾", + "description": "swimmer tone 4", "unicodeVersion": "8.0", "digest": "20b4bff9baa1c694ad98067dde834c56092f023b9664bec382c2e512232bd480" }, "swimmer_tone5": { "category": "activity", "moji": "🏊🏿", + "description": "swimmer tone 5", "unicodeVersion": "8.0", "digest": "0ff8eb57c2be8e80a1bc6ba75b8d9ffb9bd8d3be636150c4c03399ec1886f218" }, "symbols": { "category": "symbols", "moji": "🔣", + "description": "input symbol for symbols", "unicodeVersion": "6.0", "digest": "2a2a79816c4d0751a0d73586eec5e63b410653d3c85cc968906bf1fc03d89b94" }, "synagogue": { "category": "travel", "moji": "🕍", + "description": "synagogue", "unicodeVersion": "8.0", "digest": "98569cdd7c61528963b67b7891dfa46025c5e810cbb22ee18ddb3bd85de2da69" }, "syringe": { "category": "objects", "moji": "💉", + "description": "syringe", "unicodeVersion": "6.0", "digest": "e1538e645ccc571227c994b71b3d1be2c4d072d8bd9c944a42ff4a11c91a34a6" }, "taco": { "category": "food", "moji": "🌮", + "description": "taco", "unicodeVersion": "8.0", "digest": "e1e45aefdb7445faeae75c3831df6a3d6f2590fcdd48a20d847593c246df613b" }, "tada": { "category": "objects", "moji": "🎉", + "description": "party popper", "unicodeVersion": "6.0", "digest": "1d2e6cbb2a3244240bc70209715d2213d1efee2e370cccfbcc046c333ae2d650" }, "tanabata_tree": { "category": "nature", "moji": "🎋", + "description": "tanabata tree", "unicodeVersion": "6.0", "digest": "592f2907ffc1b914390e1a106c15120ff3607e99192158b94d237975647c5540" }, "tangerine": { "category": "food", "moji": "🍊", + "description": "tangerine", "unicodeVersion": "6.0", "digest": "40c9ddcde1b0bcfaeb466629a87825eb8c2037835720cbee5e2fda04be3c8d0a" }, "taurus": { "category": "symbols", "moji": "♉", + "description": "taurus", "unicodeVersion": "1.1", "digest": "21cf24cb6410ab6596e2df8b3e242cc07f9dbb247eabc00c590fe184b373d068" }, "taxi": { "category": "travel", "moji": "🚕", + "description": "taxi", "unicodeVersion": "6.0", "digest": "c546cc743831cfbf0c15452767cf2a4faf3775066797e997ae7c1fcbe4eca479" }, "tea": { "category": "food", "moji": "🍵", + "description": "teacup without handle", "unicodeVersion": "6.0", "digest": "00e3f1e389fa58c4fcd8c53ebbf83d25872f4315845ab1984b35410ae65553d9" }, "telephone": { "category": "objects", "moji": "☎", + "description": "black telephone", "unicodeVersion": "1.1", "digest": "3a53851e641f8ad938ce3597b1afca2ea63c9314ff81f62563b99937496a13d7" }, "telephone_receiver": { "category": "objects", "moji": "📞", + "description": "telephone receiver", "unicodeVersion": "6.0", "digest": "1614d67f3d8814b0d75f39d55f9149e4d28ef57b343498625e62fcfff8365046" }, "telescope": { "category": "objects", "moji": "🔭", + "description": "telescope", "unicodeVersion": "6.0", "digest": "4adf40387870276c4f59fb050d441023e8dac784365b6a8c0282fb519780b495" }, "ten": { "category": "symbols", "moji": "🔟", + "description": "keycap ten", "unicodeVersion": "6.0", "digest": "c7c9491021740d2c17edddb856f79579b0b943d8dc85a2f48dbaac84f35b8a40" }, "tennis": { "category": "activity", "moji": "🎾", + "description": "tennis racquet and ball", "unicodeVersion": "6.0", "digest": "dc1600b4d8dce3d26259eb0d1c6ab042566565e3c1f2c96112210f1550a716fd" }, "tent": { "category": "travel", "moji": "⛺", + "description": "tent", "unicodeVersion": "5.2", "digest": "30d9b17ac3219d4970ddf54d7c1a288b0ae50f7f3b82ed232c0b1b19ef585662" }, "thermometer": { "category": "objects", "moji": "🌡", + "description": "thermometer", "unicodeVersion": "7.0", "digest": "66616babbcaef256d7b652796c760e8e893cb950c073348a408fe70904f80f25" }, "thermometer_face": { "category": "people", "moji": "🤒", + "description": "face with thermometer", "unicodeVersion": "8.0", "digest": "ac2b5caddd128563711a9dcc7f690cf210f684d5e8b64b09c0431d6902437126" }, "thinking": { "category": "people", "moji": "🤔", + "description": "thinking face", "unicodeVersion": "8.0", "digest": "4f0b84e5ab8a650cafb166e93688f0e9b31b9ade22a91035261ac90490edb9d3" }, "third_place": { "category": "activity", "moji": "🥉", + "description": "third place medal", "unicodeVersion": "9.0", "digest": "27c9bcba44ad95bee30882cc0722e8b0a798206306655dd648e884447ed26808" }, "thought_balloon": { "category": "symbols", "moji": "💭", + "description": "thought balloon", "unicodeVersion": "6.0", "digest": "bf59624560c333561d636aedf2c8827089e275895cf434974daaabb3d5cea46e" }, "three": { "category": "symbols", "moji": "3️⃣", + "description": "keycap digit three", "unicodeVersion": "3.0", "digest": "d3f85828787799c769655c38a519cad0743ab799ab276c7606e6e6894cc442e6" }, "thumbsdown": { "category": "people", "moji": "👎", + "description": "thumbs down sign", "unicodeVersion": "6.0", "digest": "5954334e2dae5357312b3d629f10a496c728029e02216f8c8b887f9b51561c61" }, "thumbsdown_tone1": { "category": "people", "moji": "👎🏻", + "description": "thumbs down sign tone 1", "unicodeVersion": "8.0", "digest": "3c2853491473fd7ae2d1b5415a425cc390d26a8754446f8736c1360e4cb18ba3" }, "thumbsdown_tone2": { "category": "people", "moji": "👎🏼", + "description": "thumbs down sign tone 2", "unicodeVersion": "8.0", "digest": "4e0f8f86a06b69e423df8d93f41ec393f12800633acc82c4cb6dff64ca0d8507" }, "thumbsdown_tone3": { "category": "people", "moji": "👎🏽", + "description": "thumbs down sign tone 3", "unicodeVersion": "8.0", "digest": "e08fa35575f59978612d4330bbc35313eca9c4dfa04f4212626abc700819effe" }, "thumbsdown_tone4": { "category": "people", "moji": "👎🏾", + "description": "thumbs down sign tone 4", "unicodeVersion": "8.0", "digest": "7c6d118d20d5add8ca003e4a53e42685a1f9436b872ed10d79f67ad418fb2a44" }, "thumbsdown_tone5": { "category": "people", "moji": "👎🏿", + "description": "thumbs down sign tone 5", "unicodeVersion": "8.0", "digest": "8697c4a4ee4d6669dc2d47aa97699c42012ca59b80818ad6845878b37b4a9c58" }, "thumbsup": { "category": "people", "moji": "👍", + "description": "thumbs up sign", "unicodeVersion": "6.0", "digest": "59ec2457ab33e8897261d01a495f6cf5c668d0004807dc541c3b1be5294b1e61" }, "thumbsup_tone1": { "category": "people", "moji": "👍🏻", + "description": "thumbs up sign tone 1", "unicodeVersion": "8.0", "digest": "f57e6c525e8830779ea5026590eec3ca10869dc438a0c779734b617d04f28d21" }, "thumbsup_tone2": { "category": "people", "moji": "👍🏼", + "description": "thumbs up sign tone 2", "unicodeVersion": "8.0", "digest": "980eeeb1d8f5d79dae35c7ff81a576e980aa13a440d07b10e32e98ed34cbf7f1" }, "thumbsup_tone3": { "category": "people", "moji": "👍🏽", + "description": "thumbs up sign tone 3", "unicodeVersion": "8.0", "digest": "b3881060569e56e1dd75ca7960feab0e58ae51f440458781948d65d461116b4e" }, "thumbsup_tone4": { "category": "people", "moji": "👍🏾", + "description": "thumbs up sign tone 4", "unicodeVersion": "8.0", "digest": "86fbe2c95414bce5e38fb5c33da31305d7942fca2c9c79168dcffdbd895e9ad6" }, "thumbsup_tone5": { "category": "people", "moji": "👍🏿", + "description": "thumbs up sign tone 5", "unicodeVersion": "8.0", "digest": "49fa63ff725c746a18649df16c8fab69bad88bbb564884df79d1d15f553b7343" }, "thunder_cloud_rain": { "category": "nature", "moji": "⛈", + "description": "thunder cloud and rain", "unicodeVersion": "5.2", "digest": "dacc20b4f6b68e5834aa1b8391afa5e83b5e6eb28e2d2174d3a68186a770506d" }, "ticket": { "category": "activity", "moji": "🎫", + "description": "ticket", "unicodeVersion": "6.0", "digest": "b4326fe7761940216e6c76ee2928110a6b37bf913da9d694e96557e7c7c10420" }, "tickets": { "category": "activity", "moji": "🎟", + "description": "admission tickets", "unicodeVersion": "7.0", "digest": "fb73358c3697c04fcfde6a1e705b1c3b47635b93b9cadfe31d5657566c7d190a" }, "tiger": { "category": "nature", "moji": "🐯", + "description": "tiger face", "unicodeVersion": "6.0", "digest": "e139531e6c930bc46242dc0ed274661229de026b5419d8ea8f99fdb0f8a719ab" }, "tiger2": { "category": "nature", "moji": "🐅", + "description": "tiger", "unicodeVersion": "6.0", "digest": "f930cc8714198310d9b0edca6baff243ac5a3320f75fadb56fa5acc6fe34ff24" }, "timer": { "category": "objects", "moji": "⏲", + "description": "timer clock", "unicodeVersion": "6.0", "digest": "69b33f219523d89d81cbbc070ad7e528711e4b34e124a50acb12a0280a34d0b0" }, "tired_face": { "category": "people", "moji": "😫", + "description": "tired face", "unicodeVersion": "6.0", "digest": "775739bc9324517e614878ca0960d793df97775feeb62b14dbfb311a42a21802" }, "tm": { "category": "symbols", "moji": "™", + "description": "trade mark sign", "unicodeVersion": "1.1", "digest": "7d9fafdb72d91860478fc185719f289f359eab2c368a132cb936a269e2ab6a24" }, "toilet": { "category": "objects", "moji": "🚽", + "description": "toilet", "unicodeVersion": "6.0", "digest": "0d1b0dd0078f51104e8632a0726e1b3f075561a1ffa8a2546602de15798415d0" }, "tokyo_tower": { "category": "travel", "moji": "🗼", + "description": "tokyo tower", "unicodeVersion": "6.0", "digest": "73eaf6fd59d16396673afef620c6d928857d5cf616e95a40eaf2861686e0956a" }, "tomato": { "category": "food", "moji": "🍅", + "description": "tomato", "unicodeVersion": "6.0", "digest": "d092d8ad381d542e59b6a82b4f1ef0d10fc1ed48460952375c6c5c6258cea111" }, "tone1": { "category": "modifier", "moji": "🏻", + "description": "emoji modifier Fitzpatrick type-1-2", "unicodeVersion": "8.0", "digest": "5c62003a098b774c068be45d658db3c0dd38483c0871f7c8ae293bc1222c4f0c" }, "tone2": { "category": "modifier", "moji": "🏼", + "description": "emoji modifier Fitzpatrick type-3", "unicodeVersion": "8.0", "digest": "3c636ecbc4e58c7a360f2338daaf44e7da598fd07e0ba1514bb5c0f83fc8819f" }, "tone3": { "category": "modifier", "moji": "🏽", + "description": "emoji modifier Fitzpatrick type-4", "unicodeVersion": "8.0", "digest": "398a1e5441b64c9c2d033bbc01d7a8d90b4db30ea9f30e28f0a9120c72a48df8" }, "tone4": { "category": "modifier", "moji": "🏾", + "description": "emoji modifier Fitzpatrick type-5", "unicodeVersion": "8.0", "digest": "ff4a12195aeb7494c785b81266efad8cd60c8022c407a0fc032a02e8b83216b3" }, "tone5": { "category": "modifier", "moji": "🏿", + "description": "emoji modifier Fitzpatrick type-6", "unicodeVersion": "8.0", "digest": "9e9f0125b5d57011b7456c84719e6be6cf71d06c1b198081d0937c0979164a81" }, "tongue": { "category": "people", "moji": "👅", + "description": "tongue", "unicodeVersion": "6.0", "digest": "286e9d2583c371431d6fc979dd4ab48981676da26baada51a846657a3654c19b" }, "tools": { "category": "objects", "moji": "🛠", + "description": "hammer and wrench", "unicodeVersion": "7.0", "digest": "bf08d60dedc06de73d04dab05703bb8ad81989c72b5035d1a07821e51096f158" }, "top": { "category": "symbols", "moji": "🔝", + "description": "top with upwards arrow above", "unicodeVersion": "6.0", "digest": "c9a9f25b17db014e76b6be54aa07ef89bb18f8adb41b3199d180a559ff1d9ea5" }, "tophat": { "category": "people", "moji": "🎩", + "description": "top hat", "unicodeVersion": "6.0", "digest": "43a45dfb5d6b57a63a0491f4e3ec780774c0301b53ed39a303a0bd803d16ed71" }, "track_next": { "category": "symbols", "moji": "⏭", + "description": "black right-pointing double triangle with vertical bar", "unicodeVersion": "6.0", "digest": "88592ef6c720a32aeb752322fb4c794bf5110a72408e21e898630452115c731c" }, "track_previous": { "category": "symbols", "moji": "⏮", + "description": "black left-pointing double triangle with vertical bar", "unicodeVersion": "6.0", "digest": "98c1b3d643768d94857fb762f6d26cfb87282b449a67792242e8b7068643ac87" }, "trackball": { "category": "objects", "moji": "🖲", + "description": "trackball", "unicodeVersion": "7.0", "digest": "32a819a3129429f797ad434d0c40e263dc236808e34878c599ed2304b43702f5" }, "tractor": { "category": "travel", "moji": "🚜", + "description": "tractor", "unicodeVersion": "6.0", "digest": "5e4686290f1a4c9953ae208340b7d276f25b3b2197a43e52469aeb6450e93997" }, "traffic_light": { "category": "travel", "moji": "🚥", + "description": "horizontal traffic light", "unicodeVersion": "6.0", "digest": "d96aacade33d1ad3e0414f8a920513010f36eb7e5889774251c1d91148917ead" }, "train": { "category": "travel", "moji": "🚋", + "description": "Tram Car", "unicodeVersion": "6.0", "digest": "7423d17e131df7aadaa350b5d39dcbce3b28de331ff8b6703a3b2d0093963f4b" }, "train2": { "category": "travel", "moji": "🚆", + "description": "train", "unicodeVersion": "6.0", "digest": "06e65d549e771632f3c64287a38ba67236f9800ccb6a23c3b592bc010e24e122" }, "tram": { "category": "travel", "moji": "🚊", + "description": "tram", "unicodeVersion": "6.0", "digest": "21a7699f1a94f06dcb4d1e896448b98a4205f8efe902a8ac169a5005d11ab100" }, "triangular_flag_on_post": { "category": "objects", "moji": "🚩", + "description": "triangular flag on post", "unicodeVersion": "6.0", "digest": "1f5ce3828a42f5b1717bac1521d0502cf7081ad9f15e8ed292c1a65f0d1386da" }, "triangular_ruler": { "category": "objects", "moji": "📐", + "description": "triangular ruler", "unicodeVersion": "6.0", "digest": "a0367dcf663ec934f1fc7c88bfaccc02b229a896f60930a66bb02241c933e501" }, "trident": { "category": "symbols", "moji": "🔱", + "description": "trident emblem", "unicodeVersion": "6.0", "digest": "ee45920845d3b35c2e45b934cf30ce97bfe2f24c5d72ef1ac6e0842e52b50fc1" }, "triumph": { "category": "people", "moji": "😤", + "description": "face with look of triumph", "unicodeVersion": "6.0", "digest": "4aa44b8e1682c1269624a359f4b0bf613553683b883d947561ab169d7f85da0f" }, "trolleybus": { "category": "travel", "moji": "🚎", + "description": "trolleybus", "unicodeVersion": "6.0", "digest": "f610b4fd1123f06778a8e3bb8f738d5b0079aeb0b0926b6a63268c0dd0ee03ed" }, "trophy": { "category": "activity", "moji": "🏆", + "description": "trophy", "unicodeVersion": "6.0", "digest": "50cfbedac18bf0fa5dec727643e15ec47f64068944b536e97518ee3be4f08006" }, "tropical_drink": { "category": "food", "moji": "🍹", + "description": "tropical drink", "unicodeVersion": "6.0", "digest": "54144fce60d650f426b1edf09e47c70b2762222398c1fe40231881f074603a69" }, "tropical_fish": { "category": "nature", "moji": "🐠", + "description": "tropical fish", "unicodeVersion": "6.0", "digest": "fd92100aaa9328da35e6090388824921b9726b474d1432a926d2cf9c45ad6528" }, "truck": { "category": "travel", "moji": "🚚", + "description": "delivery truck", "unicodeVersion": "6.0", "digest": "0d1571e58e900abc453df0ff683fe7acb5906ecbdd52ab35b7101074359faf18" }, "trumpet": { "category": "activity", "moji": "🎺", + "description": "trumpet", "unicodeVersion": "6.0", "digest": "cea3614c309f5573f328f4603120dbe930016a35f0dfa400b0d968fe9fff2d55" }, "tulip": { "category": "nature", "moji": "🌷", + "description": "tulip", "unicodeVersion": "6.0", "digest": "e744e8dbbdc6b126bd5b15aad56b524191de5a604189f4ab6d96730dfef4d086" }, "tumbler_glass": { "category": "food", "moji": "🥃", + "description": "tumbler glass", "unicodeVersion": "9.0", "digest": "7a38658274b9ff28836725a1dbfad49b8fa3af5ec8385e629db6bfdc7d93907a" }, "turkey": { "category": "nature", "moji": "🦃", + "description": "turkey", "unicodeVersion": "8.0", "digest": "bf5daef15716b66636a5fdb6d059420521443c0603e2d56bd7c99c791a7285f4" }, "turtle": { "category": "nature", "moji": "🐢", + "description": "turtle", "unicodeVersion": "6.0", "digest": "588c35fb42c9502a908e9805517d4cc8c4ba4e74c9beed4035779fea1efe14f8" }, "tv": { "category": "objects", "moji": "📺", + "description": "television", "unicodeVersion": "6.0", "digest": "1279f3f3955a58dbbf74e248fc914b0bdba9c4c6b6a5176e9d12bf2750ecfeb4" }, "twisted_rightwards_arrows": { "category": "symbols", "moji": "🔀", + "description": "twisted rightwards arrows", "unicodeVersion": "6.0", "digest": "fed07eebc2cf0d977ca0826bbd80defafbbcf118508444148f47b58949ebe27c" }, "two": { "category": "symbols", "moji": "2️⃣", + "description": "keycap digit two", "unicodeVersion": "3.0", "digest": "b346f51f6523b02ebcbd753256804e2f9cc1574c96aa634362bf9401dac2c661" }, "two_hearts": { "category": "symbols", "moji": "💕", + "description": "two hearts", "unicodeVersion": "6.0", "digest": "6ded120a59aed790b441ec8fbbdea6f5cbfb4fa48e9e4b224cc29c9fde2d2e4c" }, "two_men_holding_hands": { "category": "people", "moji": "👬", + "description": "two men holding hands", "unicodeVersion": "6.0", "digest": "bfcf9e20a67d00262cdf6e85f1acd545dda91f2e370d68bfd41ce02f232a2987" }, "two_women_holding_hands": { "category": "people", "moji": "👭", + "description": "two women holding hands", "unicodeVersion": "6.0", "digest": "9d9d2b37a7f8e16fde1468dd8b5645003ea81ae4bf8bcf68471e2381845dd0dd" }, "u5272": { "category": "symbols", "moji": "🈹", + "description": "squared cjk unified ideograph-5272", "unicodeVersion": "6.0", "digest": "01e6cb8f74ea3c19fdade59c2d13d158b90dc6b4b293421b2014b7478bf20870" }, "u5408": { "category": "symbols", "moji": "🈴", + "description": "squared cjk unified ideograph-5408", "unicodeVersion": "6.0", "digest": "084cdbd5436670ea4dc22010e269c1ab7b0432897b8675301e69120374bcdd14" }, "u55b6": { "category": "symbols", "moji": "🈺", + "description": "squared cjk unified ideograph-55b6", "unicodeVersion": "6.0", "digest": "c1017023d20d4aae78d59342dd3bfc5282716ea0601d9a8c2476335cbf7a2e12" }, "u6307": { "category": "symbols", "moji": "🈯", + "description": "squared cjk unified ideograph-6307", "unicodeVersion": "5.2", "digest": "f459b092b974f459db1fb9cc13617a448b2e4f2b4dc46cc316d8c46af6e7d8bd" }, "u6708": { "category": "symbols", "moji": "🈷", + "description": "squared cjk unified ideograph-6708", "unicodeVersion": "6.0", "digest": "928815abf5b30f92efe5168de0c7e6cf8c17899a03e358ab42f42667e0a4a04c" }, "u6709": { "category": "symbols", "moji": "🈶", + "description": "squared cjk unified ideograph-6709", "unicodeVersion": "6.0", "digest": "f63a48ee06c892d24acec8b5634c021658d2ebde67a42d8faa86f27804a9f26d" }, "u6e80": { "category": "symbols", "moji": "🈵", + "description": "squared cjk unified ideograph-6e80", "unicodeVersion": "6.0", "digest": "489181d90a5e43068459530673a153e4af04fdad8514ec341ff7afbcfd366c3b" }, "u7121": { "category": "symbols", "moji": "🈚", + "description": "squared cjk unified ideograph-7121", "unicodeVersion": "5.2", "digest": "9c50fd2ba14221affd2dcd3746322c2137dd75458493f4d385b544eb5bd8d6cd" }, "u7533": { "category": "symbols", "moji": "🈸", + "description": "squared cjk unified ideograph-7533", "unicodeVersion": "6.0", "digest": "2b05819b380a2ea47cc5fde8fcce3d53922fd223d6f5bd83d696d44175b69f18" }, "u7981": { "category": "symbols", "moji": "🈲", + "description": "squared cjk unified ideograph-7981", "unicodeVersion": "6.0", "digest": "adbe12601b22972003ddebcb0bd1532b979aa9c78bfdc147511854b5014eabc0" }, "u7a7a": { "category": "symbols", "moji": "🈳", + "description": "squared cjk unified ideograph-7a7a", "unicodeVersion": "6.0", "digest": "b9ee0ec7bb0b86c3eb73d4dbbb91848c427bf356ae30a263b9b44bd9bd784482" }, "umbrella": { "category": "nature", "moji": "☔", + "description": "umbrella with rain drops", "unicodeVersion": "4.0", "digest": "0328a2f48b7df47905e2655460e524c0794ef12d3d7c32a049a10892d5662f77" }, "umbrella2": { "category": "nature", "moji": "☂", + "description": "umbrella", "unicodeVersion": "1.1", "digest": "2f6a58110dc590480a822a3ffa2b5bc86f295e0c994a4a632837d25d4cf9fc58" }, "unamused": { "category": "people", "moji": "😒", + "description": "unamused face", "unicodeVersion": "6.0", "digest": "0d597088e3e7880918d0166e5c69243b18fe64afa31685c39bfdbc71494aa132" }, "underage": { "category": "symbols", "moji": "🔞", + "description": "no one under eighteen symbol", "unicodeVersion": "6.0", "digest": "b6b194614ca714ac2b1c2c17b75fe5922c7fdadb3d1157ba89ab2a5d03494a67" }, "unicorn": { "category": "nature", "moji": "🦄", + "description": "unicorn face", "unicodeVersion": "8.0", "digest": "f71bb485a7c208e999dd45f2b36d7b7d517898c0627947926b05aa28603804ca" }, "unlock": { "category": "objects", "moji": "🔓", + "description": "open lock", "unicodeVersion": "6.0", "digest": "9554ef3a6a315938b873e77970d9b0212e61f13c6cc36e4f17f87acc930a9a53" }, "up": { "category": "symbols", "moji": "🆙", + "description": "squared up with exclamation mark", "unicodeVersion": "6.0", "digest": "ff2554ccf08c7208b38794c5fa3d9a93a46ff191a49401195d8f740846121906" }, "upside_down": { "category": "people", "moji": "🙃", + "description": "upside-down face", "unicodeVersion": "8.0", "digest": "5129121f0a28f5b334268c28565de26a5907559568deca11de6ec620b097dfe1" }, "urn": { "category": "objects", "moji": "⚱", + "description": "funeral urn", "unicodeVersion": "4.1", "digest": "9bebf589eed8dd361f6a03cd1b325078f2cd0e82270ef63a7dd1b6aee08cd1e6" }, "v": { "category": "people", "moji": "✌", + "description": "victory hand", "unicodeVersion": "1.1", "digest": "9825bf440df289a8edf8ede494e8c778dc63c95f967f4d7bbea3245cf4f558ec" }, "v_tone1": { "category": "people", "moji": "✌🏻", + "description": "victory hand tone 1", "unicodeVersion": "8.0", "digest": "76e358250d9ca519b60b8d7b6a32900700d784433dcc609e9442254a410f6e37" }, "v_tone2": { "category": "people", "moji": "✌🏼", + "description": "victory hand tone 2", "unicodeVersion": "8.0", "digest": "4081b674be8416136022523fa9f29ec70a0f7e3aa05ca13152606609f3fd003c" }, "v_tone3": { "category": "people", "moji": "✌🏽", + "description": "victory hand tone 3", "unicodeVersion": "8.0", "digest": "b6afb3a4c78384280610b953592d378241c75597a82aa6d16c86a993f8d8f3b0" }, "v_tone4": { "category": "people", "moji": "✌🏾", + "description": "victory hand tone 4", "unicodeVersion": "8.0", "digest": "7ddc3cdd0138da2c8d7f6d8257ffdb8801496043e8a2395f93b0663447ac7fce" }, "v_tone5": { "category": "people", "moji": "✌🏿", + "description": "victory hand tone 5", "unicodeVersion": "8.0", "digest": "a85dc5c589f0d1cf32f8bfa5c82e5c11c40b35439636914686a2f06f7359f539" }, "vertical_traffic_light": { "category": "travel", "moji": "🚦", + "description": "vertical traffic light", "unicodeVersion": "6.0", "digest": "8cfd49a8f96b15a8313ef855f2e234ea3fa58332e68896dea34760740de9f020" }, "vhs": { "category": "objects", "moji": "📼", + "description": "videocassette", "unicodeVersion": "6.0", "digest": "3fb1acaf25805cf86f8d40ee2c17cf25da587b7ca93b931167ab43fce041eee8" }, "vibration_mode": { "category": "symbols", "moji": "📳", + "description": "vibration mode", "unicodeVersion": "6.0", "digest": "c9a8899222f46fe51dd8cee3e59f77c48268f0b7cfae2bcb34a791213acb1755" }, "video_camera": { "category": "objects", "moji": "📹", + "description": "video camera", "unicodeVersion": "6.0", "digest": "62e56f26c286a7964ef1021f0f23fcb4b38cdcfb5b5af569b472340c412c619a" }, "video_game": { "category": "activity", "moji": "🎮", + "description": "video game", "unicodeVersion": "6.0", "digest": "2787e302aa9e6fd7e9dc382c9bc7f5fbf244ef4940e08a4f9e80d33324f3032e" }, "violin": { "category": "activity", "moji": "🎻", + "description": "violin", "unicodeVersion": "6.0", "digest": "1e69d531ce2b5d5bf1dd9470187dbbe76f479d14428834b6a9e2bf5296dc0ec9" }, "virgo": { "category": "symbols", "moji": "♍", + "description": "virgo", "unicodeVersion": "1.1", "digest": "0f75e9c228bc467fd0cec0f93f0e087c943bc5fb1d945fb0d4de53d07718388e" }, "volcano": { "category": "travel", "moji": "🌋", + "description": "volcano", "unicodeVersion": "6.0", "digest": "41c92ef88ca533df342a0ebe59d2b676873bfa944c3988495b8a96060a9b8e16" }, "volleyball": { "category": "activity", "moji": "🏐", + "description": "volleyball", "unicodeVersion": "8.0", "digest": "774a83357f7aee890b4d4383236f0a90946dbd7c86aaabadc5753dcc9b4c9d69" }, "vs": { "category": "symbols", "moji": "🆚", + "description": "squared vs", "unicodeVersion": "6.0", "digest": "ac943e4c737459c2e1adbac8b71d3fdaebb704dbaf5713012e7a77beb09db1ef" }, "vulcan": { "category": "people", "moji": "🖖", + "description": "raised hand with part between middle and ring fingers", "unicodeVersion": "7.0", "digest": "b4d409a0b019e7b06333cefd15ea46cb54aef5132d86e8ba361c1c3b911fe265" }, "vulcan_tone1": { "category": "people", "moji": "🖖🏻", + "description": "raised hand with part between middle and ring fingers tone 1", "unicodeVersion": "8.0", "digest": "cc6072c85031b5081995f98a57f09ab177168318f69a51f3acc63251760499a4" }, "vulcan_tone2": { "category": "people", "moji": "🖖🏼", + "description": "raised hand with part between middle and ring fingers tone 2", "unicodeVersion": "8.0", "digest": "858bd5a1ac91dc4d7735f57ba4dd69d39138aa6dac1c80cfc05de30a59a5bc33" }, "vulcan_tone3": { "category": "people", "moji": "🖖🏽", + "description": "raised hand with part between middle and ring fingers tone 3", "unicodeVersion": "8.0", "digest": "2f74b6f3eab2a75063591b66f1c7350af0d23153e1427af91de20c48a5f4a54a" }, "vulcan_tone4": { "category": "people", "moji": "🖖🏾", + "description": "raised hand with part between middle and ring fingers tone 4", "unicodeVersion": "8.0", "digest": "87cf8b87d3610f742857a9704b658462df32b4924d8f1ddba26f761e738c4e11" }, "vulcan_tone5": { "category": "people", "moji": "🖖🏿", + "description": "raised hand with part between middle and ring fingers tone 5", "unicodeVersion": "8.0", "digest": "11e9ff62f2385edeb477dbf66c63734536531def5771daf80b66a3425ac71493" }, "walking": { "category": "people", "moji": "🚶", + "description": "pedestrian", "unicodeVersion": "6.0", "digest": "ae77471fe1e8a734d11711cdb589f64347c35d6ee2fc10f6db16ac550c0557fa" }, "walking_tone1": { "category": "people", "moji": "🚶🏻", + "description": "pedestrian tone 1", "unicodeVersion": "8.0", "digest": "3de871c234e1340ccf95338df7babd94d175cfcb17a57b5a74d950e0a31f03b1" }, "walking_tone2": { "category": "people", "moji": "🚶🏼", + "description": "pedestrian tone 2", "unicodeVersion": "8.0", "digest": "620eb7bfb753a331a5822b02bdaf08d8dde7b573efd210287a3d3dfdd84a40b9" }, "walking_tone3": { "category": "people", "moji": "🚶🏽", + "description": "pedestrian tone 3", "unicodeVersion": "8.0", "digest": "ff39545acc2256006128f8c186433c28052b8c9aaec46fe06f25cff02c71f6b8" }, "walking_tone4": { "category": "people", "moji": "🚶🏾", + "description": "pedestrian tone 4", "unicodeVersion": "8.0", "digest": "a9499d142392977a9b9e54fb957952359e9bdffce7ec2f1e8320523d185fb066" }, "walking_tone5": { "category": "people", "moji": "🚶🏿", + "description": "pedestrian tone 5", "unicodeVersion": "8.0", "digest": "b47a4c48ce40298f842f454fc1abccae70f69725d73ee2c80e4018f4c4065d7d" }, "waning_crescent_moon": { "category": "nature", "moji": "🌘", + "description": "waning crescent moon symbol", "unicodeVersion": "6.0", "digest": "2ec7896eefcf821e0ea013556a17af59e997503662c07f080d0a84ab13ef4cf1" }, "waning_gibbous_moon": { "category": "nature", "moji": "🌖", + "description": "waning gibbous moon symbol", "unicodeVersion": "6.0", "digest": "ce2f5aca8fccdacaaf174d10da4e493e853e4608cc4d159aa3081d108a8b58d5" }, "warning": { "category": "symbols", "moji": "⚠", + "description": "warning sign", "unicodeVersion": "4.0", "digest": "745f1d203958f42bf37ecb5909cd0819934e300308ba0ff20964c8c203092f90" }, "wastebasket": { "category": "objects", "moji": "🗑", + "description": "wastebasket", "unicodeVersion": "7.0", "digest": "221a1b6d9975051038d9d97e18a16556cdf4254a6bca4c29bf1c51f306c79f2a" }, "watch": { "category": "objects", "moji": "⌚", + "description": "watch", "unicodeVersion": "1.1", "digest": "acc0c96751404a789b3085f10425cf34f942185215df459515d2439cde3efc6b" }, "water_buffalo": { "category": "nature", "moji": "🐃", + "description": "water buffalo", "unicodeVersion": "6.0", "digest": "ba6a840d4f57f8f9f3e9f29b8a030faf02a3a3d912e3e31b067616b2ac48a3d1" }, "water_polo": { "category": "activity", "moji": "🤽", + "description": "water polo", "unicodeVersion": "9.0", "digest": "fc77e1d2a84a9f4cf0cf19c1ea10cf137cf0940b9103a523121eda87677ad148" }, "water_polo_tone1": { "category": "activity", "moji": "🤽🏻", + "description": "water polo tone 1", "unicodeVersion": "9.0", "digest": "3be28384edd29ada8109f07720d601a9d5866ed63e6234efe9ee1a194ed5d0c5" }, "water_polo_tone2": { "category": "activity", "moji": "🤽🏼", + "description": "water polo tone 2", "unicodeVersion": "9.0", "digest": "afcd3f28c6719f869ca79a6fd1ccade2ea976ade844fbc1081fc72865bcb652f" }, "water_polo_tone3": { "category": "activity", "moji": "🤽🏽", + "description": "water polo tone 3", "unicodeVersion": "9.0", "digest": "d19481c9b82d9413e99c2652e020fd763f2b54408dedaffec8dfe80973ded407" }, "water_polo_tone4": { "category": "activity", "moji": "🤽🏾", + "description": "water polo tone 4", "unicodeVersion": "9.0", "digest": "375972d882b627e8d525e632e58b30346fc3e01858d7d08d62a9d3bf8132bbc7" }, "water_polo_tone5": { "category": "activity", "moji": "🤽🏿", + "description": "water polo tone 5", "unicodeVersion": "9.0", "digest": "a8e1ced1c5382a8147a1d1801a133cada9a0e52e41de6272e56c3c1f426f6048" }, "watermelon": { "category": "food", "moji": "🍉", + "description": "watermelon", "unicodeVersion": "6.0", "digest": "42a3821d2e4dd595c93f5db7a5c70b7af486b8f0ddd3b9d26bc4e743a88e699a" }, "wave": { "category": "people", "moji": "👋", + "description": "waving hand sign", "unicodeVersion": "6.0", "digest": "cddbd764d471604446cbaca91f77f6c4119d1cfc2c856732ca0eaac4593cb736" }, "wave_tone1": { "category": "people", "moji": "👋🏻", + "description": "waving hand sign tone 1", "unicodeVersion": "8.0", "digest": "cf40797437ddf68ec0275f337e6aac4bed81e28da7636d56c9f817ddf8e2b30a" }, "wave_tone2": { "category": "people", "moji": "👋🏼", + "description": "waving hand sign tone 2", "unicodeVersion": "8.0", "digest": "12c8a3e82c03ee35a734c642be482ba2d9d5948dacf91ec1fda243316dd4a0d0" }, "wave_tone3": { "category": "people", "moji": "👋🏽", + "description": "waving hand sign tone 3", "unicodeVersion": "8.0", "digest": "ebcaef43e21b475f76de811d4f4d1a67d9393973b57b03876e02164345a2ba4a" }, "wave_tone4": { "category": "people", "moji": "👋🏾", + "description": "waving hand sign tone 4", "unicodeVersion": "8.0", "digest": "7df7b70cf76766836ba146c3d91b6104930c384450cf2688426e60c1c06a1fc8" }, "wave_tone5": { "category": "people", "moji": "👋🏿", + "description": "waving hand sign tone 5", "unicodeVersion": "8.0", "digest": "8dfdba6aeff5d7dfd807467d431a137547726b34d021f1a5a0b74e155d270ea7" }, "wavy_dash": { "category": "symbols", "moji": "〰", + "description": "wavy dash", "unicodeVersion": "1.1", "digest": "7b1968474f01d12fd09a1f2572282927138d9e9d6a3642de4bf68af80a8c3738" }, "waxing_crescent_moon": { "category": "nature", "moji": "🌒", + "description": "waxing crescent moon symbol", "unicodeVersion": "6.0", "digest": "852d7e55a19074d061fa3aa80d6b1e7e87a9280bdf44d94bbdbbe6d59178b1be" }, "waxing_gibbous_moon": { "category": "nature", "moji": "🌔", + "description": "waxing gibbous moon symbol", "unicodeVersion": "6.0", "digest": "a3a1c7cc72521a3f74929789a90e1c35d81ac86e21225c9f844d718d8940e3b3" }, "wc": { "category": "symbols", "moji": "🚾", + "description": "water closet", "unicodeVersion": "6.0", "digest": "4b95d54e0b53e4b705277917653503b32d6a143c2eaf6c547bc8e01c2dc23659" }, "weary": { "category": "people", "moji": "😩", + "description": "weary face", "unicodeVersion": "6.0", "digest": "3528f85540996cd5b562efe5421c495fc1bb414dc797bc20062783ae1b730847" }, "wedding": { "category": "travel", "moji": "💒", + "description": "wedding", "unicodeVersion": "6.0", "digest": "980f3522cc4c19c3096e668032ea2cd19e7900cdc4b73bbb1c9b4c4d28dc78af" }, "whale": { "category": "nature", "moji": "🐳", + "description": "spouting whale", "unicodeVersion": "6.0", "digest": "6368fe4bc4a7f68aa2bd5386686a5f1b159feacbec16d59515f2b6e5d01adfbd" }, "whale2": { "category": "nature", "moji": "🐋", + "description": "whale", "unicodeVersion": "6.0", "digest": "ccd3edf88167965f2abc18631ffb80e2532f728da35bc0c11144376685da18e8" }, "wheel_of_dharma": { "category": "symbols", "moji": "☸", + "description": "wheel of dharma", "unicodeVersion": "1.1", "digest": "4a0a13fcd507b9621686c8090bf340aa8770c064e0e3eb576fbae1229000d6da" }, "wheelchair": { "category": "symbols", "moji": "♿", + "description": "wheelchair symbol", "unicodeVersion": "4.1", "digest": "f5250f2b4b5b4ffe6a6f77d30865c3f5d7173fc91aee547869589b2a96da91c8" }, "white_check_mark": { "category": "symbols", "moji": "✅", + "description": "white heavy check mark", "unicodeVersion": "6.0", "digest": "45eb17bde6e503f22c8579d6e4d507ad6557a15f9eaad14aa716ec9ba1540876" }, "white_circle": { "category": "symbols", "moji": "⚪", + "description": "medium white circle", "unicodeVersion": "4.1", "digest": "2e7323fa4d1e3929e529d49210a0b82a043eae4f7c95128ec86b98c46fdb0e7c" }, "white_flower": { "category": "symbols", "moji": "💮", + "description": "white flower", "unicodeVersion": "6.0", "digest": "ace093b310eeefdecf4a4bdaf4fbcbb568457b0191ac80778a466ac5f3f4025a" }, "white_large_square": { "category": "symbols", "moji": "⬜", + "description": "white large square", "unicodeVersion": "5.1", "digest": "0db6957ee9ff7325b534b730fc05345a63d4ed9060f0f816807d0dcf004baa3e" }, "white_medium_small_square": { "category": "symbols", "moji": "◽", + "description": "white medium small square", "unicodeVersion": "3.2", "digest": "d79689981a7b38211c60a025a81e44fd39ac6ea4062e227cae3aab8f51572cd4" }, "white_medium_square": { "category": "symbols", "moji": "◻", + "description": "white medium square", "unicodeVersion": "3.2", "digest": "6c4ce26d3f69667219f29ea18b04f3e79373024426275f25936e09a683e9a4fc" }, "white_small_square": { "category": "symbols", "moji": "▫", + "description": "white small square", "unicodeVersion": "1.1", "digest": "ae0d35a6bbba4592b89b2f0f1f2d183efb2f93cf2a2136c0c195aab72f0bb1c8" }, "white_square_button": { "category": "symbols", "moji": "🔳", + "description": "white square button", "unicodeVersion": "6.0", "digest": "797f3d9e44e88e940ffc118e52d0f709eec2ef14b13bdf873ad4b0c96cc0b042" }, "white_sun_cloud": { "category": "nature", "moji": "🌥", + "description": "white sun behind cloud", "unicodeVersion": "7.0", "digest": "0e714038bb0a5b091dd4ad8829c5c72dece493e09da6d56ceadcd0b68e1c0fd5" }, "white_sun_rain_cloud": { "category": "nature", "moji": "🌦", + "description": "white sun behind cloud with rain", "unicodeVersion": "7.0", "digest": "82fb2a91d43c7c511afed216e12f98e32aef4475e7f3c7ccc0f39732d2f7d5e5" }, "white_sun_small_cloud": { "category": "nature", "moji": "🌤", + "description": "white sun with small cloud", "unicodeVersion": "7.0", "digest": "0a6164cdadf2413555b7ef47b95f823f5a010f36d2dacfb1a38335a0f59e9601" }, "wilted_rose": { "category": "nature", "moji": "🥀", + "description": "wilted flower", "unicodeVersion": "9.0", "digest": "2c9e01ab9a61d057c71478b09ba7d82ae08f4a5a1c2212b7ad562b74f616677f" }, "wind_blowing_face": { "category": "nature", "moji": "🌬", + "description": "wind blowing face", "unicodeVersion": "7.0", "digest": "e4f63149cbc8829118571f6a93487b96d26665fc15d17d578cca4e5c752cd54f" }, "wind_chime": { "category": "objects", "moji": "🎐", + "description": "wind chime", "unicodeVersion": "6.0", "digest": "1b1b212fbd74a9edc62aee7ffab9bcf91d3a9f69bffb2be4b7fd527914c14ced" }, "wine_glass": { "category": "food", "moji": "🍷", + "description": "wine glass", "unicodeVersion": "6.0", "digest": "d99107d6809386bc5e219aa58ee4930d27b7c3a6d2b10deb9f523df369f766d1" }, "wink": { "category": "people", "moji": "😉", + "description": "winking face", "unicodeVersion": "6.0", "digest": "56e29994a47335a901d0c98fa141d26faae8f647a860517bd3615fa980921885" }, "wolf": { "category": "nature", "moji": "🐺", + "description": "wolf face", "unicodeVersion": "6.0", "digest": "4a983f5ec8ec0872fcde7890e17605b1229064e5e194b6fca1c4259068d1caed" }, "woman": { "category": "people", "moji": "👩", + "description": "woman", "unicodeVersion": "6.0", "digest": "a06a22a48eeb3aeb885321358fe234e97797ed33be17f52d232ce2830cfbcd97" }, "woman_tone1": { "category": "people", "moji": "👩🏻", + "description": "woman tone 1", "unicodeVersion": "8.0", "digest": "c2e4b135c1dac6a0b002569a6ccd9d098f6cb18481c68b5d9115e11241a0978d" }, "woman_tone2": { "category": "people", "moji": "👩🏼", + "description": "woman tone 2", "unicodeVersion": "8.0", "digest": "4848e650051214a53c4cd9f6d3d94158f77f65ecb34f891789de34ee0a713006" }, "woman_tone3": { "category": "people", "moji": "👩🏽", + "description": "woman tone 3", "unicodeVersion": "8.0", "digest": "b6f751ad47da019cdfb9d6d78f9610adb92120abf204c30df79a9150b57dbdee" }, "woman_tone4": { "category": "people", "moji": "👩🏾", + "description": "woman tone 4", "unicodeVersion": "8.0", "digest": "fd27d3a669dc34313fbfe518df7dc2ded3ade5dde695f8d773afe87bf8a8b0d4" }, "woman_tone5": { "category": "people", "moji": "👩🏿", + "description": "woman tone 5", "unicodeVersion": "8.0", "digest": "9ae9b14dfff40fa60a565d89479727feeba4fd6ffea9acb353a81b14aba751d4" }, "womans_clothes": { "category": "people", "moji": "👚", + "description": "womans clothes", "unicodeVersion": "6.0", "digest": "d12a27810780fe5cd8118ed4587e0c4e70dbe9bcd014c6866fe6a8c9c7c55698" }, "womans_hat": { "category": "people", "moji": "👒", + "description": "womans hat", "unicodeVersion": "6.0", "digest": "52a0255b3483085bd125d39b74516ab6a81003964f44995c2fac821e7ff93086" }, "womens": { "category": "symbols", "moji": "🚺", + "description": "womens symbol", "unicodeVersion": "6.0", "digest": "7e38964006f8b28dfa2b3e9b2b16553bb50c18a63455f556b0bff35ee172137e" }, "worried": { "category": "people", "moji": "😟", + "description": "worried face", "unicodeVersion": "6.1", "digest": "5a073985e1344bc34201ef94a491f7f2b946f5828c9fdbc57eeb2dcd87ac3a6b" }, "wrench": { "category": "objects", "moji": "🔧", + "description": "wrench", "unicodeVersion": "6.0", "digest": "81aae53bc892035b905bf3ec5b442a8ecc95027c5fa9eb51b7c3e7d8fad3f3f4" }, "wrestlers": { "category": "activity", "moji": "🤼", + "description": "wrestlers", "unicodeVersion": "9.0", "digest": "9be983f3f9438f3ab8f6b643a958371d1e710c6d78e728f3465141811f05c2d5" }, "wrestlers_tone1": { "category": "activity", "moji": "🤼🏻", + "description": "wrestlers tone 1", "unicodeVersion": "9.0", "digest": "60461f83bfc93ce59dd027eab4782b7f206a7b142719fa72f301e047dc83a5d9" }, "wrestlers_tone2": { "category": "activity", "moji": "🤼🏼", + "description": "wrestlers tone 2", "unicodeVersion": "9.0", "digest": "67ad93c86e6c58d552c18e7a0105cc81fd9bb0474da51f788eba2e4c14b4a636" }, "wrestlers_tone3": { "category": "activity", "moji": "🤼🏽", + "description": "wrestlers tone 3", "unicodeVersion": "9.0", "digest": "6bfd06c4435cabf2def153912040e05bf8db424fa383148ddda6d0ce8a8a3349" }, "wrestlers_tone4": { "category": "activity", "moji": "🤼🏾", + "description": "wrestlers tone 4", "unicodeVersion": "9.0", "digest": "597312678834c4d288c238482879856d5eba4620deb1eaef495f428e2ba5f2a5" }, "wrestlers_tone5": { "category": "activity", "moji": "🤼🏿", + "description": "wrestlers tone 5", "unicodeVersion": "9.0", "digest": "d6aebdf1e44fd825b9a5b3716aefbc53f4b4dbb73cb2a628c0f2994ebfd34614" }, "writing_hand": { "category": "people", "moji": "✍", + "description": "writing hand", "unicodeVersion": "1.1", "digest": "110517ae4da5587e8b0662881658e27da4120bfacec54734fd6657831d4d782f" }, "writing_hand_tone1": { "category": "people", "moji": "✍🏻", + "description": "writing hand tone 1", "unicodeVersion": "8.0", "digest": "2c7e2108e1990490b681343c1b01b4183d4f18fbdef792f113b2f87595e0dad0" }, "writing_hand_tone2": { "category": "people", "moji": "✍🏼", + "description": "writing hand tone 2", "unicodeVersion": "8.0", "digest": "87ec8d44f472d301adbcbd50d8c852b609e46584057f59cc1527401db363c1bf" }, "writing_hand_tone3": { "category": "people", "moji": "✍🏽", + "description": "writing hand tone 3", "unicodeVersion": "8.0", "digest": "4a48ddef91f7264e8fa9cca223554db22b3a2e3153e94b88d146644ea6dd661e" }, "writing_hand_tone4": { "category": "people", "moji": "✍🏾", + "description": "writing hand tone 4", "unicodeVersion": "8.0", "digest": "e5254564a1f91e42ee59f359d8cd26f52abdc04dca8f3b37cb2f140cb7f71390" }, "writing_hand_tone5": { "category": "people", "moji": "✍🏿", + "description": "writing hand tone 5", "unicodeVersion": "8.0", "digest": "61299bf86d83d323ca3e6052c535ae66c6f7b3d9866a37db0464223b8bc28523" }, "x": { "category": "symbols", "moji": "❌", + "description": "cross mark", "unicodeVersion": "6.0", "digest": "3e5a7918e31ddefdf1ce73972365e2f0bfd2917d6a450c1a278c108349c9425d" }, "yellow_heart": { "category": "symbols", "moji": "💛", + "description": "yellow heart", "unicodeVersion": "6.0", "digest": "a1098f2f04c29754cc9974324508386787d4d803b57cf691d42de414cb2679d6" }, "yen": { "category": "objects", "moji": "💴", + "description": "banknote with yen sign", "unicodeVersion": "6.0", "digest": "944daaeb3f6369c807c0e63b106cee1360040f7800a70c0d942a992f25a55da7" }, "yin_yang": { "category": "symbols", "moji": "☯", + "description": "yin yang", "unicodeVersion": "1.1", "digest": "5ee8d13dacf41306a09237bfcff6abeef110331b40eb7d6e80600628c1327545" }, "yum": { "category": "people", "moji": "😋", + "description": "face savouring delicious food", "unicodeVersion": "6.0", "digest": "31a89088c21bd7a74a3a26d731a907d1bc49436300a9f9c55248703cf7ef44c7" }, "zap": { "category": "nature", "moji": "⚡", + "description": "high voltage sign", "unicodeVersion": "4.0", "digest": "9f8144ae6f866129aea41bbf694b0c858ef9352a139969e57cd8db73385f52c3" }, "zero": { "category": "symbols", "moji": "0️⃣", + "description": "keycap digit zero", "unicodeVersion": "3.0", "digest": "1b27b5c904defadbdd28ace67a6be5c277ff043297db7cd9f672bbf84e37fa1a" }, "zipper_mouth": { "category": "people", "moji": "🤐", + "description": "zipper-mouth face", "unicodeVersion": "8.0", "digest": "81bee5aa1202dfd5a4c7badb71ec0e44b8f75c2cbef94e6fd35c593d8770ae43" }, "zzz": { "category": "people", "moji": "💤", + "description": "sleeping symbol", "unicodeVersion": "6.0", "digest": "b3313d0c44a59fa9d4ce9f7eb4d07ff71dfc8bb01798154250f27cdcf3c693b5" } diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index e5793fbc5cb..710deba5ae3 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -20,6 +20,8 @@ module API error!(errors[:validate_fork], 422) elsif errors[:validate_branches].any? conflict!(errors[:validate_branches]) + elsif errors[:base].any? + error!(errors[:base], 422) end render_api_error!(errors, 400) diff --git a/lib/api/v3/merge_requests.rb b/lib/api/v3/merge_requests.rb index 3077240e650..1616142a619 100644 --- a/lib/api/v3/merge_requests.rb +++ b/lib/api/v3/merge_requests.rb @@ -23,6 +23,8 @@ module API error!(errors[:validate_fork], 422) elsif errors[:validate_branches].any? conflict!(errors[:validate_branches]) + elsif errors[:base].any? + error!(errors[:base], 422) end render_api_error!(errors, 400) diff --git a/lib/banzai/reference_parser/merge_request_parser.rb b/lib/banzai/reference_parser/merge_request_parser.rb index 84a28b33d7c..8b0662749fd 100644 --- a/lib/banzai/reference_parser/merge_request_parser.rb +++ b/lib/banzai/reference_parser/merge_request_parser.rb @@ -33,7 +33,8 @@ module Banzai { namespace: :owner }, { group: [:owners, :group_members] }, :invited_groups, - :project_members + :project_members, + :project_feature ] }), self.class.data_attribute diff --git a/lib/constraints/group_url_constrainer.rb b/lib/constraints/group_url_constrainer.rb index bae4db1ca4d..1501f64d537 100644 --- a/lib/constraints/group_url_constrainer.rb +++ b/lib/constraints/group_url_constrainer.rb @@ -2,16 +2,8 @@ class GroupUrlConstrainer def matches?(request) id = request.params[:id] - return false unless valid?(id) + return false unless DynamicPathValidator.valid?(id) Group.find_by_full_path(id).present? end - - private - - def valid?(id) - id.split('/').all? do |namespace| - NamespaceValidator.valid?(namespace) - end - end end diff --git a/lib/constraints/project_url_constrainer.rb b/lib/constraints/project_url_constrainer.rb index a10b4657d7d..d0ce2caffff 100644 --- a/lib/constraints/project_url_constrainer.rb +++ b/lib/constraints/project_url_constrainer.rb @@ -4,9 +4,7 @@ class ProjectUrlConstrainer project_path = request.params[:project_id] || request.params[:id] full_path = namespace_path + '/' + project_path - unless ProjectPathValidator.valid?(project_path) - return false - end + return false unless DynamicPathValidator.valid?(full_path) Project.find_by_full_path(full_path).present? end diff --git a/lib/gitlab/ci/cron_parser.rb b/lib/gitlab/ci/cron_parser.rb index a3cc350ef22..dad8c3cdf5b 100644 --- a/lib/gitlab/ci/cron_parser.rb +++ b/lib/gitlab/ci/cron_parser.rb @@ -6,7 +6,7 @@ module Gitlab def initialize(cron, cron_timezone = 'UTC') @cron = cron - @cron_timezone = cron_timezone + @cron_timezone = ActiveSupport::TimeZone.find_tzinfo(cron_timezone).name end def next_time_from(time) @@ -24,8 +24,23 @@ module Gitlab private + # NOTE: + # cron_timezone can only accept timezones listed in TZInfo::Timezone. + # Aliases of Timezones from ActiveSupport::TimeZone are NOT accepted, + # because Rufus::Scheduler only supports TZInfo::Timezone. + # + # For example, those codes have the same effect. + # Time.zone = 'Pacific Time (US & Canada)' (ActiveSupport::TimeZone) + # Time.zone = 'America/Los_Angeles' (TZInfo::Timezone) + # + # However, try_parse_cron only accepts the latter format. + # try_parse_cron('* * * * *', 'Pacific Time (US & Canada)') -> Doesn't work + # try_parse_cron('* * * * *', 'America/Los_Angeles') -> Works + # If you want to know more, please take a look + # https://github.com/rails/rails/blob/master/activesupport/lib/active_support/values/time_zone.rb def try_parse_cron(cron, cron_timezone) - Rufus::Scheduler.parse("#{cron} #{cron_timezone}") + cron_line = Rufus::Scheduler.parse("#{cron} #{cron_timezone}") + cron_line if cron_line.is_a?(Rufus::Scheduler::CronLine) rescue # noop end diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index 6dabbe0264c..298b1a1f4e6 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -498,6 +498,29 @@ module Gitlab columns(table).find { |column| column.name == name } end + + # This will replace the first occurance of a string in a column with + # the replacement + # On postgresql we can use `regexp_replace` for that. + # On mysql we find the location of the pattern, and overwrite it + # with the replacement + def replace_sql(column, pattern, replacement) + quoted_pattern = Arel::Nodes::Quoted.new(pattern.to_s) + quoted_replacement = Arel::Nodes::Quoted.new(replacement.to_s) + + if Database.mysql? + locate = Arel::Nodes::NamedFunction. + new('locate', [quoted_pattern, column]) + insert_in_place = Arel::Nodes::NamedFunction. + new('insert', [column, locate, pattern.size, quoted_replacement]) + + Arel::Nodes::SqlLiteral.new(insert_in_place.to_sql) + else + replace = Arel::Nodes::NamedFunction. + new("regexp_replace", [column, quoted_pattern, quoted_replacement]) + Arel::Nodes::SqlLiteral.new(replace.to_sql) + end + end end end end diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1.rb new file mode 100644 index 00000000000..89530082cd2 --- /dev/null +++ b/lib/gitlab/database/rename_reserved_paths_migration/v1.rb @@ -0,0 +1,35 @@ +# This module can be included in migrations to make it easier to rename paths +# of `Namespace` & `Project` models certain paths would become `reserved`. +# +# If the way things are stored on the filesystem related to namespaces and +# projects ever changes. Don't update this module, or anything nested in `V1`, +# since it needs to keep functioning for all migrations using it using the state +# that the data is in at the time. Instead, create a `V2` module that implements +# the new way of reserving paths. +module Gitlab + module Database + module RenameReservedPathsMigration + module V1 + def self.included(kls) + kls.include(MigrationHelpers) + end + + def rename_wildcard_paths(one_or_more_paths) + rename_child_paths(one_or_more_paths) + paths = Array(one_or_more_paths) + RenameProjects.new(paths, self).rename_projects + end + + def rename_child_paths(one_or_more_paths) + paths = Array(one_or_more_paths) + RenameNamespaces.new(paths, self).rename_namespaces(type: :child) + end + + def rename_root_paths(paths) + paths = Array(paths) + RenameNamespaces.new(paths, self).rename_namespaces(type: :top_level) + end + end + end + end +end diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb new file mode 100644 index 00000000000..4fdcb682c2f --- /dev/null +++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb @@ -0,0 +1,76 @@ +module Gitlab + module Database + module RenameReservedPathsMigration + module V1 + module MigrationClasses + module Routable + def full_path + if route && route.path.present? + @full_path ||= route.path + else + update_route if persisted? + + build_full_path + end + end + + def build_full_path + if parent && path + parent.full_path + '/' + path + else + path + end + end + + def update_route + prepare_route + route.save + end + + def prepare_route + route || build_route(source: self) + route.path = build_full_path + @full_path = nil + end + end + + class Namespace < ActiveRecord::Base + include MigrationClasses::Routable + self.table_name = 'namespaces' + belongs_to :parent, + class_name: "#{MigrationClasses.name}::Namespace" + has_one :route, as: :source + has_many :children, + class_name: "#{MigrationClasses.name}::Namespace", + foreign_key: :parent_id + + # Overridden to have the correct `source_type` for the `route` relation + def self.name + 'Namespace' + end + end + + class Route < ActiveRecord::Base + self.table_name = 'routes' + belongs_to :source, polymorphic: true + end + + class Project < ActiveRecord::Base + include MigrationClasses::Routable + has_one :route, as: :source + self.table_name = 'projects' + + def repository_storage_path + Gitlab.config.repositories.storages[repository_storage]['path'] + end + + # Overridden to have the correct `source_type` for the `route` relation + def self.name + 'Project' + end + end + end + end + end + end +end diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb new file mode 100644 index 00000000000..de4e6e7c404 --- /dev/null +++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb @@ -0,0 +1,131 @@ +module Gitlab + module Database + module RenameReservedPathsMigration + module V1 + class RenameBase + attr_reader :paths, :migration + + delegate :update_column_in_batches, + :replace_sql, + to: :migration + + def initialize(paths, migration) + @paths = paths + @migration = migration + end + + def path_patterns + @path_patterns ||= paths.map { |path| "%#{path}" } + end + + def rename_path_for_routable(routable) + old_path = routable.path + old_full_path = routable.full_path + # Only remove the last occurrence of the path name to get the parent namespace path + namespace_path = remove_last_occurrence(old_full_path, old_path) + new_path = rename_path(namespace_path, old_path) + new_full_path = join_routable_path(namespace_path, new_path) + + # skips callbacks & validations + routable.class.where(id: routable). + update_all(path: new_path) + + rename_routes(old_full_path, new_full_path) + + [old_full_path, new_full_path] + end + + def rename_routes(old_full_path, new_full_path) + replace_statement = replace_sql(Route.arel_table[:path], + old_full_path, + new_full_path) + + update_column_in_batches(:routes, :path, replace_statement) do |table, query| + query.where(MigrationClasses::Route.arel_table[:path].matches("#{old_full_path}%")) + end + end + + def rename_path(namespace_path, path_was) + counter = 0 + path = "#{path_was}#{counter}" + + while route_exists?(join_routable_path(namespace_path, path)) + counter += 1 + path = "#{path_was}#{counter}" + end + + path + end + + def remove_last_occurrence(string, pattern) + string.reverse.sub(pattern.reverse, "").reverse + end + + def join_routable_path(namespace_path, top_level) + if namespace_path.present? + File.join(namespace_path, top_level) + else + top_level + end + end + + def route_exists?(full_path) + MigrationClasses::Route.where(Route.arel_table[:path].matches(full_path)).any? + end + + def move_pages(old_path, new_path) + move_folders(pages_dir, old_path, new_path) + end + + def move_uploads(old_path, new_path) + return unless file_storage? + + move_folders(uploads_dir, old_path, new_path) + end + + def move_folders(directory, old_relative_path, new_relative_path) + old_path = File.join(directory, old_relative_path) + return unless File.directory?(old_path) + + new_path = File.join(directory, new_relative_path) + FileUtils.mv(old_path, new_path) + end + + def remove_cached_html_for_projects(project_ids) + update_column_in_batches(:projects, :description_html, nil) do |table, query| + query.where(table[:id].in(project_ids)) + end + + update_column_in_batches(:issues, :description_html, nil) do |table, query| + query.where(table[:project_id].in(project_ids)) + end + + update_column_in_batches(:merge_requests, :description_html, nil) do |table, query| + query.where(table[:target_project_id].in(project_ids)) + end + + update_column_in_batches(:notes, :note_html, nil) do |table, query| + query.where(table[:project_id].in(project_ids)) + end + + update_column_in_batches(:milestones, :description_html, nil) do |table, query| + query.where(table[:project_id].in(project_ids)) + end + end + + def file_storage? + CarrierWave::Uploader::Base.storage == CarrierWave::Storage::File + end + + def uploads_dir + File.join(CarrierWave.root, "uploads") + end + + def pages_dir + Settings.pages.path + end + end + end + end + end +end diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb new file mode 100644 index 00000000000..b9f4f3cff3c --- /dev/null +++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb @@ -0,0 +1,72 @@ +module Gitlab + module Database + module RenameReservedPathsMigration + module V1 + class RenameNamespaces < RenameBase + include Gitlab::ShellAdapter + + def rename_namespaces(type:) + namespaces_for_paths(type: type).each do |namespace| + rename_namespace(namespace) + end + end + + def namespaces_for_paths(type:) + namespaces = case type + when :child + MigrationClasses::Namespace.where.not(parent_id: nil) + when :top_level + MigrationClasses::Namespace.where(parent_id: nil) + end + with_paths = MigrationClasses::Route.arel_table[:path]. + matches_any(path_patterns) + namespaces.joins(:route).where(with_paths) + end + + def rename_namespace(namespace) + old_full_path, new_full_path = rename_path_for_routable(namespace) + + move_repositories(namespace, old_full_path, new_full_path) + move_uploads(old_full_path, new_full_path) + move_pages(old_full_path, new_full_path) + remove_cached_html_for_projects(projects_for_namespace(namespace).map(&:id)) + end + + def move_repositories(namespace, old_full_path, new_full_path) + repo_paths_for_namespace(namespace).each do |repository_storage_path| + # Ensure old directory exists before moving it + gitlab_shell.add_namespace(repository_storage_path, old_full_path) + + unless gitlab_shell.mv_namespace(repository_storage_path, old_full_path, new_full_path) + message = "Exception moving path #{repository_storage_path} \ + from #{old_full_path} to #{new_full_path}" + Rails.logger.error message + end + end + end + + def repo_paths_for_namespace(namespace) + projects_for_namespace(namespace).distinct.select(:repository_storage). + map(&:repository_storage_path) + end + + def projects_for_namespace(namespace) + namespace_ids = child_ids_for_parent(namespace, ids: [namespace.id]) + namespace_or_children = MigrationClasses::Project. + arel_table[:namespace_id]. + in(namespace_ids) + MigrationClasses::Project.where(namespace_or_children) + end + + def child_ids_for_parent(namespace, ids: []) + namespace.children.each do |child| + ids << child.id + child_ids_for_parent(child, ids: ids) if child.children.any? + end + ids + end + end + end + end + end +end diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb new file mode 100644 index 00000000000..448717eb744 --- /dev/null +++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb @@ -0,0 +1,45 @@ +module Gitlab + module Database + module RenameReservedPathsMigration + module V1 + class RenameProjects < RenameBase + include Gitlab::ShellAdapter + + def rename_projects + projects_for_paths.each do |project| + rename_project(project) + end + + remove_cached_html_for_projects(projects_for_paths.map(&:id)) + end + + def rename_project(project) + old_full_path, new_full_path = rename_path_for_routable(project) + + move_repository(project, old_full_path, new_full_path) + move_repository(project, "#{old_full_path}.wiki", "#{new_full_path}.wiki") + move_uploads(old_full_path, new_full_path) + move_pages(old_full_path, new_full_path) + end + + def move_repository(project, old_path, new_path) + unless gitlab_shell.mv_repository(project.repository_storage_path, + old_path, + new_path) + Rails.logger.error "Error moving #{old_path} to #{new_path}" + end + end + + def projects_for_paths + return @projects_for_paths if @projects_for_paths + + with_paths = MigrationClasses::Route.arel_table[:path] + .matches_any(path_patterns) + + @projects_for_paths = MigrationClasses::Project.joins(:route).where(with_paths) + end + end + end + end + end +end diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb index 419d56a51e0..c270c0ea9ff 100644 --- a/lib/gitlab/email/receiver.rb +++ b/lib/gitlab/email/receiver.rb @@ -70,6 +70,8 @@ module Gitlab # Handle emails from clients which append with commas, # example clients are Microsoft exchange and iOS app Gitlab::IncomingEmail.scan_fallback_references(references) + when nil + [] end end diff --git a/lib/gitlab/emoji.rb b/lib/gitlab/emoji.rb index a16d9fc2265..e3e36b35ce9 100644 --- a/lib/gitlab/emoji.rb +++ b/lib/gitlab/emoji.rb @@ -54,7 +54,7 @@ module Gitlab unicode_version: emoji_unicode_version(emoji_name) } - ActionController::Base.helpers.content_tag('gl-emoji', emoji_info['moji'], data: data) + ActionController::Base.helpers.content_tag('gl-emoji', emoji_info['moji'], title: emoji_info['description'], data: data) end end end diff --git a/lib/gitlab/etag_caching/router.rb b/lib/gitlab/etag_caching/router.rb index f6e4f279c06..aac210f19e8 100644 --- a/lib/gitlab/etag_caching/router.rb +++ b/lib/gitlab/etag_caching/router.rb @@ -2,31 +2,39 @@ module Gitlab module EtagCaching class Router Route = Struct.new(:regexp, :name) - - RESERVED_WORDS = NamespaceValidator::WILDCARD_ROUTES.map { |word| "/#{word}/" }.join('|') + # We enable an ETag for every request matching the regex. + # To match a regex the path needs to match the following: + # - Don't contain a reserved word (expect for the words used in the + # regex itself) + # - Ending in `noteable/issue/<id>/notes` for the `issue_notes` route + # - Ending in `issues/id`/rendered_title` for the `issue_title` route + USED_IN_ROUTES = %w[noteable issue notes issues rendered_title + commit pipelines merge_requests new].freeze + RESERVED_WORDS = DynamicPathValidator::WILDCARD_ROUTES - USED_IN_ROUTES + RESERVED_WORDS_REGEX = Regexp.union(*RESERVED_WORDS) ROUTES = [ Gitlab::EtagCaching::Router::Route.new( - %r(^(?!.*(#{RESERVED_WORDS})).*/noteable/issue/\d+/notes\z), + %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/noteable/issue/\d+/notes\z), 'issue_notes' ), Gitlab::EtagCaching::Router::Route.new( - %r(^(?!.*(#{RESERVED_WORDS})).*/issues/\d+/rendered_title\z), + %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/issues/\d+/rendered_title\z), 'issue_title' ), Gitlab::EtagCaching::Router::Route.new( - %r(^(?!.*(#{RESERVED_WORDS})).*/commit/\S+/pipelines\.json\z), + %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/commit/\S+/pipelines\.json\z), 'commit_pipelines' ), Gitlab::EtagCaching::Router::Route.new( - %r(^(?!.*(#{RESERVED_WORDS})).*/merge_requests/new\.json\z), + %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/merge_requests/new\.json\z), 'new_merge_request_pipelines' ), Gitlab::EtagCaching::Router::Route.new( - %r(^(?!.*(#{RESERVED_WORDS})).*/merge_requests/\d+/pipelines\.json\z), + %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/merge_requests/\d+/pipelines\.json\z), 'merge_request_pipelines' ), Gitlab::EtagCaching::Router::Route.new( - %r(^(?!.*(#{RESERVED_WORDS})).*/pipelines\.json\z), + %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/pipelines\.json\z), 'project_pipelines' ) ].freeze diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 452dba7971d..acd0037ee4f 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -45,17 +45,13 @@ module Gitlab # Default branch in the repository def root_ref - # NOTE: This feature is intentionally disabled until - # https://gitlab.com/gitlab-org/gitaly/issues/179 is resolved - # @root_ref ||= Gitlab::GitalyClient.migrate(:root_ref) do |is_enabled| - # if is_enabled - # gitaly_ref_client.default_branch_name - # else - @root_ref ||= discover_default_branch - # end - # end - rescue GRPC::BadStatus => e - raise CommandError.new(e) + @root_ref ||= gitaly_migrate(:root_ref) do |is_enabled| + if is_enabled + gitaly_ref_client.default_branch_name + else + discover_default_branch + end + end end # Alias to old method for compatibility @@ -72,17 +68,13 @@ module Gitlab # Returns an Array of branch names # sorted by name ASC def branch_names - # Gitlab::GitalyClient.migrate(:branch_names) do |is_enabled| - # NOTE: This feature is intentionally disabled until - # https://gitlab.com/gitlab-org/gitaly/issues/179 is resolved - # if is_enabled - # gitaly_ref_client.branch_names - # else - branches.map(&:name) - # end - # end - rescue GRPC::BadStatus => e - raise CommandError.new(e) + gitaly_migrate(:branch_names) do |is_enabled| + if is_enabled + gitaly_ref_client.branch_names + else + branches.map(&:name) + end + end end # Returns an Array of Branches @@ -122,30 +114,43 @@ module Gitlab # Returns the number of valid branches def branch_count - rugged.branches.count do |ref| - begin - ref.name && ref.target # ensures the branch is valid + Gitlab::GitalyClient.migrate(:branch_names) do |is_enabled| + if is_enabled + gitaly_ref_client.count_branch_names + else + rugged.branches.count do |ref| + begin + ref.name && ref.target # ensures the branch is valid - true - rescue Rugged::ReferenceError - false + true + rescue Rugged::ReferenceError + false + end + end + end + end + end + + # Returns the number of valid tags + def tag_count + Gitlab::GitalyClient.migrate(:tag_names) do |is_enabled| + if is_enabled + gitaly_ref_client.count_tag_names + else + rugged.tags.count end end end # Returns an Array of tag names def tag_names - # Gitlab::GitalyClient.migrate(:tag_names) do |is_enabled| - # NOTE: This feature is intentionally disabled until - # https://gitlab.com/gitlab-org/gitaly/issues/179 is resolved - # if is_enabled - # gitaly_ref_client.tag_names - # else - rugged.tags.map { |t| t.name } - # end - # end - rescue GRPC::BadStatus => e - raise CommandError.new(e) + gitaly_migrate(:tag_names) do |is_enabled| + if is_enabled + gitaly_ref_client.tag_names + else + rugged.tags.map { |t| t.name } + end + end end # Returns an Array of Tags @@ -451,7 +456,7 @@ module Gitlab # Returns true is +from+ is direct ancestor to +to+, otherwise false def is_ancestor?(from, to) - Gitlab::GitalyClient::Commit.is_ancestor(self, from, to) + gitaly_commit_client.is_ancestor(from, to) end # Return an array of Diff objects that represent the diff @@ -1273,6 +1278,18 @@ module Gitlab @gitaly_ref_client ||= Gitlab::GitalyClient::Ref.new(self) end + def gitaly_commit_client + @gitaly_commit_client ||= Gitlab::GitalyClient::Commit.new(self) + end + + def gitaly_migrate(method, &block) + Gitlab::GitalyClient.migrate(method, &block) + rescue GRPC::NotFound => e + raise NoRepository.new(e) + rescue GRPC::BadStatus => e + raise CommandError.new(e) + end + # Returns the `Rugged` sorting type constant for a given # sort type key. Valid keys are `:none`, `:topo`, and `:date` def rugged_sort_type(key) diff --git a/lib/gitlab/gitaly_client/commit.rb b/lib/gitlab/gitaly_client/commit.rb index b7f39f3ef0b..27db1e19bc1 100644 --- a/lib/gitlab/gitaly_client/commit.rb +++ b/lib/gitlab/gitaly_client/commit.rb @@ -5,6 +5,23 @@ module Gitlab # See http://stackoverflow.com/a/40884093/1856239 and https://github.com/git/git/blob/3ad8b5bf26362ac67c9020bf8c30eee54a84f56d/cache.h#L1011-L1012 EMPTY_TREE_ID = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'.freeze + attr_accessor :stub + + def initialize(repository) + @gitaly_repo = repository.gitaly_repository + @stub = Gitaly::Commit::Stub.new(nil, nil, channel_override: repository.gitaly_channel) + end + + def is_ancestor(ancestor_id, child_id) + request = Gitaly::CommitIsAncestorRequest.new( + repository: @gitaly_repo, + ancestor_id: ancestor_id, + child_id: child_id + ) + + @stub.commit_is_ancestor(request).value + end + class << self def diff_from_parent(commit, options = {}) repository = commit.project.repository @@ -20,18 +37,6 @@ module Gitlab Gitlab::Git::DiffCollection.new(stub.commit_diff(request), options) end - - def is_ancestor(repository, ancestor_id, child_id) - gitaly_repo = repository.gitaly_repository - stub = Gitaly::Commit::Stub.new(nil, nil, channel_override: repository.gitaly_channel) - request = Gitaly::CommitIsAncestorRequest.new( - repository: gitaly_repo, - ancestor_id: ancestor_id, - child_id: child_id - ) - - stub.commit_is_ancestor(request).value - end end end end diff --git a/lib/gitlab/gitaly_client/ref.rb b/lib/gitlab/gitaly_client/ref.rb index d3c0743db4e..f6c77ef1a3e 100644 --- a/lib/gitlab/gitaly_client/ref.rb +++ b/lib/gitlab/gitaly_client/ref.rb @@ -11,7 +11,9 @@ module Gitlab def default_branch_name request = Gitaly::FindDefaultBranchNameRequest.new(repository: @gitaly_repo) - stub.find_default_branch_name(request).name.gsub(/^refs\/heads\//, '') + branch_name = stub.find_default_branch_name(request).name + + Gitlab::Git.branch_name(branch_name) end def branch_names @@ -34,6 +36,14 @@ module Gitlab stub.find_ref_name(request).name end + def count_tag_names + tag_names.count + end + + def count_branch_names + branch_names.count + end + private def consume_refs_response(response, prefix:) diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index 899a6567768..b95cea371b9 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -41,7 +41,6 @@ project_tree: - :statuses - triggers: - :trigger_schedule - - :deploy_keys - :services - :hooks - protected_branches: @@ -53,10 +52,6 @@ project_tree: # Only include the following attributes for the models specified. included_attributes: - project: - - :description - - :visibility_level - - :archived user: - :id - :email @@ -66,6 +61,29 @@ included_attributes: # Do not include the following attributes for the models specified. excluded_attributes: + project: + - :name + - :path + - :namespace_id + - :creator_id + - :import_url + - :import_status + - :avatar + - :import_type + - :import_source + - :import_error + - :mirror + - :runners_token + - :repository_storage + - :repository_read_only + - :lfs_enabled + - :import_jid + - :created_at + - :updated_at + - :import_jid + - :import_jid + - :id + - :star_count snippets: - :expired_at merge_request_diff: @@ -94,3 +112,5 @@ methods: - :utf8_st_diffs merge_requests: - :diff_head_sha + project: + - :description_html
\ No newline at end of file diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index 2e349b5f9a9..84ab1977dfa 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -71,14 +71,14 @@ module Gitlab def restore_project return @project unless @tree_hash - @project.update(project_params) + @project.update_columns(project_params) @project end def project_params @tree_hash.reject do |key, value| # return params that are not 1 to many or 1 to 1 relations - value.is_a?(Array) || key == key.singularize + value.respond_to?(:each) && !Project.column_names.include?(key) end end diff --git a/lib/gitlab/import_export/reader.rb b/lib/gitlab/import_export/reader.rb index a1e7159fe42..eb7f5120592 100644 --- a/lib/gitlab/import_export/reader.rb +++ b/lib/gitlab/import_export/reader.rb @@ -15,7 +15,10 @@ module Gitlab # Outputs a hash in the format described here: http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html # for outputting a project in JSON format, including its relations and sub relations. def project_tree - @attributes_finder.find_included(:project).merge(include: build_hash(@tree)) + attributes = @attributes_finder.find(:project) + project_attributes = attributes.is_a?(Hash) ? attributes[:project] : {} + + project_attributes.merge(include: build_hash(@tree)) rescue => e @shared.error(e) false diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index 08b061d5e31..b7fef5dd068 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -22,6 +22,10 @@ module Gitlab @namespace_regex ||= /\A#{NAMESPACE_REGEX_STR}\z/.freeze end + def full_namespace_regex + @full_namespace_regex ||= %r{\A#{FULL_NAMESPACE_REGEX_STR}\z} + end + def namespace_route_regex @namespace_route_regex ||= /#{NAMESPACE_REGEX_STR}/.freeze end diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb index 54728e5ff0e..e46ff313654 100644 --- a/lib/gitlab/user_access.rb +++ b/lib/gitlab/user_access.rb @@ -44,9 +44,7 @@ module Gitlab if ProtectedBranch.protected?(project, ref) return true if project.empty_repo? && project.user_can_push_to_empty_repo?(user) - has_access = project.protected_branches.protected_ref_accessible_to?(ref, user, action: :push) - - has_access || !project.repository.branch_exists?(ref) && can_merge_to_branch?(ref) + project.protected_branches.protected_ref_accessible_to?(ref, user, action: :push) else user.can?(:push_code, project) end diff --git a/lib/tasks/gemojione.rake b/lib/tasks/gemojione.rake index 5293f5af12d..b5572a39d30 100644 --- a/lib/tasks/gemojione.rake +++ b/lib/tasks/gemojione.rake @@ -19,6 +19,7 @@ namespace :gemojione do entry = { category: emoji_hash['category'], moji: emoji_hash['moji'], + description: emoji_hash['description'], unicodeVersion: Gitlab::Emoji.emoji_unicode_version(name), digest: hash_digest, } diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake index 046780481ba..3c5bc0146a1 100644 --- a/lib/tasks/gitlab/gitaly.rake +++ b/lib/tasks/gitlab/gitaly.rake @@ -1,18 +1,18 @@ namespace :gitlab do namespace :gitaly do desc "GitLab | Install or upgrade gitaly" - task :install, [:dir] => :environment do |t, args| + task :install, [:dir, :repo] => :environment do |t, args| require 'toml' warn_user_is_not_gitlab unless args.dir.present? abort %(Please specify the directory where you want to install gitaly:\n rake "gitlab:gitaly:install[/home/git/gitaly]") end + args.with_defaults(repo: 'https://gitlab.com/gitlab-org/gitaly.git') version = Gitlab::GitalyClient.expected_server_version - repo = 'https://gitlab.com/gitlab-org/gitaly.git' - checkout_or_clone_version(version: version, repo: repo, target_dir: args.dir) + checkout_or_clone_version(version: version, repo: args.repo, target_dir: args.dir) _, status = Gitlab::Popen.popen(%w[which gmake]) command = status.zero? ? 'gmake' : 'make' diff --git a/lib/tasks/gitlab/workhorse.rake b/lib/tasks/gitlab/workhorse.rake index a00b02188cf..e7ac0b5859f 100644 --- a/lib/tasks/gitlab/workhorse.rake +++ b/lib/tasks/gitlab/workhorse.rake @@ -1,16 +1,16 @@ namespace :gitlab do namespace :workhorse do desc "GitLab | Install or upgrade gitlab-workhorse" - task :install, [:dir] => :environment do |t, args| + task :install, [:dir, :repo] => :environment do |t, args| warn_user_is_not_gitlab unless args.dir.present? abort %(Please specify the directory where you want to install gitlab-workhorse:\n rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]") end + args.with_defaults(repo: 'https://gitlab.com/gitlab-org/gitlab-workhorse.git') version = Gitlab::Workhorse.version - repo = 'https://gitlab.com/gitlab-org/gitlab-workhorse.git' - checkout_or_clone_version(version: version, repo: repo, target_dir: args.dir) + checkout_or_clone_version(version: version, repo: args.repo, target_dir: args.dir) _, status = Gitlab::Popen.popen(%w[which gmake]) command = status.zero? ? 'gmake' : 'make' diff --git a/package.json b/package.json index f8c151ebd81..9ed5e1a7475 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "jszip-utils": "^0.0.2", "marked": "^0.3.6", "mousetrap": "^1.4.6", + "pdfjs-dist": "^1.8.252", "pikaday": "^1.5.1", "prismjs": "^1.6.0", "raphael": "^2.2.7", @@ -46,6 +47,7 @@ "three-stl-loader": "^1.0.4", "timeago.js": "^2.0.5", "underscore": "^1.8.3", + "url-loader": "^0.5.8", "visibilityjs": "^1.2.4", "vue": "^2.2.6", "vue-loader": "^11.3.4", diff --git a/rubocop/cop/migration/add_column_with_default_to_large_table.rb b/rubocop/cop/migration/add_column_with_default_to_large_table.rb new file mode 100644 index 00000000000..2372e6b60ea --- /dev/null +++ b/rubocop/cop/migration/add_column_with_default_to_large_table.rb @@ -0,0 +1,51 @@ +require_relative '../../migration_helpers' + +module RuboCop + module Cop + module Migration + # This cop checks for `add_column_with_default` on a table that's been + # explicitly blacklisted because of its size. + # + # Even though this helper performs the update in batches to avoid + # downtime, using it with tables with millions of rows still causes a + # significant delay in the deploy process and is best avoided. + # + # See https://gitlab.com/gitlab-com/infrastructure/issues/1602 for more + # information. + class AddColumnWithDefaultToLargeTable < RuboCop::Cop::Cop + include MigrationHelpers + + MSG = 'Using `add_column_with_default` on the `%s` table will take a ' \ + 'long time to complete, and should be avoided unless absolutely ' \ + 'necessary'.freeze + + LARGE_TABLES = %i[ + events + issues + merge_requests + namespaces + notes + projects + routes + users + ].freeze + + def_node_matcher :add_column_with_default?, <<~PATTERN + (send nil :add_column_with_default $(sym ...) ...) + PATTERN + + def on_send(node) + return unless in_migration?(node) + + matched = add_column_with_default?(node) + return unless matched + + table = matched.to_a.first + return unless LARGE_TABLES.include?(table) + + add_offense(node, :expression, format(MSG, table)) + end + end + end + end +end diff --git a/rubocop/cop/migration/add_column_with_default.rb b/rubocop/cop/migration/reversible_add_column_with_default.rb index 54a920d4b49..f413f06f39b 100644 --- a/rubocop/cop/migration/add_column_with_default.rb +++ b/rubocop/cop/migration/reversible_add_column_with_default.rb @@ -5,29 +5,30 @@ module RuboCop module Migration # Cop that checks if `add_column_with_default` is used with `up`/`down` methods # and not `change`. - class AddColumnWithDefault < RuboCop::Cop::Cop + class ReversibleAddColumnWithDefault < RuboCop::Cop::Cop include MigrationHelpers + def_node_matcher :add_column_with_default?, <<~PATTERN + (send nil :add_column_with_default $...) + PATTERN + + def_node_matcher :defines_change?, <<~PATTERN + (def :change ...) + PATTERN + MSG = '`add_column_with_default` is not reversible so you must manually define ' \ 'the `up` and `down` methods in your migration class, using `remove_column` in `down`'.freeze def on_send(node) return unless in_migration?(node) - - name = node.children[1] - - return unless name == :add_column_with_default + return unless add_column_with_default?(node) node.each_ancestor(:def) do |def_node| - next unless method_name(def_node) == :change + next unless defines_change?(def_node) add_offense(def_node, :name) end end - - def method_name(node) - node.children.first - end end end end diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb index d580aa6857a..4ff204f939e 100644 --- a/rubocop/rubocop.rb +++ b/rubocop/rubocop.rb @@ -1,9 +1,10 @@ require_relative 'cop/custom_error_class' require_relative 'cop/gem_fetcher' require_relative 'cop/migration/add_column' -require_relative 'cop/migration/add_column_with_default' +require_relative 'cop/migration/add_column_with_default_to_large_table' require_relative 'cop/migration/add_concurrent_foreign_key' require_relative 'cop/migration/add_concurrent_index' require_relative 'cop/migration/add_index' require_relative 'cop/migration/remove_concurrent_index' require_relative 'cop/migration/remove_index' +require_relative 'cop/migration/reversible_add_column_with_default' diff --git a/scripts/lint-doc.sh b/scripts/lint-doc.sh index 62236ed539a..54c1ef3dfdd 100755 --- a/scripts/lint-doc.sh +++ b/scripts/lint-doc.sh @@ -21,4 +21,3 @@ fi echo "✔ Linting passed" exit 0 - diff --git a/scripts/notify_slack.sh b/scripts/notify_slack.sh deleted file mode 100755 index 6b3bc563c7a..00000000000 --- a/scripts/notify_slack.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh -# Sends Slack notification ERROR_MSG to CHANNEL -# An env. variable CI_SLACK_WEBHOOK_URL needs to be set. - -CHANNEL=$1 -ERROR_MSG=$2 - -if [ -z "$CHANNEL" ] || [ -z "$ERROR_MSG" ] || [ -z "$CI_SLACK_WEBHOOK_URL" ]; then - echo "Missing argument(s) - Use: $0 channel message" - echo "and set CI_SLACK_WEBHOOK_URL environment variable." -else - curl -X POST --data-urlencode 'payload={"channel": "'"$CHANNEL"'", "username": "gitlab-ci", "text": "'"$ERROR_MSG"'", "icon_emoji": ":gitlab:"}' "$CI_SLACK_WEBHOOK_URL" -fi
\ No newline at end of file diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh index fd173c0ba88..c727a0e2d88 100755..100644 --- a/scripts/prepare_build.sh +++ b/scripts/prepare_build.sh @@ -1,5 +1,3 @@ -#!/bin/sh - . scripts/utils.sh export SETUP_DB=${SETUP_DB:-true} @@ -32,7 +30,7 @@ sed -i 's/localhost/redis/g' config/resque.yml cp config/gitlab.yml.example config/gitlab.yml if [ "$USE_BUNDLE_INSTALL" != "false" ]; then - retry bundle install --clean $BUNDLE_INSTALL_FLAGS && bundle check + bundle install --clean $BUNDLE_INSTALL_FLAGS && bundle check fi # Only install knapsack after bundle install! Otherwise oddly some native diff --git a/scripts/static-analysis b/scripts/static-analysis new file mode 100755 index 00000000000..192d9d4c3ba --- /dev/null +++ b/scripts/static-analysis @@ -0,0 +1,40 @@ +#!/usr/bin/env ruby + +require ::File.expand_path('../lib/gitlab/popen', __dir__) + +tasks = [ + %w[bundle exec rake config_lint], + %w[bundle exec rake flay], + %w[bundle exec rake haml_lint], + %w[bundle exec rake scss_lint], + %w[bundle exec rake brakeman], + %w[bundle exec license_finder], + %w[scripts/lint-doc.sh], + %w[yarn run eslint], + %w[bundle exec rubocop --require rubocop-rspec] +] + +failed_tasks = tasks.reduce({}) do |failures, task| + output, status = Gitlab::Popen.popen(task) + + puts "Running: #{task.join(' ')}" + puts output + + failures[task.join(' ')] = output unless status.zero? + + failures +end + +if failed_tasks.empty? + puts 'All static analyses passed successfully.' +else + puts "\n===================================================\n\n" + puts "Some static analyses failed:" + + failed_tasks.each do |failed_task, output| + puts "\n**** #{failed_task} failed with the following error:\n\n" + puts output + end + + exit 1 +end diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 760f33b09c1..1bf0533ca24 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -4,7 +4,7 @@ describe ApplicationController do let(:user) { create(:user) } describe '#check_password_expiration' do - let(:controller) { ApplicationController.new } + let(:controller) { described_class.new } it 'redirects if the user is over their password expiry' do user.password_expires_at = Time.new(2002) @@ -34,7 +34,7 @@ describe ApplicationController do describe "#authenticate_user_from_token!" do describe "authenticating a user from a private token" do - controller(ApplicationController) do + controller(described_class) do def index render text: "authenticated" end @@ -66,7 +66,7 @@ describe ApplicationController do end describe "authenticating a user from a personal access token" do - controller(ApplicationController) do + controller(described_class) do def index render text: 'authenticated' end @@ -115,7 +115,7 @@ describe ApplicationController do end context 'two-factor authentication' do - let(:controller) { ApplicationController.new } + let(:controller) { described_class.new } describe '#check_two_factor_requirement' do subject { controller.send :check_two_factor_requirement } diff --git a/spec/controllers/blob_controller_spec.rb b/spec/controllers/blob_controller_spec.rb deleted file mode 100644 index 44e011fd3a8..00000000000 --- a/spec/controllers/blob_controller_spec.rb +++ /dev/null @@ -1,67 +0,0 @@ -require 'spec_helper' - -describe Projects::BlobController do - let(:project) { create(:project, :repository) } - let(:user) { create(:user) } - - before do - sign_in(user) - - project.team << [user, :master] - - allow(project).to receive(:branches).and_return(['master', 'foo/bar/baz']) - allow(project).to receive(:tags).and_return(['v1.0.0', 'v2.0.0']) - controller.instance_variable_set(:@project, project) - end - - describe "GET show" do - render_views - - before do - get(:show, - namespace_id: project.namespace, - project_id: project, - id: id) - end - - context "valid branch, valid file" do - let(:id) { 'master/README.md' } - it { is_expected.to respond_with(:success) } - end - - context "valid branch, invalid file" do - let(:id) { 'master/invalid-path.rb' } - it { is_expected.to respond_with(:not_found) } - end - - context "invalid branch, valid file" do - let(:id) { 'invalid-branch/README.md' } - it { is_expected.to respond_with(:not_found) } - end - - context "binary file" do - let(:id) { 'binary-encoding/encoding/binary-1.bin' } - it { is_expected.to respond_with(:success) } - end - end - - describe 'GET show with tree path' do - render_views - - before do - get(:show, - namespace_id: project.namespace, - project_id: project, - id: id) - controller.instance_variable_set(:@blob, nil) - end - - context 'redirect to tree' do - let(:id) { 'markdown/doc' } - it 'redirects' do - expect(subject). - to redirect_to("/#{project.path_with_namespace}/tree/markdown/doc") - end - end - end -end diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb index 6e4b5f78e33..7cf2996ffd0 100644 --- a/spec/controllers/groups/milestones_controller_spec.rb +++ b/spec/controllers/groups/milestones_controller_spec.rb @@ -6,6 +6,16 @@ describe Groups::MilestonesController do let(:project2) { create(:empty_project, group: group) } let(:user) { create(:user) } let(:title) { '肯定不是中文的问题' } + let(:milestone) do + project_milestone = create(:milestone, project: project) + + GroupMilestone.build( + group, + [project], + project_milestone.title + ) + end + let(:milestone_path) { group_milestone_path(group, milestone.safe_title, title: milestone.title) } before do sign_in(user) @@ -14,6 +24,8 @@ describe Groups::MilestonesController do controller.instance_variable_set(:@group, group) end + it_behaves_like 'milestone tabs' + describe "#create" do it "creates group milestone with Chinese title" do post :create, diff --git a/spec/controllers/oauth/authorizations_controller_spec.rb b/spec/controllers/oauth/authorizations_controller_spec.rb new file mode 100644 index 00000000000..d321bfcea9d --- /dev/null +++ b/spec/controllers/oauth/authorizations_controller_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe Oauth::AuthorizationsController do + let(:user) { create(:user) } + + let(:doorkeeper) do + Doorkeeper::Application.create( + name: "MyApp", + redirect_uri: 'http://example.com', + scopes: "") + end + + let(:params) do + { + response_type: "code", + client_id: doorkeeper.uid, + redirect_uri: doorkeeper.redirect_uri, + state: 'state' + } + end + + before do + sign_in(user) + end + + describe 'GET #new' do + context 'without valid params' do + it 'returns 200 code and renders error view' do + get :new + + expect(response).to have_http_status(200) + expect(response).to render_template('doorkeeper/authorizations/error') + end + end + + context 'with valid params' do + it 'returns 200 code and renders view' do + get :new, params + + expect(response).to have_http_status(200) + expect(response).to render_template('doorkeeper/authorizations/new') + end + + it 'deletes session.user_return_to and redirects when skip authorization' do + request.session['user_return_to'] = 'http://example.com' + allow(controller).to receive(:skip_authorization?).and_return(true) + + get :new, params + + expect(request.session['user_return_to']).to be_nil + expect(response).to have_http_status(302) + end + end + end +end diff --git a/spec/controllers/profiles/personal_access_tokens_spec.rb b/spec/controllers/profiles/personal_access_tokens_controller_spec.rb index 98a43e278b2..98a43e278b2 100644 --- a/spec/controllers/profiles/personal_access_tokens_spec.rb +++ b/spec/controllers/profiles/personal_access_tokens_controller_spec.rb diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb index 0fd09d156c4..3b3caa9d3e6 100644 --- a/spec/controllers/projects/blob_controller_spec.rb +++ b/spec/controllers/projects/blob_controller_spec.rb @@ -3,6 +3,57 @@ require 'rails_helper' describe Projects::BlobController do let(:project) { create(:project, :public, :repository) } + describe "GET show" do + render_views + + context 'with file path' do + before do + get(:show, + namespace_id: project.namespace, + project_id: project, + id: id) + end + + context "valid branch, valid file" do + let(:id) { 'master/README.md' } + it { is_expected.to respond_with(:success) } + end + + context "valid branch, invalid file" do + let(:id) { 'master/invalid-path.rb' } + it { is_expected.to respond_with(:not_found) } + end + + context "invalid branch, valid file" do + let(:id) { 'invalid-branch/README.md' } + it { is_expected.to respond_with(:not_found) } + end + + context "binary file" do + let(:id) { 'binary-encoding/encoding/binary-1.bin' } + it { is_expected.to respond_with(:success) } + end + end + + context 'with tree path' do + before do + get(:show, + namespace_id: project.namespace, + project_id: project, + id: id) + controller.instance_variable_set(:@blob, nil) + end + + context 'redirect to tree' do + let(:id) { 'markdown/doc' } + it 'redirects' do + expect(subject). + to redirect_to("/#{project.path_with_namespace}/tree/markdown/doc") + end + end + end + end + describe 'GET diff' do let(:user) { create(:user) } diff --git a/spec/controllers/projects/deployments_controller_spec.rb b/spec/controllers/projects/deployments_controller_spec.rb new file mode 100644 index 00000000000..89692b601b2 --- /dev/null +++ b/spec/controllers/projects/deployments_controller_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +describe Projects::DeploymentsController do + include ApiHelpers + + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + let(:environment) { create(:environment, name: 'production', project: project) } + + before do + project.add_master(user) + + sign_in(user) + end + + describe 'GET #index' do + it 'returns list of deployments from last 8 hours' do + create(:deployment, environment: environment, created_at: 9.hours.ago) + create(:deployment, environment: environment, created_at: 7.hours.ago) + create(:deployment, environment: environment) + + get :index, environment_params(after: 8.hours.ago) + + expect(response).to be_ok + + expect(json_response['deployments'].count).to eq(2) + end + + it 'returns a list with deployments information' do + create(:deployment, environment: environment) + + get :index, environment_params + + expect(response).to be_ok + expect(response).to match_response_schema('deployments') + end + end + + def environment_params(opts = {}) + opts.reverse_merge(namespace_id: project.namespace, project_id: project, environment_id: environment.id) + end +end diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb index 47e61c3cea8..84a61b2784e 100644 --- a/spec/controllers/projects/milestones_controller_spec.rb +++ b/spec/controllers/projects/milestones_controller_spec.rb @@ -7,6 +7,7 @@ describe Projects::MilestonesController do let(:issue) { create(:issue, project: project, milestone: milestone) } let!(:label) { create(:label, project: project, title: 'Issue Label', issues: [issue]) } let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, milestone: milestone) } + let(:milestone_path) { namespace_project_milestone_path } before do sign_in(user) @@ -14,6 +15,8 @@ describe Projects::MilestonesController do controller.instance_variable_set(:@project, project) end + it_behaves_like 'milestone tabs' + describe "#show" do render_views diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index f140eaef5d5..45f4cf9180d 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -167,6 +167,47 @@ describe Projects::NotesController do end end + describe 'DELETE destroy' do + let(:request_params) do + { + namespace_id: project.namespace, + project_id: project, + id: note, + format: :js + } + end + + context 'user is the author of a note' do + before do + sign_in(note.author) + project.team << [note.author, :developer] + end + + it "returns status 200 for html" do + delete :destroy, request_params + + expect(response).to have_http_status(200) + end + + it "deletes the note" do + expect { delete :destroy, request_params }.to change { Note.count }.from(1).to(0) + end + end + + context 'user is not the author of a note' do + before do + sign_in(user) + project.team << [user, :developer] + end + + it "returns status 404" do + delete :destroy, request_params + + expect(response).to have_http_status(404) + end + end + end + describe 'POST toggle_award_emoji' do before do sign_in(user) diff --git a/spec/controllers/projects/todo_controller_spec.rb b/spec/controllers/projects/todos_controller_spec.rb index c5a4153d991..c5a4153d991 100644 --- a/spec/controllers/projects/todo_controller_spec.rb +++ b/spec/controllers/projects/todos_controller_spec.rb diff --git a/spec/controllers/snippets/notes_controller_spec.rb b/spec/controllers/snippets/notes_controller_spec.rb new file mode 100644 index 00000000000..1c494b8c7ab --- /dev/null +++ b/spec/controllers/snippets/notes_controller_spec.rb @@ -0,0 +1,196 @@ +require 'spec_helper' + +describe Snippets::NotesController do + let(:user) { create(:user) } + + let(:private_snippet) { create(:personal_snippet, :private) } + let(:internal_snippet) { create(:personal_snippet, :internal) } + let(:public_snippet) { create(:personal_snippet, :public) } + + let(:note_on_private) { create(:note_on_personal_snippet, noteable: private_snippet) } + let(:note_on_internal) { create(:note_on_personal_snippet, noteable: internal_snippet) } + let(:note_on_public) { create(:note_on_personal_snippet, noteable: public_snippet) } + + describe 'GET index' do + context 'when a snippet is public' do + before do + note_on_public + + get :index, { snippet_id: public_snippet } + end + + it "returns status 200" do + expect(response).to have_http_status(200) + end + + it "returns not empty array of notes" do + expect(JSON.parse(response.body)["notes"].empty?).to be_falsey + end + end + + context 'when a snippet is internal' do + before do + note_on_internal + end + + context 'when user not logged in' do + it "returns status 404" do + get :index, { snippet_id: internal_snippet } + + expect(response).to have_http_status(404) + end + end + + context 'when user logged in' do + before do + sign_in(user) + end + + it "returns status 200" do + get :index, { snippet_id: internal_snippet } + + expect(response).to have_http_status(200) + end + end + end + + context 'when a snippet is private' do + before do + note_on_private + end + + context 'when user not logged in' do + it "returns status 404" do + get :index, { snippet_id: private_snippet } + + expect(response).to have_http_status(404) + end + end + + context 'when user other than author logged in' do + before do + sign_in(user) + end + + it "returns status 404" do + get :index, { snippet_id: private_snippet } + + expect(response).to have_http_status(404) + end + end + + context 'when author logged in' do + before do + note_on_private + + sign_in(private_snippet.author) + end + + it "returns status 200" do + get :index, { snippet_id: private_snippet } + + expect(response).to have_http_status(200) + end + + it "returns 1 note" do + get :index, { snippet_id: private_snippet } + + expect(JSON.parse(response.body)['notes'].count).to eq(1) + end + end + end + + context 'dont show non visible notes' do + before do + note_on_public + + sign_in(user) + + expect_any_instance_of(Note).to receive(:cross_reference_not_visible_for?).and_return(true) + end + + it "does not return any note" do + get :index, { snippet_id: public_snippet } + + expect(JSON.parse(response.body)['notes'].count).to eq(0) + end + end + end + + describe 'DELETE destroy' do + let(:request_params) do + { + snippet_id: public_snippet, + id: note_on_public, + format: :js + } + end + + context 'when user is the author of a note' do + before do + sign_in(note_on_public.author) + end + + it "returns status 200" do + delete :destroy, request_params + + expect(response).to have_http_status(200) + end + + it "deletes the note" do + expect{ delete :destroy, request_params }.to change{ Note.count }.from(1).to(0) + end + + context 'system note' do + before do + expect_any_instance_of(Note).to receive(:system?).and_return(true) + end + + it "does not delete the note" do + expect{ delete :destroy, request_params }.not_to change{ Note.count } + end + end + end + + context 'when user is not the author of a note' do + before do + sign_in(user) + + note_on_public + end + + it "returns status 404" do + delete :destroy, request_params + + expect(response).to have_http_status(404) + end + + it "does not update the note" do + expect{ delete :destroy, request_params }.not_to change{ Note.count } + end + end + end + + describe 'POST toggle_award_emoji' do + let(:note) { create(:note_on_personal_snippet, noteable: public_snippet) } + before do + sign_in(user) + end + + subject { post(:toggle_award_emoji, snippet_id: public_snippet, id: note.id, name: "thumbsup") } + + it "toggles the award emoji" do + expect { subject }.to change { note.award_emoji.count }.by(1) + + expect(response).to have_http_status(200) + end + + it "removes the already awarded emoji when it exists" do + note.toggle_award_emoji('thumbsup', user) # create award emoji before + + expect { subject }.to change { AwardEmoji.count }.by(-1) + + expect(response).to have_http_status(200) + end + end +end diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb index 234f3edd3d8..41cd5bdcdd8 100644 --- a/spec/controllers/snippets_controller_spec.rb +++ b/spec/controllers/snippets_controller_spec.rb @@ -350,144 +350,138 @@ describe SnippetsController do end end - %w(raw download).each do |action| - describe "GET #{action}" do - context 'when the personal snippet is private' do - let(:personal_snippet) { create(:personal_snippet, :private, author: user) } + describe "GET #raw" do + context 'when the personal snippet is private' do + let(:personal_snippet) { create(:personal_snippet, :private, author: user) } - context 'when signed in' do - before do - sign_in(user) - end + context 'when signed in' do + before do + sign_in(user) + end - context 'when signed in user is not the author' do - let(:other_author) { create(:author) } - let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_author) } + context 'when signed in user is not the author' do + let(:other_author) { create(:author) } + let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_author) } - it 'responds with status 404' do - get action, id: other_personal_snippet.to_param + it 'responds with status 404' do + get :raw, id: other_personal_snippet.to_param - expect(response).to have_http_status(404) - end + expect(response).to have_http_status(404) end + end - context 'when signed in user is the author' do - before { get action, id: personal_snippet.to_param } + context 'when signed in user is the author' do + before { get :raw, id: personal_snippet.to_param } - it 'responds with status 200' do - expect(assigns(:snippet)).to eq(personal_snippet) - expect(response).to have_http_status(200) - end + it 'responds with status 200' do + expect(assigns(:snippet)).to eq(personal_snippet) + expect(response).to have_http_status(200) + end - it 'has expected headers' do - expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8') + it 'has expected headers' do + expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8') - if action == :download - expect(response.header['Content-Disposition']).to match(/attachment/) - elsif action == :raw - expect(response.header['Content-Disposition']).to match(/inline/) - end - end + expect(response.header['Content-Disposition']).to match(/inline/) end end + end - context 'when not signed in' do - it 'redirects to the sign in page' do - get action, id: personal_snippet.to_param + context 'when not signed in' do + it 'redirects to the sign in page' do + get :raw, id: personal_snippet.to_param - expect(response).to redirect_to(new_user_session_path) - end + expect(response).to redirect_to(new_user_session_path) end end + end - context 'when the personal snippet is internal' do - let(:personal_snippet) { create(:personal_snippet, :internal, author: user) } + context 'when the personal snippet is internal' do + let(:personal_snippet) { create(:personal_snippet, :internal, author: user) } - context 'when signed in' do - before do - sign_in(user) - end + context 'when signed in' do + before do + sign_in(user) + end - it 'responds with status 200' do - get action, id: personal_snippet.to_param + it 'responds with status 200' do + get :raw, id: personal_snippet.to_param - expect(assigns(:snippet)).to eq(personal_snippet) - expect(response).to have_http_status(200) - end + expect(assigns(:snippet)).to eq(personal_snippet) + expect(response).to have_http_status(200) end + end - context 'when not signed in' do - it 'redirects to the sign in page' do - get action, id: personal_snippet.to_param + context 'when not signed in' do + it 'redirects to the sign in page' do + get :raw, id: personal_snippet.to_param - expect(response).to redirect_to(new_user_session_path) - end + expect(response).to redirect_to(new_user_session_path) end end + end - context 'when the personal snippet is public' do - let(:personal_snippet) { create(:personal_snippet, :public, author: user) } + context 'when the personal snippet is public' do + let(:personal_snippet) { create(:personal_snippet, :public, author: user) } - context 'when signed in' do - before do - sign_in(user) - end + context 'when signed in' do + before do + sign_in(user) + end - it 'responds with status 200' do - get action, id: personal_snippet.to_param + it 'responds with status 200' do + get :raw, id: personal_snippet.to_param - expect(assigns(:snippet)).to eq(personal_snippet) - expect(response).to have_http_status(200) - end + expect(assigns(:snippet)).to eq(personal_snippet) + expect(response).to have_http_status(200) + end - context 'CRLF line ending' do - let(:personal_snippet) do - create(:personal_snippet, :public, author: user, content: "first line\r\nsecond line\r\nthird line") - end + context 'CRLF line ending' do + let(:personal_snippet) do + create(:personal_snippet, :public, author: user, content: "first line\r\nsecond line\r\nthird line") + end - it 'returns LF line endings by default' do - get action, id: personal_snippet.to_param + it 'returns LF line endings by default' do + get :raw, id: personal_snippet.to_param - expect(response.body).to eq("first line\nsecond line\nthird line") - end + expect(response.body).to eq("first line\nsecond line\nthird line") + end - it 'does not convert line endings when parameter present' do - get action, id: personal_snippet.to_param, line_ending: :raw + it 'does not convert line endings when parameter present' do + get :raw, id: personal_snippet.to_param, line_ending: :raw - expect(response.body).to eq("first line\r\nsecond line\r\nthird line") - end + expect(response.body).to eq("first line\r\nsecond line\r\nthird line") end end + end - context 'when not signed in' do - it 'responds with status 200' do - get action, id: personal_snippet.to_param + context 'when not signed in' do + it 'responds with status 200' do + get :raw, id: personal_snippet.to_param - expect(assigns(:snippet)).to eq(personal_snippet) - expect(response).to have_http_status(200) - end + expect(assigns(:snippet)).to eq(personal_snippet) + expect(response).to have_http_status(200) end end + end - context 'when the personal snippet does not exist' do - context 'when signed in' do - before do - sign_in(user) - end + context 'when the personal snippet does not exist' do + context 'when signed in' do + before do + sign_in(user) + end - it 'responds with status 404' do - get action, id: 'doesntexist' + it 'responds with status 404' do + get :raw, id: 'doesntexist' - expect(response).to have_http_status(404) - end + expect(response).to have_http_status(404) end + end - context 'when not signed in' do - it 'responds with status 404' do - get action, id: 'doesntexist' + context 'when not signed in' do + it 'responds with status 404' do + get :raw, id: 'doesntexist' - expect(response).to have_http_status(404) - end + expect(response).to have_http_status(404) end end end diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index 93f4903119c..44c3186d813 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -5,7 +5,7 @@ include ActionDispatch::TestProcess FactoryGirl.define do factory :note do project factory: :empty_project - note "Note" + note { generate(:title) } author on_issue diff --git a/spec/factories/project_hooks.rb b/spec/factories/project_hooks.rb index 39c2a9dd1fb..0210e871a63 100644 --- a/spec/factories/project_hooks.rb +++ b/spec/factories/project_hooks.rb @@ -1,6 +1,7 @@ FactoryGirl.define do factory :project_hook do url { generate(:url) } + enable_ssl_verification false trait :token do token { SecureRandom.hex(10) } @@ -11,6 +12,7 @@ FactoryGirl.define do merge_requests_events true tag_push_events true issues_events true + confidential_issues_events true note_events true build_events true pipeline_events true diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 0db2fe04edd..3580752a805 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -32,6 +32,10 @@ FactoryGirl.define do request_access_enabled true end + trait :with_avatar do + avatar { File.open(Rails.root.join('spec/fixtures/dk.png')) } + end + trait :repository do # no-op... for now! end diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb index fb519a9bf12..c5f24d412d7 100644 --- a/spec/features/admin/admin_hooks_spec.rb +++ b/spec/features/admin/admin_hooks_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe "Admin::Hooks", feature: true do +describe 'Admin::Hooks', feature: true do before do @project = create(:project) login_as :admin @@ -8,24 +8,24 @@ describe "Admin::Hooks", feature: true do @system_hook = create(:system_hook) end - describe "GET /admin/hooks" do - it "is ok" do + describe 'GET /admin/hooks' do + it 'is ok' do visit admin_root_path - page.within ".layout-nav" do - click_on "Hooks" + page.within '.layout-nav' do + click_on 'Hooks' end expect(current_path).to eq(admin_hooks_path) end - it "has hooks list" do + it 'has hooks list' do visit admin_hooks_path expect(page).to have_content(@system_hook.url) end end - describe "New Hook" do + describe 'New Hook' do let(:url) { generate(:url) } it 'adds new hook' do @@ -40,11 +40,36 @@ describe "Admin::Hooks", feature: true do end end - describe "Test" do + describe 'Update existing hook' do + let(:new_url) { generate(:url) } + + it 'updates existing hook' do + visit admin_hooks_path + + click_link 'Edit' + fill_in 'hook_url', with: new_url + check 'Enable SSL verification' + click_button 'Save changes' + + expect(page).to have_content 'SSL Verification: enabled' + expect(current_path).to eq(admin_hooks_path) + expect(page).to have_content(new_url) + end + end + + describe 'Remove existing hook' do + it 'remove existing hook' do + visit admin_hooks_path + + expect { click_link 'Remove' }.to change(SystemHook, :count).by(-1) + end + end + + describe 'Test' do before do WebMock.stub_request(:post, @system_hook.url) visit admin_hooks_path - click_link "Test hook" + click_link 'Test hook' end it { expect(current_path).to eq(admin_hooks_path) } diff --git a/spec/features/admin/admin_requests_profiles_spec.rb b/spec/features/admin/admin_requests_profiles_spec.rb new file mode 100644 index 00000000000..e8ecb70306b --- /dev/null +++ b/spec/features/admin/admin_requests_profiles_spec.rb @@ -0,0 +1,69 @@ +require 'spec_helper' + +describe 'Admin::RequestsProfilesController', feature: true do + before do + FileUtils.mkdir_p(Gitlab::RequestProfiler::PROFILES_DIR) + login_as(:admin) + end + + after do + Gitlab::RequestProfiler.remove_all_profiles + end + + describe 'GET /admin/requests_profiles' do + it 'shows the current profile token' do + allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new) + + visit admin_requests_profiles_path + + expect(page).to have_content("X-Profile-Token: #{Gitlab::RequestProfiler.profile_token}") + end + + it 'lists all available profiles' do + time1 = 1.hour.ago + time2 = 2.hours.ago + time3 = 3.hours.ago + profile1 = "|gitlab-org|gitlab-ce_#{time1.to_i}.html" + profile2 = "|gitlab-org|gitlab-ce_#{time2.to_i}.html" + profile3 = "|gitlab-com|infrastructure_#{time3.to_i}.html" + + FileUtils.touch("#{Gitlab::RequestProfiler::PROFILES_DIR}/#{profile1}") + FileUtils.touch("#{Gitlab::RequestProfiler::PROFILES_DIR}/#{profile2}") + FileUtils.touch("#{Gitlab::RequestProfiler::PROFILES_DIR}/#{profile3}") + + visit admin_requests_profiles_path + + within('.panel', text: '/gitlab-org/gitlab-ce') do + expect(page).to have_selector("a[href='#{admin_requests_profile_path(profile1)}']", text: time1.to_s(:long)) + expect(page).to have_selector("a[href='#{admin_requests_profile_path(profile2)}']", text: time2.to_s(:long)) + end + + within('.panel', text: '/gitlab-com/infrastructure') do + expect(page).to have_selector("a[href='#{admin_requests_profile_path(profile3)}']", text: time3.to_s(:long)) + end + end + end + + describe 'GET /admin/requests_profiles/:profile' do + context 'when a profile exists' do + it 'displays the content of the profile' do + content = 'This is a request profile' + profile = "|gitlab-org|gitlab-ce_#{Time.now.to_i}.html" + + File.write("#{Gitlab::RequestProfiler::PROFILES_DIR}/#{profile}", content) + + visit admin_requests_profile_path(profile) + + expect(page).to have_content(content) + end + end + + context 'when a profile does not exist' do + it 'shows an error message' do + visit admin_requests_profile_path('|non|existent_12345.html') + + expect(page).to have_content('Profile not found') + end + end + end +end diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb index 55e10a1a89b..7a2987e815d 100644 --- a/spec/features/atom/users_spec.rb +++ b/spec/features/atom/users_spec.rb @@ -53,7 +53,7 @@ describe "User Feed", feature: true do end it 'has XHTML summaries in issue descriptions' do - expect(body).to match /we have a bug!<\/p>\n\n<hr ?\/>\n\n<p dir="auto">I guess/ + expect(body).to match /<hr ?\/>/ end it 'has XHTML summaries in notes' do diff --git a/spec/features/explore/groups_list_spec.rb b/spec/features/explore/groups_list_spec.rb index 8e5421a984b..9828cb179a7 100644 --- a/spec/features/explore/groups_list_spec.rb +++ b/spec/features/explore/groups_list_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Explore Groups page', js: true, feature: true do +describe 'Explore Groups page', :js, :feature do let!(:user) { create :user } let!(:group) { create(:group) } let!(:public_group) { create(:group, :public) } @@ -46,19 +46,39 @@ describe 'Explore Groups page', js: true, feature: true do it 'shows non-archived projects count' do # Initially project is not archived expect(find('.js-groups-list-holder .content-list li:first-child .stats span:first-child')).to have_text("1") - + # Archive project empty_project.archive! visit explore_groups_path # Check project count expect(find('.js-groups-list-holder .content-list li:first-child .stats span:first-child')).to have_text("0") - + # Unarchive project empty_project.unarchive! visit explore_groups_path # Check project count - expect(find('.js-groups-list-holder .content-list li:first-child .stats span:first-child')).to have_text("1") + expect(find('.js-groups-list-holder .content-list li:first-child .stats span:first-child')).to have_text("1") + end + + describe 'landing component' do + it 'should show a landing component' do + expect(page).to have_content('Below you will find all the groups that are public.') + end + + it 'should be dismissable' do + find('.dismiss-button').click + + expect(page).not_to have_content('Below you will find all the groups that are public.') + end + + it 'should persistently not show once dismissed' do + find('.dismiss-button').click + + visit explore_groups_path + + expect(page).not_to have_content('Below you will find all the groups that are public.') + end end end diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb index 755992069ff..21b8cf3add5 100644 --- a/spec/features/issues/form_spec.rb +++ b/spec/features/issues/form_spec.rb @@ -2,6 +2,7 @@ require 'rails_helper' describe 'New/edit issue', feature: true, js: true do include GitlabRoutingHelper + include ActionView::Helpers::JavaScriptHelper let!(:project) { create(:project) } let!(:user) { create(:user)} @@ -105,6 +106,33 @@ describe 'New/edit issue', feature: true, js: true do expect(find('.js-label-select')).to have_content('Labels') end + + it 'correctly updates the selected user when changing assignee' do + click_button 'Assignee' + page.within '.dropdown-menu-user' do + click_link user.name + end + + expect(find('input[name="issue[assignee_id]"]', visible: false).value).to match(user.id.to_s) + + click_button user.name + + expect(find('.dropdown-menu-user a.is-active').first(:xpath, '..')['data-user-id']).to eq(user.id.to_s) + + # check the ::before pseudo element to ensure checkmark icon is present + expect(before_for_selector('.dropdown-menu-selectable a.is-active')).not_to eq('') + expect(before_for_selector('.dropdown-menu-selectable a:not(.is-active)')).to eq('') + + page.within '.dropdown-menu-user' do + click_link user2.name + end + + expect(find('input[name="issue[assignee_id]"]', visible: false).value).to match(user2.id.to_s) + + click_button user2.name + + expect(find('.dropdown-menu-user a.is-active').first(:xpath, '..')['data-user-id']).to eq(user2.id.to_s) + end end context 'edit issue' do @@ -154,4 +182,14 @@ describe 'New/edit issue', feature: true, js: true do end end end + + def before_for_selector(selector) + js = <<-JS.strip_heredoc + (function(selector) { + var el = document.querySelector(selector); + return window.getComputedStyle(el, '::before').getPropertyValue('content'); + })("#{escape_javascript(selector)}") + JS + page.evaluate_script(js) + end end diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb index baacd7edb86..82b80a69bed 100644 --- a/spec/features/issues/issue_sidebar_spec.rb +++ b/spec/features/issues/issue_sidebar_spec.rb @@ -3,7 +3,8 @@ require 'rails_helper' feature 'Issue Sidebar', feature: true do include MobileHelpers - let(:project) { create(:project, :public) } + let(:group) { create(:group, :nested) } + let(:project) { create(:project, :public, namespace: group) } let(:issue) { create(:issue, project: project) } let!(:user) { create(:user)} let!(:label) { create(:label, project: project, title: 'bug') } @@ -55,10 +56,12 @@ feature 'Issue Sidebar', feature: true do # Resize the window resize_screen_sm # Make sure the sidebar is collapsed + find(sidebar_selector) expect(page).to have_css(sidebar_selector) # Once is collapsed let's open the sidebard and reload open_issue_sidebar refresh + find(sidebar_selector) expect(page).to have_css(sidebar_selector) # Restore the window size as it was including the sidebar restore_window_size @@ -149,9 +152,7 @@ feature 'Issue Sidebar', feature: true do end def open_issue_sidebar - page.within('aside.right-sidebar.right-sidebar-collapsed') do - find('.js-sidebar-toggle').click - sleep 1 - end + find('aside.right-sidebar.right-sidebar-collapsed .js-sidebar-toggle').click + find('aside.right-sidebar.right-sidebar-expanded') end end diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb index 16b09933bda..f1b3e7f158c 100644 --- a/spec/features/merge_requests/create_new_mr_spec.rb +++ b/spec/features/merge_requests/create_new_mr_spec.rb @@ -34,7 +34,7 @@ feature 'Create New Merge Request', feature: true, js: true do expect(page).to have_content('Target branch') first('.js-target-branch').click - first('.dropdown-target-branch .dropdown-content a', text: 'v1.1.0').click + find('.dropdown-target-branch .dropdown-content a', text: 'v1.1.0', match: :first).click expect(page).to have_content "b83d6e3" end diff --git a/spec/features/milestones/milestones_spec.rb b/spec/features/milestones/milestones_spec.rb index 50d7ca39045..9eec3d7f270 100644 --- a/spec/features/milestones/milestones_spec.rb +++ b/spec/features/milestones/milestones_spec.rb @@ -86,6 +86,9 @@ describe 'Milestone draggable', feature: true, js: true do visit namespace_project_milestone_path(project.namespace, project, milestone) page.find("a[href='#tab-merge-requests']").click + + wait_for_ajax + scroll_into_view('.milestone-content') drag_to(selector: '.merge_requests-sortable-list', list_to_index: 1) diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb index cc11cb7a55f..8dba2ccbafa 100644 --- a/spec/features/projects/blobs/blob_show_spec.rb +++ b/spec/features/projects/blobs/blob_show_spec.rb @@ -1,13 +1,10 @@ require 'spec_helper' feature 'File blob', :js, feature: true do - include TreeHelper - include WaitForAjax - let(:project) { create(:project, :public) } def visit_blob(path, fragment = nil) - visit namespace_project_blob_path(project.namespace, project, tree_join('master', path), anchor: fragment) + visit namespace_project_blob_path(project.namespace, project, File.join('master', path), anchor: fragment) end context 'Ruby file' do @@ -27,6 +24,9 @@ feature 'File blob', :js, feature: true do # shows an enabled copy button expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + + # shows a raw button + expect(page).to have_link('Open raw') end end end @@ -39,7 +39,7 @@ feature 'File blob', :js, feature: true do wait_for_ajax end - it 'displays the blob' do + it 'displays the blob using the rich viewer' do aggregate_failures do # hides the simple viewer expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false) @@ -53,6 +53,9 @@ feature 'File blob', :js, feature: true do # shows a disabled copy button expect(page).to have_selector('.js-copy-blob-source-btn.disabled') + + # shows a raw button + expect(page).to have_link('Open raw') end end @@ -63,7 +66,7 @@ feature 'File blob', :js, feature: true do wait_for_ajax end - it 'displays the blob' do + it 'displays the blob using the simple viewer' do aggregate_failures do # hides the rich viewer expect(page).to have_selector('.blob-viewer[data-type="simple"]') @@ -84,7 +87,7 @@ feature 'File blob', :js, feature: true do wait_for_ajax end - it 'displays the blob' do + it 'displays the blob using the rich viewer' do aggregate_failures do # hides the simple viewer expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false) @@ -105,7 +108,7 @@ feature 'File blob', :js, feature: true do wait_for_ajax end - it 'displays the blob' do + it 'displays the blob using the simple viewer' do aggregate_failures do # hides the rich viewer expect(page).to have_selector('.blob-viewer[data-type="simple"]') @@ -163,6 +166,9 @@ feature 'File blob', :js, feature: true do # does not show a copy button expect(page).not_to have_selector('.js-copy-blob-source-btn') + + # shows a raw button + expect(page).to have_link('Open raw') end end @@ -206,6 +212,9 @@ feature 'File blob', :js, feature: true do # shows an enabled copy button expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + + # shows a raw button + expect(page).to have_link('Open raw') end end end @@ -222,7 +231,7 @@ feature 'File blob', :js, feature: true do branch_name: 'master', commit_message: "Add PDF", file_path: 'files/test.pdf', - file_content: File.read(Rails.root.join('spec/javascripts/blob/pdf/test.pdf')) + file_content: project.repository.blob_at('add-pdf-file', 'files/pdf/test.pdf').data ).execute visit_blob('files/test.pdf') @@ -240,6 +249,9 @@ feature 'File blob', :js, feature: true do # does not show a copy button expect(page).not_to have_selector('.js-copy-blob-source-btn') + + # shows a download button + expect(page).to have_link('Download') end end end @@ -265,6 +277,9 @@ feature 'File blob', :js, feature: true do # does not show a copy button expect(page).not_to have_selector('.js-copy-blob-source-btn') + + # shows a download button + expect(page).to have_link('Download') end end end @@ -286,6 +301,9 @@ feature 'File blob', :js, feature: true do # shows an enabled copy button expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + + # shows a raw button + expect(page).to have_link('Open raw') end end end @@ -308,6 +326,9 @@ feature 'File blob', :js, feature: true do # does not show a copy button expect(page).not_to have_selector('.js-copy-blob-source-btn') + + # shows a download button + expect(page).to have_link('Download') end end end diff --git a/spec/features/projects/commit/cherry_pick_spec.rb b/spec/features/projects/commit/cherry_pick_spec.rb index 5d64d42fd61..fa67d390c47 100644 --- a/spec/features/projects/commit/cherry_pick_spec.rb +++ b/spec/features/projects/commit/cherry_pick_spec.rb @@ -74,8 +74,10 @@ describe 'Cherry-pick Commits' do wait_for_ajax - page.within('#modal-cherry-pick-commit .dropdown-menu .dropdown-content') do - click_link 'feature' + page.within('#modal-cherry-pick-commit .dropdown-menu') do + find('.dropdown-input input').set('feature') + wait_for_ajax + click_link "feature" end page.within('#modal-cherry-pick-commit') do diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb index acc3efe04e6..1e12f8542e2 100644 --- a/spec/features/projects/environments/environment_spec.rb +++ b/spec/features/projects/environments/environment_spec.rb @@ -200,7 +200,7 @@ feature 'Environment', :feature do end scenario 'user deletes the branch with running environment' do - visit namespace_project_branches_path(project.namespace, project) + visit namespace_project_branches_path(project.namespace, project, search: 'feature') remove_branch_with_hooks(project, user, 'feature') do page.within('.js-branch-feature') { find('a.btn-remove').click } diff --git a/spec/features/projects/merge_request_button_spec.rb b/spec/features/projects/merge_request_button_spec.rb index 05f3162f13c..1370ab1c521 100644 --- a/spec/features/projects/merge_request_button_spec.rb +++ b/spec/features/projects/merge_request_button_spec.rb @@ -85,8 +85,8 @@ feature 'Merge Request button', feature: true do context 'on branches page' do it_behaves_like 'Merge request button only shown when allowed' do let(:label) { 'Merge request' } - let(:url) { namespace_project_branches_path(project.namespace, project) } - let(:fork_url) { namespace_project_branches_path(forked_project.namespace, forked_project) } + let(:url) { namespace_project_branches_path(project.namespace, project, search: 'feature') } + let(:fork_url) { namespace_project_branches_path(forked_project.namespace, forked_project, search: 'feature') } end end diff --git a/spec/features/projects/milestones/milestone_spec.rb b/spec/features/projects/milestones/milestone_spec.rb index 5e19907eef9..b4fc0edbde8 100644 --- a/spec/features/projects/milestones/milestone_spec.rb +++ b/spec/features/projects/milestones/milestone_spec.rb @@ -78,11 +78,10 @@ feature 'Project milestone', :feature do it 'shows the total MR and issue counts' do find('.milestone-sidebar .block', match: :first) - blocks = all('.milestone-sidebar .block') aggregate_failures 'MR and issue blocks' do - expect(blocks[3]).to have_content 1 - expect(blocks[4]).to have_content 0 + expect(find('.milestone-sidebar .block.issues')).to have_content 1 + expect(find('.milestone-sidebar .block.merge-requests')).to have_content 0 end end end diff --git a/spec/features/projects/settings/integration_settings_spec.rb b/spec/features/projects/settings/integration_settings_spec.rb new file mode 100644 index 00000000000..7909234556e --- /dev/null +++ b/spec/features/projects/settings/integration_settings_spec.rb @@ -0,0 +1,94 @@ +require 'spec_helper' + +feature 'Integration settings', feature: true do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + let(:role) { :developer } + let(:integrations_path) { namespace_project_settings_integrations_path(project.namespace, project) } + + background do + login_as(user) + project.team << [user, role] + end + + context 'for developer' do + given(:role) { :developer } + + scenario 'to be disallowed to view' do + visit integrations_path + + expect(page.status_code).to eq(404) + end + end + + context 'for master' do + given(:role) { :master } + + context 'Webhooks' do + let(:hook) { create(:project_hook, :all_events_enabled, enable_ssl_verification: true, project: project) } + let(:url) { generate(:url) } + + scenario 'show list of webhooks' do + hook + + visit integrations_path + + expect(page.status_code).to eq(200) + expect(page).to have_content(hook.url) + expect(page).to have_content('SSL Verification: enabled') + expect(page).to have_content('Push Events') + expect(page).to have_content('Tag Push Events') + expect(page).to have_content('Issues Events') + expect(page).to have_content('Confidential Issues Events') + expect(page).to have_content('Note Events') + expect(page).to have_content('Merge Requests Events') + expect(page).to have_content('Pipeline Events') + expect(page).to have_content('Wiki Page Events') + end + + scenario 'create webhook' do + visit integrations_path + + fill_in 'hook_url', with: url + check 'Tag push events' + check 'Enable SSL verification' + + click_button 'Add webhook' + + expect(page).to have_content(url) + expect(page).to have_content('SSL Verification: enabled') + expect(page).to have_content('Push Events') + expect(page).to have_content('Tag Push Events') + end + + scenario 'edit existing webhook' do + hook + visit integrations_path + + click_link 'Edit' + fill_in 'hook_url', with: url + check 'Enable SSL verification' + click_button 'Save changes' + + expect(page).to have_content 'SSL Verification: enabled' + expect(page).to have_content(url) + end + + scenario 'test existing webhook' do + WebMock.stub_request(:post, hook.url) + visit integrations_path + + click_link 'Test' + + expect(current_path).to eq(integrations_path) + end + + scenario 'remove existing webhook' do + hook + visit integrations_path + + expect { click_link 'Remove' }.to change(ProjectHook, :count).by(-1) + end + end + end +end diff --git a/spec/features/projects/snippets/show_spec.rb b/spec/features/projects/snippets/show_spec.rb new file mode 100644 index 00000000000..cedf3778c7e --- /dev/null +++ b/spec/features/projects/snippets/show_spec.rb @@ -0,0 +1,144 @@ +require 'spec_helper' + +feature 'Project snippet', :js, feature: true do + let(:user) { create(:user) } + let(:project) { create(:project, :repository) } + let(:snippet) { create(:project_snippet, project: project, file_name: file_name, content: content) } + + before do + project.team << [user, :master] + login_as(user) + end + + context 'Ruby file' do + let(:file_name) { 'popen.rb' } + let(:content) { project.repository.blob_at('master', 'files/ruby/popen.rb').data } + + before do + visit namespace_project_snippet_path(project.namespace, project, snippet) + + wait_for_ajax + end + + it 'displays the blob' do + aggregate_failures do + # shows highlighted Ruby code + expect(page).to have_content("require 'fileutils'") + + # does not show a viewer switcher + expect(page).not_to have_selector('.js-blob-viewer-switcher') + + # shows an enabled copy button + expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + + # shows a raw button + expect(page).to have_link('Open raw') + + # shows a download button + expect(page).to have_link('Download') + end + end + end + + context 'Markdown file' do + let(:file_name) { 'ruby-style-guide.md' } + let(:content) { project.repository.blob_at('master', 'files/markdown/ruby-style-guide.md').data } + + context 'visiting directly' do + before do + visit namespace_project_snippet_path(project.namespace, project, snippet) + + wait_for_ajax + end + + it 'displays the blob using the rich viewer' do + aggregate_failures do + # hides the simple viewer + expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false) + expect(page).to have_selector('.blob-viewer[data-type="rich"]') + + # shows rendered Markdown + expect(page).to have_link("PEP-8") + + # shows a viewer switcher + expect(page).to have_selector('.js-blob-viewer-switcher') + + # shows a disabled copy button + expect(page).to have_selector('.js-copy-blob-source-btn.disabled') + + # shows a raw button + expect(page).to have_link('Open raw') + + # shows a download button + expect(page).to have_link('Download') + end + end + + context 'switching to the simple viewer' do + before do + find('.js-blob-viewer-switch-btn[data-viewer=simple]').click + + wait_for_ajax + end + + it 'displays the blob using the simple viewer' do + aggregate_failures do + # hides the rich viewer + expect(page).to have_selector('.blob-viewer[data-type="simple"]') + expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false) + + # shows highlighted Markdown code + expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)") + + # shows an enabled copy button + expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + end + end + + context 'switching to the rich viewer again' do + before do + find('.js-blob-viewer-switch-btn[data-viewer=rich]').click + + wait_for_ajax + end + + it 'displays the blob using the rich viewer' do + aggregate_failures do + # hides the simple viewer + expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false) + expect(page).to have_selector('.blob-viewer[data-type="rich"]') + + # shows an enabled copy button + expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + end + end + end + end + end + + context 'visiting with a line number anchor' do + before do + visit namespace_project_snippet_path(project.namespace, project, snippet, anchor: 'L1') + + wait_for_ajax + end + + it 'displays the blob using the simple viewer' do + aggregate_failures do + # hides the rich viewer + expect(page).to have_selector('.blob-viewer[data-type="simple"]') + expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false) + + # highlights the line in question + expect(page).to have_selector('#LC1.hll') + + # shows highlighted Markdown code + expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)") + + # shows an enabled copy button + expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + end + end + end + end +end diff --git a/spec/features/protected_branches/access_control_ce_spec.rb b/spec/features/protected_branches/access_control_ce_spec.rb index eb3cea775da..d30e7947106 100644 --- a/spec/features/protected_branches/access_control_ce_spec.rb +++ b/spec/features/protected_branches/access_control_ce_spec.rb @@ -9,7 +9,7 @@ RSpec.shared_examples "protected branches > access control > CE" do allowed_to_push_button = find(".js-allowed-to-push") unless allowed_to_push_button.text == access_type_name - allowed_to_push_button.click + allowed_to_push_button.trigger('click') within(".dropdown.open .dropdown-menu") { click_on access_type_name } end end diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb index acc5641f930..fc9b293c393 100644 --- a/spec/features/protected_branches_spec.rb +++ b/spec/features/protected_branches_spec.rb @@ -8,7 +8,7 @@ feature 'Projected Branches', feature: true, js: true do before { login_as(user) } def set_protected_branch_name(branch_name) - find(".js-protected-branch-select").click + find(".js-protected-branch-select").trigger('click') find(".dropdown-input-field").set(branch_name) click_on("Create wildcard #{branch_name}") end diff --git a/spec/features/protected_tags/access_control_ce_spec.rb b/spec/features/protected_tags/access_control_ce_spec.rb index 5b2baf8616c..5b24ac0292b 100644 --- a/spec/features/protected_tags/access_control_ce_spec.rb +++ b/spec/features/protected_tags/access_control_ce_spec.rb @@ -10,6 +10,7 @@ RSpec.shared_examples "protected tags > access control > CE" do unless allowed_to_create_button.text == access_type_name allowed_to_create_button.click + find('.dropdown.open .dropdown-menu li', match: :first) within(".dropdown.open .dropdown-menu") { click_on access_type_name } end end diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb index a1a36931824..26879a77c48 100644 --- a/spec/features/security/project/internal_access_spec.rb +++ b/spec/features/security/project/internal_access_spec.rb @@ -466,6 +466,21 @@ describe "Internal Project Access", feature: true do it { is_expected.to be_denied_for(:visitor) } end + describe "GET /:project_path/environments/:id/deployments" do + let(:environment) { create(:environment, project: project) } + subject { namespace_project_environment_deployments_path(project.namespace, project, environment) } + + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } + end + describe "GET /:project_path/environments/new" do subject { new_namespace_project_environment_path(project.namespace, project) } diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb index 5d58494a22a..699ca4f724c 100644 --- a/spec/features/security/project/private_access_spec.rb +++ b/spec/features/security/project/private_access_spec.rb @@ -449,6 +449,21 @@ describe "Private Project Access", feature: true do it { is_expected.to be_denied_for(:visitor) } end + describe "GET /:project_path/environments/:id/deployments" do + let(:environment) { create(:environment, project: project) } + subject { namespace_project_environment_deployments_path(project.namespace, project, environment) } + + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } + end + describe "GET /:project_path/environments/new" do subject { new_namespace_project_environment_path(project.namespace, project) } diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb index 5df5b710dc4..624f0d0f485 100644 --- a/spec/features/security/project/public_access_spec.rb +++ b/spec/features/security/project/public_access_spec.rb @@ -286,6 +286,21 @@ describe "Public Project Access", feature: true do it { is_expected.to be_denied_for(:visitor) } end + describe "GET /:project_path/environments/:id/deployments" do + let(:environment) { create(:environment, project: project) } + subject { namespace_project_environment_deployments_path(project.namespace, project, environment) } + + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } + end + describe "GET /:project_path/environments/new" do subject { new_namespace_project_environment_path(project.namespace, project) } diff --git a/spec/features/snippets/create_snippet_spec.rb b/spec/features/snippets/create_snippet_spec.rb index 5470276bf06..9409c323288 100644 --- a/spec/features/snippets/create_snippet_spec.rb +++ b/spec/features/snippets/create_snippet_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -feature 'Create Snippet', feature: true do +feature 'Create Snippet', :js, feature: true do before do login_as :user visit new_snippet_path @@ -9,10 +9,11 @@ feature 'Create Snippet', feature: true do scenario 'Authenticated user creates a snippet' do fill_in 'personal_snippet_title', with: 'My Snippet Title' page.within('.file-editor') do - find(:xpath, "//input[@id='personal_snippet_content']").set 'Hello World!' + find('.ace_editor').native.send_keys 'Hello World!' end click_button 'Create snippet' + wait_for_ajax expect(page).to have_content('My Snippet Title') expect(page).to have_content('Hello World!') @@ -22,10 +23,11 @@ feature 'Create Snippet', feature: true do fill_in 'personal_snippet_title', with: 'My Snippet Title' page.within('.file-editor') do find(:xpath, "//input[@id='personal_snippet_file_name']").set 'snippet+file+name' - find(:xpath, "//input[@id='personal_snippet_content']").set 'Hello World!' + find('.ace_editor').native.send_keys 'Hello World!' end click_button 'Create snippet' + wait_for_ajax expect(page).to have_content('My Snippet Title') expect(page).to have_content('snippet+file+name') diff --git a/spec/features/snippets/notes_on_personal_snippets_spec.rb b/spec/features/snippets/notes_on_personal_snippets_spec.rb new file mode 100644 index 00000000000..c646039e0b1 --- /dev/null +++ b/spec/features/snippets/notes_on_personal_snippets_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe 'Comments on personal snippets', feature: true do + let!(:user) { create(:user) } + let!(:snippet) { create(:personal_snippet, :public) } + let!(:snippet_notes) do + [ + create(:note_on_personal_snippet, noteable: snippet, author: user), + create(:note_on_personal_snippet, noteable: snippet) + ] + end + let!(:other_note) { create(:note_on_personal_snippet) } + + before do + login_as user + visit snippet_path(snippet) + end + + subject { page } + + context 'viewing the snippet detail page' do + it 'contains notes for a snippet with correct action icons' do + expect(page).to have_selector('#notes-list li', count: 2) + + # comment authored by current user + page.within("#notes-list li#note_#{snippet_notes[0].id}") do + expect(page).to have_content(snippet_notes[0].note) + expect(page).to have_selector('.js-note-delete') + expect(page).to have_selector('.note-emoji-button') + end + + page.within("#notes-list li#note_#{snippet_notes[1].id}") do + expect(page).to have_content(snippet_notes[1].note) + expect(page).not_to have_selector('.js-note-delete') + expect(page).to have_selector('.note-emoji-button') + end + end + end +end diff --git a/spec/features/snippets/public_snippets_spec.rb b/spec/features/snippets/public_snippets_spec.rb index 34300ccb940..2df483818c3 100644 --- a/spec/features/snippets/public_snippets_spec.rb +++ b/spec/features/snippets/public_snippets_spec.rb @@ -1,10 +1,11 @@ require 'rails_helper' -feature 'Public Snippets', feature: true do +feature 'Public Snippets', :js, feature: true do scenario 'Unauthenticated user should see public snippets' do public_snippet = create(:personal_snippet, :public) visit snippet_path(public_snippet) + wait_for_ajax expect(page).to have_content(public_snippet.content) end diff --git a/spec/features/snippets/show_spec.rb b/spec/features/snippets/show_spec.rb new file mode 100644 index 00000000000..e36cf547f80 --- /dev/null +++ b/spec/features/snippets/show_spec.rb @@ -0,0 +1,138 @@ +require 'spec_helper' + +feature 'Snippet', :js, feature: true do + let(:project) { create(:project, :repository) } + let(:snippet) { create(:personal_snippet, :public, file_name: file_name, content: content) } + + context 'Ruby file' do + let(:file_name) { 'popen.rb' } + let(:content) { project.repository.blob_at('master', 'files/ruby/popen.rb').data } + + before do + visit snippet_path(snippet) + + wait_for_ajax + end + + it 'displays the blob' do + aggregate_failures do + # shows highlighted Ruby code + expect(page).to have_content("require 'fileutils'") + + # does not show a viewer switcher + expect(page).not_to have_selector('.js-blob-viewer-switcher') + + # shows an enabled copy button + expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + + # shows a raw button + expect(page).to have_link('Open raw') + + # shows a download button + expect(page).to have_link('Download') + end + end + end + + context 'Markdown file' do + let(:file_name) { 'ruby-style-guide.md' } + let(:content) { project.repository.blob_at('master', 'files/markdown/ruby-style-guide.md').data } + + context 'visiting directly' do + before do + visit snippet_path(snippet) + + wait_for_ajax + end + + it 'displays the blob using the rich viewer' do + aggregate_failures do + # hides the simple viewer + expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false) + expect(page).to have_selector('.blob-viewer[data-type="rich"]') + + # shows rendered Markdown + expect(page).to have_link("PEP-8") + + # shows a viewer switcher + expect(page).to have_selector('.js-blob-viewer-switcher') + + # shows a disabled copy button + expect(page).to have_selector('.js-copy-blob-source-btn.disabled') + + # shows a raw button + expect(page).to have_link('Open raw') + + # shows a download button + expect(page).to have_link('Download') + end + end + + context 'switching to the simple viewer' do + before do + find('.js-blob-viewer-switch-btn[data-viewer=simple]').click + + wait_for_ajax + end + + it 'displays the blob using the simple viewer' do + aggregate_failures do + # hides the rich viewer + expect(page).to have_selector('.blob-viewer[data-type="simple"]') + expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false) + + # shows highlighted Markdown code + expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)") + + # shows an enabled copy button + expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + end + end + + context 'switching to the rich viewer again' do + before do + find('.js-blob-viewer-switch-btn[data-viewer=rich]').click + + wait_for_ajax + end + + it 'displays the blob using the rich viewer' do + aggregate_failures do + # hides the simple viewer + expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false) + expect(page).to have_selector('.blob-viewer[data-type="rich"]') + + # shows an enabled copy button + expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + end + end + end + end + end + + context 'visiting with a line number anchor' do + before do + visit snippet_path(snippet, anchor: 'L1') + + wait_for_ajax + end + + it 'displays the blob using the simple viewer' do + aggregate_failures do + # hides the rich viewer + expect(page).to have_selector('.blob-viewer[data-type="simple"]') + expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false) + + # highlights the line in question + expect(page).to have_selector('#LC1.hll') + + # shows highlighted Markdown code + expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)") + + # shows an enabled copy button + expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + end + end + end + end +end diff --git a/spec/features/todos/todos_filtering_spec.rb b/spec/features/todos/todos_filtering_spec.rb index cecb98641a6..f32e70c2c3f 100644 --- a/spec/features/todos/todos_filtering_spec.rb +++ b/spec/features/todos/todos_filtering_spec.rb @@ -45,8 +45,8 @@ describe 'Dashboard > User filters todos', feature: true, js: true do wait_for_ajax - expect(find('.todos-list')).to have_content user_1.name - expect(find('.todos-list')).not_to have_content user_2.name + expect(find('.todos-list')).to have_content 'merge request' + expect(find('.todos-list')).not_to have_content 'issue' end it "shows only authors of existing todos" do diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb index 50c207fb9cb..be5b3af417f 100644 --- a/spec/features/todos/todos_spec.rb +++ b/spec/features/todos/todos_spec.rb @@ -99,6 +99,83 @@ describe 'Dashboard Todos', feature: true do end end + context 'User created todos for themself' do + before do + login_as(user) + end + + context 'issue assigned todo' do + before do + create(:todo, :assigned, user: user, project: project, target: issue, author: user) + visit dashboard_todos_path + end + + it 'shows issue assigned to yourself message' do + page.within('.js-todos-all') do + expect(page).to have_content("You assigned issue #{issue.to_reference(full: true)} to yourself") + end + end + end + + context 'marked todo' do + before do + create(:todo, :marked, user: user, project: project, target: issue, author: user) + visit dashboard_todos_path + end + + it 'shows you added a todo message' do + page.within('.js-todos-all') do + expect(page).to have_content("You added a todo for issue #{issue.to_reference(full: true)}") + expect(page).not_to have_content('to yourself') + end + end + end + + context 'mentioned todo' do + before do + create(:todo, :mentioned, user: user, project: project, target: issue, author: user) + visit dashboard_todos_path + end + + it 'shows you mentioned yourself message' do + page.within('.js-todos-all') do + expect(page).to have_content("You mentioned yourself on issue #{issue.to_reference(full: true)}") + expect(page).not_to have_content('to yourself') + end + end + end + + context 'directly_addressed todo' do + before do + create(:todo, :directly_addressed, user: user, project: project, target: issue, author: user) + visit dashboard_todos_path + end + + it 'shows you directly addressed yourself message' do + page.within('.js-todos-all') do + expect(page).to have_content("You directly addressed yourself on issue #{issue.to_reference(full: true)}") + expect(page).not_to have_content('to yourself') + end + end + end + + context 'approval todo' do + let(:merge_request) { create(:merge_request) } + + before do + create(:todo, :approval_required, user: user, project: project, target: merge_request, author: user) + visit dashboard_todos_path + end + + it 'shows you set yourself as an approver message' do + page.within('.js-todos-all') do + expect(page).to have_content("You set yourself as an approver for merge request #{merge_request.to_reference(full: true)}") + expect(page).not_to have_content('to yourself') + end + end + end + end + context 'User has done todos', js: true do before do create(:todo, :mentioned, :done, user: user, project: project, target: issue, author: author) diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb index 81fa2de1cc3..783f330221c 100644 --- a/spec/features/triggers_spec.rb +++ b/spec/features/triggers_spec.rb @@ -104,6 +104,24 @@ feature 'Triggers', feature: true, js: true do expect(page).to have_content 'The form contains the following errors' end + + context 'when GitLab time_zone is ActiveSupport::TimeZone format' do + before do + allow(Time).to receive(:zone) + .and_return(ActiveSupport::TimeZone['Eastern Time (US & Canada)']) + end + + scenario 'do fill form with valid data and save' do + find('#trigger_trigger_schedule_attributes_active').click + fill_in 'trigger_trigger_schedule_attributes_cron', with: '1 * * * *' + fill_in 'trigger_trigger_schedule_attributes_cron_timezone', with: 'UTC' + fill_in 'trigger_trigger_schedule_attributes_ref', with: 'master' + click_button 'Save trigger' + + expect(page.find('.flash-notice')) + .to have_content 'Trigger was successfully updated.' + end + end end context 'disabling schedule' do diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index a1ae1d746af..a5f717e6233 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -16,7 +16,7 @@ describe IssuesFinder do set(:label_link) { create(:label_link, label: label, target: issue2) } let(:search_user) { user } let(:params) { {} } - let(:issues) { IssuesFinder.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute } + let(:issues) { described_class.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute } before(:context) do project1.team << [user, :master] @@ -282,15 +282,15 @@ describe IssuesFinder do let!(:confidential_issue) { create(:issue, project: project, confidential: true) } it 'returns non confidential issues for nil user' do - expect(IssuesFinder.send(:not_restricted_by_confidentiality, nil)).to include(public_issue) + expect(described_class.send(:not_restricted_by_confidentiality, nil)).to include(public_issue) end it 'returns non confidential issues for user not authorized for the issues projects' do - expect(IssuesFinder.send(:not_restricted_by_confidentiality, user)).to include(public_issue) + expect(described_class.send(:not_restricted_by_confidentiality, user)).to include(public_issue) end it 'returns all issues for user authorized for the issues projects' do - expect(IssuesFinder.send(:not_restricted_by_confidentiality, authorized_user)).to include(public_issue, confidential_issue) + expect(described_class.send(:not_restricted_by_confidentiality, authorized_user)).to include(public_issue, confidential_issue) end end end diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index 21ef94ac5d1..58b7cd5e098 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -23,26 +23,26 @@ describe MergeRequestsFinder do describe "#execute" do it 'filters by scope' do params = { scope: 'authored', state: 'opened' } - merge_requests = MergeRequestsFinder.new(user, params).execute + merge_requests = described_class.new(user, params).execute expect(merge_requests.size).to eq(3) end it 'filters by project' do params = { project_id: project1.id, scope: 'authored', state: 'opened' } - merge_requests = MergeRequestsFinder.new(user, params).execute + merge_requests = described_class.new(user, params).execute expect(merge_requests.size).to eq(1) end it 'filters by non_archived' do params = { non_archived: true } - merge_requests = MergeRequestsFinder.new(user, params).execute + merge_requests = described_class.new(user, params).execute expect(merge_requests.size).to eq(3) end it 'filters by iid' do params = { project_id: project1.id, iids: merge_request1.iid } - merge_requests = MergeRequestsFinder.new(user, params).execute + merge_requests = described_class.new(user, params).execute expect(merge_requests).to contain_exactly(merge_request1) end diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb index 765bf44d863..ba6bbb3bce0 100644 --- a/spec/finders/notes_finder_spec.rb +++ b/spec/finders/notes_finder_spec.rb @@ -110,6 +110,15 @@ describe NotesFinder do expect(notes.count).to eq(1) end + it 'finds notes on personal snippets' do + note = create(:note_on_personal_snippet) + params = { target_type: 'personal_snippet', target_id: note.noteable_id } + + notes = described_class.new(project, user, params).execute + + expect(notes.count).to eq(1) + end + it 'raises an exception for an invalid target_type' do params[:target_type] = 'invalid' expect { described_class.new(project, user, params).execute }.to raise_error('invalid target_type') diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb index 975e99c5807..cb6c80d1bd0 100644 --- a/spec/finders/snippets_finder_spec.rb +++ b/spec/finders/snippets_finder_spec.rb @@ -14,13 +14,13 @@ describe SnippetsFinder do let!(:snippet3) { create(:personal_snippet, :public) } it "returns all private and internal snippets" do - snippets = SnippetsFinder.new.execute(user, filter: :all) + snippets = described_class.new.execute(user, filter: :all) expect(snippets).to include(snippet2, snippet3) expect(snippets).not_to include(snippet1) end it "returns all public snippets" do - snippets = SnippetsFinder.new.execute(nil, filter: :all) + snippets = described_class.new.execute(nil, filter: :all) expect(snippets).to include(snippet3) expect(snippets).not_to include(snippet1, snippet2) end @@ -32,7 +32,7 @@ describe SnippetsFinder do let!(:snippet3) { create(:personal_snippet, :public) } it "returns public public snippets" do - snippets = SnippetsFinder.new.execute(nil, filter: :public) + snippets = described_class.new.execute(nil, filter: :public) expect(snippets).to include(snippet3) expect(snippets).not_to include(snippet1, snippet2) @@ -45,36 +45,36 @@ describe SnippetsFinder do let!(:snippet3) { create(:personal_snippet, :public, author: user) } it "returns all public and internal snippets" do - snippets = SnippetsFinder.new.execute(user1, filter: :by_user, user: user) + snippets = described_class.new.execute(user1, filter: :by_user, user: user) expect(snippets).to include(snippet2, snippet3) expect(snippets).not_to include(snippet1) end it "returns internal snippets" do - snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_internal") + snippets = described_class.new.execute(user, filter: :by_user, user: user, scope: "are_internal") expect(snippets).to include(snippet2) expect(snippets).not_to include(snippet1, snippet3) end it "returns private snippets" do - snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_private") + snippets = described_class.new.execute(user, filter: :by_user, user: user, scope: "are_private") expect(snippets).to include(snippet1) expect(snippets).not_to include(snippet2, snippet3) end it "returns public snippets" do - snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_public") + snippets = described_class.new.execute(user, filter: :by_user, user: user, scope: "are_public") expect(snippets).to include(snippet3) expect(snippets).not_to include(snippet1, snippet2) end it "returns all snippets" do - snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user) + snippets = described_class.new.execute(user, filter: :by_user, user: user) expect(snippets).to include(snippet1, snippet2, snippet3) end it "returns only public snippets if unauthenticated user" do - snippets = SnippetsFinder.new.execute(nil, filter: :by_user, user: user) + snippets = described_class.new.execute(nil, filter: :by_user, user: user) expect(snippets).to include(snippet3) expect(snippets).not_to include(snippet2, snippet1) end @@ -88,43 +88,43 @@ describe SnippetsFinder do end it "returns public snippets for unauthorized user" do - snippets = SnippetsFinder.new.execute(nil, filter: :by_project, project: project1) + snippets = described_class.new.execute(nil, filter: :by_project, project: project1) expect(snippets).to include(@snippet3) expect(snippets).not_to include(@snippet1, @snippet2) end it "returns public and internal snippets for non project members" do - snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1) + snippets = described_class.new.execute(user, filter: :by_project, project: project1) expect(snippets).to include(@snippet2, @snippet3) expect(snippets).not_to include(@snippet1) end it "returns public snippets for non project members" do - snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_public") + snippets = described_class.new.execute(user, filter: :by_project, project: project1, scope: "are_public") expect(snippets).to include(@snippet3) expect(snippets).not_to include(@snippet1, @snippet2) end it "returns internal snippets for non project members" do - snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_internal") + snippets = described_class.new.execute(user, filter: :by_project, project: project1, scope: "are_internal") expect(snippets).to include(@snippet2) expect(snippets).not_to include(@snippet1, @snippet3) end it "does not return private snippets for non project members" do - snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_private") + snippets = described_class.new.execute(user, filter: :by_project, project: project1, scope: "are_private") expect(snippets).not_to include(@snippet1, @snippet2, @snippet3) end it "returns all snippets for project members" do project1.team << [user, :developer] - snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1) + snippets = described_class.new.execute(user, filter: :by_project, project: project1) expect(snippets).to include(@snippet1, @snippet2, @snippet3) end it "returns private snippets for project members" do project1.team << [user, :developer] - snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_private") + snippets = described_class.new.execute(user, filter: :by_project, project: project1, scope: "are_private") expect(snippets).to include(@snippet1) end end diff --git a/spec/fixtures/api/schemas/deployments.json b/spec/fixtures/api/schemas/deployments.json new file mode 100644 index 00000000000..1112f23aab2 --- /dev/null +++ b/spec/fixtures/api/schemas/deployments.json @@ -0,0 +1,58 @@ +{ + "additionalProperties": false, + "properties": { + "deployments": { + "items": { + "additionalProperties": false, + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "iid": { + "type": "integer" + }, + "last?": { + "type": "boolean" + }, + "ref": { + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "sha": { + "type": "string" + }, + "tag": { + "type": "boolean" + } + }, + "required": [ + "sha", + "created_at", + "iid", + "tag", + "last?", + "ref", + "id" + ], + "type": "object" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "deployments" + ], + "type": "object" +} diff --git a/spec/helpers/award_emoji_helper_spec.rb b/spec/helpers/award_emoji_helper_spec.rb new file mode 100644 index 00000000000..7dfd6a3f6b4 --- /dev/null +++ b/spec/helpers/award_emoji_helper_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe AwardEmojiHelper do + describe '.toggle_award_url' do + context 'note on personal snippet' do + let(:note) { create(:note_on_personal_snippet) } + + it 'returns correct url' do + expected_url = "/snippets/#{note.noteable.id}/notes/#{note.id}/toggle_award_emoji" + + expect(helper.toggle_award_url(note)).to eq(expected_url) + end + end + + context 'note on project item' do + let(:note) { create(:note_on_project_snippet) } + + it 'returns correct url' do + @project = note.noteable.project + + expected_url = "/#{@project.namespace.path}/#{@project.path}/notes/#{note.id}/toggle_award_emoji" + + expect(helper.toggle_award_url(note)).to eq(expected_url) + end + end + + context 'personal snippet' do + let(:snippet) { create(:personal_snippet) } + + it 'returns correct url' do + expected_url = "/snippets/#{snippet.id}/toggle_award_emoji" + + expect(helper.toggle_award_url(snippet)).to eq(expected_url) + end + end + + context 'merge request' do + let(:merge_request) { create(:merge_request) } + + it 'returns correct url' do + @project = merge_request.project + + expected_url = "/#{@project.namespace.path}/#{@project.path}/merge_requests/#{merge_request.id}/toggle_award_emoji" + + expect(helper.toggle_award_url(merge_request)).to eq(expected_url) + end + end + + context 'issue' do + let(:issue) { create(:issue) } + + it 'returns correct url' do + @project = issue.project + + expected_url = "/#{@project.namespace.path}/#{@project.path}/issues/#{issue.id}/toggle_award_emoji" + + expect(helper.toggle_award_url(issue)).to eq(expected_url) + end + end + end +end diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb index 379f62f73e1..075f1887d91 100644 --- a/spec/helpers/blob_helper_spec.rb +++ b/spec/helpers/blob_helper_spec.rb @@ -157,6 +157,7 @@ describe BlobHelper do describe '#blob_render_error_options' do before do assign(:project, project) + assign(:blob, blob) assign(:id, File.join('master', blob.path)) controller.params[:controller] = 'projects/blob' diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb index a7c3c281083..c3bd0cb3542 100644 --- a/spec/helpers/events_helper_spec.rb +++ b/spec/helpers/events_helper_spec.rb @@ -56,7 +56,7 @@ describe EventsHelper do it 'preserves code color scheme' do input = "```ruby\ndef test\n 'hello world'\nend\n```" - expected = '<pre class="code highlight js-syntax-highlight ruby">' \ + expected = "\n<pre class=\"code highlight js-syntax-highlight ruby\">" \ "<code><span class=\"line\"><span class=\"k\">def</span> <span class=\"nf\">test</span>...</span>\n" \ "</code></pre>" expect(helper.event_note(input)).to eq(expected) diff --git a/spec/helpers/markup_helper_spec.rb b/spec/helpers/markup_helper_spec.rb index c10f4b09b5b..2a0de0b0656 100644 --- a/spec/helpers/markup_helper_spec.rb +++ b/spec/helpers/markup_helper_spec.rb @@ -111,7 +111,7 @@ describe MarkupHelper do it 'replaces commit message with emoji to link' do actual = link_to_gfm(':book: Book', '/foo') expect(actual). - to eq '<gl-emoji data-name="book" data-unicode-version="6.0">📖</gl-emoji><a href="/foo"> Book</a>' + to eq '<gl-emoji title="open book" data-name="book" data-unicode-version="6.0">📖</gl-emoji><a href="/foo"> Book</a>' end end diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb index e9037749ef2..10681af5f7e 100644 --- a/spec/helpers/merge_requests_helper_spec.rb +++ b/spec/helpers/merge_requests_helper_spec.rb @@ -64,7 +64,7 @@ describe MergeRequestsHelper do it do @project = project - + is_expected.to eq("#1, #2, and #{other_project.namespace.path}/#{other_project.path}#3") end end @@ -149,6 +149,50 @@ describe MergeRequestsHelper do end end + describe '#target_projects' do + let(:project) { create(:empty_project) } + let(:fork_project) { create(:empty_project, forked_from_project: project) } + + context 'when target project has enabled merge requests' do + it 'returns the forked_from project' do + expect(target_projects(fork_project)).to contain_exactly(project, fork_project) + end + end + + context 'when target project has disabled merge requests' do + it 'returns the forked project' do + project.project_feature.update(merge_requests_access_level: 0) + + expect(target_projects(fork_project)).to contain_exactly(fork_project) + end + end + end + + describe '#new_mr_path_from_push_event' do + subject(:url_params) { URI.decode_www_form(new_mr_path_from_push_event(event)).to_h } + let(:user) { create(:user) } + let(:project) { create(:empty_project, creator: user) } + let(:fork_project) { create(:project, forked_from_project: project, creator: user) } + let(:event) do + push_data = Gitlab::DataBuilder::Push.build_sample(fork_project, user) + create(:event, :pushed, project: fork_project, target: fork_project, author: user, data: push_data) + end + + context 'when target project has enabled merge requests' do + it 'returns link to create merge request on source project' do + expect(url_params['merge_request[target_project_id]'].to_i).to eq(project.id) + end + end + + context 'when target project has disabled merge requests' do + it 'returns link to create merge request on forked project' do + project.project_feature.update(merge_requests_access_level: 0) + + expect(url_params['merge_request[target_project_id]'].to_i).to eq(fork_project.id) + end + end + end + describe '#mr_issues_mentioned_but_not_closing' do let(:user_1) { create(:user) } let(:user_2) { create(:user) } diff --git a/spec/javascripts/blob/pdf/index_spec.js b/spec/javascripts/blob/pdf/index_spec.js index d3a4d04345b..bbeaf95e68d 100644 --- a/spec/javascripts/blob/pdf/index_spec.js +++ b/spec/javascripts/blob/pdf/index_spec.js @@ -1,5 +1,7 @@ +/* eslint-disable import/no-unresolved */ + import renderPDF from '~/blob/pdf'; -import testPDF from './test.pdf'; +import testPDF from '../../fixtures/blob/pdf/test.pdf'; describe('PDF renderer', () => { let viewer; @@ -59,7 +61,7 @@ describe('PDF renderer', () => { describe('error getting file', () => { beforeEach((done) => { - viewer.dataset.endpoint = 'invalid/endpoint'; + viewer.dataset.endpoint = 'invalid/path/to/file.pdf'; app = renderPDF(); checkLoaded(done); diff --git a/spec/javascripts/blob/pdf/test.pdf b/spec/javascripts/blob/pdf/test.pdf Binary files differdeleted file mode 100644 index eb3d147fde3..00000000000 --- a/spec/javascripts/blob/pdf/test.pdf +++ /dev/null diff --git a/spec/javascripts/fixtures/environments.rb b/spec/javascripts/fixtures/environments.rb new file mode 100644 index 00000000000..3474f4696ef --- /dev/null +++ b/spec/javascripts/fixtures/environments.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe Projects::EnvironmentsController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project_empty_repo, namespace: namespace, path: 'environments-project') } + let(:environment) { create(:environment, name: 'production', project: project) } + + render_views + + before(:all) do + clean_frontend_fixtures('environments/metrics') + end + + before(:each) do + sign_in(admin) + end + + it 'environments/metrics/metrics.html.raw' do |example| + get :metrics, + namespace_id: project.namespace, + project_id: project, + id: environment.id + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end +end diff --git a/spec/javascripts/fixtures/environments/metrics.html.haml b/spec/javascripts/fixtures/environments/metrics.html.haml deleted file mode 100644 index e2dd9519898..00000000000 --- a/spec/javascripts/fixtures/environments/metrics.html.haml +++ /dev/null @@ -1,62 +0,0 @@ -.prometheus-container{ 'data-has-metrics': "false", 'data-doc-link': '/help/administration/monitoring/prometheus/index.md', 'data-prometheus-integration': '/root/hello-prometheus/services/prometheus/edit' } - .top-area - .row - .col-sm-6 - %h3.page-title - Metrics for environment - .prometheus-state - .js-getting-started.hidden - .row - .col-md-4.col-md-offset-4.state-svg - %svg - .row - .col-md-6.col-md-offset-3 - %h4.text-center.state-title - Get started with performance monitoring - .row - .col-md-6.col-md-offset-3 - .description-text.text-center.state-description - Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments. Learn more about performance monitoring - .row.state-button-section - .col-md-4.col-md-offset-4.text-center.state-button - %a.btn.btn-success - Configure Prometheus - .js-loading.hidden - .row - .col-md-4.col-md-offset-4.state-svg - %svg - .row - .col-md-6.col-md-offset-3 - %h4.text-center.state-title - Waiting for performance data - .row - .col-md-6.col-md-offset-3 - .description-text.text-center.state-description - Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available. - .row.state-button-section - .col-md-4.col-md-offset-4.text-center.state-button - %a.btn.btn-success - View documentation - .js-unable-to-connect.hidden - .row - .col-md-4.col-md-offset-4.state-svg - %svg - .row - .col-md-6.col-md-offset-3 - %h4.text-center.state-title - Unable to connect to Prometheus server - .row - .col-md-6.col-md-offset-3 - .description-text.text-center.state-description - Ensure connectivity is available from the GitLab server to the Prometheus server - .row.state-button-section - .col-md-4.col-md-offset-4.text-center.state-button - %a.btn.btn-success - View documentation - .prometheus-graphs - .row - .col-sm-12 - %svg.prometheus-graph{ 'graph-type' => 'cpu_values' } - .row - .col-sm-12 - %svg.prometheus-graph{ 'graph-type' => 'memory_values' } diff --git a/spec/javascripts/fixtures/line_highlighter.html.haml b/spec/javascripts/fixtures/line_highlighter.html.haml index 514877340e4..2782c50e298 100644 --- a/spec/javascripts/fixtures/line_highlighter.html.haml +++ b/spec/javascripts/fixtures/line_highlighter.html.haml @@ -1,4 +1,4 @@ -#blob-content-holder +.file-holder .file-content .line-numbers - 1.upto(25) do |i| diff --git a/spec/javascripts/fixtures/pdf.rb b/spec/javascripts/fixtures/pdf.rb new file mode 100644 index 00000000000..6b2422a7986 --- /dev/null +++ b/spec/javascripts/fixtures/pdf.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe 'PDF file', '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project, namespace: namespace, path: 'pdf-project') } + + before(:all) do + clean_frontend_fixtures('blob/pdf/') + end + + it 'blob/pdf/test.pdf' do |example| + blob = project.repository.blob_at('e774ebd33', 'files/pdf/test.pdf') + + store_frontend_fixture(blob.data.force_encoding("utf-8"), example.description) + end +end diff --git a/spec/javascripts/landing_spec.js b/spec/javascripts/landing_spec.js new file mode 100644 index 00000000000..7916073190a --- /dev/null +++ b/spec/javascripts/landing_spec.js @@ -0,0 +1,160 @@ +import Landing from '~/landing'; +import Cookies from 'js-cookie'; + +describe('Landing', function () { + describe('class constructor', function () { + beforeEach(function () { + this.landingElement = {}; + this.dismissButton = {}; + this.cookieName = 'cookie_name'; + + this.landing = new Landing(this.landingElement, this.dismissButton, this.cookieName); + }); + + it('should set .landing', function () { + expect(this.landing.landingElement).toBe(this.landingElement); + }); + + it('should set .cookieName', function () { + expect(this.landing.cookieName).toBe(this.cookieName); + }); + + it('should set .dismissButton', function () { + expect(this.landing.dismissButton).toBe(this.dismissButton); + }); + + it('should set .eventWrapper', function () { + expect(this.landing.eventWrapper).toEqual({}); + }); + }); + + describe('toggle', function () { + beforeEach(function () { + this.isDismissed = false; + this.landingElement = { classList: jasmine.createSpyObj('classList', ['toggle']) }; + this.landing = { + isDismissed: () => {}, + addEvents: () => {}, + landingElement: this.landingElement, + }; + + spyOn(this.landing, 'isDismissed').and.returnValue(this.isDismissed); + spyOn(this.landing, 'addEvents'); + + Landing.prototype.toggle.call(this.landing); + }); + + it('should call .isDismissed', function () { + expect(this.landing.isDismissed).toHaveBeenCalled(); + }); + + it('should call .classList.toggle', function () { + expect(this.landingElement.classList.toggle).toHaveBeenCalledWith('hidden', this.isDismissed); + }); + + it('should call .addEvents', function () { + expect(this.landing.addEvents).toHaveBeenCalled(); + }); + + describe('if isDismissed is true', function () { + beforeEach(function () { + this.isDismissed = true; + this.landingElement = { classList: jasmine.createSpyObj('classList', ['toggle']) }; + this.landing = { + isDismissed: () => {}, + addEvents: () => {}, + landingElement: this.landingElement, + }; + + spyOn(this.landing, 'isDismissed').and.returnValue(this.isDismissed); + spyOn(this.landing, 'addEvents'); + + this.landing.isDismissed.calls.reset(); + + Landing.prototype.toggle.call(this.landing); + }); + + it('should not call .addEvents', function () { + expect(this.landing.addEvents).not.toHaveBeenCalled(); + }); + }); + }); + + describe('addEvents', function () { + beforeEach(function () { + this.dismissButton = jasmine.createSpyObj('dismissButton', ['addEventListener']); + this.eventWrapper = {}; + this.landing = { + eventWrapper: this.eventWrapper, + dismissButton: this.dismissButton, + dismissLanding: () => {}, + }; + + Landing.prototype.addEvents.call(this.landing); + }); + + it('should set .eventWrapper.dismissLanding', function () { + expect(this.eventWrapper.dismissLanding).toEqual(jasmine.any(Function)); + }); + + it('should call .addEventListener', function () { + expect(this.dismissButton.addEventListener).toHaveBeenCalledWith('click', this.eventWrapper.dismissLanding); + }); + }); + + describe('removeEvents', function () { + beforeEach(function () { + this.dismissButton = jasmine.createSpyObj('dismissButton', ['removeEventListener']); + this.eventWrapper = { dismissLanding: () => {} }; + this.landing = { + eventWrapper: this.eventWrapper, + dismissButton: this.dismissButton, + }; + + Landing.prototype.removeEvents.call(this.landing); + }); + + it('should call .removeEventListener', function () { + expect(this.dismissButton.removeEventListener).toHaveBeenCalledWith('click', this.eventWrapper.dismissLanding); + }); + }); + + describe('dismissLanding', function () { + beforeEach(function () { + this.landingElement = { classList: jasmine.createSpyObj('classList', ['add']) }; + this.cookieName = 'cookie_name'; + this.landing = { landingElement: this.landingElement, cookieName: this.cookieName }; + + spyOn(Cookies, 'set'); + + Landing.prototype.dismissLanding.call(this.landing); + }); + + it('should call .classList.add', function () { + expect(this.landingElement.classList.add).toHaveBeenCalledWith('hidden'); + }); + + it('should call Cookies.set', function () { + expect(Cookies.set).toHaveBeenCalledWith(this.cookieName, 'true', { expires: 365 }); + }); + }); + + describe('isDismissed', function () { + beforeEach(function () { + this.cookieName = 'cookie_name'; + this.landing = { cookieName: this.cookieName }; + + spyOn(Cookies, 'get').and.returnValue('true'); + + this.isDismissed = Landing.prototype.isDismissed.call(this.landing); + }); + + it('should call Cookies.get', function () { + expect(Cookies.get).toHaveBeenCalledWith(this.cookieName); + }); + + it('should return a boolean', function () { + expect(typeof this.isDismissed).toEqual('boolean'); + }); + }); +}); diff --git a/spec/javascripts/monitoring/deployments_spec.js b/spec/javascripts/monitoring/deployments_spec.js new file mode 100644 index 00000000000..19bc11d0f24 --- /dev/null +++ b/spec/javascripts/monitoring/deployments_spec.js @@ -0,0 +1,133 @@ +import d3 from 'd3'; +import PrometheusGraph from '~/monitoring/prometheus_graph'; +import Deployments from '~/monitoring/deployments'; +import { prometheusMockData } from './prometheus_mock_data'; + +describe('Metrics deployments', () => { + const fixtureName = 'environments/metrics/metrics.html.raw'; + let deployment; + let prometheusGraph; + + const graphElement = () => document.querySelector('.prometheus-graph'); + + preloadFixtures(fixtureName); + + beforeEach((done) => { + // Setup the view + loadFixtures(fixtureName); + + d3.selectAll('.prometheus-graph') + .append('g') + .attr('class', 'graph-container'); + + prometheusGraph = new PrometheusGraph(); + deployment = new Deployments(1000, 500); + + spyOn(prometheusGraph, 'init'); + spyOn($, 'ajax').and.callFake(() => { + const d = $.Deferred(); + d.resolve({ + deployments: [{ + id: 1, + created_at: deployment.chartData[10].time, + sha: 'testing', + tag: false, + ref: { + name: 'testing', + }, + }, { + id: 2, + created_at: deployment.chartData[15].time, + sha: '', + tag: true, + ref: { + name: 'tag', + }, + }], + }); + + setTimeout(done); + + return d.promise(); + }); + + prometheusGraph.configureGraph(); + prometheusGraph.transformData(prometheusMockData.metrics); + + deployment.init(prometheusGraph.graphSpecificProperties.memory_values.data); + }); + + it('creates line on graph for deploment', () => { + expect( + graphElement().querySelectorAll('.deployment-line').length, + ).toBe(2); + }); + + it('creates hidden deploy boxes', () => { + expect( + graphElement().querySelectorAll('.prometheus-graph .js-deploy-info-box').length, + ).toBe(2); + }); + + it('hides the info boxes by default', () => { + expect( + graphElement().querySelectorAll('.prometheus-graph .js-deploy-info-box.hidden').length, + ).toBe(2); + }); + + it('shows sha short code when tag is false', () => { + expect( + graphElement().querySelector('.deploy-info-1-cpu_values .js-deploy-info-box').textContent.trim(), + ).toContain('testin'); + }); + + it('shows ref name when tag is true', () => { + expect( + graphElement().querySelector('.deploy-info-2-cpu_values .js-deploy-info-box').textContent.trim(), + ).toContain('tag'); + }); + + it('shows info box when moving mouse over line', () => { + deployment.mouseOverDeployInfo(deployment.data[0].xPos, 'cpu_values'); + + expect( + graphElement().querySelectorAll('.prometheus-graph .js-deploy-info-box.hidden').length, + ).toBe(1); + + expect( + graphElement().querySelector('.deploy-info-1-cpu_values .js-deploy-info-box.hidden'), + ).toBeNull(); + }); + + it('hides previously visible info box when moving mouse away', () => { + deployment.mouseOverDeployInfo(500, 'cpu_values'); + + expect( + graphElement().querySelectorAll('.prometheus-graph .js-deploy-info-box.hidden').length, + ).toBe(2); + + expect( + graphElement().querySelector('.deploy-info-1-cpu_values .js-deploy-info-box.hidden'), + ).not.toBeNull(); + }); + + describe('refText', () => { + it('returns shortened SHA', () => { + expect( + Deployments.refText({ + tag: false, + sha: '123456789', + }), + ).toBe('123456'); + }); + + it('returns tag name', () => { + expect( + Deployments.refText({ + tag: true, + ref: 'v1.0', + }), + ).toBe('v1.0'); + }); + }); +}); diff --git a/spec/javascripts/monitoring/prometheus_graph_spec.js b/spec/javascripts/monitoring/prometheus_graph_spec.js index 4b904fc2960..25578bf1c6e 100644 --- a/spec/javascripts/monitoring/prometheus_graph_spec.js +++ b/spec/javascripts/monitoring/prometheus_graph_spec.js @@ -3,7 +3,7 @@ import PrometheusGraph from '~/monitoring/prometheus_graph'; import { prometheusMockData } from './prometheus_mock_data'; describe('PrometheusGraph', () => { - const fixtureName = 'static/environments/metrics.html.raw'; + const fixtureName = 'environments/metrics/metrics.html.raw'; const prometheusGraphContainer = '.prometheus-graph'; const prometheusGraphContents = `${prometheusGraphContainer}[graph-type=cpu_values]`; @@ -77,7 +77,7 @@ describe('PrometheusGraph', () => { }); describe('PrometheusGraphs UX states', () => { - const fixtureName = 'static/environments/metrics.html.raw'; + const fixtureName = 'environments/metrics/metrics.html.raw'; preloadFixtures(fixtureName); beforeEach(() => { diff --git a/spec/javascripts/pdf/index_spec.js b/spec/javascripts/pdf/index_spec.js new file mode 100644 index 00000000000..f661fae5fe2 --- /dev/null +++ b/spec/javascripts/pdf/index_spec.js @@ -0,0 +1,61 @@ +/* eslint-disable import/no-unresolved */ + +import Vue from 'vue'; +import { PDFJS } from 'pdfjs-dist'; +import workerSrc from 'vendor/pdf.worker'; + +import PDFLab from '~/pdf/index.vue'; +import pdf from '../fixtures/blob/pdf/test.pdf'; + +PDFJS.workerSrc = workerSrc; +const Component = Vue.extend(PDFLab); + +describe('PDF component', () => { + let vm; + + const checkLoaded = (done) => { + if (vm.loading) { + setTimeout(() => { + checkLoaded(done); + }, 100); + } else { + done(); + } + }; + + describe('without PDF data', () => { + beforeEach((done) => { + vm = new Component({ + propsData: { + pdf: '', + }, + }); + + vm.$mount(); + + checkLoaded(done); + }); + + it('does not render', () => { + expect(vm.$el.tagName).toBeUndefined(); + }); + }); + + describe('with PDF data', () => { + beforeEach((done) => { + vm = new Component({ + propsData: { + pdf, + }, + }); + + vm.$mount(); + + checkLoaded(done); + }); + + it('renders pdf component', () => { + expect(vm.$el.tagName).toBeDefined(); + }); + }); +}); diff --git a/spec/javascripts/pdf/page_spec.js b/spec/javascripts/pdf/page_spec.js new file mode 100644 index 00000000000..ac76ebbfbe6 --- /dev/null +++ b/spec/javascripts/pdf/page_spec.js @@ -0,0 +1,57 @@ +/* eslint-disable import/no-unresolved */ + +import Vue from 'vue'; +import pdfjsLib from 'pdfjs-dist'; +import workerSrc from 'vendor/pdf.worker'; + +import PageComponent from '~/pdf/page/index.vue'; +import testPDF from '../fixtures/blob/pdf/test.pdf'; + +const Component = Vue.extend(PageComponent); + +describe('Page component', () => { + let vm; + let testPage; + pdfjsLib.PDFJS.workerSrc = workerSrc; + + const checkRendered = (done) => { + if (vm.rendering) { + setTimeout(() => { + checkRendered(done); + }, 100); + } else { + done(); + } + }; + + beforeEach((done) => { + pdfjsLib.getDocument(testPDF) + .then(pdf => pdf.getPage(1)) + .then((page) => { + testPage = page; + done(); + }) + .catch((error) => { + console.error(error); + }); + }); + + describe('render', () => { + beforeEach((done) => { + vm = new Component({ + propsData: { + page: testPage, + number: 1, + }, + }); + + vm.$mount(); + + checkRendered(done); + }); + + it('renders first page', () => { + expect(vm.$el.tagName).toBeDefined(); + }); + }); +}); diff --git a/spec/lib/banzai/renderer_spec.rb b/spec/lib/banzai/renderer_spec.rb index e6f8d2a1fed..0e094405e33 100644 --- a/spec/lib/banzai/renderer_spec.rb +++ b/spec/lib/banzai/renderer_spec.rb @@ -11,7 +11,7 @@ describe Banzai::Renderer do end describe '#render_field' do - let(:renderer) { Banzai::Renderer } + let(:renderer) { described_class } subject { renderer.render_field(object, :field) } context 'with a stale cache' do diff --git a/spec/lib/constraints/group_url_constrainer_spec.rb b/spec/lib/constraints/group_url_constrainer_spec.rb index 96dacdc5cd2..f95adf3a84b 100644 --- a/spec/lib/constraints/group_url_constrainer_spec.rb +++ b/spec/lib/constraints/group_url_constrainer_spec.rb @@ -17,6 +17,13 @@ describe GroupUrlConstrainer, lib: true do it { expect(subject.matches?(request)).to be_truthy } end + context 'valid request for nested group with reserved top level name' do + let!(:nested_group) { create(:group, path: 'api', parent: group) } + let!(:request) { build_request('gitlab/api') } + + it { expect(subject.matches?(request)).to be_truthy } + end + context 'invalid request' do let(:request) { build_request('foo') } diff --git a/spec/lib/gitlab/changes_list_spec.rb b/spec/lib/gitlab/changes_list_spec.rb index 69d86144e32..464508fcd73 100644 --- a/spec/lib/gitlab/changes_list_spec.rb +++ b/spec/lib/gitlab/changes_list_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::ChangesList do let(:invalid_changes) { 1 } context 'when changes is a valid string' do - let(:changes_list) { Gitlab::ChangesList.new(valid_changes_string) } + let(:changes_list) { described_class.new(valid_changes_string) } it 'splits elements by newline character' do expect(changes_list).to contain_exactly({ diff --git a/spec/lib/gitlab/checks/force_push_spec.rb b/spec/lib/gitlab/checks/force_push_spec.rb index 7a84bbebd02..bc66ce83d4a 100644 --- a/spec/lib/gitlab/checks/force_push_spec.rb +++ b/spec/lib/gitlab/checks/force_push_spec.rb @@ -1,19 +1,19 @@ require 'spec_helper' -describe Gitlab::Checks::ChangeAccess, lib: true do +describe Gitlab::Checks::ForcePush, lib: true do let(:project) { create(:project, :repository) } context "exit code checking" do it "does not raise a runtime error if the `popen` call to git returns a zero exit code" do allow(Gitlab::Popen).to receive(:popen).and_return(['normal output', 0]) - expect { Gitlab::Checks::ForcePush.force_push?(project, 'oldrev', 'newrev') }.not_to raise_error + expect { described_class.force_push?(project, 'oldrev', 'newrev') }.not_to raise_error end it "raises a runtime error if the `popen` call to git returns a non-zero exit code" do allow(Gitlab::Popen).to receive(:popen).and_return(['error', 1]) - expect { Gitlab::Checks::ForcePush.force_push?(project, 'oldrev', 'newrev') }.to raise_error(RuntimeError) + expect { described_class.force_push?(project, 'oldrev', 'newrev') }.to raise_error(RuntimeError) end end end diff --git a/spec/lib/gitlab/ci/build/credentials/factory_spec.rb b/spec/lib/gitlab/ci/build/credentials/factory_spec.rb index 10b4b7a8826..d53db05e5e6 100644 --- a/spec/lib/gitlab/ci/build/credentials/factory_spec.rb +++ b/spec/lib/gitlab/ci/build/credentials/factory_spec.rb @@ -3,14 +3,14 @@ require 'spec_helper' describe Gitlab::Ci::Build::Credentials::Factory do let(:build) { create(:ci_build, name: 'spinach', stage: 'test', stage_idx: 0) } - subject { Gitlab::Ci::Build::Credentials::Factory.new(build).create! } + subject { described_class.new(build).create! } class TestProvider def initialize(build); end end before do - allow_any_instance_of(Gitlab::Ci::Build::Credentials::Factory).to receive(:providers).and_return([TestProvider]) + allow_any_instance_of(described_class).to receive(:providers).and_return([TestProvider]) end context 'when provider is valid' do diff --git a/spec/lib/gitlab/ci/build/credentials/registry_spec.rb b/spec/lib/gitlab/ci/build/credentials/registry_spec.rb index 84e44dd53e2..c6054138cde 100644 --- a/spec/lib/gitlab/ci/build/credentials/registry_spec.rb +++ b/spec/lib/gitlab/ci/build/credentials/registry_spec.rb @@ -4,14 +4,14 @@ describe Gitlab::Ci::Build::Credentials::Registry do let(:build) { create(:ci_build, name: 'spinach', stage: 'test', stage_idx: 0) } let(:registry_url) { 'registry.example.com:5005' } - subject { Gitlab::Ci::Build::Credentials::Registry.new(build) } + subject { described_class.new(build) } before do stub_container_registry_config(host_port: registry_url) end it 'contains valid DockerRegistry credentials' do - expect(subject).to be_kind_of(Gitlab::Ci::Build::Credentials::Registry) + expect(subject).to be_kind_of(described_class) expect(subject.username).to eq 'gitlab-ci-token' expect(subject.password).to eq build.token @@ -20,7 +20,7 @@ describe Gitlab::Ci::Build::Credentials::Registry do end describe '.valid?' do - subject { Gitlab::Ci::Build::Credentials::Registry.new(build).valid? } + subject { described_class.new(build).valid? } context 'when registry is enabled' do before do diff --git a/spec/lib/gitlab/ci/cron_parser_spec.rb b/spec/lib/gitlab/ci/cron_parser_spec.rb index 0864bc7258d..809fda11879 100644 --- a/spec/lib/gitlab/ci/cron_parser_spec.rb +++ b/spec/lib/gitlab/ci/cron_parser_spec.rb @@ -60,14 +60,60 @@ describe Gitlab::Ci::CronParser do end end - context 'when cron_timezone is US/Pacific' do - let(:cron) { '0 0 * * *' } - let(:cron_timezone) { 'US/Pacific' } + context 'when cron_timezone is TZInfo format' do + before do + allow(Time).to receive(:zone) + .and_return(ActiveSupport::TimeZone['UTC']) + end - it_behaves_like "returns time in the future" + let(:hour_in_utc) do + ActiveSupport::TimeZone[cron_timezone] + .now.change(hour: 0).in_time_zone('UTC').hour + end + + context 'when cron_timezone is US/Pacific' do + let(:cron) { '* 0 * * *' } + let(:cron_timezone) { 'US/Pacific' } + + it_behaves_like "returns time in the future" + + it 'converts time in server time zone' do + expect(subject.hour).to eq(hour_in_utc) + end + end + end + + context 'when cron_timezone is ActiveSupport::TimeZone format' do + before do + allow(Time).to receive(:zone) + .and_return(ActiveSupport::TimeZone['UTC']) + end + + let(:hour_in_utc) do + ActiveSupport::TimeZone[cron_timezone] + .now.change(hour: 0).in_time_zone('UTC').hour + end + + context 'when cron_timezone is Berlin' do + let(:cron) { '* 0 * * *' } + let(:cron_timezone) { 'Berlin' } + + it_behaves_like "returns time in the future" + + it 'converts time in server time zone' do + expect(subject.hour).to eq(hour_in_utc) + end + end - it 'converts time in server time zone' do - expect(subject.hour).to eq((Time.zone.now.in_time_zone(cron_timezone).utc_offset / 60 / 60).abs) + context 'when cron_timezone is Eastern Time (US & Canada)' do + let(:cron) { '* 0 * * *' } + let(:cron_timezone) { 'Eastern Time (US & Canada)' } + + it_behaves_like "returns time in the future" + + it 'converts time in server time zone' do + expect(subject.hour).to eq(hour_in_utc) + end end end end @@ -76,9 +122,21 @@ describe Gitlab::Ci::CronParser do let(:cron) { 'invalid_cron' } let(:cron_timezone) { 'invalid_cron_timezone' } - it 'returns nil' do - is_expected.to be_nil - end + it { is_expected.to be_nil } + end + + context 'when cron syntax is quoted' do + let(:cron) { "'0 * * * *'" } + let(:cron_timezone) { 'UTC' } + + it { expect(subject).to be_nil } + end + + context 'when cron syntax is rufus-scheduler syntax' do + let(:cron) { 'every 3h' } + let(:cron_timezone) { 'UTC' } + + it { expect(subject).to be_nil } end end @@ -96,6 +154,12 @@ describe Gitlab::Ci::CronParser do it { is_expected.to eq(false) } end + + context 'when cron syntax is quoted' do + let(:cron) { "'0 * * * *'" } + + it { is_expected.to eq(false) } + end end describe '#cron_timezone_valid?' do @@ -112,5 +176,11 @@ describe Gitlab::Ci::CronParser do it { is_expected.to eq(false) } end + + context 'when cron_timezone is ActiveSupport::TimeZone format' do + let(:cron_timezone) { 'Eastern Time (US & Canada)' } + + it { is_expected.to eq(true) } + end end end diff --git a/spec/lib/gitlab/current_settings_spec.rb b/spec/lib/gitlab/current_settings_spec.rb index b01c4805a34..c796c98ec9f 100644 --- a/spec/lib/gitlab/current_settings_spec.rb +++ b/spec/lib/gitlab/current_settings_spec.rb @@ -10,7 +10,7 @@ describe Gitlab::CurrentSettings do describe '#current_application_settings' do context 'with DB available' do before do - allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(true) + allow_any_instance_of(described_class).to receive(:connect_to_db?).and_return(true) end it 'attempts to use cached values first' do @@ -36,7 +36,7 @@ describe Gitlab::CurrentSettings do context 'with DB unavailable' do before do - allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(false) + allow_any_instance_of(described_class).to receive(:connect_to_db?).and_return(false) end it 'returns an in-memory ApplicationSetting object' do diff --git a/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb index c455cd9b942..d8757c601ab 100644 --- a/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb @@ -20,7 +20,7 @@ describe Gitlab::CycleAnalytics::BaseEventFetcher do before do allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return(Issue.all) - allow_any_instance_of(Gitlab::CycleAnalytics::BaseEventFetcher).to receive(:serialize) do |event| + allow_any_instance_of(described_class).to receive(:serialize) do |event| event end diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index a044b871730..737fac14f92 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -726,4 +726,37 @@ describe Gitlab::Database::MigrationHelpers, lib: true do expect(model.column_for(:users, :kittens)).to be_nil end end + + describe '#replace_sql' do + context 'using postgres' do + before do + allow(Gitlab::Database).to receive(:mysql?).and_return(false) + end + + it 'builds the sql with correct functions' do + expect(model.replace_sql(Arel::Table.new(:users)[:first_name], "Alice", "Eve").to_s). + to include('regexp_replace') + end + end + + context 'using mysql' do + before do + allow(Gitlab::Database).to receive(:mysql?).and_return(true) + end + + it 'builds the sql with the correct functions' do + expect(model.replace_sql(Arel::Table.new(:users)[:first_name], "Alice", "Eve").to_s). + to include('locate', 'insert') + end + end + + describe 'results' do + let!(:user) { create(:user, name: 'Kathy Alice Aliceson') } + + it 'replaces the correct part of the string' do + model.update_column_in_batches(:users, :name, model.replace_sql(Arel::Table.new(:users)[:name], 'Alice', 'Eve')) + expect(user.reload.name).to eq('Kathy Eve Aliceson') + end + end + end end diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb new file mode 100644 index 00000000000..64bc5fc0429 --- /dev/null +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb @@ -0,0 +1,197 @@ +require 'spec_helper' + +describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase do + let(:migration) { FakeRenameReservedPathMigrationV1.new } + let(:subject) { described_class.new(['the-path'], migration) } + + before do + allow(migration).to receive(:say) + end + + def migration_namespace(namespace) + Gitlab::Database::RenameReservedPathsMigration::V1::MigrationClasses:: + Namespace.find(namespace.id) + end + + def migration_project(project) + Gitlab::Database::RenameReservedPathsMigration::V1::MigrationClasses:: + Project.find(project.id) + end + + describe "#remove_last_ocurrence" do + it "removes only the last occurance of a string" do + input = "this/is/a-word-to-replace/namespace/with/a-word-to-replace" + + expect(subject.remove_last_occurrence(input, "a-word-to-replace")) + .to eq("this/is/a-word-to-replace/namespace/with/") + end + end + + describe '#remove_cached_html_for_projects' do + let(:project) { create(:empty_project, description_html: 'Project description') } + + it 'removes description_html from projects' do + subject.remove_cached_html_for_projects([project.id]) + + expect(project.reload.description_html).to be_nil + end + + it 'removes issue descriptions' do + issue = create(:issue, project: project, description_html: 'Issue description') + + subject.remove_cached_html_for_projects([project.id]) + + expect(issue.reload.description_html).to be_nil + end + + it 'removes merge request descriptions' do + merge_request = create(:merge_request, + source_project: project, + target_project: project, + description_html: 'MergeRequest description') + + subject.remove_cached_html_for_projects([project.id]) + + expect(merge_request.reload.description_html).to be_nil + end + + it 'removes note html' do + note = create(:note, + project: project, + noteable: create(:issue, project: project), + note_html: 'note description') + + subject.remove_cached_html_for_projects([project.id]) + + expect(note.reload.note_html).to be_nil + end + + it 'removes milestone description' do + milestone = create(:milestone, + project: project, + description_html: 'milestone description') + + subject.remove_cached_html_for_projects([project.id]) + + expect(milestone.reload.description_html).to be_nil + end + end + + describe '#rename_path_for_routable' do + context 'for namespaces' do + let(:namespace) { create(:namespace, path: 'the-path') } + it "renames namespaces called the-path" do + subject.rename_path_for_routable(migration_namespace(namespace)) + + expect(namespace.reload.path).to eq("the-path0") + end + + it "renames the route to the namespace" do + subject.rename_path_for_routable(migration_namespace(namespace)) + + expect(Namespace.find(namespace.id).full_path).to eq("the-path0") + end + + it "renames the route for projects of the namespace" do + project = create(:project, path: "project-path", namespace: namespace) + + subject.rename_path_for_routable(migration_namespace(namespace)) + + expect(project.route.reload.path).to eq("the-path0/project-path") + end + + it 'returns the old & the new path' do + old_path, new_path = subject.rename_path_for_routable(migration_namespace(namespace)) + + expect(old_path).to eq('the-path') + expect(new_path).to eq('the-path0') + end + + context "the-path namespace -> subgroup -> the-path0 project" do + it "updates the route of the project correctly" do + subgroup = create(:group, path: "subgroup", parent: namespace) + project = create(:project, path: "the-path0", namespace: subgroup) + + subject.rename_path_for_routable(migration_namespace(namespace)) + + expect(project.route.reload.path).to eq("the-path0/subgroup/the-path0") + end + end + end + + context 'for projects' do + let(:parent) { create(:namespace, path: 'the-parent') } + let(:project) { create(:empty_project, path: 'the-path', namespace: parent) } + + it 'renames the project called `the-path`' do + subject.rename_path_for_routable(migration_project(project)) + + expect(project.reload.path).to eq('the-path0') + end + + it 'renames the route for the project' do + subject.rename_path_for_routable(project) + + expect(project.reload.route.path).to eq('the-parent/the-path0') + end + + it 'returns the old & new path' do + old_path, new_path = subject.rename_path_for_routable(migration_project(project)) + + expect(old_path).to eq('the-parent/the-path') + expect(new_path).to eq('the-parent/the-path0') + end + end + end + + describe '#move_pages' do + it 'moves the pages directory' do + expect(subject).to receive(:move_folders) + .with(TestEnv.pages_path, 'old-path', 'new-path') + + subject.move_pages('old-path', 'new-path') + end + end + + describe "#move_uploads" do + let(:test_dir) { File.join(Rails.root, 'tmp', 'tests', 'rename_reserved_paths') } + let(:uploads_dir) { File.join(test_dir, 'public', 'uploads') } + + it 'moves subdirectories in the uploads folder' do + expect(subject).to receive(:uploads_dir).and_return(uploads_dir) + expect(subject).to receive(:move_folders).with(uploads_dir, 'old_path', 'new_path') + + subject.move_uploads('old_path', 'new_path') + end + + it "doesn't move uploads when they are stored in object storage" do + expect(subject).to receive(:file_storage?).and_return(false) + expect(subject).not_to receive(:move_folders) + + subject.move_uploads('old_path', 'new_path') + end + end + + describe '#move_folders' do + let(:test_dir) { File.join(Rails.root, 'tmp', 'tests', 'rename_reserved_paths') } + let(:uploads_dir) { File.join(test_dir, 'public', 'uploads') } + + before do + FileUtils.remove_dir(test_dir) if File.directory?(test_dir) + FileUtils.mkdir_p(uploads_dir) + allow(subject).to receive(:uploads_dir).and_return(uploads_dir) + end + + it 'moves a folder with files' do + source = File.join(uploads_dir, 'parent-group', 'sub-group') + FileUtils.mkdir_p(source) + destination = File.join(uploads_dir, 'parent-group', 'moved-group') + FileUtils.touch(File.join(source, 'test.txt')) + expected_file = File.join(destination, 'test.txt') + + subject.move_folders(uploads_dir, File.join('parent-group', 'sub-group'), File.join('parent-group', 'moved-group')) + + expect(File.exist?(expected_file)).to be(true) + end + end +end diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb new file mode 100644 index 00000000000..a25c5da488a --- /dev/null +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb @@ -0,0 +1,171 @@ +require 'spec_helper' + +describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do + let(:migration) { FakeRenameReservedPathMigrationV1.new } + let(:subject) { described_class.new(['the-path'], migration) } + + before do + allow(migration).to receive(:say) + end + + def migration_namespace(namespace) + Gitlab::Database::RenameReservedPathsMigration::V1::MigrationClasses:: + Namespace.find(namespace.id) + end + + describe '#namespaces_for_paths' do + context 'nested namespaces' do + let(:subject) { described_class.new(['parent/the-Path'], migration) } + + it 'includes the namespace' do + parent = create(:namespace, path: 'parent') + child = create(:namespace, path: 'the-path', parent: parent) + + found_ids = subject.namespaces_for_paths(type: :child). + map(&:id) + expect(found_ids).to contain_exactly(child.id) + end + end + + context 'for child namespaces' do + it 'only returns child namespaces with the correct path' do + _root_namespace = create(:namespace, path: 'THE-path') + _other_path = create(:namespace, + path: 'other', + parent: create(:namespace)) + namespace = create(:namespace, + path: 'the-path', + parent: create(:namespace)) + + found_ids = subject.namespaces_for_paths(type: :child). + map(&:id) + expect(found_ids).to contain_exactly(namespace.id) + end + end + + context 'for top levelnamespaces' do + it 'only returns child namespaces with the correct path' do + root_namespace = create(:namespace, path: 'the-path') + _other_path = create(:namespace, path: 'other') + _child_namespace = create(:namespace, + path: 'the-path', + parent: create(:namespace)) + + found_ids = subject.namespaces_for_paths(type: :top_level). + map(&:id) + expect(found_ids).to contain_exactly(root_namespace.id) + end + end + end + + describe '#move_repositories' do + let(:namespace) { create(:group, name: 'hello-group') } + it 'moves a project for a namespace' do + create(:project, namespace: namespace, path: 'hello-project') + expected_path = File.join(TestEnv.repos_path, 'bye-group', 'hello-project.git') + + subject.move_repositories(namespace, 'hello-group', 'bye-group') + + expect(File.directory?(expected_path)).to be(true) + end + + it 'moves a namespace in a subdirectory correctly' do + child_namespace = create(:group, name: 'sub-group', parent: namespace) + create(:project, namespace: child_namespace, path: 'hello-project') + + expected_path = File.join(TestEnv.repos_path, 'hello-group', 'renamed-sub-group', 'hello-project.git') + + subject.move_repositories(child_namespace, 'hello-group/sub-group', 'hello-group/renamed-sub-group') + + expect(File.directory?(expected_path)).to be(true) + end + + it 'moves a parent namespace with subdirectories' do + child_namespace = create(:group, name: 'sub-group', parent: namespace) + create(:project, namespace: child_namespace, path: 'hello-project') + expected_path = File.join(TestEnv.repos_path, 'renamed-group', 'sub-group', 'hello-project.git') + + subject.move_repositories(child_namespace, 'hello-group', 'renamed-group') + + expect(File.directory?(expected_path)).to be(true) + end + end + + describe "#child_ids_for_parent" do + it "collects child ids for all levels" do + parent = create(:namespace) + first_child = create(:namespace, parent: parent) + second_child = create(:namespace, parent: parent) + third_child = create(:namespace, parent: second_child) + all_ids = [parent.id, first_child.id, second_child.id, third_child.id] + + collected_ids = subject.child_ids_for_parent(parent, ids: [parent.id]) + + expect(collected_ids).to contain_exactly(*all_ids) + end + end + + describe "#rename_namespace" do + let(:namespace) { create(:namespace, path: 'the-path') } + + it 'renames paths & routes for the namespace' do + expect(subject).to receive(:rename_path_for_routable). + with(namespace). + and_call_original + + subject.rename_namespace(namespace) + + expect(namespace.reload.path).to eq('the-path0') + end + + it "moves the the repository for a project in the namespace" do + create(:project, namespace: namespace, path: "the-path-project") + expected_repo = File.join(TestEnv.repos_path, "the-path0", "the-path-project.git") + + subject.rename_namespace(namespace) + + expect(File.directory?(expected_repo)).to be(true) + end + + it "moves the uploads for the namespace" do + expect(subject).to receive(:move_uploads).with("the-path", "the-path0") + + subject.rename_namespace(namespace) + end + + it "moves the pages for the namespace" do + expect(subject).to receive(:move_pages).with("the-path", "the-path0") + + subject.rename_namespace(namespace) + end + + it 'invalidates the markdown cache of related projects' do + project = create(:empty_project, namespace: namespace, path: "the-path-project") + + expect(subject).to receive(:remove_cached_html_for_projects).with([project.id]) + + subject.rename_namespace(namespace) + end + end + + describe '#rename_namespaces' do + let!(:top_level_namespace) { create(:namespace, path: 'the-path') } + let!(:child_namespace) do + create(:namespace, path: 'the-path', parent: create(:namespace)) + end + + it 'renames top level namespaces the namespace' do + expect(subject).to receive(:rename_namespace). + with(migration_namespace(top_level_namespace)) + + subject.rename_namespaces(type: :top_level) + end + + it 'renames child namespaces' do + expect(subject).to receive(:rename_namespace). + with(migration_namespace(child_namespace)) + + subject.rename_namespaces(type: :child) + end + end +end diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb new file mode 100644 index 00000000000..59e8de2712d --- /dev/null +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb @@ -0,0 +1,102 @@ +require 'spec_helper' + +describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects do + let(:migration) { FakeRenameReservedPathMigrationV1.new } + let(:subject) { described_class.new(['the-path'], migration) } + + before do + allow(migration).to receive(:say) + end + + describe '#projects_for_paths' do + it 'searches using nested paths' do + namespace = create(:namespace, path: 'hello') + project = create(:empty_project, path: 'THE-path', namespace: namespace) + + result_ids = described_class.new(['Hello/the-path'], migration). + projects_for_paths.map(&:id) + + expect(result_ids).to contain_exactly(project.id) + end + + it 'includes the correct projects' do + project = create(:empty_project, path: 'THE-path') + _other_project = create(:empty_project) + + result_ids = subject.projects_for_paths.map(&:id) + + expect(result_ids).to contain_exactly(project.id) + end + end + + describe '#rename_projects' do + let!(:projects) { create_list(:empty_project, 2, path: 'the-path') } + + it 'renames each project' do + expect(subject).to receive(:rename_project).twice + + subject.rename_projects + end + + it 'invalidates the markdown cache of related projects' do + expect(subject).to receive(:remove_cached_html_for_projects). + with(projects.map(&:id)) + + subject.rename_projects + end + end + + describe '#rename_project' do + let(:project) do + create(:empty_project, + path: 'the-path', + namespace: create(:namespace, path: 'known-parent' )) + end + + it 'renames path & route for the project' do + expect(subject).to receive(:rename_path_for_routable). + with(project). + and_call_original + + subject.rename_project(project) + + expect(project.reload.path).to eq('the-path0') + end + + it 'moves the wiki & the repo' do + expect(subject).to receive(:move_repository). + with(project, 'known-parent/the-path.wiki', 'known-parent/the-path0.wiki') + expect(subject).to receive(:move_repository). + with(project, 'known-parent/the-path', 'known-parent/the-path0') + + subject.rename_project(project) + end + + it 'moves uploads' do + expect(subject).to receive(:move_uploads). + with('known-parent/the-path', 'known-parent/the-path0') + + subject.rename_project(project) + end + + it 'moves pages' do + expect(subject).to receive(:move_pages). + with('known-parent/the-path', 'known-parent/the-path0') + + subject.rename_project(project) + end + end + + describe '#move_repository' do + let(:known_parent) { create(:namespace, path: 'known-parent') } + let(:project) { create(:project, path: 'the-path', namespace: known_parent) } + + it 'moves the repository for a project' do + expected_path = File.join(TestEnv.repos_path, 'known-parent', 'new-repo.git') + + subject.move_repository(project, 'known-parent/the-path', 'known-parent/new-repo') + + expect(File.directory?(expected_path)).to be(true) + end + end +end diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb new file mode 100644 index 00000000000..f8cc1eb91ec --- /dev/null +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +shared_examples 'renames child namespaces' do |type| + it 'renames namespaces' do + rename_namespaces = double + expect(described_class::RenameNamespaces). + to receive(:new).with(['first-path', 'second-path'], subject). + and_return(rename_namespaces) + expect(rename_namespaces).to receive(:rename_namespaces). + with(type: :child) + + subject.rename_wildcard_paths(['first-path', 'second-path']) + end +end + +describe Gitlab::Database::RenameReservedPathsMigration::V1 do + let(:subject) { FakeRenameReservedPathMigrationV1.new } + + before do + allow(subject).to receive(:say) + end + + describe '#rename_child_paths' do + it_behaves_like 'renames child namespaces' + end + + describe '#rename_wildcard_paths' do + it_behaves_like 'renames child namespaces' + + it 'should rename projects' do + rename_projects = double + expect(described_class::RenameProjects). + to receive(:new).with(['the-path'], subject). + and_return(rename_projects) + + expect(rename_projects).to receive(:rename_projects) + + subject.rename_wildcard_paths(['the-path']) + end + end + + describe '#rename_root_paths' do + it 'should rename namespaces' do + rename_namespaces = double + expect(described_class::RenameNamespaces). + to receive(:new).with(['the-path'], subject). + and_return(rename_namespaces) + expect(rename_namespaces).to receive(:rename_namespaces). + with(type: :top_level) + + subject.rename_root_paths('the-path') + end + end +end diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb index 2a86b427806..f127e45ae6a 100644 --- a/spec/lib/gitlab/email/receiver_spec.rb +++ b/spec/lib/gitlab/email/receiver_spec.rb @@ -7,9 +7,17 @@ describe Gitlab::Email::Receiver, lib: true do context "when we cannot find a capable handler" do let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "!!!") } - it "raises a UnknownIncomingEmail" do + it "raises an UnknownIncomingEmail error" do expect { receiver.execute }.to raise_error(Gitlab::Email::UnknownIncomingEmail) end + + context "and the email contains no references header" do + let(:email_raw) { fixture_file("emails/auto_reply.eml").gsub(mail_key, "!!!") } + + it "raises an UnknownIncomingEmail error" do + expect { receiver.execute }.to raise_error(Gitlab::Email::UnknownIncomingEmail) + end + end end context "when the email is blank" do diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index f88653cb1fe..ddedb7c3443 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -24,21 +24,26 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - # TODO: Uncomment when feature is reenabled - # context 'with gitaly enabled' do - # before { stub_gitaly } - # - # it 'gets the branch name from GitalyClient' do - # expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name) - # repository.root_ref - # end - # - # it 'wraps GRPC exceptions' do - # expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name). - # and_raise(GRPC::Unknown) - # expect { repository.root_ref }.to raise_error(Gitlab::Git::CommandError) - # end - # end + context 'with gitaly enabled' do + before { stub_gitaly } + + it 'gets the branch name from GitalyClient' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name) + repository.root_ref + end + + it 'wraps GRPC not found' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name). + and_raise(GRPC::NotFound) + expect { repository.root_ref }.to raise_error(Gitlab::Git::Repository::NoRepository) + end + + it 'wraps GRPC exceptions' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name). + and_raise(GRPC::Unknown) + expect { repository.root_ref }.to raise_error(Gitlab::Git::CommandError) + end + end end describe "#rugged" do @@ -113,21 +118,26 @@ describe Gitlab::Git::Repository, seed_helper: true do it { is_expected.to include("master") } it { is_expected.not_to include("branch-from-space") } - # TODO: Uncomment when feature is reenabled - # context 'with gitaly enabled' do - # before { stub_gitaly } - # - # it 'gets the branch names from GitalyClient' do - # expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names) - # subject - # end - # - # it 'wraps GRPC exceptions' do - # expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names). - # and_raise(GRPC::Unknown) - # expect { subject }.to raise_error(Gitlab::Git::CommandError) - # end - # end + context 'with gitaly enabled' do + before { stub_gitaly } + + it 'gets the branch names from GitalyClient' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names) + subject + end + + it 'wraps GRPC not found' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names). + and_raise(GRPC::NotFound) + expect { subject }.to raise_error(Gitlab::Git::Repository::NoRepository) + end + + it 'wraps GRPC other exceptions' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names). + and_raise(GRPC::Unknown) + expect { subject }.to raise_error(Gitlab::Git::CommandError) + end + end end describe '#tag_names' do @@ -145,21 +155,26 @@ describe Gitlab::Git::Repository, seed_helper: true do it { is_expected.to include("v1.0.0") } it { is_expected.not_to include("v5.0.0") } - # TODO: Uncomment when feature is reenabled - # context 'with gitaly enabled' do - # before { stub_gitaly } - # - # it 'gets the tag names from GitalyClient' do - # expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names) - # subject - # end - # - # it 'wraps GRPC exceptions' do - # expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names). - # and_raise(GRPC::Unknown) - # expect { subject }.to raise_error(Gitlab::Git::CommandError) - # end - # end + context 'with gitaly enabled' do + before { stub_gitaly } + + it 'gets the tag names from GitalyClient' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names) + subject + end + + it 'wraps GRPC not found' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names). + and_raise(GRPC::NotFound) + expect { subject }.to raise_error(Gitlab::Git::Repository::NoRepository) + end + + it 'wraps GRPC exceptions' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names). + and_raise(GRPC::Unknown) + expect { subject }.to raise_error(Gitlab::Git::CommandError) + end + end end shared_examples 'archive check' do |extenstion| @@ -1074,20 +1089,8 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe '#branch_count' do - before(:each) do - valid_ref = double(:ref) - invalid_ref = double(:ref) - - allow(valid_ref).to receive_messages(name: 'master', target: double(:target)) - - allow(invalid_ref).to receive_messages(name: 'bad-branch') - allow(invalid_ref).to receive(:target) { raise Rugged::ReferenceError } - - allow(repository.rugged).to receive_messages(branches: [valid_ref, invalid_ref]) - end - it 'returns the number of branches' do - expect(repository.branch_count).to eq(1) + expect(repository.branch_count).to eq(9) end end diff --git a/spec/lib/gitlab/git/util_spec.rb b/spec/lib/gitlab/git/util_spec.rb index bcca4d4c746..69d3ca55397 100644 --- a/spec/lib/gitlab/git/util_spec.rb +++ b/spec/lib/gitlab/git/util_spec.rb @@ -9,7 +9,7 @@ describe Gitlab::Git::Util do ["foo\n\n", 2], ].each do |string, line_count| it "counts #{line_count} lines in #{string.inspect}" do - expect(Gitlab::Git::Util.count_lines(string)).to eq(line_count) + expect(described_class.count_lines(string)).to eq(line_count) end end end diff --git a/spec/lib/git_ref_validator_spec.rb b/spec/lib/gitlab/git_ref_validator_spec.rb index cc8daa535d6..cc8daa535d6 100644 --- a/spec/lib/git_ref_validator_spec.rb +++ b/spec/lib/gitlab/git_ref_validator_spec.rb diff --git a/spec/lib/gitlab/gitaly_client/ref_spec.rb b/spec/lib/gitlab/gitaly_client/ref_spec.rb index 5405eafd281..255f23e6270 100644 --- a/spec/lib/gitlab/gitaly_client/ref_spec.rb +++ b/spec/lib/gitlab/gitaly_client/ref_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Gitlab::GitalyClient::Ref do let(:project) { create(:empty_project) } let(:repo_path) { project.repository.path_to_repo } - let(:client) { Gitlab::GitalyClient::Ref.new(project.repository) } + let(:client) { described_class.new(project.repository) } before do allow(Gitlab.config.gitaly).to receive(:enabled).and_return(true) diff --git a/spec/lib/gitlab/healthchecks/db_check_spec.rb b/spec/lib/gitlab/health_checks/db_check_spec.rb index 33c6c24449c..33c6c24449c 100644 --- a/spec/lib/gitlab/healthchecks/db_check_spec.rb +++ b/spec/lib/gitlab/health_checks/db_check_spec.rb diff --git a/spec/lib/gitlab/healthchecks/fs_shards_check_spec.rb b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb index 4cd8cf313a5..4cd8cf313a5 100644 --- a/spec/lib/gitlab/healthchecks/fs_shards_check_spec.rb +++ b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb diff --git a/spec/lib/gitlab/healthchecks/redis_check_spec.rb b/spec/lib/gitlab/health_checks/redis_check_spec.rb index 734cdcb893e..734cdcb893e 100644 --- a/spec/lib/gitlab/healthchecks/redis_check_spec.rb +++ b/spec/lib/gitlab/health_checks/redis_check_spec.rb diff --git a/spec/lib/gitlab/healthchecks/simple_check_shared.rb b/spec/lib/gitlab/health_checks/simple_check_shared.rb index 1fa6d0faef9..1fa6d0faef9 100644 --- a/spec/lib/gitlab/healthchecks/simple_check_shared.rb +++ b/spec/lib/gitlab/health_checks/simple_check_shared.rb diff --git a/spec/lib/gitlab/import_export/fork_spec.rb b/spec/lib/gitlab/import_export/fork_spec.rb index c5ce06afd73..42f3fc59f04 100644 --- a/spec/lib/gitlab/import_export/fork_spec.rb +++ b/spec/lib/gitlab/import_export/fork_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe 'forked project import', services: true do let(:user) { create(:user) } let!(:project_with_repo) { create(:project, :test_repo, name: 'test-repo-restorer', path: 'test-repo-restorer') } - let!(:project) { create(:empty_project) } + let!(:project) { create(:empty_project, name: 'test-repo-restorer-no-repo', path: 'test-repo-restorer-no-repo') } let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) } let(:forked_from_project) { create(:project) } diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index bfecfa28ed1..fdbb6a0556d 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -2,6 +2,7 @@ "description": "Nisi et repellendus ut enim quo accusamus vel magnam.", "visibility_level": 10, "archived": false, + "description_html": "description", "labels": [ { "id": 2, diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index 0e9607c5bd3..14338515892 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -30,6 +30,10 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::ENABLED) end + it 'has the project html description' do + expect(Project.find_by_path('project').description_html).to eq('description') + end + it 'has the same label associated to two issues' do expect(ProjectLabel.find_by_title('test2').issues.count).to eq(2) end diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index d2d89e3b019..6e145947104 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -189,6 +189,16 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do end end end + + context 'project attributes' do + it 'contains the html description' do + expect(saved_project_json).to include("description_html" => 'description') + end + + it 'does not contain the runners token' do + expect(saved_project_json).not_to include("runners_token" => 'token') + end + end end end @@ -209,6 +219,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do releases: [release], group: group ) + project.update(description_html: 'description') project_label = create(:label, project: project) group_label = create(:group_label, group: group) create(:label_link, label: project_label, target: issue) diff --git a/spec/lib/gitlab/import_export/reader_spec.rb b/spec/lib/gitlab/import_export/reader_spec.rb index 48d74b07e27..d700af142be 100644 --- a/spec/lib/gitlab/import_export/reader_spec.rb +++ b/spec/lib/gitlab/import_export/reader_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::ImportExport::Reader, lib: true do let(:test_config) { 'spec/support/import_export/import_export.yml' } let(:project_tree_hash) do { - only: [:name, :path], + except: [:id, :created_at], include: [:issues, :labels, { merge_requests: { only: [:id], diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 0372e3f7dbf..ebfaab4eacd 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -329,6 +329,28 @@ Project: - snippets_enabled - visibility_level - archived +- created_at +- updated_at +- last_activity_at +- star_count +- ci_id +- shared_runners_enabled +- build_coverage_regex +- build_allow_git_fetchs +- build_timeout +- pending_delete +- public_builds +- last_repository_check_failed +- last_repository_check_at +- container_registry_enabled +- only_allow_merge_if_pipeline_succeeds +- has_external_issue_tracker +- request_access_enabled +- has_external_wiki +- only_allow_merge_if_all_discussions_are_resolved +- auto_cancel_pending_pipelines +- printing_merge_request_link_enabled +- build_allow_git_fetch Author: - name ProjectFeature: diff --git a/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb b/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb index 071e5fac3f0..071e5fac3f0 100644 --- a/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb +++ b/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb diff --git a/spec/lib/gitlab/ldap/person_spec.rb b/spec/lib/gitlab/ldap/person_spec.rb index 9a556cde5d5..087c4d8c92c 100644 --- a/spec/lib/gitlab/ldap/person_spec.rb +++ b/spec/lib/gitlab/ldap/person_spec.rb @@ -20,7 +20,7 @@ describe Gitlab::LDAP::Person do it 'uses the configured name attribute and handles values as an array' do name = 'John Doe' entry['cn'] = [name] - person = Gitlab::LDAP::Person.new(entry, 'ldapmain') + person = described_class.new(entry, 'ldapmain') expect(person.name).to eq(name) end @@ -30,7 +30,7 @@ describe Gitlab::LDAP::Person do it 'returns the value of mail, if present' do mail = 'john@example.com' entry['mail'] = mail - person = Gitlab::LDAP::Person.new(entry, 'ldapmain') + person = described_class.new(entry, 'ldapmain') expect(person.email).to eq([mail]) end @@ -38,7 +38,7 @@ describe Gitlab::LDAP::Person do it 'returns the value of userPrincipalName, if mail and email are not present' do user_principal_name = 'john.doe@example.com' entry['userPrincipalName'] = user_principal_name - person = Gitlab::LDAP::Person.new(entry, 'ldapmain') + person = described_class.new(entry, 'ldapmain') expect(person.email).to eq([user_principal_name]) end diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb index ab6e311b1e8..208a8d028cd 100644 --- a/spec/lib/gitlab/metrics_spec.rb +++ b/spec/lib/gitlab/metrics_spec.rb @@ -20,7 +20,7 @@ describe Gitlab::Metrics do expect(pool).to receive(:with).and_yield(connection) expect(connection).to receive(:write_points).with(an_instance_of(Array)) - expect(Gitlab::Metrics).to receive(:pool).and_return(pool) + expect(described_class).to receive(:pool).and_return(pool) described_class.submit_metrics([{ 'series' => 'kittens', 'tags' => {} }]) end @@ -64,7 +64,7 @@ describe Gitlab::Metrics do describe '.measure' do context 'without a transaction' do it 'returns the return value of the block' do - val = Gitlab::Metrics.measure(:foo) { 10 } + val = described_class.measure(:foo) { 10 } expect(val).to eq(10) end @@ -74,7 +74,7 @@ describe Gitlab::Metrics do let(:transaction) { Gitlab::Metrics::Transaction.new } before do - allow(Gitlab::Metrics).to receive(:current_transaction). + allow(described_class).to receive(:current_transaction). and_return(transaction) end @@ -88,11 +88,11 @@ describe Gitlab::Metrics do expect(transaction).to receive(:increment). with('foo_call_count', 1) - Gitlab::Metrics.measure(:foo) { 10 } + described_class.measure(:foo) { 10 } end it 'returns the return value of the block' do - val = Gitlab::Metrics.measure(:foo) { 10 } + val = described_class.measure(:foo) { 10 } expect(val).to eq(10) end @@ -105,7 +105,7 @@ describe Gitlab::Metrics do expect_any_instance_of(Gitlab::Metrics::Transaction). not_to receive(:add_tag) - Gitlab::Metrics.tag_transaction(:foo, 'bar') + described_class.tag_transaction(:foo, 'bar') end end @@ -113,13 +113,13 @@ describe Gitlab::Metrics do let(:transaction) { Gitlab::Metrics::Transaction.new } it 'adds the tag to the transaction' do - expect(Gitlab::Metrics).to receive(:current_transaction). + expect(described_class).to receive(:current_transaction). and_return(transaction) expect(transaction).to receive(:add_tag). with(:foo, 'bar') - Gitlab::Metrics.tag_transaction(:foo, 'bar') + described_class.tag_transaction(:foo, 'bar') end end end @@ -130,7 +130,7 @@ describe Gitlab::Metrics do expect_any_instance_of(Gitlab::Metrics::Transaction). not_to receive(:action=) - Gitlab::Metrics.action = 'foo' + described_class.action = 'foo' end end @@ -138,12 +138,12 @@ describe Gitlab::Metrics do it 'sets the action of a transaction' do trans = Gitlab::Metrics::Transaction.new - expect(Gitlab::Metrics).to receive(:current_transaction). + expect(described_class).to receive(:current_transaction). and_return(trans) expect(trans).to receive(:action=).with('foo') - Gitlab::Metrics.action = 'foo' + described_class.action = 'foo' end end end @@ -160,7 +160,7 @@ describe Gitlab::Metrics do expect_any_instance_of(Gitlab::Metrics::Transaction). not_to receive(:add_event) - Gitlab::Metrics.add_event(:meow) + described_class.add_event(:meow) end end @@ -170,10 +170,10 @@ describe Gitlab::Metrics do expect(transaction).to receive(:add_event).with(:meow) - expect(Gitlab::Metrics).to receive(:current_transaction). + expect(described_class).to receive(:current_transaction). and_return(transaction) - Gitlab::Metrics.add_event(:meow) + described_class.add_event(:meow) end end end diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index 127cd8c78d8..72e947f2cc2 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -45,8 +45,8 @@ describe Gitlab::Regex, lib: true do it { is_expected.not_to match('foo-') } end - describe 'FULL_NAMESPACE_REGEX_STR' do - subject { %r{\A#{Gitlab::Regex::FULL_NAMESPACE_REGEX_STR}\z} } + describe '.full_namespace_regex' do + subject { described_class.full_namespace_regex } it { is_expected.to match('gitlab.org') } it { is_expected.to match('gitlab.org/gitlab-git') } diff --git a/spec/lib/gitlab/request_profiler_spec.rb b/spec/lib/gitlab/request_profiler_spec.rb new file mode 100644 index 00000000000..ae9c06ebb7d --- /dev/null +++ b/spec/lib/gitlab/request_profiler_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Gitlab::RequestProfiler, lib: true do + describe '.profile_token' do + it 'returns a token' do + expect(described_class.profile_token).to be_present + end + + it 'caches the token' do + expect(Rails.cache).to receive(:fetch).with('profile-token') + + described_class.profile_token + end + end + + describe '.remove_all_profiles' do + it 'removes Gitlab::RequestProfiler::PROFILES_DIR directory' do + dir = described_class::PROFILES_DIR + FileUtils.mkdir_p(dir) + + expect(Dir.exist?(dir)).to be true + + described_class.remove_all_profiles + expect(Dir.exist?(dir)).to be false + end + end +end diff --git a/spec/lib/gitlab/backend/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb index 6675d26734e..6675d26734e 100644 --- a/spec/lib/gitlab/backend/shell_spec.rb +++ b/spec/lib/gitlab/shell_spec.rb diff --git a/spec/lib/gitlab/sidekiq_throttler_spec.rb b/spec/lib/gitlab/sidekiq_throttler_spec.rb index ff32e0e699d..6374ac80207 100644 --- a/spec/lib/gitlab/sidekiq_throttler_spec.rb +++ b/spec/lib/gitlab/sidekiq_throttler_spec.rb @@ -13,14 +13,14 @@ describe Gitlab::SidekiqThrottler do describe '#execute!' do it 'sets limits on the selected queues' do - Gitlab::SidekiqThrottler.execute! + described_class.execute! expect(Sidekiq::Queue['build'].limit).to eq 4 expect(Sidekiq::Queue['project_cache'].limit).to eq 4 end it 'does not set limits on other queues' do - Gitlab::SidekiqThrottler.execute! + described_class.execute! expect(Sidekiq::Queue['merge'].limit).to be_nil end diff --git a/spec/lib/gitlab/slash_commands/dsl_spec.rb b/spec/lib/gitlab/slash_commands/dsl_spec.rb index 26217a0e3b2..2763d950716 100644 --- a/spec/lib/gitlab/slash_commands/dsl_spec.rb +++ b/spec/lib/gitlab/slash_commands/dsl_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Gitlab::SlashCommands::Dsl do before :all do DummyClass = Struct.new(:project) do - include Gitlab::SlashCommands::Dsl + include Gitlab::SlashCommands::Dsl # rubocop:disable RSpec/DescribedClass desc 'A command with no args' command :no_args, :none do diff --git a/spec/lib/gitlab/template/gitignore_template_spec.rb b/spec/lib/gitlab/template/gitignore_template_spec.rb index 9750a012e22..97797f42aaa 100644 --- a/spec/lib/gitlab/template/gitignore_template_spec.rb +++ b/spec/lib/gitlab/template/gitignore_template_spec.rb @@ -24,7 +24,7 @@ describe Gitlab::Template::GitignoreTemplate do it 'returns the Gitignore object of a valid file' do ruby = subject.find('Ruby') - expect(ruby).to be_a Gitlab::Template::GitignoreTemplate + expect(ruby).to be_a described_class expect(ruby.name).to eq('Ruby') end end diff --git a/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb b/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb index e3b8321eda3..6541326d1de 100644 --- a/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb +++ b/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb @@ -25,7 +25,7 @@ describe Gitlab::Template::GitlabCiYmlTemplate do it 'returns the GitlabCiYml object of a valid file' do ruby = subject.find('Ruby') - expect(ruby).to be_a Gitlab::Template::GitlabCiYmlTemplate + expect(ruby).to be_a described_class expect(ruby.name).to eq('Ruby') end end diff --git a/spec/lib/gitlab/template/issue_template_spec.rb b/spec/lib/gitlab/template/issue_template_spec.rb index 9213ced7b19..329d1d74970 100644 --- a/spec/lib/gitlab/template/issue_template_spec.rb +++ b/spec/lib/gitlab/template/issue_template_spec.rb @@ -37,7 +37,7 @@ describe Gitlab::Template::IssueTemplate do it 'returns the issue object of a valid file' do ruby = subject.find('bug', project) - expect(ruby).to be_a Gitlab::Template::IssueTemplate + expect(ruby).to be_a described_class expect(ruby.name).to eq('bug') end end diff --git a/spec/lib/gitlab/template/merge_request_template_spec.rb b/spec/lib/gitlab/template/merge_request_template_spec.rb index 77dd3079e22..2b0056d9bab 100644 --- a/spec/lib/gitlab/template/merge_request_template_spec.rb +++ b/spec/lib/gitlab/template/merge_request_template_spec.rb @@ -37,7 +37,7 @@ describe Gitlab::Template::MergeRequestTemplate do it 'returns the merge request object of a valid file' do ruby = subject.find('bug', project) - expect(ruby).to be_a Gitlab::Template::MergeRequestTemplate + expect(ruby).to be_a described_class expect(ruby.name).to eq('bug') end end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 7f21288cf88..bf1dfe7f412 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::UsageData do let!(:board) { create(:board, project: project) } describe '#data' do - subject { Gitlab::UsageData.data } + subject { described_class.data } it "gathers usage data" do expect(subject.keys).to match_array(%i( @@ -58,7 +58,7 @@ describe Gitlab::UsageData do end describe '#license_usage_data' do - subject { Gitlab::UsageData.license_usage_data } + subject { described_class.license_usage_data } it "gathers license data" do expect(subject[:uuid]).to eq(current_application_settings.uuid) diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb index 611cdbbc865..2b27ff66c09 100644 --- a/spec/lib/gitlab/user_access_spec.rb +++ b/spec/lib/gitlab/user_access_spec.rb @@ -87,10 +87,10 @@ describe Gitlab::UserAccess, lib: true do expect(access.can_push_to_branch?(branch.name)).to be_falsey end - it 'returns true if branch does not exist and user has permission to merge' do + it 'returns false if branch does not exist' do project.team << [user, :developer] - expect(access.can_push_to_branch?(not_existing_branch.name)).to be_truthy + expect(access.can_push_to_branch?(not_existing_branch.name)).to be_falsey end end diff --git a/spec/lib/light_url_builder_spec.rb b/spec/lib/light_url_builder_spec.rb deleted file mode 100644 index 3fe8cf43934..00000000000 --- a/spec/lib/light_url_builder_spec.rb +++ /dev/null @@ -1,119 +0,0 @@ -require 'spec_helper' - -describe Gitlab::UrlBuilder, lib: true do - describe '.build' do - context 'when passing a Commit' do - it 'returns a proper URL' do - commit = build_stubbed(:commit) - - url = described_class.build(commit) - - expect(url).to eq "#{Settings.gitlab['url']}/#{commit.project.path_with_namespace}/commit/#{commit.id}" - end - end - - context 'when passing an Issue' do - it 'returns a proper URL' do - issue = build_stubbed(:issue, iid: 42) - - url = described_class.build(issue) - - expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}" - end - end - - context 'when passing a MergeRequest' do - it 'returns a proper URL' do - merge_request = build_stubbed(:merge_request, iid: 42) - - url = described_class.build(merge_request) - - expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}" - end - end - - context 'when passing a Note' do - context 'on a Commit' do - it 'returns a proper URL' do - note = build_stubbed(:note_on_commit) - - url = described_class.build(note) - - expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}" - end - end - - context 'on a Commit Diff' do - it 'returns a proper URL' do - note = build_stubbed(:diff_note_on_commit) - - url = described_class.build(note) - - expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}" - end - end - - context 'on an Issue' do - it 'returns a proper URL' do - issue = create(:issue, iid: 42) - note = build_stubbed(:note_on_issue, noteable: issue) - - url = described_class.build(note) - - expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}#note_#{note.id}" - end - end - - context 'on a MergeRequest' do - it 'returns a proper URL' do - merge_request = create(:merge_request, iid: 42) - note = build_stubbed(:note_on_merge_request, noteable: merge_request) - - url = described_class.build(note) - - expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}" - end - end - - context 'on a MergeRequest Diff' do - it 'returns a proper URL' do - merge_request = create(:merge_request, iid: 42) - note = build_stubbed(:diff_note_on_merge_request, noteable: merge_request) - - url = described_class.build(note) - - expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}" - end - end - - context 'on a ProjectSnippet' do - it 'returns a proper URL' do - project_snippet = create(:project_snippet) - note = build_stubbed(:note_on_project_snippet, noteable: project_snippet) - - url = described_class.build(note) - - expect(url).to eq "#{Settings.gitlab['url']}/#{project_snippet.project.path_with_namespace}/snippets/#{note.noteable_id}#note_#{note.id}" - end - end - - context 'on another object' do - it 'returns a proper URL' do - project = build_stubbed(:empty_project) - - expect { described_class.build(project) }. - to raise_error(NotImplementedError, 'No URL builder defined for Project') - end - end - end - - context 'when passing a WikiPage' do - it 'returns a proper URL' do - wiki_page = build(:wiki_page) - url = described_class.build(wiki_page) - - expect(url).to eq "#{Gitlab.config.gitlab.url}#{wiki_page.wiki.wiki_base_path}/#{wiki_page.slug}" - end - end - end -end diff --git a/spec/mailers/emails/merge_requests_spec.rb b/spec/mailers/emails/merge_requests_spec.rb index e22858d1d8f..2ad572bb5c7 100644 --- a/spec/mailers/emails/merge_requests_spec.rb +++ b/spec/mailers/emails/merge_requests_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' require 'email_spec' -describe Notify, "merge request notifications" do +describe Emails::MergeRequests do include EmailSpec::Matchers describe "#resolved_all_discussions_email" do diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb index 5ca936f28f0..8c1c9bf135f 100644 --- a/spec/mailers/emails/profile_spec.rb +++ b/spec/mailers/emails/profile_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' require 'email_spec' -describe Notify do +describe Emails::Profile do include EmailSpec::Matchers include_context 'gitlab email notification' @@ -15,106 +15,104 @@ describe Notify do end end - describe 'profile notifications' do - describe 'for new users, the email' do - let(:example_site_path) { root_path } - let(:new_user) { create(:user, email: new_user_address, created_by_id: 1) } - let(:token) { 'kETLwRaayvigPq_x3SNM' } + describe 'for new users, the email' do + let(:example_site_path) { root_path } + let(:new_user) { create(:user, email: new_user_address, created_by_id: 1) } + let(:token) { 'kETLwRaayvigPq_x3SNM' } - subject { Notify.new_user_email(new_user.id, token) } + subject { Notify.new_user_email(new_user.id, token) } - it_behaves_like 'an email sent from GitLab' - it_behaves_like 'a new user email' - it_behaves_like 'it should not have Gmail Actions links' - it_behaves_like 'a user cannot unsubscribe through footer link' + it_behaves_like 'an email sent from GitLab' + it_behaves_like 'a new user email' + it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like 'a user cannot unsubscribe through footer link' - it 'contains the password text' do - is_expected.to have_body_text /Click here to set your password/ - end + it 'contains the password text' do + is_expected.to have_body_text /Click here to set your password/ + end - it 'includes a link for user to set password' do - params = "reset_password_token=#{token}" - is_expected.to have_body_text( - %r{http://#{Gitlab.config.gitlab.host}(:\d+)?/users/password/edit\?#{params}} - ) - end + it 'includes a link for user to set password' do + params = "reset_password_token=#{token}" + is_expected.to have_body_text( + %r{http://#{Gitlab.config.gitlab.host}(:\d+)?/users/password/edit\?#{params}} + ) + end - it 'explains the reset link expiration' do - is_expected.to have_body_text(/This link is valid for \d+ (hours?|days?)/) - is_expected.to have_body_text(new_user_password_url) - is_expected.to have_body_text(/\?user_email=.*%40.*/) - end + it 'explains the reset link expiration' do + is_expected.to have_body_text(/This link is valid for \d+ (hours?|days?)/) + is_expected.to have_body_text(new_user_password_url) + is_expected.to have_body_text(/\?user_email=.*%40.*/) end + end - describe 'for users that signed up, the email' do - let(:example_site_path) { root_path } - let(:new_user) { create(:user, email: new_user_address, password: "securePassword") } + describe 'for users that signed up, the email' do + let(:example_site_path) { root_path } + let(:new_user) { create(:user, email: new_user_address, password: "securePassword") } - subject { Notify.new_user_email(new_user.id) } + subject { Notify.new_user_email(new_user.id) } - it_behaves_like 'an email sent from GitLab' - it_behaves_like 'a new user email' - it_behaves_like 'it should not have Gmail Actions links' - it_behaves_like 'a user cannot unsubscribe through footer link' + it_behaves_like 'an email sent from GitLab' + it_behaves_like 'a new user email' + it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like 'a user cannot unsubscribe through footer link' - it 'does not contain the new user\'s password' do - is_expected.not_to have_body_text /password/ - end + it 'does not contain the new user\'s password' do + is_expected.not_to have_body_text /password/ end + end - describe 'user added ssh key' do - let(:key) { create(:personal_key) } + describe 'user added ssh key' do + let(:key) { create(:personal_key) } - subject { Notify.new_ssh_key_email(key.id) } + subject { Notify.new_ssh_key_email(key.id) } - it_behaves_like 'an email sent from GitLab' - it_behaves_like 'it should not have Gmail Actions links' - it_behaves_like 'a user cannot unsubscribe through footer link' + it_behaves_like 'an email sent from GitLab' + it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like 'a user cannot unsubscribe through footer link' - it 'is sent to the new user' do - is_expected.to deliver_to key.user.email - end + it 'is sent to the new user' do + is_expected.to deliver_to key.user.email + end - it 'has the correct subject' do - is_expected.to have_subject /^SSH key was added to your account$/i - end + it 'has the correct subject' do + is_expected.to have_subject /^SSH key was added to your account$/i + end - it 'contains the new ssh key title' do - is_expected.to have_body_text /#{key.title}/ - end + it 'contains the new ssh key title' do + is_expected.to have_body_text /#{key.title}/ + end - it 'includes a link to ssh keys page' do - is_expected.to have_body_text /#{profile_keys_path}/ - end + it 'includes a link to ssh keys page' do + is_expected.to have_body_text /#{profile_keys_path}/ + end - context 'with SSH key that does not exist' do - it { expect { Notify.new_ssh_key_email('foo') }.not_to raise_error } - end + context 'with SSH key that does not exist' do + it { expect { Notify.new_ssh_key_email('foo') }.not_to raise_error } end + end - describe 'user added email' do - let(:email) { create(:email) } + describe 'user added email' do + let(:email) { create(:email) } - subject { Notify.new_email_email(email.id) } + subject { Notify.new_email_email(email.id) } - it_behaves_like 'it should not have Gmail Actions links' - it_behaves_like 'a user cannot unsubscribe through footer link' + it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like 'a user cannot unsubscribe through footer link' - it 'is sent to the new user' do - is_expected.to deliver_to email.user.email - end + it 'is sent to the new user' do + is_expected.to deliver_to email.user.email + end - it 'has the correct subject' do - is_expected.to have_subject /^Email was added to your account$/i - end + it 'has the correct subject' do + is_expected.to have_subject /^Email was added to your account$/i + end - it 'contains the new email address' do - is_expected.to have_body_text /#{email.email}/ - end + it 'contains the new email address' do + is_expected.to have_body_text /#{email.email}/ + end - it 'includes a link to emails page' do - is_expected.to have_body_text /#{profile_emails_path}/ - end + it 'includes a link to emails page' do + is_expected.to have_body_text /#{profile_emails_path}/ end end end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index e6f0a3b5920..9f12e40d808 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -40,7 +40,7 @@ describe Notify do let(:issue_with_description) { create(:issue, author: current_user, assignee: assignee, project: project, description: 'My awesome description') } describe 'that are new' do - subject { Notify.new_issue_email(issue.assignee_id, issue.id) } + subject { described_class.new_issue_email(issue.assignee_id, issue.id) } it_behaves_like 'an assignee email' it_behaves_like 'an email starting a new thread with reply-by-email enabled' do @@ -69,7 +69,7 @@ describe Notify do end describe 'that are new with a description' do - subject { Notify.new_issue_email(issue_with_description.assignee_id, issue_with_description.id) } + subject { described_class.new_issue_email(issue_with_description.assignee_id, issue_with_description.id) } it_behaves_like 'it should show Gmail Actions View Issue link' @@ -79,7 +79,7 @@ describe Notify do end describe 'that have been reassigned' do - subject { Notify.reassigned_issue_email(recipient.id, issue.id, previous_assignee.id, current_user.id) } + subject { described_class.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 with reply-by-email enabled' do @@ -105,7 +105,7 @@ describe Notify do end describe 'that have been relabeled' do - subject { Notify.relabeled_issue_email(recipient.id, issue.id, %w[foo bar baz], current_user.id) } + subject { described_class.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 with reply-by-email enabled' do @@ -132,7 +132,7 @@ describe Notify do describe 'status changed' do let(:status) { 'closed' } - subject { Notify.issue_status_changed_email(recipient.id, issue.id, status, current_user.id) } + subject { described_class.issue_status_changed_email(recipient.id, issue.id, status, current_user.id) } it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do let(:model) { issue } @@ -158,7 +158,7 @@ describe Notify do describe 'moved to another project' do let(:new_issue) { create(:issue) } - subject { Notify.issue_moved_email(recipient, issue, new_issue, current_user) } + subject { described_class.issue_moved_email(recipient, issue, new_issue, current_user) } it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do let(:model) { issue } @@ -190,7 +190,7 @@ describe Notify do let(:merge_request_with_description) { create(:merge_request, author: current_user, assignee: assignee, source_project: project, target_project: project, description: 'My awesome description') } describe 'that are new' do - subject { Notify.new_merge_request_email(merge_request.assignee_id, merge_request.id) } + subject { described_class.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 with reply-by-email enabled' do @@ -221,7 +221,7 @@ describe Notify do end describe 'that are new with a description' do - subject { Notify.new_merge_request_email(merge_request_with_description.assignee_id, merge_request_with_description.id) } + subject { described_class.new_merge_request_email(merge_request_with_description.assignee_id, merge_request_with_description.id) } it_behaves_like 'it should show Gmail Actions View Merge request link' it_behaves_like "an unsubscribeable thread" @@ -232,7 +232,7 @@ describe Notify do end describe 'that are reassigned' do - subject { Notify.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id) } + subject { described_class.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 with reply-by-email enabled' do @@ -258,7 +258,7 @@ describe Notify do end describe 'that have been relabeled' do - subject { Notify.relabeled_merge_request_email(recipient.id, merge_request.id, %w[foo bar baz], current_user.id) } + subject { described_class.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 with reply-by-email enabled' do @@ -283,7 +283,7 @@ describe Notify do describe 'status changed' do let(:status) { 'reopened' } - subject { Notify.merge_request_status_email(recipient.id, merge_request.id, status, current_user.id) } + subject { described_class.merge_request_status_email(recipient.id, merge_request.id, status, current_user.id) } it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do let(:model) { merge_request } @@ -308,7 +308,7 @@ describe Notify do end describe 'that are merged' do - subject { Notify.merged_merge_request_email(recipient.id, merge_request.id, merge_author.id) } + subject { described_class.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 with reply-by-email enabled' do @@ -337,7 +337,7 @@ describe Notify do describe 'project was moved' do let(:project) { create(:empty_project) } let(:user) { create(:user) } - subject { Notify.project_was_moved_email(project.id, user.id, "gitlab/gitlab") } + subject { described_class.project_was_moved_email(project.id, user.id, "gitlab/gitlab") } it_behaves_like 'an email sent from GitLab' it_behaves_like 'it should not have Gmail Actions links' @@ -363,7 +363,7 @@ describe Notify do project.request_access(user) project.requesters.find_by(user_id: user.id) end - subject { Notify.member_access_requested_email('project', project_member.id) } + subject { described_class.member_access_requested_email('project', project_member.id) } it_behaves_like 'an email sent from GitLab' it_behaves_like 'it should not have Gmail Actions links' @@ -390,7 +390,7 @@ describe Notify do project.request_access(user) project.requesters.find_by(user_id: user.id) end - subject { Notify.member_access_requested_email('project', project_member.id) } + subject { described_class.member_access_requested_email('project', project_member.id) } it_behaves_like 'an email sent from GitLab' it_behaves_like 'it should not have Gmail Actions links' @@ -416,7 +416,7 @@ describe Notify do project.request_access(user) project.requesters.find_by(user_id: user.id) end - subject { Notify.member_access_denied_email('project', project.id, user.id) } + subject { described_class.member_access_denied_email('project', project.id, user.id) } it_behaves_like 'an email sent from GitLab' it_behaves_like 'it should not have Gmail Actions links' @@ -434,7 +434,7 @@ describe Notify do let(:project) { create(:empty_project, :public, :access_requestable, namespace: owner.namespace) } let(:user) { create(:user) } let(:project_member) { create(:project_member, project: project, user: user) } - subject { Notify.member_access_granted_email('project', project_member.id) } + subject { described_class.member_access_granted_email('project', project_member.id) } it_behaves_like 'an email sent from GitLab' it_behaves_like 'it should not have Gmail Actions links' @@ -465,7 +465,7 @@ describe Notify do let(:master) { create(:user).tap { |u| project.team << [u, :master] } } let(:project_member) { invite_to_project(project, inviter: master) } - subject { Notify.member_invited_email('project', project_member.id, project_member.invite_token) } + subject { described_class.member_invited_email('project', project_member.id, project_member.invite_token) } it_behaves_like 'an email sent from GitLab' it_behaves_like 'it should not have Gmail Actions links' @@ -490,7 +490,7 @@ describe Notify do invitee end - subject { Notify.member_invite_accepted_email('project', project_member.id) } + subject { described_class.member_invite_accepted_email('project', project_member.id) } it_behaves_like 'an email sent from GitLab' it_behaves_like 'it should not have Gmail Actions links' @@ -514,7 +514,7 @@ describe Notify do invitee end - subject { Notify.member_invite_declined_email('project', project.id, project_member.invite_email, master.id) } + subject { described_class.member_invite_declined_email('project', project.id, project_member.invite_email, master.id) } it_behaves_like 'an email sent from GitLab' it_behaves_like 'it should not have Gmail Actions links' @@ -574,7 +574,7 @@ describe Notify do before(:each) { allow(note).to receive(:noteable).and_return(commit) } - subject { Notify.note_commit_email(recipient.id, note.id) } + subject { described_class.note_commit_email(recipient.id, note.id) } it_behaves_like 'a note email' it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do @@ -596,7 +596,7 @@ describe Notify do let(:note_on_merge_request_path) { namespace_project_merge_request_path(project.namespace, project, merge_request, anchor: "note_#{note.id}") } before(:each) { allow(note).to receive(:noteable).and_return(merge_request) } - subject { Notify.note_merge_request_email(recipient.id, note.id) } + subject { described_class.note_merge_request_email(recipient.id, note.id) } it_behaves_like 'a note email' it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do @@ -618,7 +618,7 @@ describe Notify do let(:note_on_issue_path) { namespace_project_issue_path(project.namespace, project, issue, anchor: "note_#{note.id}") } before(:each) { allow(note).to receive(:noteable).and_return(issue) } - subject { Notify.note_issue_email(recipient.id, note.id) } + subject { described_class.note_issue_email(recipient.id, note.id) } it_behaves_like 'a note email' it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do @@ -680,7 +680,7 @@ describe Notify do before(:each) { allow(note).to receive(:noteable).and_return(commit) } - subject { Notify.note_commit_email(recipient.id, note.id) } + subject { described_class.note_commit_email(recipient.id, note.id) } it_behaves_like 'a discussion note email', :discussion_note_on_commit it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do @@ -704,7 +704,7 @@ describe Notify do let(:note_on_merge_request_path) { namespace_project_merge_request_path(project.namespace, project, merge_request, anchor: "note_#{note.id}") } before(:each) { allow(note).to receive(:noteable).and_return(merge_request) } - subject { Notify.note_merge_request_email(recipient.id, note.id) } + subject { described_class.note_merge_request_email(recipient.id, note.id) } it_behaves_like 'a discussion note email', :discussion_note_on_merge_request it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do @@ -728,7 +728,7 @@ describe Notify do let(:note_on_issue_path) { namespace_project_issue_path(project.namespace, project, issue, anchor: "note_#{note.id}") } before(:each) { allow(note).to receive(:noteable).and_return(issue) } - subject { Notify.note_issue_email(recipient.id, note.id) } + subject { described_class.note_issue_email(recipient.id, note.id) } it_behaves_like 'a discussion note email', :discussion_note_on_issue it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do @@ -798,7 +798,7 @@ describe Notify do let(:commit) { project.commit } let(:note) { create(:diff_note_on_commit) } - subject { Notify.note_commit_email(recipient.id, note.id) } + subject { described_class.note_commit_email(recipient.id, note.id) } it_behaves_like 'an email for a note on a diff discussion', :diff_note_on_commit it_behaves_like 'it should show Gmail Actions View Commit link' @@ -809,7 +809,7 @@ describe Notify do let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } let(:note) { create(:diff_note_on_merge_request) } - subject { Notify.note_merge_request_email(recipient.id, note.id) } + subject { described_class.note_merge_request_email(recipient.id, note.id) } it_behaves_like 'an email for a note on a diff discussion', :diff_note_on_merge_request it_behaves_like 'it should show Gmail Actions View Merge request link' @@ -826,7 +826,7 @@ describe Notify do group.request_access(user) group.requesters.find_by(user_id: user.id) end - subject { Notify.member_access_requested_email('group', group_member.id) } + subject { described_class.member_access_requested_email('group', group_member.id) } it_behaves_like 'an email sent from GitLab' it_behaves_like 'it should not have Gmail Actions links' @@ -847,7 +847,7 @@ describe Notify do group.request_access(user) group.requesters.find_by(user_id: user.id) end - subject { Notify.member_access_denied_email('group', group.id, user.id) } + subject { described_class.member_access_denied_email('group', group.id, user.id) } it_behaves_like 'an email sent from GitLab' it_behaves_like 'it should not have Gmail Actions links' @@ -865,7 +865,7 @@ describe Notify do let(:user) { create(:user) } let(:group_member) { create(:group_member, group: group, user: user) } - subject { Notify.member_access_granted_email('group', group_member.id) } + subject { described_class.member_access_granted_email('group', group_member.id) } it_behaves_like 'an email sent from GitLab' it_behaves_like 'it should not have Gmail Actions links' @@ -896,7 +896,7 @@ describe Notify do let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } } let(:group_member) { invite_to_group(group, inviter: owner) } - subject { Notify.member_invited_email('group', group_member.id, group_member.invite_token) } + subject { described_class.member_invited_email('group', group_member.id, group_member.invite_token) } it_behaves_like 'an email sent from GitLab' it_behaves_like 'it should not have Gmail Actions links' @@ -921,7 +921,7 @@ describe Notify do invitee end - subject { Notify.member_invite_accepted_email('group', group_member.id) } + subject { described_class.member_invite_accepted_email('group', group_member.id) } it_behaves_like 'an email sent from GitLab' it_behaves_like 'it should not have Gmail Actions links' @@ -945,7 +945,7 @@ describe Notify do invitee end - subject { Notify.member_invite_declined_email('group', group.id, group_member.invite_email, owner.id) } + subject { described_class.member_invite_declined_email('group', group.id, group_member.invite_email, owner.id) } it_behaves_like 'an email sent from GitLab' it_behaves_like 'it should not have Gmail Actions links' @@ -994,7 +994,7 @@ describe Notify do let(:user) { create(:user) } let(:tree_path) { namespace_project_tree_path(project.namespace, project, "empty-branch") } - subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/empty-branch', action: :create) } + subject { described_class.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/empty-branch', action: :create) } it_behaves_like 'it should not have Gmail Actions links' it_behaves_like 'a user cannot unsubscribe through footer link' @@ -1020,7 +1020,7 @@ describe Notify do let(:user) { create(:user) } let(:tree_path) { namespace_project_tree_path(project.namespace, project, "v1.0") } - subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/tags/v1.0', action: :create) } + subject { described_class.repository_push_email(project.id, author_id: user.id, ref: 'refs/tags/v1.0', action: :create) } it_behaves_like 'it should not have Gmail Actions links' it_behaves_like "a user cannot unsubscribe through footer link" @@ -1045,7 +1045,7 @@ describe Notify do let(:example_site_path) { root_path } let(:user) { create(:user) } - subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :delete) } + subject { described_class.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :delete) } it_behaves_like 'it should not have Gmail Actions links' it_behaves_like 'a user cannot unsubscribe through footer link' @@ -1067,7 +1067,7 @@ describe Notify do let(:example_site_path) { root_path } let(:user) { create(:user) } - subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/tags/v1.0', action: :delete) } + subject { described_class.repository_push_email(project.id, author_id: user.id, ref: 'refs/tags/v1.0', action: :delete) } it_behaves_like 'it should not have Gmail Actions links' it_behaves_like 'a user cannot unsubscribe through footer link' @@ -1096,7 +1096,7 @@ describe Notify do let(:send_from_committer_email) { false } let(:diff_refs) { Gitlab::Diff::DiffRefs.new(base_sha: project.merge_base_commit(sample_image_commit.id, sample_commit.id).id, head_sha: sample_commit.id) } - subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, diff_refs: diff_refs, send_from_committer_email: send_from_committer_email) } + subject { described_class.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, diff_refs: diff_refs, send_from_committer_email: send_from_committer_email) } it_behaves_like 'it should not have Gmail Actions links' it_behaves_like 'a user cannot unsubscribe through footer link' @@ -1189,7 +1189,7 @@ describe Notify do let(:diff_path) { namespace_project_commit_path(project.namespace, project, commits.first) } let(:diff_refs) { Gitlab::Diff::DiffRefs.new(base_sha: project.merge_base_commit(sample_image_commit.id, sample_commit.id).id, head_sha: sample_commit.id) } - subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, diff_refs: diff_refs) } + subject { described_class.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, diff_refs: diff_refs) } it_behaves_like 'it should show Gmail Actions View Commit link' it_behaves_like 'a user cannot unsubscribe through footer link' @@ -1215,7 +1215,7 @@ describe Notify do describe 'HTML emails setting' do let(:project) { create(:empty_project) } let(:user) { create(:user) } - let(:multipart_mail) { Notify.project_was_moved_email(project.id, user.id, "gitlab/gitlab") } + let(:multipart_mail) { described_class.project_was_moved_email(project.id, user.id, "gitlab/gitlab") } context 'when disabled' do it 'only sends the text template' do diff --git a/spec/migrations/schema_spec.rb b/spec/migrations/active_record/schema_spec.rb index e132529d8d8..e132529d8d8 100644 --- a/spec/migrations/schema_spec.rb +++ b/spec/migrations/active_record/schema_spec.rb diff --git a/spec/models/ci/trigger_schedule_spec.rb b/spec/models/ci/trigger_schedule_spec.rb index 75d21541cee..92447564d7c 100644 --- a/spec/models/ci/trigger_schedule_spec.rb +++ b/spec/models/ci/trigger_schedule_spec.rb @@ -73,4 +73,36 @@ describe Ci::TriggerSchedule, models: true do end end end + + describe '#real_next_run' do + subject do + Ci::TriggerSchedule.last.real_next_run(worker_cron: worker_cron, + worker_time_zone: worker_time_zone) + end + + context 'when GitLab time_zone is UTC' do + before do + allow(Time).to receive(:zone) + .and_return(ActiveSupport::TimeZone[worker_time_zone]) + end + + let(:worker_time_zone) { 'UTC' } + + context 'when cron_timezone is Eastern Time (US & Canada)' do + before do + create(:ci_trigger_schedule, :nightly, + cron_timezone: 'Eastern Time (US & Canada)') + end + + let(:worker_cron) { '0 1 2 3 *' } + + it 'returns the next time worker executes' do + expect(subject.min).to eq(0) + expect(subject.hour).to eq(1) + expect(subject.day).to eq(2) + expect(subject.month).to eq(3) + end + end + end + end end diff --git a/spec/models/concerns/awardable_spec.rb b/spec/models/concerns/awardable_spec.rb index de791abdf3d..63ad3a3630b 100644 --- a/spec/models/concerns/awardable_spec.rb +++ b/spec/models/concerns/awardable_spec.rb @@ -1,10 +1,12 @@ require 'spec_helper' -describe Issue, "Awardable" do +describe Awardable do let!(:issue) { create(:issue) } let!(:award_emoji) { create(:award_emoji, :downvote, awardable: issue) } describe "Associations" do + subject { build(:issue) } + it { is_expected.to have_many(:award_emoji).dependent(:destroy) } end diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb index 4edafbc4e32..40bbb10eaac 100644 --- a/spec/models/concerns/cache_markdown_field_spec.rb +++ b/spec/models/concerns/cache_markdown_field_spec.rb @@ -170,6 +170,12 @@ describe CacheMarkdownField do is_expected.to be_truthy end + + it 'returns false if the markdown field is set but the html is not' do + thing.foo_html = nil + + is_expected.to be_falsy + end end describe '#refresh_markdown_cache!' do diff --git a/spec/models/concerns/discussion_on_diff_spec.rb b/spec/models/concerns/discussion_on_diff_spec.rb index 0002a00770f..8571e85627c 100644 --- a/spec/models/concerns/discussion_on_diff_spec.rb +++ b/spec/models/concerns/discussion_on_diff_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe DiffDiscussion, DiscussionOnDiff, model: true do +describe DiscussionOnDiff, model: true do subject { create(:diff_note_on_merge_request).to_discussion } describe "#truncated_diff_lines" do @@ -8,9 +8,9 @@ describe DiffDiscussion, DiscussionOnDiff, model: true do context "when diff is greater than allowed number of truncated diff lines " do it "returns fewer lines" do - expect(subject.diff_lines.count).to be > described_class::NUMBER_OF_TRUNCATED_DIFF_LINES + expect(subject.diff_lines.count).to be > DiffDiscussion::NUMBER_OF_TRUNCATED_DIFF_LINES - expect(truncated_lines.count).to be <= described_class::NUMBER_OF_TRUNCATED_DIFF_LINES + expect(truncated_lines.count).to be <= DiffDiscussion::NUMBER_OF_TRUNCATED_DIFF_LINES end end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 4522206fab1..3ecba2e9687 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -1,10 +1,13 @@ require 'spec_helper' -describe Issue, "Issuable" do +describe Issuable do + let(:issuable_class) { Issue } let(:issue) { create(:issue) } let(:user) { create(:user) } describe "Associations" do + subject { build(:issue) } + it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:author) } it { is_expected.to belong_to(:assignee) } @@ -23,10 +26,14 @@ describe Issue, "Issuable" do end describe 'Included modules' do + let(:described_class) { issuable_class } + it { is_expected.to include_module(Awardable) } end describe "Validation" do + subject { build(:issue) } + before do allow(subject).to receive(:set_iid).and_return(false) end @@ -39,9 +46,11 @@ describe Issue, "Issuable" do end describe "Scope" do - it { expect(described_class).to respond_to(:opened) } - it { expect(described_class).to respond_to(:closed) } - it { expect(described_class).to respond_to(:assigned) } + subject { build(:issue) } + + it { expect(issuable_class).to respond_to(:opened) } + it { expect(issuable_class).to respond_to(:closed) } + it { expect(issuable_class).to respond_to(:assigned) } end describe 'author_name' do @@ -115,16 +124,16 @@ describe Issue, "Issuable" do let!(:searchable_issue) { create(:issue, title: "Searchable issue") } it 'returns notes with a matching title' do - expect(described_class.search(searchable_issue.title)). + expect(issuable_class.search(searchable_issue.title)). to eq([searchable_issue]) end it 'returns notes with a partially matching title' do - expect(described_class.search('able')).to eq([searchable_issue]) + expect(issuable_class.search('able')).to eq([searchable_issue]) end it 'returns notes with a matching title regardless of the casing' do - expect(described_class.search(searchable_issue.title.upcase)). + expect(issuable_class.search(searchable_issue.title.upcase)). to eq([searchable_issue]) end end @@ -135,31 +144,31 @@ describe Issue, "Issuable" do end it 'returns notes with a matching title' do - expect(described_class.full_search(searchable_issue.title)). + expect(issuable_class.full_search(searchable_issue.title)). to eq([searchable_issue]) end it 'returns notes with a partially matching title' do - expect(described_class.full_search('able')).to eq([searchable_issue]) + expect(issuable_class.full_search('able')).to eq([searchable_issue]) end it 'returns notes with a matching title regardless of the casing' do - expect(described_class.full_search(searchable_issue.title.upcase)). + expect(issuable_class.full_search(searchable_issue.title.upcase)). to eq([searchable_issue]) end it 'returns notes with a matching description' do - expect(described_class.full_search(searchable_issue.description)). + expect(issuable_class.full_search(searchable_issue.description)). to eq([searchable_issue]) end it 'returns notes with a partially matching description' do - expect(described_class.full_search(searchable_issue.description)). + expect(issuable_class.full_search(searchable_issue.description)). to eq([searchable_issue]) end it 'returns notes with a matching description regardless of the casing' do - expect(described_class.full_search(searchable_issue.description.upcase)). + expect(issuable_class.full_search(searchable_issue.description.upcase)). to eq([searchable_issue]) end end diff --git a/spec/models/concerns/noteable_spec.rb b/spec/models/concerns/noteable_spec.rb index 92cc8859a8c..bdae742ff1d 100644 --- a/spec/models/concerns/noteable_spec.rb +++ b/spec/models/concerns/noteable_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe MergeRequest, Noteable, model: true do +describe Noteable, model: true do let!(:active_diff_note1) { create(:diff_note_on_merge_request) } let(:project) { active_diff_note1.project } subject { active_diff_note1.noteable } diff --git a/spec/models/concerns/relative_positioning_spec.rb b/spec/models/concerns/relative_positioning_spec.rb index 255b584a85e..494e6f1b6f6 100644 --- a/spec/models/concerns/relative_positioning_spec.rb +++ b/spec/models/concerns/relative_positioning_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Issue, 'RelativePositioning' do +describe RelativePositioning do let(:project) { create(:empty_project) } let(:issue) { create(:issue, project: project) } let(:issue1) { create(:issue, project: project) } diff --git a/spec/models/concerns/spammable_spec.rb b/spec/models/concerns/spammable_spec.rb index fd3b8307571..e698207166c 100644 --- a/spec/models/concerns/spammable_spec.rb +++ b/spec/models/concerns/spammable_spec.rb @@ -1,9 +1,11 @@ require 'spec_helper' -describe Issue, 'Spammable' do +describe Spammable do let(:issue) { create(:issue, description: 'Test Desc.') } describe 'Associations' do + subject { build(:issue) } + it { is_expected.to have_one(:user_agent_detail).dependent(:destroy) } end diff --git a/spec/models/concerns/strip_attribute_spec.rb b/spec/models/concerns/strip_attribute_spec.rb index c3af7a0960f..8c945686b66 100644 --- a/spec/models/concerns/strip_attribute_spec.rb +++ b/spec/models/concerns/strip_attribute_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Milestone, "StripAttribute" do +describe StripAttribute do let(:milestone) { create(:milestone) } describe ".strip_attributes" do diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 8ffde6f7fbb..a11805926cc 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -57,6 +57,32 @@ describe Group, models: true do it { is_expected.not_to validate_presence_of :owner } it { is_expected.to validate_presence_of :two_factor_grace_period } it { is_expected.to validate_numericality_of(:two_factor_grace_period).is_greater_than_or_equal_to(0) } + + describe 'path validation' do + it 'rejects paths reserved on the root namespace when the group has no parent' do + group = build(:group, path: 'api') + + expect(group).not_to be_valid + end + + it 'allows root paths when the group has a parent' do + group = build(:group, path: 'api', parent: create(:group)) + + expect(group).to be_valid + end + + it 'rejects any wildcard paths when not a top level group' do + group = build(:group, path: 'tree', parent: create(:group)) + + expect(group).not_to be_valid + end + + it 'rejects reserved group paths' do + group = build(:group, path: 'activity', parent: create(:group)) + + expect(group).not_to be_valid + end + end end describe '.visible_to_user' do diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index b0f3657d3b5..ccc3deac199 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -390,13 +390,15 @@ describe Member, models: true do %w[project group].each do |source_type| context "when source is a #{source_type}" do let!(:source) { create(source_type, :public, :access_requestable) } - let!(:user) { create(:user) } let!(:admin) { create(:admin) } + let(:user1) { create(:user) } + let(:user2) { create(:user) } it 'returns a <Source>Member objects' do - members = described_class.add_users(source, [user], :master) + members = described_class.add_users(source, [user1, user2], :master) expect(members).to be_a Array + expect(members.size).to eq(2) expect(members.first).to be_a "#{source_type.classify}Member".constantize expect(members.first).to be_persisted end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index e406d0a16bd..8624616316c 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -34,6 +34,13 @@ describe Namespace, models: true do let(:group) { build(:group, :nested, path: 'tree') } it { expect(group).not_to be_valid } + + it 'rejects nested paths' do + parent = create(:group, :nested, path: 'environments') + namespace = build(:project, path: 'folders', namespace: parent) + + expect(namespace).not_to be_valid + end end context 'top-level group' do @@ -47,6 +54,7 @@ describe Namespace, models: true do describe "Respond to" do it { is_expected.to respond_to(:human_name) } it { is_expected.to respond_to(:to_param) } + it { is_expected.to respond_to(:has_parent?) } end describe '#to_param' do diff --git a/spec/models/project_services/pipeline_email_service_spec.rb b/spec/models/project_services/pipelines_email_service_spec.rb index 03932895b0e..03932895b0e 100644 --- a/spec/models/project_services/pipeline_email_service_spec.rb +++ b/spec/models/project_services/pipelines_email_service_spec.rb diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 92d420337f9..36ce3070a6e 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -253,6 +253,34 @@ describe Project, models: true do expect(new_project.errors.full_messages.first).to eq('The project is still being deleted. Please try again later.') end end + + describe 'path validation' do + it 'allows paths reserved on the root namespace' do + project = build(:project, path: 'api') + + expect(project).to be_valid + end + + it 'rejects paths reserved on another level' do + project = build(:project, path: 'tree') + + expect(project).not_to be_valid + end + + it 'rejects nested paths' do + parent = create(:group, :nested, path: 'environments') + project = build(:project, path: 'folders', namespace: parent) + + expect(project).not_to be_valid + end + + it 'allows a reserved group name' do + parent = create(:group) + project = build(:project, path: 'avatar', namespace: parent) + + expect(project).to be_valid + end + end end describe 'default_scope' do @@ -781,17 +809,14 @@ describe Project, models: true do let(:project) { create(:empty_project) } - context 'When avatar file is uploaded' do - before do - project.update_columns(avatar: 'uploads/avatar.png') - allow(project.avatar).to receive(:present?) { true } - end + context 'when avatar file is uploaded' do + let(:project) { create(:empty_project, :with_avatar) } - let(:avatar_path) do - "/uploads/project/avatar/#{project.id}/uploads/avatar.png" - end + it 'creates a correct avatar path' do + avatar_path = "/uploads/project/avatar/#{project.id}/dk.png" - it { should eq "http://#{Gitlab.config.gitlab.host}#{avatar_path}" } + expect(project.avatar_url).to eq("http://#{Gitlab.config.gitlab.host}#{avatar_path}") + end end context 'When avatar file in git' do @@ -1881,4 +1906,23 @@ describe Project, models: true do expect(project.pipeline_status).to be_loaded end end + + describe '#append_or_update_attribute' do + let(:project) { create(:project) } + + it 'shows full error updating an invalid MR' do + error_message = 'Failed to replace merge_requests because one or more of the new records could not be saved.'\ + ' Validate fork Source project is not a fork of the target project' + + expect { project.append_or_update_attribute(:merge_requests, [create(:merge_request)]) }. + to raise_error(ActiveRecord::RecordNotSaved, error_message) + end + + it 'updates the project succesfully' do + merge_request = create(:merge_request, target_project: project, source_project: project) + + expect { project.append_or_update_attribute(:merge_requests, [merge_request]) }. + not_to raise_error + end + end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 98d0641443e..5216764a82d 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1379,12 +1379,22 @@ describe Repository, models: true do describe '#branch_count' do it 'returns the number of branches' do expect(repository.branch_count).to be_an(Integer) + + # NOTE: Until rugged goes away, make sure rugged and gitaly are in sync + rugged_count = repository.raw_repository.rugged.branches.count + + expect(repository.branch_count).to eq(rugged_count) end end describe '#tag_count' do it 'returns the number of tags' do expect(repository.tag_count).to be_an(Integer) + + # NOTE: Until rugged goes away, make sure rugged and gitaly are in sync + rugged_count = repository.raw_repository.rugged.tags.count + + expect(repository.tag_count).to eq(rugged_count) end end @@ -1849,17 +1859,15 @@ describe Repository, models: true do end end - # TODO: Uncomment when feature is reenabled - # describe '#is_ancestor?' do - # context 'Gitaly is_ancestor feature enabled' do - # it 'asks Gitaly server if it\'s an ancestor' do - # commit = repository.commit - # allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:is_ancestor).and_return(true) - # expect(Gitlab::GitalyClient::Commit).to receive(:is_ancestor). - # with(repository.raw_repository, commit.id, commit.id).and_return(true) - # - # expect(repository.is_ancestor?(commit.id, commit.id)).to be true - # end - # end - # end + describe '#is_ancestor?' do + context 'Gitaly is_ancestor feature enabled' do + it "asks Gitaly server if it's an ancestor" do + commit = repository.commit + expect(repository.raw_repository).to receive(:is_ancestor?).and_call_original + allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:is_ancestor).and_return(true) + + expect(repository.is_ancestor?(commit.id, commit.id)).to be true + end + end + end end diff --git a/spec/models/snippet_blob_spec.rb b/spec/models/snippet_blob_spec.rb new file mode 100644 index 00000000000..120b390586b --- /dev/null +++ b/spec/models/snippet_blob_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe SnippetBlob, models: true do + let(:snippet) { create(:snippet) } + + subject { described_class.new(snippet) } + + describe '#id' do + it 'returns the snippet ID' do + expect(subject.id).to eq(snippet.id) + end + end + + describe '#name' do + it 'returns the snippet file name' do + expect(subject.name).to eq(snippet.file_name) + end + end + + describe '#size' do + it 'returns the data size' do + expect(subject.size).to eq(subject.data.bytesize) + end + end + + describe '#data' do + it 'returns the snippet content' do + expect(subject.data).to eq(snippet.content) + end + end + + describe '#rendered_markup' do + context 'when the content is GFM' do + let(:snippet) { create(:snippet, file_name: 'file.md') } + + it 'returns the rendered GFM' do + expect(subject.rendered_markup).to eq(snippet.content_html) + end + end + + context 'when the content is not GFM' do + it 'returns nil' do + expect(subject.rendered_markup).to be_nil + end + end + end +end diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb index 8095d01b69e..75b1fc7e216 100644 --- a/spec/models/snippet_spec.rb +++ b/spec/models/snippet_spec.rb @@ -5,7 +5,6 @@ describe Snippet, models: true do subject { described_class } it { is_expected.to include_module(Gitlab::VisibilityLevel) } - it { is_expected.to include_module(Linguist::BlobHelper) } it { is_expected.to include_module(Participable) } it { is_expected.to include_module(Referable) } it { is_expected.to include_module(Sortable) } @@ -241,4 +240,16 @@ describe Snippet, models: true do end end end + + describe '#blob' do + let(:snippet) { create(:snippet) } + + it 'returns a blob representing the snippet data' do + blob = snippet.blob + + expect(blob).to be_a(Blob) + expect(blob.path).to eq(snippet.file_name) + expect(blob.data).to eq(snippet.content) + end + end end diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb index 581305ad39f..3f80e1ac534 100644 --- a/spec/models/todo_spec.rb +++ b/spec/models/todo_spec.rb @@ -125,4 +125,50 @@ describe Todo, models: true do expect(subject.target_reference).to eq issue.to_reference(full: true) end end + + describe '#self_added?' do + let(:user_1) { build(:user) } + + before do + subject.user = user_1 + end + + it 'is true when the user is the author' do + subject.author = user_1 + + expect(subject).to be_self_added + end + + it 'is false when the user is not the author' do + subject.author = build(:user) + + expect(subject).not_to be_self_added + end + end + + describe '#self_assigned?' do + let(:user_1) { build(:user) } + + before do + subject.user = user_1 + subject.author = user_1 + subject.action = Todo::ASSIGNED + end + + it 'is true when todo is ASSIGNED and self_added' do + expect(subject).to be_self_assigned + end + + it 'is false when the todo is not ASSIGNED' do + subject.action = Todo::MENTIONED + + expect(subject).not_to be_self_assigned + end + + it 'is false when todo is not self_added' do + subject.author = build(:user) + + expect(subject).not_to be_self_assigned + end + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 0bcebc27598..1c2df4c9d97 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -97,6 +97,18 @@ describe User, models: true do expect(user.errors.values).to eq [['dashboard is a reserved name']] end + it 'allows child names' do + user = build(:user, username: 'avatar') + + expect(user).to be_valid + end + + it 'allows wildcard names' do + user = build(:user, username: 'blob') + + expect(user).to be_valid + end + it 'validates uniqueness' do expect(subject).to validate_uniqueness_of(:username).case_insensitive end diff --git a/spec/policies/issue_policy_spec.rb b/spec/policies/issue_policy_spec.rb index 2905d5b26a5..9a870b7fda1 100644 --- a/spec/policies/issue_policy_spec.rb +++ b/spec/policies/issue_policy_spec.rb @@ -1,118 +1,192 @@ require 'spec_helper' describe IssuePolicy, models: true do - let(:user) { create(:user) } - - describe '#rules' do - context 'using a regular issue' do - let(:project) { create(:empty_project, :public) } - let(:issue) { create(:issue, project: project) } - let(:policies) { described_class.abilities(user, issue).to_set } - - context 'with a regular user' do - it 'includes the read_issue permission' do - expect(policies).to include(:read_issue) - end - - it 'does not include the admin_issue permission' do - expect(policies).not_to include(:admin_issue) - end - - it 'does not include the update_issue permission' do - expect(policies).not_to include(:update_issue) - end - end + let(:guest) { create(:user) } + let(:author) { create(:user) } + let(:assignee) { create(:user) } + let(:reporter) { create(:user) } + let(:group) { create(:group, :public) } + let(:reporter_from_group_link) { create(:user) } + + def permissions(user, issue) + described_class.abilities(user, issue).to_set + end + + context 'a private project' do + let(:non_member) { create(:user) } + let(:project) { create(:empty_project, :private) } + let(:issue) { create(:issue, project: project, assignee: assignee, author: author) } + let(:issue_no_assignee) { create(:issue, project: project) } + + before do + project.team << [guest, :guest] + project.team << [author, :guest] + project.team << [assignee, :guest] + project.team << [reporter, :reporter] + + group.add_reporter(reporter_from_group_link) + + create(:project_group_link, group: group, project: project) + end + + it 'does not allow non-members to read issues' do + expect(permissions(non_member, issue)).not_to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(non_member, issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows guests to read issues' do + expect(permissions(guest, issue)).to include(:read_issue) + expect(permissions(guest, issue)).not_to include(:update_issue, :admin_issue) + + expect(permissions(guest, issue_no_assignee)).to include(:read_issue) + expect(permissions(guest, issue_no_assignee)).not_to include(:update_issue, :admin_issue) + end + + it 'allows reporters to read, update, and admin issues' do + expect(permissions(reporter, issue)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows reporters from group links to read, update, and admin issues' do + expect(permissions(reporter_from_group_link, issue)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows issue authors to read and update their issues' do + expect(permissions(author, issue)).to include(:read_issue, :update_issue) + expect(permissions(author, issue)).not_to include(:admin_issue) + + expect(permissions(author, issue_no_assignee)).to include(:read_issue) + expect(permissions(author, issue_no_assignee)).not_to include(:update_issue, :admin_issue) + end + + it 'allows issue assignees to read and update their issues' do + expect(permissions(assignee, issue)).to include(:read_issue, :update_issue) + expect(permissions(assignee, issue)).not_to include(:admin_issue) + + expect(permissions(assignee, issue_no_assignee)).to include(:read_issue) + expect(permissions(assignee, issue_no_assignee)).not_to include(:update_issue, :admin_issue) + end - context 'with a user that is a project reporter' do - before do - project.team << [user, :reporter] - end + context 'with confidential issues' do + let(:confidential_issue) { create(:issue, :confidential, project: project, assignee: assignee, author: author) } + let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) } - it 'includes the read_issue permission' do - expect(policies).to include(:read_issue) - end + it 'does not allow non-members to read confidential issues' do + expect(permissions(non_member, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(non_member, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + end + + it 'does not allow guests to read confidential issues' do + expect(permissions(guest, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(guest, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + end - it 'includes the admin_issue permission' do - expect(policies).to include(:admin_issue) - end + it 'allows reporters to read, update, and admin confidential issues' do + expect(permissions(reporter, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + end - it 'includes the update_issue permission' do - expect(policies).to include(:update_issue) - end + it 'allows reporters from group links to read, update, and admin confidential issues' do + expect(permissions(reporter_from_group_link, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) end - context 'with a user that is a project guest' do - before do - project.team << [user, :guest] - end + it 'allows issue authors to read and update their confidential issues' do + expect(permissions(author, confidential_issue)).to include(:read_issue, :update_issue) + expect(permissions(author, confidential_issue)).not_to include(:admin_issue) - it 'includes the read_issue permission' do - expect(policies).to include(:read_issue) - end + expect(permissions(author, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + end - it 'does not include the admin_issue permission' do - expect(policies).not_to include(:admin_issue) - end + it 'allows issue assignees to read and update their confidential issues' do + expect(permissions(assignee, confidential_issue)).to include(:read_issue, :update_issue) + expect(permissions(assignee, confidential_issue)).not_to include(:admin_issue) - it 'does not include the update_issue permission' do - expect(policies).not_to include(:update_issue) - end + expect(permissions(assignee, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) end end + end - context 'using a confidential issue' do - let(:issue) { create(:issue, :confidential) } + context 'a public project' do + let(:project) { create(:empty_project, :public) } + let(:issue) { create(:issue, project: project, assignee: assignee, author: author) } + let(:issue_no_assignee) { create(:issue, project: project) } - context 'with a regular user' do - let(:policies) { described_class.abilities(user, issue).to_set } + before do + project.team << [guest, :guest] + project.team << [reporter, :reporter] - it 'does not include the read_issue permission' do - expect(policies).not_to include(:read_issue) - end + group.add_reporter(reporter_from_group_link) - it 'does not include the admin_issue permission' do - expect(policies).not_to include(:admin_issue) - end + create(:project_group_link, group: group, project: project) + end - it 'does not include the update_issue permission' do - expect(policies).not_to include(:update_issue) - end - end + it 'allows guests to read issues' do + expect(permissions(guest, issue)).to include(:read_issue) + expect(permissions(guest, issue)).not_to include(:update_issue, :admin_issue) + + expect(permissions(guest, issue_no_assignee)).to include(:read_issue) + expect(permissions(guest, issue_no_assignee)).not_to include(:update_issue, :admin_issue) + end + + it 'allows reporters to read, update, and admin issues' do + expect(permissions(reporter, issue)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows reporters from group links to read, update, and admin issues' do + expect(permissions(reporter_from_group_link, issue)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + end - context 'with a user that is a project member' do - let(:policies) { described_class.abilities(user, issue).to_set } + it 'allows issue authors to read and update their issues' do + expect(permissions(author, issue)).to include(:read_issue, :update_issue) + expect(permissions(author, issue)).not_to include(:admin_issue) - before do - issue.project.team << [user, :reporter] - end + expect(permissions(author, issue_no_assignee)).to include(:read_issue) + expect(permissions(author, issue_no_assignee)).not_to include(:update_issue, :admin_issue) + end + + it 'allows issue assignees to read and update their issues' do + expect(permissions(assignee, issue)).to include(:read_issue, :update_issue) + expect(permissions(assignee, issue)).not_to include(:admin_issue) - it 'includes the read_issue permission' do - expect(policies).to include(:read_issue) - end + expect(permissions(assignee, issue_no_assignee)).to include(:read_issue) + expect(permissions(assignee, issue_no_assignee)).not_to include(:update_issue, :admin_issue) + end - it 'includes the admin_issue permission' do - expect(policies).to include(:admin_issue) - end + context 'with confidential issues' do + let(:confidential_issue) { create(:issue, :confidential, project: project, assignee: assignee, author: author) } + let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) } - it 'includes the update_issue permission' do - expect(policies).to include(:update_issue) - end + it 'does not allow guests to read confidential issues' do + expect(permissions(guest, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(guest, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) end - context 'without a user' do - let(:policies) { described_class.abilities(nil, issue).to_set } + it 'allows reporters to read, update, and admin confidential issues' do + expect(permissions(reporter, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows reporter from group links to read, update, and admin confidential issues' do + expect(permissions(reporter_from_group_link, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + end - it 'does not include the read_issue permission' do - expect(policies).not_to include(:read_issue) - end + it 'allows issue authors to read and update their confidential issues' do + expect(permissions(author, confidential_issue)).to include(:read_issue, :update_issue) + expect(permissions(author, confidential_issue)).not_to include(:admin_issue) + + expect(permissions(author, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + end - it 'does not include the admin_issue permission' do - expect(policies).not_to include(:admin_issue) - end + it 'allows issue assignees to read and update their confidential issues' do + expect(permissions(assignee, confidential_issue)).to include(:read_issue, :update_issue) + expect(permissions(assignee, confidential_issue)).not_to include(:admin_issue) - it 'does not include the update_issue permission' do - expect(policies).not_to include(:update_issue) - end + expect(permissions(assignee, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) end end end diff --git a/spec/policies/issues_policy_spec.rb b/spec/policies/issues_policy_spec.rb deleted file mode 100644 index 2b7b6cad654..00000000000 --- a/spec/policies/issues_policy_spec.rb +++ /dev/null @@ -1,193 +0,0 @@ -require 'spec_helper' - -describe IssuePolicy, models: true do - let(:guest) { create(:user) } - let(:author) { create(:user) } - let(:assignee) { create(:user) } - let(:reporter) { create(:user) } - let(:group) { create(:group, :public) } - let(:reporter_from_group_link) { create(:user) } - - def permissions(user, issue) - IssuePolicy.abilities(user, issue).to_set - end - - context 'a private project' do - let(:non_member) { create(:user) } - let(:project) { create(:empty_project, :private) } - let(:issue) { create(:issue, project: project, assignee: assignee, author: author) } - let(:issue_no_assignee) { create(:issue, project: project) } - - before do - project.team << [guest, :guest] - project.team << [author, :guest] - project.team << [assignee, :guest] - project.team << [reporter, :reporter] - - group.add_reporter(reporter_from_group_link) - - create(:project_group_link, group: group, project: project) - end - - it 'does not allow non-members to read issues' do - expect(permissions(non_member, issue)).not_to include(:read_issue, :update_issue, :admin_issue) - expect(permissions(non_member, issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) - end - - it 'allows guests to read issues' do - expect(permissions(guest, issue)).to include(:read_issue) - expect(permissions(guest, issue)).not_to include(:update_issue, :admin_issue) - - expect(permissions(guest, issue_no_assignee)).to include(:read_issue) - expect(permissions(guest, issue_no_assignee)).not_to include(:update_issue, :admin_issue) - end - - it 'allows reporters to read, update, and admin issues' do - expect(permissions(reporter, issue)).to include(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) - end - - it 'allows reporters from group links to read, update, and admin issues' do - expect(permissions(reporter_from_group_link, issue)).to include(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter_from_group_link, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) - end - - it 'allows issue authors to read and update their issues' do - expect(permissions(author, issue)).to include(:read_issue, :update_issue) - expect(permissions(author, issue)).not_to include(:admin_issue) - - expect(permissions(author, issue_no_assignee)).to include(:read_issue) - expect(permissions(author, issue_no_assignee)).not_to include(:update_issue, :admin_issue) - end - - it 'allows issue assignees to read and update their issues' do - expect(permissions(assignee, issue)).to include(:read_issue, :update_issue) - expect(permissions(assignee, issue)).not_to include(:admin_issue) - - expect(permissions(assignee, issue_no_assignee)).to include(:read_issue) - expect(permissions(assignee, issue_no_assignee)).not_to include(:update_issue, :admin_issue) - end - - context 'with confidential issues' do - let(:confidential_issue) { create(:issue, :confidential, project: project, assignee: assignee, author: author) } - let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) } - - it 'does not allow non-members to read confidential issues' do - expect(permissions(non_member, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue) - expect(permissions(non_member, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) - end - - it 'does not allow guests to read confidential issues' do - expect(permissions(guest, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue) - expect(permissions(guest, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) - end - - it 'allows reporters to read, update, and admin confidential issues' do - expect(permissions(reporter, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) - end - - it 'allows reporters from group links to read, update, and admin confidential issues' do - expect(permissions(reporter_from_group_link, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) - end - - it 'allows issue authors to read and update their confidential issues' do - expect(permissions(author, confidential_issue)).to include(:read_issue, :update_issue) - expect(permissions(author, confidential_issue)).not_to include(:admin_issue) - - expect(permissions(author, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) - end - - it 'allows issue assignees to read and update their confidential issues' do - expect(permissions(assignee, confidential_issue)).to include(:read_issue, :update_issue) - expect(permissions(assignee, confidential_issue)).not_to include(:admin_issue) - - expect(permissions(assignee, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) - end - end - end - - context 'a public project' do - let(:project) { create(:empty_project, :public) } - let(:issue) { create(:issue, project: project, assignee: assignee, author: author) } - let(:issue_no_assignee) { create(:issue, project: project) } - - before do - project.team << [guest, :guest] - project.team << [reporter, :reporter] - - group.add_reporter(reporter_from_group_link) - - create(:project_group_link, group: group, project: project) - end - - it 'allows guests to read issues' do - expect(permissions(guest, issue)).to include(:read_issue) - expect(permissions(guest, issue)).not_to include(:update_issue, :admin_issue) - - expect(permissions(guest, issue_no_assignee)).to include(:read_issue) - expect(permissions(guest, issue_no_assignee)).not_to include(:update_issue, :admin_issue) - end - - it 'allows reporters to read, update, and admin issues' do - expect(permissions(reporter, issue)).to include(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) - end - - it 'allows reporters from group links to read, update, and admin issues' do - expect(permissions(reporter_from_group_link, issue)).to include(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter_from_group_link, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) - end - - it 'allows issue authors to read and update their issues' do - expect(permissions(author, issue)).to include(:read_issue, :update_issue) - expect(permissions(author, issue)).not_to include(:admin_issue) - - expect(permissions(author, issue_no_assignee)).to include(:read_issue) - expect(permissions(author, issue_no_assignee)).not_to include(:update_issue, :admin_issue) - end - - it 'allows issue assignees to read and update their issues' do - expect(permissions(assignee, issue)).to include(:read_issue, :update_issue) - expect(permissions(assignee, issue)).not_to include(:admin_issue) - - expect(permissions(assignee, issue_no_assignee)).to include(:read_issue) - expect(permissions(assignee, issue_no_assignee)).not_to include(:update_issue, :admin_issue) - end - - context 'with confidential issues' do - let(:confidential_issue) { create(:issue, :confidential, project: project, assignee: assignee, author: author) } - let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) } - - it 'does not allow guests to read confidential issues' do - expect(permissions(guest, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue) - expect(permissions(guest, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) - end - - it 'allows reporters to read, update, and admin confidential issues' do - expect(permissions(reporter, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) - end - - it 'allows reporter from group links to read, update, and admin confidential issues' do - expect(permissions(reporter_from_group_link, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) - end - - it 'allows issue authors to read and update their confidential issues' do - expect(permissions(author, confidential_issue)).to include(:read_issue, :update_issue) - expect(permissions(author, confidential_issue)).not_to include(:admin_issue) - - expect(permissions(author, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) - end - - it 'allows issue assignees to read and update their confidential issues' do - expect(permissions(assignee, confidential_issue)).to include(:read_issue, :update_issue) - expect(permissions(assignee, confidential_issue)).not_to include(:admin_issue) - - expect(permissions(assignee, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) - end - end - end -end diff --git a/spec/requests/api/doorkeeper_access_spec.rb b/spec/requests/api/doorkeeper_access_spec.rb index b5897b2e346..868fef65c1c 100644 --- a/spec/requests/api/doorkeeper_access_spec.rb +++ b/spec/requests/api/doorkeeper_access_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::API do +describe 'doorkeeper access' do let!(:user) { create(:user) } let!(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) } let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "api" } diff --git a/spec/requests/api/api_internal_helpers_spec.rb b/spec/requests/api/helpers/internal_helpers_spec.rb index f5265ea60ff..db716b340f1 100644 --- a/spec/requests/api/api_internal_helpers_spec.rb +++ b/spec/requests/api/helpers/internal_helpers_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe ::API::Helpers::InternalHelpers do - include ::API::Helpers::InternalHelpers + include described_class describe '.clean_project_path' do project = 'namespace/project' diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb index 06c8eb1d0b7..ed392acc607 100644 --- a/spec/requests/api/helpers_spec.rb +++ b/spec/requests/api/helpers_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe API::Helpers do include API::APIGuard::HelperMethods - include API::Helpers + include described_class include SentryHelper let(:user) { create(:user) } diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index c4bff1647b5..16e5efb2f5b 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -434,6 +434,19 @@ describe API::MergeRequests do expect(json_response['title']).to eq('Test merge_request') end + it 'returns 422 when target project has disabled merge requests' do + project.project_feature.update(merge_requests_access_level: 0) + + post api("/projects/#{fork_project.id}/merge_requests", user2), + title: 'Test', + target_branch: 'master', + source_branch: 'markdown', + author: user2, + target_project_id: project.id + + expect(response).to have_http_status(422) + end + it "returns 400 when source_branch is missing" do post api("/projects/#{fork_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id diff --git a/spec/requests/api/oauth_tokens_spec.rb b/spec/requests/api/oauth_tokens_spec.rb index 819df105960..0d56e1f732e 100644 --- a/spec/requests/api/oauth_tokens_spec.rb +++ b/spec/requests/api/oauth_tokens_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::API do +describe 'OAuth tokens' do context 'Resource Owner Password Credentials' do def request_oauth_token(user) post '/oauth/token', username: user.username, password: user.password, grant_type: 'password' diff --git a/spec/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb index 6c2950a6e6f..f6ff96be566 100644 --- a/spec/requests/api/v3/merge_requests_spec.rb +++ b/spec/requests/api/v3/merge_requests_spec.rb @@ -338,6 +338,19 @@ describe API::MergeRequests do expect(json_response['title']).to eq('Test merge_request') end + it "returns 422 when target project has disabled merge requests" do + project.project_feature.update(merge_requests_access_level: 0) + + post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + title: 'Test', + target_branch: "master", + source_branch: 'markdown', + author: user2, + target_project_id: project.id + + expect(response).to have_http_status(422) + end + it "returns 400 when source_branch is missing" do post v3_api("/projects/#{fork_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id diff --git a/spec/requests/request_profiler_spec.rb b/spec/requests/request_profiler_spec.rb new file mode 100644 index 00000000000..51fbfecec4b --- /dev/null +++ b/spec/requests/request_profiler_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe 'Request Profiler' do + let(:user) { create(:user) } + + shared_examples 'profiling a request' do + before do + allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new) + allow(RubyProf::Profile).to receive(:profile) do |&blk| + blk.call + RubyProf::Profile.new + end + end + + it 'creates a profile of the request' do + project = create(:project, namespace: user.namespace) + time = Time.now + path = "/#{project.path_with_namespace}" + + Timecop.freeze(time) do + get path, nil, 'X-Profile-Token' => Gitlab::RequestProfiler.profile_token + end + + profile_path = "#{Gitlab.config.shared.path}/tmp/requests_profiles/#{path.tr('/', '|')}_#{time.to_i}.html" + expect(File.exist?(profile_path)).to be true + end + + after do + Gitlab::RequestProfiler.remove_all_profiles + end + end + + context "when user is logged-in" do + before do + login_as(user) + end + + include_examples 'profiling a request' + end + + context "when user is not logged-in" do + include_examples 'profiling a request' + end +end diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb index 99c44bde151..e5fc0b676af 100644 --- a/spec/routing/admin_routing_spec.rb +++ b/spec/routing/admin_routing_spec.rb @@ -71,13 +71,15 @@ describe Admin::ProjectsController, "routing" do end end -# admin_hook_test GET /admin/hooks/:hook_id/test(.:format) admin/hooks#test +# admin_hook_test GET /admin/hooks/:id/test(.:format) admin/hooks#test # admin_hooks GET /admin/hooks(.:format) admin/hooks#index # POST /admin/hooks(.:format) admin/hooks#create # admin_hook DELETE /admin/hooks/:id(.:format) admin/hooks#destroy +# PUT /admin/hooks/:id(.:format) admin/hooks#update +# edit_admin_hook GET /admin/hooks/:id(.:format) admin/hooks#edit describe Admin::HooksController, "routing" do it "to #test" do - expect(get("/admin/hooks/1/test")).to route_to('admin/hooks#test', hook_id: '1') + expect(get("/admin/hooks/1/test")).to route_to('admin/hooks#test', id: '1') end it "to #index" do @@ -88,6 +90,14 @@ describe Admin::HooksController, "routing" do expect(post("/admin/hooks")).to route_to('admin/hooks#create') end + it "to #edit" do + expect(get("/admin/hooks/1/edit")).to route_to('admin/hooks#edit', id: '1') + end + + it "to #update" do + expect(put("/admin/hooks/1")).to route_to('admin/hooks#update', id: '1') + end + it "to #destroy" do expect(delete("/admin/hooks/1")).to route_to('admin/hooks#destroy', id: '1') end diff --git a/spec/routing/environments_spec.rb b/spec/routing/environments_spec.rb index ba124de70bb..624f3c43f0a 100644 --- a/spec/routing/environments_spec.rb +++ b/spec/routing/environments_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Projects::EnvironmentsController, :routing do +describe 'environments routing', :routing do let(:project) { create(:empty_project) } let(:environment) do diff --git a/spec/routing/notifications_routing_spec.rb b/spec/routing/notifications_routing_spec.rb index 24592942a96..54ed87b5520 100644 --- a/spec/routing/notifications_routing_spec.rb +++ b/spec/routing/notifications_routing_spec.rb @@ -1,13 +1,11 @@ require "spec_helper" -describe Profiles::NotificationsController do - describe "routing" do - it "routes to #show" do - expect(get("/profile/notifications")).to route_to("profiles/notifications#show") - end +describe "notifications routing" do + it "routes to #show" do + expect(get("/profile/notifications")).to route_to("profiles/notifications#show") + end - it "routes to #update" do - expect(put("/profile/notifications")).to route_to("profiles/notifications#update") - end + it "routes to #update" do + expect(put("/profile/notifications")).to route_to("profiles/notifications#update") end end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index a3de022d242..163df072cf6 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -340,14 +340,16 @@ describe 'project routing' do # test_project_hook GET /:project_id/hooks/:id/test(.:format) hooks#test # project_hooks GET /:project_id/hooks(.:format) hooks#index # POST /:project_id/hooks(.:format) hooks#create - # project_hook DELETE /:project_id/hooks/:id(.:format) hooks#destroy + # edit_project_hook GET /:project_id/hooks/:id/edit(.:format) hooks#edit + # project_hook PUT /:project_id/hooks/:id(.:format) hooks#update + # DELETE /:project_id/hooks/:id(.:format) hooks#destroy describe Projects::HooksController, 'routing' do it 'to #test' do expect(get('/gitlab/gitlabhq/hooks/1/test')).to route_to('projects/hooks#test', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') end it_behaves_like 'RESTful project resources' do - let(:actions) { [:index, :create, :destroy] } + let(:actions) { [:index, :create, :destroy, :edit, :update] } let(:controller) { 'hooks' } end end diff --git a/spec/rubocop/cop/migration/add_column_with_default_to_large_table_spec.rb b/spec/rubocop/cop/migration/add_column_with_default_to_large_table_spec.rb new file mode 100644 index 00000000000..07cb3fc4a2e --- /dev/null +++ b/spec/rubocop/cop/migration/add_column_with_default_to_large_table_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +require 'rubocop' +require 'rubocop/rspec/support' + +require_relative '../../../../rubocop/cop/migration/add_column_with_default_to_large_table' + +describe RuboCop::Cop::Migration::AddColumnWithDefaultToLargeTable do + include CopHelper + + subject(:cop) { described_class.new } + + context 'in migration' do + before do + allow(cop).to receive(:in_migration?).and_return(true) + end + + described_class::LARGE_TABLES.each do |table| + it "registers an offense for the #{table} table" do + inspect_source(cop, "add_column_with_default :#{table}, :column, default: true") + + aggregate_failures do + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.map(&:line)).to eq([1]) + end + end + end + + it 'registers no offense for non-blacklisted tables' do + inspect_source(cop, "add_column_with_default :table, :column, default: true") + + expect(cop.offenses).to be_empty + end + end + + context 'outside of migration' do + it 'registers no offense' do + table = described_class::LARGE_TABLES.sample + inspect_source(cop, "add_column_with_default :#{table}, :column, default: true") + + expect(cop.offenses).to be_empty + end + end +end diff --git a/spec/rubocop/cop/migration/add_column_with_default_spec.rb b/spec/rubocop/cop/migration/reversible_add_column_with_default_spec.rb index 6b9b6b19650..3723d635083 100644 --- a/spec/rubocop/cop/migration/add_column_with_default_spec.rb +++ b/spec/rubocop/cop/migration/reversible_add_column_with_default_spec.rb @@ -3,9 +3,9 @@ require 'spec_helper' require 'rubocop' require 'rubocop/rspec/support' -require_relative '../../../../rubocop/cop/migration/add_column_with_default' +require_relative '../../../../rubocop/cop/migration/reversible_add_column_with_default' -describe RuboCop::Cop::Migration::AddColumnWithDefault do +describe RuboCop::Cop::Migration::ReversibleAddColumnWithDefault do include CopHelper subject(:cop) { described_class.new } diff --git a/spec/serializers/analytics_generic_entity_spec.rb b/spec/serializers/analytics_issue_entity_spec.rb index 68086216ba9..68086216ba9 100644 --- a/spec/serializers/analytics_generic_entity_spec.rb +++ b/spec/serializers/analytics_issue_entity_spec.rb diff --git a/spec/serializers/deployment_entity_spec.rb b/spec/serializers/deployment_entity_spec.rb index 95eca5463eb..69355bcde42 100644 --- a/spec/serializers/deployment_entity_spec.rb +++ b/spec/serializers/deployment_entity_spec.rb @@ -3,25 +3,23 @@ require 'spec_helper' describe DeploymentEntity do let(:user) { create(:user) } let(:request) { double('request') } + let(:deployment) { create(:deployment) } + let(:entity) { described_class.new(deployment, request: request) } + subject { entity.as_json } before do allow(request).to receive(:user).and_return(user) end - let(:entity) do - described_class.new(deployment, request: request) - end - - let(:deployment) { create(:deployment) } - - subject { entity.as_json } - it 'exposes internal deployment id' do expect(subject).to include(:iid) end it 'exposes nested information about branch' do expect(subject[:ref][:name]).to eq 'master' - expect(subject[:ref][:ref_path]).not_to be_empty + end + + it 'exposes creation date' do + expect(subject).to include(:created_at) end end diff --git a/spec/serializers/status_entity_spec.rb b/spec/serializers/status_entity_spec.rb index c94902dbab8..3964b998084 100644 --- a/spec/serializers/status_entity_spec.rb +++ b/spec/serializers/status_entity_spec.rb @@ -18,6 +18,12 @@ describe StatusEntity do it 'contains status details' do expect(subject).to include :text, :icon, :favicon, :label, :group expect(subject).to include :has_details, :details_path + expect(subject[:favicon]).to eq('/assets/ci_favicons/favicon_status_success.ico') + end + + it 'contains a dev namespaced favicon if dev env' do + allow(Rails.env).to receive(:development?) { true } + expect(entity.as_json[:favicon]).to eq('/assets/ci_favicons/dev/favicon_status_success.ico') end end end diff --git a/spec/services/issues/resolve_discussions_spec.rb b/spec/services/issues/resolve_discussions_spec.rb index 4a4929daefc..c3b4c2176ee 100644 --- a/spec/services/issues/resolve_discussions_spec.rb +++ b/spec/services/issues/resolve_discussions_spec.rb @@ -1,15 +1,15 @@ require 'spec_helper.rb' -class DummyService < Issues::BaseService - include ::Issues::ResolveDiscussions +describe Issues::ResolveDiscussions, services: true do + class DummyService < Issues::BaseService + include ::Issues::ResolveDiscussions - def initialize(*args) - super - filter_resolve_discussion_params + def initialize(*args) + super + filter_resolve_discussion_params + end end -end -describe DummyService, services: true do let(:project) { create(:project, :repository) } let(:user) { create(:user) } @@ -23,7 +23,7 @@ describe DummyService, services: true do let(:other_merge_request) { create(:merge_request, source_project: project, source_branch: "other") } describe "#merge_request_for_resolving_discussion" do - let(:service) { described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid) } + let(:service) { DummyService.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid) } it "finds the merge request" do expect(service.merge_request_to_resolve_discussions_of).to eq(merge_request) @@ -43,7 +43,7 @@ describe DummyService, services: true do describe "#discussions_to_resolve" do it "contains a single discussion when matching merge request and discussion are passed" do - service = described_class.new( + service = DummyService.new( project, user, discussion_to_resolve: discussion.id, @@ -61,7 +61,7 @@ describe DummyService, services: true do noteable: merge_request, project: merge_request.target_project, line_number: 15)]) - service = described_class.new( + service = DummyService.new( project, user, merge_request_to_resolve_discussions_of: merge_request.iid @@ -79,7 +79,7 @@ describe DummyService, services: true do project: merge_request.target_project, line_number: 15, )]) - service = described_class.new( + service = DummyService.new( project, user, merge_request_to_resolve_discussions_of: merge_request.iid @@ -92,7 +92,7 @@ describe DummyService, services: true do end it "is empty when a discussion and another merge request are passed" do - service = described_class.new( + service = DummyService.new( project, user, discussion_to_resolve: discussion.id, diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb index be9f9ea2dec..6f9d1208b1d 100644 --- a/spec/services/merge_requests/build_service_spec.rb +++ b/spec/services/merge_requests/build_service_spec.rb @@ -261,6 +261,16 @@ describe MergeRequests::BuildService, services: true do end end + context 'upstream project has disabled merge requests' do + let(:upstream_project) { create(:empty_project, :merge_requests_disabled) } + let(:project) { create(:empty_project, forked_from_project: upstream_project) } + let(:commits) { Commit.decorate([commit_1], project) } + + it 'sets target project correctly' do + expect(merge_request.target_project).to eq(project) + end + end + context 'target_project is set and accessible by current_user' do let(:target_project) { create(:project, :public, :repository)} let(:commits) { Commit.decorate([commit_1], project) } diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb index 290e00ea1ba..4a7d8ab4c6c 100644 --- a/spec/services/merge_requests/get_urls_service_spec.rb +++ b/spec/services/merge_requests/get_urls_service_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" describe MergeRequests::GetUrlsService do let(:project) { create(:project, :public, :repository) } - let(:service) { MergeRequests::GetUrlsService.new(project) } + let(:service) { described_class.new(project) } let(:source_branch) { "my_branch" } let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=#{source_branch}" } let(:show_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/#{merge_request.iid}" } @@ -89,7 +89,7 @@ describe MergeRequests::GetUrlsService do let!(:merge_request) { create(:merge_request, source_project: forked_project, target_project: project, source_branch: source_branch) } let(:changes) { existing_branch_changes } # Source project is now the forked one - let(:service) { MergeRequests::GetUrlsService.new(forked_project) } + let(:service) { described_class.new(forked_project) } before do allow(forked_project).to receive(:empty_repo?).and_return(false) diff --git a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb index 35804d41b46..935f4710851 100644 --- a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb +++ b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe MergeRequests::MergeRequestDiffCacheService do - let(:subject) { MergeRequests::MergeRequestDiffCacheService.new } + let(:subject) { described_class.new } describe '#execute' do it 'retrieves the diff files to cache the highlighted result' do diff --git a/spec/services/merge_requests/resolve_service_spec.rb b/spec/services/merge_requests/resolve_service_spec.rb index eaf7785e549..3afd6b92900 100644 --- a/spec/services/merge_requests/resolve_service_spec.rb +++ b/spec/services/merge_requests/resolve_service_spec.rb @@ -50,7 +50,7 @@ describe MergeRequests::ResolveService do context 'when the source and target project are the same' do before do - MergeRequests::ResolveService.new(project, user, params).execute(merge_request) + described_class.new(project, user, params).execute(merge_request) end it 'creates a commit with the message' do @@ -75,7 +75,7 @@ describe MergeRequests::ResolveService do end before do - MergeRequests::ResolveService.new(fork_project, user, params).execute(merge_request_from_fork) + described_class.new(fork_project, user, params).execute(merge_request_from_fork) end it 'creates a commit with the message' do @@ -115,7 +115,7 @@ describe MergeRequests::ResolveService do end before do - MergeRequests::ResolveService.new(project, user, params).execute(merge_request) + described_class.new(project, user, params).execute(merge_request) end it 'creates a commit with the message' do @@ -154,7 +154,7 @@ describe MergeRequests::ResolveService do } end - let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) } + let(:service) { described_class.new(project, user, invalid_params) } it 'raises a MissingResolution error' do expect { service.execute(merge_request) }. @@ -180,7 +180,7 @@ describe MergeRequests::ResolveService do } end - let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) } + let(:service) { described_class.new(project, user, invalid_params) } it 'raises a MissingResolution error' do expect { service.execute(merge_request) }. @@ -202,7 +202,7 @@ describe MergeRequests::ResolveService do } end - let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) } + let(:service) { described_class.new(project, user, invalid_params) } it 'raises a MissingFiles error' do expect { service.execute(merge_request) }. diff --git a/spec/services/merge_requests/resolved_discussion_notification_service.rb b/spec/services/merge_requests/resolved_discussion_notification_service_spec.rb index 7ddd812e513..7ddd812e513 100644 --- a/spec/services/merge_requests/resolved_discussion_notification_service.rb +++ b/spec/services/merge_requests/resolved_discussion_notification_service_spec.rb diff --git a/spec/services/projects/enable_deploy_key_service_spec.rb b/spec/services/projects/enable_deploy_key_service_spec.rb index a37510cf159..78626fbad4b 100644 --- a/spec/services/projects/enable_deploy_key_service_spec.rb +++ b/spec/services/projects/enable_deploy_key_service_spec.rb @@ -21,6 +21,16 @@ describe Projects::EnableDeployKeyService, services: true do end end + context 'add the same key twice' do + before do + project.deploy_keys << deploy_key + end + + it 'returns existing key' do + expect(service.execute).to eq(deploy_key) + end + end + def service Projects::EnableDeployKeyService.new(project, user, params) end diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb index eaf63457b32..fff12beed71 100644 --- a/spec/services/projects/housekeeping_service_spec.rb +++ b/spec/services/projects/housekeeping_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Projects::HousekeepingService do - subject { Projects::HousekeepingService.new(project) } + subject { described_class.new(project) } let(:project) { create(:project, :repository) } before do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e67ad8f3455..e2d5928e5b2 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -13,8 +13,9 @@ rspec_profiling_is_configured = ENV['RSPEC_PROFILING_POSTGRES_URL'] || ENV['RSPEC_PROFILING'] branch_can_be_profiled = - ENV['CI_COMMIT_REF_NAME'] == 'master' || - ENV['CI_COMMIT_REF_NAME'] =~ /rspec-profile/ + ENV['GITLAB_DATABASE'] == 'postgresql' && + (ENV['CI_COMMIT_REF_NAME'] == 'master' || + ENV['CI_COMMIT_REF_NAME'] =~ /rspec-profile/) if rspec_profiling_is_configured && (!ENV.key?('CI') || branch_can_be_profiled) require 'rspec_profiling/rspec' diff --git a/spec/support/fake_migration_classes.rb b/spec/support/fake_migration_classes.rb new file mode 100644 index 00000000000..3de0460c3ca --- /dev/null +++ b/spec/support/fake_migration_classes.rb @@ -0,0 +1,3 @@ +class FakeRenameReservedPathMigrationV1 < ActiveRecord::Migration + include Gitlab::Database::RenameReservedPathsMigration::V1 +end diff --git a/spec/support/import_export/import_export.yml b/spec/support/import_export/import_export.yml index 17136dee000..734d6838f4d 100644 --- a/spec/support/import_export/import_export.yml +++ b/spec/support/import_export/import_export.yml @@ -11,9 +11,6 @@ project_tree: - :user included_attributes: - project: - - :name - - :path merge_requests: - :id user: @@ -21,4 +18,7 @@ included_attributes: excluded_attributes: merge_requests: - - :iid
\ No newline at end of file + - :iid + project: + - :id + - :created_at
\ No newline at end of file diff --git a/spec/support/milestone_tabs_examples.rb b/spec/support/milestone_tabs_examples.rb new file mode 100644 index 00000000000..c69f8e11008 --- /dev/null +++ b/spec/support/milestone_tabs_examples.rb @@ -0,0 +1,68 @@ +shared_examples 'milestone tabs' do + def go(path, extra_params = {}) + params = if milestone.is_a?(GlobalMilestone) + { group_id: group.id, id: milestone.safe_title, title: milestone.title } + else + { namespace_id: project.namespace.to_param, project_id: project, id: milestone.iid } + end + + get path, params.merge(extra_params) + end + + describe '#merge_requests' do + context 'as html' do + before { go(:merge_requests, format: 'html') } + + it 'redirects to milestone#show' do + expect(response).to redirect_to(milestone_path) + end + end + + context 'as json' do + before { go(:merge_requests, format: 'json') } + + it 'renders the merge requests tab template to a string' do + expect(response).to render_template('shared/milestones/_merge_requests_tab') + expect(json_response).to have_key('html') + end + end + end + + describe '#participants' do + context 'as html' do + before { go(:participants, format: 'html') } + + it 'redirects to milestone#show' do + expect(response).to redirect_to(milestone_path) + end + end + + context 'as json' do + before { go(:participants, format: 'json') } + + it 'renders the participants tab template to a string' do + expect(response).to render_template('shared/milestones/_participants_tab') + expect(json_response).to have_key('html') + end + end + end + + describe '#labels' do + context 'as html' do + before { go(:labels, format: 'html') } + + it 'redirects to milestone#show' do + expect(response).to redirect_to(milestone_path) + end + end + + context 'as json' do + before { go(:labels, format: 'json') } + + it 'renders the labels tab template to a string' do + expect(response).to render_template('shared/milestones/_labels_tab') + expect(json_response).to have_key('html') + end + end + end +end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 5c8ee8d62f5..0b3c6169c9b 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -39,7 +39,8 @@ module TestEnv 'wip' => 'b9238ee', 'csv' => '3dd0896', 'v1.1.0' => 'b83d6e3', - 'add-ipython-files' => '6d85bb69' + 'add-ipython-files' => '6d85bb69', + 'add-pdf-file' => 'e774ebd3' }.freeze # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily diff --git a/spec/support/wait_for_requests.rb b/spec/support/wait_for_requests.rb index 0bfa7f72ff8..73da23391ee 100644 --- a/spec/support/wait_for_requests.rb +++ b/spec/support/wait_for_requests.rb @@ -1,11 +1,15 @@ +require_relative './wait_for_ajax' + module WaitForRequests extend self + include WaitForAjax # This is inspired by http://www.salsify.com/blog/engineering/tearing-capybara-ajax-tests def wait_for_requests_complete Gitlab::Testing::RequestBlockerMiddleware.block_requests! wait_for('pending AJAX requests complete') do - Gitlab::Testing::RequestBlockerMiddleware.num_active_requests.zero? + Gitlab::Testing::RequestBlockerMiddleware.num_active_requests.zero? && + finished_all_ajax_requests? end ensure Gitlab::Testing::RequestBlockerMiddleware.allow_requests! diff --git a/spec/tasks/config_lint_spec.rb b/spec/tasks/config_lint_spec.rb index c32f9a740b7..ed6c5b09663 100644 --- a/spec/tasks/config_lint_spec.rb +++ b/spec/tasks/config_lint_spec.rb @@ -5,11 +5,11 @@ describe ConfigLint do let(:files){ ['lib/support/fake.sh'] } it 'errors out if any bash scripts have errors' do - expect { ConfigLint.run(files){ system('exit 1') } }.to raise_error(SystemExit) + expect { described_class.run(files){ system('exit 1') } }.to raise_error(SystemExit) end it 'passes if all scripts are fine' do - expect { ConfigLint.run(files){ system('exit 0') } }.not_to raise_error + expect { described_class.run(files){ system('exit 0') } }.not_to raise_error end end diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index 0a4a6ed8145..df2f2ce95e6 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -230,11 +230,13 @@ describe 'gitlab:app namespace rake task' do before do FileUtils.mkdir('tmp/tests/default_storage') FileUtils.mkdir('tmp/tests/custom_storage') + gitaly_address = Gitlab.config.repositories.storages.default.gitaly_address storages = { - 'default' => { 'path' => Settings.absolute('tmp/tests/default_storage') }, - 'custom' => { 'path' => Settings.absolute('tmp/tests/custom_storage') } + 'default' => { 'path' => Settings.absolute('tmp/tests/default_storage'), 'gitaly_address' => gitaly_address }, + 'custom' => { 'path' => Settings.absolute('tmp/tests/custom_storage'), 'gitaly_address' => gitaly_address } } allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) + Gitlab::GitalyClient.configure_channels # Create the projects now, after mocking the settings but before doing the backup project_a diff --git a/spec/validators/dynamic_path_validator_spec.rb b/spec/validators/dynamic_path_validator_spec.rb new file mode 100644 index 00000000000..b114bfc1bca --- /dev/null +++ b/spec/validators/dynamic_path_validator_spec.rb @@ -0,0 +1,266 @@ +require 'spec_helper' + +describe DynamicPathValidator do + let(:validator) { described_class.new(attributes: [:path]) } + + # Pass in a full path to remove the format segment: + # `/ci/lint(.:format)` -> `/ci/lint` + def without_format(path) + path.split('(', 2)[0] + end + + # Pass in a full path and get the last segment before a wildcard + # That's not a parameter + # `/*namespace_id/:project_id/builds/artifacts/*ref_name_and_path` + # -> 'builds/artifacts' + def path_before_wildcard(path) + path = path.gsub(STARTING_WITH_NAMESPACE, "") + path_segments = path.split('/').reject(&:empty?) + wildcard_index = path_segments.index { |segment| parameter?(segment) } + + segments_before_wildcard = path_segments[0..wildcard_index - 1] + + segments_before_wildcard.join('/') + end + + def parameter?(segment) + segment =~ /[*:]/ + end + + # If the path is reserved. Then no conflicting paths can# be created for any + # route using this reserved word. + # + # Both `builds/artifacts` & `build` are covered by reserving the word + # `build` + def wildcards_include?(path) + described_class::WILDCARD_ROUTES.include?(path) || + described_class::WILDCARD_ROUTES.include?(path.split('/').first) + end + + def failure_message(missing_words, constant_name, migration_helper) + missing_words = Array(missing_words) + <<-MSG + Found new routes that could cause conflicts with existing namespaced routes + for groups or projects. + + Add <#{missing_words.join(', ')}> to `DynamicPathValidator::#{constant_name} + to make sure no projects or namespaces can be created with those paths. + + To rename any existing records with those paths you can use the + `Gitlab::Database::RenameReservedpathsMigration::<VERSION>.#{migration_helper}` + migration helper. + + Make sure to make a note of the renamed records in the release blog post. + + MSG + end + + let(:all_routes) do + Rails.application.routes.routes.routes. + map { |r| r.path.spec.to_s } + end + + let(:routes_without_format) { all_routes.map { |path| without_format(path) } } + + # Routes not starting with `/:` or `/*` + # all routes not starting with a param + let(:routes_not_starting_in_wildcard) { routes_without_format.select { |p| p !~ %r{^/[:*]} } } + + let(:top_level_words) do + routes_not_starting_in_wildcard.map do |route| + route.split('/')[1] + end.compact.uniq + end + + # All routes that start with a namespaced path, that have 1 or more + # path-segments before having another wildcard parameter. + # - Starting with paths: + # - `/*namespace_id/:project_id/` + # - `/*namespace_id/:id/` + # - Followed by one or more path-parts not starting with `:` or `*` + # - Followed by a path-part that includes a wildcard parameter `*` + # At the time of writing these routes match: http://rubular.com/r/Rv2pDE5Dvw + STARTING_WITH_NAMESPACE = %r{^/\*namespace_id/:(project_)?id} + NON_PARAM_PARTS = %r{[^:*][a-z\-_/]*} + ANY_OTHER_PATH_PART = %r{[a-z\-_/:]*} + WILDCARD_SEGMENT = %r{\*} + let(:namespaced_wildcard_routes) do + routes_without_format.select do |p| + p =~ %r{#{STARTING_WITH_NAMESPACE}/#{NON_PARAM_PARTS}/#{ANY_OTHER_PATH_PART}#{WILDCARD_SEGMENT}} + end + end + + # This will return all paths that are used in a namespaced route + # before another wildcard path: + # + # /*namespace_id/:project_id/builds/artifacts/*ref_name_and_path + # /*namespace_id/:project_id/info/lfs/objects/*oid + # /*namespace_id/:project_id/commits/*id + # /*namespace_id/:project_id/builds/:build_id/artifacts/file/*path + # -> ['builds/artifacts', 'info/lfs/objects', 'commits', 'artifacts/file'] + let(:all_wildcard_paths) do + namespaced_wildcard_routes.map do |route| + path_before_wildcard(route) + end.uniq + end + + STARTING_WITH_GROUP = %r{^/groups/\*(group_)?id/} + let(:group_routes) do + routes_without_format.select do |path| + path =~ STARTING_WITH_GROUP + end + end + + let(:paths_after_group_id) do + group_routes.map do |route| + route.gsub(STARTING_WITH_GROUP, '').split('/').first + end.uniq + end + + describe 'TOP_LEVEL_ROUTES' do + it 'includes all the top level namespaces' do + failure_block = lambda do + missing_words = top_level_words - described_class::TOP_LEVEL_ROUTES + failure_message(missing_words, 'TOP_LEVEL_ROUTES', 'rename_root_paths') + end + + expect(described_class::TOP_LEVEL_ROUTES) + .to include(*top_level_words), failure_block + end + end + + describe 'GROUP_ROUTES' do + it "don't contain a second wildcard" do + failure_block = lambda do + missing_words = paths_after_group_id - described_class::GROUP_ROUTES + failure_message(missing_words, 'GROUP_ROUTES', 'rename_child_paths') + end + + expect(described_class::GROUP_ROUTES) + .to include(*paths_after_group_id), failure_block + end + end + + describe 'WILDCARD_ROUTES' do + it 'includes all paths that can be used after a namespace/project path' do + aggregate_failures do + all_wildcard_paths.each do |path| + expect(wildcards_include?(path)) + .to be(true), failure_message(path, 'WILDCARD_ROUTES', 'rename_wildcard_paths') + end + end + end + end + + describe '.without_reserved_wildcard_paths_regex' do + subject { described_class.without_reserved_wildcard_paths_regex } + + it 'rejects paths starting with a reserved top level' do + expect(subject).not_to match('dashboard/hello/world') + expect(subject).not_to match('dashboard') + end + + it 'matches valid paths with a toplevel word in a different place' do + expect(subject).to match('parent/dashboard/project-path') + end + + it 'rejects paths containing a wildcard reserved word' do + expect(subject).not_to match('hello/edit') + expect(subject).not_to match('hello/edit/in-the-middle') + expect(subject).not_to match('foo/bar1/refs/master/logs_tree') + end + + it 'matches valid paths' do + expect(subject).to match('parent/child/project-path') + end + end + + describe '.regex_excluding_child_paths' do + let(:subject) { described_class.without_reserved_child_paths_regex } + + it 'rejects paths containing a child reserved word' do + expect(subject).not_to match('hello/group_members') + expect(subject).not_to match('hello/activity/in-the-middle') + expect(subject).not_to match('foo/bar1/refs/master/logs_tree') + end + + it 'allows a child path on the top level' do + expect(subject).to match('activity/foo') + expect(subject).to match('avatar') + end + end + + describe ".valid?" do + it 'is not case sensitive' do + expect(described_class.valid?("Users")).to be_falsey + end + + it "isn't valid when the top level is reserved" do + test_path = 'u/should-be-a/reserved-word' + + expect(described_class.valid?(test_path)).to be_falsey + end + + it "isn't valid if any of the path segments is reserved" do + test_path = 'the-wildcard/wikis/is-not-allowed' + + expect(described_class.valid?(test_path)).to be_falsey + end + + it "is valid if the path doesn't contain reserved words" do + test_path = 'there-are/no-wildcards/in-this-path' + + expect(described_class.valid?(test_path)).to be_truthy + end + + it 'allows allows a child path on the last spot' do + test_path = 'there/can-be-a/project-called/labels' + + expect(described_class.valid?(test_path)).to be_truthy + end + + it 'rejects a child path somewhere else' do + test_path = 'there/can-be-no/labels/group' + + expect(described_class.valid?(test_path)).to be_falsey + end + + it 'rejects paths that are in an incorrect format' do + test_path = 'incorrect/format.git' + + expect(described_class.valid?(test_path)).to be_falsey + end + end + + describe '#path_reserved_for_record?' do + it 'reserves a sub-group named activity' do + group = build(:group, :nested, path: 'activity') + + expect(validator.path_reserved_for_record?(group, 'activity')).to be_truthy + end + + it "doesn't reserve a project called activity" do + project = build(:project, path: 'activity') + + expect(validator.path_reserved_for_record?(project, 'activity')).to be_falsey + end + end + + describe '#validates_each' do + it 'adds a message when the path is not in the correct format' do + group = build(:group) + + validator.validate_each(group, :path, "Path with spaces, and comma's!") + + expect(group.errors[:path]).to include(Gitlab::Regex.namespace_regex_message) + end + + it 'adds a message when the path is not in the correct format' do + group = build(:group, path: 'users') + + validator.validate_each(group, :path, 'users') + + expect(group.errors[:path]).to include('users is a reserved name') + end + end +end diff --git a/spec/views/projects/blob/_viewer.html.haml_spec.rb b/spec/views/projects/blob/_viewer.html.haml_spec.rb index a4915264abe..501f90c5f9a 100644 --- a/spec/views/projects/blob/_viewer.html.haml_spec.rb +++ b/spec/views/projects/blob/_viewer.html.haml_spec.rb @@ -21,6 +21,7 @@ describe 'projects/blob/_viewer.html.haml', :view do before do assign(:project, project) + assign(:blob, blob) assign(:id, File.join('master', blob.path)) controller.params[:controller] = 'projects/blob' diff --git a/spec/views/projects/commit/show.html.haml_spec.rb b/spec/views/projects/commit/show.html.haml_spec.rb new file mode 100644 index 00000000000..122075cc10e --- /dev/null +++ b/spec/views/projects/commit/show.html.haml_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe 'projects/commit/show.html.haml', :view do + let(:project) { create(:project, :repository) } + + before do + assign(:project, project) + assign(:repository, project.repository) + assign(:commit, project.commit) + assign(:noteable, project.commit) + assign(:notes, []) + assign(:diffs, project.commit.diffs) + + allow(view).to receive(:current_user).and_return(nil) + allow(view).to receive(:can?).and_return(false) + allow(view).to receive(:can_collaborate_with_project?).and_return(false) + allow(view).to receive(:current_ref).and_return(project.repository.root_ref) + allow(view).to receive(:diff_btn).and_return('') + end + + context 'inline diff view' do + before do + allow(view).to receive(:diff_view).and_return(:inline) + + render + end + + it 'keeps container-limited' do + expect(rendered).not_to have_selector('.limit-container-width') + end + end + + context 'parallel diff view' do + before do + allow(view).to receive(:diff_view).and_return(:parallel) + + render + end + + it 'spans full width' do + expect(rendered).to have_selector('.limit-container-width') + end + end +end diff --git a/spec/workers/delete_user_worker_spec.rb b/spec/workers/delete_user_worker_spec.rb index 0765573408c..5912dd76262 100644 --- a/spec/workers/delete_user_worker_spec.rb +++ b/spec/workers/delete_user_worker_spec.rb @@ -8,13 +8,13 @@ describe DeleteUserWorker do expect_any_instance_of(Users::DestroyService).to receive(:execute). with(user, {}) - DeleteUserWorker.new.perform(current_user.id, user.id) + described_class.new.perform(current_user.id, user.id) end it "uses symbolized keys" do expect_any_instance_of(Users::DestroyService).to receive(:execute). with(user, test: "test") - DeleteUserWorker.new.perform(current_user.id, user.id, "test" => "test") + described_class.new.perform(current_user.id, user.id, "test" => "test") end end diff --git a/spec/workers/emails_on_push_worker_spec.rb b/spec/workers/emails_on_push_worker_spec.rb index 8cf2b888f9a..a0ed85cc0b3 100644 --- a/spec/workers/emails_on_push_worker_spec.rb +++ b/spec/workers/emails_on_push_worker_spec.rb @@ -12,7 +12,7 @@ describe EmailsOnPushWorker do let(:perform) { subject.perform(project.id, recipients, data.stringify_keys) } let(:email) { ActionMailer::Base.deliveries.last } - subject { EmailsOnPushWorker.new } + subject { described_class.new } describe "#perform" do context "when push is a new branch" do diff --git a/spec/workers/expire_build_instance_artifacts_worker_spec.rb b/spec/workers/expire_build_instance_artifacts_worker_spec.rb index d202b3de77e..1d8da68883b 100644 --- a/spec/workers/expire_build_instance_artifacts_worker_spec.rb +++ b/spec/workers/expire_build_instance_artifacts_worker_spec.rb @@ -34,12 +34,14 @@ describe ExpireBuildInstanceArtifactsWorker do context 'when associated project was removed' do let(:build) do create(:ci_build, :artifacts, artifacts_expiry) do |build| - build.project.delete + build.project.pending_delete = true end end it 'does not remove artifacts' do - expect(build.reload.artifacts_file.exists?).to be_truthy + expect do + build.reload.artifacts_file + end.not_to raise_error end end end diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb index 029f35512e0..7a590f64e3c 100644 --- a/spec/workers/git_garbage_collect_worker_spec.rb +++ b/spec/workers/git_garbage_collect_worker_spec.rb @@ -6,7 +6,7 @@ describe GitGarbageCollectWorker do let(:project) { create(:project, :repository) } let(:shell) { Gitlab::Shell.new } - subject { GitGarbageCollectWorker.new } + subject { described_class.new } describe "#perform" do it "flushes ref caches when the task is 'gc'" do diff --git a/spec/workers/gitlab_usage_ping_worker_spec.rb b/spec/workers/gitlab_usage_ping_worker_spec.rb index b6c080f36f4..26241044533 100644 --- a/spec/workers/gitlab_usage_ping_worker_spec.rb +++ b/spec/workers/gitlab_usage_ping_worker_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe GitlabUsagePingWorker do - subject { GitlabUsagePingWorker.new } + subject { described_class.new } it "sends POST request" do stub_application_setting(usage_ping_enabled: true) diff --git a/spec/workers/group_destroy_worker_spec.rb b/spec/workers/group_destroy_worker_spec.rb index 1ff5a3b9034..c78efc67076 100644 --- a/spec/workers/group_destroy_worker_spec.rb +++ b/spec/workers/group_destroy_worker_spec.rb @@ -5,7 +5,7 @@ describe GroupDestroyWorker do let(:user) { create(:admin) } let!(:project) { create(:empty_project, namespace: group) } - subject { GroupDestroyWorker.new } + subject { described_class.new } describe "#perform" do it "deletes the project" do diff --git a/spec/workers/merge_worker_spec.rb b/spec/workers/merge_worker_spec.rb index b5e1fdb8ded..303193bab9b 100644 --- a/spec/workers/merge_worker_spec.rb +++ b/spec/workers/merge_worker_spec.rb @@ -15,7 +15,7 @@ describe MergeWorker do it 'clears cache of source repo after removing source branch' do expect(source_project.repository.branch_names).to include('markdown') - MergeWorker.new.perform( + described_class.new.perform( merge_request.id, merge_request.author_id, commit_message: 'wow such merge', should_remove_source_branch: true) diff --git a/spec/workers/pipeline_proccess_worker_spec.rb b/spec/workers/pipeline_process_worker_spec.rb index 86e9d7f6684..86e9d7f6684 100644 --- a/spec/workers/pipeline_proccess_worker_spec.rb +++ b/spec/workers/pipeline_process_worker_spec.rb diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index a2a559a2369..5ab3c4a0e34 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -10,7 +10,7 @@ describe PostReceive do context "as a resque worker" do it "reponds to #perform" do - expect(PostReceive.new).to respond_to(:perform) + expect(described_class.new).to respond_to(:perform) end end @@ -25,7 +25,7 @@ describe PostReceive do it "calls GitTagPushService" do expect_any_instance_of(GitPushService).to receive(:execute).and_return(true) expect_any_instance_of(GitTagPushService).not_to receive(:execute) - PostReceive.new.perform(pwd(project), key_id, base64_changes) + described_class.new.perform(pwd(project), key_id, base64_changes) end end @@ -35,7 +35,7 @@ describe PostReceive do it "calls GitTagPushService" do expect_any_instance_of(GitPushService).not_to receive(:execute) expect_any_instance_of(GitTagPushService).to receive(:execute).and_return(true) - PostReceive.new.perform(pwd(project), key_id, base64_changes) + described_class.new.perform(pwd(project), key_id, base64_changes) end end @@ -45,12 +45,12 @@ describe PostReceive do it "does not call any of the services" do expect_any_instance_of(GitPushService).not_to receive(:execute) expect_any_instance_of(GitTagPushService).not_to receive(:execute) - PostReceive.new.perform(pwd(project), key_id, base64_changes) + described_class.new.perform(pwd(project), key_id, base64_changes) end end context "gitlab-ci.yml" do - subject { PostReceive.new.perform(pwd(project), key_id, base64_changes) } + subject { described_class.new.perform(pwd(project), key_id, base64_changes) } context "creates a Ci::Pipeline for every change" do before do @@ -75,7 +75,7 @@ describe PostReceive do context "webhook" do it "fetches the correct project" do expect(Project).to receive(:find_by_full_path).with(project.path_with_namespace).and_return(project) - PostReceive.new.perform(pwd(project), key_id, base64_changes) + described_class.new.perform(pwd(project), key_id, base64_changes) end it "does not run if the author is not in the project" do @@ -85,7 +85,7 @@ describe PostReceive do expect(project).not_to receive(:execute_hooks) - expect(PostReceive.new.perform(pwd(project), key_id, base64_changes)).to be_falsey + expect(described_class.new.perform(pwd(project), key_id, base64_changes)).to be_falsey end it "asks the project to trigger all hooks" do @@ -93,14 +93,14 @@ describe PostReceive do expect(project).to receive(:execute_hooks).twice expect(project).to receive(:execute_services).twice - PostReceive.new.perform(pwd(project), key_id, base64_changes) + described_class.new.perform(pwd(project), key_id, base64_changes) end it "enqueues a UpdateMergeRequestsWorker job" do allow(Project).to receive(:find_by_full_path).and_return(project) expect(UpdateMergeRequestsWorker).to receive(:perform_async).with(project.id, project.owner.id, any_args) - PostReceive.new.perform(pwd(project), key_id, base64_changes) + described_class.new.perform(pwd(project), key_id, base64_changes) end end diff --git a/spec/workers/project_destroy_worker_spec.rb b/spec/workers/project_destroy_worker_spec.rb index 0ab42f99510..3d135f40c1f 100644 --- a/spec/workers/project_destroy_worker_spec.rb +++ b/spec/workers/project_destroy_worker_spec.rb @@ -4,7 +4,7 @@ describe ProjectDestroyWorker do let(:project) { create(:project, :repository) } let(:path) { project.repository.path_to_repo } - subject { ProjectDestroyWorker.new } + subject { described_class.new } describe "#perform" do it "deletes the project" do diff --git a/spec/workers/remove_expired_members_worker_spec.rb b/spec/workers/remove_expired_members_worker_spec.rb index 402aa1e714e..058fdf4c009 100644 --- a/spec/workers/remove_expired_members_worker_spec.rb +++ b/spec/workers/remove_expired_members_worker_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe RemoveExpiredMembersWorker do - let(:worker) { RemoveExpiredMembersWorker.new } + let(:worker) { described_class.new } describe '#perform' do context 'project members' do diff --git a/spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb b/spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb index 6d42946de38..1c183ce54f4 100644 --- a/spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb +++ b/spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe RemoveUnreferencedLfsObjectsWorker do - let(:worker) { RemoveUnreferencedLfsObjectsWorker.new } + let(:worker) { described_class.new } describe '#perform' do let!(:unreferenced_lfs_object1) { create(:lfs_object, oid: '1') } diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb index 7d6a2db2972..5e1cb74c7fc 100644 --- a/spec/workers/repository_fork_worker_spec.rb +++ b/spec/workers/repository_fork_worker_spec.rb @@ -5,7 +5,7 @@ describe RepositoryForkWorker do let(:fork_project) { create(:project, :repository, forked_from_project: project) } let(:shell) { Gitlab::Shell.new } - subject { RepositoryForkWorker.new } + subject { described_class.new } before do allow(subject).to receive(:gitlab_shell).and_return(shell) diff --git a/yarn.lock b/yarn.lock index 8f38fb4a9a4..fdef0665d15 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3638,7 +3638,7 @@ mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.13, mime-types@~2.1.7: dependencies: mime-db "~1.26.0" -mime@1.3.4, mime@^1.3.4: +mime@1.3.4, mime@1.3.x, mime@^1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" @@ -3710,6 +3710,10 @@ nested-error-stacks@^1.0.0: dependencies: inherits "~2.0.1" +node-ensure@^0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/node-ensure/-/node-ensure-0.0.0.tgz#ecae764150de99861ec5c810fd5d096b183932a7" + node-libs-browser@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-1.1.1.tgz#2a38243abedd7dffcd07a97c9aca5668975a6fea" @@ -4102,6 +4106,13 @@ pbkdf2@^3.0.3: dependencies: create-hmac "^1.1.2" +pdfjs-dist@^1.8.252: + version "1.8.252" + resolved "https://registry.yarnpkg.com/pdfjs-dist/-/pdfjs-dist-1.8.252.tgz#2477245695341f7fe096824dacf327bc324c0f52" + dependencies: + node-ensure "^0.0.0" + worker-loader "^0.8.0" + pend@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" @@ -5538,6 +5549,13 @@ update-notifier@0.5.0: semver-diff "^2.0.0" string-length "^1.0.0" +url-loader@^0.5.8: + version "0.5.8" + resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-0.5.8.tgz#b9183b1801e0f847718673673040bc9dc1c715c5" + dependencies: + loader-utils "^1.0.2" + mime "1.3.x" + url-parse@1.0.x: version "1.0.5" resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.0.5.tgz#0854860422afdcfefeb6c965c662d4800169927b" @@ -5821,6 +5839,12 @@ wordwrap@~0.0.2: version "0.0.3" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" +worker-loader@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-0.8.0.tgz#13582960dcd7d700dc829d3fd252a7561696167e" + dependencies: + loader-utils "^1.0.2" + wrap-ansi@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" |